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 e8e2cea
Browse filesBrowse the repository at this point in the historyBrowse files
Copy file name to clipboardExpand all lines: CLAUDE.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -68,7 +68,7 @@ CLI-specific behavior is encapsulated behind a `CliProvider` interface (`src/mai
68
68
- **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.
69
69
-`session-activity.ts` — Tracks working/waiting/idle status with debounced transitions
70
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.
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)
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). Both this module and `session-context.ts` feed the terminal pane's **status rail** (right cluster = `profile · model · $cost | in/out`, left cluster = the context meter) off the *same* `session:costData` payload — Claude's statusLine writes `{cost, context_window, model}` into one `<sid>.cost` file; there is no `.context` file. Two things make the rail fragile on restore, and both are load-bearing. (1) `setCostData`/`setContextData` **dedupe on unchanged values** (the statusLine rewrites `.cost` on every render, and each event costs a persist + re-render), and `restoreCost`/`restoreContext` seed those maps **silently** from the persisted `SessionRecord.cost`/`.contextWindow` at load. A resumed session reports exactly the context that was persisted at quit, so the first post-resume payload is deduped away and `onContextChange` never fires — cost only self-heals because *its* dedupe set includes the ticking `totalDurationMs`. (2) The rail's left cluster is built lazily inside `updateContextDisplay` and hidden by CSS `.context-indicator:empty`, so "no event" renders as "no meter", not as a stale one. `createTerminalPane` therefore **pulls both clusters from `getCost`/`getContext` right after `instances.set`** — the general invariant being that *a store with a silent `restore*` seeder must be pulled by any consumer that builds its DOM lazily; a push-only consumer is correct only if its DOM provably predates every event.* (Every other consumer of these two stores — board cards, the sessions widget, `refreshProfileLabels` — already pulls, so the pane rail was the lone exception.) The cost pull is unconditional so that `updateCostDisplay` stays the **single renderer** of `.cost-display`, painting the `$0.0000` placeholder itself when cost is null; the pane constructor only toggles `.hidden` off the provider capabilities. `ArchivedSession` persists `cost` but no `contextWindow`, so resume-from-history still starts both empty
72
72
- **Session auto-naming** — `session-title.ts` adopts the CLI's own session title as the tab name, so users don't have to rename manually. The title arrives as the **`session_name` field of Claude's statusLine stdin payload** (the custom name from `--name` / `/rename` when one exists, otherwise Claude's AI-generated topic title; the key is absent until the session has one). Hook payloads carry no title — verified against the 2.1.237 binary, whose base hook schema is `{session_id, transcript_path, cwd, prompt_id, permission_mode, agent_id, agent_type, effort}` — so the statusLine is the only live source. `buildStatusLinePython` in `src/main/hook-status.ts` builds one body used on every platform; `installStatusLineScript` writes it to `~/.vibeyard/run/statusline.py` and the `.sh`/`.cmd` wrapper invokes it **by path**, never inlined into the shell command (see the `hook-commands.ts` module docstring for why inlining Python here is fragile). It writes `<STATUS_DIR>/<sid>.name` as `{"name": "…", "session_id": "…"}`, keyed on `CLAUDE_IDE_SESSION_ID`; the watcher forwards it as `session:sessionName` → `applyCliSessionName`, which skips sessions with `userRenamed` (a Vibeyard rename is sticky and always wins), drops a title whose `session_id` no longer matches `session.cliSessionId` (a stale `.name` survives `/clear` until the next render, and `resyncAllSessions` replays every file on window activate), and is gated on `preferences.autoTitleEnabled`. Three script details are load-bearing: it writes **only on change** (statusLine fires on every render, and each write costs a persist + full tab-bar re-render) — which is why flipping `autoTitleEnabled` back on sends `session:resyncStatus` to replay every status file, since an unchanged `.name` produces no further fs event and `app.on('activate')` is effectively macOS-only; it uses `json.dumps` so a CJK/emoji title is ASCII on disk (a raw write hits Windows' locale codepage, raises `UnicodeEncodeError`, and would take the `.cost` write down with it); and `STATUS_DIR` is embedded via `JSON.stringify`, whose escapes are all valid Python ones — a raw `r'…'` literal breaks on an apostrophe in the path (a Windows user named O'Brien), and a `SyntaxError` there silently kills cost, context, sessionid and name at once. `.name` is a **provider-facing channel**, not Claude-private — any provider can write that file the way `codex-session-watcher.ts` already writes `.sessionid` without hooks; today only Claude does, so non-Claude sessions no longer auto-name (this replaced an approach that scraped Claude's box-drawing chrome out of raw PTY bytes). Default names come from `src/renderer/state/session-naming.ts`: `nextNumberFor(key, project)` counts the highest existing number across sessions *and* history for **every locale's template** of that i18n key, with `nextSessionNumber` / `defaultSessionName` as the `Session {num}` wrappers and the MCP inspector tabs numbering independently off the same helper. This replaces three separate `length + 1` formulas, which reissued a number already on screen after a middle tab was closed.
73
73
- `components/active-sessions-panel.ts` — Global, cross-project **Active Sessions** list rendered into `#sidebar-active-sessions` (a persistent sidebar block above Discussions). `selectActiveSessions(projects, statusOf, activeStatuses)` is a DOM-free selector that collects open CLI sessions (`isCliSession`) across `appState.projects` whose live `getStatus()` is in the configured set, ordered by `STATUS_PRIORITY` (from `project-status.ts`, now exported) then project name. `initActiveSessions()` subscribes to `session-activity.onChange` + the relevant `appState` events and re-renders; rows route clicks through `setActiveProject` + `setActiveSession`. Visibility is gated by `preferences.sidebarViews.activeSessions` **and** requires more than one project (with a single project the rows just duplicate its own tab bar); the "active" status set comes from `preferences.activeSessionStatuses` (`{ working, waiting, input, completed }`, default working/input/completed via `resolveActiveStatuses`). Both preferences are edited in the Preferences → Appearance section. Styles live in `styles/sidebar.css`; status dots reuse the existing `.project-status.<status>` classes.
74
74
-`components/git-panel.ts` — Git changes for the **active project**, rendered inside its sidebar card as a `git` panel-toggle tab (a third `ProjectPanel` alongside `history`/`files` in `sidebar.ts`), not a standalone bottom panel. The "Git" tab carries a `.project-action-badge` count of total changes (badge + click-to-open); `Cmd/Ctrl+Shift+G` (`toggleGitPanel`, exported from `sidebar.ts`) toggles it. `mountGitPanel(project, container)` reparents a single persistent node into `.project-panel-git` so file rows/scroll survive sidebar re-renders; `closeGitPanel()` detaches it. The shared `loadFiles`/worktree-selector logic is unchanged; `sidebarViews.gitPanel` now gates the tab button. There is no `#git-panel` node in `index.html`.
0 commit comments