* 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>
* refactor(mobile): split home modal and rpc client
* fix(mobile): restore render-phase remount key in NewWorktreeModal
The split moved the form-reset epoch from render-phase refs into
useState + useLayoutEffect, which changed when the remount key is
computed. On the render where visible flips false->true the key was
still the old epoch, so the previous session's NewWorktreeModalContent
rendered with visible === true carrying stale form state. Child layout
effects run before the parent's, so visible-gated hooks
(useNewWorkspaceRepositories, useNewWorktreeDrawerNavigation,
useNewWorkspaceRuntimeContext) fired for that stale instance before the
parent bumped the epoch and remounted.
Restore the ref-based computation so the key is correct on the first
render where visible flips true, keeping the composite open/client
epoch semantics and the file split intact.
* 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.
A diagnostic's span often reaches past the block a split moved — most commonly
to a hook dependency array, which legitimately grows when closure variables
become props. Requiring every line of the span to match contiguously reported
the moved body as new.
The block must still start at the same line in the base and appear in order,
and >=90% of it must be present. Genuinely new code shares neither the anchor
nor the ordering.
* 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.
A file-splitting refactor makes every line of the new module an added line, so
pre-existing lint debt in code that merely moved starts failing the gate. The
only way to satisfy it is to edit the moved code, which is what a
behavior-preserving refactor must not do. Exempt a diagnostic when its
highlighted lines already existed verbatim and contiguous in the base revision.
- 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>
* build(windows): drop the packaged node-pty prebuild that can silently replace the patch
node-pty's loader tries build/Release, then build/Debug, then
prebuilds/<platform>-<arch>, and swallows every failure in between. Windows
packaging ships both the source build and the prebuild, and only the source
build carries Orca's job-object exports (listJobProcessIds, terminateJob,
assignCurrentProcessToJob).
So an ABI mismatch, a truncated file, or an AV quarantine of
build/Release/conpty.node degrades the shipped app to the UNPATCHED prebuild:
PTY teardown silently falls back to guessing by PID ancestry, with no error
anywhere. That is the failure mode that made #16059 hard to see -- an install
that looks fine and quietly cannot own a PTY tree.
Removing the fallback turns a silent downgrade into a loud load failure.
Scoped narrowly: only win32, and only when the source build is actually
present, so a build that legitimately has no build/Release keeps something
loadable. macOS and Linux prebuilds are untouched -- they have no patched
export to lose.
Refs #16059.
* fix: delete only the stale conpty fallback, not the whole prebuilds tree
Review caught a P0 in the first version of this change, and it was the same
defect the PR exists to prevent, pointed at a different target.
Orca's own patch removes the `conpty_console_list` and winpty `pty` gyp
targets, so a Windows source build emits conpty.node and nothing else.
conpty_console_list.node, pty.node, winpty.dll and winpty-agent.exe therefore
exist ONLY in prebuilds/. Deleting the tree removed them:
- the forked console-list agent throws at require, and its caller resolves null
with silent: true, so console-membership probing dies with no log anywhere --
a new silent degradation, in a PR whose thesis is "make it loud";
- node-pty still selects winpty below Windows build 18309, so PTY spawn would
fail outright on Server 2019 / Win10 LTSC 2019.
Now removes only prebuilds/win32-<arch>/conpty{.node,.pdb}, and only when
electronArch matches the host arch -- a cross-arch package copies the host's
build/Release, so its presence does not mean it matches the target, and
deleting the target-arch prebuild would remove the only loadable binary.
The old fixture wrote just conpty.node, so it could not see any of this. It now
seeds a realistic prebuilds directory, and four tests assert each sibling
survives; all four fail against the broad delete.
Credit: review counsel.
* 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.
* fix(startup): stop the PATH seed pinning nvm to its newest install
patchPackagedProcessPath prepends the newest nvm version dir to
process.env.PATH, then hydrateShellPath probes the login shell with that
same env. nvm's startup `use` honors whatever node is already on PATH
instead of the user's `default` alias, so the probe returns a PATH pinned
to the newest install and every terminal pane inherits it.
A user whose newest nvm node is a bare install then loses every global CLI
(codex, claude, gemini, vercel...) inside Orca while they still resolve in
Ghostty/Terminal, which start from the bare GUI PATH and fall through to
`default`.
Probe with the PATH the process launched with. Windows already gets this
through WindowsShellPathOwnership.
* test(startup): pin the platform in the probe-env test
shellProbeEnv short-circuits on win32, so the POSIX-only assertion failed
for anyone running the suite on Windows. Matches the convention in
hydrate-shell-path.windows.test.ts.
Also narrows the win32 exemption comment: WindowsShellPathOwnership
snapshots its baseline after the seeds land, so it does not unwind them.
The exemption holds because Windows keys PATH as `Path` and no Windows
seed pins a node version.
* fix(startup): give win32 the same probe insulation, keyed by Path
The previous commit exempted win32 on the stated grounds that no Windows
seed pins a node version. That is wrong: getVersionManagerDirectories
calls getNvmVersionDirectories on every platform, so a Git Bash user whose
nvm uses the POSIX ~/.nvm/versions/node layout gets the newest version dir
seeded on Windows too, and the -ilc Git Bash probe inherits it.
Record the PATH key alongside the value and overwrite that entry in place,
so Windows never carries both `Path` and `PATH` — which was the only real
reason to skip win32.
* fix(startup): snapshot the launch PATH at module load, log probe failures
Three loose ends from the review, folded in rather than deferred.
The probe's clean PATH was handed over by an explicit recordLaunchPath call
from the seeding site, so the invariant lived across three files and a
refactor that moved the seed call would silently re-pin nvm. Snapshot PATH
during module init instead: that runs while the import graph is evaluated,
strictly before any statement in main's body, so it cannot observe the
seeds and there is no call ordering left to break. All seven importers are
static, so no lazy import can defeat it. A test asserts the probe ignores
later process.env mutation, and fails if the live read is reintroduced.
A failed startup probe leaves the seeded newest-nvm dir in front and said
nothing, so the population whose rc files blow the 5s budget hit the
original symptom with no diagnosable trace. Log the failureReason.
The probe is an interactive login shell, so rc files that exec into a
multiplexer or start a heavy prompt can outrun that budget with no way to
opt out. Set ORCA_SHELL_PATH_PROBE=1 so they can take a fast path.
* fix(startup): drop the other-cased PATH key from the Windows probe env
Caught by running the suite on a real Windows machine, not a mocked
platform. The spread of process.env is a plain, case-sensitive object,
while Windows resolves env names case-insensitively. Writing the captured
`Path` back onto it left the seeded value still live under `PATH`, so the
probe shell could read either one — the exact duplicate-key hazard the
win32 branch was supposed to prevent.
Drop any other-cased variant of the key before writing.
* fix(startup): preserve launch PATH across app restarts
* test(pty): pin the renderer-liveness guard STA-2373 relies on (STA-5373)
STA-5373 reported that #15927 dropped the `webContents.isDestroyed()` half of
the guard #10065 added for STA-2373, and that the app therefore dies when a
daemon death fans out to every pane. The guard is present on main: #15172
restored it at both senders when it split the PTY monolith, and a per-file
count across v1.4.188..HEAD shows only relocation (pty.ts:4 ->
write-input.ts:2 + bind-listeners.ts:2). The reported 4 -> 0 was scoped to
`src/main/ipc/pty.ts`, now a 39-line barrel. No production fix is needed.
What was real is that nothing pinned the guard — it survived #15927 only
because #15172 happened to re-expand it — and the shared PTY test fake made
that invisible: its `webContents` had no `isDestroyed` at all, so both guards
passed vacuously in every suite. That is also why they are written defensively
as `typeof ... === 'function' && ...`.
- Give the fake an `isDestroyed` mock, re-stubbed to `false` each beforeEach
rather than left `undefined`, so "alive" is stated rather than accidental.
- Cover both senders red/green: the daemon-death fan-out
(bind-listeners.ts:37) and the per-write reporter (write-input.ts:57).
Verified red against a window-only guard and green with the real one.
The per-write case needs a chunked write. A single-chunk write is already
fenced by `isPtyWriteEventFromMainWindow`, which rejects the event once the
WebContents is gone; only the multi-chunk path yields a macrotask mid-write,
letting the renderer die after the sender check passes. That is the sole route
to this sender with a dead WebContents, and what makes its own guard
load-bearing.
Also deletes `src/main/ipc/pty-renderer-surface.ts`: zero importers repo-wide,
and its `isRendererGone` is exactly the weakened predicate. Its comment claims
the headless path "now passes null", but register-headless-runtime.ts:26 still
fakes `{ isDestroyed: () => true }` — #15172 rolled that back too. Adopting it
would reintroduce STA-5373 for real.
Verified: 17 tests across the two pty write suites; full src/main/ipc run 3156
pass (4 pre-existing @parcel/watcher failures in filesystem-watcher-real and
worktree-base-directory-poller, unrelated and failing identically on a
pristine tree); pnpm typecheck clean; oxlint clean.
* docs(pty): correct headless-path comments the #15172 rebase left stale
#15927 made `registerPtyHandlers` accept `BrowserWindow | null` so the headless
path could pass null instead of faking a window. #15172 reverted that signature
while splitting the PTY monolith, but the comments describing it survived, so
three of them now document an API that does not exist.
- orcad-entry.ts: the module docstring claimed orcad installs its controller via
`registerPtyHandlers(null, …)`. It uses `registerHeadlessPtyRuntime`, and null
is not accepted. It also claimed desktop surfaces are "declared rather than
faked" — true except for the renderer window, which is still faked.
- register-headless-runtime.ts: record why the fake is safe rather than leaving
`isDestroyed: () => true` looking arbitrary. It is load-bearing: every
renderer-liveness guard reads it and skips, so no send is attempted.
Also gives the fake a `webContents.isDestroyed`. The real guards check both, and
a missing method reads as "alive" — the same gap this PR fixes in the test fake.
No behavior change: the window-level check already short-circuits.
Verified: 45 tests across the liveness-guard, startup-barrier, management and
kill/exit suites; pnpm typecheck clean; oxlint clean.
* test(terminal): pin park/reveal re-subscribe on a shared multiplexer
Investigating STA-5098. Parking a mirrored remote tab closes its stream
while a sibling tab keeps the multiplexer alive, so the reveal
re-subscribes on an instance that already retired a stream.
This came back green, which exonerates the multiplexer as the cause of
STA-5098 — the wedge is above it. Kept as a contract guard; the header
says explicitly that it is not coverage for that ticket.
* fix(terminal): stop stale multiplex stream handles from swallowing input
A stream handle whose record was dropped (park close, or a reconnect that
clears the stream table) kept reporting success: sendFrame gates only on
socket readiness, never on stream membership. The host drops those frames
for an unknown stream id, so a revealed cold-parked remote pane looked
connected while the PTY never saw a byte and never painted (STA-5098).
Reporting success also defeated the transport's own recovery — it re-sends
input over terminal.send when the stream refuses it, which never ran.
Guard the three public senders on stream membership, the check close() and
setOutputPaused already use, and drain input queued behind a viewport claim
on every stream install rather than only a still-pending claim.
Withdraws the parked-reveal re-subscribe test: its fake host answered
Subscribe with an immediate snapshot, so it could not fail.
* chore(terminal): tighten the stale-stream comments