Preview links (detected HTML files / localhost dev URLs) were rendered as
CHILDREN of the background StatusSection, which is collapsed by default — so the
moment a background task appeared, the previews got swallowed into the collapsed
"N Background" expandable and vanished until you manually expanded it. With no
background group they rendered as a standalone always-visible block, so the bug
only showed once a bg task was running.
Render the preview links as their own always-visible block right after the
background section instead of as collapsible children. They stay visually
associated with the background group (a localhost dev server and its preview are
the same thing) but are no longer hidden by its collapse — a one-tap open is the
whole point.
After the slash dispatcher, the next-largest body unit was submitPromptText —
a ~280-line submit pipeline. Lift it into a colocated useSubmitPrompt sub-hook
(use-prompt-actions/submit.ts) with a typed SubmitPromptDeps object; body moves
verbatim. SubmitTextOptions moves to utils.ts (shared by submit + submitText).
Pure restructuring, no behaviour change (full use-prompt-actions suite green).
index.ts: 1,212 -> 937.
The remaining bulk of useMessageStream was handleGatewayEvent — a ~550-line
event-type dispatcher. Lift it into a colocated useGatewayEventHandler sub-hook
(use-message-stream/gateway-event.ts): the values it closed over (sibling
streaming callbacks + the 3 stable refs the deps array omitted + options)
become a typed GatewayEventDeps object; the dispatcher body moves verbatim.
Pure restructuring, no behaviour change (utils tests still green). index.ts:
1,120 -> 540.
The usePromptActions body's largest unit was executeSlashCommand — a ~530-line
`/command` dispatcher. Lift it into a colocated useSlashCommand sub-hook
(use-prompt-actions/slash.ts): the ~13 values it closed over become a typed
SlashCommandDeps object the parent passes in; the dispatcher body (and its inner
runSlash recursion) moves verbatim. SlashActionCtx (slash-only) moves with it.
Pure restructuring, no behaviour change (verified: full use-prompt-actions test
suite still green). index.ts: 1,772 -> ~1,250.
Extract the standalone gateway-event helpers (session-info patch derivation,
completion-error detection, todo-payload routing, delegate_task -> subagent
spec mapping, + the stream-flush/subagent-event constants) out of the
1,285-line hook into a colocated, tested use-message-stream/utils.ts. index.ts
keeps the stateful streaming hook and consumes the helpers.
Pure restructuring, no behaviour change; folder index keeps the import path
intact. index.ts: 1,285 -> ~1,120. Adds unit tests for the pure helpers.
Extract the ~16 standalone helpers (message reconciliation, optimistic/resolved
session upserts, stored-session resolution, runtime-info application, error
classification) out of the 1,254-line god hook into a colocated, tested
use-session-actions/utils.ts. index.ts keeps the hook orchestrator (the
stateful action callbacks) and consumes the helpers.
Pure restructuring, no behaviour change; folder index keeps the import path
(`@/app/session/hooks/use-session-actions`) intact. index.ts: 1,254 -> ~950.
Adds unit tests for the pure helpers.
The top-center floating HUDs (command palette + session switcher) pin at
top-3, overlapping the titlebar's `[-webkit-app-region:drag]` bands. Drag
regions win hit-testing over the DOM regardless of z-index, so the top of
each surface — the search input — swallowed clicks, leaving only a ~2px
strip focusable. Add `[-webkit-app-region:no-drag]` to the shared
HUD_SURFACE so the whole surface is interactive.
Finding 2 of the desktop UI-consistency pass. Several surfaces intentionally
make an entire row/cell the click target while hosting nested layout inside a
raw <button> (each re-justifying the pattern in a local comment). Introduce a
zero-style RowButton primitive (components/ui/row-button.tsx) that bakes in the
shared semantics — type="button" + a stable data-slot — without imposing any
styling, then migrate every genuine row-button onto it:
- app/overlays/panel.tsx
- app/artifacts/index.tsx
- app/chat/sidebar/chrome.tsx (SidebarRowBody, SidebarRowLink)
- app/settings/providers-settings.tsx
- components/desktop-onboarding-overlay.tsx (PROVIDER_ROW_CLASS rows)
Fully behavior-preserving: RowButton adds no classes, so each row keeps its
exact layout/look (verified by a unit test asserting className passthrough).
Left as-is (not row-buttons; converting would risk visual regressions): the
compact bespoke buttons in shell/statusbar-controls.tsx (STATUSBAR_ACTION_CLASS,
also a nested DropdownMenuTrigger asChild) and pet-generate/reference-chip.tsx.
Finding 1 of the desktop UI-consistency pass: SVG icon sizing had four
competing conventions with no source of truth. Introduce a named icon-size
scale (iconSize.xs/sm/md/lg/xl -> size-3/3.5/4/5/6) in lib/icons.ts and migrate
the genuine icon deviants onto it:
- desktop-install-overlay.tsx: Loader2/Check/AlertTriangle/Chevron* (h-4 w-4,
h-3.5 w-3.5 -> iconSize.md/sm)
- composer/controls.tsx, voice-activity.tsx, queue-panel.tsx: numeric size={N}
on Tabler icons -> iconSize classes
Sizes snap to the nearest scale step; the only rendered deltas are size={11}
-> 12px (queue/stop glyphs, +1px) and AudioLines size={15} -> 14px (-1px, now
matches its sibling toolbar icons). All other migrations are exact (12/14/16px).
Out of scope (different sizing mechanisms, left untouched): non-icon h-N w-N
layout (sliders, skeletons, swatches), sprite size props (PixelEggSprite), and
Codicon font-icon sizing. Broader size-N -> token adoption is follow-up.
The usePromptActions hook is the textbook "god hook" AGENTS.md warns against.
As a first, safe slice, pull its module-level standalone helpers (no closure
over hook state) into a focused, testable use-prompt-actions-utils.ts sibling:
- error classifiers: isSessionNotFoundError, isSessionBusyError,
isProviderSetupError, inlineErrorMessage
- session-busy retry: withSessionBusyRetry (+ its constants)
- attachment IO: base64FromDataUrl, imageFilenameFromPath,
readImageForRemoteAttach, readFileDataUrlForAttach, friendlyRemoteAttachError
- misc: delay, isSessionIdCandidate, blobToDataUrl, renderCommandsCatalog,
slashStatusText, appendText, visibleUserOrdinal, visibleUserIndexAtOrdinal,
the _submitInFlight guard set, and the GatewayRequest type
Pure restructuring, no behavior change; the usePromptActions and
uploadComposerAttachment exports (and their import paths) are unchanged. Adds
unit tests for the pure helpers. use-prompt-actions.ts: 1,956 -> 1,772.
DesktopController is a route root that had grown a controller's worth of
session-list plumbing inline. Extract the cohesive fetch/paging cluster into
a focused hook and a tested pure helper, per AGENTS.md's "keep route roots
thin" guidance:
- use-session-list-actions.ts: refreshSessions / loadMoreSessions /
loadMoreSessionsForProfile / loadMoreMessagingForPlatform / refreshCronJobs
(plus the private cron/messaging refreshers, sessionsToKeep, and the
excluded-source constants)
- desktop-controller-utils.ts: pure sameCronSignature helper (+ unit tests)
Pure restructuring, no behavior change. desktop-controller.tsx: 1,441 -> 1,233.
Pull ChatBar's module-level pure helpers, constants, and the QueueEditState
type out of the 2.3k-line composer/index.tsx into a focused, testable
composer-utils.ts sibling:
- constants: COMPOSER_STACK_BREAKPOINT_PX, COMPOSER_SINGLE_LINE_MAX_PX,
COMPOSER_FADE_BACKGROUND, DRAFT_PERSIST_DEBOUNCE_MS
- helpers: pickPlaceholder, COMPLETION_ACTIONS, slashChipKindForItem,
slashArgStage, slashCommandToken, cloneAttachments
- type: QueueEditState
Pure restructuring, no behavior change; adds unit tests for the slash helpers.
(The ChatBar component itself is a single tightly-coupled megacomponent; a
deeper hook-based decomposition is left for a dedicated follow-up.)
Behavior-preserving extraction of the 1,942-line thread.tsx transcript
renderer into co-located sibling modules, matching the existing flat
assistant-ui/ convention:
- thread-content.ts / thread-timestamp.ts: pure helpers (+ unit tests)
- thread-types.ts: shared RestoreMessageTarget
- thread-status.tsx: loading / stall / background-resume indicators
- thread-message-parts.tsx: reasoning + tool part components
- assistant-message.tsx, system-message.tsx, user-message.tsx,
user-edit-composer.tsx: the message renderers
thread.tsx now holds only the Thread route component (1,942 -> 119 lines).
Also drops a dead readAloudAudio module variable (no references).
The Cmd-K "Install theme…" palette listed Marketplace themes with no hint
that you already had them, and clicking one re-downloaded + re-installed a
theme you owned. The Appearance settings grid already detected this, but by
parsing theme descriptions inline on every render — plumbing that never made
it to the palette.
Lift it into one reactive source and reuse it everywhere:
- $marketplaceInstalls (computed over $userThemes): extensionId -> installed
theme, derived once via marketplaceIdOf and memoized, instead of rebuilding
a Set per render.
- Both install surfaces now mark owned rows installed and, on click,
re-activate the installed theme rather than re-fetching it.
- Drops the duplicated description-parsing in settings and the per-session
"installed here" state in both surfaces (the store is the source of truth,
so previously-installed themes show correctly too).
DRY: the roomier-side bias computed its probability two ways
(STROLL_TOWARD_ROOM and 1 - STROLL_TOWARD_ROOM). One draw XNOR'd against
the roomier side says the same thing more plainly.
The floating pet wandered almost constantly: every idle beat picked a new
walk and hops fired ~45% of the time, so it read as nervous rather than
alive. Make movement the exception, not the default, and split the
overgrown roam hook into focused modules.
Behavior (per ambient game-AI: GameAIPro ch.36 + idle/wander state
machines):
- Loaf, don't pace: most decision beats just keep resting (REST_CHANCE
0.62) instead of always re-walking.
- Memoryless dwell: pauses now draw from an exponential distribution
(mostly short rests, the occasional long loaf) instead of a uniform
1.8-5.2s window, so the cadence never reads as a metronome.
- Hops dialed back 0.45 -> 0.2 (the jumpiest, noisiest motion).
Structure (no god-file; a hook should own one narrow job):
- roam-behavior.ts - what to do & when (dwellMs, chooseMove,
pickStrollTarget) + tuning. Pure, rng-injectable.
- roam-geometry.ts - where it can stand (snapshotLedges, overlayLedge,
resolveLedge, overlapsX, groundTop). DOM measurement + pure ledge math.
- use-pet-roam.ts - the physics/RAF loop only.
Tests: deterministic, rng-seeded unit coverage for the decision + geometry
helpers (behavior contracts, not snapshots).
When a full-screen route overlay (settings/profiles/cron/agents/command-center) is up, the pet's walkable surface swaps to a single ledge at the overlay card's bottom edge — derived from OverlayView's shared inset, not measured — so it patrols there; closing the overlay restores the normal surfaces and it drops back down.
Walk speed is derived from the sprite's animation loop + on-screen size (one body-width per loop) instead of a fixed px/s, so it steps rather than glides; the pet also sinks a few px so its feet meet the surface instead of hovering.
usePetRoam re-measures ledges from the live DOM each beat and walks/hops/falls between them, driving DOM position imperatively (no per-frame re-render).
Opt-in $petRoam (localStorage), $petMotion (run/jump pose) and $petRoamDir (-1/0/1) feed the shared $petState only while the agent is at rest ($petAtRest), so a wander never overrides real activity.
The Gateway item is the only statusbar entry with variant === 'menu'.
Since da73223f4 wrapped every render branch in `Tip`, the menu branch
nested `<DropdownMenu>` (a Radix Root that renders no DOM node) inside
`Tip`'s `<TooltipTrigger asChild>`. With no element to attach to, Radix
could never wire hover listeners, so the tooltip silently never showed.
`Tip` also can't be moved inside `DropdownMenuTrigger asChild` (the shape
proposed in #54859): it's a plain component, not a Slot-forwarding one, so
the trigger's injected ref/handlers would land on `TooltipContent` instead
of the button and break the menu's click + popper anchoring.
Fix by composing both trigger Slots directly onto a single <button>
(`TooltipTrigger asChild` over `DropdownMenuTrigger asChild`), the pattern
already used in profile-switcher.tsx, and skip the tooltip wrapper entirely
when the item has no title.
Supersedes #54859.
Co-authored-by: wnuuee1 <wnuuee1@users.noreply.github.com>
Subagent session pop-outs (`watch=1`) spectate a run driven elsewhere, so
editing/steering the transcript from there makes no sense. Gate the composer
and the user-bubble mutations on `isWatchWindow()`:
- hide the composer (folds into `showChatBar`)
- user prompts become a read-only button that toggles the 2-line clamp so long
prompts stay fully readable, instead of opening the edit composer
- drop the stop/restore actions and the checkpoint branch-picker
Keyed off the narrow `isWatchWindow()` (not `isSecondaryWindow()`), so the
new-session and cmd-click pop-outs are unaffected.
Let users click the status bar context indicator to see how tokens are
split across system prompt, tools, rules, skills, MCP, and conversation.
Co-authored-by: Cursor <cursoragent@cursor.com>
The bootstrap-runner PowerShell spawn is formatted multiline (spawn(\n ps,\n fullArgs,...), so the literal substring 'spawn(ps, fullArgs' never matched and the assertion was failing on main independent of #54635. Convert it to a whitespace-tolerant regex like every other call-site assertion in this file.
The recurring Windows desktop console-flash bug (#54220) is governed by the
*parent's* console, not by each child spawn. The desktop backend was launched as
GUI-subsystem pythonw.exe, which has no console at all — so every
console-subsystem child it spawns (git, gh, cmd, wmic, powershell, ...) had to
allocate its own console, flashing a window. That is why the fix had become an
endless per-call-site sweep of CREATE_NO_WINDOW flags: each leaf spawn was
papering over a missing console on the root.
Launch the backend as the venv's console python.exe instead. Under the existing
hiddenWindowsChildOptions() wrapper (windowsHide: true -> CREATE_NO_WINDOW) the
backend owns a single *windowless* console, and every descendant spawn inherits
it instead of allocating a visible one. This makes "no flashing windows" a
property of the one backend launch rather than a flag that must be remembered at
every spawn site — including spawns inside third-party libraries that no
call-site sweep can reach.
Verified on Windows 11 25H2 (Windows Terminal default): with the per-site hide
flag forcibly neutered, the canonical culprits (git/gh/cmd/wmic/powershell)
spawned naively and none flashed, while the same naive spawn from the old
console-less pythonw parent did flash — isolating the parent console as the cause.
Two premises behind the old pythonw approach did not hold up on current Windows
and are dropped here:
- The venv Scripts\python.exe uv shim, under CREATE_NO_WINDOW, re-execs base
python *windowless* — it does not flash a conhost (the #52239 concern), so the
base-pythonw detour is unnecessary.
- Console python restores stdout, so the backend announces its port on the normal
HERMES_DASHBOARD_READY stdout line; the pythonw-only ready-file side channel is
no longer needed and the readyFile opt-in is removed.
Removes the now-dead pythonw machinery (getNoConsoleVenvPython, toNoConsolePython,
applyWindowsNoConsoleSpawnHints, readVenvHome) and updates the test to assert the
new invariant: backend command is never pythonw, both backend spawns still go
through hiddenWindowsChildOptions, and no backend opts into the ready-file path.
Scope: this fixes the high-frequency backend-descendant flash classes. The
updater/UAC handoff (#54543) and embedded-terminal PTY accumulation (#53555)
classes have separate root causes and are unaffected.
User terminal tabs and their recent scrollback now survive an app restart
(VS Code parity). Tabs, active selection, cwd, and a serialized scrollback
snapshot are written to localStorage on every change; on launch the tabs
reopen with their history replayed above a fresh shell. Processes are NOT
revived — a new shell starts one line below the restored block.
- Capture: SerializeAddon snapshots the buffer on a 750ms leading-edge
throttle, so a `cmd; quit` lands on disk before teardown; the snapshot is
trimmed of its trailing idle prompt (no "double prompt" on restore) and
capped (200 scrollback lines / 48k chars) to stay under the storage budget.
- Teardown guard: app quit/reload kills the PTYs from the main process,
firing onExit in the renderer, but React skips effect cleanups on teardown
so the per-instance `disposed` flag never flips. A pagehide/beforeunload
flag stops onExit from calling closeTerminal() and wiping the persisted
tabs right before relaunch restores them. A real `exit`/Ctrl-D still closes.
- Agent mirror tabs stay runtime-only — only user tabs persist.