* fix(terminal): don't count a PTY reattach as workspace activity
A remount (hidden prewarm, background mount) reattaching the tab's own live PTY bumped
lastActivityAt and sortEpoch, re-sorting the sidebar on hover. Also keep store map
identity on that no-op path so subscribers skip re-rendering.
* perf(terminal): prewarm hovered and keyboard-neighbor workspaces before the switch
A row hovered for 120 ms, and the previous/next workspaces in sidebar order (local hosts,
500 ms after a switch settles), take a prewarm lease: hover holds one, neighbors two.
A lease mounts only each group's visible snapshot-restorable terminal plus live tabs,
never wakes sleeping agents, and lays out and warms WebGL in separate idle tasks.
Hover stays laid out until it ends; a neighbor hides again once warm. Held leases are
exempt from ordinary cold parking (the retention budget still applies), individually
parked sibling tabs stay unmounted, a narrowed live mount is handed back on release,
and prewarm is off when hidden-view parking is disabled.
* perf(sidebar): coalesce held workspace-step keys
Auto-repeated Cmd+Shift+Up/Down and list arrows move only the sidebar highlight (and the
listbox's active descendant); the workspace under it switches once the repeat stops, on
keyup or a missed repeat since macOS withholds keyup while Cmd is held. Single presses
still switch synchronously. Keyboard navigation also publishes its sidebar neighbors to
the prewarm lease.
* perf(palette): close Cmd+J without the exit fade after a selection
A selection already moved the user to the new surface; the 150 ms overlay fade only
dimmed it. Escape and outside dismissals keep the fade.
* fix(terminal): undo a prewarm narrowing only while the lease still owns it
Releasing a lease after its workspace was activated (or after another mount replaced the
restriction) stripped the deferred tab ids that activation now owns, so over-cap tabs
stayed on the restoring placeholder and lost parked watchers. The undo now runs only
when the lease's restriction is still installed and the workspace was not activated.
Simulator workspaces also get no restriction on any unrestricted mount, parked included,
so the emulator overlay stays mounted.
* fix(terminal): prewarm never reattaches a session the daemon has not confirmed live
After a daemon loss a parked shell tab keeps its PTY id and snapshot capability, and its
reattach spawns a replacement process. A lease now asks main (pty:hasPty, authoritative
true only) for the revealable tabs it would reattach and admits only confirmed sessions;
unknown or remote-runtime answers decline. Workspaces with a simulator tab never take a
lease, since any restriction unmounts the emulator overlay and its local state.
* fix(terminal): confirm every split leaf and keep parked siblings out after a lease ends
A split tab reconnects each leaf on mount, so prewarm now confirms every resolved leaf
session (re-resolved after the answer; any change fails) instead of only the tab's
first PTY. Releasing a narrowed lease ends its measurement, which starts tab cold
parking's 30 s cool-down with an empty park set; the narrowing now outlasts that
cool-down so individually parked siblings stay unmounted after an abandoned hover.
Prewarm target selection moves to its own file.
* fix(terminal): hold a prewarm narrowing through any newer measurement cool-down
A mount that measures the worktree after the lease ended starts its own tab cold-park
cool-down; lifting the narrowing on the original deadline would remount parked
siblings while parking is still off. The undo now re-waits while the worktree is
measuring or inside its park cool-down. Teardown cancels pending undos after the final
releases, which would otherwise schedule timers past the unmount.
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
Claude reports its session at startup but writes no transcript until the
first prompt, so the host answers pending and the phone said "Can't find
this conversation yet". A session the running agent reported live is new;
only remembered sessions keep the not-found copy.
* fix(mobile): keep dictated words through interruptions, busy taps and failed inserts
A call, Siri or mic dropout now finishes the dictation instead of
discarding it, a tap while transcribing no longer cancels, a transcript
its target refuses is copied to the clipboard with a toast, and host
dictation error codes show as plain sentences.
* test(mobile): share one session dictation harness for refusals and kept transcripts
* refactor(mobile): share the lost-session dictation sentence and keep speechless helpers private
* fix(mobile): keep a dictated terminal transcript when the live send throws
* fix: forward Cmd+E dictation chord when voice is on but no STT model is set
Both the main-process shortcut router and the renderer's keydown/control
handlers early-returned whenever voice.sttModel was empty, before ever
reaching startDictation() — which is the only place that shows the
"No speech model selected" toast. The result: enabling voice but not
downloading/selecting a model makes Cmd+E (and the mic button) a
complete no-op with zero feedback.
Fix drops the sttModel check from the early-return in all three spots
so the existing toast in startDictation() actually gets a chance to
fire. The enabled=false path is unchanged (still a no-op), and hold
mode is untouched since it already checked voice.enabled and had its
own separate handling.
Added a regression test covering the toggle-mode chord with an empty
sttModel, alongside the existing disabled/hold-mode/toggle-mode cases.
Fixes#18691
* fix(dictation): show the no-model toast from hold mode and the chat mic
The no-model check also sat in the hold-mode gesture and the native chat
mic button, so those still did nothing. Drop it there too, let held keys
replace the toast instead of stacking it, and fold the main-process
regression into the existing dictation chord test.
* fix(dictation): update the no-model toast in place on held-key repeats
* fix(dictation): keep the no-model toast a plain toast for the localization audit
---------
Co-authored-by: Kelvin Amoaba <97001695+AmoabaKelvin@users.noreply.github.com>
* fix(mobile): release the dictation audio session on stop and fail clearly without iOS voice processing
Stopping dictation only muted the native input, so the iOS voiceChat session
and Android audio focus stayed held until the screen unmounted and other apps'
audio stayed ducked. End now tears the engine down; the next open rebuilds it.
iOS tearDown stops I/O before deactivating and notifies other apps.
The iOS engine returned from setup before installing the mic tap when voice
processing could not be enabled, so recording reported success and captured
nothing. Setup now throws, so initialize fails and the phone shows the
microphone error.
* test(mobile): expect dictation stop to tear the audio engine down
* test(mobile): drop the duplicate release-throw case from native capture tests
* fix(browser): retire superseded DevTools opens
* fix(browser): keep undocking renderer when a retained old guest closes its dock
---------
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
Hosts that predate the census protocol set report a null protocol whenever
terminals span daemon generations, so a forced update stayed refused there
even though the incoming build could reach every daemon. When the census
cannot answer, the candidate's profile preflight now lists the daemon
generations alive under the data root the server serves, passed to it
explicitly, and the planner checks attachability against that list. An
uncounted census still refuses, and any generation the probe cannot rule in
or out leaves the update unverifiable.
* feat(files): show images and PDFs from the byte-range preview path, local and remote
ImageViewer and PdfViewer read an orca-media:// URL instead of base64 when
the editor opens a file: images load by URL, and pdf.js reads PDFs by
range, so a large PDF no longer arrives whole through files.readPreview or
pages into memory first. The grant mints only for the editor's viewers;
diffs, git blobs, markdown images and the paired web client keep base64.
- Minting an image checks its header with the base64 path's rule, so an
image proven too large to decode safely never reaches the decoder.
- orca-media gains fetch and CORS privileges, and range responses expose
Accept-Ranges and Content-Range, so pdf.js can read by range.
- Grants report the file size the viewer used to estimate from base64.
* fix(files): read PDFs by real range, check served images, keep old hosts' previews
- pdf.js fetches ranges only over http(s), so an orca-media URL was read
whole; a small range transport now hands it bytes=a-b reads from the
grant, and a failed range fails the preview instead of hanging it.
- A file can change after mint, so the raster header is checked again on
the bytes each image request serves (the same handle locally).
- A paired host without files.readChunk falls back to files.readPreview
for images and PDFs, as before; only audio and video need the update.
* fix(files): mint no local document grant for a renderer gone mid-mint
The local image/PDF mint is async now (authorize, open, header check), so
a renderer that closed meanwhile could leave a grant no teardown drops. It
binds the renderer lifetime first and refuses to mint once it's gone, as
the remote mint does.
* fix(files): keep the 50 MB cap for URL images, and probe remote headers incrementally
An <img> decodes the whole file, so image grants keep the base64 path's
50 MB cap; PDFs, now read by range, don't need one. A remote header probe
reads only each wider step's new bytes instead of re-reading from the
start. A replaced image of a different size resets its zoom.
* fix(files): tear down a PDF load whose range failed, and read 256 KiB ranges
A failed range only showed the error and left the pdf.js task, its worker
and pages alive; it now destroys that load first. pdf.js asks for one
readChunk-sized range at a time instead of 64 KiB, and a Range preflight
gets its CORS answer instead of 405.
* fix(files): read PDF ranges over IPC so orca-media needs no fetch or CORS
orca-media had gained supportFetchAPI and corsEnabled, and its responses
sent Access-Control-Allow-Origin: *, so pdf.js could fetch ranges. That let
any default-session frame holding a token read the bytes. pdf.js now reads
a PDF grant's ranges through fs:readPreviewRange, which main answers only
for the renderer that minted the token. The scheme is back to element-load
privileges (standard, secure, stream), responses carry no CORS headers, and
preflights get 405.
Grant bookkeeping moves to preview-grant-registry.ts. An e2e spec proves an
orca-media URL can't be fetched from the app page or read from another
session.
* ci(e2e): route the byte-range preview sources to the viewer specs that exercise them
Changes to the orca-media grant path, the image/PDF viewers and the PDF
range transport now run the PDF reload, rebuild and find specs, the remote
large-file open, media playback and the partition-isolation spec.
* fix(files): stop PDF range reads on abort, abort remote reads with the window, fix the CI route
- A closed or reloaded viewer schedules no further pieces of a merged range,
and a remote range read stops when its window goes away.
- A URL without a size is an error rather than a fetch the scheme refuses.
- The PR guard job's sparse checkout and its admission test include the new
route module, and the route now also covers the mint handler, preload
bridge, editor surfaces, scheme registration and read client.
* fix(files): classify linked previews by their target, keep range reads off stopped WSL distros
A local image or PDF reached through a link is classified by the file it
resolves to, at mint and on every read, and a re-mint after the link is
retargeted gets a fresh grant of the new type. A paired host's document
whose first bytes are another kind defers to files.readPreview, which
classifies the target there. Lazy local reads go through the non-waking
WSL guard, as fs:readFile and fs:readFileChunk do.
* perf(i18n): cache option-free translations per language and resource version
A workspace switch re-renders chrome that resolves hundreds of static strings
through i18next (~3 ms per switch). Option-free lookups only change with the
language or the loaded resources, so cache them keyed on both.
* perf(file-search): skip file search state writes that change nothing
Every workspace switch cancels the outgoing workspace's idle search, which wrote
an identical state object and re-notified every store subscriber.
* perf(sidebar): keep unchanged worktree cards and project headers memoized across switches
A plain press replaces the sidebar selection, and the selection-keyed gesture
callbacks plus the shared selectedWorktrees array were passed to every card, so
every card and its menus re-rendered on each workspace switch. Gesture callbacks
now read the latest selection from a ref, only selected rows receive the
selection, and project header actions are memoized.
* test(terminal): bound toast translations from above now that static strings are cached
* perf(terminal): stop re-rendering hidden panes and retained tab strips on workspace switches
Hidden panes re-measured their title-overlay rects on every switch, forcing a
layout and clearing the rects, which re-rendered the pane once on hide and again
on show. Hidden overlays are display:none, so they keep their last rects until
shown. Retained tab strips subscribed to the active repo and worktree for a
Windows-only shell menu, re-rendering every strip; those reads are gated like
the project maps beside them.
* perf(app-shell): let root overlays skip the App render a workspace switch causes
App re-renders on every switch (its chrome layout tracks the active worktree),
and it handed fresh floating-workspace and onboarding objects to the root
overlays, re-rendering every overlay and dialog. Both objects are memoized,
AppRootSurfaces and the floating toggle button are memo components, and the
toggle callback is stable.
* perf(worktrees): switch the active repo and worktree in one store write
Activating a worktree in another project wrote activeRepoId and then the
active worktree separately, so every store subscriber re-ran its selector twice
(~1 ms per switch). The repo change now rides along as a state transition of the
same setActiveWorktree write, as the SSH server-copy activation already does.
* perf(app-shell): stop App re-rendering on workspace switches
App subscribed to the raw active worktree and tab ids through the chrome layout
and three sync hooks, so every switch re-rendered the whole app shell. The
layout now exposes booleans, shortcut dispatch and the titlebar expand button
read the active ids when they fire, and the git-status polling, web session tab
sync and review refresh reporting hooks run in a null gate component under
AppBackgroundServices.
* perf(worktrees): fold visit recency and nav history into the activation write
A switch wrote the active worktree, then the leave/enter recency stamps and the
nav-history entry as separate store writes, each re-running every subscriber's
selector. The stamps (resolved against the post-activation selection, as
markVisited did) and the history entry now ride in the same setActiveWorktree
state transition as the repo follow.
* perf(terminal): present instead of clearing the shared glyph atlas on worktree reveal
The settled reveal reset predates xterm's per-renderer page-layout versioning
(renderers now rebuild their model when a shared atlas page changes), so a
healthy atlas no longer needs re-rasterizing on every worktree switch. DPR
repair and system wake keep the full reset.
* fix(worktrees): keep fresh visit stamps when the activation's history entry is de-duplicated
* fix(app-shell): repaint memoized shell controls on a language change
* test(floating-terminal): stub the language subscription in the toggle button's hook harness
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
* feat(files): save a remote text file larger than one request
The editor opened files up to 50 MB from a WSL host or SSH server but refused to save anything
over 720 KB. A staged save sends the content in chunks to a temp the host makes beside the
target, and the commit replaces the target by rename under the same locks and disk
precondition as files.writeIfUnchanged. Bounded per save, connection and host; idle saves are
dropped; a journal lets host start remove temps a crash left. Older hosts keep the refusal.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(files): stage large remote saves privately and commit them with the one-request save
Review of #35073: chunks now go to the profile's private staging folder, the commit
re-authorizes the target and writes it in place through writeLocalTextFileWithDiskPrecondition
(same locks, precondition and inode as a one-request save), ops on a save run one at a time so
an abort or commit never races an append, a closed socket's saves are aborted, and host start
clears the staging folder in the background.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(files): check a staged save's temp size at commit, and stage per process
Review of #35073: the commit refuses a temp whose size on disk isn't the declared total, and each
process stages in its own subfolder; the start sweep removes only its own folder and those whose
owner has exited, so runtimes sharing a profile never wipe each other's saves.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(files): a runtime without staged saves closes a socket cleanly
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat(wsl): stop an idle WSL hook relay so its distro can stop
Once no bridge terminal is left in a distro, the hook relay was the only thing keeping the
distro (and the WSL VM's memory) running, for as long as Orca ran. After 5 minutes with no
bridge terminal and no hook traffic the relay now stops, detached first so recovery never
restarts it, and its config is parked; the next terminal or agent in that distro starts it
again. Nothing new starts a relay, so nothing boots a distro.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(wsl): a WSL terminal of unreported distro keeps every hook relay
Review of #35173: a WSL shell whose spawn reported no distro (an older daemon) still uses the
default distro's relay, so it now counts as presence for every distro rather than none.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(wsl): a hook relay stays while the daemon still runs a bridge session in its distro
Review of #35173: after a relaunch, a hidden worktree's or headless tab's bridge session is live
in the terminal daemon though no pane in this run noted it, and orchestration, automations or
mobile may wake its agent. Before an idle stop the relay now asks the daemon's own session
inventory; a live session there, or an inventory it can't vouch for, keeps the relay.
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* perf(sidebar): switch workspaces on mouse press instead of click
A plain primary mouse press on a sidebar workspace row now activates it; the click that
ends the same press is skipped. The ~80-120 ms a press is held before release hides the
switch, so the new workspace is usually painted before the button comes up.
Modified presses, touch and pen, nested controls, rows inside a multi-selection, kanban
cards and keyboard-forwarded clicks keep click activation.
* test(sidebar): drop wall-clock cache stub from press activation test
* fix(sidebar): keep press activation off drags that wake, resume or close the board
A press-activated switch now keeps focus in the new terminal: the press's native mousedown
no longer moves it to the workspace list. Presses fall back to click activation when the
target would be woken (sleeping workspace or agent session, slept VM, disconnected SSH),
outside the terminal view, on selectable text, and while the workspace board is open, so a
row dragged onto the board no longer closes it. The skip-the-next-click flag now ends with
its gesture instead of surviving a drag-suppressed click.
* fix(sidebar): keep press activation off hosts a focus could boot
Switching workspaces focuses the row's host: a stopped WSL distro boots on focus and a recipe
VM resumes on activation, both before a press is known to be a drag. A press now activates only
on a proven-ready host (local, an SSH target, a WSL host reporting ready, a connected paired
runtime), resolved from the row's own host so folder workspaces and host twins match; recipe-VM
targets and unknown hosts keep click activation.
* fix(sidebar): activate on press only for local workspaces
Remote hosts can connect, boot or resume when their workspace gains focus, and the
focus subscribers resolve a workspace by bare id, so a twin or an SSH folder row could
still reach a remote host from a press that becomes a drag. Press activation now needs
the row's own host and the id's owning host to both be local; every SSH, WSL, runtime
and VM workspace keeps click activation.
* fix(sidebar): keep click activation while a text field has focus or a terminal would respawn
* fix(sidebar): keep click activation for workspaces without a running terminal; leave focus to the list when the press moved none
* fix(sidebar): keep a multi-selected folder row's press for the group drag
* fix(sidebar): drop the pre-press focus reference when the gesture ends
* fix(sidebar): keep click activation while a menu or dialog is open
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
* fix(browser): retire passkey account requests with their documents
* test(browser): fold passkey document-retirement coverage into the picker suite
---------
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
* fix(session): a managed host's old SSH rows no longer replace its runtime workspace on reload
On a desktop that still catalogs a host's pre-managed-server SSH repo, startup
adoption let the old ssh: partition supersede the runtime partition's rows for
the same workspace. Its editor tabs and split were dropped, and stale closed
tabs came back on every reload. Workspaces a runtime partition holds are now
foreign to SSH adoption; the SSH rows stay parked in their own partition.
Moves the SSH partition catalog attribution into its own module unchanged
apart from the runtime-owner check, to stay under the file line limit.
Fixes#34376Fixes#34416
* fix(session): only an SSH host's own server supersedes its old SSH rows
Treating every runtime-held workspace as foreign to SSH adoption broke the
contested gap-fill for unrelated hosts that share a bare workspace id. Limit it
to the managed or SSH-tunnelled server running on that same SSH host, which
startup now learns from the runtime environment list.
A workspaceDir of "~/..." was never expanded, so it resolved relative to
each repo and worktrees were created under <repo>/~. Expand a leading ~
when settings are loaded and on every settings write, and share the
expansion with the floating-workspace path resolver.
Co-authored-by: Neil <neil@stably.ai>
* fix: retire extension frame listeners with their document
* fix: snapshot extension owner before native destruction
* refactor(extensions): keep listener registry within its line budget
---------
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
The legacy Compact card-property migration returned the Compact preset even
when the stored list already was a current Compact preset, so every clean
reload of a Compact profile scheduled a full profile write. Treat both the
raw ['status'] and the normalized ['status', 'unread'] form (persisted by any
UI write) as already migrated.
Fixes#34828
Co-authored-by: Neil <neil@stably.ai>
A background hooks:check refused for a stopped WSL distro now says so (unavailable:
'wsl-distro-stopped'). The setup-script prompt maps it to a distro-stopped state: the card hides
instead of showing 'Couldn't verify…' with a Retry that cannot succeed, and focus re-inspects, so
the card returns once the distro runs. An unverifiable distro stays a retryable error.
* fix(wsl): never boot a terminated distro off a list taken while it was still stopping
WSL keeps listing a distro for a moment while it stops. A bridge pane's exit now marks its distro
as possibly stopped, its host-loss verdict and boot-id reread only count a running list started
after that settles, and deferred background work (file watcher and hook relay restarts, history
cleanup, CLI repair) is drained only by a settled list. Before, a bridge exit's own list could
read 'running' mid-stop and lead to a boot-id exec, and any confirmed list probed mid-stop
drained deferred restarts that spawn wsl.exe, booting the distro the user had just terminated.
* fix(wsl): gate the hook-relay restart and setup-script inspection on a settled running read
- A deferred hook-relay restart re-reads a settled running list right before spawning the relay
(wsl.exe), and waits again if the distro stopped; a drained deferral is only a hint.
- Setup-script import-candidate inspection (sidebar prompt, runtime RPC) opens nothing under
\\\\wsl.localhost while the distro is stopped or unverifiable: those UNC opens booted it about 2 s
after each terminate on the awin verify.
* fix(wsl): close a pane's clean exit at once, and settle the guest-process fast path
- A bridge pane's code-0 exit is the user's own (a terminate gives -1): it now closes at once,
with no stop mark, settle wait or running-list read. Non-zero exits keep the E9 discipline.
- readWslGuestPaneProcesses' fast path for a live pane (Sleep, quit) trusts the cached running
list only when it was probed after any stop of the distro settled.
- When a mid-stop list skipped deferred work, one unref'd probe just past the settle drains it,
instead of waiting on an unrelated probe.
* fix(wsl): mark a clean bridge exit as a possible stop, and fix the exit check test's probe timing
A code-0 bridge exit still closes at once, but now also marks its distro as possibly stopped, so
an in-guest poweroff or wsl --shutdown that exits 0 cannot leave background reads trusting an old
running list. The coalesced-probe test waits for the probe, which now starts after the settle
anchor, before answering it.
* fix: preserve successor plugin panels after renderer loss
* test(plugins): drop redundant panel lifetime control test
---------
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
* perf(electron): target persisted UI broadcasts to main renderer
* test(ui): use a supported top-level view in broadcast regression
* test(ui): collapse UI broadcast routing tests into one wiring check
---------
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
hooks:check takes an optional background flag. The sidebar setup-script prompt and the setup
guide's progress probe, which run on render, pass it: on a stopped or unverifiable WSL distro
the check answers status 'error' without opening orca.yaml over \\\\wsl.localhost, which would boot
the distro. Launch preflight and settings, user acts that start the distro anyway, are unchanged.
When the pinned Bun binary is installed but its version probe times out,
required typecheck callers (repo typecheck, CLI build, E2E and mobile
typecheck) now report the timeout with the executable path and budget
instead of claiming Bun is missing. Each caller suggests its own compiler
fallback. Optional Bun consumers keep returning null and falling back.
Fixes#33707
Co-authored-by: Neil <neil@stably.ai>
run-use --takeover-legacy on an ordinary (non-legacy, non-adopted) Run
hit the takeover authority gate first and told the caller to repair
terminal authentication, even though the flag can never apply there.
Only require takeover authority when the target is legacy, missing, or
the adopted Run, so ordinary Runs reach bindRun, which keeps the caller
and worker fences and rejects the flag before any binding changes.
Fixes#30779
Co-authored-by: Neil <neil@stably.ai>
Pass --literal-pathspecs to the per-file diff in collectAddedLineRanges so
filenames containing glob characters (e.g. `[ab].tsx`) match only
themselves instead of also pulling in other files' hunks (e.g. `a.tsx`).
Covers both edits and renames with real-Git regression tests.
Fixes#34784
Co-authored-by: Neil <neil@stably.ai>
On awin a wsl --terminate exited each bridge pane with -1, while exit and exit 0 gave 0, so a
terminate never yields 0. A clean exit keeps today's verdict without the extra wsl.exe spawn.
Co-authored-by: Claude <noreply@anthropic.com>
resolveSimulatorUdid matched the first device whose name contained the
requested name, so asking for "iPhone 17" could attach an earlier
"iPhone 17e". Prefer a case-insensitive exact name match first, then fall
back to the existing substring/UDID match.
Only srcset is a comma-separated candidate list; src and href hold one URL.
Splitting them invented missing targets for paths containing commas or
spaces and could pass a missing target whose fragments were tracked.
Fixes#34815
* perf(terminal): release hidden WebGL canvases' drawing buffers
Chromium keeps a display:none WebGL canvas's drawing buffers alive (on macOS
a back buffer plus up to four pooled full-size surfaces). Hidden terminal
tabs and retained worktrees therefore held ~80 MB of GPU memory each until
they were parked. Collapse the canvas backing store while it has no layout
box and keep the context; xterm's device-pixel observer restores the size
and redraws synchronously on reveal.
7 terminal tabs, 1920x1050@2x: GPU process 670-760 MB -> 350-408 MB.
* perf(terminal): free hidden canvases only after 30s and never for suspended worktrees
Reallocating the backing on reveal cost about one frame. Delaying the release keeps rapid
switching allocation-free, and worktree-suspended panes keep their backing so worktree
switches match main. A MutationObserver re-checks a canvas that xterm resizes while hidden.
* fix(terminal): restore released WebGL canvases before the reveal frame paints
- Reveal restore reallocates at the current DPR and draws past xterm's paused
renderer inside the ResizeObserver callback, so the first painted frame has
content; also covers engines without devicePixelContentBoxSize.
- DPR repair reports released canvases as 'released' (current) instead of
'deferred', so window-resize recovery stops retrying them.
- Veto release for panes of a hidden worktree surface (marked from
isWorktreeActive) instead of webglAttachmentDeferred, which a hidden
prewarm clears.
- Schedule release for canvases attached or regrown while already boxless;
a zero-size element gets no initial ResizeObserver entry.
- Clear the timer through the canvas's own window.
* test(terminal): guard hidden WebGL canvas release and reveal restore
* perf(terminal): restore a released WebGL canvas at its exact device size without a full resize
Set the backing to the reveal entry's device-pixel size (xterm's own fallback
size where that box is unavailable) and draw once, instead of handleResize:
xterm's device-pixel observer then finds nothing to resize, which removes a
second reallocation and redraw from the reveal. A DPR change while hidden
still takes the full resize path.
* fix(terminal): paint a revealed released canvas through a held synchronized-output frame
A released canvas has no last coherent frame to keep, so when a TUI holds DEC
2026 at reveal, the buffered refresh left it blank for up to xterm's 1s
watchdog. Use forceFullViewportPresent, which draws the current buffer once
directly; the TUI's sync end still presents its finished frame normally.
---------
Co-authored-by: m4air <m4air@Mac.localdomain>