Skip to content

feat(workspace): give terminals the same session features as agents - #872

Merged
Juliusolsson05 merged 23 commits into
mainfrom
feat/terminal-session-parity
Sep 11, 2026
Merged

Juliusolsson05 merged 23 commits into
mainfrom
feat/terminal-session-parity

Conversation

@Juliusolsson05

Copy link
Copy Markdown
Owner

Problem

Plain terminal panes are first-class sessions structurally: grid leaves, Dispatch rows, command targets, operator workspace.observe entries, tmux-persistent across restarts. But about 40 checks asked "is this an agent?" (isAgentProviderKind) when they meant "is this a session the user manages". So a shell could not be titled, named, pinned, jumped to by its pane label, auto-followed, inspected in Agent Status, or seen as busy. Its header was a hand-drawn terminal $ strip older than every header feature since April (folder, color flag, title/name row, Status Mode). Shells also had no activity signal at all: they only ever emitted started, data and exit.

Prompt templates and the Key Vault already reached terminals (#830 / #831 via #840). This PR finishes the job for both kinds of terminal:

  • plain shells (SessionKind 'terminal', TerminalLeaf);
  • agents shown in the raw terminal view (AgentTerminalLeaf).

It also closes a remote-phone leak (#866) and the terminal-view related-agent gap (#858).

What changed

Session features for every session kind (the terminal exclusions are deleted, not generalized):

  • Titles: "Set Title…" (id agent.title.set unchanged) and the operator's agents.titleSet.
  • Spoken names, from the same pool and under the same setting, with the name-row height reservation so a late name never SIGWINCHes a TUI running in the shell.
  • Pins: "Pin Sessions…" / "Unpin Session", ids unchanged.
  • Jump by pane label: "Go to A2" / A2! now reaches terminals, and the palette copy names a terminal correctly.
  • Lists and panels: Agent Status panel, Close Old Agents, the Activity modal (including Bury), and the Worktrees panel.
  • Operator: agents.locate/show/close/restore/list/titleSet/pinSet and agents.search (whose provider filter now accepts terminal) all accept terminals. agents.prompt refuses a terminal before any wake or write, and points to terminals.input.

One header:

  • TerminalLeaf renders the shared PaneHeader, like AgentTerminalLeaf since fix(workspace): light the status header for terminal-view agent panes #853: live folder (follows cd through tmux), color flag, title/name row, Status Mode fill, TAIL pill, and the foreground command as the badge.
  • Plain terminals get data-pane-id and auto-follow / Jump to Latest / Auto-follow All through the shared useTerminalFollow hook (renamed from useAgentTerminalFollow).

Shell activity:

  • A new main-process TerminalForegroundMonitor reads each terminal's foreground command. tmux-backed terminals use one tmux list-panes -a per second for all managed sessions (active pane of the active window); direct PTYs use node-pty's process.
  • It emits only on change, on a dedicated session:terminal-foreground channel.
  • The renderer folds this into the existing processActive / activityStatus inputs. Status Mode, tab running counts, the "still working" close confirmation and the Dispatch subtitle (shell running · npm) therefore light for shells with no terminal-specific code downstream.
  • A finished command marks the session unread (NEW). After restore, a per-window snapshot keeps busy shells lit across a renderer reload.

Naming: one sessionDisplayTitle rule. An untitled session is named by its live folder, then its spawn folder, never by its raw session UUID (which the close dialog used to show). A terminal's Dispatch row follows cd. Shell rows show NEW and ERROR.

Terminal view (#858): the one-line status row names the related agent being displayed (raw codex · orchestration worker-2), and a parent button returns to the owner. No chip row is added, so the TUI is never resized by this.

Small fixes found during research:

  • Dispatch's "New Agent…" picker offers Terminal and files it on the clicked project.
  • "Undo Clear Composer" is hidden on shells.
  • Copy Last Response, View Prompts and Reader no longer appear (and silently do nothing) on OpenCode Terminal, via a shared sessionHasTranscript predicate.
  • The template fill pane shows "Pastes at the terminal cursor…" instead of Replace/Append radios for terminal targets.
  • Stale comments corrected.

Remote phone (#866):

  • SessionFeedSource drops any frame whose session is a terminal at its single emit choke point. The check also consults the spawn-time kind, so the pre-registration starting readiness frame is covered.
  • RemoteServer refuses submit, interrupt and permission-reply for non-agent sessions.
  • A paired phone can no longer learn terminal session ids or press Enter or Esc in a shell.

Design decisions and tradeoffs

  • Deliberately reverses feat(workspace): add persistent agent titles #660 ("plain shell terminals are intentionally out of scope"), feat(workspace): give agents stable voice-friendly names #836 ("Terminals are never named") and the Add terminal sessions to Dispatch Mode #152 v1 pin deferral. A title, a name, a pin and a header are session metadata. The operator gained terminals.input in feat(control): expose external MCP through an isolated capability SDK #793, so a named shell is routable.
  • Stays agent-only (D2): prompt delivery (deliverPromptToAgent is untouched), transcript features, composer features, Reader Mode (now guarded at the action level too), and the remote phone.
  • Activity uses a dedicated channel, not process-state, because the remote tap and the session recorder subscribe to process-state and must never see terminal state. The poll has an in-flight guard, a 2 s tmux timeout, and runs only while terminals exist.
  • "Running" means a foreground process other than the shell. A dev server or vim keeps its pane lit, which is literally true and makes the close confirmation useful. Commands shorter than one poll interval are never seen as busy.
  • TerminalLeaf keeps its no-re-render-per-chunk contract: only primitive store selectors. Dispatch rows select activityStatus only for terminal rows, so Codex's per-second working… 12s tick does not re-render agent rows.
  • Command ids are unchanged (they key saved keybindings and visibility); only titles changed.

Tests

Tests sit next to the modules they protect. Tests that asserted the old exclusions were rewritten, each with the reason. Added or extended:

  • terminalForeground.test.ts: classifier, track/untrack lifecycle, in-flight guard, tmux failure keeps state, direct PTYs not blocked by a pending listing.
  • TmuxRegistry.test.ts: listing parser, prefix filter, active pane of the active window, paths containing tabs.
  • session-runtime/terminalForeground.test.ts and useTerminalForeground.renderer.test.tsx: running/NEW semantics, snapshot waits for restore.
  • TerminalLeaf.header.renderer.test.tsx: shared header, data-pane-id, command badge, Status Mode, live cwd, color flag, title row, TAIL.
  • AgentTerminalLeaf.related.renderer.test.tsx: bug(workspace): terminal view hides which related agent a pane is showing #858.
  • sessionDisplayTitle.test.ts, rowTitle.test.ts, closeConfirmation.test.ts.
  • Pins, labels and navigation: dispatchSelectors.test.ts, paneLabels.test.ts, agentIndexNavigation.test.ts (grid, tiled-Dispatch lane, detached swap), agentIndexCommand.test.ts.
  • Lists: CloseOldAgentsModal.rows.renderer.test.ts, agentStatusModel.renderer.test.ts, loadWorktreeDump.test.ts.
  • agents.renderer.test.ts: terminal metadata and navigation accepted, prompt refused, show refused while Reader owns the screen.
  • transcriptAvailability.test.ts, PromptTemplateFillPane.renderer.test.tsx, NewAgentPlacementOverlay.renderer.test.tsx.
  • SessionFeedSource.test.ts (including the pre-registration window) and RemoteServer.integration.test.ts (submit, interrupt and permission-reply refused for terminals).
  • useIpcSubscriptions.renderer.test.tsx: terminalForeground cleared on exit.

There is no SessionManager-level wiring test: it would need a real node-pty spawn, which this suite does not do. The monitor, classifier and parser carry the behavior.

Verification

Run on Node 24.14.1 at the final head:

  • npm run test:contract: pass

  • npm run check:keybindings: pass

  • npm run typecheck: pass

  • npm test: 3113 passed, 2 failed. Both are known local-only environment failures, in areas this branch does not touch:

    • imageAttachment.test.ts cites a Claude session file that was deleted from this machine;
    • store.test.ts's prompt-template migration test times out at 5 s under full-suite load and passes 8/8 on its own.

    CI's quality-gate is the authoritative run.

Process: each of the 13 implementation tasks had an independent spec and quality review; Tasks 8 and 10 each needed one fix round. An Opus whole-branch review found three important and nine minor findings. One fix wave addressed them, and a scoped Opus re-review confirmed every item.

Known limitations and follow-ups

  • OpenCode Terminal still never shows "running" (bug(opencode): OpenCode Terminal runtime sessions never report running #857). It needs spinner detection in opencode-headless, because its foreground process is always opencode.
  • A terminal's "last active" is stamped renderer-side. It resets on reload or restart, and is cleared when the shell exits. This fails safe: idle shells look fresh and are never wrongly closed by Close Old Agents.
  • Auto-follow All Visible now also pins plain terminals. They cannot be scrolled up while it is on; previously they were never affected.
  • The Worktrees panel matches shells by spawn cwd, not the live tmux cwd.
  • Follow-up: reader.ts setReaderModeTarget / toggleReaderMode still check isAgentProviderKind rather than sessionHasTranscript, so OpenCode Terminal can still open an empty Reader by keybind. This predates the branch.
  • Follow-up: an agent spawn that fails before registration leaves its starting readiness frame in the remote late-joiner cache. This predates the branch; it is the same class as bug(remote): remote clients can act on terminal sessions the phone is never shown #866 but for agents.

Needs a live check (the app was not launched during this work)

  • Status Mode lights while npm test runs in a tmux terminal and clears when it finishes. The Dispatch row shows shell running · node and then NEW.
  • A busy shell stays lit across Cmd+R.
  • Set a title, pin, and "Go to A2" on a plain terminal.
  • With a paired phone: terminals never appear, and no terminal id shows up in any frame.

Fixes #865
Fixes #866
Fixes #858
Refs #857 #851 #853 #830 #831 #816 #660 #152 #793

🤖 Generated with Claude Code

https://claude.ai/code/session_01LEwGeHkkhnDxU3X3chXX3F

Juliusolsson05 and others added 22 commits September 10, 2026 18:18
…gents

Research found that prompt templates and the key vault already reach both
kinds of terminal (#840). What is still agent-only is session metadata and
chrome, gated by ~40 checks that ask "is this an agent?" when they mean "is
this a session": title, spoken name, pin, label jump, header, auto-follow, the
status panel and the operator's session capabilities. Shells also have no
activity signal at all. This plan lifts those checks, gives plain terminals the
shared PaneHeader, adds a main-process foreground monitor for shell activity,
and fixes the remote leak (#866) and the terminal-view related-agent gap (#858).

Refs #865

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEwGeHkkhnDxU3X3chXX3F
A title is session metadata the user writes to scan a busy grid, and every
reader of SessionMeta.title already handled terminals. Only the command guard,
the action and the reducer refused them, a scope decision from #660 that #865
reverses. The command id stays agent.title.set so saved keybindings survive.

Refs #865

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEwGeHkkhnDxU3X3chXX3F
Names were withheld from shells because a spoken shell name looked like an
unroutable target. Since the operator gained terminals.input that reason is
gone, and a shell running a server is exactly what a voice user wants to say
out loud. Shells also reserve the name row, because the same late-arrival
resize would otherwise SIGWINCH a TUI running in the shell.

Refs #865

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEwGeHkkhnDxU3X3chXX3F
…ocess

Shells emitted only started/data/exit, so nothing could say a terminal was
busy: Status Mode, tab counts and the Dispatch shell subtitle never lit. A
single poller asks tmux (one list-panes for every managed session) or node-pty
which process owns each terminal's foreground, and emits only on change on a
dedicated channel, so the remote tap and the recorder never see terminal state.
The poll exists only while terminals do and cannot overlap itself.

Refs #865

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEwGeHkkhnDxU3X3chXX3F
…nread

Main's foreground observations become the same processActive/activityStatus
inputs an agent spinner produces, so Status Mode, tab counts, the close
confirmation and Dispatch treat a busy shell as running with no terminal
branches downstream. A finished command marks the session unread like a
completed turn. withUnread moves to session-runtime so both writers share the
attention-outranks-output rule.

Refs #865

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEwGeHkkhnDxU3X3chXX3F
…-follow

TerminalLeaf hand-drew a 'terminal $' strip that predated every header feature,
so shells had no folder, color flag, title/name row or Status Mode fill, the
same drift #851 fixed for agent terminal views. It now renders PaneHeader with
the foreground command as its badge and the live tmux cwd, carries
data-pane-id, and follows output through the shared xterm follow hook, which
un-gates Auto-follow and Jump to Latest for shells.

Refs #865

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEwGeHkkhnDxU3X3chXX3F
…ncluded

A dozen title fallbacks disagreed. The close dialog showed a raw session UUID
for any untitled session, and the buried picker showed 'kind · folder'. One
sessionDisplayTitle rule now covers them, a terminal's Dispatch row follows the
live tmux cwd, and shell rows show NEW, ERROR and the running command like any
other row.

Refs #865

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEwGeHkkhnDxU3X3chXX3F
Pins excluded terminals at seven layers, a scope decision from before shells
were Dispatch rows at all. Since #671 a terminal is a full row, and a pinned
dev-server shell is the one-keystroke-away session pins exist for. The
invariant shrinks to every pinned id is a live session; command ids stay stable.

Refs #865

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEwGeHkkhnDxU3X3chXX3F
Typing a terminal's label fell through to ordinary command search while
Dispatch keyboard navigation already selected terminals, so the two ways of
reaching a pane disagreed. Label resolution and index navigation now accept
every session kind.

Refs #865

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEwGeHkkhnDxU3X3chXX3F
The pane-label command row hardcoded "agent" in its title/description even
though buildAgentPaneLabelTarget now returns terminal targets too, so typing
a terminal's label showed copy claiming resume/clone semantics no shell has.
Branch the copy on target.kind, and cover both kinds plus two more
terminal-target navigation paths (Tiled Dispatch lane, grid swap).

Refs #865

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEwGeHkkhnDxU3X3chXX3F
…ists

Close Old Agents and the Activity modal had no age for a shell, Agent Status
refused one, the Worktrees panel ignored shells running inside a worktree, and
the Activity modal hid Bury for terminals although burying them works. Each now
treats a terminal as a session, aged by its last foreground change.

Refs #865

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEwGeHkkhnDxU3X3chXX3F
The operator could create, read and write a terminal but not find, show,
title, pin, restore or close it. Session capabilities now accept every kind,
while agents.prompt refuses a terminal with the route that exists instead
(terminals.input), so a spoken shell name can never become a provider prompt.

Refs #865

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEwGeHkkhnDxU3X3chXX3F
requireSession stopped refusing terminals for #865, which made agents.show
newly reachable with one while Reader Mode was open. Reader has no transcript
view for a terminal, and its own setReaderModeSession action had no kind
guard (unlike its toggleReaderMode/setReaderModeTarget siblings) — it was
protected only by ReaderView's rendering filter, by accident. Give
setReaderModeSession the same agent-kind guard as its siblings, and have
agents.show refuse a terminal up front whenever Reader Mode owns the screen,
before any focus/navigation call. Spotlight is unaffected; it shows
terminals fine.

Refs #865

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEwGeHkkhnDxU3X3chXX3F
A persisted related selection mounted a child's raw TUI under the parent's
pane label with nothing marking it, so input could go to the wrong agent. The
existing status row now names the displayed agent and offers the way back to
the parent, without adding a row that would resize the live TUI.

Fixes #858
Refs #865

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEwGeHkkhnDxU3X3chXX3F
Dispatch's New Agent picker could not create a terminal and its commit path
kept a dead branch that ignored the project header. Undo Clear Composer showed
on shells. Copy Last Response, View Prompts and Reader appeared on OpenCode
Terminal and silently did nothing. The template fill pane claimed replace or
append on a terminal that only ever pastes at the cursor. Stale comments about
proxy retention and template scope are corrected.

Also widens createDetachedDispatchAgent's declared parameter type to the full
SessionSpawnSelection (was SessionSpawnSelection & Exclude<SessionKind,
'terminal'>), matching what the underlying implementation already accepted —
its createDetachedSession alias never carried the narrower type, and
control/terminals.ts has been passing kind: 'terminal' through the same
function for a while. Without this the Dispatch terminal call above does not
type-check.

Refs #865

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEwGeHkkhnDxU3X3chXX3F
The remote feed filtered terminals only on the started event, so readiness,
exit and process-state frames still carried terminal session ids, and submit
and interrupt accepted any id. A paired client could press Enter in a shell it
was never shown. The source now drops terminal frames at its single emit choke
point, and inbound writes require an agent session.

Fixes #866

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEwGeHkkhnDxU3X3chXX3F
…gate

I1/#866: SessionFeedSource.emit() gated on SessionManager.getSessionKind,
which reads only the live registry entry. Every spawn (terminals included)
emits input-readiness {ready:false, reason:'starting'} BEFORE that entry is
inserted, so a terminal's very first readiness frame slipped past the gate,
got broadcast, and stuck forever in RemoteServer.lastInputReadiness (later
exit/removed frames for that id ARE filtered once the entry exists, so
nothing ever evicts the stale cache row).

Add SessionManager.getSpawnKind(), reading the earlier-populated spawnInfo
map, and fall back to it in the gate: (getSessionKind ?? getSpawnKind) ===
'terminal'. spawnInfo is deleted in cleanupSessionState alongside the
registry entry, so the fallback never outlives a session's lifecycle.

T13: add a sibling RemoteServer.integration.test.ts case proving
permission-reply is refused for a terminal session id ({ ok: false, error:
'not an agent session' }, manager.write/resolveCondition never called) —
kept separate from the existing submit/interrupt refusal test because
permission-reply exercises a third manager method that test's assertions
don't cover, and blurring them would hide which guard a future regression
broke.

Refs #865, #866

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEwGeHkkhnDxU3X3chXX3F
…s, and fix multi-window pane selection

M1: TmuxRegistry.runTmuxCapture resolved on the child's 'exit' event, but
stdout is a separate pipe that can still have buffered data in flight at
that instant — resolve on 'close' instead, once stdio is fully drained.
Also add an optional timeout (2000ms on listPaneForeground only, since it
runs on the 1s foreground poll for every open terminal and a wedged tmux
server would otherwise freeze that poll forever; listManagedSessions keeps
running with no timeout, since it only runs once at launch and hanging
there is a startup bug worth surfacing rather than racing a clock).

TerminalForegroundMonitor.tick() sampled direct PTYs and tmux panes in one
combined loop AFTER awaiting the tmux listing, so a slow/hung tmux server
delayed every direct-PTY terminal's foreground update too. Direct sessions
are now sampled and emitted unconditionally before the tmux await.

M4: PANE_FOREGROUND_FORMAT read only #{pane_active}, but tmux tracks an
active pane PER WINDOW — a background window's own active pane still
reports pane_active=1. Add #{window_active} and require both flags so a
multi-window session reports the pane actually visible to an attached
client, not whichever window's line happened to sort last.

M7: wrap each onChange call in try/catch (applySample) so one bad
subscriber can't turn `void this.tick()`'s interval driver into an
unhandled promise rejection that takes down foreground polling for every
other tracked terminal.

Refs #865

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEwGeHkkhnDxU3X3chXX3F
…s, and unify transcript/title rules

I3/D3: DispatchAgentList's per-row useShallow selector picked up
activityStatus for every row. Codex's activity string ticks every second
("working… 12s"), and the dispatch list never paints activityStatus for a
non-terminal row (dispatchSubtitle only reads it under kind === 'terminal'),
so every live Codex/Claude row was re-rendering once a second for a value it
never shows. Select it only for terminal rows. DispatchMiniList never
selected activityStatus, so it needed no change.

M3: useIpcSubscriptions' onSessionExit handler cleared processActive/
activityStatus but left terminalForeground untouched. The foreground monitor
(#865) only emits on change, so a dead shell's last command/cwd survived
forever with nothing left to correct it, and a same-id respawn's first
genuinely-idle sample risked being deduped against that ghost "busy" state.
Clear it alongside the other exit-time fields.

M5: reader.ts's setReaderModeSession guard and agents.ts's agents.show
refusal both used isAgentProviderKind/`provider === 'terminal'`, which admits
OpenCode Terminal (agent-provider-kind but providerRuntime 'terminal', so it
never loads a transcript). readerCommands.ts already gates on
sessionHasTranscript for the same reason; both guards now agree with it
instead of diverging.

M6: dispatchSelectors' sessionTitle/basename reimplemented its own
title→folder→cwd fallback instead of using sessionDisplayTitle, the shared
D6 title rule every other workspace-layer reader already follows. Dispatch
rows have no live tmux cwd available (that only lives in zustand runtime
state), so this resolves title → spawn folder → raw cwd with no live-cwd
override, matching the old behavior; DispatchAgentList layers the live cwd
back on top separately via dispatchRowTitle. The local basename helper is
now unused and removed; a local 'agent' fallback stays for the
meta-undefined case sessionDisplayTitle cannot itself handle.

Refs #865

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEwGeHkkhnDxU3X3chXX3F
…/T7)

Several comments and user-facing strings still described pre-#865 behavior
after terminal-session parity landed: TerminalLeaf.tsx now resolves
tailAllMode too (uiShell/types.ts said it never did), a terminal pane now
renders the same shared PaneHeader chrome as an agent pane (workspace/
types.ts said "no Agent Code chrome"), and Close Old Agents now batch-closes
terminals alongside agents (its dialog copy, empty state, and command
description/comment still said "Claude and Codex agents" / "stale provider
panes"). The Set Title command was renamed from "Set Agent Title" but a
uiShell/types.ts comment and the test file's describe block (T1) still
quoted the old name. The Pin Sessions command was renamed from "Pin Agents"
but paneCommands.ts comments and PinAgentsModal.tsx's header comment (T7)
still quoted the old title, and its "favorite agents" description text
didn't yet mention terminals now being pinnable too.

None of this changes behavior — command ids are untouched, only titles/
descriptions/comments — so no test asserts on the old strings; the affected
suites (agentTitleCommands.test.ts, catalog.test.ts, closeConfirmation.test.ts,
CloseOldAgentsModal.rows.renderer.test.ts, closedModalDerivations.renderer.test.tsx)
all still pass.

Refs #865

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEwGeHkkhnDxU3X3chXX3F
agents.search's `provider` filter enum was `['claude', 'codex', 'opencode']`
— terminals have been searchable/returnable by this capability since #865
(session.provider is genuinely 'terminal' for a shell), but zod rejected
`provider: 'terminal'` outright before it ever reached the filter, making
"find just my shells" impossible through the operator surface. Add
'terminal' to the enum.

agents.titleSet's input/output .describe() text called the field "Agent
display title" even though the capability's own description already says
it sets an "agent or terminal title" — the schema text contradicted the
capability it documents. Reworded to "Session display title" (agents.create
keeps its own separate title field/text; it only ever creates agents, never
terminals, so it stays as-is).

Refs #865, #866

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEwGeHkkhnDxU3X3chXX3F
…itle

Comment and label corrections from the final re-review; no behavior change. Refs #865

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEwGeHkkhnDxU3X3chXX3F
Brings #860 (New Agent In), #861 (Reader Mode from the render ledger) and
#870 (Codex cyber-block native clone) into the branch before merge. The
textual merge is clean; eight files changed on both sides, so the merged
tree is re-verified with tsc and the full suite rather than trusting the
pre-merge CI run.

Refs #865

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEwGeHkkhnDxU3X3chXX3F
@Juliusolsson05
Juliusolsson05 merged commit ce0b36f into main Sep 11, 2026
3 of 4 checks passed
@Juliusolsson05
Juliusolsson05 deleted the feat/terminal-session-parity branch September 11, 2026 17:47
Juliusolsson05 added a commit that referenced this pull request Sep 12, 2026
…undary

Brings the wheel-boundary fix (#791) onto current main, 379 commits ahead,
including the xterm 6.1.0-beta.304 / WebGL re-enable (#873), terminal
session parity (#872) and OpenCode Terminal (#882).

Conflicts, all at the three xterm hosts:
- AgentInlineTerminal.tsx: main moved the WebGL attach below `scheduleFit`
  (onRendererChange); keep only the boundary attach right after open().
- AgentTerminalLeaf.tsx: imports - keep the boundary import, drop
  AgentTitleHeader (removed on main in favor of PaneHeader); body - boundary
  attach after open(), then main's onRendererChange WebGL attach.
- TerminalLeaf.tsx: imports - keep both sides; body - same as above.

Re-checked against the installed beta: the scrollable element still only
consumes a wheel it scrolled (alwaysConsumeMouseWheel and
consumeMouseWheelIfScrollbarIsNeeded default false, Viewport sets neither),
and MouseService's passive wheel handler returns without preventDefault
while the buffer has scrollback, so the boundary leak persists and the
bubbling host listener remains the right place to cancel it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S4AoJJUBex14FAEZZfHPsV
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant