A profile carried `activity.idleMinDays = 20` that the user never set, hiding 253
of 799 workspaces on open. Chromium mutates a *focused* number input on every wheel
tick, and before #14629 the facet panel could not scroll, so the natural response --
cursor into the panel, spin the wheel -- walked the threshold up and persisted it.
Two fixes:
- `FacetNumberField` renders `type="text" inputMode="numeric"`. A wheel cannot
mutate a text input, and the parser already takes strings. `preventDefault` on a
focused number input would also work, but it blocks the wheel's default action --
which includes scrolling the nearest scrollable ancestor -- and would re-break the
panel scrolling #14629 just fixed, in exactly the reported gesture. All five
numeric facets share this one field.
- `patchFilters` closed over the render's `browse` snapshot, so two patches in one
tick dropped one. It now writes through a functional update against the latest
store state. `toggleSortField` and `clearFilters` had the same defect.
Both tests were confirmed to fail against the unfixed source before being kept.
* fix(workspace-cleanup): stop pre-selecting workspaces for deletion
'Ready' is Orca's verdict about the user's own work. The dialog already
refuses to display the tier as a workspace fact, then pre-checked rows based
on it anyway — acting on the verdict more loudly than showing it would. Open
with an empty selection and let the user decide.
Removing auto-select unmasks two selection defects it was hiding, both fixed
here because they become reachable the moment select-all is the primary path:
- The header checkbox compared a canQueue-scoped selected count against a
canSelect-scoped selectable count, so hand-picking review-tier rows flipped
it to fully-checked and the next click cleared the entire selection.
- A filter change silently dropped selected rows; auto-select used to refill
them instantly, so the loss was invisible. It is now reported.
Also drops the scan toast's 'N cleanup suggestions' clause, which published
the same verdict outside the dialog, and labels select-all with the count it
actually takes (the deletable subset, not every matched row).
* chore(i18n): sync the catalog for the cleanup selection strings
* fix(workspace-cleanup): preserve explicit selections
* fix(workspace-cleanup): name the local context instead of totalling it
The delete confirmation showed 'Context: 2', a sum of five unrelated things
(terminal tabs, clean editor tabs, browser tabs, diff notes, finished agents)
rendered as plain text with no icon or tooltip. A reader cannot tell what the
number counts, which is the one thing that screen exists to tell them.
Reuse the breakdown the expanded row already renders ('Terminal tabs: 1,
Browser tabs: 1') so the confirmation names what deleting would discard. No new
strings: the per-kind labels already exist and were already translated.
* fix(workspace-cleanup): keep context labels legible
Document that Git worktree removal may also delete the checked-out local branch, while clarifying that --force does not force branch deletion and that Orca retains branches whose changes cannot be proven merged.
* reland(opencode): session continuity without the command-finished deferral (STA-4557)
Relands #14866 (reverted in #14943) minus its `orca-runtime.ts` change, which
is what caused the revert.
## Why the original runtime change was wrong
`retirePtyAgentLaunchAuthorityAfterCommandFinished` deferred launch-authority
retirement behind an async foreground read, on the premise that OpenCode emits
`command-finished` while still in the foreground. Raw PTY capture disproves it:
OpenCode emits no OSC 133 of its own, and Orca's shell wrappers emit exactly one
`133;D` per pane — at OpenCode's exit — under both zsh and bash. The event being
deferred past only ever fires at exit, which is exactly when authority should be
retired. Both call sites stay on the synchronous `retirePtyAgentLaunchAuthority`.
## Why the deferral was unsafe
`confirmPtyAgentExit` uses the same async-foreground pattern four lines away, but
its early return means "don't record an exit" — conservative. The deferral copied
that shape into a site where the early return means "don't revoke a secret". Same
code, inverted consequence: every guard failed open, so a stale or racing read
silently kept a finished session's authority alive, and the pane's persisted
`launchTokenHash` was never scrubbed — so it rehydrated as `restored` authority
after an app restart.
## Why the deferral's guards could not have worked
`ORCA_AGENT_LAUNCH_TOKEN` lives in the PTY environment, so every process started
in that shell inherits it — both sessions in a reused pane post the same token. A
pane-lifetime bearer secret cannot be a session identity baseline, by
construction, and `incarnationId` tracks the PTY, not the agent. The only field
that separates sessions is the provider `sessionID`.
## What lands
- Status/session-boundary work from #14866: opencode emits `SessionStart` for
root sessions (mimo-code does not), launch-token fencing, and `SessionStart`
as an opencode turn boundary.
- The two `server.ts` fixes from #14941: re-fence a still-authorized pane on a
tokened `SessionStart`, and restore mimo-code's explicit-prompt restart
boundary (mimo emits no `SessionStart`, so opencode-only stranded its panes).
#14941's re-poll hunk is dropped along with the code it patched.
- Five regression tests in `opencode-finished-session-authority.test.ts`. They
pass here and all five go red if the deferral is re-added.
* chore: drop incidental reformatting of files unrelated to this PR
Antigravity's models are named "Gemini <n.n> <Name>" — the real `agy models`
output is already parsed in commit-message-agent-spec.test.ts — so an agy pane's
own title carries a whole `gemini` token. getAgentLabel checks Gemini CLI before
Antigravity, first match wins, so the model name won and the pane read as Gemini
CLI. Measured on '⠋ agy · Gemini 3.7 Flash · high': geminiGlyphs false,
geminiToken true, agyToken true, label 'Gemini CLI'. Even
'Antigravity · Gemini 3.7 Flash' resolved to Gemini CLI.
This surfaced as the tab bar and the sidebar disagreeing about the same pane,
because the two reach different copies of the chain and apply different
precedence to its result.
Defer only the token path: if a title carries an agy/antigravity token, the
bare-`gemini` branch declines. The four Gemini OSC glyphs stay decisive, and agy
emits none of them. Same shape as the existing isPiAgentTitle veto directly
above, which exists because substring matching made paths like 'gemini-project'
masquerade as Gemini CLI.
Narrowing the token rather than reordering the chain, deliberately: a real
recorded pane title from local terminal history is
'STA-4011 Linux Antigravity Commit Messages - grok' — a Grok pane whose task
text contains the token Antigravity. It resolves correctly only because grok is
checked before antigravity, so hoisting the Antigravity branch would break it.
That title ships as a regression case.
Both copies of the chain are fixed; the sidebar reaches one and the tab the
other, so fixing one alone would only move the disagreement.
* refactor: split pty-connection.ts under 400 lines
* rm design doc
* refactor(pty-connection): extract reattach payload handlers as factories
- Replace bindApplyReattachPayload with createReattachPayloadHandlers factory that returns handlers instead of mutating session directly, enabling better composability and testing
- Extract waitForUserInitiatedSshConnect as standalone function for reuse across deferred session attach flows
- Create ReattachPayloadSession type to document and isolate required session capabilities
- Add test coverage for overlapping reattach payload attempts
- Clean up comments to remove redundant prefixes (session.pane → pane, session.transport → transport)
* fix(pty-connection): correct sequencing and state bugs in spawn and reat
- Fix terminal tail slice to take prefix instead of suffix, preserving escape
sequence markers needed by next scan
- Clear pending pane serializer when direct SSH retry PTY is unclaimed
- Initialize interrupt status baseline to undefined so first input advances
sequence counter
- Bump reattach generation only after confirming current attempt owns the stream,
preventing superseded results from canceling in-flight prepaint
* fix(pty-connection): correct sequencing and state bugs in spawn and reat
- Fix terminal tail slice to take prefix instead of suffix, preserving escape
sequence markers needed by next scan
- Clear pending pane serializer when direct SSH retry PTY is unclaimed
- Initialize interrupt status baseline to undefined so first input advances
sequence counter
- Bump reattach generation only after confirming current attempt owns the stream,
preventing superseded results from canceling in-flight prepaint
* fix(test): increase poll iterations to prevent Node 26 test leakage
Increase event loop turns from 40 to 200 in the timer settlement loop.
Node 26's libuv poll phase can briefly starve when concurrent workers
transform tests, causing cleanup to leak into the next test. The higher
iteration count ensures async operations complete before returning.
* fix(foreground-output-budgets): use >= for budget window boundary check
At the exact window boundary, the budget should roll over. Change the
comparison from > to >= so the window resets when now equals
windowStart + FOREGROUND_BUDGET_WINDOW_MS, not just after. Add tests
to verify budget rejection and rollover behavior.
* refactor(pty-connection): add status observations and routing improvemen
- Track agent status observations with origin and transition metadata
- Separate interactive redraw input timing from general terminal input
- Restore pane authority on bind and reattach
- Refine routing trust and confirmation state handling
- Invoke queued startup callbacks when PTY is bound
- Resolve Windows shell overrides with user settings
* refactor: extract resolveLaunchAgentCandidate helper
Consolidate duplicated launch-agent resolution logic into a shared helper to prevent future divergence between paneExpectsLaunchAgent and resolveExpectedLaunchTuiAgent.
* refactor(pty-connection): use model snapshot for direct SSH reconnects
Direct SSH reconnects now restore from the full SSH model snapshot (complete scrollback) when dimensions are compatible, instead of the bounded relay tail. Falls back gracefully when incompatible or alternate-screen was exited.
* refactor(pty): retry unverifiable SSH reattaches via preserved bindings
Preserve deferred SSH session IDs longer when they serve as the only retry binding,
allowing the system to attempt recovery through direct SSH retries or PTY remounts
when reattach fails in an unverifiable way. Simplify reconnect model restoration
by removing the conditional model snapshot probe and using relay replay directly.
* test: poll terminal readiness in expectSingleOwningPty
Retry the terminal list assertion with polling to account for timing
delays in PTY state reporting from the runtime.
* fix(codex): stop rebuilding shared Codex state from a read that failed (STA-4823)
Six shared files were rebuilt, erased or reported healthy after a read that had
only failed. Batch A of the STA-4606 split: every one of these is reachable and
testable on the host lane, so none of them wait on the WSL work.
- `config-toml-trust.ts` upsertHookTrustEntries: `existsSync` reported a locked
config.toml as absent, so the base content became '' and the upsert rewrote
the file from the trust entries alone — a trust-only stub, with the user's
model, provider, MCP servers, approvals and comments gone. It refuses now;
every hook-service caller already turns that into "trust entries could not be
written. Run /hooks in Codex to approve."
- `codex-trust-grant-ledger.ts`: an unreadable ledger degraded to empty and the
next write persisted a file holding only the home being written, dropping
every other home's grants. The write paths refuse; the read path still
degrades, and a corrupt ledger is still rebuilt.
- `codex-pane-account-registry.ts`: an unreadable registry erased every pane's
attribution AND cached that erasure, so it survived the file recovering. The
failure is no longer cached, and both write sites refuse rather than persist a
registry derived from an empty stand-in.
- `hooks-json-read.ts`: the read arm already separated "no hooks" from "could
not read", but the `existsSync` arm in front of it returned a valid empty
config for a file that could not be opened. One read now classifies both.
- `config-settings-baseline.ts`: absent, unparseable and unreadable all collapsed
into `null`, so the snapshot rebuilt a baseline it could not read — recording
an in-Codex edit as Orca's own write, after which promotion skips it forever.
- `config-sync-stall.ts`: an unreadable runtime config read as absent and the
status reported `synced` while the mirror was refusing. It reports
`managed-home-unavailable`, the existing reason for exactly this, rather than
borrowing a source-side one and blaming the wrong path.
Absent and malformed still rebuild throughout — resetting corrupt state is the
intent, and conflating it with unreadable would wedge a user on a broken file.
* fix(codex): close shared state read-denial gaps
* fix(codex): recover oversized settings baselines
* fix(codex): name the stalled managed config
* fix(codex): preserve hooks after failed source reads
* test(codex): correct what the denyExistence rig actually models
MEASURED on both platforms: a file-permission denial leaves existsSync TRUE
and fails only the content read — chmod 000 gives EACCES on macOS, icacls
/deny (R) gives EPERM errno -4048 on Windows, with stat/lstat succeeding in
both. The rig's docblock claimed this mode modelled that denial. It does not.
What it models is the UNC / \\wsl$ transport, where an unreachable distro
reports errno UNKNOWN at every level and existsSync folds it to false.
The distinction decides what the D29 guard is worth: under a permission denial
the pre-fix code already failed safe, because existsSync was true so it took
the read branch and threw. Only a transport that lies about existence reaches
the rebuild-from-empty path. No behaviour change; the comment was wrong, not
the code.
* test(codex): exercise live baseline read denial
* fix(codex): retry pane attribution writes
* fix(codex): retry reconciliation registry writes
* fix(codex): report unreadable sync baselines
* fix(agent-hooks): revive a retired pane on each provider's own new-turn event
The un-retirement gate matched two raw event-name literals, UserPromptSubmit and
SessionStart. Only 5 of 18 hook sources name their turn boundary that way, so for
the rest a reused pane stayed rowless forever: the user starts a new turn and no
row ever appears in the sidebar or dashboard.
Measured, not estimated — 10 providers fail the new test on main: gemini
(BeforeAgent), antigravity (PreInvocation), amp (agent.start), cursor
(beforeSubmitPrompt), pi/omp/prime-agent (before_agent_start), grok
(user_prompt_submit), copilot (sessionStart, seen raw), hermes (pre_llm_call).
The gate was also wrong in the other direction: opencode has no turn boundary at
all, yet a literal SessionStart revived its pane. Both directions are covered.
The correct per-provider classifier, isNewTurnEvent, was already imported into
this file and already used 160 lines below — #14706 added that call specifically
so consumers would stop re-deriving boundaries from literals, and left this one.
Keeps the literal check when the remote envelope omits source: that field is
optional, an older relay does not send it, and requiring it would have left every
source-less remote pane retired forever.
* fix(agent-hooks): tell an absent source apart from an unrecognized one
Review of the first commit found that `isAgentHookSource(envelope.source) ?
envelope.source : undefined` collapses two different wire conditions into one:
an older relay that omits `source`, and a NEWER host relaying a provider this
build does not know. They need opposite answers.
Case two is the normal upgrade order — hosts and clients update independently —
and its boundary event will not be UserPromptSubmit/SessionStart, because 13 of
the 18 providers we already ship are named something else. So the legacy-literal
fallback stranded that provider's panes permanently: the exact defect this branch
fixes, silently reintroduced for traffic our own wire doctrine calls normal.
Pass the raw wire value so the gate can distinguish them, and fail OPEN on an
unrecognized provider. The costs are asymmetric: a stranded pane is invisible,
permanent, and has no user-facing recovery, while a spurious revive produces a
row that decays after AGENT_STATUS_STALE_AFTER_MS.
Also from review:
- Cover the source-less branches, which had no coverage at all — including a test
pinning what the legacy shim CANNOT do, so nobody later widens the literal list
to "fix" it.
- Use the source as agentType rather than always 'claude'; 15 of 16 rows were
describing a state that cannot occur.
- Reword the opencode comment: no plugin in that family emits these literals, so
this closes a hole rather than removing behavior.
Checked and not changed: the local HTTP path 404s an unresolvable source, so the
fallback is unreachable there and masks nothing.
* test(agent-hooks): stop pinning opencode's fence behavior on a synthetic event
The negative case asserted that an opencode pane stays retired after a literal
SessionStart. That rests on a false premise: origin/main's opencode plugin never
emits SessionStart at all — zero occurrences in opencode/hook-service.ts, and the
shared family source is what mimo-code uses too. So the assertion pinned an event
no plugin sends, and it would have become actively wrong the moment a pending
change gives opencode a real SessionStart, silently re-breaking the rowless
reused-pane case that change exists to fix.
Assert against mimo-code and command-code instead. Both genuinely have no
boundary event in any planned state, so the case tests what it claims to.
* fix(agent-hooks): reject malformed relay sources at retired fence
Only a non-empty unknown string can identify a future provider. Keep null, blank, numeric, and object source values behind the retired-pane fence instead of treating malformed wire data as a new turn.
* fix(status-bar): remove pet menu reserved space
* test(status-bar): add pet segment layout validation tests
- Unit test guards against pr-[6.5rem] padding reintroduction
- E2E test measures trailing overhang instead of total width delta
for more accurate layout validation
- Extract enableExperimentalPet helper for test clarity
---------
Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
* fix(runtime): classify tui-idle from the visible screen only
The adopted-PTY tui-idle probe added in #15569 read the provider snapshot as
`scrollbackAnsi + data`, and the Codex readiness classifier matches the startup
banner. For a daemon-hosted adopted worker — where the retained tail stays empty
forever — every wait re-probed and could resolve `satisfied: true` off banner
history while Codex was actively working, turning a loud timeout into a silent
false ready.
- probe now requests and parses the visible grid, never scrollback
- retirement of a timed-out provider acquisition is checked before the
re-acquire branch, so a wider row request can no longer resurrect a hung
provider
- probe builds its result before clearing the poll interval, so a stale handle
cannot leave the waiter with neither poll nor probe
Fixture follow-ups from the same review:
- resume legs pin the captured `launchConfig.agentCommand` to the fake instead
of bare `codex`, which resolved the machine's real Codex off PATH
- the command override is quoted for the Windows shell the runtime will actually
use, and specs pin that shell alongside the override
- fake agents acknowledge a bare submit after a short grace, so an unbracketed
delivery path fails with a diagnosable ACK instead of a suite timeout
Refs STA-4907, STA-4885
* test: assert tui-idle probes serialize visible grid only
- Verify idle timeout probes exclude scrollback from serialization
- Add test case for Git Bash shell path quoting with apostrophes
- Simplify verbose test helper comments
* test: improve fake agent paste protocol validation
Refactor paste end detection to properly track both begin and end markers,
validate bracketed paste protocol (RFC 2544) through chronological event
sequencing, and emit correct error messages for protocol violations. This
ensures reliable detection of when pastes complete even when delivered
across multiple chunks, and correctly distinguishes between bracketed and
unbracketed paste modes.
* fix(runtime): reject provider snapshots when live output advances
Provider snapshots become stale when live output is received after the
snapshot is requested. Reject snapshots where the current output sequence
exceeds the snapshot sequence, preventing callers from consuming outdated
terminal state. Add tests verifying stale frame rejection.
* perf(preflight): read the WSL mount table once per shell, not once per CLI
The prelude is embedded in the lookup script, and the caller wraps that in
`for cmd in <every agent>`, so the unconditional assignment forked awk once per
probed CLI -- 36 of them inside the distro against a 10s detection budget. The
comment claimed it was read once outside the loop; it was not.
`${x+set}` rather than `[ -n ... ]`: a host with no Windows mounts yields the
empty string, which must still count as read.
Pinned by counting real awk forks through /bin/sh with a stub on PATH, because
nothing covered this expression at all -- a wrong-field mutation shipped green.
Verified to bind: the unconditional form counts 4 for 4 commands.
* fix(wsl): make launch resolve the same binary detection reported
Agent detection skips Windows mounts during the PATH walk; the Codex WSL
command builder and the WSL branch of isCommandOnPath did not. So Orca could
report the guest codex as installed and then launch the Windows one sitting
ahead of it on PATH, or disagree with itself between preflight and detection
about the same distro.
Both now pass the same option.
Verified on a real Windows host against a real WSL2 distro, with a Windows
binary planted ahead of a guest one on PATH:
plain `command -v orcaprobe` -> /mnt/c/Users/neil/orca-agree/orcaprobe
this lookup -> /home/neil/.orca-agree/bin/orcaprobe
That host reports /mnt/c as 9p, which the mount expression matches, so the
fstype list is confirmed against hardware rather than fixtures.
* test(preflight): prove the memoised mount list applies past the first command
Counting awk forks with a stub that reports no mounts cannot see what the
hoist trades correctness for. A mutant that empties `_orca_win_mounts` inside
the walk keeps the fork count at 1 and keeps every existing test green, while
every agent after the first stops skipping /mnt.
This runs two commands behind a stubbed Windows mount and asserts both resolve
to the guest binary. Verified against that exact mutant.
Credit: review counsel.
* fix(wsl): budget the whole command line, not just the script
The argv/stdin threshold measured `script.length`, but the cap applies to the
finished command line -- which also carries `PATH=<login PATH>` and `HOME=`.
A login PATH is itself a few KB.
That produced a perverse band: with a long enough PATH, a 7,999-char hook was
placed on argv and CreateProcess refused it, while the SAME hook at 8,001 chars
flipped to stdin and ran. Size decided how a hook behaved, in the wrong
direction, and the failure looked like "your setup hook failed" with nothing
pointing at length.
Now the argv form is built, measured, and only used if the whole line fits;
otherwise the script goes to stdin as before. The count over-estimates slightly
(it charges quoting for every argument) because over-counting is the safe
direction for a cap.
The regression test uses a 7,000-char script -- deliberately under any
script-only threshold -- with a 27KB PATH, and asserts it lands on stdin. My
first attempt used 7,999 + `echo `, which is 8,004 and flipped under the old
rule too, so it passed either way and proved nothing.
Credit: Grok.
* fix(wsl): charge quoting and measure the line that is actually spawned
Two under-counts the review found in the estimator I added.
The doc comment claimed it over-counts. It did not: libuv escapes every `"`
and doubles a backslash run before a quote, so a quote-dense script costs more
than its length. And `wsl.exe` plus `-d <distro> --exec` are prepended AFTER
the measurement, so ~45 characters of the budget were never counted.
Together those put a quote-heavy ~26KB script on argv and over the real 32767
ceiling -- where the old script-only rule would have sent it to stdin and it
would have run. A narrower band than the one this PR removes, but the same
shape of bug, so worth closing before merge rather than after.
Now charges one character per `"` or backslash and measures the full spawn
line. New test: 26,000 quote characters must land on stdin; verified to fail
with the quoting charge removed.
* fix(preflight): do not count a Windows binary reached through interop as a WSL install
WSL appends the Windows PATH to the guest PATH by default, so on a distro with
no guest `claude`, `command -v claude` resolves to
`/mnt/c/Users/me/.../claude.exe`. That path is POSIX-absolute, so the existing
absolute-path check accepted it and preflight reported the agent as installed
in the distro.
That is worse than reporting it absent. Absent tells the user to install it; a
false positive launches a Windows executable inside a Linux session, where it
sees Windows paths, no guest $HOME and none of the distro's config -- and the
failure surfaces later, somewhere less obvious.
Rejects `/mnt/<drive>/` and any `.exe`, case-insensitively. A genuine guest
install is unaffected.
* fix(preflight): skip Windows mounts during the PATH walk, not after it
The review caught this and it is the more important half of the fix.
Rejecting the interop path in TypeScript happens after the guest walk has
already stopped on it: the lookup breaks at the first executable, and the
version-manager fallback dirs are APPENDED, so they sit behind the Windows
entries WSL appends. A user with claude in nvm AND on the Windows PATH
therefore went from a false positive to "not installed" -- the exact #9725
population the fallback dirs exist to serve. Worse than the bug being fixed.
The lookup now takes `skipWindowsMountDirs` and skips those PATH components
mid-walk, so the guest binary behind the shadow is still found. Matched by
mount metadata from /proc/mounts (drvfs/9p/virtiofs), not by a `/mnt` name:
the automount root is configurable, and `/mnt` is an ordinary directory on a
Linux box. That also closes the custom-root hole the reviewers found in the
name-based predicate.
The TypeScript check stays as a secondary net for a mount the guest does not
report, with a comment saying why it must never be the thing that decides.
Proven with a real /bin/sh: a Windows `claude` ahead of an nvm `claude` on
PATH now resolves to the nvm one.
Credit: review counsel, and community PR #12794 (spfcraze), which proposed
this shape first.
* fix(preflight): let the mount table be the only word on what is a Windows path
The name-based check could veto a path the walk had deliberately kept. /mnt/d
is a perfectly ordinary Linux mount, so a guest binary there was resolved
correctly by the walk and then discarded by its name -- the #9725 false
negative, reintroduced by the belt-and-braces net I added "just in case". And
if awk were missing, the name rule became the only rule, which is precisely
the failure it was supposed to backstop.
The walk skips components the guest itself reports as drvfs/9p/virtiofs. That
is authoritative. Without a mount table we now degrade to main's behaviour (the
old false positive) rather than inventing a new false negative.
Net: one predicate, three fixtures and an import deleted.
Three bugs in one screen, reported in #15256 with a diff of the user's
orca-data.json showing defaultTuiAgent going from "claude" to null.
1. The Auto pill's handler writes null, and it was rendered as the ACTIVE
choice whenever the stored agent was merely not detected right now. So the
pill that already looked selected was destructive: one click erased the
setting, and a later successful detection did not bring it back. Auto is now
active only when null is actually stored. Detection is a transient fact; the
stored value is not, and this control reports the stored value.
2. With zero agents detected there were no agent pills at all, so the stored
choice was both invisible and unrecoverable -- nothing to click to put it
back. The stored agent is now always offered, labelled as saved but not
currently detected.
3. Refresh lived inside the Installed section, which only renders when at least
one agent was found, so the only retry control vanished in exactly the state
that needs it. An empty result now renders its own Refresh.
This matters more now than when it was filed: #16028 makes WSL detection
legitimately return an empty set on machines where the only agent was a Windows
binary reached through interop, so the empty path is about to get more traffic.
Each of the three tests was verified to fail with its own fix reverted.
`safeStorage.getSelectedStorageBackend` is `@platform linux`, so it is genuinely
undefined on macOS and Windows — confirmed against the installed Electron 43,
where it reads `undefined` on darwin and `function` on Linux. The shipped code
called it behind a `process.platform === 'linux'` check, so it never threw, but
the guard was the only thing standing between that call and a startup TypeError.
The platform check now lives with the probe, alongside a typeof check and a
try/catch, and an unreadable or unknown backend reports no gap — claiming one we
cannot prove would be its own kind of lie.
The gap this closes is in the tests, not just the code: every suite here mocks
safeStorage with the method present, so the suite could stay green while the
shipped app threw. The new case deletes the member from the live mock rather than
re-mocking, because the module already holds that object and a later vi.doMock is
inert — the first version of this test passed against the unguarded code, which
is the failure mode it exists to catch. Verified against the expression currently
on main: three cases fail.
The gap warning fired every startup with no way to stop it. It usually needs a
keyring installed and unlocked to fix, so repeating it every launch is nagging
the user cannot act on and will learn to ignore.
It now reports when the answer changes: once when the gap starts being true,
again if it becomes true for a different reason, and once when it is fixed —
because silence after a "your secrets are not protected" warning would leave the
user assuming that is still the case.
State lives beside the profile data file, which is why the call moved out of the
port bootstrap: that state has nowhere to live until the profile exists. A
corrupt state file re-reports rather than trusting it, and a failed write logs
instead of failing startup, since re-reporting next launch is the safe direction.
ORCA_ALWAYS_REPORT_SECRET_PROTECTION=1 forces a re-report for support without
disturbing the stored state.
Opening a remote HTML preview to the side created an empty split and made it
the active group. The next host session-tab snapshot still had the terminal
active, so the client treated that empty group as a terminal focus change.
Do not activate the empty split for unfocused remote previews, and when a
reserved preview group is still empty, keep the sibling editor as the
visible tab instead of following the host terminal.
On Linux with no keyring, Electron falls back to the `basic_text` backend, which
"encrypts" with a hardcoded password. `isEncryptionAvailable()` returns true for
it, so Orca reported those secrets as sealed. They are not.
The obvious fix — returning false for basic_text — is wrong and would have been a
credential regression: `decryptWithStatus()` skips decryption entirely when
encryption is unavailable, so every already-stored secret would read back empty.
Sealing genuinely works on basic_text and must keep working.
So capability and trust are now separate questions. `isEncryptionAvailable()`
still answers "can this host seal and unseal", and `describeProtectionGap()`
(renamed from `describeUnavailable`) answers "is my data actually protected",
covering both no-sealing and weak-sealing.
That method had no production caller — the port documented a promise nothing
kept. `reportSecretProtectionGap()` now reads it at startup. A user-visible
surface is follow-up; this at least stops the silence.
Adds a bootstrap wiring guard over all nine host port installs. The no-op
defaults are correct for a renderer-less host and silently wrong for the desktop,
and a dropped or reordered install fails no existing test. Verified in both
directions: it fails when an install is removed, and when one moves after the
runtime is constructed.
* fix(pr-page): route remaining user avatars through GitHubUserAvatar
On a private-mode GitHub Enterprise instance the stored avatar URL 302s to
/login, and the renderer's default Electron session carries no cookie, so the
image never loads. #8784 added GitHubUserAvatar for exactly this — it degrades
to an initials placeholder via onError — but three call sites in
PullRequestPage kept a bare <img>: the reviewer picker, the comment author,
and the @ mention suggestions.
Each only guarded on avatarUrl being absent, so on GHE the URL is present, the
placeholder branch never runs, and a broken image is left on screen. The
authorAvatarUrl type comment already documents the intended contract ("falls
back to the login URL and finally an initials placeholder").
GitHubUserAvatar was already imported in this file for the PR author, so this
makes all five avatars in the page consistent. Note the three switched slots
now carry the shared border/bg styling, matching the two that already did.
Add a boundary test that fails if any avatar is rendered through a bare <img>
again.
Fixes#13976
* fix(task-page): route GitHub avatar cells through GitHubUserAvatar too
Auditing the rest of the GHE avatar path turned up the same bare <img> in
TaskPage: GitHubAssigneeAvatar, GHAssigneesCell and PRReviewCell. Fixing only
the PR page would leave half of #13976 in place.
GitHubAssigneeAvatar is the clearest case — ReviewChipAvatar directly above it
already renders through GitHubUserAvatar, so two adjacent functions disagreed
on how a GitHub user avatar is drawn. Its border also moves from
border-border/40 to /50, matching the neighbour.
Linear member avatars in this file are left alone; they use their own provider
path and are out of scope here.
Move the regression assertions into the existing repro-8784 file rather than a
new boundary test — that file already guards PullRequestPage and TaskPage
together, so it is where this belongs. The PR-page check matches the <img>
pattern instead of specific field names, so a rename or a newly added avatar
slot cannot slip past it; the TaskPage check is scoped per function to avoid
catching the Linear cells.
* test(github): scope the avatar guard per call site and cover TaskPage names
Addresses review feedback on the regression guard.
The field-name regex missed aliases and resolver expressions, and the PR-page
assertion did not require GitHubUserAvatar in each migrated slot — deleting all
three would have passed. Reject any bare <img> within the component scope
instead, which is safe now that every assertion is scoped to one function.
Drive all six slots from one table so each gets its own named case, and extend
the display-name contract to TaskPage, which previously went unchecked. The
ConversationTab entry carries displayName: null because PRComment has no
display-name field.
Reverting the fix now fails 11 cases instead of 3.
The restored-snapshot baseline permanently drops every delivery chunk at or
below the snapshot's seq, on the model's claim that those bytes are already
painted. reconcileChunkAgainstRestoredSnapshot recovers when the baseline
under-reports (a gap ahead re-restores; a rawLength mismatch re-restores) but
has no path for a baseline that over-reports: those chunks return
drop-duplicate forever, and an idle shell never re-sends them.
Gate arming on whether the snapshot painted printable cells. A snapshot
claiming seq > 0 while painting nothing cannot be the rendering of the output
it claims to cover, so the claim is disproven and the redelivery is the only
remaining copy.
Also pins disposeHeadlessTerminal's two-part write ordering, which was
previously unpinned and silently reversible.
Refs STA-5179