Skip to content

feat(terminal): native theming and chrome-free pane rebuild - #1965

Merged
2witstudios merged 5 commits into
masterfrom
pu/terminal-theming-pane-chrome
Jul 9, 2026
Merged

2witstudios merged 5 commits into
masterfrom
pu/terminal-theming-pane-chrome

Conversation

@2witstudios

@2witstudios 2witstudios commented Jul 9, 2026 •

Copy link
Copy Markdown
Owner

Summary

Round 1 ("theming foundation") of the Terminal — Command Center UI epic. Fixes the Terminal feature looking broken/unstyled inside PageSpace — xterm.js was constructed with no theme option, falling back to its own green-tinted default palette, and TerminalPanes.tsx hardcoded bg-black/ring-emerald-500/raw red-green status colors instead of design tokens.

Fully decoupled from the Machine-page routing rebuild epic (separate sibling node) — ships standalone against the current right-sidebar-hosted Terminal feature.

  • Shared color-resolution util: extracted useMonacoTheme's CSS-var → hex pipeline (hex/rgb parse, CSS.supports + canvas fallback, hidden-span computed-style fallback, withAlpha) into apps/web/src/lib/theme/css-color-resolution.ts, plus a shared THEME_FALLBACK light/dark swatch constant so Monaco and xterm's themes can't drift apart on a future rebrand. useMonacoTheme now imports from it — no behavior change, existing Monaco theming unaffected. The canvas-based parse path also now correctly falls through to the computed-style fallback instead of reporting transparent black when CSS.supports() accepts a color the canvas fillStyle parser silently rejects.
  • useXtermTheme: reactive to next-themes' resolvedTheme, resolves --background/--foreground/--primary via the shared util into xterm's ITheme shape, with a curated muted/low-contrast ANSI 16-color palette per light/dark mode (no ANSI CSS tokens exist in the codebase — these are new design surface).
  • XtermTerminal.tsx: wires theme, fontFamily (from --font-mono), fontSize, cursorStyle, letterSpacing, lineHeight into the Terminal constructor. Theme updates live via terminal.options.theme = theme in an effect keyed on the returned theme object — the [socket, sessionId]-keyed PTY connection effect is untouched.
  • TerminalPanes.tsx chrome rebuild: dropped the per-pane header entirely (session identity is sidebar-only now, matching PurePoint's real chrome-free design, verified against its actual SwiftUI source) — kept a top 2px --primary accent bar for focus and hover-revealed Split Right / Split Down (new) / Close controls using --card/--border/--muted-foreground tokens. The controls toolbar is visible on group-hover/pane (hovering anywhere in the pane) and, scoped tightly to just the controls themselves via plain focus-within (not the whole pane — an earlier version of this fix used group-focus-within/pane, which also fired on xterm's own hidden input during ordinary typing and pinned the overlay visible the whole time; caught and fixed via a second self-review pass), so keyboard users tabbing through never land on an invisible control. Connecting/error states use --destructive/--primary/muted-foreground instead of hardcoded red-400/green-400.
    • Known tradeoff, documented in-code: with 2+ panes open, the Navigator sidebar doesn't yet indicate which open terminal is showing in which visible pane. That's Navigator/sidebar work and out of scope for this theming-foundation round.
  • Real vertical split axis: workspace-reducer.ts now models a horizontal row of columns, each an independent vertical stack of panes (columns: { id, panes }[]), via splitRight/splitDown — not a full recursive split tree. Both no-op on an unresolved pane id (consistent with closePane/selectPane). Rendered as nested ResizablePanelGroups (outer horizontal, inner vertical per column) over the existing resizable primitives. Pane/column ids use crypto.randomUUID(), matching the established in-file/client-side convention for ephemeral, never-persisted UI-state ids (this store's pre-existing ensureWorkspace, XtermTerminal.tsx's connection id) — @paralleldrive/cuid2 is this codebase's convention for persisted DB-entity ids, a different concern.
  • Resize-handle fix: chrome-free panes strip every other seam cue, leaving ResizableHandle's resting opacity-0 as the only clue a seam exists — confirmed unusable via live testing. Added a variant="chrome-free" prop that keeps the center line at opacity-60 at rest, full opacity + --primary while dragging, without touching the component's global default used elsewhere in the app. The accent line itself is now sized (not absolute-positioned) so the parent's existing flex-centering handles both orientations correctly — the prior absolute/translate implementation was hardcoded vertical and silently broke for the new orientation="vertical" split-down groups.

Test plan

  • bun run typecheck — green across all 16 workspace packages (including next build)
  • bunx eslint on all touched files — clean
  • Reducer/store tests — 29 tests passing (workspace-reducer.test.ts, useTerminalWorkspaceStore.test.ts), covering split-right creating a column, split-down stacking within a column, closing the last pane in a column removing it, closing the last column no-op'ing like today, and both split transitions no-op'ing on an unresolved pane id
  • Full apps/web hook test suite (518 tests) still green after the useMonacoTheme refactor
  • CI (Unit Tests + Lint & TypeScript Check) green on every push, including this final round
  • Two rounds of proactive high-effort self-review (workflow-backed, multi-agent) — round 1 found and fixed 3 confirmed defects (handle geometry, splitRight no-op consistency, duplicated fallback constants), round 2 (re-review after fixes) caught and fixed a regression in the accessibility fix itself (focus-within scoping too broad, firing on xterm's own input)
  • Codex automated review — 1 accessibility finding (hover controls invisible on keyboard focus) fixed with focus-within, matching the existing pattern in MessageHoverToolbar.tsx
  • CodeRabbit automated review (2 passes) — 2 findings addressed: a canvas-fillStyle edge case fixed, an ID-generation convention finding investigated and determined not applicable (explained inline) since it governs persisted DB ids, not this code's ephemeral client-state ids
  • Manual verification of useXtermTheme reactivity and chrome-free pane behavior in a running browser (not run in this session — no dev server available in this pass)

🤖 Generated with Claude Code

https://claude.ai/code/session_01BxroUuSJVV1wsXUpvhF8bb

@coderabbitai

coderabbitai Bot commented Jul 9, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR introduces a shared CSS color resolution module and a new xterm theme hook consumed by the terminal component, refactors Monaco's fallback palette to reuse that module, and reworks terminal panes into a column-based split layout (splitRight/splitDown) with chrome-free UI styling, along with corresponding reducer, store, and test updates.

Changes

Terminal and Monaco Theming

Layer / File(s) Summary
Shared CSS color resolution
apps/web/src/lib/theme/css-color-resolution.ts
New module exporting THEME_FALLBACK, getCssVar, resolveColor, and withAlpha, resolving hex/rgb/canvas/computed-style color syntaxes to hex with fallback handling.
Xterm theme hook
apps/web/src/hooks/useXtermTheme.ts
New useXtermTheme hook builds fallback and resolved ITheme objects using curated muted ANSI palettes and the shared color helpers, reacting to dark/light mode.
XtermTerminal theming integration
apps/web/src/components/layout/middle-content/page-views/terminal/XtermTerminal.tsx
Applies resolved theme/font to the Xterm instance at creation and on live updates, and clears the terminal ref on teardown.
Monaco fallback palette refactor
apps/web/src/hooks/useMonacoTheme.ts
Replaces local color utilities with shared helpers; fallback palettes now spread from THEME_FALLBACK with selective overrides.

Chrome-free Terminal Panes and Column-based Splitting

Layer / File(s) Summary
Column-based workspace reducer
apps/web/src/stores/terminal-workspace/workspace-reducer.ts, .../__tests__/workspace-reducer.test.ts
WorkspaceState reshaped into columns of panes; adds TerminalColumnState, findPaneLocation, splitRight, splitDown, and updates openTerminal/closePane/selectPane; tests rewritten and extended for the new shape and split/close behaviors.
Workspace store split actions
apps/web/src/stores/terminal-workspace/useTerminalWorkspaceStore.ts, .../__tests__/useTerminalWorkspaceStore.test.ts
Replaces split with splitRight/splitDown store methods generating fresh UUIDs; tests updated with an allPanes helper and new split-based assertions.
Chrome-free panes UI and resizable handle variant
apps/web/src/components/layout/middle-content/page-views/terminal/workspace/TerminalPanes.tsx, apps/web/src/components/ui/resizable.tsx
TerminalPanes/TerminalPane render hover-revealed split-right/split-down overlay controls, updated close button and connecting-overlay styling; ResizableHandle gains an optional variant prop with orientation-aware seam styling.

Estimated code review effort: 4 (Complex) | ~55 minutes

Possibly related PRs

  • 2witstudios/PageSpace#511: Both PRs modify useMonacoTheme.ts's color resolution logic, with this PR building on that integration by adopting the shared css-color-resolution helpers.
  • 2witstudios/PageSpace#523: Both PRs touch the same Monaco dark-mode fallback palette construction in useMonacoTheme.ts.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main terminal theming and chrome-free pane rebuild changes in this PR.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pu/terminal-theming-pane-chrome

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Round 1 of the Terminal Command Center UI epic: makes the Terminal
feature feel native to PageSpace instead of xterm's raw green-tinted
default look, and rebuilds pane chrome to match PurePoint's real
chrome-free design.

- Extract useMonacoTheme's CSS-var-to-hex resolution pipeline into a
  shared apps/web/src/lib/theme/css-color-resolution.ts util (no
  behavior change).
- Add useXtermTheme, reactive to next-themes' resolvedTheme, with a
  curated muted ANSI palette per light/dark mode.
- Wire theme + typography into XtermTerminal; live theme updates via
  terminal.options.theme without remounting the PTY connection.
- Rebuild TerminalPanes chrome-free (no per-pane header, ever):
  top accent-bar focus indicator, hover-revealed Split Right/Split
  Down/Close controls, design tokens instead of hardcoded black/white
  and red/green states.
- Add a real vertical split axis: workspace-reducer now models a
  horizontal row of columns, each an independent vertical stack of
  panes, via splitRight/splitDown. Rendered as nested
  ResizablePanelGroups over the existing resizable primitives.
- Add a chrome-free ResizableHandle variant so the seam stays faintly
  visible at rest (chrome-free panes have no other cue), without
  touching the global default used elsewhere in the app.

Tests: reducer/store tests rewritten for the columns structure
(28 tests, given/should style). typecheck + build green across all
16 workspace packages.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0581006178

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

</span>
<div className="group/pane relative flex h-full flex-col" onClick={onSelect}>
<div className={`absolute inset-x-0 top-0 z-10 h-0.5 ${isActive ? 'bg-primary' : 'bg-transparent'}`} />
<div className="absolute right-1.5 top-1.5 z-10 flex items-center gap-0.5 rounded-md border border-border bg-card/90 p-0.5 opacity-0 shadow-sm backdrop-blur-sm transition-opacity group-hover/pane:opacity-100">

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Make pane controls visible on keyboard focus

When a user navigates the terminal pane controls with the keyboard, this toolbar stays opacity-0 because it only becomes visible on group-hover; the split/close buttons are still focusable since opacity does not remove them from the tab order. That leaves keyboard users tabbing to invisible controls (and touch users can also hit invisible buttons before any hover state appears), so add a focus-within visibility state or otherwise keep the controls out of the interaction flow while hidden.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in the next commit: added group-focus-within/pane:opacity-100 alongside the existing group-hover/pane:opacity-100, matching the pattern already used elsewhere in this codebase (sidebar.tsx, MessageHoverToolbar.tsx, NotificationItem.tsx). Now the toolbar becomes visible as soon as keyboard focus lands on any control inside it, so Tab users never land on an invisible button.

@2witstudios
2witstudios force-pushed the pu/terminal-theming-pane-chrome branch from 0581006 to 5cd66e5 Compare July 9, 2026 19:03
…geometry

Proactive high-effort self-review (no external reviewer available yet —
CodeRabbit hit its rate limit) surfaced 3 confirmed defects, all fixed:

- resizable.tsx: the handle's accent-line div was hardcoded to vertical
  geometry (absolute + translateX centering) regardless of orientation.
  This PR's splitDown is the first orientation="vertical" panel group in
  the app, and the new chrome-free variant makes the line permanently
  visible — so the bug, latent before, now renders a floating vertical
  dash instead of a horizontal seam between stacked panes. Fixed by
  sizing the line (h-full w-px vs h-px w-full via an
  aria-orientation-aware variant) and letting the parent's existing flex
  centering position it, instead of fighting it with absolute/translate.

- workspace-reducer.ts: splitRight silently appended an orphan column
  when fromPaneId didn't resolve, unlike every sibling transition
  (splitDown/closePane/selectPane), which no-op on an unresolved pane id.
  Now consistent; added a matching no-op test.

- useXtermTheme.ts / useMonacoTheme.ts: the light/dark fallback swatches
  (background/foreground/primary hex) were duplicated verbatim between
  both hooks. Extracted to THEME_FALLBACK in css-color-resolution.ts so
  Monaco and xterm can't silently drift apart on a future rebrand.

Also documented (not reverted) the header-removal tradeoff flagged by
the review: TerminalPanes drops the per-pane header per explicit,
PurePoint-verified spec ("never shown, in any state"), but the Navigator
sidebar doesn't yet show which open terminal maps to which visible pane
with 2+ panes open. That's Navigator/sidebar work, out of scope for this
theming-foundation round — added a comment recording it as a known,
intentional gap rather than an oversight.

typecheck/build green across all 16 workspace packages; 518 apps/web
tests passing (29 in terminal-workspace, +1 new no-op case).
Addresses Codex review feedback on PR #1965: the Split Right/Split
Down/Close toolbar was opacity-0 until group-hover, but the buttons
stayed in the tab order — keyboard users tabbing through would land on
invisible controls with no visual focus indicator.

Add group-focus-within/pane:opacity-100 alongside the existing
group-hover/pane:opacity-100, matching the same pattern already used
in sidebar.tsx, MessageHoverToolbar.tsx, and NotificationItem.tsx.
@2witstudios

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 9, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

…hole pane

Second-pass self-review caught a regression in the previous commit's
accessibility fix: group-focus-within/pane fires on ANY focus inside
the pane, including xterm.js's own hidden helper textarea that
captures keystrokes during ordinary terminal typing (a sibling of the
controls div under the same group/pane wrapper). That pinned the
split/close controls overlay visible for the entire time a user was
typing, not just when a keyboard user tabs onto the buttons —
defeating the chrome-free design intent and occluding terminal output.

Fix: drop the group indirection for the focus case. Plain
focus-within:opacity-100 directly on the controls div only matches
focus on its own descendants (the 3 buttons), not the sibling terminal
content. This exactly matches the existing pattern in
MessageHoverToolbar.tsx (group-hover/msg:opacity-100 combined with
plain focus-within:opacity-100) — should have used it the first time.

group-hover/pane:opacity-100 is unchanged and correct: hovering
anywhere in the pane revealing the controls is the intended UX.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/src/lib/theme/css-color-resolution.ts`:
- Around line 153-160: The canvas-based color parsing in css-color-resolution.ts
can return a transparent sentinel when CSS.supports accepts a value that canvas
rejects, so update the color resolution flow to detect that case in the same
branch that uses context.fillStyle and fall back to the computed-style path
instead of returning `#00000000`. Use the existing color resolution logic around
the canvas draw/read sequence and the computed-style fallback helper to ensure
invalid canvas parses do not bypass the fallback.

In `@apps/web/src/stores/terminal-workspace/useTerminalWorkspaceStore.ts`:
- Around line 87-98: The new splitRight and splitDown ID generation is using
crypto.randomUUID(), which should be replaced with the project’s CUID2-based ID
generator to keep TypeScript IDs in the expected format. Update the splitRight
and splitDown methods in useTerminalWorkspaceStore to call the existing
`@paralleldrive/cuid2` helper used elsewhere in the codebase instead of generating
UUIDs, and keep the newColumnId/newPaneId creation logic otherwise unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c023c153-9a67-42ae-9382-9f4d716166fa

📥 Commits

Reviewing files that changed from the base of the PR and between fee4369 and ee6eee0.

📒 Files selected for processing (10)
  • apps/web/src/components/layout/middle-content/page-views/terminal/XtermTerminal.tsx
  • apps/web/src/components/layout/middle-content/page-views/terminal/workspace/TerminalPanes.tsx
  • apps/web/src/components/ui/resizable.tsx
  • apps/web/src/hooks/useMonacoTheme.ts
  • apps/web/src/hooks/useXtermTheme.ts
  • apps/web/src/lib/theme/css-color-resolution.ts
  • apps/web/src/stores/terminal-workspace/__tests__/useTerminalWorkspaceStore.test.ts
  • apps/web/src/stores/terminal-workspace/__tests__/workspace-reducer.test.ts
  • apps/web/src/stores/terminal-workspace/useTerminalWorkspaceStore.ts
  • apps/web/src/stores/terminal-workspace/workspace-reducer.ts

Comment thread apps/web/src/lib/theme/css-color-resolution.ts
Comment thread apps/web/src/stores/terminal-workspace/useTerminalWorkspaceStore.ts
…ent black

Addresses CodeRabbit review feedback on PR #1965: CSS.supports('color',
input) can accept syntax the canvas 2D fillStyle parser still silently
rejects. An invalid fillStyle assignment is ignored per spec, leaving
fillStyle on whatever was set before it — the rgba(0, 0, 0, 0) sentinel
this function sets to detect exactly that. Without a check, that read
back as a successfully-resolved transparent-black color instead of a
rejected parse, bypassing the computed-style fallback.

Capture the sentinel's canonical string right after setting it; if
fillStyle still reads back as that same string after assigning `input`,
treat it as rejected and fall through to the DOM-probe fallback instead
of returning #00000000.

No observable behavior change for any color value this codebase
actually resolves (oklch/hex/rgb from globals.css all parse identically
in canvas and CSS.supports on evergreen browsers) — this only closes a
theoretical gap for pathological inputs.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant