* fix(terminal): preflight Codex in Windows cmd and Git Bash
* test(terminal): run Windows preflight through ConPTY
* test(terminal): isolate cmd harness exit status
* test(terminal): allow slow Git Bash ConPTY startup
* fix(codex): resolve the launch preflight to a verified absolute Orca CLI path (STA-4270)
The Codex launch preflight carried a bare command name ('orca' / 'orca-dev') in
ORCA_CODEX_LAUNCH_PREFLIGHT. The codex() wrapper that invokes it is emitted after
the user's profile scripts are sourced, and those routinely rewrite PATH, so the
name was resolved against a PATH Orca neither controls nor can predict.
Resolve and verify the shipped CLI's absolute path instead, and return null when
no path verifies so the preflight is skipped rather than run against an
unidentified program.
* test(codex): align bundled launcher fixture across CI hosts
Devin documents config.json as JSONC. Installing hooks parsed it with
jsonc-parser and then reserialized with JSON.stringify, silently dropping
the user's comments, key order, and formatting on every install.
Edit the original text with modify/applyEdits one hook event at a time so
untouched entries keep their attached comments, and let both writers accept
pre-serialized text so the shared atomic write and rolling backup are reused.
The two existing tests asserted with JSON.parse, which could only pass once
the comment had been stripped; both now parse as JSONC and assert the
comment survives.
* fix(daemon): bound the caller's wait on final durable-history checkpoints (STA-4228)
shutdownWithHistoryLock threaded the caller's absolute deadline into
ensureConnected and into the kill RPC, but awaited the final keep-history
checkpoint between them with no bound at all. Worktree sleep supplies that
deadline, so a stalled history write pinned the process-wide checkpoint tail
and stranded Sleep Terminals until an app restart.
Bound only the caller's wait. The checkpoint itself stays deadline-free: it
remains the exclusive tail, runs to completion, and still commits, so nothing
durable is cancelled or deferred. On expiry the caller stops awaiting, throws
FinalCheckpointWaitExpiredError, and never falls through to the kill, so the
PTY stays alive and the stop is reported unverified.
* test(daemon): prove final checkpoint deadline outcomes
* fix(daemon): persist the pending-output counter across empty incremental takes (STA-4297)
An empty incremental take advanced pendingOutputSeq without writing a log
batch, so the in-memory counter ran permanently ahead of the log. The next
warm reattach could not prove continuity and committed the live 1000-row
window over a deep durable checkpoint.
Advance the counter only for takes that get persisted: a snapshot take
(stamped into the checkpoint) or one carrying records/overflow. This matches
the layers below, which already treat an empty take as a no-op write.
* test(daemon): keep empty-take coverage outcome-based
* Add global external worktree visibility defaults
* Expand global worktree visibility source defaults
* Fix host-scoped visibility settings races
* Fix global worktree visibility integration
* Enable source visibility defaults on mobile
* Polish external worktree settings navigation
* Clarify inherited worktree visibility settings
* feat(sidebar): replace the inherited-visibility switch with a Show/Hide picker
Each source row now shows a two-segment Show / Hide control preselected to the
global setting, and explains itself only where the project actually disagrees:
an "Overriding global setting: <value>" card names the value being ignored.
Picking the segment global already holds drops the override instead of pinning
a duplicate, so the same control both overrides and reverts, retiring the
separate "Use global" link. The dialog footer now lists every inheritable
source with its global value.
* fix(sidebar): preserve reset for matching visibility overrides
* fix(linear): label Start workspace and add Open on Linear
The issue page header used three unlabeled icons. Match the GitHub
issue header: copy stays quiet, Open on Linear is an external-link
control, and Start workspace is a labeled primary button.
* Address PR review feedback (#14492)
- Make the header source contract ignore Start workspace formatting
A hidden-delivery byte gap can strand more than the SGR pen, and the reset
#14241 added to the split alt-screen replay is undone before any content is
painted: xterm answers `?1049l` with restoreCursor(), which reloads the pen,
all four G-set designations, GL, origin mode and wraparound from the register
saved at `?1049h`.
- Bracket the buffer switch with the baseline: before, so `?1049h` banks
grounded state rather than the gap's; after, so `?1049l`'s restore cannot
reapply it.
- Ground everything a serialized payload is diffed against, not just the pen:
SGR, GL, all four G-sets, origin, autowrap, insert, the per-buffer scroll
region, and the saved-cursor register.
- Switch buffers only when the pane is actually on the other one. `?1049` is
not a no-op otherwise — it still swaps the kitty flag registers, which would
park the flags of an agent that negotiated them on the normal screen.
- Return to the normal buffer when the gap ate the TUI's exit sequence; the
restored history was painting into the alt buffer with scrollback left empty.
- Restore the CAN #14241 dropped, so a control string the gap truncated is
discarded instead of committed by the next ESC.
- Ground the abandon path exactly once instead of twice.
- Derive the parity/fuzz preambles from the same builder; they had drifted and
were asserting against bytes production no longer emits.
* test(e2e): headless preedit-geometry coverage for Korean and CJK terminal input
Both IME defects that shipped and were reverted passed a suite of ~3000 IME
assertions, because every one of them checked bytes reaching the pty and a
preedit rendered into a hidden overlay satisfies all of them while the user
composes blind. The one arm that asserted real geometry was headful-gated and
macOS-only, so it never ran in CI.
Drives composition through CDP Input.imeSetComposition rather than a native
input source, which removes the accessibility grant, the system input source
and the visible window that forced that gate, so this runs in the ordinary
electron-headless project.
The load-bearing assertion is the composition overlay's real bounding rect.
Verified to have teeth: with max-width 0 and overflow hidden injected, the
active class, the textContent, display block and checkVisibility all still
pass, and only the rect assertion fails.
* test(e2e): restore the CDP composition drivers the preedit specs need
The trimmed copy on main kept only the key-dispatch helpers, so the composition
drivers the geometry specs import were missing. Adds them back: setImeComposition,
commitImeText, dispatchImeProcessKey, composeHangulSyllable and
dispatchResumedCompositionUpdate. The shared helpers are unchanged.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* fix(terminal): strip the captured shim dir across trailing-separator spellings
The scrub compared the captured ORCA_ATTRIBUTION_SHIM_DIR to PATH entries literally, so a trailing-separator difference left the legacy shim directory on the spawned PATH. Same class already fixed in the generated wrappers.
Also drops a dead default in the POSIX filter, consolidates comments that had accumulated across fixes, and normalizes the legacy directory once rather than per PATH entry in the cmd wrapper. The boundary scanner stays: a shim path can contain the PATH delimiter, which splitting would fragment.
* fix(terminal): keep the git shim tombstone parseable on Windows
The cmd wrapper carried two em dashes in comments. cmd.exe seeks through a
batch file in bytes but advances by decoded character count, so those four
extra UTF-8 bytes made it drop the first four characters of every line and
the wrapper died with "The syntax of the command is incorrect."
Also move the legacy-dir trailing-separator strip into a CALL body: cmd
expands a whole line before evaluating `if defined`, so inline it ran its
substring syntax against an unset variable and mangled the line.
Rooted-path checks are shared by a single subroutine, a relative or
drive-relative captured ORCA_REAL_* is rejected, and relative PATH entries
are dropped from the exported PATH so the cwd cannot select spawned tools.
The POSIX tombstone is deleted rather than written when no absolute
interpreter can be verified.
Verified on Windows 11 (cmd and PowerShell 5.1): normal lookup, relative
and drive-relative ORCA_REAL_GIT, relative and drive-relative PATH
entries, trailing separators, legacy shim dir, empty PATH, and a
cwd-only PATH with a planted git.cmd/git.bat.
Co-authored-by: Orca <help@stably.ai>
* fix(terminal): stop the git shim tombstone re-expanding PATH data
cmd re-expands a CALL command line, so path data handed to a subroutine as an
argument got a second round of percent expansion. A PATH entry holding a
literal %CD% became the current directory before the rooted-path guard saw it,
and the wrapper then ran a planted git.cmd from that directory (exit 66,
reproduced on Windows 11). Callers now pass the value in a variable, which is
expanded once.
Re-verified on Windows 11 across 15 cases: the %CD% entry is now dropped and
the real git runs, and a PATH entry spelled 'C:\paren9 (x86)\cmd' still
resolves, so the new for-block did not regress paths with parentheses.
Co-authored-by: Orca <help@stably.ai>
* fix(terminal): keep percent expressions out of the shim tombstone comments
cmd expands variables inside rem, so a comment naming the working directory
substituted a path into itself. Verified on Windows 11 that rem does not
re-parse the result -- a cwd of 'C:\x&pwned&rem' executed nothing and the real
git still ran -- so this was not exploitable, but rem handles separators
differently inside a parenthesized block and this script now has some. A test
now rejects any percent sign in an emitted rem line.
Co-authored-by: Orca <help@stably.ai>
* fix(terminal): pin shim tombstone shell state and directory identity
Three fixes, each proven before and after.
Delayed expansion: bare setlocal inherits the caller's state. Under a parent
shell started with /V:ON, a literal !CD! PATH entry became the current
directory and a planted git.cmd ran (exit 66), and a legitimate directory
containing ! stopped resolving (exit 127). Both on Windows 11, both gone with
setlocal DisableDelayedExpansion.
Directory identity: the POSIX filter compared the legacy shim directory
lexically while comparing its own directory with -ef, so a symlink or a
<legacy>/../<legacy> spelling escaped the filter and the live attribution
wrapper won the lookup. It now tests both. The env scrub had the same gap and
now normalizes before its suffix test.
Retained POSIX wrappers: with no absolute bash verifiable the wrapper was
deleted, which strands a shell that already hashed the path on 127 instead of
falling through to PATH. It now reuses the shebang of the wrapper it replaces,
which is known to work on that host, and rejects /usr/bin/env so the ambient
lookup stays closed. Deleting is the last resort.
Two Windows test pins matched the wrong occurrence and stayed green with the
guard they claimed to protect removed; they now assert the subroutine body.
All five fixes were mutation-tested.
Co-authored-by: Orca <help@stably.ai>
* fix(terminal): require bash for a reused shebang and exclude slash-spelled dirs
The retained-shebang fallback accepted any absolute executable that was not
env, but the rendered body needs BASH_SOURCE, [[ and local, so a #!/bin/zsh
wrapper was accepted and then exited 1 on 'BASH_SOURCE[0]: parameter not set'.
It now requires bash, which also rejects /usr/bin/env as before.
The PowerShell filter trimmed only backslashes while its rooted-path regex
accepts forward slashes, so a wrapper or legacy directory spelled with a
trailing / missed the lexical exclusion. Verified on Windows 11 that both
spellings are now excluded and the real git still runs.
The test that claimed to cover the shebang fallback only called the resolver
directly, so deleting the wiring left the suite green on any host with
/bin/bash. It now mocks the resolver to null and asserts through
neutralizeLegacyTerminalShimDir that the wrapper survives with the retained
shebang, and that a wrapper without a reusable one is still deleted. Both
mutations are now killed.
The Windows wrapper text assertions move to their own file rather than taking
a max-lines exemption.
Co-authored-by: Orca <help@stably.ai>
* fix(terminal): stop the shim PATH scrub deleting a legitimate directory
The previous round collapsed '..' lexically before classifying a PATH entry.
That is not the same as resolving it: when <shim>/posix is a symlink,
<shim>/posix/../posix lands elsewhere, so a legitimate directory was
classified as the shim and removed, leaving git unresolvable. Reproduced with
a symlinked shim/posix and a real git behind it.
Resolving for real is not available here either -- this env is also built for
remote and WSL panes whose paths name nothing on the local filesystem -- so the
classifier is lexical again, deliberately. A '..' spelling that slips through
costs nothing at runtime: that directory holds the pass-through tombstone, and
the tombstone excludes its own directory by -ef, so the lookup still reaches
the real git.
Separately, pathEntrySpellings can only enumerate one added separator, so a
captured directory spelled with two or more survived the literal removal. The
split filter now also compares separator-stripped forms, which covers any
number. Both changes are mutation-tested.
Co-authored-by: Orca <help@stably.ai>
* fix(terminal): stop a relative shim dir letting the cwd pick the binary
The -ef identity test added two rounds ago resolves a relative right-hand
operand against the wrapper's current directory, so a relative
ORCA_ATTRIBUTION_SHIM_DIR let the cwd decide which PATH entry counted as the
legacy directory and got a legitimate one skipped. Reproduced as SAFE vs LATER
purely by changing the cwd. Identity is now attempted only for an absolute
target; the lexical compare still covers the rest.
The cmd wrapper had the same shape: full-path expansion made a relative
captured value absolute against the cwd before PATH filtering. It now requires
a rooted value and leaves the normalized form unset otherwise, which makes the
reject subroutine a no-op. Verified on Windows 11 that two runs differing only
in cwd now agree.
Separately, trailing-separator stripping treated a backslash as a separator on
POSIX, where it is a legal filename character, so '/tmp/captured\' and
'/tmp/captured' compared equal and a real directory was deleted from PATH. The
rule is platform-specific now; the cross-platform classifier still understands
both styles because a Windows PATH reaches it through the remote env.
Both fixes are mutation-tested.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
`resolveWorktreeSelector` resolved every selector kind from the whole-fleet snapshot, so a targeted `id:<repoId>::<path>` lookup fanned `git worktree list` across every registered repo to answer a question about one of them. With a cold scan cache -- app startup, or the first lookup after a mutation clears the snapshot -- that is one subprocess per repo, ~17ms each, to find a worktree whose owning repo the id already names. Measured on a ten-repo fleet: one `id:` lookup scans 10 repos before and 1 after.
Scope only `id:`. Every other selector kind is matched across the fleet and its `selector_ambiguous` contract is defined over all repos, so scoping `branch:`, `name:`, `issue:`, or a bare selector would silently pick a winner where they correctly refuse today. A test pins that: `branch:main` across ten repos still throws `selector_ambiguous` and still scans all ten.
Lineage stays correct because edges are intra-repo by construction. The scoped path returns null and falls back whenever that does not hold: a repo id registered on several execution hosts, an unknown repo id, or a worktree the scoped scan does not contain. A warm fleet snapshot always wins.
Row resolution moves out of orca-runtime.ts into repo-worktree-row-resolution.ts, which owns no state -- the cache-aware scan and folder-workspace stamping are injected. orca-runtime.ts ends up 65 lines shorter than before despite the added feature.
* fix(runtime): cap remote git.diff and file previews at the transport budget
A remote or mobile user who opens the diff of a large image loses their whole
WebSocket, not just that request: the E2EE channel closes with 1013 when a reply
exceeds the 4 MiB outbound envelope. Two producers can exceed it unaided.
git.diff/branchDiff/commitDiff cap text with MAX_RENDERED_DIFF_COMBINED_CHARACTERS
(6M chars) -- a *renderer* budget that sits above the transport limit -- and return
base64 for previewable binaries bounded only by MAX_GIT_SHOW_BYTES, so a 10 MiB PNG
changed in place is ~26.7 MiB in one envelope. files.readPreview inlines base64 up
to 10 MiB, and mobile calls it for every image tab.
Both now measure against a budget derived from the outbound limit. The check sits in
orca-runtime-git.ts, downstream of the dedupe and of both the SSH-provider and local
branches, so a payload forwarded verbatim by an old relay is covered by the same code
and src/relay needs no change. Local and in-process callers pass no budget and keep
full fidelity.
Measuring raw bytes would not work, which is the whole reason this needs a module.
JSON escaping turns one control byte into six (\u00XX), and binary-buffer.ts sniffs
only for NUL in the first 8 KiB -- so a NUL-free file of 0x01-0x1f bytes is classified
as *text*, would pass a raw-byte cap, and would then blow the envelope. The budget is
escape-aware, with a three-branch fast path that keeps normal diffs at two native
byteLength calls and scans only the ambiguous band.
The SSH branch of readFileExplorerPreview had the same raw-vs-escaped gap: its stat
gate sizes base64 binaries, but text crossed unbounded. It now honours the same
decoded-text limit the local branch already enforced.
No wire change: GitDiffResult is untouched -- no third kind, no new field. Old clients
see an error for one request instead of a dropped connection. diff_too_large joins the
structured passthrough codes and lands on an existing error arm in both mobile
consumers and the desktop remote path; file_too_large was already handled on both.
Instruments the 1013 close, which nothing measured before, so the incidence this cap
is meant to drive to zero is finally observable. `emitter` separates a producer size
bug from a wedged link.
Known regression: remote image previews between ~3.096 and ~3.146 MB now return
file_too_large. They only intermittently worked before -- above ~3.0 MB they killed
the socket -- so this trades intermittent connection loss for a consistent error.
Test: 10281 passed in src/main/runtime + src/shared + src/main/git; mobile 3427
passed. Each of the six budget-enforcement sites is independently mutation-killed.
Escaping fixtures cover newline-dense, control-char, CJK, lone-surrogate and base64
content against native JSON.stringify. tsc clean for node, web and cli; oxlint clean.
Co-authored-by: Orca <help@stably.ai>
* fix(runtime): harden remote reply transport budgets
* test(runtime): cover desktop remote preview budgets
* test(runtime): close telemetry review gaps
* chore(shared): repoint budget imports after the shared/types barrel removal
Upstream #14447 dropped the shared/types barrel; GitDiffResult now lives in
git-diff-compare-types and GlobalSettings in global-settings-types.
Co-authored-by: Orca <help@stably.ai>
* fix(ssh): surface an over-cap preview read as file_too_large
The stream reader aborts an over-cap read with StreamProtocolError, whose numeric
code falls through mapRuntimeError to a generic runtime_error carrying the raw
"Reported totalSize N exceeds client cap M" string. Neither preview client
recognizes that: runtime-file-client.ts and mobile-file-preview-response.ts both
key on file_too_large. It also made the two file_too_large guards directly below
the read unreachable on the streaming path.
Gives the cap its own error type so the caller can translate it, keeping the
bandwidth saving the cap exists for. A genuine protocol fault still propagates
unmasked.
Found by the readiness review. Mutation-verified: removing the translation fails
exactly the new test.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* fix(agent-status): stop start-less child stops from minting phantom working
buildClaudeCachedLeadStatusPayload fell back to 'working' whenever the pane had
no cached lead-turn state. That default is right for a spawn or a child tool
call, but the same helper serves SubagentStop and TeammateIdle, which end work
and prove the opposite.
claudeLeadStateByPaneKey is in-memory only, so every app restart empties it. A
Claude session that outlives the restart reports its next child event into an
empty map and the pane latches 'working' with an empty roster -- no Stop ever
clears it, and the 30-minute window only decays the sidebar dot, never the
stored state.
Fall back by the event's evidence: terminating child events resolve to 'done',
which still gates up through resolveClaudePaneState when the roster or
background work proves the pane is busy.
* fix(agent-status): require evidence for child completion
* fix(agent-status): publish matched teammate idle
* fix(agent-status): preserve confirmed child work
* fix(agent-status): retain live restored teammates
* fix(agent-status): reap unconfirmed siblings after child drain
* fix(agent-status): preserve unmatched restored children
* fix(agent-status): wait for lead completion after child stop
* fix(agent-status): persist restored child transitions
---------
Co-authored-by: Brennan Benson <brennan@stably.ai>
macOS maps a control chord by physical key regardless of layout: measured with
UCKeyTranslate, physical A/U under Control produce U+0001/U+0015 on 2SetHangul,
Russian and Greek exactly as on ABC, though unmodified those keys give ㅁ/ㅕ,
ф/г, α/θ. A native terminal inherits this by passing the OS characters through.
The browser does not expose that translation. xterm's legacy encoder works
anyway because it reads keyCode, which Chromium reports from the physical key,
but its kitty encoder derives the key number from `key` and only consults
`code` when Shift or Option is held. Ctrl is not in that gate, so a pane with
the kitty protocol negotiated reports CSI-u for U+3141 rather than 'a' and the
chord does nothing. Ctrl+C escaped this only via its hand-written ETX bypass.
Recover the byte from `code` when `key` is non-ASCII, which reproduces the
OS control table. An ASCII `key` stays authoritative so a Dvorak remap is
honoured. KeyC is excluded: the interrupt policy owns it, and off macOS that
policy declines to a selection so the copy binding wins.
Fixes#13331
* fix(workspaces): support full cleanup scans
* feat(workspaces): persist cleanup snapshots
* feat(workspaces): add cleanup filter model
* refactor(workspaces): remove cleanup presets
* feat(workspaces): rework cleanup dialog
* fix(workspaces): keep cleanup row ordering render-pure
* refactor(workspaces): simplify cleanup browsing
* refactor(workspaces): show cleanup facts
* refactor(workspaces): surface cleanup row facts
* fix(workspaces): remove misleading cleanup count
* fix(workspaces): preserve full scan semantics
* fix(workspaces): scope snapshot persistence
* fix(workspaces): preserve cleanup browse compatibility
* fix(workspaces): reconcile cleanup dialog state
* test(workspaces): update snapshot store fixtures
* test(workspaces): preserve cleanup scan modes
* perf(workspace-cleanup): stream scan progress and size results
* fix(workspace-cleanup): address review feedback
* fix(workspace-cleanup): preserve host-scoped cleanup metadata
* fix(workspace-cleanup): declare review source dependencies
* fix(workspace-cleanup): align size scan banner
* fix(workspace-cleanup): shorten scan action
* perf(workspace-cleanup): avoid redundant scan IO
* perf(workspace-cleanup): bound restarted evidence scans
* fix(workspace-cleanup): satisfy scan queue lint
* perf(workspace-cleanup): bound scan and snapshot work
* perf(workspace-cleanup): serialize final enrichment
* test(workspace-cleanup): assert final enrichment drain
* fix(workspace-cleanup): stop progress after renderer teardown
* perf: batch workspace cleanup git evidence scans
* perf(workspace-cleanup): stop redundant snapshot and scan work
* fix(workspace-cleanup): resolve review findings across scan, store, and dialog
Correctness:
- Chunk git-evidence dispatches at the shared 500-target limit and exclude
queued/in-flight ids from target selection, so fleets past the limit can no
longer strand rows permanently mislabeled as checked-but-unknown.
- Key destructive selection pruning on the user's filter state instead of the
per-tick matched-set identity; streaming reclassification no longer silently
deselects rows.
- Clamp the facet clock to max(scannedAt, open time): a stale hydrated
snapshot no longer misbuckets idle thresholds or keeps dead agents fresh;
row labels use the same clock.
- Supersede and cancel the previous broad scan when a new one starts (renderer
registry and same-sender guard in main) instead of racing two fleet scans.
- Gate snapshot persistence on hasTargetedWorkspaceCleanupScan so
worktreeIds: [] can never persist an empty fleet snapshot.
- Re-apply dismissals at set-time in progress application so a dismissal
landing mid-enrichment is not clobbered.
- Record a one-off local snapshot prune for single (unbatched) remote deletes
so removed workspaces cannot resurrect from cache.
- Strip .exe when normalizing foreground process names so Windows agent
processes match.
Performance:
- Cache per-candidate facet and review-info objects on candidate identity;
no-op streaming ticks reuse the previous rows array and skip every
downstream pass; matched-set identity is stable under equal membership.
- Compute facet counts/options only while the filter popover is open.
- Equality-bail git-evidence publishes; structural (non-stringify) facet-group
comparison memoized in the toolbar.
- Identity-token fast path for the enrichment cache (cache hits skip both
JSON.stringify signatures); prune viewed/dismissal records on removal and
expiry; bound the superseded-scan-id set.
- Restore the no-op bail in removeWorkspaceSpaceWorktrees (regression).
- Abort main-side scans when the renderer is destroyed; module-scope
controller maps survive handler re-registration.
- Batch removal preflight into one targeted scan (with refreshActivity) per
500 ids instead of one scan per row.
- Scan repos at concurrency 2, report discovered counts upfront for honest
progress, share fs-activity probes per path (folder workspaces), read only
the reflog tail, and skip the snapshot read-before-write via a remembered
scannedAt.
Split workspace-cleanup-worktree-listing, workspace-cleanup-facet-row-caches,
and workspace-cleanup-selection-model out of files that crossed max-lines.
* fix(workspace-cleanup): address verifier findings
- Fall back to a full reflog read when the newest record exceeds the 8KB
tail window, so an oversized subject cannot hide recent ref activity.
- Bound the single-removal snapshot prune batch id with a UUID; embedding
the unbounded worktreeId silently failed main's 128-char validation and
skipped the prune for long remote ids.
- Key the main-side broad-scan supersession by sender AND scan mode so
legacy suggestion-only and full-workspace scans stay isolated, matching
the renderer registry.
* fix(workspace-cleanup): own facet caches with useMemo instead of render-time ref writes
React Doctor (CI changed-lines gate) correctly flagged the three cache refs
written during render. Each per-candidate cache now lives in one memo with
the derived context it is keyed on, so the memo deps are the invalidation
and interior fills stay content-addressed; the matched-set identity
stabilization is dropped since its only consumer reads through a
useEffectEvent and never keys on identity.
WorktreeList.tsx was 6.8k lines behind an `eslint-disable max-lines`. Break it
into `sidebar/worktree-list/`: the container keeps store wiring and composition,
the virtualized viewport keeps layout, and the drag, reveal, virtualization,
row-model, and row-render concerns each get their own file. Every file now fits
the oxlint budget, so the suppression and its baseline entry are gone.
Behaviour-preserving. The only deliberate cleanups are duplicate branches folded
into shared helpers (drop-preview state updates, status-hover fallback, the two
identical scroll-to-index reveal branches) and a dead sticky-header-index ref.
Tests that asserted on WorktreeList.tsx source text or imported its named
helpers now point at the module that owns them.
With a non-Latin input source the OS reports the layout's own glyph for `key` —
a Hangul jamo on Korean 2-Set, Cyrillic es on Russian — while `code` stays KeyC.
The interrupt policy read `key` for identity and only consulted `code` when
`key` was empty or Unidentified, so a jamo short-circuited it to false: the
press missed the ETX path and was CSI-u encoded instead, leaving a TUI running.
Trust `key` only when it is a Latin letter, which keeps a Dvorak remap of C
authoritative. Otherwise ask the layout map what the physical key produces
unmodified: an IME layered over a Latin layout answers 'c', and over a Dvorak
base answers 'j', which correctly declines. When the map is itself non-Latin it
cannot answer either, so fall back to physical position — how terminals have
always resolved control chords.
Fixes#14460
* fix(browser): restore replaced cookies through CDP identities
Both remaining callers of the imported-domain replacement rolled back by
rebuilding cookies with cookies.set, which silently drops partitionKey.
The rollback in importValidatedCookies puts back the user's ORIGINAL
cookies that the import already deleted, so a CHIPS cookie came back as
an ordinary one and no restart recovered it.
Snapshot CDP identities before the first removal and undo through them,
the same machinery removeTransplantableCookies already uses. The store
type omits 'set' so the lossy reconstruction cannot be reintroduced, and
restoreImportedDomainCookies is deleted now that both callers are gone.
* fix(browser): skip the CDP rollback when nothing was replaced
restoreClearIdentities attaches the debugger before it iterates, so an
empty restore set would spin up a hidden BrowserWindow to put nothing
back. The old cookies.set restore was a no-op loop in that case.
#14397 split `shared/types.ts` into 46 per-domain modules but kept the path as
a re-export barrel so the import sites did not have to change. This removes
the barrel: every consumer now imports from the module that actually declares
the type, and `src/shared/types.ts` is deleted.
Barrels hide where a type lives, make every consumer look like it depends on
the whole domain, and let an unrelated edit invalidate a module that ~2,000
files transitively import.
2,323 import declarations across 2,321 files. Rewritten mechanically: each
specifier was resolved to an absolute path via the TypeScript AST and
recomputed, rather than string-substituted, so alias forms (`@/../../shared/
types`) and per-specifier `type` modifiers survive.
Four cases the mechanical pass had to handle, each found by a gate rather than
by reading the diff:
- Modules inside `src/shared` import the barrel as `./types`, not
`shared/types`. A pre-filter on the latter string skipped 176 of them and
left imports dangling at a deleted file, which surfaced as confusing
`Property 'x' is optional in type 'Repo' but required in Pick<Repo, ...>`
errors rather than "module not found".
- The barrel RENAMED one type on the way through
(`WorkspaceSource as WorkspaceCreateTelemetrySource`), so the original name
in the owning module has to be re-aliased at each consumer.
- Three test files put `;(globalThis as ...)` on the line after the import.
TypeScript parses that `;` as the import statement's terminator, so
replacing through `statement.getEnd()` deletes it and breaks ASI. The
rewrite now stops at the module specifier.
- A file that already imported directly from a module got a SECOND import
from it, because the barrel re-exported those same names — which trips
`import/no-duplicates` under `--deny-warnings`. A post-pass merges
declarations sharing a specifier and type-only-ness; the `import type` plus
`import` pair from one module is left alone, since that form is allowed.
Splitting one barrel import into several genuinely adds lines, which pushed
`terminal-layout-pty-ownership.ts` to 301 counted lines: its 107-character
import must wrap, and neither local type collapses onto one line (101 and 116
characters). Rather than contort a type declaration to fit a line budget,
`collectLeafIds` and `pruneLeaves` move to `terminal-pane-layout-tree.ts` —
they are pure structural operations on the layout tree and independent of PTY
ownership. `visible-worktrees.ts` similarly loses its own mini-barrel
re-export of `isDefaultBranchWorkspace`, with the four real consumers
repointed at the declaring module. No `max-lines` bypass added.
Verified: cold `tsc --noEmit` green on node, cli, and web (buildinfo deleted
first — these projects are `composite: true` and reuse stale caches); the full
`pnpm lint` green, not just bare oxlint — the narrower local check is what let
the duplicate imports reach CI; max-lines ratchet OK at 344.
The generated tmux shim fell back to a bare orca / orca.cmd / orca-ide, and cmd.exe resolves an unqualified command against the current directory before PATH (sh does the same via ./empty PATH entries), so a stray orca.cmd in an agent's checkout could run with the agent-teams team id and token in its environment.
Resolve only absolute paths, honor the Windows Path env spelling, degrade to in-process teammates when no CLI can be qualified, and make both shims exit 127 instead of guessing. Verified on macOS, Linux (dash + bash), and Windows (cmd.exe + Git Bash).
Fixes STA-4215.
* fix(worktree-scan): keep the admin-fingerprint wait inside the caller's per-repo budget
The awaited probe was capped at 10s while `computeResolvedWorktrees` gives each repo
5s, so a slow mount always blew the budget: the caller gave up and republished
persisted rows. The resolved snapshot was then stamped from the *start* of the
compute, so a compute longer than its 1s TTL published an already-expired entry and
the next poll repeated the whole 5s wait — deterministically, on every TTL expiry.
Cap the probe at 2s so the remaining budget still covers the fallback
`git worktree list`, and stamp the snapshot on completion.
* fix(worktree-scan): derive the probe deadline from the caller budget
A flat 2s cut reuse for hosts whose probe lands between 2s and 5s, which used to fit
the caller's budget — trading the stall for a repeating `git worktree list`. Subtract
a fallback-scan allowance from RESOLVED_WORKTREE_REPO_TIMEOUT_MS instead, so the
invariant holds by construction and only probes that could not have fitted are cut.
Tests now pin both ends: too large fails the budget invariant, too small fails reuse
for a slow-but-healthy probe.
* refactor(store): unify the catalog structural-equality walks
Three near-identical structural deep-equality walks had landed independently in
the same window: areValuesEqual (#13744, repo-identity-reconcile.ts),
areCatalogEntriesEqual (#13770, repos.ts — already folded into the first on this
branch's base) and catalogValuesEqual (#13662,
worktree-catalog-reconciliation.ts). All three walk plain records and arrays and
fall back to reference equality for anything exotic.
They are not interchangeable. Two axes genuinely differ, and each caller depends
on its own side:
- Own-key set. #13744/#13770 require equal own-key counts plus hasOwnProperty,
so an absent key differs from a key present and holding `undefined`. #13662
compares the union of both sides' keys, so those are equal. The strict side is
load-bearing: the repo/project merges branch on
`'localWindowsRuntimePreference' in project` (repos-project-runtime.test.ts
"clears stale local runtime preferences"), and projects are now reconciled
with this comparator. The loose side is test-pinned by
worktree-catalog-reconciliation.test.ts "reuses rows with equivalent nested
catalog data", where a locally built row carries `optional: undefined` that
the host omits.
- Leaf comparison. #13744/#13770 use `===` (NaN never equal, 0 equals -0);
#13662 uses `Object.is` (the reverse).
So instead of picking a winner, src/shared/structural-value-equality.ts holds
one walk parameterised by those two axes and exports the two policies as
`structuralValuesEqual` and `structuralValuesEqualIgnoringUndefined`. Every
caller keeps its exact current semantics; the ~40 duplicated lines and the
silent divergence go away. src/shared/persisted-ui-equality.ts (a fourth copy
with a Set branch and no plain-object guard) is deliberately left alone: it
gates a disk write in main with no direct test coverage.
Also folded, all provably behaviour-identical:
- The `${hostId}\0${repoId}` composite key had three copies
(getRepoHostIdentityForParts, repoOwnerKey, getEntryKey) that must agree or
repos silently stop reconciling. Moved to src/shared/repo-host-identity.ts
because one of them lives in src/shared; the renderer module re-exports it.
- mergeFetchedReposForHost's hand-inlined upsert loop now calls mergeByIdentity.
mergeByIdentity additionally skips replacing a structurally equal row, which
cannot change the result here: reconcileFetchedRepos runs immediately after
over the same identities in the same order and restores exactly those rows.
- Renamed repos.ts's `catalogRowsUnchanged` to `arrayElementsUnchanged`. It is a
pure element-identity compare, two files away from
`catalogRowsEqual`, which is a full structural compare.
src/shared/structural-value-equality.test.ts pins both policies over arrays,
nested records, null-prototype records, absent-vs-undefined keys, symbol keys,
and non-plain objects (Date/Map/Set/class) falling back to reference equality.
* fix(store): keep merged sourceRepoIds order host-independent
Prefixing the cross-host remainder made a cross-host project's sourceRepoIds
order a function of the refreshing host, so the projects reconcile never reused
the row. Also pins the repo-derived host-id contribution the new per-project
slice feeds the host-id resolvers.
Co-authored-by: Orca <help@stably.ai>
* refactor(store): migrate call sites that landed after this branch
github.ts and ai-vault-session-identity.ts began using areValuesEqual on main
while this branch was stale, and repo-identity-reconcile's record reconciler
still called its own deleted walker. All three now use structuralValuesEqual;
reuseEqualCatalogRows keeps its duplicate-id cap and calls the ignoring-undefined
variant, which is the key-union semantics catalogValuesEqual had.
---------
Co-authored-by: Orca <help@stably.ai>
Why: a hook-path or scan refusal that races the caller's abort was
converted into a WSL unavailability error, so cancellation looked like
a stalled distro.
Three test-suite problems, all root-caused in the tests rather than in
production behavior.
1. src/relay/agent-exec-handler.test.ts (real failure, not a flake)
The two spawn-argument assertions failed with "Number of calls: 1" — spawn
ran, but the env differed. Cause: both assert
`expect.objectContaining({ ...process.env, ... })`, which demands that every
ambient variable reach the child verbatim. #7986 (1a6abc87d1) changed both
sides at once: it rewrote the assertion from `env: process.env` to that
objectContaining form, and in the same commit made the handler apply
`applyTerminalGitCredentialPromptGuard`, which appends its own entries to
Git's indexed-config protocol (GIT_CONFIG_COUNT / KEY_n / VALUE_n).
So whenever the test runner's own environment already carries that protocol —
exactly what Orca exports into its agent terminals — the snapshot expects
GIT_CONFIG_COUNT=2 while the correctly guarded child gets 4. The test passes
on a bare CI shell and fails when run from a guarded terminal.
The implementation is right: appending the guard after the caller's config is
the documented contract, and "guards wrapped agents after atomically replacing
inherited indexed config" already covers it. Fixed the test instead, by
clearing the guard-owned keys (GIT_CONFIG_* protocol and WSLENV) from the
ambient env for the duration of the suite and restoring them afterwards, so
the passthrough baseline is deterministic. No assertion was weakened or
removed.
2. project-view-wrapper-source-context-boundary.test.ts (flake: 30s timeout)
`buildProjectWorkItem` is a pure function, but it lived in
ProjectViewWrapper.tsx, so importing it pulled in the store, sonner, lucide,
and the whole UI kit — ~8.8s of transform and module evaluation for one
assertion, which tipped past the 30s limit under parallel load.
Extracted it to project-work-item.ts (its only dependency is
githubProjectHost) and pointed the test there. Both test cases are unchanged.
Also dropped the now-unneeded happy-dom environment, since nothing in the file
touches the DOM any more. 9.15s -> 0.12s.
3. model-manager-download-resume.test.ts (flake: 30s timeout)
"bounds a server that advances by pathologically tiny segments forever"
drives the loop to the MAX_TOTAL_DOWNLOAD_REQUESTS ceiling of 4096. Each
iteration did a real writeFileSync plus two statSync calls through
getPartialDownloadBytes — ~12k synchronous filesystem syscalls in a tight
loop. Fast on an idle disk, but it serializes against every other vitest
worker on a loaded machine, which is what blew the per-test timeout.
Stubbed getPartialDownloadBytes to read the byte counter the test already
maintains, so the loop is pure CPU. The file was only ever a stand-in for that
counter. Ceiling and rejection assertions are unchanged: 332ms -> 15ms.
The two remaining ~1.1s cases in that file spend their time in the real 1s
retry backoff around real stream and file-write plumbing; they are left on
real timers because faking them would mean faking the transport too, and 1.1s
leaves ample headroom.
`src/preload/api-types.ts` was 3,752 raw lines (3,533 counted, 11.8x the
300-line budget) behind an `eslint-disable max-lines`. Almost all of it was a
single `PreloadApi` object type whose ~83 namespace properties were declared
inline, so any IPC surface change meant editing one 2,600-line type.
Give each namespace a named type in its own module under `src/preload/api/`
(`pty-api.ts`, `filesystem-api.ts`, `github-pull-request-api.ts`, ...) and
recompose `PreloadApi` from those names. `api-types.ts` keeps the `declare
global` Window augmentation and re-exports every moved name, so all 52 import
sites are untouched.
Two shapes needed care to stay type-identical rather than merely compatible:
- Three keys (`gh`, `git`, `ui`) are composed from two modules each. A plain
intersection is NOT identical to the original flat object literal, so those
use a `Merged<T>` mapped type; a negative control confirmed that dropping it
fails the parity assertion.
- Keys whose module groups several namespaces use indexed access
(`fs: FilesystemApi['fs']`) to preserve exact identity and source order.
`config/tsconfig.web.json` and `tsconfig.tc.web.json` enumerate files by path,
so they need `src/preload/api/**/*` alongside the existing `api-types.ts` seed
or the web projects fail TS6307.
Verified by exact type identity, not assignability: 41 assertions of the form
`Equals<Now.X, Before.X>` against a frozen pre-split snapshot, covering every
exported name, plus a per-key pass over all 83 `PreloadApi` keys. All three
projects typecheck clean with those assertions active.
Verification note: these tsconfigs are `composite: true`, and `tsc --noEmit`
will reuse a stale `.tsbuildinfo` and report clean for a state that genuinely
fails. Every result above was produced after deleting the buildinfo, including
a negative control confirming the gate still fails on deliberate drift.
Drops the `max-lines` bypass and its baseline entry (ratchet 346 -> 345).
`src/shared` is a flat directory of ~1,150 entries. The worktree, github, and
linear domains accounted for 71 of them, so finding the module you wanted meant
scanning a wall of same-prefixed filenames.
Move each domain into its own folder and drop the now-redundant prefix:
src/shared/github-pr-types.ts -> src/shared/github/pull-request-types.ts
src/shared/worktree-id.ts -> src/shared/worktree/id.ts
src/shared/linear-links.ts -> src/shared/linear/links.ts
This follows the existing `network/` and `new-workspace/` convention in the
same directory, which also drop the prefix inside the folder.
Whole clusters move, including tests. Foldering only part of a domain would be
worse than flat: a reader would have to check both `github/` and the flat
directory, and `github-auth-types.ts` / `github-project-types.ts` are type
modules that belong with the rest. No files with these prefixes remain flat.
Import specifiers were rewritten by resolving each one to an absolute path and
recomputing it, not by string substitution, so the `@/../../shared/...` alias
forms are handled correctly. 501 specifiers across 298 files.
Two things `tsc` cannot catch, handled explicitly:
- `github-project-types.ts` carries its own `max-lines` bypass, so its baseline
entry is REPOINTED to the new path rather than pruned. Pruning would drop the
bypass and then flag the new path as a fresh violation. Ratchet stays at 345.
- `mobile/` is outside `pnpm typecheck` and cannot be typechecked here
(`mobile/node_modules` is empty). Instead every relative specifier in the repo
was resolved against the filesystem: 174 unresolved before this change and 174
after — identical, so nothing broke in mobile either.
The pinned `tests/e2e/.cross-version-checkouts` fixtures are deliberately NOT
rewritten; they are a snapshot of an older release and still reference the old
paths.
Verified: cold `tsc --noEmit` green on node, cli, and web (buildinfo deleted
first — these projects are `composite: true` and reuse stale caches).
`src/shared/types.ts` was 3,981 raw lines (2,825 counted, 9.4x the 300-line
budget) behind an `eslint-disable max-lines`, and is imported by 2,092 files —
the single widest contract surface in the repo.
Move all 320 top-level declarations into 46 per-domain modules
(`repo-types.ts`, `worktree-types.ts`, `github-pr-types.ts`, ...) and reduce
`types.ts` to an explicit re-export barrel, so the 2,092 import sites are
untouched.
`HostSettingOverrides` moves into the pre-existing `host-setting-overrides.ts`
alongside the accessors that operate on it, which also removes that module's
circular import back into `types.ts`.
Named re-exports only, never `export type *`: with star re-exports a name
exported by two modules is silently dropped, which would surface as a confusing
"has no exported member" at a random call site.
Verified lossless mechanically, not by inspection:
- export parity — the module's resolved export set through the TS checker is
identical before and after (396 names, no additions, no removals)
- declaration parity — all 320 declarations compare character-identical modulo
comments and whitespace, so no optionality, union order, or generic
parameter drifted
- `tsc --noEmit` green on the node, cli, and web projects
- `oxfmt --write` is byte-identical, so the barrel is format-stable
Drops the `max-lines` bypass and its baseline entry (ratchet 346 -> 345).
* fix(workspaces): gate the GitHub palette number match on repo identity
`repoMatchesGitHubSlug` returned the permissive `'unknown'` whenever the repo
displayName was not in `owner/repo` form and no upstream metadata existed — the
common basename-named non-fork case. The caller only rejects on `false`, so a
pasted issue/PR URL could activate a workspace in a different repo that happened
to share the number, since issue/PR numbers are per-repo.
Mirror the GitLab gate from #14381: fall back to the probed
`gitRemoteIdentity.canonicalKey` before giving up, comparing host and owner/repo
after normalizing port, `www.`, and case. An `upstream`-derived identity stays
`'unknown'` because `deriveGitRemoteIdentity` ranks `upstream` above `origin`, so
a fork's own origin is invisible and rejecting would drop URLs from the fork the
user actually checked out.
The canonicalKey compare runs after the displayName branch: displayName is
compared host-agnostically, so mirrors and host aliases of the same owner/repo
keep matching as they do today, and the probed remote only fills in where no
name evidence exists.
Refs STA-4237
* fix(workspaces): keep SSH host aliases matching in the palette identity gate
`git remote -v` reports ssh.github.com, www., and ~/.ssh/config `Host` aliases
verbatim, so comparing a probed canonicalKey against a pasted URL host rejected
legitimate GitHub/GitLab remotes. Normalize the alias hosts both sides can fold
offline, and downgrade a host-only mismatch to 'unknown' when the probed host is
dotless (an unexpandable OpenSSH alias); dotted hosts like ghe.example.com still
lose. Lifts the GitHub host normalizer into shared instead of a third copy.
* fix(repos): keep the www host fold out of the derived project identity
getProjectIdentityKey feeds the persisted Project id, so folding www. there
re-keyed existing projects on upgrade and dropped localWindowsRuntimePreference.
Restrict the fold to the palette's URL-vs-remote comparison, and pin the derived
id for a www. remote so it cannot drift silently again.
* fix(repo-identity): re-probe resolved git remote identities on a long TTL
A resolved gitRemoteIdentity was written once and frozen for the life of the
repo record, so adding an `upstream` remote later — or a project rename or
transfer — left identity gates judging against the path the repo had when it
was added. Re-probe resolved repos on a 6h TTL, seeded 5 minutes after a repo
is first seen in a process and capped at 4 refreshes per sweep so a restart
cannot fan out a subprocess per repo. Only a successful probe that yields a
different canonicalKey overwrites; failures and no-remote answers leave the
existing identity alone.
Also explain why the worktree-scan admin fingerprint timeout deliberately
exceeds its caller budget, and log when that probe expires — expiry was
silent and indistinguishable from "fingerprint unavailable".
Refs STA-4247
* fix(projects): carry project state across derived project id changes
A project id is derived from repo identity, so a remote re-probe (or a
repo:->git:->github: promotion) rewrites it. The compatibility merge matched
prior rows by id only, dropping the user's localWindowsRuntimePreference and
leaving a ghost project row that independent host setups still pointed at.
Both merge sites now fall back to the prior row whose sourceRepoIds overlap and
re-point independent setups at the surviving project.
* fix(terminal): stop raw PTY-not-found and session-expired tokens reaching the error toast
A reattach the host answers "no such session" for surfaced its wire token
verbatim — `SSH_SESSION_EXPIRED: orca:<conn>@@pty-N` or the relay's raw
`PTY "..." not found` — including the internal PTY id, and invited the user
to file an issue for an ordinary lifecycle event.
Humanize both in the toast's existing daemon-boundary seam and mark them
explained so the issue link is suppressed. The copy stays silent on whether
the remote shell died: absence from the host is not proof of exit.
Refs STA-4238
* style(terminal): apply oxfmt to the toast humanization tests
* fix(terminal): treat the humanized session copy as literal replacement text
Five pins compared indexOf positions without asserting presence, so a missing needle returned -1 and the assertion passed. Deleting the legacy-shim-dir capture in both Windows wrappers stayed green that way, disabling legacy-dir PATH removal. All five now route through a helper that asserts both operands exist first.
Adds coverage for four properties proven load-bearing by execution: the path_entry_kept guard (without it an empty cleaned PATH resolves a cwd-local git), the cleaned PATH on exec, and multi-separator matching in the shared path matcher.
* fix(workspaces): gate the GitLab palette number match on repo identity
After a full GitLab identity compare failed, matching fell through to an
unguarded iid compare, so a pasted URL could activate a workspace from another
project or host. GitLab iids are per-project and start at 1, so small-number
collisions across projects are the norm.
Mirror the GitHub shape rather than rejecting outright: gate the number-only
fallback on a tri-state repo identity check that stays permissive when identity
is unresolvable, so forks and host aliases keep matching. A stored URL that
parses to a different project with the same type and number is a direct
contradiction and is rejected even when identity is unknown.
Refs STA-4155
* fix(workspaces): keep the GitLab repo gate permissive for fork checkouts
deriveGitRemoteIdentity keeps a single remote and ranks `upstream` above
`origin`, so a fork checkout resolves to the upstream project and the fork's
own origin is invisible to the gate. Rejecting on that mismatch dropped MR
URLs from the fork the user actually checked out — a false negative the
tri-state was meant to prevent. Treat an upstream-derived identity as unknown.
Refs STA-4155
* docs(workspaces): note that the GitLab repo identity is a one-shot snapshot