* Fix flaky e2e tests with improved locators and synchronization
Add explicit waits, use more robust element selectors, and simplify test
setup to reduce race conditions. Replace file-based fixtures with
programmatic browser creation, use parent-scoped locators for menu
interactions, and poll for stable state before assertions.
* Add E2E failure triage report for run 33564563164
- Reconciles 14 failed tests against job logs and trace artifacts
- Categorizes failures: 8 product bugs, 2 flaky tests, 4 test updates
- Documents test-maintenance fixes and diagnostic findings
- Files 8 Linear issues with owners and fresh recurrence evidence
- Provides next actions for product owners and repository maintenance
* rm artifact notes
* Refactor browser creation E2E test to use UI interactions
- Click through menu instead of manipulating internal store state
- Use Playwright's locator and toBeVisible() assertion patterns
* Record E2E browser creation pageId before barrier check
Move createdPageId assignment before the barrier arm/fire checks. This
ensures the pageId is recorded unconditionally when tracking is enabled,
allowing tests to distinguish between creations rejected before the host
attempt vs those that failed after creation.
* Remove browser page reclamation assertion from restart test
Simplifies test by removing page ID tracking and poll checking
if pages persist after paired runtime restart.
A visible remote/SSH terminal that has never run an agent inspected its
execution host every ~2s forever for a strictly negative answer — ~30
RPC round trips per minute per pane, each a network hop plus a host-side
foreground process scan.
The `no-evidence` 15s cadence tier exists to bound exactly that volume,
but `isProcessInspectionCostly` gated it on local Windows only and
explicitly excluded remote-execution-host PTYs — the most expensive
inspection shape in the codebase.
Extract the predicate to `agent-process-inspection-cost.ts` and treat a
remote-execution-host PTY as costly on every client platform. The local
branch (Windows costly, POSIX cheap) is byte-identical.
Client-side timer choice only: no wire change, no new field, no opcode.
Activity (output/title/hook) re-arms the 2s cadence, agent evidence
returns the tier to active/idle, and the `unavailable` branch and its
error backoff are untouched.
* fix(terminal): replay paired-runtime snapshots at the host's grid
A paired remote pane parsed the host's authoritative terminal image at
whatever grid its own xterm happened to have. The host dimensions every
snapshot it publishes, but only the REQUESTED snapshot path ever read
`cols`/`rows` back — both PUSH paths (initial subscribe and server
recovery) dropped them, so `onSnapshot` handed the transport an image
with no grid and the drain wrote it as-is.
Serialized frames are grid-relative: rows are newline-fed and the frame
ends in an absolute CUP. Parsed at a different grid they re-wrap and
clip, and because an alternate-screen TUI has no scrollback the rows
scrolled off the top are gone. An idle agent never repaints, so the pane
stays wrong until the next byte arrives — which for a finished Claude
Code session is never.
Carry the grid the host already publishes through the multiplexer and
transport, then reuse the choreography the reattach payload already
follows: resize to the source grid, replay, fit back to the pane, and
push the resulting grid to the PTY. A host that publishes no dimensions
reads as unknown and keeps today's behaviour, so no wire change and no
capability negotiation is involved.
* fix(terminal): keep the source-grid fit correct under mobile fit overrides
Two follow-ups on the source-grid replay:
- A mobile fit override skipped the post-replay fit entirely, stranding
the pane at the host's replay grid. Fit without the PTY grid push
instead, matching applyMainBufferSnapshot.
- Reset the source-grid flag when a drain is scheduled: a transaction
whose restore was skipped never runs afterRestore, and the stale flag
would fit a later drain that never left the pane's own grid.
* perf(terminal): clear the replay buffer before the source-grid resize
The drain resized xterm to the host's serialization grid and only then
wrote the clearing `2J`/`3J`/`H`. `clearBeforeReplay` is true for every
pushed remote snapshot, so a column change reflowed a full scrollback
that the next sequence discarded microseconds later — on the recovery
push that lands under output flood, when the renderer is already loaded.
The clear is grid-independent, so running it first is equivalent: the
resize then operates on an empty buffer. Verified identical end state
(content, cursor, buffer type, baseY) across cols-change, rows-change,
alt-screen, no-scrollback and equal-grid shapes. Interleaved 25-run
medians on a 10k-line scrollback: 6.19ms -> 2.48ms on the normal buffer,
unchanged on the alternate screen (where `3J` cannot free the normal
buffer's history, so the reflow is paid either way).
`src/shared/process-table-snapshot.ts` is 308 code lines against the 300
cap for `**/*.ts`, so `static analysis` is red on `main` and every open PR
inherits it.
Neither PR that grew the file crossed the cap alone. #18151 took it to 427
raw lines; #18166 added ~35 more. #18166's branch predated #18151, so the
head CI linted was 428 raw lines and passed, while the squash onto main is
463 -> 308 code lines. The gate lints the PR head, not the merge result, so
nothing linted the sum until it was on main.
Pure move, no behaviour change: the generic index machinery
(ProcessIdentityRow, ProcessTableIndexOf, buildProcessTableIndex,
collectDescendantsFromIndex, lookupProcessTableIndex, getProcessTableIndex
and its WeakMap) moves to process-table-index.ts. `ProcessTableIndex` and
`scoreForegroundCandidateRow` stay behind because they need
`ProcessTableRow`, which keeps the new module free of any import back and
so introduces no cycle.
* fix(native-chat): show pasted images while they save, and make them previewable
Pasting an image into the native chat composer showed nothing until the
clipboard image finished being written to disk, and the resulting chip could
never render the image at all.
Preview was blocked by path authorization, not by rendering. Clipboard pastes
are written to the OS temp dir, which sits outside every allowed root, so the
composer's own `fs:readFile` of the file Orca had just written was denied.
`saveClipboardImageBufferAsTempFile` now authorizes the path it writes, the
same way other Orca-produced external files are handled.
The delay is the macOS paste route: Cmd+V is intercepted in main and delivered
through the app-menu paste channel, which has no clipboard blob in hand, so the
composer only learned an image existed after the save round-trip. A new
`clipboard:readImageThumbnail` probe reads the clipboard in memory and returns a
downscaled preview; it runs alongside the save rather than before it, so text
paste gains no latency. The DOM-paste route needs no probe — it mints a blob URL
from the clipboard file on the same tick.
Attachments now carry `pending` and `previewUrl`: the chip appears immediately
with the real image dimmed under a spinner, then settles in place on the saved
path. Send is blocked while anything is pending, because a pending chip has no
agent-readable path yet. Pending chips are kept out of the pane attachment cache
so a mid-save unmount cannot strand one, and blob previews are revoked on
remove/clear. SSH pastes now carry their connectionId onto the chip so remote
previews read over SFTP.
Verified in a real Codex native chat under an isolated dev instance: the chip
appears in 42-61ms with a spinner, settles at ~141ms, three rapid pastes produce
three independent chips with Send disabled throughout, and the lightbox opens the
full 5120x2880 image read from disk. Ablation confirms the authorization fix:
the written path reads back, an unauthorized sibling in the same temp dir does
not.
Claude-Session: https://claude.ai/code/session_01NnEfY8NpfFtVnboLKnmgdW
* fix(native-chat): avoid stale image attachments and preview cache growth
---------
Co-authored-by: Merge Sim <sim@local>
* Make agents activity always-on; toggle via bell icon
- Remove optional showAgentsSidebar setting
- Replace sidebar view-toggle with bell-button for activity access
- Agents activity now always accessible in sidebar
- Preserve migration flag for introduction to existing users
- Remove visibility inference utilities
* Simplify sidebar when agents view active: hide workspace options, add to
- Hide workspace options menu and add project button when agents view is
active, reducing UI clutter in that mode
- Add tooltip to the activity bell button for better discoverability
- Localize sidebar search field text
- Move search and filter toggles to local state in SidebarAgentsList,
removing unused callbacks from thread list components
- Manage search input focus properly when opening
* Add automation runs dashboard with pagination and filtering
Adds a new Runs view in the Automations page that lets users browse all runs across automations with status/host filtering, search, and pagination support. Includes virtualized table rendering for efficient handling of large run histories and summary cards showing 24h/7d success/failure counts.
* Fix missing dependencies in useCallback hooks and imports
Missing dependencies in useCallback can cause stale closure bugs. This
adds missing state setters to dependency arrays and consolidates type
imports for consistency.
* Use keyset pagination for stable automation runs pages
Pagination now uses createdAt:id boundaries instead of offsets, so new
runs arriving between pages don't shift the window. Maintains backwards
compatibility with legacy offset cursors.
Move pagination to shared module, fix outcome counting for future-dated
runs, and improve hook state tracking on authority re-pairing or target
changes.
* Extract automation run details to top-level page view
Moves run display from detail pane to dedicated page, establishing
three-level navigation (Automations → Runs → Run Details) and simplifying
the detail pane component.
* Fix pagination stability when automation runs share createdAt
- Define a stable total order with createdAt and id tiebreaker to prevent runs tied on createdAt from being dropped when the boundary run is pruned between page requests
- Retain cursor on failed pagination so pages remain retryable
- Update ownerNotice type to AutomationActionNotice
* Extract automations list panel and worktree map logic
Split AutomationsPageSurface into smaller, focused modules for better maintainability and reusability. Move list panel UI rendering to AutomationsPageListPanel component and worktree map selection logic to a standalone utility function.
* Add i18n strings for automation runs dashboard
Adds localized strings for the automation runs dashboard view, including search, filtering by host and status, run counts for 24h/7d windows, and empty state messaging across all supported languages.
* fix missing translation
* fix missing translation
* perf(renderer): stop re-running useRef initializers and ref-mirror effects every render
React evaluates the argument you pass to `useRef` on every render and discards
every result after the first. 30 renderer sites did real work in there — walking
every browser page/tab across all worktrees, building activation-order maps, and
minting `crypto.randomUUID()` per render on browser pages and the AI vault.
Also moves 12 verbatim ref-mirror Effects to render-phase assignment, and routes
the `tab.rename` shortcut straight to the focused tab instead of through a store
field every mounted tab subscribed to.
* perf(renderer): drop Fix 2 (render-phase ref mirrors) to satisfy no-ref-current-in-render
* perf(renderer): convert the four lazy-useRef sites that landed on main
Readiness-review follow-ups to #18144.
The parity test in closed-editor-tab-disposal.test.ts cannot see prefix bleed:
buildScenario closes tab-0..tab-99, so tab-10 is in the closed batch too and the
per-tab oracle disposes its models via tab-10's own prefix. Batched and oracle
agree and the assertion passes even with a bleeding predicate. Verified by
mutation: replacing the boundary probe with a naive startsWith leaves all five
of that file's tests green.
Adds a test through the batched disposeClosedEditorTabs entry point with a
still-OPEN tab-10 alongside a closed tab-1, which does fail under that mutation.
Also records the `boundary + 1` advance in hasPaneScopeOwner as load-bearing for
`:::` runs, with a test that fails under a `+ 2` "tidy-up", and removes
disposeUnattachedMonacoModelsByPathPrefix, which #18144 left with zero production
callers and a comment claiming it was kept for callers that do not exist.
Finally, documents why title-derived rows carry `startedAt: 0`, which is the sole
reason their `now` stamps cannot move a dashboard bucket.
`checkOrcaStarred`, `starOrca` and `getAuthenticatedViewer` were the only gh
call sites that reached for the legacy `execFileAsync` instead of
`ghExecFileAsync`, so they ran with no deadline, no process-tree kill and no
coalescing. A `gh` that never exits therefore ran forever and never released
its slot in the 4-wide GitHub semaphore in gh-utils.
Route all three through `ghExecFileAsync`, coalesce concurrent star checks onto
one child, and hoist the Landing star-state effect out of the conditionally
rendered footer so a repo-catalog rewrite no longer remounts it and re-forks gh.
Adds a ratchet test asserting no file outside the command runner names `gh` as
a spawned program.
Fixes#18234
Both PR files viewers rebuilt the section-index Map with
`useMemo(..., [sections])`. An on-demand section load replaces the sections
array while the section keys stay identical, so every load handed the file
tree a new Map identity — a memo miss for all ~900 `CombinedDiffFileTreeRow`s.
`useCombinedDiffTreeNavigation` already cached the map behind an
entry-signature + per-index key comparison. Extract that into
`useCombinedDiffSectionIndexMap` and use it from all three call sites. The
extracted hook seeds its cache from a `useLayoutEffect` rather than during
render, so a render React discards cannot leave behind an entry describing
sections that never committed.
Forced foreground repaints asked xterm for rows 0..rows-1. xterm's render
debouncer unions ranges, so one full-grid request widened every frame to a
whole-viewport `_updateModel` cell walk even when the write changed five rows.
Re-issue the repair over the parse's own dirty span instead, keeping the
whole grid for viewport scroll, alternate-screen flips, and any write whose
span cannot be observed.
The POSIX process-table capture ran `execFile('ps', ...)` with no `maxBuffer`,
inheriting Node's 1MB default. Measured at 1,460 processes the capture is 326KB
with a 5,116-char longest row — ~3x headroom, which a busy host clears.
Two separate defects follow, fixed here:
1. `parseProcessTableRows` drops unparseable lines, so any short capture reads
as a COMPLETE table whose missing processes simply are not running. Verified:
a capture cut at 4KB parses to 59 of 1,463 rows, and an empty capture parses
to `[]`, both with no error — and `resolveAgentForegroundProcessWithAvailability`
then answers `available: true`. That is the `unverifiable` -> `exited` collapse
the execution boundary forbids. The capture now rejects with
`ProcessTableCaptureError` on a ceiling-length or row-less capture, so both the
lenient and strict views fail loudly and callers report unavailable.
2. `maxBuffer` is now an explicit 32MB, matching the sibling reader in
`pty-descendant-termination.ts` and its stated reasoning. Without it a 4,000-
process host fails EVERY capture, degrading the whole subsystem permanently.
Separately, `readStructuredTuiProcessIdentity` polled a fresh whole-machine `ps`
every 50ms for up to 5s. Each capture costs ~0.065 CPU-s, and the 5s ceiling is
only reached when the child never appears — where the tight interval buys
nothing. The interval now holds at 50ms for the first second, then doubles to a
500ms cap. Identification latency is unchanged for any child appearing inside
that window, and the 5s ceiling is unchanged.
`runHistoryGc` walked every terminal-history directory synchronously ten
seconds after launch: `readdirSync` on the root, then per directory a
`statSync`, a `readdirSync`, a `statSync` per file, an `existsSync`, a
`readFileSync` and a `JSON.parse`. On a real 613 MB root (2,776 dirs /
6,703 files) that is ~20,000 syscalls and 2,774 parses in one
uninterruptible pass — the main process was frozen for the whole of it,
7.5-10.6 s on the reporting machine.
Move the enumeration to `fs/promises` behind the existing
`forEachWithConcurrency` fixed-worker pool over an iterative frontier,
yielding through the shared `yieldToEventLoop()` every 32 entries. The
pass is cancellable and a second call joins the in-flight one rather
than racing its tombstone renames.
Max main-thread gap over the real root: 122-155 ms -> 1.1-1.9 ms idle,
522 ms -> 1.1 ms under load. Syscalls per pass 20,578 -> 17,803. Total
elapsed is lower too (88-108 ms vs 126-154 ms warm), so nothing is
smeared into a longer tail.
The prune decision logic and the tombstone path are untouched. A new
suite asserts the new walk removes exactly the set the synchronous walk
chose over a fixture covering every decision shape, and covers the races
async introduces: a directory removed mid-walk, a half-written
`meta.json`, and malformed/truncated/oversized metadata. All of those
resolve to "keep", matching what the sync version did on a read error.
* perf(startup): stop an unreachable SSH host from gating local terminal restore
An asleep or unreachable SSH target held the terminal-restoration gate for the
full 15s reconnect timeout, so no terminal restored — local ones included.
Startup now awaits only the target that owns the active workspace's tabs and
lets the rest connect in the background, folded into the existing deferred path
that reattaches their PTYs on tab focus.
Also splits the renderer's git-environment fence out of the first-window PTY
services barrier: worktree hydration needs shell-PATH generation and the managed
WSL CLI registration, not a daemon PTY spawn or a hook-server bind. Terminal
restoration still fences on the first-window services via
app:prepareTerminalStartupRestoration.
Measured with tests/tools/benchmarks/startup-time-bench.mjs (382 restored tabs,
28k-file profile, medians of 3):
unreachable SSH host: 17.27s -> 1.34s to renderer-startup-hydration-done
all-local: 1.98s -> 1.33s
* fix(startup): restore the startup-ordering oracle and keep a connected background SSH target undeferred
app-startup-routing.test.ts pinned the old step names, so the two ordering cases
went vacuous-then-red when the barrier split. Repoint them at the steps that now
carry the same fences: 'git-environment-barrier-await' (shell PATH + managed WSL,
the fence host Git needs) before hydration worktrees, and
'prepare-terminal-startup-restoration' (which awaits firstWindowStartupServicesReady
in main) before terminal reconnect. Both still fail against main's hydration source.
Also: the timed-out-eager rewrite of the deferred list re-added background targets
that had already connected, undoing removeDeferredSshReconnectTarget and sending
fresh panes on a reachable host down the cold-restore path.
A retained hidden pane keeps a live WebglRenderer, and the only thing that
stops its 600 ms cursor-blink timer is a real DOM blur event. Today that
arrives incidentally from display:none/visibility:hidden; under a hide mode
that keeps focus (opacity:0 without inert) it never fires and the pane blinks
— redrawing its whole cursor row per toggle — until the 5-minute idle timeout.
Park terminal.options.cursorBlink on suspend and restore the parked value on
resume, so the property holds regardless of which CSS hid the pane. Settings
writes land on the parked value while hidden, so a mid-hide settings change
cannot re-arm the timer behind the surface, and a user who disabled blink
never gets it back.
Two costs on the Windows process-table hot path, plus the EDR doc that
described neither of them accurately.
1. The snapshot set `ProcessDataFlag.Memory` and surfaced `memoryBytes`,
which nothing read. The addon serves that flag with a second
`OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ)` and a
`GetProcessMemoryInfo` per process (process.cc:47-63), so the flag was
one wasted handle per process per snapshot.
2. The shared TTL cache gave every pane the same native rows array, but
each pane still ran `native.map(toProcessRow)` over the whole table,
rebuilt a `childrenByPpid` Map from scratch, and did two linear scans.
The `.map()` also handed `getProcessTableIndex` a new array each call,
defeating the POSIX memo by construction. Both now cache per snapshot
identity, and the POSIX resolver drops its duplicate descendant walk.
`getProcessTableIndex` / `buildProcessTableIndex` are generic over the row
shape so the Windows rows reuse the existing pass instead of a parallel one.
No behavior change: same rows in, same rows out, same descendant ordering
and same has-children answers.
* perf(renderer): reconcile hydrated workspaces in one store write
Session hydration reconciled each workspace with its own set(), so a
193-workspace session fanned 193 writes out to every non-React store
subscriber and re-spread three whole workspace-keyed maps per workspace.
Fold the whole session into one patch, release the string-keyed terminal
scroll-intent entries on pane close, and drop the per-workspace/per-tab
reconnect debug logs.
* fix(test): make the hydration fixture bucket switch exhaustive
oxlint --type-aware flags the default arm; naming the editor case clears it.
Zustand reruns every subscriber's selector on each store write. Three
selectors did an O(N) scan of a store collection inside that path, so at
10 repos / 423 worktrees / 382 tabs they were paid thousands of times a
second while the app sat idle.
- getLocalWorktree / getLocalRuntimeRepoForWorktree now read the shared
WeakMap indexes (getIndexedWorktreeById, getIndexedRepoMap) instead of
`Object.values(worktreesByRepo).flat().find(...)` and `repos.find(...)`.
SidebarTaskNavButton is always mounted and calls this on every write.
- selectRepoByIdForActiveWorkspace caches its host-scoped resolution in a
WeakMap keyed on the `repos` array, mirroring getIndexedRepoMap.
- getProjectRuntimeSessionSummary memoizes per (tabsByWorktree,
ptyIdsByTabId, agentStatusByPaneKey, repoId) and reuses the existing
identity-cached getTabIdToWorktreeId index.
Zustand re-runs every mounted subscriber's selector on every store write. The
per-worktree sidebar selectors built a fresh Record per call, so 15 visible
cards x 6 reads x every write allocated a record each time even when nothing
they read had changed.
- Add createWorktreeRecordSelector: gates the build on the source slice
identities, memoizes per worktree id, and carries the previous generation
forward so a rebuild with equal contents keeps its reference.
- Route the pane-title, live-PTY, layout-root and terminal-layout selectors
through it, and return a shared frozen empty when a worktree has no tabs.
- Swap useWorktreeAgentRows' inactive-branch `[]`/`{}` literals for the shared
frozen constants so the `active` gate actually short-circuits on identity.
- Identity-cache the sidebar pending-worktree-creation key list, which ran
Object.values(...).map(...) from an always-mounted subscriber.
- Drop `key={text}` from TruncatedSidebarLabel so a label change remeasures in
place instead of remounting the span and rebuilding its ResizeObserver.
- Remove the non-compositable `width` from the board drop indicator's
will-change hint.
`window.api.platform.get()` runs ~19x/sec while the app is idle. Every call
recomputed a payload whose fields are all fixed for the process lifetime
(`process.platform`, `process.getSystemVersion()`, `process.arch`, the shell
env vars, and the env-derived Linux display server), allocated a fresh object,
and crossed the context bridge.
Memoize the payload lazily at preload module scope and freeze it, and cache the
resolved platform in `getRendererAppPlatform()` so the 32 renderer call sites
stop crossing the bridge on every render. The user-agent fallback stays uncached
because the web client installs its platform API after boot.
* Move Copy Session ID from tab to terminal pane context menu
- Relocates session ID copy to the exact pane that owns it, not the tab's active pane
- Adds support for durable sleeping agent sessions as fallback for cleared live status
- Generalizes copy-rejection guards to handle any identity type, not just pane IDs
- Updates e2e test to verify pane-specific session ID copying
* Gate session ID liveness by shell foreground state
Once OSC 133;D proves a pane is back at the shell, don't return the
session ID even if a durable record survived the exit. This prevents
treating exited sessions as still active when the user is typing at
the prompt.
* Update hook order parity test for session-ID projection hook
The pane session-ID projection adds a render hook to TerminalPane.
Update the expected hook count from 229 to 230 and the corresponding
SHA256 hash.
Every debounced save stringified the full persisted state, then ran two
`String.replace` passes per secret sentinel — one for the on-disk payload, one
for the guard hash. Each replace returns a rope the next one has to flatten
before it can search, so three sentinels cost seven flattened copies of a
4.65 MB state (a two-byte V8 string, ~8.9 MB each), and the state was then
UTF-8 encoded twice more: once inside `sha1.update(string)` and again inside
`handle.writeFile(payload, 'utf-8')`.
`applySecretSentinelSubstitutions` walks the state once with a single
alternation regex, encodes each literal run to a Buffer exactly once, and feeds
those same buffers to both the payload and the hash. Measured on the author's
4.65 MB store with three live secret slots: 48.8 MB -> 17.9 MB allocated per
save, 26.6 MB -> 0 of large_object_space churn, and 22.1 -> 15.1 ms (min) /
32.3 -> 16.9 ms (median) for build+hash+encode. Bytes on disk and the guard
hash are proven identical to the previous loop.
Separately, non-local host session partitions carried stale replicas of the
`browserUrlHistory` global — 589,807 bytes, 12.7% of the file — that neither
the split (which writes globals only to 'local') nor the merge (which reads
them only from 'local' unless local has none) can ever reach. The load path now
drops them when the local slice already holds the field. Only the two history
globals are dropped: the rest are read out of every partition by the worktree
ownership sweep or the mobile/runtime projections.
The always-mounted terminal controller looped every workspace surface (423 on
a large profile) and called syncParkedTerminalTabWatchers per surface; that
function scans both module-level registries in full, so one effect fire cost
surfaces x registry — 323,172 map-row visits at 423 workspaces / 382 tabs.
Add syncParkedTerminalTabWatchersForWorkspaces, which walks each registry once
and then runs the per-tab start/reconcile pass; the single-worktree entry point
delegates to it. Registry rows are tab-id keyed and a tab belongs to exactly
one worktree, so hoisting the dispose and capture sweeps ahead of the start
passes only reorders work across disjoint tab sets.
Also derive workspaceSurfaceIds/workspaceSurfaceIdSet once in the workspace
foundation (through the existing useReusedArrayIdentity) and key the watcher,
parking and browser-retention effects on the id array instead of the surface
array, which is re-identified on every worktree write. And pass the sidebar's
already-computed defaultHostId into useVisibleSidebarWorktrees so an unrelated
settings write stops re-running the 423-worktree visibility scan.
Main re-asserts a working OSC title per pane every 80ms (12.5/sec) while an
agent works, and every frame became its own pty:sideEffect IPC message. Both
renderer store writes already discard those frames via
isDecorativeAgentTitleFrameChange, and paired remote clients already never see
them (RuntimeClientEventBus's per-listener title gate). Only the local desktop
renderer was still paying for them.
Apply the same decorative gate main already computes for the mobile fan-out one
hop earlier, keeping a 500ms heartbeat so the renderer's 1500ms hook-done quiet
window still sees a working title and can cancel a Pi/OMP milestone 'done'.
`useRuntimeGraphSync` is mounted unconditionally, and its projection layer runs
on every store write. Four of those projections did work proportional to the
whole slice rather than to what changed:
- `buildRuntimeMobileEditorDraftsProjection` FNV-hashed every open dirty draft
on every `setEditorDraft`, which Monaco fires per keystroke with no debounce.
- `buildRuntimeMobileOpenFilesProjection` and the browser projection rebuilt and
re-stringified everything on any `isDirty`/title/url/loading change.
- The agent-status sort built an ICU collation per comparison for a string that
is only ever compared with `===`.
Each now memoizes per entry against the previous build, mirroring the tabs and
agent-status projections that already did. The duplicated draft-hash loop in
`mobile-session-inputs` is gone; both consumers share one memo.
The session-write subscriber also identity-scans SESSION_RELEVANT_FIELDS before
allocating its 35-field snapshot and changed-field array.
Projections are byte-identical apart from the agent-status sort order, which is
never displayed.
Idle-app CPU profiling showed `titleHasAgentName` running 11,771x/sec and the
legacy any-agent regex 4,399x/sec, roughly once per zustand subscriber notify.
The regexes were already precompiled; the problem was call volume — every store
write re-classified every unchanged pane title through the whole agent-name
ladder.
Every title classifier is pure in the title string, so memoize them on it
(bounded FIFO, 1024 entries). A new title is a new key, so there is no staleness
window. The same profile showed the sidebar lineage projection re-scanning all
worktrees several times per pass; cache it on the identity pair of its two
immutable inputs, mirroring store/worktree-repo-index.ts.
Shortcut labels were rebuilt from scratch in the render body of every
component that shows one, which kept parseKeybinding running ~120x/sec
in a fully idle app.
- Cache the label layer per overrides object (WeakMap), so a keybinding
edit hands out a new object and therefore a fresh cache.
- Memoize parseKeybinding behind a bounded cache; binding strings come
from a fixed definition set plus user overrides.
- Hoist the per-call token/label object literals in normalizeKeyToken
and formatKeyToken to module constants.
Closing N diff tabs scanned the global Monaco model registry 2N times and
rendered both URI forms for every retained model on each scan. The Source
Control panel opened 42 store subscriptions from one hook, 40 of which watched
action identities that are fixed at store construction and can never change.
commentableLineSet was memoized on array identity. Review surfaces hand the
decorator a fresh-but-equal number[] on every PR/MR data refresh, so the set
churned, tore down the overlay+zone effect (unmounting every comment card's
React root and clearing the zone map) while the zone-creating effect — which
does not depend on the set — never re-ran. Monaco kept the view zones as
untracked blank gaps, and the next refresh stacked more on top.
- memoize the set on a joined value key so equal refreshes are a no-op
- split the add-button overlay (needs the set) from the zone teardown (must
not), so the teardown's deps stay a subset of the zone-creating effect's
- have the teardown actually removeZone what it stops tracking
Four copies of the same loop ran `git remote` and then a serial
`git remote get-url <name>` per remote to answer "which remote has this
URL". On a repo with 58 remotes that is 59 subprocesses -- measured at
1083 ms -- for one question, and worktree create asks it several times.
`git remote -v` answers for every remote from one child, reporting the
same insteadOf-expanded first fetch URL `get-url` prints.
The batched `cat-file --batch-check` branch-conflict probe decides from
stdout, but its WSL route was unfenced, so a login-shell fallback printed
the distro banner onto the stream it parses. That broke the
one-line-per-ref contract, made every batch undecided, and fell straight
back to one `show-ref` per remote -- the cost the batch exists to remove.
Measured at 58 remotes / 4346 branches, spawns and wall time:
push-target remote scan 59 -> 1 (1083 ms -> 8 ms)
branch-conflict probe 60 -> 3 (984 ms -> 43 ms)
configured push target 123 -> 6 (2707 ms -> 157 ms)
Both changes shipped in #18055 were written against strings never observed
in a real session, and neither fixed a reported problem. Guessing at agent
output we have not seen is how the picker got a row that silently no-ops.
Fable consent detection is removed outright. It watched the session for
"Fable N uses usage credits and needs a one-time consent" and answered
`interaction-required`. No consent prompt appeared in any validation run —
the test account had already consented — so the matched wording was never
confirmed. With the detector gone nothing produces `interaction-required`,
so the outcome leaves the union and its unreachable handler goes with it.
A real consent prompt now reports the switch as unverified, which is the
honest failure mode for output we cannot recognize.
The weekly usage scope goes back to exact `display_name === 'fable'`. It
had been widened to `/^fable\b/` against a hypothetical rename of
Anthropic's own usage window; the API still reports "Fable", so the match
was insurance against a scenario with no evidence behind it.
Tests covering the removed behavior are deleted rather than rewritten,
including the two pre-existing `interaction-required` cases that asserted
the terminal is revealed.
The disabled-row filter from #18055 is deliberately untouched.
Claude-Session: https://claude.ai/code/session_01SJy4XGrdre6YaU1wYNKak4
Co-authored-by: Merge Sim <sim@local>