* fix(browser): focus unified tab on browser page palette activation
When activating a browser page from the palette, find and focus the
corresponding unified tab before setting active state. Ensures the
tab group receives focus. Also increase e2e test timeouts to improve
stability on slower runners.
* test(e2e): read latest restored terminal frame
* Fail browser page activation when unified tab is missing
Without a unified tab, the workspace can't render in the pane. Reporting
success leaves the previous tab on screen. Fail the activation to prevent
this confusing state.
* Add skill deletion with cross-platform transaction safety
Implements end-to-end skill removal with placement enumeration, dependency guards, and transactional recovery. Covers native, WSL, and remote hosts; users can delete canonical directories and alias placements (symlinked directories or files) in a single atomic batch. Includes UI selection flow, preview, confirmation, and results band. Block reasons (bundled, plugin, unowned, stale) gate deletions that would fail or contradict user intent.
* Organize IPC handlers into module subdirectories
Move register-core-handlers and skill-delete-ipc-handlers into
dedicated subdirectories for improved code organization and to
reduce the flat structure in src/main/ipc/.
* Make skill deletion recovery transactions idempotent
Defer journal cleanup until both staging removal and receipt cleanup succeed, leaving the journal in place for startup to retry if either operation fails. This ensures the recovery process is safe to run multiple times without leaving partially-deleted skills.
* Consolidate skill-delete files into dedicated module
Reorganize skill deletion functionality into a modular structure under
`src/main/skills/skill-delete/` with simplified file names. Remove the
redundant `skill-delete-` prefix from file names since they now live in
the dedicated directory. Update all import paths throughout the codebase
to reflect the new structure, including imports from IPC handlers and
RPC methods.
* Fix broken import paths and add deletion robustness improvements
Import paths using `..//'` were invalid and broken. Replace with explicit
module names (`skill-discovery-sources`, `skill-install-filesystem`, etc.)
to clarify dependencies.
- Bind WSL filesystem methods to preserve `this` context
- Keep recovery journal when rollback rename fails, so startup can retry
- Skip symlink-based tests on Windows where they cannot run
- Only treat ENOENT/ENOTDIR as empty directories; propagate other errors
- Fix cross-platform path parent calculation to handle drive roots
- Replace shared constant with localized string for user-facing message
- Use `runProcess` for WSL integration test instead of bare `execFile`
* Add batch limit for skill deletion and improve host availability checkin
- Limit concurrent deletions to prevent remote host overload
- Add retry logic for capability probing to handle transient unavailability
- Add reprobe() method to recheck capability after errors or user refresh
- Fix status logic: receipt cleanup is best-effort, completion depends only on content removal
- Improve error message for unreachable hosts
* fix(cli): seed nvm's default version, not the newest install
#16314 stopped the login-shell probe inheriting the seeded PATH, but left the
seed itself picking the newest installed nvm version. That ordering decides
which node a CLI runs under whenever the probe does not land — a timeout, or a
login shell whose rc never initializes nvm — and newest is precisely the wrong
guess: it is usually the version the user just added and has installed nothing
into. That is the root cause reported in #10932.
Resolve `alias/default` instead, mirroring nvm: follow the alias chain
(`default` -> `lts/*` -> `lts/krypton` -> a version), resolve a partial version
like `24` to the highest matching install, and treat `system`/`node`/`stable`
as no preference. The chain is bounded and cycle-guarded because nvm's own
resolver tracks seen aliases and hand-edited files can point at each other.
Ordering is a preference, not a restriction: the remaining versions stay behind
the default, so a CLI installed outside it is still reachable.
Measured on a real machine with nvm default=24 and a bare v26.7.0 installed:
the old resolver seeds v26.7.0/bin (no CLIs), the new one seeds v24.18.0/bin
(every CLI). Tests were written first and verified to fail on the three bug
cases against main before the fix existed.
Also raise the probe budget from 5s to 10s. The old value was never measured
against a real profile: a bash -ilc loading nvm, rvm, conda and gcloud takes
~1s idle but 6-7s on a loaded machine, so a cold start under load silently
fell back to the seed. Startup does not block on the probe, and the one
awaited consumer is agent detection, which is better served by a probe that
finishes late than one that gives up early.
* fix(cli): reject non-version alias tokens instead of matching v0.x
Review finding, and a real bug I introduced. parseVersionSegment coerces
every unparseable segment to 0, so an unresolvable default alias — `garbage`,
`iojs`, `lts/nonexistent`, any hand-named alias — became [0] and prefix-matched
a `v0.12.x` install, or any stray non-version directory. Orca would then seed a
decade-old node as the preferred runtime. Real nvm answers N/A for all of them.
The `wanted.length === 0` bail could never have caught this: ''.split('.') is
[''], never empty. Replaced with a shape check that still admits legitimate
numeric prefixes — verified against nvm itself, which resolves `24` to
v24.18.0 and `0` to an installed v0.x while answering N/A for the rest.
Also corrects two comments that no longer described the code: the seed is no
longer "newest install", and the probe budget note claimed startup never blocks
on hydration, which is false on packaged Windows where it gates terminal
services and git. The traversal-guard comment claimed a containment join()
already normalizes away; the real guarantee is that matchNvmVersion can only
return an entry of the versions directory.
* fix(cli): match nvm's version-token grammar, not just its first character
Round-2 review finding, and the same bug one layer down. The previous guard
anchored only the first character, but parseInt stops at the first non-digit,
so `0x18`, `00` and `0abc` still parsed to [0] and prefix-matched a v0.12.x
install — the decade-old-node seed the earlier fix was supposed to close.
Reachable: `nvm alias default 0x18` warns that the version does not exist and
writes the alias anyway, then resolves it to N/A.
Use nvm's actual grammar, leading zeros included — nvm calls `00` and `024`
N/A while parseInt reads them as 0 and 24. Verified by executing 17 tokens
against a five-version fixture: every one now agrees with nvm, including the
legitimate prefixes `0`, `0.12`, `24` and `v24.18.0`.
Also drops a dead disjunct (the hop bound already caps the loop, so seen.size
can never exceed it) and corrects the log comment in index.ts, which still
told the reader a failed probe leaves the newest install in front. It leaves
the default version in front now, which is usually survivable but still not
what the shell would have resolved.
* test(cli): skip the lts/* chain fixture on Windows
Round-3 review finding. makeNvmHome materializes each alias as a real file,
and the chain case uses nvm's actual `lts/*` alias — `*` is a reserved Win32
filename character, so writeFileSync fails with EINVAL. PR CI runs a Windows
allowlist that excludes this file, so the breakage only reaches a Windows
developer running the suite locally.
Skipped rather than renamed: `lts/*` is the alias nvm really ships, and the
assertion pins platform: 'darwin' anyway, so the real name costs no coverage.
Matches the skipIf convention already used across src/shared.
Also reflows a comment line that a previous edit ran to 143 characters;
oxfmt does not reflow comments, so nothing would have caught it.
* refactor(git): split runner.ts into focused command-runner modules
* chore(ratchets): repoint child_process and wsl.exe allowlists at the split modules
---------
Co-authored-by: Neil <n@example.com>
* fix(terminal): collapse identity group in the title churn signature
Replaces the ingest-time title rewrite from #16373 with a non-destructive
fix at the actual cause.
The churn suppressor `isDecorativeAgentTitleFrameChange` keyed on the
literal label, so `working:OMP` and `working:Pi` compared unequal and every
alternating frame from a wrapped harness committed a store patch. #16373
made the labels agree by rewriting the stored title to the tab's launch
owner — but `runtimePaneTitlesByTabId` is also the Windows Shift+Enter
byte-encoding input, so normalizing at ingest destroyed evidence other
consumers read (fixed separately in #16376).
Collapse the identity group inside the signature instead. Which member of
a group a frame names is decoration, exactly like the spinner glyph the
signature already strips, so frames compare equal without touching what is
stored. Suppression now changes only WHETHER a frame commits, never WHAT
it says.
Also fixes the flap under a multiplexer (#8032): the collapse runs over
wrapper segments, so "zsh | ⠋ Pi" and "zsh | ⠙ OMP" compare equal, which
the anchored owner-relabel in #16373 never matched.
Reverts the store changes from #16373 and drops the helper it added.
Co-authored-by: Seongho.Bak <49228032+psh4607@users.noreply.github.com>
* fix(terminal): fold only bare identity frames into the group token
A legacy "π - <session> - <cwd>" title is Pi-compatible too, so folding
every profile match collapsed two different sessions to the same signature
and suppressed the change outright — reintroducing #16093 through the
churn signature.
Fold only exact bare identity frames, matched per wrapper segment, so
semantic session titles keep comparing on their own text.
Co-authored-by: Seongho.Bak <49228032+psh4607@users.noreply.github.com>
* docs(terminal): correct the flap diagnosis in the repro header
Verified against the OMP source: it emits only π-glyph frames
(`DEFAULT_TERMINAL_TITLE = "π"`, title-generator.ts:25), and on an
Orca-hosted pane its native titler cedes to Orca's own injected extension,
which writes `⠋ π - <session> - <cwd>`.
So OMP emits neither "OMP" nor "Pi". Both flap sides are Orca's:
"OMP" from driveSyntheticTitleFromHook, "Pi" from normalizeTerminalTitle
collapsing our own extension's output to a hardcoded literal.
The prior header credited the wrapped harness for frames it never sends,
which is the same wrong narrative that produced eight fixes at eight
layers. No behavior change.
* fix(terminal): stop Orca mangling the OMP/Pi title it writes itself
Verified against the OMP source: it emits only π-branded frames
(`DEFAULT_TERMINAL_TITLE = "π"`, title-generator.ts:25), and on an
Orca-hosted pane its native titler cedes to Orca's OWN injected extension,
which writes `π - <session> - <cwd>` / `⠋ π - <session> - <cwd>` at 80ms.
So neither flapping string came from OMP. Orca made both:
"Pi" — normalizeTerminalTitle collapsing our extension's output to a
hardcoded literal, discarding the session name and cwd (#16093)
"OMP" — driveSyntheticTitleFromHook injecting over it every 80ms
Fixed at the source:
- normalizeTerminalTitle canonicalizes only the rotating braille frame and
keeps the rest, in both spinner positions and through a multiplexer
prefix (#8032). Status still round-trips through normalization.
- detectAgentStatusFromTitle reads the π state separator, so `π ! <label>`
is permission instead of the blanket idle that hid a blocked agent.
- normalizeCompatibleAgentTitleForOwner swaps only the brand for the
owner's label, so a pane still reads as its launch owner (#6689, #7633,
#9077) without losing the session text.
- pi/omp set synthesizeWorkingTitle: false — the agent animates its own
working title. Terminal states still synthesize; they carry the pane's
agent identity downstream.
Reverts the ingest-time title rewrite from #16373, whose normalization of
runtimePaneTitlesByTabId also changed Windows Shift+Enter bytes (#16376).
Co-authored-by: Seongho.Bak <49228032+psh4607@users.noreply.github.com>
* fix(terminal): match the state separator only in exact profile casing
The separator check runs on every title, so `omp - deploy notes` and
`pi - refactor the parser` read as an idle agent. The owner rewrite only
ever emits the exact profile labels, so dropping case-insensitivity keeps
`OMP - tmp` classifying while ordinary prose stops matching.
Co-authored-by: Seongho.Bak <49228032+psh4607@users.noreply.github.com>
* test(terminal): pin one real OMP turn to two committed patches
Drives 30 working frames as Orca's injected extension emits them plus the
idle transition, and asserts what survives the churn gate. Before the fix
every frame alternated "⠋ Pi"/"⠋ OMP" and each one committed — ~12 store
patches per second on a working tab.
Co-authored-by: Seongho.Bak <49228032+psh4607@users.noreply.github.com>
* fix(terminal): carry the permission guard inside the separator reader
`-` is both a π state separator and the delimiter in the synthetic
permission label, so `OMP - action required` read as idle. It resolved
correctly only because detectAgentStatusFromTitle happens to check the
synthetic label first — and the separator fn is exported, so a direct
caller inherited the bug.
Also pins the owner rewrite's fixed-point property, which holds only
because getAgentLabel does not tokenize omp/pi, and corrects a comment
that overstated how tightly the brand swap is scoped.
Co-authored-by: Seongho.Bak <49228032+psh4607@users.noreply.github.com>
* docs(terminal): name the flag the code actually sets
The suite header cited `synthesizeTerminalTitle: false`; the profiles set
`synthesizeWorkingTitle: false`. The distinction is the whole reason the
narrower flag was chosen — terminal-state frames still carry the pane's
agent identity downstream — so the wrong name buried the rationale.
Co-authored-by: Seongho.Bak <49228032+psh4607@users.noreply.github.com>
---------
Co-authored-by: Seongho.Bak <49228032+psh4607@users.noreply.github.com>
* refactor(ipc): split repos.ts into focused modules
* test: point repo notification mocks at the extracted module
* fix(ipc): repoint the child-process allowlists after the repos split
The type-only `import type { ChildProcess }` moved from repos.ts to
repos/repo-clone-lifecycle.ts, so the import-boundary entry follows it and the
windows-console entry (now stale, and that list only shrinks) is dropped.
Fixture-only; the base file had no runtime child_process use at all.
* perf(git): make local Git metadata observation event-driven
Replaces the recurring per-repo metadata scan with native filesystem events on
macOS, Linux, and Windows. Polling is retained purely as a fallback.
- Narrow @parcel/watcher stream over <common>/worktrees, extended from macOS to
Linux and Windows, with the Windows backend pinned explicitly.
- New shallow watcher mode over the allowlisted primary metadata leaves. It
watches the containing directory rather than each file, so Git's atomic
write-and-rename does not orphan the binding.
- Selected upstream refs stay on the existing bounded stat poll.
Verified on real hosts rather than in principle:
- Windows: `git worktree remove` and `git worktree prune` both succeed while the
narrow stream holds the directory. The historical concern that an open handle
would block prune does not reproduce.
- Linux: inotify costs one instance per event loop, not one per watch, so the
watch budget is not a constraint.
- macOS/Linux/Windows: shallow events survive repeated commit, checkout,
config, and pack-refs cycles.
Failure handling, each reproduced before being fixed:
- fs.watch binds an inode and reports nothing once that inode is replaced, with
no error. Directory bindings are re-checked on a bounded cadence and rebound.
- A host whose notification path is dead accepts registrations and stays mute
forever. Observed on a macOS machine whose fseventsd had grown to ~15GB and
saturated a core. A one-shot delivery probe now fails the shallow subscribe on
such a host so it falls back to polling instead of showing stale metadata.
This change stands alone on main and does not depend on the metadata poll
scheduler.
* test(git-watch): hold reserved inodes across root replacements
Linux returns a released reservation to the free list, so the second
replacement could land back on the first replacement's inode and look
unchanged to reconciliation. Verified on ext4: releasing yields inodes
[N, N+43, N+43] while holding yields [N, N+43, N+44]. macOS never
recycles, which is why this only failed on CI.
* refactor(git-watch): share one single-flight helper between watcher fallbacks
Both fallbacks tracked their in-flight promise with the same self-comparison
on settle, duplicated verbatim. Hoisting it removes a subtle invariant that
was being hand-maintained in two places.
* fix(git-watch): close the silent-staleness paths in primary metadata
Two independent reviews converged on the same root cause: nothing bounded
how long primary metadata could stay wrong once the shallow watcher stopped
reporting. Four distinct paths led there.
- A terminal watch error arriving while the status-ref poll was still starting
left the repo with status-ref coverage only. handleWatcherError ran its
teardown against nulls, then the in-flight poll installed itself, and the
fallback guard mistook it for coverage and discarded the fallback. Primary
metadata was then never observed again. The guard no longer treats status-ref
polling as primary coverage, and startup re-checks watcher liveness after its
awaits.
- Nothing re-read the six primary files while the watcher was nominally live.
A lossy notification path, a dropped batch, or inotify queue overflow raises
no error, so the error-driven fallback never fired and the inode rebind sweep
does not detect loss. A 15-tick backstop re-stats them, turning permanent
staleness into one tick. Measured cost is ~0.2 stats/s/repo against the 3/s
the old poll cost.
- Reconciliation treated any late-observed entry create as a root replacement,
so an ordinary tore down a healthy stream ~30s later and
opened a deaf window. It now also requires the root itself to be recreated.
- The shallow watcher recorded directory identity from a stat issued after
binding, so a replacement in that gap pinned the dead inode's watcher to the
new identity and the sweep would never rebind. Identity is now read first,
which errs toward a harmless extra rebind.
Test helper: replacing the worktrees root frees several inodes at once, so
reserving one still let the recreated root reuse its own. It now verifies the
inode actually changed. Confirmed on ext4, where three holds were needed.
* Refactor terminal coordination modules
* preserve terminal completion and stale-connect guards
* restore pre-spawn E2E barrier and stale-connect check order in ipc-pty-connect
* restore merge-base title-working replay and stamped-tail delete semantics
* fix(terminal): merge the duplicated shortcut-matching import
Two adjacent imports of the same module tripped oxlint's
no-duplicate-imports under --deny-warnings. Import-only; no behavior change.
* refactor(rate-limits): split Codex and Claude fetchers
* refactor(rate-limits): restore base error-message defaulting
The split moved the 'Unknown error' fallback from inside String() to the call site, which changed behavior for an Error with an empty .message: base surfaced '', head surfaced 'Unknown error'. Restore the base form.
* refactor oversized Electron facilities
* fix interactive process timeout and shortcut repeat guard
* chore(child-process): drop stale cli-installer allowlist entry
cli-installer.ts now routes privileged spawns through runProcess via
cli-privileged-processes.ts, so the shrink-only ratchet flags it as stale.
* refactor(child-process): extract the bounded output sink
runProcess's timeoutMs opt-out (required to preserve the unbounded osascript
admin prompt) pushed run-process.ts past the 300-line cap. Move createOutputSink
to its own module rather than add a max-lines bypass, which AGENTS.md forbids.
Moved verbatim; no behavior change.
* refactor(editor): split editor and watch surfaces
* fix(editor): revert behavior changes smuggled into the surface split
Restore merge-base React keys in IpynbCellOutputs: the content-identity keys
JSON.stringify'd every output value, including raw base64 image payloads, on
every keystroke.
Collapse the duplicated lazy() declarations into editor-lazy-views so each
viewer keeps a single React.lazy identity across the extracted surfaces.
* fix(crash-reporting): correlate concurrent process deaths on a renderer report
Two 1.4.184 reports (a326935a, 1862f316) are renderer "crashed"/-1 crash reports
whose renderer only died alongside a sibling Chromium child that died at the same
instant:
F0BQMB30GJX network.mojom.NetworkService crashed/-1 -21ms -> renderer crashed/-1
F0BRPP8TC0Y audio.mojom.AudioService crashed/-1 -2ms -> renderer crashed/-1
GPU crashed/-1 +180ms
process-gone-classification.ts classifies each event in isolation:
isRecoverableChromiumChildProcess discards the utility/GPU halves as recoverable
churn, and `if (reason !== 'killed') return true` then reports the renderer half as
a genuine renderer crash before any cross-source signal exists. Triage reads
"renderer crashed" for what died with three other processes.
process-gone-sibling-correlation keeps a bounded ring of child deaths, populated
before the suppression early-return so churn-suppressed siblings stay visible, and
matches a renderer death against child deaths sharing its failure signature.
What the timing can and cannot support:
- The window is asymmetric. 1s of lookback (a child that died first can plausibly
have taken the renderer with it), but only 250ms of lookahead: a child dying well
after the renderer is at least as likely to be an effect of it — Chromium tearing
down the dead renderer's channels, or renderer_recovery_reload at +264ms — and a
symmetric window would retro-label a genuine lone crash as collateral.
- crashAttribution is 'concurrent-process-deaths', not a causal claim. The largest
1.4.184 cluster is an external taskkill /T where renderer and children are
co-victims; no sibling caused anything there.
- The verdict is not derived from timing alone. A host with a child looping at the
observed 1459/min drops a death into every window, so crashAttribution is set only
when the nearest sibling is within 250ms and no identity repeats. Looser or
repeating deaths still ship as evidence (siblingProcessDeathCount, signed offsets,
siblingProcessDeathRepeats) with no attribution.
- The signature match buckets `crashed` with `abnormal-exit` and gates on the exit
code only on win32. Both fixtures are win32, where every process in a collateral
pair reports crashed/-1; POSIX surfaces a per-process wait status, so an equality
gate would mean this never fires on macOS or Linux.
The report stays reportable and gains evidence rather than being suppressed
(#14667). Both arrival orders are covered without delaying persistence: a sibling
that dies first is folded into the initial record, a sibling that dies after amends
the record already on disk through attachDetails, the same way the minidump
signature does. Late amends are capped at two per report and skipped when the
rendered evidence is unchanged, so a crash-looping child cannot rewrite the store
during renderer recovery, and a failed amend now leaves a
sibling_attribution_attach_failed breadcrumb instead of vanishing.
Relationship to #12484: it is still OPEN and adds process-tree-kill-window.ts, the
same ring/lookback bookkeeping with a 250ms settle, patching the same recorder
hunks with the opposite policy (suppress the killed/1 renderer report instead of
keeping it). #14667 is test-only — it pinned the keep-the-report policy in tests, it
did not remove a shipped implementation. #12484 has to be closed or rebased out
before this lands.
* chore: remove merge hook formatting drift
* fix(linear): make new-issue dialog popovers scrollable
`[data-slot='popover-content']` already caps every popover to
`--radix-popover-content-available-height`, but PopoverContent's base class is
`overflow-hidden`. A team list taller than that cap is therefore clipped at the
window edge with no scrollbar and no way to reach the entries past the cut.
The dialog's other attribute popovers had an inner max-h-60 box, but none of the
six carried the popover-scroll-content / popover-wheel-scroll marker that
popover.tsx's wheel shim needs, so Radix's dialog scroll-lock swallowed the wheel
there too.
Move all six to the popover-scroll-content pattern already used by
LinearItemDrawer, JiraIssueWorkspace, and github-item-dialog: it re-declares the
cap as min(15rem, available-height) and adds overflow-y: auto, and the class name
opts the content into the wheel shim.
Measured on the team switcher with 25 teams:
before max-height 611px, overflow-y hidden, 609 of 735px visible
after max-height 240px, overflow-y auto, scrollTop reaches 497
The inner max-h-60 boxes are dropped because stacking them under the outer cap
creates nested scrollers whose combined height exceeds it, leaving the bottom of
each list unreachable.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test(linear): match the inner-scroller classes regardless of order
The previous assertion pinned one exact class order, so reintroducing the
wrapper as `scrollbar-sleek overflow-y-auto max-h-60` slipped through. Collect
the section's `<div>` classNames and check the three tokens as a set instead.
Scoped to wrapper divs on purpose: the dialog's description textarea caps its
own growth with those same classes and is not a popover child, so a plain
whole-section match flags it as a false positive.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Worktree removal inventories PTYs through DaemonPtyAdapter.listProcesses. That
called ensureConnected bare, so once the terminal-host pipe was dead the
removal failed with `connect ENOENT \\?\pipe\orca-terminal-host-...` and stayed
broken until the whole app was restarted.
spawn already wrapped its work in withDaemonRetry and recovered from exactly
this. Inventory did not — so the one path that must not get stuck was the only
one that could not heal itself.
Both the connect and the listSessions request go inside the retry: a host that
dies between them throws the same daemon-gone error, so retrying only the
connect would still fail. The reconciliation after the request is deliberately
outside it; retrying that would be wrong.
Reproduced first, with a real daemon killed mid-test: listProcesses threw
DaemonConnectionLostError while a control asserting spawn recovery from the
identical kill passed. Both are now regression tests, so the asymmetry cannot
come back silently.
Co-authored-by: innocarpe <innocarpe@users.noreply.github.com>
* fix(ssh): restore the reconnect model-paint gate dropped by #15166#15166 split pty-connection.ts and dropped the "paint from main's model on SSH
reconnect" half of the reattach gate that shipped in v1.4.188 (#14844), leaving
only the park-reveal half. A non-park SSH reconnect has repainted from the
~100KiB relay tail ever since, which cannot rebuild a full-screen frame whose
start it no longer holds.
Restores followsDirectSshReconnect (PENDING-only retry read), reconnectMayUseModel,
the exited-transition veto computed before the probe, and the kitty scanReplay
layered after the snapshot baseline. Adds a call-site test over
createReattachPayloadHandlers, because the surviving pure-function test stayed
green through the entire removal.
Fixes STA-5395
* fix(ssh): restore empty-tail reconnect snapshots
The pairing ratchet matched spawn|spawnProcess|spawnSync|runProcess only, so
a resolved CLI handed to execFile was the same unpaired launch with none of
the enforcement. codex-trust-grant-host.ts resolves codex and calls
execFileSync, and escaped the ratchet purely through that omission.
Widen to the exec/fork family. The negative lookbehind keeps method calls
such as `RE.exec(` out, which is what made the bare `exec` name safe to
include; a fixture mutation confirms `/x/.exec('x')` does not trip it, and
adding execFileSync(resolvedCli) to a paired file does.
codex-trust-grant-host is allowlisted rather than changed: its only exec is
a wsl.exe identity probe for the binary stamp, and its actual codex launch
is a CodexAppServerInvocation paired centrally in codex-app-server-session.
The entry records what would invalidate it.
* refactor: split agent config and auth services
* chore: repoint wsl and global-fetch guards at split module paths
* fix: restore merge-base Claude CLI error propagation
Drop the secret-redaction rewriting added to Claude CLI error paths in the
refactor: spawn errors again reject with the original Error (preserving
.code/.errno/.syscall/.stack) and command output/auth-status logs are no
longer rewritten.
* refactor(renderer): split composer state
* fix(renderer): satisfy composer static analysis
* test(renderer): migrate composer boundary contracts
* fix(composer): restore project group reset effect
Revert read-side mask back to the merge-base state clear so a momentarily
unavailable host permanently drops the folder group instead of silently
retargeting Create when the host reappears.
Follow-up to #16365, which paired 8 spawn sites by hand. Hand-pairing is how
the class got introduced, so close it structurally instead.
cliPath is now required on CodexAppServerInvocation, `null` only for the
guest-side wsl.exe launcher where a host path pairs nothing. Optional let a
native builder omit it and silently fall back to pairing against a cmd.exe
wrapper with no type error. Every production site already passed it; only
test fixtures needed updating, which is the type doing its job.
Four more sites now pair. codex-state-db-backfill-recovery spawns the same
`codex app-server` subcommand #16365 fixed elsewhere. cli/handlers/account
was the worst case: addAgentNodePaths prepends the *newest* version-manager
bin, which is not necessarily where the CLI being launched lives, so it
actively created the mismatch — pairing now runs last so the CLI's own node
wins. commit-message-text-generation and skills/skill-update-run spawn
resolved binaries with inherited env.
cli/handlers/skills had grown its own buildNpxPath: a weaker local copy that
prepended unconditionally, ignored the Windows `Path` key, and special-cased
a '.' dirname. Deleted in favor of the shared helper, which checks the
sibling node actually exists — the behavior change one test had pinned.
The ratchet is the point: any file that resolves a CLI and spawns must
reference withCliRuntimeOnPath, with a shrink-only allowlist. It caught
skill-update-run, which I had missed. Its first draft required a call paren
and so let dependency-injected resolvers (`resolveCommand: resolveCodexCommand`)
through — verified by removing a pairing and watching it stay green, then
widened until it failed. A second assertion fails on a stale allowlist entry
so an exemption cannot outlive its reason.
external-editor-launch stays allowlisted: it launches a GUI editor, not a
Node CLI whose ABI matters.
orcad's AppEnvironment implemented three of seven AppPathNames and returned the
userData directory for the rest — including 'exe', where a data directory is not
an executable. Every name now has a Node answer: 'appData' is the platform's
per-user application-data root, 'logs' lives inside the data root so a headless
deployment stays one removable directory, 'downloads' honours XDG_DOWNLOAD_DIR,
and 'exe' is the Node binary. getAppPath() is the directory orcad was launched
from rather than cwd, so children resolve against the bundle instead of wherever
the supervisor happened to be.
The watcher child was the load-bearing consequence: resolveWatcherProcessEntryPath
probed for the adjacent entry only when !isPackaged, so orcad resolved a desktop
out/main path that no deployment has — and build-orcad never emitted the child
anyway. isPackaged stays true (consumers read it as "production, not a dev
checkout" and it gates HTTPS-only skill downloads); the resolver now asks whether
the app root is an asar archive, which is the question it actually meant. The
child ships beside orcad.js, and the build forks it to prove it runs.
* fix(i18n): localize the keep-awake corner chip
Route the status-bar keep-awake chip through the shared Agents copy
helpers and add missing locale entries for chip-only words.
Fixes#14490
* test(i18n): restore previous language after keep-awake locale suite
* test(i18n): render component in localization tests instead of static che
Converts the keep-awake localization test from static source-code validation to actual component rendering with React Testing Library, providing more reliable verification that the UI displays correctly across all supported languages. Improves translated descriptions for consistency and accuracy.
* test(i18n): add aria labels and descriptions to localization test
- Adds missing localization keys to test data for Spanish, Japanese, Korean, and Simplified Chinese
- Updates test assertions to verify `ariaLabel`, `onDescription`, `autoDescription`, and `offDescription` are properly translated
- Completes localization coverage for the keep-awake corner chip component
---------
Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
* refactor(workspaces): split lifecycle modules
* preserve workspace cleanup consent contract
* restore workspace delete shortcut hint in context menu view
* restore host-qualified visit recency and viewed-candidate predicate
* test(cleanup): pin the viewed-mark upgrade path and host-qualified visit reads
Two invariants a refactor broke in this PR, both silent:
- viewed marks are persisted, so gating `shouldPreserveCleanupInspection` on any
newer field voids the grace period for every entry written by an older build
- visits are stamped under `${hostId}|${worktreeId}` whenever the host is known
(the normal case, including 'local'), so a bare map[worktreeId] read misses
every modern entry and yields 0, disabling the recent-visible-context blocker
Verified discriminating: reintroducing each bug fails exactly its own test.
Linear list issues never carry `project` (only getIssue maps it, via
includeProject: true). If `project` joins EDITED_LINEAR_ISSUE_FIELDS, an edit
made while getIssue is in flight overwrites the hydrated project with the list
issue's undefined, permanently blanking it — the sidebar shows 'Add to project'
and LinearIssueSubIssues then files sub-issues with projectId: null instead of
inheriting the parent's project.
Verified discriminating: re-adding 'project' to the field set fails these.
- Add searchable Combobox for workspace selection with label and type filtering
- Redesign agent picker from collapsible to popover with better visual hierarchy
- Restructure skill install review screens with card-based sections
- Add comprehensive tests for workspace search and agent selection
* fix(cli): spawn a version-manager CLI with its own node runtime
resolveCliCommand falls back to scanning every version-manager install when
PATH misses, so it can hand back ~/.nvm/versions/node/v20.x/bin/codex while
PATH still leads with v22. Nothing paired the binary with the runtime it was
installed against, so its `#!/usr/bin/env node` shebang loaded a v20-built
native module under a v22 ABI and the agent died on first require (#10932).
Reproduced with a real addon rather than asserted: a CLI requiring a
cpu-features build for NODE_MODULE_VERSION 115, spawned with v24 leading
PATH, fails with ERR_DLOPEN_FAILED and exit 1. With the CLI's own bin
directory prepended it runs clean.
withCliRuntimeOnPath prepends the resolved command's directory when that
directory ships a sibling node, and is a no-op otherwise — so a Homebrew or
/usr/local CLI is untouched, and the WSL paths pass a bare `codex`/`claude`
that is not absolute and so never matches.
Host CLI resolution in the Claude login path is now lazy, keeping the WSL
branch from resolving a host binary it never spawns.
* fix(cli): split PATH on the delimiter we join with, pair app-server too
Readiness review findings, all four addressed.
withCliRuntimeOnPath chose its join delimiter from the platform option but
split with the host's. Passing platform:'win32' from a posix host turned
`C:\Windows;C:\Windows\System32` into `C;\Windows;C;\Windows\System32` —
every drive letter torn off at its colon. Latent, since no shipped caller
passes platform, but the sole win32 test was written against the corrupted
value and asserted one split segment, so it green-lit the shredding.
That test's other assertion was vacuous: it seeded only `Path`, so the
`PATH` key it asserted absent could never exist. Deleting the whole
case-dedupe block left the suite green. It now seeds both keys and asserts
the full joined string; removing the block fails it.
Nothing covered the wiring, and the argument choice is the easy thing to get
silently wrong. Note it only diverges on win32 — on posix
getSpawnArgsForWindows returns the CLI itself, so pairing the spawn command
is indistinguishable there. The new test drives the win32 branch with a .cmd
fixture; pairing spawnCmd or dropping the wrapper both fail it now.
codex-trust-grant-host and codex-session-index-heal spawn the same
`codex app-server` subcommand through runCodexAppServerSession and were left
unpaired. Pair centrally there via a new optional cliPath, since
invocation.command may be a cmd.exe wrapper.
Pairing tests live in their own file: adding them inline pushed
codex-fetcher.test.ts past the 800-line ratchet.
* fix(cli): read the Windows path key the child will actually use
Round-2 review finding. The read was narrower than the delete: the key was
picked from exactly two spellings (`Path`, else `PATH`), while the twin
dedupe removed every key whose lowercase form is `path`. A block spelling it
`path` or `pATh` therefore had its value deleted without ever being read,
handing the child a PATH containing only the CLI's own directory — a strictly
worse outcome than not pairing at all.
Win32 resolves env names case-insensitively and object order preserves block
order, so the entry the child reads is the first case-insensitive match. The
repo already encodes that rule in resolvePathEnvKey
(src/main/pty/windows-path-segment-merge.ts); src/shared cannot import from
src/main, so mirror it locally.
Verified by execution across six env shapes: lowercase, mixed-case, Path-only,
PATH-only, both twins, and a PATHEXT control that must not be touched. All
preserve the original PATH; before the fix the first two lost it entirely.
Reverting the selector fails the new test and nothing else.
OMP wraps Pi's TUI, so Shift+Enter bytes land in a Pi reader that decodes
CSI-u. The omp profile had no `windowsShiftEnterEncoding`, so it fell back
to Esc+CR — which submits instead of inserting a newline (#9703).
This was latent while an OMP pane's stored title could read either "Pi" or
"OMP" depending on which interleaved frame committed first. Pinning the
title to the launch owner (#16373) made it deterministically "OMP", so the
Windows Shift+Enter fallback now always resolves `omp` and always picks the
wrong encoding.
`prime-agent` already carries this entry for the identical reason.
Co-authored-by: Seongho.Bak <49228032+psh4607@users.noreply.github.com>
* fix(orcad): close the browser-provider gaps
The providers landed without enforced coverage, so a regression in either path
would have landed silently.
- CI: the external-Chromium integration test was gated on ORCA_BROWSER_EXECUTABLE
and nothing ever set it, so it skipped forever. It now runs in its own job
against the runner's Chrome and FAILS when Chrome is absent rather than
skipping, because an unset variable is exactly how it went uncovered. Timeout
raised to 120s: a warm run is ~7s but the first launch against an unseeded
profile took 30s and hit Vitest's default, and CI is always that cold case.
- Electron provider had no test at all. It is the path anyone with the desktop
app hits.
- Browser unavailability reported one message for four causes, including telling
an operator to set a variable they had already set.
Fixes a live defect found while covering it: the runtime advertises
browser.tabCreate.known-id.v1 unconditionally, so a web client sends a
provisional page id for a page that does not exist yet — and the sidecar's
generic requestedPageId branch ran require() on it first and threw. Every
known-id create against the Electron provider failed. The adoption logic was
already there; only the ordering was wrong.
Also updates the workflow-parallelism guard, which correctly caught the new job
missing from verify's required-check list, and asserts verify actually reads it.
* build(orcad): gate orcad's own graph, and prove it loads under plain Node
Two gaps the artifact's own comment asked for.
The ratchet measured only orca-runtime + runtime-rpc, but orcad imports ipc/pty
directly to install the PTY controller, so its graph is strictly larger. The gate
could read zero while the shipped artifact regressed. orcad's entry is now a
ratchet entry point, and the baseline stays empty with it included.
orcad cannot join plain-node-entry-guard — that is a rollup plugin keyed on
electron-vite input names, and orcad is an esbuild artifact. But the half that
matters here is the guard's smoke-load: scanning the metafile proves no module
NAMES electron, not that the graph resolves under plain Node. A dynamic require,
a missing native or a top-level throw all pass the scan and fail at runtime.
build-orcad now runs the bundle with a bogus flag and requires the argv rejection
that only a fully loaded graph can produce.
Verified: a bundle that builds but throws on load fails the gate.
* fix(terminal): stop OMP tab title flapping between OMP and Pi
OMP wraps Pi, and both share the `pi-compatible` title-identity group. Two
writers publish frames for the same pane under different labels: main's
synthetic spinner injects "<frame> OMP" every 80ms, while the wrapped Pi
harness emits its own "Pi" frames.
`isDecorativeAgentTitleFrameChange` keys on `status:textWithoutSpinner`, so
`working:OMP` and `working:Pi` read as meaningful changes. The alternation
defeated spinner-churn suppression entirely: every 80ms frame committed a
store patch plus a runtime-graph sync, on both the tab-title and
runtime-pane-title paths.
Pin same-group identity frames to the tab's launch owner at both store
choke points, reusing the existing owner-normalization helper already
applied on the sidebar, remote-sync, and mounted-pane paths.
The relabel is scoped to bare identity frames ("⠋ Pi", "Pi ready"); a
semantic session title ("π - <session> - <cwd>") carries text no agent
profile can reproduce and is left untouched, so this does not reintroduce
the generic-label complaint in #16093.
* fix(terminal): scope owner relabel to cross-identity frames
A frame that already names the tab's own agent carries authoritative status
wording, so relabeling it restated bare "Pi" as "Pi ready" and changed a
Pi-owned tab that never flapped. Only relabel when the frame names a
different member of the identity group.
Also fixes the repro suite's types against the project typecheck.
Co-authored-by: Seongho.Bak <49228032+psh4607@users.noreply.github.com>
---------
Co-authored-by: Seongho.Bak <49228032+psh4607@users.noreply.github.com>
* refactor(daemon): split oversized PTY services
* revert(daemon): restore merge-base session listing and canceled-spawn behavior
Two behavior changes rode along with the file-splitting refactor:
- listLiveTerminalHostSessions dropped sessions with isTerminating, not just
dead ones, hiding sessions the merge base still advertised.
- spawnAndPublishSession called session.beginTermination() before publishing a
canceled spawn into the host map.
Both hunks are reverted to the merge base; the refactor is untouched.
* refactor Linear workspace surfaces
* refactor(linear): restore merge-base behavior in split modules
The Linear surface split smuggled in three behavior changes; revert them
so the refactor is a pure move.
- detail-state: drop 'project' from EDITED_LINEAR_ISSUE_FIELDS. List
issues never carry `project` (only getIssue maps it), so preserving it
across hydration permanently blanked the hydrated project whenever an
edit landed while linearGetIssue was in flight.
- detail-state: handleProjectChanged no longer sets hasEditedRef.
- project-selector: remove the mountedRef/requestId guards around the
global patchLinearIssue write and the success/error toasts.
- sub-issues: remove the added isComposing guard on the title Enter key.
The detail-state test asserted the smuggled project-preservation; updated
to assert hydration owns `project`.
* refactor settings maintenance modules
* revert behavior changes smuggled into settings split
- hoist isAdvancedOpen state back into RepositoryHooksSection so it survives
SearchableSetting unmount during settings search
- drop isComposing guards absent from the merge-base AgentsPane handlers
- restore merge-base JSX for the 'when one exists.' fragment (no separator)
* refactor feature wall animated visuals
* fix(feature-wall): restore merge-base render behavior in split visuals
- Hoist workbench reduced-motion state to module constants so cursorTarget
identity is stable and the cursor layout effect stops re-firing per render.
- Render one frame component and branch on the state source so toggling
reducedMotion re-renders the storyboard instead of remounting its DOM.