mirror of
https://github.com/stablyai/orca.git
synced 2026-10-28 00:02:54 +00:00
sta-4961-cdp-cookie-timeout
5093
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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
|
||
|
|
d8e9fa1bb9 |
Revert "fix(terminal): apply pane padding on all four edges (#15544)" (#15623)
This reverts commit
|
||
|
|
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
|
||
|
|
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> |
||
|
|
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 |
||
|
|
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 |
||
|
|
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. |
||
|
|
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 |
||
|
|
4daace6251 | fix(tabs): stop split workspaces multiplying columns on return (#15482) | ||
|
|
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. |
||
|
|
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 |
||
|
|
cb95582cea | feat(release): build unsigned Windows artifacts for the dev channels (#15465) | ||
|
|
0e5b348414 | fix(repos): keep the desktop-owned manual project order authoritative across paired clients and SSH catalog publishes (STA-4850) (#15538) | ||
|
|
fcdbcf85d0 | fix(terminal): keep a cold-parked pane's runtime-graph leaf while its PTY lives (STA-2854) (#15514) | ||
|
|
7675da363e | fix(remote): stop paired-tab resurrection, ghost agent rows, and frozen visible panes (STA-4593) (#15459) | ||
|
|
174039a14e | fix(workspaces): re-seed a terminal when an emptied workspace is opened (#15513) | ||
|
|
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 |
||
|
|
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. |
||
|
|
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 |
||
|
|
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 |
||
|
|
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. |
||
|
|
8612a2dabb | Fix skill install dialog overflow and simplify file summaries (#15406) | ||
|
|
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. |
||
|
|
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> |
||
|
|
a77a2f93f7 | fix(remote): search Quick Open paths on the host (#15158) | ||
|
|
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 |
||
|
|
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>
|
||
|
|
96d88dd5fe | fix(tab-bar): restore the tab-results fixture after occupantAgent became required (#15277) | ||
|
|
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 |
||
|
|
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. |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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. |
||
|
|
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. |
||
|
|
19ba83d496 | docs(mobile): add Android APK install guidance (#14978) | ||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
a54c27f00d |
Restructure automation editor dialog into three-column layout (#14803)
* Restructure automation editor dialog into three-column layout - Separate prompt editing from settings configuration - Add Monaco editor for prompt with find widget support - Extract settings into right sidebar for better organization - Move automation name into prompt section for context - Simplify header and footer to focus on key actions - Settings controls now smoothly collapse when switching between Orca and Hermes targets * Fix React Doctor leak on automation prompt Escape listener. Move addEventListener into a helper that returns cleanup so the changed-code quality gate can see the subscription is released. * Fix stale ref closures in automation prompt editor - Move `onDismissRef.current` update into useLayoutEffect with `[onDismiss]` dependency to prevent stale closures in event listeners - Move `contentRef.current` update into the layout effect that syncs it, ensuring editor has current value when effects reference it |
||
|
|
f8e728bb8b |
fix(watcher): watch the resolved worktree root so symlinked and differently-cased paths work (#15077)
* fix(watcher): keep macOS FSEvents paths under the subscribed worktree root
macOS FSEvents reports OS-canonical paths: symlinks resolved and every
directory in its on-disk spelling. Linux (inotify) and Windows both rebuild
event paths from the directory that was subscribed, so only macOS observes
the mismatch.
Orca's watcher contract is "event paths live under worktreePath". Consumers
derive a worktree-relative path with relativePathInsideRoot(), which returns
null when the event falls outside the root -- and a null relative path drops
the event silently. So on a Mac whose worktree or folder path traverses a
symlink (~/code -> /Volumes/..., anything under /tmp or /var), or is spelled
with different casing than disk on a case-insensitive volume, every watcher
event was discarded: the editor never reloaded an agent's edit, the File
Explorer never refreshed, and Source Control never re-ran status. Nothing
errored, which is why this looked like "the file watcher stopped working"
on some machines and not others.
Rewrite event paths back onto the subscribed root inside
subscribeThroughWatcherSupervisor -- the single boundary every desktop,
runtime-environment, and SSH-relay watch passes through -- so one change
covers all three transports.
The resolution runs alongside the subscribe rather than before it: an await
ahead of the subscribe call lets a caller's abort land in a window where no
watcher-process subscription exists to cancel, which hangs the existing
cancellation contracts. The subscribe promise settles only after the rewrite
is installed, and only after the subscription itself is recorded, so
teardown never waits on a realpath.
Matching folds per path segment (NFC + case) instead of by prefix length,
because both folds change length and a folded-prefix length would slice the
raw event path mid-character. Byte-exact fast paths run first, so unaliased
roots -- every Linux and Windows watch, and most macOS ones -- cost one
string comparison per event and allocate nothing.
* fix(watcher): watch the resolved root so symlinked worktrees work on Linux too
Verified on a real Linux host: @parcel/watcher passes IN_DONT_FOLLOW |
IN_ONLYDIR to inotify_add_watch, so a symlinked worktree root fails outright
with ENOTDIR ('Not a directory'). The watch never installs and Orca caches the
root in unwatchableRoots, so it is never retried for that session. That is a
worse symptom than the macOS path-spelling mismatch and hits Linux users of
symlinked checkouts on every machine.
Hand the backend the resolved directory instead of the caller's spelling, and
keep mapping delivered paths back. Resolving the root also lets
@parcel/watcher's own ignore paths match again on macOS, where they were
computed from the unresolved root and silently excluded nothing.
The resolve is synchronous on purpose. Every caller reserves and forks its
watcher child in the same tick as the subscribe call -- capacity accounting and
cancellation ordering both depend on it, and 30+ existing tests encode it -- so
an await here would open a window where a subscribe is issued but no
cancellable child exists.
* test(watcher): use a directory junction on Windows so the alias repro runs there
Creating a directory symlink on Windows needs elevation or Developer Mode, so
the alias tests failed with EPERM on a real Windows host. A junction needs
neither, is what users actually have (a junctioned C:\dev), and realpath
resolves it identically -- so one fixture now covers all three platforms and the
end-to-end repro no longer skips outside Linux and macOS.
* test(watcher): pin the fabricated-path failure modes of the root rewrite
A rewrite that returns a WRONG path is worse than no fix -- a consumer would
act on the wrong file -- so pin the cases that could produce one: sibling
directories that share a prefix with the root (POSIX and UNC), the root itself
versus a shorter path, drive-letter casing, a root-only canonical path, and a
script where toLowerCase changes length. Found by running the rewrite over an
adversarial table; all already passed, so these lock in behaviour rather than
fix it.
* docs(watcher): drop an unverified claim about ignore paths
I claimed resolving the root also repairs @parcel/watcher's ignore-path
matching for aliased roots. Probing it on macOS shows the node_modules write is
excluded either way: FSEvents resolves symlinks in its own exclusion paths, so
the daemon filters at the source regardless of which spelling we subscribe with.
On Linux the exclusions are userspace globs relative to the watched directory
and there was no watch at all before this change, so there is nothing to
compare. Removing the claim rather than leaving a plausible-but-wrong rationale
in the module header.
* refactor(watcher): simplify root path rewriter
* test(palette): build searchable fixture documents
|
||
|
|
0e96b82e44 |
fix(mobile): keep phone tab selection across host snapshots
* fix(mobile): keep phone tab selection across host snapshots Preserve device-owned tab focus across ordinary host republications while explicit follow navigation remains authoritative. Retire closed selections across clients so stale snapshots cannot resurrect tabs. * fix(mobile): acknowledge session tab closes * fix(mobile): avoid tombstones for uncommitted closes * fix(web): implement session close IPC stubs * refactor: simplify mobile tab close flow * fix: bound session tab close confirmation |
||
|
|
9b8e9dc226 |
Prevent tab search results from jumping while typing (#15133)
* Prevent tab search results from jumping while typing - Retain deferred results that still match the current query - Add retainOpenTabResultsForQuery utility with query matching logic - Refactor TabBarCreateEntry to use useTabCreateEntrySearchResults hook * Re-check retained tab search rows with full search engines Instead of checking if row text contains the query, retention now re-runs the search engines on deferred results. This respects all matching rules (type aliases, paths, workspace labels, agent snippets) and ensures stale or mismatched rows don't linger on screen as the user types. |
||
|
|
24e662adc1 |
feat(ssh): verify host keys, and restore panes correctly across a reconnect (#14844)
* docs(ssh): design for real host key verification (STA-4319)
Today's ssh2 verifier records a fingerprint and returns true — every host key is
accepted, with no known_hosts consult and no change detection anywhere in
src/main/ssh/. Scope is per-connection, so exec, SFTP, port forwarding, the
watcher and relay deploy all ride that one unverified handshake, and the
ProxyJump path puts the final hop — the topology most likely to cross untrusted
network — on ssh2 specifically.
Decisions worth calling out:
- Read the user's known_hosts as a trust source but NEVER write to it. That file
is shared with every other SSH tool on the machine; appending means line
endings, permissions, concurrent writers and a corruption blast radius well
beyond us. Accepted keys go to our own per-target store. Reading theirs is also
the entire migration story: most developers already have their hosts there.
- Mismatch is scoped to the SAME key type. A host with only an RSA entry that
presents ed25519 is unknown, not changed. ssh2 negotiates ed25519 first, so
without this we would fire a change-of-key alarm at nearly every existing user
on their first upgraded connect — training them to dismiss the one warning that
is supposed to mean something. Flagged in review as the decision I am least
sure of; a downgrade-vector argument against it is being tested.
- Changed key hard-fails with no override button; recovery is a separate explicit
action, offered only when OUR store is what disagreed, because forgetting our
record cannot unblock a known_hosts conflict.
- Background reconnects deny rather than prompt. A dialog the user cannot place
in context only teaches click-through.
Two traps are documented because either would make the fix silently do nothing:
an async verifier returns a Promise, which ssh2 reads as truthy and accepts
immediately; and the existing test mock invokes hostVerifier with one argument
and ignores the return, so it would pass against a verifier that never decides.
Design only — no behaviour change. The doc is added to the tracked-reference
allowlist in .gitignore alongside the other docs/reference entries.
* docs(ssh): revise the host key design after security and migration review
Three things the reviews changed, kept visible rather than quietly edited out.
THREAT MODEL WAS WRONG IN THREE PLACES. Jump hosts are not the worst case — they
are already safe: shouldUseSystemSshTransport branches on exactly the inputs
resolveEffectiveProxy does, and attemptConnect returns after the system probe, so
ProxyJump goes through OpenSSH and is verified. Agent forwarding was overstated
(gated on the user's ForwardAgent). Credential theft was understated: any auth
error counts as agent fallback, so a MITM walks the user to the password AND
private-key passphrase prompts, and cachedPassword replays without prompting. The
relay claim was backwards — the attacker owns their own machine; the real impact
is the return direction, where they become the host our workspace trusts.
TYPE SCOPING IS A DOWNGRADE VECTOR WITHOUT ALGORITHM ORDERING. This was the
decision I flagged as least certain and asked to have argued both ways. OpenSSH
is safe only because order_hostkeyalgs() puts known types first and RFC 4253
gives the client's order priority. ssh2 negotiates ed25519 first regardless, so
an attacker who cannot forge the RSA key on file just presents ed25519 and gets a
friendly first-contact prompt instead of a hard failure. Keep scoping, but set
algorithms.serverHostKey to lead with the types on file — and add a sixth
outcome for 'unknown type, known host', which must never read as first contact.
SHIP THE DEFENCE BEFORE THE DIALOG. Startup restore fires eager connects for all
targets in parallel with a 15s timeout while a prompt would live 120s; ephemeral
VM targets present a new key every launch; paired-web connects run on the host
desktop, so the dialog opens on someone else's screen. Phase 1 is therefore no
modal at all: consult known_hosts and our store, match connects, unknown persists
with accept-new semantics, mismatch and revoked hard-fail. That is the whole MITM
defence with none of the migration risk.
Also folded in, verified live against OpenSSH 10.2p1: the without-port fallback
(bracketed lookup first, then bare, where the second pass can only yield match or
unknown — otherwise a bare line plus a non-default port produces a spurious
prompt); hashed entries hash the candidate form; multiple files union; a
cert-authority line does not match a plain key. IPv6 and bracket parsing moved
INTO scope — that is a parser requirement, not a scope call, and getting it wrong
produces the prompt-training harm the design exists to avoid.
* feat(ssh): parse and match OpenSSH known_hosts
The matcher half of STA-4319. No behaviour change yet — nothing calls this.
Hand-rolled because no maintained JS implementation exists, and written against
behaviour observed from OpenSSH 10.2p1 rather than inferred from the man page.
Three of those behaviours a reasonable reading gets wrong:
- A non-default port is TWO ordered lookups, not one candidate set: '[host]:port'
first, then bare host ('checking without port identifier' in ssh -v). The
fallback pass can only yield match or unknown — OpenSSH downgrades a wrong key
there rather than reporting a change. Collapse them and anyone holding a bare
line who connects off-port gets a spurious first-contact result; treat the
fallback as authoritative and they get a false change-of-key alarm.
- Revocation resolves in its own pass so the verdict cannot depend on line order.
Verified both orderings.
- A cert-authority line never matches a plain host key; it only validates
certificates. A normal line alongside it still decides.
Mismatch is scoped to the same key type, and a host known by a DIFFERENT type
returns unknown-type-known-host rather than plain unknown — an attacker who
cannot forge the key on file must not get a friendly first-contact result by
presenting another type. That outcome is only half the defence; the other half
(leading serverHostKey with known types) lands with the wiring.
47 tests from vectors executed against real sshd, including ssh-keygen -H hashed
entries. Each of six mutations reddens it: collapsing the passes, letting the
fallback report mismatch, dropping type scoping, resolving revocation in line
order, honouring an unrecognised marker, and skipping the blob/type agreement
check.
* feat(ssh): decide what to do with a presented host key
The policy half of STA-4319, kept separate from the ssh2 wiring so it is testable
without a handshake and injected rather than importing its sources, so a test
states its own trust state instead of writing files.
Phase 1 ships no dialog — a test asserts the decision is never 'prompt'. Startup
restore opens every previously-active target at once, ephemeral VM targets would
ask every launch, and paired-web connects run on the host desktop where the
dialog would appear on someone else's screen.
Ordering that matters: revocation outranks everything including
StrictHostKeyChecking=no, because a revoked key is a statement that this key is
known-bad rather than merely unrecognised. known_hosts is named before our own
store on a change, because its remedy (ssh-keygen -R) is the one that also
unblocks ssh and git — pointing at a remedy that cannot work is worse than none.
Two carve-outs with reasons: an ephemeral runtime target accepts WITHOUT
recording, since a fresh VM presents a new key every launch and a stored record
would accumulate per launch and eventually read as a spurious change; and when
ssh -G ran on the HOME-divergent path that suppresses /etc/ssh/ssh_config, an
unknown host is denied, because a site-wide policy may forbid it and being laxer
than ssh is the one outcome that is never acceptable.
Rejection text deliberately avoids 'authentication failed' and 'permission
denied': the reconnect ladder classifies on those substrings, so a denial phrased
that way is retried forever against a decision that will never change. Pinned by
a test.
* feat(ssh): build the host key verifier and the algorithm order that makes it safe
Still not wired into the handshake — that lands next. This is the piece that
turns a decision into an ssh2 callback, plus the half of the design that is easy
to forget because it lives in a different config field.
The verifier MUST be a plain function returning undefined. ssh2 does
'const ret = verifier(key, verify); if (ret !== undefined) verify(ret)', so an
async function returns a Promise — neither undefined nor falsy — and ssh2 accepts
the key immediately while ignoring whatever the callback later decides. Making
this async would silently restore exactly the accept-everything behaviour the
module exists to remove, so a test asserts the return value is undefined.
orderServerHostKeyAlgorithms is what makes type-scoped matching safe rather than
a downgrade. RFC 4253 gives the client's algorithm order priority, so leading
with the types we already hold for a host denies a server the choice of
presenting some other type to convert a hard failure into first contact. Without
it, an attacker who cannot forge the key on file just offers a different
algorithm. Revoked entries never contribute to that order.
Also fails closed on two paths that would otherwise hang or over-trust: a key
whose own length-prefixed header cannot be read is refused rather than reasoned
about, and a throw from any dependency denies, because ssh2 may not catch an
exception raised inside the verifier and the handshake would hang instead of
failing.
18 tests. Includes the two negative cases that matter — first-contact keys are
recorded, but keys we already know, rejected keys, ephemeral runtime targets and
a lax StrictHostKeyChecking are not.
* fix(ssh): promote every RSA signature algorithm for a known ssh-rsa key
A known_hosts entry names the KEY type, which is not the negotiated ALGORITHM
name. One ssh-rsa key is offered as rsa-sha2-512, rsa-sha2-256 or ssh-rsa
depending on the signature algorithm, so matching the literal name only would
leave a host we know by RSA ordered behind ed25519 — precisely the ordering this
function exists to prevent, and precisely the population (RSA-era known_hosts
entries) it was written for.
Verified from ssh2's own negotiation while wiring this: kex.js iterates the
CLIENT list and takes the first entry the server also offers, so client order
does decide, as RFC 4253 says. ssh2's default order leads with ed25519 and places
the RSA algorithms fifth through seventh.
* fix(ssh): verify host keys instead of accepting every one (STA-4319)
The actual fix. ssh-connection's verifier recorded a fingerprint and returned
true, so every ssh2 connection accepted every host key — no known_hosts consult,
no change detection. It now consults the user's known_hosts plus our own store
and refuses a changed, revoked or unverifiable key.
Phase 1 by design: no dialog. Unknown hosts are accepted and recorded
(accept-new semantics), because startup restore opens every previously-active
target at once, ephemeral VM targets present a new key each launch, and
paired-web connects run on the host desktop where a prompt would appear on
someone else's screen. The MITM defence lands now; the prompt is Phase 2.
Also sets algorithms.serverHostKey to lead with the types already known for the
host. Without it the type-scoped matching is a downgrade — an attacker who cannot
forge the key on file just presents another type and turns a hard failure into
first contact. Verified from ssh2's kex.js that the client list decides.
Denial replaces ssh2's generic handshake error with the specific reason, because
the reconnect ladder cannot distinguish a generic failure from a transient fault
and would retry forever against a decision that will never change.
An unreadable trust store degrades to known_hosts only rather than failing the
connect: a changed key is still refused, and a host trusted only by us falls back
to first contact and is re-recorded, reaching the same decision.
The ssh2 mock now uses the callback form and aborts the handshake on denial. As
written it called hostVerifier(key) with one argument and ignored the result, so
it would have passed against a verifier that never decides — flagged in the
design as a mock that had to change, not a test to quietly rewrite. Two new tests
pin the wiring rather than the module: an unidentifiable blob is refused, and a
well-formed key is accepted.
Note for review: commit
|
||
|
|
9f4ea42493 |
fix(agent-status): say what an OpenCode permission request is waiting on (STA-3160) (#14614)
* fix(agent-status): say what an OpenCode permission request is waiting on (STA-3160)
A permission.asked arrives as hook_event_name PermissionRequest, but
extractOpenCodeToolFields had no branch for it, so the pane reported a bare
{state:'waiting'} with no tool or command. The user could see that OpenCode was
blocked but not on what.
Read the fields @opencode-ai/sdk fixes for EventPermissionAsked: 'permission'
names the request, and metadata/patterns carry the command or paths it covers.
The normalizer is shared with mimo-code, so both are covered.
* fix(agent-status): show the OpenCode permission on the row, and retire it after (STA-3160)
Live validation against opencode 1.18.18 showed the original change populated
toolName/toolInput on a `waiting` entry that no surface rendered, while leaving
the answered permission cached for the rest of the pane's session.
Retire the tool fields on every OpenCode-family event except PermissionRequest.
isNewTurnEvent is false for this family, so resolveToolState otherwise inherits
one answered permission onto every later frame and the row reads a resolved
command as the live tool. Reproduced end to end: after approving `rm -rf build/`,
an unrelated later turn still reported it.
Read `filepath` from permission metadata. The SDK types metadata as
Record<string, unknown>, so its keys come from each tool; a live opencode 1.18.18
sends `filepath` (one word) for `edit`, which the previous key list missed. The
fallback to `patterns` covered it by accident, and the test that claimed to cover
it used `metadata: {}` — a shape OpenCode never emits. Tests now use captured
payloads for bash, edit and webfetch.
Show tool fields on `waiting` as well as `working`. All three consumers gated on
`working`, so a permission request rendered nothing at all; before/after of the
sidebar was pixel-identical. The rule now lives in one place (showsAgentToolPreview)
because a gate duplicated across three surfaces is a gate that drifts.
|
||
|
|
64de8dd637 |
fix(workspaces): delete on the confirmed host, and make both hosts' rows selectable (STA-4343) (#15013)
* fix(workspaces): host-qualified workspace deletion (STA-4343, STA-4448) Squashed integration of PR #14606 + the codex review-loop output, replayed onto current main. Granular history preserved on brennanb2025/sta-4343-review-full. Fixes the regression from #13413: a workspace id is repoId::path with no host component, so the same repo at the same path on two hosts published one id for two workspaces, and deletion routed by that id landed on whichever host routing preferred - usually the ACTIVE one, not the row the user confirmed. - removeWorktree takes a REQUIRED host-qualified WorktreeRemovalTarget; omitting the host is a type error. All destructive callers migrated. - Projections dedup on (host, id), so two hosts render as two selectable rows while the createWorktree/fetchWorktrees race duplicate still collapses. - Ephemeral VM cleanup is host-scoped. It matched on bare workspaceId, so the host-scoped delete path destroyed the SURVIVING host's VM and its unpushed filesystem - a leak fix that had become data destruction. - Selection, keyboard routing, lineage grouping and Space rows carry host identity end to end; fixing the executor dedupe alone would have turned one-row intent into deleting both hosts. Files split to stay under max-lines rather than raising any cap. * refactor: split files that crossed max-lines The review-loop commits used --no-verify, so the pre-commit hook never enforced the caps. Extracted cohesive units rather than raising any limit: renderer teardown, delete-with-toast, pinned-group rows, host-scope helpers, workspace-kind predicates, filter actions, kanban drag selection, the renderer removal result type, and the native-chat persistence tests. * refactor(workspaces): extract cleanup deletion-phase selector Clears the last max-lines violation and the import-type side effect the changed-code gate flagged. * refactor(sidebar): track the delete-dialog extraction modules * fix(workspaces): preserve host identity across remaining surfaces * fix(sidebar): re-carry host through the rewritten palette result model #15170 replaced PaletteSearchResult while this PR was open. Re-applied the host qualification on top of the new model instead of taking either side: results carry worktreeHostId again, and the board filter keys its matched set on host identity rather than the bare id. Known gap, documented in the board test rather than deleted: searchWorktrees resolves evidence through a `documents` map keyed by BARE worktree id, so two same-id host rows collapse before this code sees them. Closing that belongs with the palette work. * test(cmd-j): pin the palette collision gap instead of asserting the old model The palette collision test asserted two host-qualified rows, which #15170's rewrite made unreachable: item ids are bare again and worktreeMap is id-keyed. Rewritten to assert what holds — activation always names a host — and to pin the defect it exposes: two same-id rows render on ONE command value, so React sees duplicate keys and a click on the first row activates the second row's host. That reproduces on main, so it is pre-existing, not from this PR. Pinned rather than deleted so fixing it must update this test. --------- Co-authored-by: QA <qa@local> |
||
|
|
1412ae2d91 |
Revert "fix(terminal): inset the grid inside the xterm surface (#14583)" (#15181)
This reverts commit
|
||
|
|
1a04d292b6 |
fix(agents): lift the pane retirement fence when a live PTY re-attaches (STA-4114) (#14624)
* fix(agents): lift the pane retirement fence when a live PTY re-attaches (STA-4114) A detach/reattach cycle retires the pane on both sides — the main hook server's closedAgentStatusPaneKeys and the renderer's recentlyRetiredAgentStatusPaneKeys — and nothing ever cleared either one. The pane then rejected every later working/done event for the rest of its life while Pi kept running normally in the same PTY. Bind the fence to the fact it asserts: retirement claims the pane is gone, and binding a live PTY to that exact pane disproves it. Clear both tombstones at the spawn/attach chokepoint and at the daemon-backed reattach path, so recovery does not depend on the agent starting another turn — a pane re-attached mid-turn only has agent_end left to report, and one re-attached while idle emits nothing at all. Closed-tab tombstones are a separate, stronger claim and are deliberately left standing. * test(agent-hooks): re-arm the idle re-attach test against a turn-boundary fix The idle re-attach assertion posted only before_agent_start, which #14626 turns into a fence-lifting turn boundary. Under that change the test passes whether or not restorePaneAuthority runs, so it stops pinning this PR's mechanism. Assert first on agent_end — a non-turn event — so the test proves the fence was already down when the hook arrived. Verified: with restorePaneAuthority neutered AND before_agent_start added to the restart predicate, the old assertion passes and the new one fails. * fix(agents): lift a retired pane's whole fence, aliases included (STA-4114) Retirement fences the pane, its resolved owner, and every alias of it, then deletes those aliases. Restoring only the key handed to us left the rest standing — and a detached pane's process keeps posting the key it launched under (server.ts:1614), so the canonical re-attach case stayed suppressed with the fence apparently lifted. Verified against the real omp binary: the row came back under the stale launch pane instead of the detached owner. Record what each retirement fenced and replay it as a unit, rebuilding the aliases it deleted. Keys and aliases belonging to a closed tab are skipped, so the stronger claim survives and a live process is never routed back into a closed tab. The record is indexed by every fenced key and bounded at 1024 like the maps it mirrors; an evicted record degrades to the old behaviour. Also records why the renderer's restore IPC is deliberately unguarded: that map is not a mirror of main's (retirePtyAgentLaunchAuthority fences main directly on command-finished and PTY exit, and nothing pushes it back), and it is per-window and non-persisted, so gating the send on a local tombstone reintroduces this bug for exactly those panes. |