feat(THI-246 PR 3/3): Split detail pane — header chips, inline xterm, sidebar#115
Open
tdody wants to merge 5 commits into
Open
feat(THI-246 PR 3/3): Split detail pane — header chips, inline xterm, sidebar#115tdody wants to merge 5 commits into
tdody wants to merge 5 commits into
Conversation
PR 1 shipped a minimal header (kind + name + session/index + branch + StatusPill + focus button). Replace it with the chip layout the spec calls for, mirroring TerminalModal's header so the at-a-glance signal is the same whether the user is in modal or inline mode: - Branch + PR + CI rollup as one `branch-pr` chip, with the CI tone carried in the className and the PR number rendering as an external link when `prUrl` is present. - Agent activity spinner chip (when the parser surfaced one). - Ctx% chip from `agent.contextPct` — only renders when the parser reports it, so non-Claude agents stay quiet. - Status pill stays in its current position. - Pending-input action hint surfaced as ellipsized amber text next to the status pill (same pattern as TerminalModal's `term-action`). - Focus-in-tmux button stays as the trailing action. Three new tests cover the branch/PR chip, the ctx% conditional, and the action hint. CSS adds `.sb-pane-action` (amber, ellipsized) and the meta `.sep` divider; existing `.sb-pane-hd` flex/wrap already handles the chip layout. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tail
Lifts the xterm.js + WebSocket lifecycle out of TerminalModal into a
shared <PaneTerminal> component so the Split view's detail pane can
embed a live terminal without duplicating ~400 lines of imperative
setup.
What moved into PaneTerminal:
- Terminal construction (font, theme, link providers, key handler).
- WebSocket connect + auto-reconnect loop with the existing
decideCloseAction backoff (same close-code handling, same retry
ladder).
- Preview painting + snapshot replacement.
- Resize lifecycle: ResizeObserver + rAF-debounced FitAddon, forward
cols/rows to tmux.
- Select-to-copy + image-paste.
- Live zoom / live theme without rebuild.
- PromptOverlay rendering (the prompt state lives next to the WS).
- Document-level ⌘=/⌘-/⌘0 zoom hotkeys.
What stays in TerminalModal:
- Modal chrome: scrim, traffic lights, term-hd chip header, term-foot
with connection pill / Reconnect button / zoom buttons / Size buttons
/ Kill button / hint text.
- Modal-specific Esc behavior (PaneTerminal calls onEscape; modal wires
it to onClose).
What changes for the inline path:
- SplitView's detail pane now renders <PaneTerminal key={paneId}> so
selecting the same pane reuses the terminal + WS; selecting a different
pane tears down + remounts cleanly. This is the spec's "reconnect,
don't recreate" semantics, implemented via React's key-stable identity.
- New optional `onToast` prop on SplitView, wired in App.tsx to the
existing messageToast.
- DetailHeader gains a dim connection pill that turns amber while
reconnecting and red on disconnect — xterm itself still surfaces
[reconnecting…] inline as the primary signal.
- DetailPlaceholder + .sb-detail-stub copy gone; the detail pane is now
always live.
Surface guarantees: all 24 existing TerminalModal tests still pass
unchanged — the modal now wraps PaneTerminal but its assertion targets
(connection pill, Reconnect button, chip header, kill, scrim, select-to-
copy, preview placeholder behavior) all live in the modal chrome and
verify behavior PaneTerminal exposes via the onConnectionChange callback.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the Linked / Notes / Activity side panel skeleton to the Split detail pane. The three sections fill in in follow-up commits within this PR. - New persisted setting `splitDetailSidebar: boolean` (default true). Tracks the user's preference; the actual visibility ANDs it with agent-gating so shell panes never show the panel. - Sidebar toggle in DetailHeader: a `btn-icon btn-ghost` with the `docs` glyph. Hidden entirely on non-agent panes — the toggle would be a no-op there since the panel is gated off. - `.sb-side` container: 256px wide, vertical flex of sections each with an uppercase mono header and a body. Inherits the detail pane's bg-elev surface so it reads as part of the chrome. - Activity section already surfaces the spec's "Needs your input" banner when status === "waiting" — full timeline deferred per the scope decision. - Three new tests: agent default = visible, toggle persists hidden state, non-agent has no sidebar AND no toggle, waiting → need-human banner. - Test file gains xterm module mocks so PaneTerminal (which Detail now mounts) doesn't crash happy-dom on import. Same vi.hoisted pattern TerminalModal.test.tsx uses. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fills in two of the three sidebar sections; Activity's banner already shipped in the sidebar-shell commit. Linked: - PR card hydrated from the existing pr/prUrl/branch/ci Window fields — same data the branch/PR chip on Kanban/List uses. Tappable link to the GitHub PR. CI rollup colors the card border (green/red/amber). - Falls back to a non-link "branch with no open PR" card when branch is set but pr isn't. - "+ Link a PR…" dashed CTA when both are empty (disabled — manual linking is a future-feature placeholder). - "+ Link a Linear issue…" dashed CTA, disabled with a tooltip — the backend doesn't expose Linear yet. Surfaces the slot so users can see what's coming without a half-working feature. Notes: - Per-pane localStorage keyed `switchboard:pane-notes:<paneId>`. - 300 ms debounce on writes so a flurry of keystrokes doesn't thrash the store. - Hydrates lazily in useState's initializer so re-mounts pick up prior content without extra wiring. - Parent re-renders (every /api/state poll) don't re-mount the textarea; cursor position and focus survive. Four new tests: PR card render + href + CI tint, branch-only fallback, debounced persist via fake timers, hydration on mount. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The terminal modal's last cell row clipped at the bottom edge when the
modal was tall (data-column-size="wide" → 92vh × 940px cap, or any
similar configuration where the .term-body grid slot is large enough
that the 6px padding made the difference of an extra row in fit()'s
math).
Root cause: the inline `padding: 6` sat directly on .term-body, which
was also the xterm host (hostRef pointed there). xterm-addon-fit reads
the host's `clientHeight`, which INCLUDES padding — so fit() computed
rows assuming the full padded box was renderable, but xterm's cells
start at the padding-top edge. With clientHeight tall enough, the
last row's cells extended past the host's bottom padding boundary and
got chopped by `.term-body { overflow: hidden }`.
The 100px legacy bottom padding the pre-xterm renderer used to ship
hid this; the THI-103-tracked inline-`padding: 6` override revealed it.
Fix: wrap the xterm host. .term-body is now a flex container with the
visual 6px gutter; the inner .term-host (which xterm measures) has no
padding of its own. fit()'s clientHeight read now matches the exact
cell-rendering area, so the last row stays inside the box at every
modal size.
Mouseup listener for select-to-copy and the paste handler moved with
the host to .term-host (still bubbles correctly from xterm cells). One
TerminalModal test updated to fire mouseup on .term-host instead of
.term-body.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Final PR in the THI-246 Split-view trilogy. PR 1 (#113) shipped the layout
foundation; PR 2 (#114) shipped the rail features. This PR fills in the
detail pane: a chip header that mirrors the modal's at-a-glance signal, an
inline xterm with reconnect-don't-recreate on pane switch, and a sidebar
with three sections (Linked / Notes / Activity).
Five commits, each isolated so review can slice:
88a60e440a9003PaneTerminal; refactor TerminalModal; mount inline withkey={paneId}8bbc6bf1502a357d1afa8What ships
Detail header chips
The PR 1 placeholder header had just
kind + name + session/index + branch + StatusPill + focus button. Replace with the layout the spec calls for, mirroring TerminalModal's header:prUrlfield the WindowCard uses)agent.contextPct(only renders when the parser surfaced it)liveInline xterm via PaneTerminal extraction
<PaneTerminal>component lifts the xterm.js + WebSocket lifecycle out of TerminalModal (~400 lines of imperative setup) so both surfaces share one implementation.TerminalModalbecomes a thin wrapper that adds modal chrome (scrim, traffic lights, header chips, footer with connection pill / Reconnect / zoom / size / Kill) around the shared component.<PaneTerminal key={paneId}>— selecting the same pane reuses the terminal + WS; selecting a different pane tears down + remounts cleanly. This is the spec's "reconnect, don't recreate" via React's key-stable identity.Sidebar shell + agent gating
splitDetailSidebar: boolean(defaulttrue).docsglyph). Hidden entirely on non-agent panes — shell panes don't have Linked / Notes / Activity context..sb-side: 256px wide, vertical flex of sections with uppercase mono headers.Linked section
pr,prUrl,branch, andcifields. Tappable link to GitHub. CI rollup colors the card border (green / red / amber).branchis set.Notes section
localStoragekeyedswitchboard:pane-notes:<paneId>./api/statepoll) don't re-mount the textarea, so cursor position and focus survive.Activity section
status === \"waiting\"(amber background).Bug fix: xterm last-line clip
The PR 3 work surfaced a pre-existing latent bug. At tall modal sizes (
data-column-size="wide"→ 92vh × 940px cap), the bottom row of cells clipped at the box edge because:.term-bodywas both the visual chrome (withpadding: 6) and the xterm host (wherehostRefpointed).xterm-addon-fitreadsclientHeight, which includes padding.overflow: hidden.The fix: separate the visual gutter from the xterm host.
.term-bodyis now a flex wrapper with the 6px gutter; new inner.term-host(no padding) is what xterm measures.clientHeightnow matches the exact cell-rendering area.Test plan (automated)
npm test).npm run typecheck: clean.npm run build: clean (verified via pre-push hook).Manual smoke (needs a live session — only the user can run this)
Open the dashboard, switch to Split layout. The rail and selection should be the same as PR 1 + PR 2 shipped. Now:
Detail header
#Nlinking out to the PR on GitHub.ctx N%chip appears next to the spinner.waiting, the action question shows in amber after the status pill (ellipsized if long).Inline xterm + reconnect-don't-recreate
[reconnected]banner, scrollback intact).localStorage, its terminal connects fresh.Escforwards to pane (single tap on a live WS); ⌘/Cmd + zoom keys still work.Sidebar
docsicon) appears only for agent panes. Click it — sidebar slides in/out, preference persists across reloads.#N+ branch when the agent's branch has an open PR. CI rollup colors the card border. Click the card → opens the PR on GitHub.localStorage. Reload — your notes come back.waiting, an amber "Needs your input" banner shows. When the agent is running/idle/done, only the placeholder text appears.Bug fix verification (the headline)
/to open size buttons, click size+ until "wide"). Verify the last line of the active terminal is fully visible — cursor / input prompt / Claude's status line all show in full, not cut at the bottom. Pre-fix this was the visible regression.Themes
.sb-side*surfaces and the PR card use existing tokens — no white-on-white, no invisible borders.Out of scope (don't test these)
Follow-up tickets to file
🤖 Generated with Claude Code