promptCacheTtlMs was a third store subscription for a field the card
already reads via foundation.settings; derive it instead.
Does NOT bundle the 26 foundation selectors as originally planned.
Measured on this store with React (150 cards, 400 unrelated writes,
26 keys): 26 separate useAppStore hooks 100.7ms; one useShallow bundle
402.5ms (4.0x worse); one bundle with a non-allocating keyed compare
141.5ms (1.4x worse). zustand v5 shallow() rebuilds two Maps from
Object.entries per compare, and a bundle must still build the whole
projection every notification, so collapsing cheap selectors only
trades away useSyncExternalStore dispatches that are cheaper than the
object it allocates. Listener count would have improved 26 -> 1 while
the hot path got slower.
* fix(terminal): carry kitty flags through Preview snapshots and pair rele
Preview was omitting the live kitty mirror from the IME bridge and dropping kitty flags from snapshots, so every commit was evaluated at flags 0. A TUI that negotiated bit-3 (report_all_keys_as_escape_codes) would receive the legacy raw text it declined.
Now the snapshot carries proven kitty flags beside their sequence boundary, the forwarder reads flags once per commit, and bit-1 (report_event_types) commits are paired with exactly one release regardless of keyup/insertText ordering. Snapshot authorities expose only the active screen's proven flags, so an old host's absent field stays unknown rather than downgraded to a manufactured zero.
* fix(terminal): sync kitty flags and IME releases across snapshots
* trim wordinesss
* fix(terminal): settle owed IME release before fresh same-key press
When a keyup is lost and the same key is pressed again, settle the stale
record's owed release instead of discarding it — this maintains correct
IME state during recovery. Also refine Kitty flag propagation to only
carry proven baselines across snapshots, and tighten related comments.
* fix(terminal): gate kitty flags on sequence boundaries
- Remote snapshots only include flags when seq is present
- Daemon uses parsed flags value when defined
- Ensures correct flag ordering in snapshot replay
fish arms `CSI ?2031h` before painting each prompt and withdraws it when it
hands the tty to a child — a ~1ms window. Orca answered that subscribe with
`CSI ?997;Nn` across a 1-3ms renderer hop, so the reply landed after the
withdrawal and was read as stdin by the next child, corrupting `brew`/`npx`
`[y/N]` prompts.
The reply is not stale by Orca's own view when written (measured
staleReplies: 0), so no suppress-the-stale-reply scheme can close this — the
information needed to suppress does not exist yet. Nothing asked for the reply
either. The Contour spec says a terminal "should only send out the DSR when the
palette has been updated"; Ghostty (Termio.zig:729 — force=true reachable only
from the ?996n DSR), iTerm2 (VT100Terminal.m:995 — flag only) and xterm.js
(InputHandler.ts:2035 — flag only) all emit nothing on the DECSET. So stop
entering the race: record the subscription, answer nothing.
Of 17 real programs measured under a pty, only fish, tmux, claude and opencode
subscribe; none block on a reply, and answering produces one redundant palette
re-query and zero rendering difference. tmux is the only one that sends `?996n`,
which Orca still answers.
- Subscribes are record-only at all four emitters (live scan, hidden-gate fact,
parked byte watcher, parked responder — the last is deleted, it only replied).
- `?996n` answers, the subscription registry, and the theme-flip push are
unchanged. `paneLastThemeMode` is still seeded at subscribe so the next
appearance re-apply is not read as a flip.
- Replay grammar carries `?2031l` alongside `?2031h`, so a late-attaching remote
client no longer registers a subscription the TUI already retired.
Also closes fish-integration gaps found alongside: `unset` (which fish lacks)
becomes `set -e` on paths parsed by the client's login shell, `config.fish` is
parsed for agent-home detection, and bracketed-paste startup delivery is made
consistent across local/daemon/relay.
Regression test drives real fish 4.7.1 under node-pty and asserts on what the
child process reads; it fails against pre-fix code with the exact payload from
the issue. CI installs fish 4 and fails loudly rather than skipping.
Closes#9993
Co-authored-by: Orca <help@stably.ai>
* fix(agent-title): treat Claude Code quarter-circle spinners as working
Claude Code 2.1.228 swapped its busy OSC title spinner from braille
(U+2800-U+28FF) to quarter circles (U+25D0/U+25D1). Orca recognized a busy
Claude title only by braille codepoints, so the new frames matched nothing.
The summary-bearing busy frame ("<glyph> Say hi in one word") carries no
"claude" name token, so it resolved to no-status. The tracker's "idle or
permission followed by no-status means the agent exited" rule then fired
mid-turn, confirmPtyAgentExit confirmed it, and the chat surface routed
exitChat -- kicking the tab to the terminal view on every message.
Widen the accepted glyph set via a shared containsAgentSpinnerGlyph helper.
Agent-specific braille frame shapes (Grok, Pi, synthetic Cursor) stay pinned
to their own glyph set.
Fixes#13889
* fix(agent-title): satisfy static analysis and trim scope
* fix(terminal): rank cold-park recency by activation order, not random UUID
The keep-warm exemption (#8262) broke equal hiddenSinceMs with
id.localeCompare over UUIDv4 tab ids, so which tab stays warm was a coin
flip. Ties are routine, not exotic: use-terminal-tab-cold-parking takes one
Date.now() per effect pass (#7214) and stamps every tab first seen hidden in
that pass with it, so switching away from a worktree ties all of them. The
tab that wins the flip is skipped by selectIdsBeyondHotRetain and never parks.
Ranks by an explicit activation sequence recorded on the hidden->visible edge.
Unit test covers the tie; red before, green after.
Does NOT fix terminal-hidden-view-parking.spec.ts:467, which still fails with
the same 'did not park' error on CI. Shipping this on its own merit.
Co-authored-by: Orca <help@stably.ai>
* fix(terminal): preserve focused tab across parking ties
---------
Co-authored-by: Orca <help@stably.ai>
Co-authored-by: E2E Test <e2e@test.local>
* fix(sidebar): give draft reviews their own glyph instead of a red PR icon
The worktree card review icon used one PR glyph for every state and
tinted it by check status, so a draft with failing checks rendered as a
red PR icon that read as closed. Shape now carries review state
(draft/closed/merged), and check tone applies only to open reviews so
the glyph agrees with its tooltip. Same fix for the source-control
header icon, where draft and closed differed only by a muted tone.
Closes#13088
Co-authored-by: Orca <help@stably.ai>
* test(sidebar): pin stateless and GitLab-closed review icon behaviour
Documents why a stateless row keeps its check tone (folder cards render
one while a linked review loads or its details fail) and covers the
closed-MR glyph override, which previously read as already-merged.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
Enable eleven oxlint rules that simplify code without changing behavior, and fix
every existing violation. Each candidate was gated on measured cost rather than
assumption, so rules that regressed runtime performance or type checking were
dropped instead of suppressed.
typescript/no-redundant-type-constituents is the largest addition: 113 sites, no
autofix. Dead constituents are deleted. Where the redundant literal existed to
document intent (`string | 'all'`), it is preserved as `(string & {})`, which
keeps the autocomplete hint the original code was reaching for instead of
flattening it away. The rule also caught a broken import —
remote-shared-control-retirement-probe.ts pulled RuntimeStatus from
src/shared/types, which does not export it, so the type silently degraded to
`any`; no tsconfig covers that file, so tsc never saw it.
oxlint stays at 1.77.0 rather than 1.78.0 because .npmrc sets
minimum-release-age=4320 and 1.78.0 is younger than that window.
Rules evaluated and rejected, with what disqualified each:
- prefer-string-raw: String.raw is a runtime call, not a literal (184x slower)
- prefer-string-replace-all: 26% slower
- text-encoding-identifier-case: ~5% slower, reproducible
- prefer-spread: [...str] is 110% slower than split('') and differs on surrogates
- no-implicit-coercion: `!!x` narrows types and `Boolean(x)` does not (22 tsc errors)
- prefer-arrow-callback: arrows are not constructible, breaking `new` on mocks
- object-shorthand: rewrites source text asserted by a tracked reliability gate
- switch-case-braces: pushes ten files past max-lines, which cannot be suppressed
- no-useless-switch-case: drops `case undefined:` that switch-exhaustiveness-check needs
- arrow-body-style: 115 violations have no fix, and it breaks max-lines
- newline-after-import: false-positives on the leading-semicolon ASI idiom
electron-vite-output-contract asserted on the literal
Object.prototype.hasOwnProperty.call text; retarget it to Object.hasOwn, which
rejects inherited keys identically.
* Fix smart sort ranking of done agents by completion time
Completed entries stayed in the Done sort class indefinitely when
same-state writes refreshed updatedAt without moving stateStartedAt.
Introduce agentEntryCompletionAt() to use actual completion time for
both age display and sort eligibility, ensuring consistent aging
regardless of hook updates.
* Fix smart sort ranking of done agents by completion time
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
Collapsed threads visibly close on the host — this is the
acknowledgement. An extra reply only adds noise. Snapshot the
resolution target at launch to prevent silent drops if the panel
navigates before agent delivery. Only reply for comments without
resolution endpoints.
Grouped/triage sections now read newest-first so recent discussion surfaces
at the top, while Timeline keeps its oldest-first history order.
- rename sortPRCommentGroupsForTimeline to sortPRCommentGroupsByRecency and
add an order parameter
- rank threads by their latest activity under newest-first so a fresh reply
refreshes an old thread
- break timestamp ties by numeric comment id in the sort direction, so a
GitHub review batch sharing one timestamp orders correctly
- sink groups with an unparseable createdAt to the bottom in both orders
Co-authored-by: Orca <help@stably.ai>
* Allow direct search with configured search engines
Users can now search directly from the tab creation menu using their
configured search engine. Forced search mode (`?` prefix) skips file and tab
matching for guaranteed search. Refactored tab-entry operations into focused
modules for clarity: forced-search parsing, network-safe selection, keyboard
focus, copy strings, empty options, and props types. Search routes through
the same workspace browser tab opening mechanism used for URLs, with safe
title and query presentation that doesn't retain sensitive details.
* Allow direct search with configured search engines
- Permit search and URL navigation while file index loads; require explicit
selection only when needed, not automatic opening
- Block malformed IPv6 addresses in bracket notation to prevent misclassification
- Support Kagi private-session links via searchUrlOptions
- Improve error handling with accessibility: show error messages in status region,
disable input during submission, display loading state
- Fall back to local browser tab creation when remote creation fails instead of
throwing; avoids remote availability blocking local search/navigation
- Add error translations for all supported locales (es, ja, ko, zh)
* Allow direct search with configured search engines
- Extract tab create entry lifecycle to key-driven component remounting, replacing conditional state reset with useEffect cleanup
- Consolidate network tab entry classification and request building into reusable helpers, eliminating duplicate logic
- Simplify owner resolution by inlining logic directly into openWorkspaceBrowserTab
- Replace custom surrogate-pair handling with native String.toWellFormed() for search queries
- Disable explicit URL classification to prioritize search-engine queries over raw URLs
* Allow direct search from quick-open tab bar entry
- Single-token queries keep file matches ranked above search (quick-open intent)
- Multi-word phrases promote search to top, since they cannot be file paths
- Arm network actions once file index fails or text is unambiguous search
- Cache prepared file index to avoid re-processing per keystroke
- Generate specific tab titles (e.g. 'example.com/docs') instead of generic 'Open URL'
- Surface opening workspace when launching browser tabs remotely
* Use readOnly instead of disabled for pending search input
Maintain keyboard focus during submission so arrow/Escape navigation continues to work. Use aria-busy to indicate loading state accessibly. Also fixes button hover styling when disabled and cleans up error message handling in the classifier.
* Treat bare searches as prompts; refine path and IP classification
- Bare search queries (e.g., "?") no longer display as error rows
- Path prefixes with existing matches are no longer blocked mid-keystroke
- Private IPv4 addresses now use http, public addresses use https
- Ambiguous inputs with non-numeric ports fall through to search instead of blocking
- Improve diagnostics by logging failure reasons in openFailure
* fix(browser): exclude Google cookies from imports (STA-3811)
Imports never write and never remove a google.com-family cookie, on any
path. Signing in directly inside Orca is the only Google session that
survives, so the live jar always beats anything an import could plant.
* fix(browser): roll back selective cookie clears
* fix(settings): read voice settings from a ref, not a stale closure
updateVoiceSettings wrote from a closure captured at mount, so an async
continuation could clobber a newer value. Uses the latest-ref pattern already
in Settings.tsx:405-407.
Found while investigating voice-microphone-selection.spec.ts:107 and confirmed
NOT to be its cause — that failure has no established root cause. The spec
gains a hydration barrier and toBeEnabled assertions here, which improve its
failure message but do not fix it.
Co-authored-by: Orca <help@stably.ai>
* test(e2e): drop the voice spec changes from this PR
The hydration poll was a no-op: prepareVoiceSettings awaits updateSettings
inside the same page.evaluate, so the store is already committed before the
poll samples it, in all three tests (the reload precedes it). Worse, its
comment asserted that the Select is disabled until hydration — the CI
accessibility snapshot at the moment of failure shows the switch checked and
the combobox enabled, so that theory is refuted, and a wrong 'why' comment is
worse than none.
The toBeEnabled assertions go with it: the control was enabled when it hung,
so they would not have caught the real failure either.
Leaves this PR as what it actually is — a product bug fix. The spec's failure
stays open and unexplained.
Co-authored-by: Orca <help@stably.ai>
* test(settings): type the clear-key mock as its API contract
vi.fn(() => clearing) returned Promise<void> where the preload contract is
Promise<{ configured: boolean }>. Vitest does not typecheck, so this passed
locally while tsc -p tsconfig.tc.web.json failed — it would have gone red in
CI's required typecheck job.
Co-authored-by: Orca <help@stably.ai>
* fix(settings): write the voice-settings ref in an effect, not during render
React Doctor's 'Ref mutated during render' rule is blocking in CI's static
analysis job, and it is right: React can replay or discard render work, so a
render-time ref write can leak from UI that never commits.
Moving it to an effect keeps the behavior this fix needs — every writer here
(key-status probe, save-key, clear-key) runs post-commit, so the ref is current
by the time any of them reads it. Test still discriminates: red without the
fix, green with.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* feat(sidebar): add jump-to-top button for hard upward scrolling
Detect intentional hard scroll-up gestures (wheel or scrollbar drag) and
offer a one-click jump-to-top affordance. Auto-hide after idle to avoid
persistent visual clutter. Addresses the common case of fast navigation
through long worktree lists ranked by agent activity.
* test(sidebar): improve scroll-to-top detection for active gestures
Only detect velocity from active scrollbar drag or touch, not
programmatic scrolls. Return focus to list after jump-to-top. Add
comprehensive hook tests with gesture simulation. Improve cumulative
down-delta tracking for dismissal.
* test(sidebar): add scroll-to-top gesture detection tests
Add comprehensive test coverage for the hard-upward-scroll detection hook,
verifying idle timer behavior, gesture suppression, scrollability checks,
and cleanup on unmount. Extract the post-jump suppression window into a
named constant for maintainability.
Adds GitHub stacked pull request creation: a contextual "Stack this PR above #N" option that appears only when the selected base branch has an open PR, plus the main-process stack preflight and registration.
Also reworks the create-review composer for cohesion: shadcn Checkbox and Label primitives, base label above a full-width searchable combobox with attached results, keyboard navigation, and a unified field skin, spacing and typography scale.
Verified end to end against real GitHub: extending an existing stack and creating a new one.
* feat(bitbucket): connect Bitbucket from Settings with encrypted credential storage
Bitbucket Cloud was the only review provider with no in-app auth: GitHub and
GitLab delegate to the gh/glab CLIs, but Bitbucket has no comparable
first-party CLI, so the only option was ORCA_BITBUCKET_* env vars plus a
restart (discussion #5364).
Adds a Connect/Edit/Disconnect flow on the Bitbucket integration card,
modeled on Linear and Jira:
- Credentials are verified against /user before they are persisted, so a
dead token is rejected inline instead of silently stored.
- The secret is encrypted with safeStorage (0600 plaintext fallback when no
OS keyring); non-secret metadata lives in a separate plaintext file so
status reads render the connected account without decrypting. Opening
Settings therefore never triggers a keychain prompt.
- Env vars keep precedence over stored credentials, so existing headless and
SSH setups are unaffected. Env-managed connections hide Disconnect.
- connect/disconnect reset the preflight cache, so no relaunch is needed.
The Bitbucket card moves to its own file to stay under the tsx max-lines cap.
* feat(bitbucket): support creating pull requests from Orca
Bitbucket was the only configured provider whose Create button reported
"This repository provider does not support creating a pull request from
Orca" — supportsReviewCreation was false and the forge provider had no
createReview, so even a correctly authenticated setup was blocked.
Adds createBitbucketPullRequest against POST /repositories/{ws}/{repo}/
pullrequests, using the same env-first / stored-credential resolution as PR
lookups (extracted into resolve-auth.ts so both share one path).
Bitbucket Cloud has no draft pull requests, so a draft request is rejected
with a clear message rather than silently publishing a live PR.
* fix(bitbucket): hide the draft toggle where drafts do not exist, plus review fixes
Bitbucket Cloud has no draft pull requests, so the composer no longer offers
the toggle for it and forces the flag off at submit — better than failing
after the user has filled the form in.
Review fixes:
- writeFileSync's `mode` only applies when it creates the file, so rewriting
a credential kept whatever permissions it already had. chmod after every
write, for the secret and the metadata.
- An explicit ORCA_BITBUCKET_API_BASE_URL now wins over a stored base URL.
Env precedence is per-setting, not all-or-nothing.
- Enter in the credentials dialog only submits from a text field, so it no
longer hijacks Cancel and the docs link.
- Replace the chmod-based delete-failure test with a mocked unlinkSync: file
modes are not portable to Windows and elevated runners unlink anyway.
* fix(bitbucket): stop a merged pull request from blocking the branch's next one
Reported on #5832: with a merged PR on a branch, Create reported "Pull
request already exists" and offered no way forward.
The branch lookup queries every PR state and returns the most recently
updated one, so a merged PR came back as the branch's current review and
eligibility blocked on it. Bitbucket only discarded such a match on the repo
default branch (#9171), while GitHub already drops any merged PR it matched
by branch alone — "a merged PR without an explicit link is just a historical
branch match, not implicit review context".
Applies that rule to Bitbucket. An explicitly linked review still resolves
through the linked-number fallback, so merging a PR Orca knows about keeps
showing it.
* fix(bitbucket): add bitbucket to the shared review-creation provider list
Reported on #5832: on a Bitbucket repo with no existing PR, Create still
said "This repository provider does not support creating a pull request
from Orca", even after the forge provider gained createReview.
There are two capability lists. Enabling supportsReviewCreation on the forge
provider was necessary but not sufficient — the blocker and the whole
renderer read the separate shared list, which never included bitbucket.
Adds it, gives Bitbucket its own provider name so review copy stops saying
"GitHub", and asserts the two lists agree so they cannot drift apart again.
* fix(bitbucket): persist pull request links after creation
* fix(bitbucket): fetch linked pull requests by number first
* fix(i18n): use generated Bitbucket integration keys
* test(bitbucket): cover forge creation delegation
* fix(bitbucket): fall back when linked pull request is stale
* docs(bitbucket): explain notFoundIsNull and fix a garbled permissions comment
notFoundIsNull arrived without the rationale its sibling flag carries, and
reads as a bare `true` at the only call site that opts in.
* fix(bitbucket): address review findings before merge
Two of these made the feature unusable in real setups:
- Create PR checked GitHub authentication for Bitbucket. isProviderAuthenticated
fell through to isGitHubAuthenticated, which was unreachable while Bitbucket
could not create reviews at all. Anyone with Bitbucket connected but no
`gh auth login` got auth_required with no way forward.
- The draft flag was only gated in ChecksPanel, not the two SourceControl call
sites. With "create as draft" saved as a default, the composer hides the
toggle for Bitbucket, so the flag could not be cleared and creation failed
every time. Bitbucket now ignores draft instead of rejecting it.
Also:
- Blocked-create copy said "GitHub is not authenticated. Run gh auth login" on
Bitbucket repos, in both the main-process and renderer paths.
- A decryption failure resolved to an anonymous config and queried anyway; a
private repo answers 404, which reads as "no pull request" and offers Create
for a branch that already has one. Requests now fail closed.
- Hiding non-open implicit branch matches was too broad: a declined PR became
permanently invisible off the default branch. Scoped to merged, restoring the
default-branch rule (#9171) for the rest.
- A failed disconnect rejected unhandled and the card silently re-rendered as
connected; a partial delete left the secret live in memory for the session.
- The credentials dialog refused to open on a remote runtime, so a local repo
could never store a credential. Now only the storage note changes, matching
the Jira dialog.
---------
Co-authored-by: devatnull <59279509+devatnull@users.noreply.github.com>
* fix(terminal): retain WebGL for recently hidden worktrees
Suspending a hidden worktree disposes every pane's WebGL addon, so every
switch-back presents DOM-renderer frames (unfloored ~5% wider advance, and
the channel through which any poisoned cell metrics reach the screen) until
reattach completes — 0.5-3s on loaded sessions. Keep the live addons for the
most recently hidden worktrees instead: LRU capped at 6 contexts (Chromium
allows ~16/page, visible worktrees use 1-4), least-recent evicted to dispose
exactly as before. Reveal then has no renderer swap and nothing to flash.
Window-occlusion callers pass no retention context and are unchanged.
* fix(terminal): pause retained hidden cursor work
* fix(terminal): preserve retained WebGL through deferred rebuilds
* fix(terminal): explain a dead terminal host instead of leaking a socket error
When the daemon that owned a session has exited, resuming that terminal surfaced
the raw connect error for its endpoint — on Windows a named pipe path, since a pipe
disappears with its server process — in a red toast ending in "file an issue". The
session is unrecoverable and the text is unactionable.
Translate it at the daemon boundary into terminal_host_gone, following the existing
terminal_pane_owner_unverified path, and humanize it in the toast. Suppress the
issue link for a condition Orca can fully explain.
Covers the remote-host case: the conversion sits in attachStablePaneOwner, the first
catch a paired client's resume reaches before the error is serialized onto the wire.
* fix(terminal): preserve host-gone errors across versions
* fix(terminal): scope host-gone error matching
* refactor(terminal): derive the host-gone test and replace forms from one source
Two regex literals matched the same token with subtly different shapes
(capturing vs non-capturing, consuming vs lookahead), so an edit to one could
silently drift from the other. Build both from a single source string; the test
form stays non-global so it carries no lastIndex state between calls.
* test(terminal): cover host-gone regex boundaries
* test(terminal): isolate leading host-gone boundaries
Forward-slash //wsl.localhost links opened as a different path string than
the Files sidebar's backslash UNC, so file watchers did not refresh the
editor. Canonicalize WSL UNC to \\wsl.localhost\... at map time.
Fixes#13349
Use color-mix to blend selection highlight instead of flat accent, making the keyboard cursor visible on light popover surfaces in both light and dark modes. Fix race condition in selection handling where deferred query lag could break arrow navigation by always accepting cmdk's internal cursor updates.
* fix(feedback): pre-include Orca version and OS in errors and feedback
Make terminal errors and Send Feedback carry easy-to-copy client
environment details so bug reports include build and platform context
without a follow-up ask.
* fix(feedback): satisfy exhaustive-deps in environment prefill hook
Destructure hook params so useEffect/useLayoutEffect dependency lists
are complete and the changed-code quality gate passes.
* fix(preload): stop importing node:os in sandboxed preload
Sandboxed Electron preloads cannot require node:os. That import crashed
the whole preload script, leaving window.api undefined and taking down
App chrome (dock badge, preflight). Keep platform.get on process APIs
only, and guard best-effort badge/preflight callers when api is missing.
* fix(feedback): count text typed below the env footer
Strip only the prefilled Orca/OS/Shell block for Send validation so
users who click past the footer and type can still submit.
## What this changes
Two renderer inputs were trusted because their TypeScript types said they were valid. Both are now validated at the boundary that owns them.
**Terminal cursor style.** `normalizeTerminalCursorStyleDefault` preserved any non-null migration-stamped value without checking it against the actual enum, so a runtime value outside `bar | block | underline` survived into `terminal.options.cursorStyle`. It now enum-checks and falls back to `block`. Applied on both read paths (desktop `Store` load, web `getStoredSettings`) and both write paths (`Store.updateSettings`, web `settings.set`), so an unsupported value cannot reach persistence, the `settings:changed` publication, or xterm.
**Plugin language packs.** The renderer stored the `listLanguagePacks()` IPC result without a runtime check, so a non-array response was assigned to state and later crashed its first array consumer on `.find`. Ingress now accepts only an array, drops members failing `isPluginLanguagePackRegistration`, keeps valid siblings in their original order, and logs which failure mode occurred. The registration guard requires `resourceLanguage` to equal `pluginLanguageResourceId(id)`, which keeps a pack with a missing or inconsistent identity out of i18next — that shape reproduces as `TypeError: Cannot read properties of undefined (reading 'includes')` with the guard removed.
Catalog validation is shape-only on this path (`validatePluginLanguagePackCatalogShape`), so revalidating an already-parsed catalog does not allocate a second copy of it. `isCatalogObject` also now requires a plain-object prototype, and the walk rejects repeated or cyclic object references.
## Root cause: partially known
Worth stating plainly, because the fix is defensive rather than causal:
- The non-array pack container and the out-of-enum cursor value are both reproduced directly by tests.
- Two additional field stacks are *consistent with* an invalid `resourceLanguage` and are blocked by this guard, but the reports do not prove that malformed registrations were their source.
- No production writer in current code or history emits an out-of-enum cursor style. The Ghostty config importer (`src/main/ghostty/mapper.ts`) already rejects unsupported `cursor-style` values. The original writer is unidentified.
Every in-tree producer supplies valid data, so these guards are no-ops on the current happy path. They are boundary hardening against a mutation we have not located, not a repair of a known writer.
## Trade-offs
- Malformed registrations are skipped with one warning rather than surfaced in the UI. Valid siblings keep their identity and order. Note the main-process registry already reports per-plugin parse errors, so this path only catches corruption after main has validated.
- Renderer ingress walks each catalog once per lazy load or `contentPacksChanged`. It does not run on render or terminal-output paths, but a maximum-size burst still costs tens of milliseconds synchronously. Pack count and plugin-ID length remain uncapped.
- An unsupported cursor value now renders and persists as `block`, so anyone relying on an undocumented third-party value loses it.
- Loading settings whose cursor style is absent or invalid now marks state dirty and rewrites once, matching the existing `terminalRightClickToPasteDefaultedForPlatform` pattern above it.
## Compatibility
`listLanguagePacks` is local `ipcMain`/`ipcRenderer` only and is not implemented in the web build, so nothing here crosses the remote wire — no RPC parameter, publication schema, stream frame, opcode, or capability changed. Validation is receiver-side at existing boundaries. Nothing is platform-, shell-, PTY-, native-module-, SSH-, WSL-, or worktree-dependent, and the web build normalizes on both read and write. Malformed plugin data is rejected before i18next sees it; no new permission, network path, executable input, or persistence schema was added.
## Verification
Locally on the final head: the 5 touched test files pass (591 tests), plus node and web typecheck, oxlint, and oxfmt. All GitHub required checks pass, including Windows packaging, static analysis, Git and wire compatibility, shell contracts, and all 32 Node 24/26 shards. The path-gated E2E job is skipped after its detector passed.
Scope note: an earlier revision generalized this hardening to `Project.sourceRepoIds` and profile transfer without field evidence. That scope was removed; the diff is cursor and plugin paths only.
Forward the renderer's already-pinned {mergeBase, headOid} to the SSH relay so a single-file branch diff reads the two blobs directly instead of rediscovering live HEAD. Six sequential git processes become two concurrent reads, and a branch move mid-review no longer changes which revision is displayed.
Equivalence with the legacy route is proven against real Git across 14 change types; wire compatibility is proven over a real SSH socket against relay bundles built from main and from the pre-merge-base.
Once V8 optimizes the calling function, `String.prototype.codePointAt` on a
sliced string pairs a trailing high surrogate with the code unit that follows
the SLICE inside its parent, returning a code point the string does not
contain. Every UTF-8 byte scan built on `codePointAt` therefore reported one
byte too many for a prefix slice cut mid-pair, but only after tier-up, which
is what made terminal-stream-byte-length.test.ts fail intermittently on the
same commit.
Read the units explicitly with `charCodeAt`, which stays bounds-correct in
every tier, via a shared `readUtf8CodePointAt`.
* perf(renderer): keep catalog array identity and path-status cache on no-op refetches
mergeByIdentity always allocated a fresh array, so project-group and folder-workspace
refetches replaced state by reference even when nothing changed, refiring a forced
folderWorkspace.getPathStatus sweep per row on every repos:changed.
Unchanged merges now return the previous array, and the paired
folderWorkspacePathStatuses resets are gated on the same check so a no-op refetch
neither clears the cache nor needs to refill it. Real catalog changes clear and
refire exactly as before.
Co-authored-by: Orca <help@stably.ai>
* refactor(renderer): make the catalog arrays readonly
The identity guard can now return the caller's array, so a future in-place push or
sort on state.projectGroups / state.folderWorkspaces would alias store state. Typing
the merge helpers and the slice fields readonly removes both `as T[]` casts the guard
introduced and adds none, matching what #13744 did for state.repos.
Partial test fixtures cast the element rather than the array, so no `as unknown as`
laundering is needed.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
setRuntimeEnvironmentStatus always cloned runtimeStatusByEnvironmentId, so a re-probe
returning an identical status still invalidated every subscriber. It now returns the
state unchanged when the computed entry matches the stored one.
checkedAt is excluded from the comparison: it is Date.now() per probe and no consumer
reads it, so including it would make the guard dead code. Generation advance and the
disconnect toasts still run on every call.
Co-authored-by: Orca <help@stably.ai>
The stream-gating hook (#13608) and the subscription parking installer
(#13621) each reimplemented `isWindowVisible() || isDocumentVisibilityProvenStale()`
plus its visibilitychange + stale-recovery subscription, and each carried
its own 500ms park delay that agreed only by coincidence.
Extract both into `lib/window-park-visibility.ts` and leave the two
consumers as thin wrappers: `useWindowStreamVisible` feeds the snapshot
and subscribe into `useSyncExternalStore`, the parking installer calls
them imperatively. The subscribe now always uses the defensive
`typeof document` guard the parking side already had, and registers
stale recovery before the visibilitychange listener so the stale latch
is cleared before the callback reads it.
Both park delays now derive from one `WINDOW_HIDE_PARK_GRACE_MS`: they
debounce the same signal against the same user behaviour (a quick
app-switch round trip). The parking side's 8x reveal-side backoff stays
where it is - that models its own resume cost, not the grace window.
Renderer-authoritative gating (#13622 native chat, #13634 editor panels)
is deliberately left alone; it keys off Zustand routing state, not OS
visibility.
Also switch the active-runtime ref writes in web-session-tabs-sync to
`useLayoutEffect` so no resume event can land between paint and the
passive flush, and correct the stale comment above them - the install
effect reads neither ref (both readers are gated on `isVisibilityResume`).
Mark invalidated diff content stale instead of dropping it, mirroring the file-content treatment from #13634. The diff read-generation fence (diffReadGenerationRef) already rejects a superseded read's write-back, so retaining the bytes and swapping them on arrival is safe and keeps every RPC saving -- it just stops the loading placeholder flashing on each reveal.
The git-status reload effect now treats a stale entry like a missing one so the lazy-load effect stays the single fetcher for it.
Co-authored-by: Orca <help@stably.ai>
* perf(native-chat): suspend hidden transcript streams
* fix(native-chat): keep assembly renders pure
* fix(native-chat): keep a transient error from stranding a revealed chat
- An error snapshot frame no longer latches frameArrived, so the in-flight
read can still seed the pane instead of leaving it on the error surface.
- Retained history is shown over a full-pane read error for the same source.
- The paged read window survives a hide/reveal; only a source change resets it.
Co-authored-by: Orca <help@stably.ai>
* fix(mobile): match the trimmed native-chat retention signature
The shared retention type no longer takes `loading`, so mobile's call was an
excess-property error that broke the mobile typecheck job. Dropping it also
lets mobile inherit the desktop behavior: a stream error or dropped client
keeps the last transcript instead of swapping it for the error empty state.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* perf(renderer): park hidden remote browser screencasts
* test(renderer): pin stale-visibility term and parked-unmount teardown
Restores the WHY behind the '|| isDocumentVisibilityProvenStale()' term the
emulator-module dedupe dropped, and pins it plus the close-tab-while-parked
path with tests. Extracts the lifecycle harness so the spec stays under
max-lines without a suppression.
Co-authored-by: Orca <help@stably.ai>
* fix(ci): exclude test-support modules from the localization audit
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* perf(remote): park hidden session mirrors
* perf(renderer): bound visibility resume cost and repair fences
- Back off the park delay after a park the user undoes quickly; each resume
re-enumerates every paired host, so short hide/reveal churn now stops
parking instead of paying that cost per cycle.
- Expire the active-worktree omission fence one visibility generation after
the inventory that set it, and name the unfiltered-inventory invariant it
depends on.
- Never skip the unchanged resume inventory for a worktree whose repair
replay is already armed.
Co-authored-by: Orca <help@stably.ai>
* fix(renderer): move active-runtime ref writes out of render
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* perf(editor): park hidden external reloads
* fix(editor): sync content refs after commit
* fix(editor): close post-commit ref window
* test(editor): cover commit-time visibility
* fix(editor): keep invalidated content visible and stop redundant reads
- Mark invalidated file content stale instead of dropping it so a reveal swaps
bytes in place; the read-generation fence already blocks stale write-backs.
- Skip the lazy read when the newest read for the tab is still outstanding, so
a worktree flip-flop cannot double-fetch.
- Stop claiming Cmd/Ctrl+S outside the workspace view, where no mounted panel
owns the request and the keystroke was silently swallowed.
* Add copy button to quick commands with visual feedback
Quick command rows now display a copy button that copies the command body to clipboard. The button shows brief visual feedback ("Copied" or "Couldn't copy") and is disabled when the command body is empty. Includes desktop and mobile UI, tests, and full i18n support.
* fix(ci): unblock verify for quick-command copy button
Key feedback to the copied body so prop changes drop stale labels without
setState-in-effect, and mock expo-clipboard in the mobile list test.
* perf(renderer): dedupe remote status hydration
* fix(runtime-status): re-list host catalog when the listing goes stale
Coverage matching cannot observe catalog edits made by another client or the
orca CLI, so gate hydration on catalog listing age too.
Co-authored-by: Orca <help@stably.ai>
* test(runtime-status): reset the catalog listing clock between tests
The staleness guard keeps its last-listed timestamp in module scope, so the
TTL test left fake time behind and made every following test in the file
depend on its position. Add a reset hook, call it in beforeEach, and pin the
TTL invariant on an externally added host rather than the timer alone.
Also extracts the coverage predicate in fetchSettings and restores the WHY
comment about compat failures clearing on a reachable status.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* perf(runtime): avoid forced SSH hydration on project refresh
* fix(runtime): backfill SSH states for targets learned without a state read
A failed-connect resync labelled a target with no ssh.getState, and the new
metadata-refresh gates then skipped it forever: no SSH chip and every worktree
mutation to that host failed the connection-generation check.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>