Skip to content

feat(inspector): per-session right-side layout (#160) - #161

Merged
jiweiyuan merged 4 commits into
mainfrom
feat/per-session-inspector
Jul 29, 2026
Merged

feat(inspector): per-session right-side layout (#160)#161
jiweiyuan merged 4 commits into
mainfrom
feat/per-session-inspector

Conversation

@jiweiyuan

Copy link
Copy Markdown
Collaborator

Closes #160.

What

The inspector's right-side state — which tab, which open detail (file / diff / trace / PR-issue), and the detail's maximize / list-collapse chrome — was a set of global singletons on TermioStore, force-cleared on every session switch. It's now per-session: each terminal tab keeps its own right-side context.

  • New TermioStore.InspectorState + inspectorStates: [Session.ID: InspectorState] (sits alongside runtimes / surfaces).
  • selectedSessionID's didSet saves the outgoing session's layout and restores the incoming one's, replacing the blanket overlay-clear that lived in TerminalPane.
  • Dead sessions' entries are pruned in syncRuntimes.

Fixes the fullscreen blank-screen bug

Maximizing a detail then switching terminal tabs used to tear the full-window maximize host down to nothing (global inspectorMaximized + clear-on-switch racing the surface re-show), leaving a blank window. Restoring the target session's real state instead of clearing removes the race.

Persistence — state.json, no SQLite

Only the durable subset is written to state.json: the tab + the open file (path / line / read-only). A diff / PR / trace is a live snapshot (siblings, PR status, transcript) that gets re-fetched, so those stay in-memory only — matching VS Code's hot exit (restore open files, not transient views). On restore the file path is validated with FileManager; a deleted file / removed worktree silently falls back to no detail, never an error overlay.

Scope decision (deliberate)

Content is per-session; width + open/closed stay global (they belong to the AppKit NSSplitViewItem autosave). This matches VS Code / JetBrains, where the sidebar viewlet and the editor area are independent — opening a file doesn't collapse the sidebar. Width-per-session would mean overriding autosave + re-driving the divider on every switch (jank-prone, low value), so it's intentionally left out.

Performance (100-tab target)

Switch hot-path is O(1): captureInspectorState / applyInspectorState touch a fixed set of fields, independent of tab count. Memory is O(N) tiny structs (mostly nil fields). The only O(N) work is building the persisted-layout dict inside persist(), dwarfed by the pre-existing projects encode. No new scaling cliff. (Pre-existing note: persist() writes synchronously on the selection-change path — a candidate for debouncing if 100-tab rapid switching needs it, independent of this change.)

Test

  • swift build clean
  • File open in session A survives switch away/back
  • Git diff / GitHub issue restore per session
  • Fullscreen + tab switch no longer blanks

🤖 Generated with Claude Code

The inspector tab and open detail (file / diff / trace / PR-issue) plus its
maximize / list-collapse chrome were global singletons on TermioStore, force-
cleared on every session switch. Each terminal tab now keeps its own right-side
context in an in-memory `[Session.ID: InspectorState]`: the outgoing session's
layout is saved and the incoming one restored, replacing the blanket overlay
clear in TerminalPane. Because the maximize host is no longer torn down to
nothing on every switch, the fullscreen blank-screen race goes away too.

Persistence rides state.json (no SQLite): only the durable subset — the tab and
the open file (path / line / read-only) — is written, since a diff / PR / trace
is a snapshot of data that gets re-fetched. On restore the file path is
validated and silently falls back to no detail if it's gone. Width and open/
closed stay global (they belong to the AppKit split item); it's the content
that is session-specific — matching VS Code's sidebar-viewlet ⊥ editor-area model.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 29, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
landing Ready Ready Preview, Comment Jul 29, 2026 12:54pm

Request Review

…sist

Three regressions the per-session inspector work introduced (PR #161 review):

1. Sidebar tab clicks cleared `openFileURL` before changing the selection, so the
   didSet captured an already-emptied layout and the outgoing session's open file
   was lost on switch-away. Only clear when re-tapping the already-selected row.
2. Durable inspector edits (open a file, switch the tab) don't move
   `selectedSessionID`, so nothing persisted them — opening a file and quitting
   without switching lost it. Added a debounced whole-state save funnelled through
   `refreshDetailPresentation`.
3. Launch restore clobbered a session's seeded layout: the programmatic selection
   change fired the didSet, which captured the still-default live inspector over
   the just-seeded state. Guard restore with `isRestoringInspector`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Addresses the two deeper findings from the PR #161 review:

4. `issuesModel` was a single global slot, so restoring a session's saved issue
   detail could render it against another repo's model (wrong-repo fetch). It's
   now a `[repoRoot: IssuesPanelModel]` cache; `issuesModel` resolves to the
   selected session's repo (keyed on `inspectorProjectPath`, the exact string
   `IssuesView` is built with), falling back to the list when none has loaded.

5. The fullscreen blank-on-tab-switch is the tickless surface sitting on a stale
   frame after the maximized-detail host is removed. Nudge the selected surface
   with a short render pump when the host tears down (`repaintSelectedSurface`).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jiweiyuan

Copy link
Copy Markdown
Collaborator Author

Addressed codex review (5 findings)

Regressions fixed (commit 85fa978):

  • chore: Use inverted icon for dev builds #1 High — Sidebar tab click cleared openFileURL before the selection changed, so the didSet captured an already-emptied layout and the outgoing file was lost. Now only clears when re-tapping the already-selected row.
  • feat(terminal): Group navigator toolbar actions #2 High — Durable inspector edits (open file / switch tab) don't move selectedSessionID, so nothing persisted them. Added a debounced whole-state save funnelled through refreshDetailPresentation.
  • fix(terminal): Align inspector toolbar height #3 High — Launch restore clobbered a session's seeded layout (programmatic selection fired the didSet, capturing the still-default live inspector). Guarded with isRestoringInspector.

Deeper findings fixed (commit df427ff):

Confirmed-sound by the review and left as-is: issue+diff restore order, UUID keying, missing-file fallback, old-JSON decode, O(1) switch hot-path.

…ack window

The issue detail's WKWebView was the one branch of `conversationBody` without a
fill frame (its error/progress siblings both have `maxWidth/maxHeight: .infinity`).
Without it, SwiftUI could size the representable from the web view's intrinsic —
near-zero while the HTML is mid-load — collapsing the detail to a sliver, and the
empty area paints the window background as black. Most visible across a
minimize/restore relayout.

Also harden the restore path: `windowDidDeminiaturize` clamps a frame that came
back under the content minimum (setFrame ignores contentMinSize), forces a
relayout, and nudges the tickless terminal surface to repaint — same surface-
won't-repaint-itself family as the maximize-teardown fix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jiweiyuan
jiweiyuan merged commit 71bf658 into main Jul 29, 2026
2 checks passed
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.

Per-session inspector layout (right-side panel state should follow the session, not be global)

1 participant