Commit Graph
5102 Commits
Author SHA1 Message Date
Jinjing 42a42281bd Remove agent map view from dashboard (#15853)
* Remove agent map view from dashboard

Removes the view toggle and simplifies the dashboard to show only the kanban board layout.

* Assert boardProps is initialized on drawer open
2026-08-21 21:43:54 -07:00
Denis Darii d5667376b0 feat(dashboard): add a keyboard shortcut to toggle the Agent Dashboard (#15353)
Adds a configurable, unbound-by-default `dashboard.toggle` action that toggles the Agent Dashboard (in-window drawer or pop-out, per the existing mode setting).

- Wired through window-shortcut-policy, main-window dispatch, browser-guest dispatch, preload, and the renderer IPC handler.
- Opening the in-window drawer reveals the sidebar first; closing leaves it alone.
- Gated on the `experimentalAgentDashboardPopout` experiment, and the Settings shortcut row is hidden while that experiment is off.
2026-08-21 21:12:12 -07:00
Jinwoo Hong e7b047f53a fix(codex): suppress false restart notices after reauth (#15835) 2026-08-21 18:09:41 -07:00
Yeray 013eb539d5 Change 'SEÑOR' to 'MR' in Spanish locale (#15594) 2026-08-21 17:27:11 -07:00
Jinwoo Hong da6b9d8065 fix(terminal): stop orphaning live agent terminals across host restarts and graph syncs (#15644) 2026-08-21 17:11:17 -07:00
OrcaWinandBrennan Benson 2a68b78bb3 fix(worktree): let a configured worktree base outrank a built-in visibility source (#15232) (#15430)
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-08-21 12:20:04 -07:00
VincentandOrcaWin 59892a2f13 fix(terminal): preserve Shift+Enter during routing confirmation (#13598)
* fix(terminal): preserve Shift+Enter during routing confirmation

Keep previously trusted CSI-u routing active only while local foreground revalidation is pending. Clear pending authority on inconclusive reads and never promote display-only identity.\n\nRefs #12541\nRefs #13597

* fix(terminal): respect global WSL routing gate

* fix(terminal): bound the retained routing capability to a live read

Two holes in the previous commit let one provider read authorize CSI-u
indefinitely.

First, routingConfirmationPending satisfied its own precondition, so a
pending entry re-published itself on every reconfirmation request. Only
one of the five callers is the Shift+Enter burst timer; the others fire
on accepted submit/interrupt bytes, focus and visibility changes, and
onAgentExited -- the last of which runs precisely when a shell title
proves the agent is gone. On cmd.exe and Git Bash there is no OSC
boundary to publish over the entry, so nothing decayed it.

Second, the flag was published even when no confirmation read was
actually scheduled -- a hidden pane, a command read already in flight, or
a null pty id -- and only the read's inconclusive settle clears it.

Require routingTrusted to grant the capability, and publish the flag only
after sampling reports a read in flight, so it cannot outlive the read
that justifies it. This also makes the canConfirmRouting gate redundant:
the tracker already refuses WSL, SSH and remote pty ids.

* review round 2: bound the retained capability to any in-flight read

onVisiblePtyBound refuses to schedule while a higher-authority command
read owns the pane, so gating the pending publish on it mistook 'a
command-finished read already owns this' for 'no read at all' and
dropped CSI-u for >=350ms — the window #13598 exists to close.

Ask the tracker whether any read is in flight instead, and settle the
visible confirmation from the command-finished branch that publishes
nothing, so the flag cannot outlive the read that justifies it.

* review round 2: tighten the reconfirmation gate and its comments

hasReadInFlight already covers the visible-pty read that
visibleForegroundSamplePending tracked, so the disjunction was
redundant. Collapse the stacked Why blocks into one.

* review round 3: release the retained capability when a read is abandoned

Every outcome path now clears routingConfirmationPending, but the two
abort guards in readForeground return without publishing or settling. A
pty rebind during the multi-second inspection RPC — a detach/remount
emits no onExit, so the store entry survives — therefore stranded the
flag permanently, leaving Shift+Enter on CSI-u with nothing left to
revalidate it. Worse, once sampling is suppressed by a live hook row the
pane routes bytes on hook evidence alone, which the resolver excludes
precisely because PTY output can forge it.

Settle an abandoned read unless a newer generation will settle for it.

* review round 4: release the retained capability at every exit without a successor

Rounds 2 and 3 closed the outcome paths and the aborted-read paths, but
two review lanes independently proved a third: cancelPendingRead bumps
the generation, so a cancel never settles, and dispose plus the three
untrackable early returns schedule no successor to settle for them. The
store entry outlives the tracker — detach/remount emits no PTY exit — so
the flag latched there with no read left alive.

A visible remount self-heals, but a hidden pane does not: the dashboard
card resolves the encoding for any pane key regardless of visibility.

Release the capability from whichever exit ends the read, and say in the
entry's own doc comment that the flag is Shift+Enter-scoped.

* test(terminal): pin the remaining capability-release exits

dispose and the command-finished exit were covered; the visible-bind and
command-start variants are the same three-line pattern and were not.
Neutering the release now kills all four.

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-08-20 21:08:41 -07:00
VincentandOrcaWin a0cc9f35ba fix(terminal): mark captured shortcut input interactive (#13800)
* fix(terminal): mark captured shortcut input interactive

Refresh the interactive-redraw timestamp after a captured shortcut send
succeeds, so the composer redraw that follows takes the low-latency
foreground path instead of waiting out the 1s coalesce fallback.

Orca's captured shortcut path sends directly through the captured pane
transport to preserve pane, PTY and transport identity, and so bypasses
xterm's onData -- the only place that previously stamped the timestamp.
An idle pane therefore scheduled the post-Shift+Enter redraw as ordinary
throughput. Measured in a real Pi pane on Windows: ~1029ms before,
~16-20ms after, at an identical ~2.3KB redraw.

Stale pane bindings and rejected transport sends cannot refresh it.

Fixes #10203
Refs #13598

* fix(terminal): keep captured shortcuts out of the pane-teardown signal

lastTerminalInputAt has two readers, and the previous commit only meant
to change one of them. onExit reads it as "the user never typed into
this pane" to keep a newborn pane mounted when its shell dies on startup
(the failing-.envrc direnv case, pty-connection.ts onExit) so the error
stays visible and the worktree stays active.

Stamping it from a captured shortcut therefore made a single Shift+Enter
before that exit close the tab and bounce the user to Landing -- on every
platform, since captured shortcuts are not Windows-only.

Split the redraw window onto its own timestamp so captured shortcuts open
the fast path without arming the teardown, and leave onExit's behaviour
byte-identical to main.

* test(terminal): pin the captured-shortcut wiring and its staleness guard

Deleting the onAccepted block, or replacing its binding-identity check
with true, both left the whole suite green — so nothing pinned the part
of this PR that actually ships. Cover both against the existing IME
keyboard harness.

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-08-20 21:08:20 -07:00
ppw-stack df7460af41 fix(ui): keep dialog title and footer inside the panel (#15707)
DialogContent used an implicit grid column, which sizes to min-content: an
unbreakable token such as a long filename in the title widened the column past
the panel, so the title overflowed and the justify-end footer buttons rendered
outside the visible surface. Pin the column to minmax(0,1fr) and let long
titles wrap.
2026-08-20 19:34:31 -07:00
JinjingandJinwoo-H 6e25a90085 fix(terminal): keep a quick command queued until its own spawn takes it (#15630)
* Increase shell readiness timeout to match daemon barrier

Slow interactive rc files can take longer than 1.5s to initialize. Raise
the startup command readiness timeout from 1.5s to 15s to match the daemon
barrier and prevent queued commands from executing mid-startup.

* fix(terminal): keep a quick command queued until its own spawn takes it (STA-4876)

Triggering a quick command opened a terminal tab titled with the command's
label, the shell started and drew its prompt, and the command never ran.

TerminalPane snapshots `pendingStartupByTabId[tabId]` in a useState lazy
initializer, and a mount effect deleted the entry immediately. The pane's key
is `${tab.id}-${tab.generation ?? 0}`, so anything that bumps generation before
the command reaches a shell — the stall-recovery remount fired from
`requestTerminalPaneRecovery`, or the allDead activation regeneration — mounted
a second pane that re-read an emptied slot and spawned with no command at all.
The loss was permanent, which is why every scope failed alike: repo, global and
agent-prompt all funnel through the same queue-then-snapshot sequence.

Spend the entry at `onPtySpawn` instead, which is the one point that proves this
pane's own fresh spawn exists. A pane retired mid-connect never reaches it, so
the command stays queued for the next mount; reattach skips `onPtySpawn`, so it
cannot spend a command it never delivers.

Three details are load-bearing:

- Ownership is reference identity (`paneOwnsQueuedStartup`). Setup and issue
  splits borrow the same `deps.startup` field for their own one-shot payload, and
  that payload can be structurally identical to the queued command, so a
  truthiness test would let a split pane spend a command it never runs.
- The consume runs after `bindActivePanePty`. While the tab still has no ptyId,
  the queued entry is the only thing holding its worktree out of the
  retention-budget force-park, so dropping it first unmounts the pane mid-spawn.
- The callback is one-shot. `onPtySpawn` fires on every fresh spawn a pane makes,
  including hibernation wake and the respawn ladder, and a command queued after
  the first launch belongs to that later launch.

Known residual, documented at the call site: the consume tracks "a pty exists",
not "the command ran". Windows embeds short commands in the shell argv, so they
execute before the spawn resolves and a pane retired in that window re-delivers
on remount; on POSIX the write waits for shell-ready, so a pty that dies in that
window loses a command already spent. Closing either needs a delivery signal
from main rather than this callback. Both windows are narrow, and both are
strictly better than losing the command unconditionally.

* fix(terminal): guard the queued-startup wiring the review found untested

Follow-ups from the final review pass on this branch.

- Collapse the ownership + one-shot decision into `createQueuedStartupConsumer`
  so the call site is a single call rather than inline logic no test could
  reach. Two mutants survived the whole suite before this: relaxing ownership to
  a truthiness check, and dropping the one-shot guard. Both now fail.
- Rewrite the throwing-consume test. It asserted `updateTabPtyId` had been
  called, which runs *before* the callback, so it passed with the try/catch
  deleted. It now asserts the throw does not escape into the connect promise,
  which is the invariant the try/catch actually provides.
- Correct the `onQueuedStartupSpawned` docblock. It claimed the callback is "the
  first moment the command is guaranteed to reach a shell"; the diff's own caveat
  says otherwise, since Windows runs an argv-embedded command before this fires
  and a POSIX shell can die before the shell-ready write. It marks a live shell,
  not delivery.

No behavior change: the consumer is the same predicate and the same one-shot,
moved behind one exported seam.

* fix(terminal): roll shell wrapper isolation into a fresh daemon

* Revert "Increase shell readiness timeout to match daemon barrier"

This reverts commit 6ab273ef36.

* Condense queued startup spawn comment

Simplify the multi-paragraph explanation into a concise summary that captures the key points: spawn must wait until after the pane is bound to preserve the worktree from force-parking, and the behavior differs between POSIX and Windows for delivery timing.

* fix(terminal): prevent consuming replaced queued startup commands

When a queued startup is replaced before the pane's first spawn,
the one-shot guard alone still allows consuming the replacement.
Add isStillQueued callback to verify the slot still holds the
originally captured command (STA-4876).

---------

Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
2026-08-20 12:15:08 -07:00
Jinjing d8e9fa1bb9 Revert "fix(terminal): apply pane padding on all four edges (#15544)" (#15623)
This reverts commit 4b2ed5ddd4.
2026-08-20 09:57:55 -07:00
Neil feaebabf2c test(terminal): pin the two conditions that keep macOS period substitution out (#15393)
macOS rewrites a double space into ". " and hands the period to the pty.
Orca is not immune to that: a plain Chromium textarea in the Electron
version pinned here does substitute, measured on hardware with real key
events, and spellcheck="false" does not prevent it. What prevents it is
that the forwarder claims a plain space keydown and then empties the
helper textarea, so the text system never sees the word preceding the
space.

Neither half was a decision. Before 01bcc8dca2 the claim predicate
excluded space, letters and digits, the field accumulated, and the
substitution fired on real hardware. That commit widened the predicate
for unrelated reasons - IME commit survival and kitty encoding - and
suppressed this as a side effect it never mentions. The predicate
already declines on modifiers and during composition, so a later
narrowing would return the bug with nothing to catch it.

This does not test the substitution, which a unit environment cannot
produce. It tests the two conditions measured to suppress it. Verified
non-vacuous: narrowing the predicate back toward punctuation fails three
of the four, and removing the blanking fails the fourth.

Refs #11504
2026-08-20 03:08:29 -07:00
NeilandBrennan c92f394cde fix(pty): delete the reply-withholding scheduler (#15578)
* fix(pty): answer a terminal colour query in its own turn

Root-cause follow-up to #15559, which stopped a CPR overtaking a deferred
colour reply but left the deferral itself in place.

Orca answers terminal queries by writing to the PTY master, which a line
discipline in ECHO copies straight back out as junk on a cooked prompt
(#12112). The guard was to withhold the write until an `stty` subprocess
proved ECHO clear — and forking is what forced the decision to be async.
Any deferral, however short, lets a reply written later in the same turn
overtake this one, so the async probe was the bug's root cause.

Read the bit synchronously instead. Linux and the BSDs redirect a
master's mode ioctls to the slave, so a `tcgetattr` on the master fd
node-pty already owns answers for the slave with no fork: measured 0.26us
against 2403us for the subprocess. With a verdict available inline, a
querying program that already cleared ECHO — every raw-mode prober,
including the colour probe behind the `gh auth login` report — is
answered in its own turn and can never be reordered.

The deferral stays for the genuinely cooked case, and the ordering
guarantee stays underneath it: hosts whose node-pty predates this patch
get no sync probe and fall back to the deferred path, which mixed
client/host versions make a live production path.

Reply routing is all-or-nothing: a payload needing neither containment
nor ordering stays on the host's own path, so a CPR answered during shell
startup cannot pass the daemon's post-ready flush gate and splice into
the buffered startup command.

Native side is fail-safe: a kernel that did not redirect would answer
from the master's own termios, whose ECHO defaults set, so the degraded
verdict is "echoing" — never a false "quiet". The JS half ships in the
pnpm patch while the binding needs a source build, so
ORCA_REQUIRE_NODE_PTY_ECHO_STATE=1 makes CI fail rather than silently
skip when it is handed an upstream prebuild.

Co-authored-by: Brennan <brennanb2025@users.noreply.github.com>

* fix(pty): keep the flush ordered under synchronous re-entry

Three defects found in external review of the reply-ordering work.

node-pty delivers onData inside the master write, so a query can be
answered while the queue is mid-flush. `flushPendingWrites` spliced the
array off before writing, so that reply saw an empty queue, took the
same-turn path, and landed ahead of entries the loop had not written yet
— reproduced as 01, 99, 02, 03. It now shifts one entry at a time so a
re-entrant reply queues behind the rest, bounded by the length at entry
so a re-entrant push cannot spin the loop.

An overflow flush can re-enter as far as teardown. `answer` did not
re-check `closed` afterwards, so it queued behind a closed delivery,
returned true, and the reply was never written and never reported.

The payload router's ownership comment overstated its guarantee. The
`any` semantics are deliberate — returning false after a constituent was
already written would have the caller re-write the whole payload and
duplicate it into the child's stdin — so the residual mixed-failure drop
is now documented rather than implied away.

* fix(pty): delete the reply-withholding scheduler

Orca answered a terminal query by withholding the write until a probe
proved the slave's ECHO bit was clear. That was the wrong mechanism, and
it is now gone: replies are written in the caller's turn and their echo
is contained on the output side, where it always was.

Withholding never removed an echo. The wait was bounded and always ended
in a write, so the output-side projections were doing the work the whole
time — including the readline rewrite, which happens with the tty already
raw and which therefore no reading of the ECHO bit can predict. What
withholding did add was an asynchronous write path, and that is what let
one reply overtake another and land in the next program's stdin (#15559),
what produced a re-entrancy inversion inside its own flush, and what four
rounds of regressions have lived in.

The last thing it covered was the verbatim echo of a `stty -echoctl` tty.
That shape is now projected directly. It starts with ESC, so it is
matched only when complete and never held as a partial: holding it would
take a bare trailing ESC from the query parser and an expired hold would
release it raw, so a query torn at its own ESC would never be answered.
Complete-match-only is what makes the shape safe to project at all.

Measured on a real pty: a cooked-mode master write is both echoed AND
delivered — ECHO copies the bytes without consuming them from the slave's
input queue, so a program arming raw mode with TCSANOW/TCSADRAIN (libuv's
setRawMode, hence every Node agent) still reads them. Only a TCSAFLUSH
switcher discards it, which it does on every terminal, none of which
gates a reply on termios state.

Deletes the pending-write queue, the async stty probe, the poll budget
and probe rate limit, the deadline-driven flush, and the answer/
answerInOrder split. Replies now leave in call order by construction.
No packaging, native or CI surface is touched.

* test(pty): restore stty-probe coverage and pin the duplicate-query retry

Archaeology on how withholding got here, and what its tests were really
protecting.

Deleting the ECHO probe took four tests with it that were not about the
probe at all: they cover createSttyProbe, which the shell-readiness
line-editor probe still uses — in-flight sharing, the per-platform stty
flag, and transient-versus-permanent failure latching. Restored against
the line-editor probe, which is now their only caller.

Also pins the property that answers the one case an immediate write
cannot serve. A program that queries while cooked and then arms raw mode
with TCSAFLUSH discards the reply with the rest of its input queue.
Nothing can prevent that from the terminal side, and no terminal tries.
What matters is that such a program re-queries after its own timeout: the
ingress declines to answer an already-answered slot but forwards the
duplicate downstream, so the renderer's emulator answers the retry, by
which point the program is raw. The retry path is the recovery, not
withholding.

* ci(pty): keep the fish real-PTY test in the shell-contracts lane only

Reverting pr.yml to main dropped the exclusion for the fish query-reply
test, which this branch keeps, so it would have run in the sharded lane
as well. Restores it to the shell-contracts include list and the shard
exclude list, and drops the parallelism expectations for the deleted
cooked-querier suite and the echo-state env guard.

---------

Co-authored-by: Brennan <brennanb2025@users.noreply.github.com>
2026-08-20 02:15:42 -07:00
Brennan Benson 3e079debec fix(sidebar): host-qualify discovery notice rows on multi-host projects (#15546)
* fix(sidebar): host-qualify discovery notice rows and collapse one checkout's twins

A project checked out on several hosts emits one discovery-notice row per
checkout, and those rows only named the project. A sidebar with paired remote
hosts therefore showed several identical "N hidden worktrees" buttons under one
project header, with no way to tell which machine each belonged to — or that
one of them was another machine's worktree inbox entirely.

Two causes, both fixed here:

- Notice rows carried no host context, unlike worktree rows, which have been
  host-labelled since STA-4343. Both notice rows now take a host label, applied
  per project (not per rendered section, since a card can land in the pinned
  fallback) and only when that project spans hosts. The label also lands in the
  review, expand, and dismiss accessible names, so the actions that write to a
  specific host's repo record say which host that is.

- One machine registered as a direct SSH target *and* paired as a runtime
  environment gives a single on-disk checkout two repo records with independent
  hidden-worktree state, so it emitted two rows for one directory. Repos now
  resolve to a (hostname, path) checkout key, and twins collapse to the record
  this client persists itself — its visibility state is the user's own and
  survives the paired runtime going away.

The key is deliberately conservative: an unresolved hostname, or a tunnelled
environment answering on loopback, yields no key and never collapses anything.
Renderer-only; no wire or persistence change.

* fix(sidebar): drop the machine-identity collapse, gate notice labels on host ids

Replaces this branch's second change after a plan review found it has no
precedent and eight concrete failure modes.

Deleted: the (hostname, path) checkout key that collapsed two repo records
believed to be one machine. Orca models a direct SSH target and a paired
runtime environment as different execution hosts everywhere else; that change
asserted sameness by resolving strings a user typed in two places. It also
dropped rows (a differing count vanished with the shadowed record), flipped
with the sidebar host filter, ignored port and user so a host and a container
on it could merge, tie-broke on repo-store order, was disabled in the one case
Orca can prove (a tunnelled pairing answers on loopback) and fired only on
coincidence, and left the visibility dialog showing state the sidebar had
hidden. Its module also carried a literal NUL byte, so git classified the file
as binary and the diff was unreviewable.

Kept, with two corrections: notice rows still carry a host label, but the gate
now counts distinct host ids rather than distinct label strings — two hosts
sharing one user-facing label is exactly when the rows are hardest to tell
apart — and membership is read from the unfiltered repo universe rather than
the host-filtered notice candidates, so a label no longer appears and
disappears with the filter.

Two hosts that share a label still render the same label. Disambiguating that
is a shared concern across worktree badges, host headers, and host-filter
options, and needs its own design; three verification passes each found a
different hole in doing it here. Follow-ups: general host-label collision, and
the repo-record duplication that produces the twin rows in the first place.

* fix(i18n): catalog notice host scope copy

* feat(sidebar): show each notice row's host with the project-on-host glyph

Notice rows on a multi-host project already carried a host label, but two
hosts can share one user-facing name, and the label truncates first in a
narrow sidebar. Each row now also carries its host's glyph.

Deliberately the same indicator worktree cards use (worktree-card-header):
a Server glyph, ServerOff when a paired runtime has no live status, and a
"Project on ..." tooltip naming the host — SSH and runtime keep their
distinct tooltip wording. Local hosts draw nothing, as on the cards.

The glyph is shrink-0, so unlike the text label it survives the sidebar
narrowing, and the row keeps an identifying mark either way.

Rows now carry the host id alongside the label, since the label alone cannot
select a glyph or its tooltip. Catalog entries for the new copy ship with the
change rather than relying on inline fallbacks.

* refactor(sidebar): draw notice-row hosts with the shared host glyph

Follow-up to the notice-row host indicator: use the one glyph vocabulary the
app already has instead of a second copy of it.

HostRowIcon — a monitor for this computer, a server for anything remote — was
private to the composer's run-target rows. Moved to a shared home and reused,
so the sidebar and the composer cannot drift apart. The run-target module
re-exports it, leaving its own call sites untouched.

Every notice row now gets a glyph, local included, so no row is the odd one
out; the tooltip still names the host and says when a paired runtime has no
live status. Same size and tone tokens across kinds, so no row reads as
decorated relative to its neighbours.

* fix(sidebar): make notice host glyphs accessible
2026-08-20 01:38:28 -07:00
Brennan Benson 4b2ed5ddd4 fix(terminal): apply pane padding on all four edges (#15544)
* fix(terminal): apply pane padding on all four edges

Move the configured inset onto xterm so the terminal fills its pane while the fit calculation accounts for both sides of each axis. Add a geometry golden that forces cell remainders and verifies dynamic padding without relying on renderer pixels.

* fix(terminal): normalize imported padding for fitting

* fix(terminal): align stored and fitted padding
2026-08-20 01:07:56 -07:00
Brennan Benson 2e895da937 fix(browser): name the requesting frame and the permission in denial notices (#15542)
* fix(browser): name the requesting frame and the permission in denial notices

Two defects in the same notice, both found by the review of #15481 and left out
of it deliberately.

The notice named the wrong site. setPermissionRequestHandler passed
webContents.getURL(), which is the top-level document, so a permission request
from a cross-origin sub-frame was attributed to the embedder. Every
PermissionRequest variant carries requestingUrl, so all three call sites now use
it and fall back to the top-level document only when it is absent.

The notice also showed raw Chromium permission names. humanizePermission mapped
two permissions and returned the raw token for the rest. That now matters more:
#15481 granted ordinary storage-access and left top-level-storage-access denied,
making it the storage denial a user can still hit - rendered as its raw token.

The default still returns the raw token. Inventing prose for a permission nobody
has seen is worse than showing its real name.

Does not change any permission verdict, and does not fix Google sign-in (#15221).

* fix(browser): keep permission denial attribution accurate

Capture fallback URLs before asynchronous media permission handling and treat opaque requesters as unknown rather than blaming the top-level page. Clarify permission descriptions and cover origin normalization, navigation races, and mapped copy.
2026-08-20 00:22:59 -07:00
Brennan Benson 9a898a84cc fix(terminal): keep xterm's render pause latched for a pane with no layout box (#15555)
* fix(terminal): keep xterm's render pause latched for a pane with no layout box

resetWebglTextureAtlas() released xterm's paused-render gate for every pane of
a visible manager, including panes that are display:none (a collapsed sibling
of an expanded pane, a restore that stays display:none for its whole reattach).

forceRepaintThroughRenderPause exists for a pane that is already DOM-visible
while xterm's IntersectionObserver lags a frame. On a pane with no box it
paints the freshly cleared render model into nothing, and because the observer
only fires on a state change it never re-pauses the service. It also clears
_needsFullRefresh, which is the only thing that makes _handleIntersectionChange
repaint on reveal and flush the deferred _pausedResizeTask.

Latch instead for those panes: terminal.refresh() re-arms _needsFullRefresh and
xterm repaints from it on reveal.

* test(terminal): type the render-service repaint mock
2026-08-19 23:25:47 -07:00
Neil 4daace6251 fix(tabs): stop split workspaces multiplying columns on return (#15482) 2026-08-19 22:54:03 -07:00
Jinjing d541982b9c Improve cmd j search usefulness (#15551)
* Show last active time for workspace tabs in cmd+j palette

Adds session-age formatting and activity tracking to help users find
recently-used tabs. Replaces host badge display with last-active timestamps
that reflect either agent activity or worktree PTY activity, whichever is
more recent.

* Improve cmd+j search ranking with direct fields and recency

Prioritize results matching direct fields (titles, content) over
container fields (worktree, branch, repo). Use tab focus time to break
ranking ties. Makes search more useful for quick navigation.

* Extract path flavor logic to cross-platform-path utility

- Remove local pathFlavor function in favor of shared cross-platform utilities
- Simplify buildExcludePathPrefixes to use relativePathInsideRoot and resolveRuntimePath
- Ensures consistent path handling for both local and remote roots

* Improve cmd+j search ranking with recency-based tiebreaking

Track lastFocusedAt on tab creation/focus and use it to break ties between equally-ranked search results. This surfaces recently-used items first, improving search utility. Also fixes hasDirectHit to check field matches directly rather than evidence metadata.
2026-08-19 19:46:45 -07:00
Brennan Benson 6ac79ef0aa fix(workspaces): settle an orca.yaml trust prompt when the modal slot is taken (#15540)
* fix(workspaces): settle an orca.yaml trust prompt when the modal slot is taken

The app has one modal slot, so any openModal/closeModal evicts whatever
held it. A pending orca.yaml trust prompt owns the promise that quick
create awaits, and eviction dropped its resolver: that submit never
settled, and because the trust prompts are serialized on a module-global
chain, every later create/remove in the session silently did nothing.

Modal data can now carry an onModalDismissed callback that the slot
invokes when it evicts an entry; the trust prompt uses it to resolve as
'skip', which is what dismissing the dialog already means.

* fix(workspaces): make trust prompt settlement one-shot
2026-08-19 18:12:29 -07:00
Neil cb95582cea feat(release): build unsigned Windows artifacts for the dev channels (#15465) 2026-08-19 17:34:27 -07:00
Jinwoo Hong 0e5b348414 fix(repos): keep the desktop-owned manual project order authoritative across paired clients and SSH catalog publishes (STA-4850) (#15538) 2026-08-19 17:26:16 -07:00
Jinwoo Hong fcdbcf85d0 fix(terminal): keep a cold-parked pane's runtime-graph leaf while its PTY lives (STA-2854) (#15514) 2026-08-19 15:47:31 -07:00
Jinwoo Hong 7675da363e fix(remote): stop paired-tab resurrection, ghost agent rows, and frozen visible panes (STA-4593) (#15459) 2026-08-19 15:31:26 -07:00
Jinwoo Hong 174039a14e fix(workspaces): re-seed a terminal when an emptied workspace is opened (#15513) 2026-08-19 14:58:00 -07:00
Brennan Benson 73f7767edd feat(sidebar): name the host when a delete batch collides on one id (#15423)
* feat(sidebar): name the host when a delete batch collides on one id

Two hosts can publish the same worktreeId, so a batch confirmation showed two
rows with identical names and paths and nothing to tell them apart. Label each
row with its execution host, but only when the batch actually contains a
same-id collision — an unconditional chip is noise.

* fix(sidebar): qualify colliding delete targets by saved host

* fix(sidebar): preserve delete host collision scope
2026-08-19 01:33:29 -07:00
Brennan Benson 391385fd5c fix(workspace-cleanup): require a host-qualified key for every candidate row (#15422)
The virtualizer keyed rows by bare worktreeId, so two hosts sharing an id
collapsed onto one key in the confirm-remove dialog. getRowKey is now a required
prop supplied host-qualified by each caller, making omission a compile error
rather than a silent fall back to the colliding id.
2026-08-19 00:29:11 -07:00
Neil d4460d34f3 fix(terminal): stop a Hangul-terminating digit being eaten as a candidate pick (#15429)
* fix(terminal): stop a Hangul-terminating digit being eaten as a candidate pick

A digit typed immediately after a Hangul syllable is dropped in the
terminal on Wayland. Typing 아1 produces 아. The syllable composes and
commits correctly; only the keystroke that ends it is lost.

Reproduced on Ubuntu 24.04, GNOME Shell 46, ibus-hangul 1.5.5, in a
nested Wayland session with real key injection. The pty receives 아 on
Wayland and 아1 under X11 on the same machine with the same engine, and
a GTK client in the same Wayland session receives 아1 - so the
compositor, the input method and the digit are all behaving.

The difference is where the digit is delivered. Under X11 ibus-hangul
swallows it into the preedit and commits 아1 as composition text. Under
Wayland it commits 아 and lets the digit through as an ordinary key -
and the jamo before it arrive with no keydown at all, only keyups.

That orphaned keyup is what breaks it. A plain letter keyup with no
matching keydown arms a 1500ms window in which a bare digit is treated
as a candidate selection, because a Pinyin engine indexes its candidate
list by digit. The digit lands microseconds later, inside the window,
and is suppressed. The window disarms after exactly one digit, which is
why the syllable survives and only the terminating key vanishes.

A Hangul engine has no numbered candidates over its preedit - a digit
ends the syllable and is literal text - so the guard was spending its
one suppression on a keystroke meant for the shell. The tracker now
records whether the current preedit is Hangul and the guard declines.

Read from compositionupdate rather than compositionend: a Pinyin preedit
is the Latin spelling being narrowed while its commit is the Han text,
so reading the commit would misclassify Pinyin and reopen the bugs the
guard was added for. Space is untouched; only digits are reclassified.

Verified A/B/A on that machine: unfixed drops the digit, this change
preserves it, reverting drops it again, three runs each. The X11 case
and both existing ibus-hangul specs still pass.

Closes #15299

* fix(terminal): expire the Hangul preedit flag and narrow its digit exemption

Review of the #15299 fix found the Hangul classification could not be
retired and that it exempted more than the reported bug.

The flag was written only on a non-empty compositionupdate and cleared
only on blur, so it latched for the whole focus session. Switching input
engine (Hangul -> Pinyin) under fcitx/ibus moves no DOM focus, and the
#8241 orphan-digit path emits no composition and no input events, so
nothing could refresh or clear it. The stale flag then turned the
#8241/#7543 Pinyin candidate-digit guard off silently and permanently -
the regression those guards exist to prevent. It now expires against
lastCompositionEventAt on the same staleness window as the neighbouring
guards, and compositionstart clears it because the following
compositionupdate re-reads the preedit script.

It is deliberately not cleared on compositionend or on input: the bug is
a digit arriving after the commit, and both recordings deliver the
commit's own insertText before it. Clearing on either reverts the fix -
the orphan-window test fails when input clears it.

The exemption also gated suppressCandidateKey as a whole, so it fired
during a live composition as well. That is broader than the bug, which
is exclusively a digit after compositionend, and it is unsafe: the
earlier claim that no Hangul engine indexes candidates by digit over a
preedit is wrong. ibus-hangul's Hanja conversion (Hanja key / F9) puts a
numbered lookup table over a live Hangul preedit, and its symbol table
behaves the same. Only the orphan-keyup guard - which arms off a bare
keyup and cannot see which engine produced it - now declines.

The tests drove a shape neither recording produces: an empty
compositionupdate immediately before compositionend, which armed the
250ms post-composition window. The Wayland reproduction has no empty
update, and the X11 fixture follows its empty update with
deleteContentBackward + insertText, which disarms that window. The suite
now follows the recorded trace, asserts the 250ms window never arms, and
covers both the expiry and the live-preedit Hanja case.

Refs #15299
2026-08-18 23:15:12 -07:00
Brennan Benson 12724068b7 fix(sidebar): render folder workspaces under every Group by mode (#15362) (#15404)
* fix(sidebar): render folder workspaces under every Group by mode (#15362)

Folder-workspace rows were emitted in exactly one place, inside the
repo-grouping branch of buildRows, so switching the sidebar to Status,
PR status or None dropped them from every lane.

Membership is now decided once, above the groupBy switch; a mode only
chooses which lane a workspace lands in. Lane assignment is an exhaustive
WorktreeGroupBy switch with no default clause, so a future grouping mode
is a compile error here rather than a silent regression.

Not a regression: the groupBy !== 'repo' gate predates folder workspaces
by two weeks (#2866), and #5172 added folder-workspace emission below it,
unreachable for non-repo grouping from its first commit.

Also fixes four further paths where the same row kind went missing:

- Flat mode gated its whole section on worktree count, so an account with
  only folder workspaces rendered nothing at all.
- A collapsed folder-only lane header rendered globally instead of under
  its host section, because empty worktree lists yield undefined host
  counts. Host id maps now carry explicit empty arrays so the host
  fallback cannot leak global worktree ids.
- Reveal resolved folder workspaces to project-group header keys only, so
  revealing one inside a collapsed lane never expanded it. Reveal now
  reuses the same lane function as grouping, expands the host header, and
  covers the agent-send path and the host-qualified row lookup.
- The Clear Filters empty state ignored folder workspaces, so a
  folder-only account lost them whenever any filter was active. This one
  reproduced under Project grouping too.

Behaviour deliberately unchanged: archived folder workspaces are still
not filtered, matching current behaviour rather than making membership
mode-dependent. Pinned folder workspaces still render in their natural
lane rather than the Pinned section, now uniformly across modes.

* fix(sidebar): count folder workspaces in host badges
2026-08-18 19:24:38 -07:00
Brennan Benson c228030516 fix(tabs): keep an open diff focused while agents stream (STA-4697) (#15390)
* refactor(tabs): put the visible-tab-type projection in one place

Three copies of toVisibleTabType had drifted: the runtime one omits 'simulator'.
Move the canonical projection next to the two unions it maps between and replace
the two copies that are already identical to it. The runtime copy is left alone
on purpose - unifying it would change behavior, so it goes with the follow-up.

* fix(tabs): keep an open diff focused while agents stream (STA-4697)

resolveWebSessionVisibleTabId answered 'which tab is the user looking at' by
inverting a many-to-one projection: it compared tab.contentType against the
coarse activeTabType. Diff tabs open with activeTabType 'editor' but carry
contentType 'diff', so the match never succeeded and the guard returned null -
which is the reconciler's signal to fall through and activate a terminal. Every
agent status echo republished the snapshot, so the diff lost focus ~300ms after
opening, once per click. Same for conflict-review and check-details.

Resolve the visible tab from group state instead, which is what is actually on
screen and is the rule deriveActiveSurfaceForWorktree already uses. The coarse
address survives only when there are no group records, now projected rather than
compared exactly.

Also follow the entity within the group when reconcile rematerializes the visible
tab under a new id, and teach the browser-create focus guard to observe the group
records the resolver now reads.
2026-08-18 18:20:56 -07:00
Jinwoo Hong 8612a2dabb Fix skill install dialog overflow and simplify file summaries (#15406) 2026-08-18 18:10:00 -07:00
Jinjing 3ac4171d14 fix(cmd-j): host-qualify worktree resolution and disambiguate list render keys (STA-4343) (#15371)
* fix(cmd-j): host-qualify worktree identity to fix STA-4343

- Same repo::path across hosts now render as two distinct rows, each resolving
  to its own worktree on activation — closes wrong-host activation bug.
- Disambiguate repeated persisted entry ids with render-key suffixes so React
  never leaves ghost rows mounted; prevents frozen display glitches when a
  session holds duplicate tab records.
- Collapse query whitespace runs, treat emoji/symbols as content, deduplicate
  query tokens, and improve CJK compound matching in palette search.
- Track filter-field cursor by option id rather than position so re-ranking
  after a toggle doesn't snap to the wrong row.
- Dedup tabs by id at hydration so corrupt sessions don't render duplicates.

* test(palette-search): add rankMode and current-state fields to palette o

Adapt test fixtures and function calls to include new rankMode parameter and
isCurrentPage/isCurrentWorktree tracking fields required by the palette search
algorithm fix.

* fix(palette-search): add host-qualified repo and worktree resolution

Handle repo and worktree collisions in multi-host environments by
querying host-qualified maps and comparing execution host IDs when
determining current worktree status. Ensures the palette correctly
displays repo identity and marks active status for worktrees across
multiple hosts.

* fix(cmd-j): namespace-safe duplicates and script-aware token matching

- Carve out `palette-dup:` namespace for duplicate keys to prevent collision with real persisted ids, which previously could match generated keys like `id#dup1`
- Gate reverse-containment token scoring to unsegmented scripts (Han, Hiragana, Katakana, Hangul), preventing false matches like "database" matching keyword "base" in Latin text
- Host-qualify worktree identity in empty-query ordering to allow switching between same-id worktrees on different hosts, which were previously treated as duplicates (STA-4343)

* Shorten comments in palette-list-entry-render-keys

Replace verbose comments with concise one-liners per style guide.
2026-08-18 15:32:36 -07:00
Brennan BensonandQA f71ef7ee03 fix(sidebar): clear the delete state when a removal is refused (#15187)
* fix(sidebar): clear the delete state when a removal is refused

Callers mark rows deleting up front for immediate sidebar feedback, but a
refusal in beginHostQualifiedRemoval returns before removeWorktree's
try/catch — the only other place that clears the flag. The failure toast
auto-dismisses after 10s, so the workspace was left on a 'Deleting…'
spinner indefinitely with no explanation still on screen.

Also removes workspace-cleanup-removal-host-guard.ts: #14731 refused a
colliding cleanup removal, #15013 replaced refusing with routing to the
confirmed host, and nothing imports the module any more — not even its
own tests.

* test(sidebar): pin the two states that actually reach the removal refusal

The existing case used an empty store, which proved the clear but not that a
user can land there. Cover a stale folder-workspace id and a hostless row under
an ambiguous legacy runtime — both resolve to no route from populated state.

---------

Co-authored-by: QA <qa@local>
2026-08-18 14:08:25 -07:00
Jinwoo Hong a77a2f93f7 fix(remote): search Quick Open paths on the host (#15158) 2026-08-18 13:32:48 -07:00
Neil 4b2c901b66 test(terminal): pin that the CJK block is the preedit overlay, not the cursor (#15242)
* test(terminal): pin that the CJK block is the preedit overlay, not the cursor

A report described the cursor sitting on a wide character's first cell
and hiding its right half, with cursor style and opacity settings
ignored. Neither defect reproduces.

Replaying the reporter's own captured byte stream leaves the cursor at
column 11, exactly where the application asked, with correct wide and
continuation cells. A block cursor also cannot hide half a glyph: it
inverts the cell and the syllable renders inside the cursor span.

The black block is the IME preedit overlay. macOS 2-set Korean keeps the
trailing syllable composing until a terminator, so it sits in an opaque
absolutely-positioned box over the grid rather than in the buffer. That
box took stock upstream colours, black on white. It explains what no
cursor theory can: the block appears at the composing cursor cell, no
cursor option reaches it, it is identical with GPU acceleration off
since it is a DOM node above both renderers, Latin never triggers it
because Latin opens no composition, and Enter clears it because Enter
commits the composition.

Already fixed by the overlay theming in #15014, which landed a day after
the reported release, so the fix ships in the next one.

Tests only, no production change. Two pin the negative results so the
cursor explanation cannot be re-derived, and one pins the actual
mechanism at end of row, beside the existing mid-line arm.

Separately confirmed and not fixed here: the WebGL renderer drops the
cursor colour's alpha, so terminal cursor opacity genuinely does nothing
for a block cursor, which is the default style on the default renderer.
That is in the webgl addon rather than in xterm or in our code.

Refs #12729

* test(terminal): make the cursor precedence assertion real and measure the overlay

Review of the first pass found one assertion that could not fail. It set
options.cursorStyle and then read decPrivateModes.cursorStyle, which are
separate fields with separate storage, so it pinned that writing one does
not clobber the other. Deleting the precedence expression from both
renderers left it green.

It now asserts the rendered cursor class: the option style renders, a
DECSCUSR overrides it, and the reset hands control back. That fails if
the precedence is removed.

The overlay's rendered width is the one measurement in the report that
argues against our explanation, and no test here could reach it, because
the unit environment performs no layout. Adds an end-of-row browser arm
beside the existing mid-line one, asserting a single composing Hangul
syllable spans about two cells. That settles whether the block the
reporter measured at one cell can be this overlay.

Also scopes two DOM queries to the test container rather than the
document, and attaches the render listener before writing so a missed
render fails instead of hanging to timeout.

Records in the file header what it does not establish: composing the
opacity into the theme is not the same as it reaching the screen, since
the webgl renderer drops the cursor colour's alpha for a block cursor.

Refs #12729
2026-08-18 02:35:22 -07:00
OrcaWinandOrcaWin 6e8da1df8d fix(wsl): pass guest argv verbatim through --exec (#15039)
* fix(wsl): pass guest argv verbatim through --exec

`wsl.exe <...> -- <argv>` expands `$name` in every argument against the
guest environment before the guest ever runs. It does this even when no
shell is involved, so `-- /usr/bin/printf %s '$HOME'` prints /home/you.

Every WSL invocation went through that preprocessor, so scripts arrived
already rewritten: `awk '{print $2}'` lost its field reference, and a
POSIX script asking for the literal `$HOME` got the expanded path.

`escapeWslShCommandForWindows` tried to compensate by escaping `$`, but
it skipped any `$` preceded by a backslash, so a script containing `\$`
was still corrupted -- and half the call sites never applied it at all.

Route every invocation through `--exec`, which passes argv through
untouched, and delete the escaper. The direct-git path already used
`--exec`, so this is not a new compatibility dependency.

A guard test fails if the `--` form reappears anywhere in the tree.

Net -50 lines of production code.

* test(wsl): drop remaining escaped-dollar assertions

* fix(wsl): cover the --exec migration's blind spots

An audit of every wsl.exe invocation found sites the first pass missed,
including two it actively broke:

- config/scripts/wsl-git-shell-benchmark.mjs imported
  escapeWslShCommandForWindows, which no longer exists, so the script
  threw on startup. Its wslShellArgs helper also still used `--`; the
  file already had an --exec helper, so route both call sites there.
- classifySubprocessCommand unwrapped `wsl.exe <...> -- <binary>` by
  breaking on `--` alone. With every Orca spawn now on --exec it never
  found the guest binary and bucketed all WSL subprocesses as plain
  "wsl", losing the git/gh/glab breakdown. Break on either separator,
  since foreign wsl.exe processes still use `--`.

CliSkillRuntimeSetup builds its setup command as a template literal
rather than an argv array, so no array-shaped search could see it. Its
decoder accepts both separators so commands persisted before this
change still decode.

The guard now scans config/ and tests/ as well as src/, and checks the
command-string spelling alongside the argv one — the two shapes that
have each shipped a regression. It skips comment lines so prose about
the old form stays allowed, and asserts it scanned a plausible file
count so a bad root cannot make it vacuous.

* fix(wsl): restore the guard's multi-line sensitivity

The guard matched line by line, so `'--',\s*'bash'` could not span a
newline -- and every argv array in this repo is formatted one element
per line, which is exactly the shape it exists to catch. Measured
against the pre-migration tree it caught 17 files before and 9 fewer
after. It now strips comment lines and matches the rejoined text, with
a case that pins the multi-line shape so this cannot silently return.

The program list is wider than shells now, which surfaced a false
positive: tmux takes a `--` separator followed by a program too
(`split-window ... -- cat`). Matching is scoped to files that mention
WSL rather than narrowing the list back.

Also:
- Replaced the `sed` regression case, which was vacuous. A backreference
  contains no `$`, so it returned `bac` under both separators and would
  have passed without the fix. The block claimed every case proved the
  bug. Swapped in a positional argument and a shell local, both measured
  to differ -- the positional is the shape `wslUncDirectoryExists` uses,
  where `--` blanked `$1` so every existing directory probed as missing.
- windows-shell-args.test.ts derived its expected argv from
  buildWslExecArgs, the helper under test, so six assertions would still
  pass if it regressed to `--`. Spelled the expectation out.
- Dropped two comments citing the removed `--` behavior as rationale.

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-08-18 02:12:21 -07:00
Neil 96d88dd5fe fix(tab-bar): restore the tab-results fixture after occupantAgent became required (#15277) 2026-08-18 01:49:57 -07:00
Brennan Benson 6414a3a2a8 fix(remote): keep the host's last assistant message on client-owned agent rows (#12906) (#14716)
* fix(remote): keep the host's last assistant message on client-owned agent rows

A remote pane has two writers for one agent-status key: this renderer's OSC
byte pipeline and the mirrored host session.tabs snapshot. When the client owns
the key, buildMirroredAgentStatusPatch keeps the client's entry wholesale and
copies only paneKey/worktreeId/tabId/providerSession off the host frame.

lastAssistantMessage is hook-only content the byte pipeline can never see, and
setAgentStatus writes payload.lastAssistantMessage straight through, so every
OSC write also blanks it. The host publishes the text and the client receives
it, then discards it on every republication — the remote agent row's message
line is permanently empty while identity and status render fine (#12906).

Adopt it across the fence the same way providerSession already is, falling back
to the mirrored value so a host that stops publishing the field cannot blank a
line it already delivered.

* refactor: mirror providerSession's coalesce shape and tighten the comment
2026-08-18 01:14:01 -07:00
Brennan Benson 7ba336099c fix(sidebar): name each split-pane agent row from its own pane title (STA-2811) (#14707)
Agent rows are per pane, but their conversation name came from `tab.title`,
which carries only the FOCUSED pane's title. In a split tab every row showed
one pane's name, and all of them changed when the user clicked a sibling.

Rows on a multi-pane tab now resolve their own leaf's runtime pane title via
the existing `resolveRuntimePaneTitleForLeaf`, and fall back to no live title
rather than a sibling's. Single-pane tabs pass `undefined` and are unchanged.

Extracts the subagent grouping out of build-dashboard-snapshot.ts, which was
exactly at the 300-line cap.
2026-08-18 00:47:14 -07:00
Brennan Benson 26bdfc0fe4 feat(agent-status): stamp observation provenance at every status ingress (STA-4293) (#14706)
Add an optional `observation` facet to agent status rows recording the origin
(hook | osc | title | process | launch | orchestration), the authority that
sequenced it, a per-pane incarnation, a monotonic revision, and the authority's
own clock. Stamp it at every ingress; no consumer reads it.

Boundary is stamped from the hook listener's existing per-provider
`isNewTurnEvent`, not a second list of event-name literals. Identity-only
(`providerSessionOnly`) rows are tagged `kind: 'identity-only'` so future
consumers do not each rediscover that they are not turn transitions.

The staleness-decay contract is documented at the type: staleness must be
computed against the same authority clock that stamped `observedAt`, or
replicas must decay on local receipt time. Not fixed here.

Behavior-neutral: optional field on existing JSON, never persisted, never
published to paired clients, and never inherited across writes.
2026-08-18 00:46:41 -07:00
Jinjing 7a695c70f1 test(e2e): harden triaged CI failures (#14656)
* test(e2e): harden triaged failures

* test(e2e): ship relay bundle to reusable shards

* test(e2e): tolerate expected IPC closures in daemon shutdown

A normal client exit can close the IPC channel before the finish ack
lands. Distinguish this from real failures by checking error codes,
only throwing if forced cleanup occurred or the error is not an IPC
closure.

* rm doc

* test(e2e): return termination status from legacy close handler

- terminateLegacyCloseClient now returns a discriminated union indicating
  whether the process had already exited ('already-exited') or termination
  was actually attempted ('termination-attempted')
- Allows finishLegacyCloseClient to only set forcedCleanup when termination
  was genuinely needed, not when the process exited cleanly on its own

* test(e2e): fix dispatch contract and voice mic locator

Point the release E2E contract at the renamed build step, and assert the
relabeled microphone through the Voice pane combobox even when Radix
leaves the listbox open.

* test(e2e): add contract test for relay artifact dispatch

Validate that the relay artifact built in CI is properly uploaded,
downloaded, and passed via ORCA_RELAY_PATH to E2E test runs.

* Distinguish between terminated and already-exited processes

Detect when processes have already exited instead of always reporting
termination success. Return booleans from cleanup functions to indicate
whether they actually signalled a process, catch tree-capture failures
when the root process exits before recording completes, and use these
signals to return accurate exit status from termination handlers.

* test(e2e): stabilize file creation and voice microphone tests

Use stable locators (aria-autocomplete, named triggers) and add retry
logic to handle file scans and device events that can interfere with
listbox state. Increase timeouts to allow async operations to complete.

* Add retry logic for transient GitHub API errors in PR body updates

GitHub API occasionally returns transient 5xx errors. Retry up to 3 times
with exponential backoff (1s, 2s, 4s) to improve reliability during
temporary service disruptions. Export updatePullRequest and add sleepImpl
parameter for test injection.

* Add tab search result retention during typing

Keep search results on screen while the deferred query catches up with
the live query. Re-validates results against the current input without
dropping rows prematurely, ensuring the user can select from what they see.

* Add proper types to tab search mock

Replace `unknown` with concrete types (`OpenTabSearchResult`,
`OpenTabSearchEntries`, `SearchableWorkspaceTab`) and use type guards
for discriminated unions to improve test type safety.
2026-08-17 23:28:38 -07:00
Neil 2b057eb21e fix(terminal): end a url at CJK punctuation instead of swallowing it (#15240)
Terminal URL detection treated every non-ASCII character as part of the
url, so Japanese or Chinese text written straight after one was absorbed
into the link. A line like

  PR: https://github.com/org/repo/pull/12345(作成済み・マージ待ち)

underlined the whole run and opened the annotation percent-encoded onto
the end of the url, which 404s.

The body terminator listed only ASCII codes, so nothing at or above 0x80
could end a url.

Terminating on all non-ASCII is the obvious repair and is wrong: a url
path may legitimately carry unencoded CJK, and the wrapped-url tests
cover exactly that - a real multi-line url whose continuation row is a
Chinese path segment. That repair was written first and broke four of
them.

The distinction is punctuation, not ASCII. Non-ASCII punctuation,
symbols and separators end a url; letters do not. Full-width brackets,
an ideographic space, a full-width comma and the katakana middle dot are
prose; 文档 in a path is not.

Adds the extraction test file the module never had, covering the
reporter's three cases, the CJK-path case that must keep working, and
ASCII behaviour as a regression guard.

Closes #10571
2026-08-17 22:26:50 -07:00
Neil d143922561 fix(terminal): deliver an IME commit the deferred textarea diff missed (#15198)
Picking a single Chinese character from the candidate window with a
number key loses it. The character flashes and disappears. Picking the
same candidate with the mouse works, and picking multi-character words
with number keys works.

Two paths can deliver an IME commit, and this falls between them. A
keydown the input method consumed routes into a setTimeout(0) diff of
the helper textarea, and that diff is what normally delivers the commit;
xterm's _keyDownSeen guard exists to defer to it. When the commit
arrives after that timer has already run, neither path delivers. Mouse
selection works because no key is down, and a real composition session
works because it takes a different path entirely. That narrows it to an
input method whose commit round-trips asynchronously and which shows no
in-application preedit.

Track that a consumed keydown still owes its commit, and deliver only
when the diff did not. The upstream guard and its single read site are
untouched, which is what keeps the duplicate-commit behaviour it was
added for sealed.

Not doing the obvious repairs deliberately: clearing the flag, skipping
it for keyCode 229, or setting it after the composition short-circuit
each unblock the input path without retiring the diff, and all three
were measured emitting the character twice.

The patch and the lockfile hash here are generated. Review
config/patches/xterm-src/@xterm__xterm@6.1.0-beta.287.src.patch, which
is the hand-written source of the change; the shipped patch and both
minified bundles are the regenerator's output from the pinned upstream
build, so nothing in this change was hand-transcribed into a bundle.

Refs xtermjs/xterm.js#6036
Closes #12099
2026-08-17 22:13:32 -07:00
Jinjing cdd3aabdd8 Display occupant agent icons in tab search results (#15134)
* Display occupant agent icons in tab search results

When searching for or viewing open tabs, terminal tabs now show the icon of their occupant agent (e.g., grok) instead of a generic terminal icon. This makes it clearer which tabs have agents actively running. The occupant resolution reuses the same logic as the tab-strip agent identity system, including support for launchAgent, hook status, sleeping sessions, and OSC title parsing.

* Simplify tab occupant agent resolution to use unified label only

Remove the recordTitle parameter and rely solely on the unified title,
which already carries live OSC titles. The terminal record's own title
can stay stale (e.g., "Terminal N") while the unified label reflects
the current state, eliminating duplication and simplifying the contract.

* Add occupantAgent field to workspace tab helper
2026-08-17 21:57:31 -07:00
Jinjing 314b02ba2d Redesign artifacts page as full-width table with drawer (#15233)
* refactor(artifacts): redesign as full-width table with detail drawer

- Artifacts list displays as a compact data table with columns (Name, Type, Size, Updated, Expires)
- Selected artifact opens in a right-side drawer instead of inline preview
- Search and refresh consolidated in top toolbar
- Better space utilization for browsing the artifact list

* refactor(artifacts,automations): extract shared list-table layout

- Extract common list-table styles (container, header, row) to @/lib for
  consistency across artifacts and automations tables
- Move row interaction utilities to @/lib/list-row-interaction for reuse
- Fix drawer width to calc(100vw-80px) to avoid macOS traffic-light controls
- Extract WINDOW_CONTROLS_WIDTH/HEIGHT constants so portaled surfaces avoid
  the Windows/Linux overlay without hardcoding pixels
- Clamp artifact search query to 2KB to prevent multi-MB pastes from pinning
  renderer memory
- Remove unused artifact list visual mock

* Extract shared artifact row actions and use CSS var for traffic lights

- Unify dropdown and context menu actions via artifactRowActions() to prevent
  them from diverging during future maintenance.
- Replace hardcoded 80px with platform-aware CSS variable
  (--mac-traffic-lights-width) so only macOS reserves space for traffic lights;
  Windows and Linux controls sit on the right edge instead.
2026-08-17 21:55:57 -07:00
Neil f48bdf8f59 fix(new-workspace): keep workspace creation reachable with zero projects (#15234)
The sidebar +, the landing Create button, the board lane +, the palette
create row, and the tour CTA all disabled themselves when repos was
empty — a dead end, since the composer's project field can add the first
project inline and auto-select it.

Drop the repo-count gate from every entry point. Submitting without a
project still shows the inline "Choose or add a project" error.
2026-08-17 21:47:29 -07:00
Jinwoo Hong 19ba83d496 docs(mobile): add Android APK install guidance (#14978) 2026-08-17 21:38:25 -07:00
Neil 598ba5d276 fix(terminal): stop the macOS IME forwarder from running on iPadOS (#15218)
* fix(terminal): stop the macOS IME forwarder from running on iPadOS

Korean typed into a terminal from an iPad web client arrives as loose
jamo instead of composed syllables.

The native-text forwarder is a macOS workaround: it claims a printable
keydown and delivers the input method's substituted text from the input
event alone. It stands aside for IME composition by checking isComposing
and compositionstart. Touch iOS/iPadOS is unreliable about firing those
for hardware-keyboard CJK input, so each jamo keydown is claimed as its
own one-shot substitution rather than deferred to xterm's composition
handling.

It runs there at all because every iOS user agent contains "Mac" -
"Macintosh" in iPad desktop mode, the default since iPadOS 13, and
"like Mac OS X" in mobile mode. maxTouchPoints is the only signal a
real Mac never sets. The Linux branch immediately below already makes
the mirror-image exclusion for Android and CrOS; the Mac branch never
got the same treatment.

Gate the forwarder install only. isMac keeps its other five consumers -
the Ctrl+C interrupt, clipboard bypass, JIS yen input and the standalone
229 keydown policy - which intentionally still treat an iPad with a
hardware keyboard like macOS, matching iPadOS shortcut conventions.

Without the forwarder, xterm's own composition path plus the deferred
textarea diff is the sole delivery mechanism, which is already the
arrangement on Linux.

Not verified on hardware: the claim that composition events are absent
on iPadOS is the mechanism the code supports and the only one matching
the reported symptom, but no on-device capture confirms it. The platform
detection stands on its own regardless.

terminal-ime-input-context-refresh.ts has the same user-agent collision
for its NSTextInputContext refresh. Narrower trigger surface, left for
a follow-up.

Refs #13345

* fix(terminal): require more than one touch point before skipping the forwarder

The gate used maxTouchPoints > 0, which is looser than the idiom already
in this repo. isIOSWebView in mobile/src/terminal/terminal-webview-html.ts
requires more than one, because a Mac with a touch-capable peripheral can
report exactly one, and such a Mac must keep the forwarder: it is a real
Mac running the input method this workaround exists for.

Under the old threshold that Mac silently lost native text substitution -
a macOS regression introduced by a fix aimed at iPadOS. A captured iPad
reports five, so the stricter bound costs nothing on the device this
targets.

The check stays user-agent based rather than adopting that helper's
platform check, because an iPhone reports platform "iPhone" rather than
"MacIntel" and would slip through.
2026-08-17 21:31:31 -07:00
Jinjing 604169f4af Filter automations list by agents (#15224)
* Add agent filter to automation list

Allows filtering automations by one or more agents with search support.
Status and last-run filters are reorganized into submenus. External
automation entries are excluded from agent filtering scope.

* Fix translation keys for agent filter in automation list

Move agent search text from AgentCombobox keys to component-specific
AutomationListFilterMenu keys. Adds translations across all locales.
2026-08-17 20:44:59 -07:00
Neil 3697d68f21 fix(cmd-j): decline a GitLab iid match when the repo remote names a different project (STA-4450) (#15193)
* fix(cmd-j): decline a GitLab iid match when the repo remote names a different project (STA-4450)

`repoMatchesGitLabSlug` laundered a definite project-path mismatch into
`'unknown'` whenever the resolved identity came from a remote named
`upstream`, and `worktreeMatchesGitLabUrl` treats `'unknown'` as permission
to accept a bare iid. Since `deriveGitRemoteIdentity` ranks `upstream` above
`origin`, any repo whose top-ranked remote is `upstream` lost GitLab project
gating entirely, so an exact URL for an unrelated project could surface that
workspace.

Return the `matchGitRemoteKeyParts` verdict directly. Resolved identities are
re-probed on a 6h TTL, so a remote naming a different project is current
evidence. `'unknown'` now means only "no identity" or "unexpanded SSH host
alias", both of which stay permissive as before.

* docs(cmd-j): correct the identity-freshness comments and drop a duplicate test

The GitLab why-comment implied resolved identities refresh unconditionally.
They only refresh when a repo/project list sweep finds one past its ~6h TTL
(`selectEnrichmentCandidates` runs from `repos:list`/`projects:list`/
`projectHostSetups:list`, four refreshes per sweep, after a 5m startup delay);
there is no background timer. State the accepted cost instead of implying the
gate is loss-free.

The GitHub-side comment still claimed the identity is "chosen when the repo
was added and never re-probed" — the exact claim this PR disproves. Rewrite it
to the reason that still holds (one stored remote hides a fork's `origin`).
Behavior on the GitHub path is unchanged; it stays with the twin ticket.

Delete `does not surface an upstream-identified repo for an unrelated project
iid`: the inverted test above it already asserts both halves (mismatched
project declines, the named project still matches) against the same
upstream-derived identity.
2026-08-17 20:36:50 -07:00