You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit ef614f9
Browse filesBrowse the repository at this point in the historyBrowse files
fix background re-renders stealing focus and killing terminal selection
The statusLine helper rewrote <sid>.sessionid on every Claude render, and
updateSessionCliId had no equality guard — so each identical write fired
session-changed, and renderLayout() answered with container.appendChild(pane).
appendChild on a node that is already a child is a remove-and-reinsert: it blurs
whatever is focused inside (the Cmd+F find bar is appended *into* .terminal-pane)
and detaches the node mid-drag, where xterm's selection math reads a zero-size
rect. setFocused then yanked focus back, because its closest('.terminal-pane')
test counts the find bar's input as "already the terminal".
Fixed at four levels:
Source. updateSessionCliId early-returns on an unchanged id, and .sessionid joins
.name behind one write_if_changed helper in the generated statusLine script.
DOM. attachToContainer no-ops when the pane is already in the container, matching
the seven sibling attach* helpers that already did; the ordering concern it was
smuggling moves up to ensurePaneOrder in the layout owner, which compares
*relative* order (hidden panes stay in the container interleaved with the
laid-out ones) and so mutates nothing in the steady state. renderSwarmMode reuses
its grid wrapper instead of rebuilding it.
Focus. focusPane gates the call on shouldFocusPane, so a render that does not
change the focused pane never reaches setFocused at all — the find bar cannot be
told apart from the terminal down there, so the decision belongs one level up.
setFocused goes back to its original guard.
Resize. fitTerminal skips pty.resize when the cols x rows memo is unchanged; a
redundant resize makes the CLI redraw under a selection, and xterm clears the
selection outright on a row-count change. spawnTerminal clears that memo and
re-fits *after* pty.create resolves: callers fire it un-awaited and fit
immediately, so the first resize can reach main while pty:create is still
suspended (Copilot awaits a hook install) and resizePty drops it — the memo would
otherwise pin the PTY at the 120x30 spawn default for the session's life.
Both decisions live in DOM-free pane-order.ts / pane-focus.ts and are unit-tested.
The terminal-pane test fake also had className and classList as separate state and
an appendChild that did not move nodes; both are fixed, which is what let the
attach and fit tests be written honestly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012qptmPxqXxKn13WfPzFXXz
- **Renders must not touch pane DOM they don't need to.** `renderLayout()` runs on every `session-changed`/`layout-changed`, and `appendChild` on a node that is *already* a child is a remove-and-reinsert: it blurs whatever is focused inside (the Cmd+F find bar is appended *into* `.terminal-pane`, see `search-bar.ts`) and collapses an in-progress selection. So every `attach*ToContainer` no-ops when `element.parentElement === container`; DOM order among the panes laid out together is corrected separately by `ensurePaneOrder` (split-layout) via the DOM-free `isInRelativeOrder` in `components/pane-order.ts` — *relative* order, because hidden panes stay in the container interleaved with the visible ones — and `renderSwarmMode` reuses its `.swarm-grid-wrapper` instead of rebuilding it. Focus is guarded one level up, not by sniffing the DOM: `setFocused` cannot tell the find bar from the terminal (it is inside the pane), so `focusPane` gates the call on the DOM-free `shouldFocusPane` (`components/pane-focus.ts`) — a render that does not change the focused pane never calls `setFocused` at all, unless focus is sitting on nothing. `fitTerminal` skips `pty.resize` when the `cols`x`rows` memo is unchanged (a redundant resize makes the CLI redraw under a selection, and xterm does `clearSelection()` on a row-count change); `spawnTerminal` clears that memo and re-fits **after** `pty.create` resolves, because a fit racing the un-awaited spawn is dropped by `resizePty` for a session it has not registered yet (Copilot awaits a hook install first) and the PTY would stay at the 120×30 spawn default. Feeding this loop, `updateSessionCliId` (`state.ts`) early-returns on an unchanged id and the statusLine's `write_if_changed` writes `.sessionid`/`.name` only on change — the statusLine fires on every render, and each write used to cost a persist plus a full re-render.
68
69
-`session-activity.ts` — Tracks working/waiting/idle status with debounced transitions
69
70
- **Hook → session-state contract** — see `HOOKS.md` for the full map; it is verified against a specific Claude Code version and must be re-checked against https://code.claude.com/docs/en/hooks whenever hook handling changes. Three points are load-bearing and easy to get wrong. (1) **`PostToolUse` fires only on success**; a tool that ran and failed fires `PostToolUseFailure` (`error`, `is_interrupt`, `duration_ms`), and a call rejected before execution fires neither. Treating any non-empty `tool_response` as a failure — which this repo did until the events were re-verified — feeds every successful tool call into `missing-tool-detector.ts`. (2) **A `Stop` is not always a completion**: the main agent fires one every time it pauses on parallel subagents. `stop_status_writer.py` resolves it from the payload's `background_tasks` array, holding `working` only for `subagent`/`teammate`/`workflow` entries; only a *non-empty* array is authoritative — an empty one is not proof of an idle session, because the CLI filters that array on an `isBackgrounded` flag freshly-dispatched subagents don't carry yet — so empty *and* absent both fall through to the legacy `<sid>.subagents` counter. `session_crons` is never consulted or a `/loop` session would never complete. (3) **Every field name in `INSPECTOR_FIELDS` (`claude-cli.ts`) must exist in a documented per-event schema, and be read by something.** A compile-time assertion against `keyof InspectorEvent` catches the internal half of that drift; the external half — whether Claude Code actually sends the field — no type system can check, and an invented name silently renders a blank timeline row forever (how `config_key`/`question`/`answer` survived for months). Beware nested keys when reading the docs: the Elicitation example's `requested_schema.properties.username.type` scans like top-level `type`/`username` fields, and both were added on a previous pass and were dead on arrival. Generated Python embeds values via `pyLiteral` (`shared/python.ts`), never a raw `r'…'` literal, which breaks on an apostrophe in the path.
70
71
-`session-cost.ts` — Structured cost tracking via Claude CLI status line (`statusLine` setting), with regex fallback for older CLI versions. Provides per-session and aggregate cost data (USD, tokens, cache, duration)
0 commit comments