Round 3 review found the residual-retention test had gone vacuous. Once the
enqueue boundary started copying each chunk, a baseline taken after enqueue
already contained the parents the test was meant to prove get released, so
disabling residual flattening left it green. Baseline before enqueue instead;
it now fails at 8,720,768 bytes with residual flattening off.
Consumed-slot clearing had no coverage at all - the existing case queues one
chunk per terminal, and the slots only accumulate below the 64-chunk
compaction threshold. Add a case that queues 40 medium chunks per terminal:
363,880 bytes retained with the clearing, 2,928,568 without.
(cherry picked from commit 2707db2b0e)
Round 2 review found a third producer feeding the queue parent-pinning
strings: agent-status-osc.ts builds its cleaned output by concatenating
slices of the combined buffer, and pty-transport hands that straight to the
scheduler for local, SSH and remote-runtime output. That retained 17,283,160
bytes for 524,288 queued chars.
Patching producers one at a time was the wrong shape - the contract was
enforced only by a comment, and a fourth producer would reintroduce it. Copy
once at the enqueue boundary instead, which makes retainedChars true by
construction, and revert the two producer-side flattens as redundant. The
copy runs at 7.6-38 GB/s against a ~30 MB/s sustained drain.
Regression test feeds the scheduler a raw slice of a 2 MB parent: 16,800,424
bytes retained without the boundary, under 4 MB with it.
(cherry picked from commit d1764d5402)
Review of the previous commit found the queue's retention bound was still
bypassable. enqueueChunk records retainedChars as data.length, but the
restore-reconciliation paths in pty-connection hand the scheduler a raw
slice of a much larger chunk, so the queue understated what it pinned:
16,803,368 bytes retained for 524,288 queued chars (32x) on Node 24.
Flatten those two overlap-trimmed payloads at the producer, where the copy
is one bounded tail rather than work on the drain hot path, and state the
enqueue contract explicitly: a caller must hand the queue a string that owns
its storage. flattenRetainedSlice moves to src/renderer/src/lib so both
owners share one documented primitive.
(cherry picked from commit d419dc1b52)
takeQueuedChunk stored the unconsumed remainder back into the queue as
chunk.data.slice(remaining). V8 backs that with a SlicedString pointing at
the whole parent, so a nearly drained 2 MB chunk kept all 2 MB alive to serve
a few KB tail while queuedChars was charged only for the remainder. Fully
consumed chunks were also left referenced until the 64-chunk compaction, so
they retained their strings and callbacks while charging nothing at all.
Track what each chunk really pins in retainedChars and copy the residual flat
once it pins twice its own length. Halving bounds retention to ~2x the honest
queued chars and keeps total copying linear in the original chunk, and the
drain path measured no slower. Drained slots are replaced with an empty
placeholder, which nothing below chunkIndex ever reads.
Measured with 8 background terminals drained to a <=64 KB tail:
8,710,264 bytes retained before, 342,776 after.
(cherry picked from commit ef3fb2af88)
* fix(pty): bound cooked reply queue
* fix(pty): bound cooked reply queue
Implement bounded storage for cooked-echo-safe replies: 64 pending
replies and 4096 UTF-16 code units. Shed oldest replies on overflow,
never ordinary input. Add drain failure containment with generation
fencing to prevent stale operations from clearing fresh input after
clear() reuse.
* fix(pty): report pty id in drain failures
When the async drain yields, the owner may rebind to a different PTY
before the failure surfaces. Pass the failing pty id so the transport can
ignore stale failures from a rebound owner. Also tighten the pending
reply queue size bound to prevent half-written entries.
(cherry picked from commit c0c893d171)
* fix(native-chat): restore transcript history across formats
* fix(native-chat): preserve safe Codex history metadata
(cherry picked from commit 1d3decdbc4)
Prevent older paired hosts from discarding the entire taskResumeState
when they encounter the strict linearIssueView field, which was causing
silent loss of github and jira queries during remote pairing. Layout,
grouping, ordering, and per-workspace filters are now device-local.
(cherry picked from commit d23dda6d48)
(cherry picked from commit e448c68672)
* Show attention badges on recent chats in Cmd+J palette
Keep current tabs visible only when they have a scannable badge
(working, permission, unread, done). Snapshot unread maps alongside
status maps to freeze recent-section membership on open instead of
churning with live updates. Unify badge logic across tab strip and
palette, and extract test fixtures for reuse between suites.
* Exclude idle current tabs from Cmd+J recent; add live attention badges
Current tabs no longer appear in Recent Chats when idle—only working, blocked, or
unread agents keep the current slot visible. `done` no longer admits current tabs;
the user watched it complete on screen, so that slot goes elsewhere.
Add live attention badges to recent rows (working/permission/unread/done), matching
the tab-bar ladder. Freeze recent-section membership at open-time to keep row order
and inclusion synchronized instead of changing live.
* minor fix
(cherry picked from commit 082cc31703)
(cherry picked from commit 8bd61feb97)
* fix(artifacts): gate agent artifact publishing behind an off-by-default capability
Public artifact sharing was reachable by any agent through `orca artifacts
share`: the Artifacts settings toggle only controlled sidebar visibility, and
nothing in the main process checked a capability before minting a public URL.
Add `artifactSharingEnabled` (default off) and enforce it in
ArtifactCloudService.share/update — before auth, network, or the share-record
write — so the CLI, relay-forwarded remote CLI, and IPC paths are all denied.
The denial carries a stable `artifact_sharing_disabled` code plus next steps
through the RPC error allowlist, so the CLI prints actionable guidance.
list, unshare, and delete stay ungated: turning publishing off must not strand
already-published links. The capability is absent from the `settings.update`
RPC schema, so an agent cannot grant it to itself — only the desktop UI can.
Co-authored-by: Orca <help@stably.ai>
* fix(artifacts): gate agent artifact publishing behind an off-by-default
Publishing is blocked until enabled in Settings → Artifacts. CLI preflights the capability before reading files to avoid unnecessary uploads. RPC surface rejects capability grants so callers cannot self-grant. UI shows opt-in workflow and recovery path when publishing is off. Web clients mirror the host's setting read-only.
---------
Co-authored-by: Orca <help@stably.ai>
(cherry picked from commit 5b377a9c0f)
* fix(agent-hooks): gate WSL relay reattach on agentStatusHooksEnabled
Spawn only ensures the guest relay distro when agent status hooks are
enabled, but reattach called ensureForDistro unconditionally — so a
disabled setting reinstalled guest hooks on every local WSL reattach.
Pass the same isAgentStatusHooksEnabled gate through all three reattach
call sites as a required argument so a new site cannot skip it.
Co-authored-by: Orca <help@stably.ai>
* Gate WSL relay at manager level for live toggle support
- Move agentStatusHooksEnabled check from reattach call sites to centralized isWslHookRelayAllowed gate
- Add non-permanent dispose mode so manager can revive relays when setting is re-enabled
- Watch setting changes and dispose live relays when agent status hooks are disabled mid-session
* Restore WSL relays when re-enabling agent status hooks
Extract guest install logic to `wsl-hook-relay-guest-install.ts` for modularity
and add `resumeStoppedRelays()` to restart relays when hooks are re-enabled.
Track distros stopped during a hooks-off teardown, but skip resuming those the
user has shut down (which would unwantedly boot a stopped distro). Strengthen
the disposed check with state identity to prevent respawning untracked relays.
Abandon in-flight launches when hooks are switched off so no relay exists after
opting out.
---------
Co-authored-by: Orca <help@stably.ai>
(cherry picked from commit 6169d67bdf)
Launch resolves options from the static seed, so a discovered model id had no
options and silently dropped --reasoning-effort. unknownModelOptions keeps the
effort menu for those ids. Leave the multi-host launch gate unchanged.
(cherry picked from commit e77dab7910)
Route cooked-echo-risk terminal replies through bounded echo-safe delivery across local, daemon, and SSH relay PTYs. Preserve repeated valid replies, bypass the daemon startup input gate, and keep ordinary input plus latency-critical replies on their existing paths.
Closes#13137
Co-authored-by: bbingz <zzb@gxsmjx.com>
* fix(sidebar): show Cursor rows and stop a stray "claude" title hijacking OpenCode
Two defects in the same title-resolution path.
**#10258** — Cursor's only native OSC title is the literal `cursor agent`, which both title trackers dropped unconditionally. A hookless Cursor pane therefore had neither a status entry nor any title carrying Cursor identity, so the worktree card showed nothing at all.
**#8940** — two owner-blind paths let an incidental `claude` token anywhere in an OpenCode session or task title outrank the pane's known owner, so the tab icon and sidebar row flipped to Claude Code.
#10258: let the literal through exactly once as identity, so a restored or mobile tab keeps its Cursor row instead of vanishing. #8940: require an *identity frame* — after stripping status decoration the title must PRESENT Claude, not merely mention it — before a Claude title may reclaim a pane from its prior identity, and make the sidebar row builder owner-aware.
> These two are in one PR because they share the `ownerAgentType` plumbing through `buildTitleDerivedAgentRow` — split apart, neither half compiles on its own.
Fixes#10258Fixes#8940
Co-authored-by: Orca <help@stably.ai>
* test(e2e): add recordable proof for sidebar-agent-row-identity
Fails on origin/main, passes on this branch.
Test: sidebar keeps a Cursor pane visible and an OpenCode pane out of Claude Code hands
Co-authored-by: Orca <help@stably.ai>
* fix(terminal): preserve restored Cursor identity
* test(terminal): cover restored Cursor redraw suppression
* refactor(terminal): tighten Cursor identity handling and Claude frame matching
Review follow-ups on the title-resolution path:
- pty-transport dropped a native Cursor literal that main emits whenever a
non-Cursor title preceded it, re-introducing the #10258 blank row in the
renderer path. The pre-filter now projects the predecessor the drain will
actually see, and defers to the drain gate while facts are still queued.
- applyTrackedPtyTitle threaded the cursor flag through 12 sites, including
ptyRecordChanged bookkeeping the sole caller ignores. Force the status null
once, and the activity-gated effects fall out unchanged.
- isClaudeIdentityFrameTitle missed a multiplexer-wrapped Claude title
("zsh | Claude Code"), costing a genuine Claude pane its identity. Reuse
the ' | ' segment split that agent-title-owner already had inline.
- Keep title normalization on launchAgent: it only rewrites within an
identity group (OMP wraps Pi), so a split does not make it wrong, and
the hook-row path normalizes the same way.
- Drop the tab.ptyId tracker fallback, which read a pty that the pane
identity check had just rejected.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* Revert "fix(terminal): detach the Korean input-source probe when its setting goes off (#13283)"
This reverts commit 5a84dbb564.
* Revert "fix(terminal): stop the Korean gate caching an unknown input source as negative (#13182)"
This reverts commit 434959965a.
* Revert "perf(terminal): gate the Korean input-source probe on its setting (#13181)"
This reverts commit 42fc5375e8.
* Revert "feat(terminal): Korean Won (₩) → backquote key mapping for Korean keyboards (#13104)"
This reverts commit 24003936a5.
* fix(agents): deliver grok launch drafts on its composer frame
Grok has no --prefill-style flag, so a launch draft (e.g. the issue URL of
a worktree created from a GitHub issue) always goes through Orca's
paste-after-ready path. That path used the default readiness signal: DECSET
2004 plus 1.5s of PTY silence. Grok shimmers its startup logo at ~12fps
until the session opens, so the quiet window never settled and the draft
fell through to the 8s hard timeout before it appeared in the composer.
Gate grok on its own composer glyph instead, anchored on the alternate-screen
switch rather than DECSET 2004: the shell that runs the launch command emits
2004 too, and its prompt may itself be the same glyph (starship, pure), so a
Codex-style anchor could paste into the shell. Grok keeps the quiet window
armed as a fallback because it renders differentially and paints the glyph
once, so a late-attaching scanner would otherwise wait out the hard timeout.
Measured against grok 1.0.0 driving the real scanner over a zsh -> grok PTY:
draft delivery moves from 8003ms to 689ms, with the URL landing unsubmitted
in the composer exactly as before.
* fix(agents): keep grok's quiet-window floor on DECSET 2004
The composer-glyph marker is anchored on the alternate-screen switch, but grok
can render inline (`--no-alt-screen`, `--minimal`, `[ui] screen_mode =
"minimal"`), where 1049h never arrives. Anchoring the quiet-window fallback
there too left those launches with no delivery path at all: readiness never
resolved, and the main-process caller drops the draft when it resolves null —
so the issue URL vanished instead of arriving late.
Give the signal two independent anchors: the marker still waits for the
alt-screen switch (so a starship/pure shell prompt can't trip it), while the
quiet window arms off DECSET 2004 exactly as the default signal does. Inline and
legacy-Windows-console launches keep their pre-existing timing; alt-screen
launches keep the fast marker path.
Verified on grok 1.0.0 over a real zsh -> grok PTY: alt-screen delivers at 687ms
via the marker, inline at 1949ms via the quiet window (the default signal
measures 1861ms on the same launch), URL landing unsubmitted in both. Adds a
recorded inline-mode trace fixture so the no-1049h path stays covered.
* fix(agents): revoke grok's alt-screen anchor when the screen is handed back
The composer-glyph anchor latched forever: once \x1b[?1049h had been seen, any
later `❯` counted as grok's composer. Two ways that pastes the launch draft into
the user's shell instead of into grok:
- grok enters the alternate screen and then dies before painting a composer;
the shell prompt that follows is `❯` under starship or pure.
- a pager or editor started from the user's shell rc enters and leaves the
alternate screen before grok is ever launched, arming the anchor against the
shell's own prompt.
Track the anchor in stream order instead of as a latch: \x1b[?1049l revokes it,
re-entering re-arms it, and a marker only counts inside a segment where the
anchor is actually held. The chunk is walked segment by segment so ordering
within a single PTY packet is honored, with a 7-char carry — one short of the
escape sequence — so a split sequence rejoins without re-walking scanned output
into a second transition. Signals with no `markerAnchorEnd` (codex, opencode,
the default) keep their existing latch semantics untouched.
Also makes the trace-replay test model the hard timeout: the real waiters settle
at 8s, so a marker landing after that is not a delivery time.
Two IME defects shipped past this suite because every assertion here was about
bytes reaching the PTY. A preedit written into a hidden overlay types blind and
still satisfies all of them.
Samples the composition overlay at each recorded compositionupdate and requires
it to be shown, so the existing recorded corpus now covers what the user sees
rather than only what the shell receives.
Co-authored-by: Orca <help@stably.ai>
Gating the prefetch call site stopped the probe from ever attaching, but not
from surviving. prefetchKoreanInputSource installs global focus/keydown/keyup
listeners behind an idempotent listenerAttached guard, and the only teardown
was test-only — so disabling the setting mid-session left the listeners live
and still spawning defaults export | plutil | plutil on keyboard activity.
Syncs in the effect body rather than its cleanup: cleanup also runs on tab
switches and on any dep-identity change, so disposing there would detach and
re-probe constantly.
Also replaces the initial-probe abort guard with an epoch. It compared window
identity, which is dead code in production where window is a singleton, so a
stop/start could let a sleeping probe loop wake and race the new one.
Co-authored-by: Orca <help@stably.ai>
* Revert "test(ime): restore coverage the composition-ownership change removed (#13168)"
This reverts commit 25a8c517e1.
* Revert "refactor(terminal): return IME composition ownership to xterm (#13128)"
This reverts commit 17b3dff3c4.
* test(ime): keep the architecture-neutral Korean trace coverage
The recorded IBus/fcitx5 and Windows MS-Korean traces from #13168 assert PTY
byte order, not composition ownership, so they still hold once the terminal
composition layer is restored. The mobile accessory-order test pinned the new
handleLiveInputChange signature and does not.
Co-authored-by: Orca <help@stably.ai>
* fix(terminal): keep the macOS Backslash bypass through the revert
The restored native-text forwarder only claims keys for input sources in its
hardcoded CJK allowlist, so third-party IMEs off that list (Qingg, #10896) still
get a raw backslash. #13128 added this bypass as a partial replacement; keep it
rather than trade the open issue back.
Scoped to the bare backslash key. The rest of shouldBypassXtermForMacNativeText
bypassed all unmodified non-ASCII text, which would race the restored forwarder.
Co-authored-by: Orca <help@stably.ai>
* fix(mobile): move the mirror-step ref write out of render
The restored hook assigned runMirrorStepRef during render, which is not
replay-safe — React can discard render work, so the mutation can leak from UI
that never commits. Its only read is inside the held-commit timer, which fires
long after commit, and the ref has a safe default, so an effect is soon enough.
Surfaced by the changed-lines React Doctor gate: the rule postdates this code,
so restoring the file re-introduced it as a new violation.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* fix tab search host routing and churn
* Fix open-tab search to resolve hosts from worktree when active host unkn
- Use worktree.hostId to resolve execution host instead of defaulting to LOCAL_EXECUTION_HOST_ID
- Correctly populate search results for remote-only worktrees when activeWorkspaceExecutionHostId is null
- Remove automatic focus of terminal tabs after search activation
* Prevent stale tab results when user keeps typing ahead of deferred searc
- useOpenTabSearch now returns {query, results} to track which query the results describe
- Gate tab results on query match so stale results don't appear on user's screen
- Add live region (role=status) for accessibility of tab switch error messages
- Distinguish missing-worktree from missing-page errors in browser page activation
- Improve host resolution to prefer active host when worktree and repo don't specify one
* Re-pin entry to deferred tab results that rank higher
Track whether selection auto-follows the top-ranked result or was
manually positioned. Re-pin entry to tabs when they rank higher,
but preserve manual selection.
* Consolidate browser focus requests and simplify selection state
- Extract requestBrowserFocus to handle queueing + event dispatch atomically
- Simplify omnibox selection tracking with single pinnedOptionId state
- Optimize host resolution in tab search to compute once per query
* Report dead browser workspaces correctly and fold dedupe case by host
Two readiness-checklist fixes for open-tab search:
- Browser page activation checked page/workspace before the worktree, but
deleting a worktree purges its browser workspaces and pages too, so a dead
workspace surfaced as "Browser page no longer exists". Check the worktree
first; routing already maps missing-worktree to the workspace wording.
- Editor-tab/file dedupe compared paths with separator normalization only, so
a Windows worktree offered both "Switch to tab" and "Open file" for the same
path in different case. Fold by the worktree path's syntax via the new
isCaseInsensitiveRuntimeRoot, keeping WSL, POSIX and SSH roots case-sensitive,
and add NFC so a macOS NFD listing matches an editor's composed path.
* Fix tab deduplication and resolve worktree host collisions
- Only editor tabs should suppress file entries; check contentType instead
of relying on path being empty for non-editor tabs.
- Add executionHostId to simulator search results to disambiguate when
the same worktree id exists on multiple execution hosts.
* Focus search inputs for immediate typing
- Autofocus inputs in AutomationListSearchField, SettingsSidebar, and WorktreeParentPickerPopover
- Only autofocus Settings search when opening directly, not via deep-link
- Use modal mode and explicit focus management in popover for proper restoration
- Forward CommandInput ref and add autofocus test coverage
* Restore focus when closing worktree parent picker popover
- Find the nearest focusable ancestor of the anchor row to restore focus
to instead of letting it drop on the detached input element
- Simplify focus assertion in AutomationListSearchField test to verify
actual focus behavior rather than autofocus attribute presence
* fix(ci): stop hourly prune from deleting the just-published release
Hourly prune sorted non-draft releases by createdAt, but nearly every
orca-hourly release shares one createdAt from bulk import. At the retain
cap, stable sort + reverse put the newest release past the window and
immediately deleted it with --cleanup-tag.
Sort by publishedAt (tagName as tie-break) and hard-skip the tag this run
just published so prune cannot self-delete.
* fix(ci): harden hourly prune protect without retain+1 drift
Review found that skipping only in the delete loop under-prunes when sort
is wrong, and excluding TAG before the retain slice would permanently keep
retain+1 releases. Force this run's tag to the front of the sorted list
before slicing so it always has a retain seat and oldest builds still prune.
Also gate prune on publish_live success and warn if TAG still appears stale.
* test(wsl): drive hook-relay reattach bench through real PTY spawn
Follow-up to #13139: stop calling ensureWslHookRelayForReattach from the
benchmark and instead reattach a surviving WSL PTY via main's
registerPtyHandlers path, so a missing or misplaced integration in pty.ts
fails the bench.
* refactor(bench): isolate reattach relay refresh measurement and verify s
- Wrap benchmark in try-finally for reliable cleanup
- Add jiti module graph duplication detection to catch missing pty.ts integration
- Track relay refreshes only during reattach phase to avoid false positives from earlier phases
- Disable agent-status hooks during PTY spawn (reattach path doesn't gate on them)
- Improve error messages and make cleanup safe with optional chaining
* fix(cmd-j): fit large palette on short screens and sync overflow copy
Cap dialog/list height against the viewport so the input, filter chips, and
footer stay visible after the larger Cmd+J shell, and align the English catalog
overflow hint with the multi-primary “scroll or keep typing” wording.
* fix(cmd-j): re-emit section headers for interleaved palette remainder ro
When both open tabs and worktrees overflow their first-screen slice, the
layout interleaves remainder rows. The trailing-section header renders
before the leading remainder, so unlabeled rows read as the wrong section.
Re-emit headers before each remainder with a distinct suffix for React keys.
Refactor type-alias matching into a reusable utility that prefers earliest
match position over declaration order.
* fix(cmd-j): stabilize palette memos and gate heavy builders when closed
P1-a: Move quickActionContext filtering into a useMemo with stable
primitive deps (activeView, activeWorktreeId, sshConnectionStates, etc.)
instead of calling buildQuickActionContext() inline every render — the
fresh object identity defeated the middleItems memo on every keystroke.
P1-b: Guard browserSortedWorktrees, browserPageEntries,
simulatorTabEntries, and workspaceTabEntries on paletteStatusInputsActive
so the always-mounted palette stops rebuilding full open-tab indexes
after every store write while closed.
P2-a: Derive sortedWorktrees from browserSortedWorktrees by filtering
out archived worktrees — both called sortWorktreesSmart with identical
deps, so one sort + filter replaces two.
P2-b: Pre-index agent metadata by tabId once per build via
buildAgentMetadataTabIndex, replacing the O(tabs × map entries) scan in
collectAgentMetadataForTerminal with O(1) lookups per tab.
P2-e: Assert in the interleaved test that rendered selectable row order
matches orderMultiPrimaryPaletteItems, keeping listEntries and the
layout function as a single source of truth.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* Allow automation deletion without SSH connection
Remove external source entries from the list and only show jobs.
This allows users to delete and manage automations even when the
remote host is not connected.
* Update test: list jobs from unavailable automation manager
Remove availability constraints (status, error, canManage) from test to
verify jobs can be listed regardless of manager connection state.
* fix(terminal): repair stale-dpr WebGL canvas backing on reveal and fit
When devicePixelRatio changes while a pane is hidden (window moved between
retina and non-retina displays, worktree then revealed), xterm's WebGL
renderer re-measures cell dimensions but its canvas keeps the old backing
store — the addon's device-pixel observer misses changes that land while the
element has no box. The browser composites the stale-scale bitmap into the
css box: half/double-size or smeared text until a manual resize. Reproduced
deterministically (2160px backing behind a 1080px css box at dpr 1) — this is
the mechanism behind the field reports of a normal pane going blurry after
switching back to a worktree.
A repair check now runs on every successful fit (via the fit-success hook)
and on the light tab-resume path (which never fits): when the canvas backing
diverges from cssWidth x devicePixelRatio beyond rounding tolerance, it
replays xterm's own dpr + resize path to rebuild the backing at the current
scale, then refreshes. Verified live: the same break sequence now self-heals
on reveal with no user action. A webgl-canvas-dpr-repair diagnostic records
each repair with the stale and expected backing widths.
* fix(terminal): keep dpr repair off the layout path
* fix(native-chat): resolve WSL Codex transcripts so Chat UI renders responses
Codex reports a guest Linux transcript path. On a Windows host `existsSync` resolved it against the current drive (`C:\home\...`) and discarded it, and the id-based fallback only searched host roots. `resolveSessionFilePath` returned null forever while the watcher reported `watching: true`, leaving Chat UI permanently empty.
Translate the guest path to its host-readable UNC twin, classifying **before** any `existsSync` probe on win32 so the `C:\home` false positive cannot fire. Adapted from #10639 with one required correction: it uses the **async cached** WSL seams (`listWslDistrosAsync` / `getWslHomeAsync`) rather than the `execFileSync` ones, which would stall the Electron main thread for up to 5s per tick of the resolve-poll loop on a cold distro.
Fixes#10326
Co-authored-by: Orca <help@stably.ai>
* fix(native-chat): stop the WSL transcript probe firing every poll tick
Three follow-ups from review of the WSL Codex transcript fix:
- The UNC translation was retried on every fast resolve-poll tick (measured 10
sync UNC stats per 100ms) because only a successful result was memoized. Gate
the retry to the slow fallback cadence.
- A non-empty WSL home list was cached for the process lifetime, so a distro
that was still booting during the first probe stayed excluded forever. Expire
both branches; getWslHomeAsync caches successes, so a refresh only re-spawns
wsl.exe for the distros that actually failed.
- codexSessionsDirs enumerated every distro's home eagerly, waking distros the
user left stopped even for native-Windows panes. Make the WSL roots a lazy
tier consulted only after the host's own Codex roots miss.
The resolve-poll suite became platform-dependent and only passed off Windows;
pin the platform and add explicit win32 coverage.
---------
Co-authored-by: Orca <help@stably.ai>
Co-authored-by: OrcaWin <alpha-eng@stably.ai>
`mapTerminalFilePath` derived the WSL distro only from the *shape* of `worktreePath`. A worktree on a native Windows drive whose project runs under the WSL runtime gets a shell whose paths are POSIX, so no distro was found, the path went verbatim to a Win32 stat probe, and the candidate was dropped — no underline, no tooltip, inert Ctrl+click.
Resolve the pane's distro from the execution runtime, falling back to the old worktree-shape derivation so existing behaviour is unchanged. Note the half of #8156 covered by merged #8215 (worktree on the WSL filesystem) was already fixed; this closes the remaining gap.
Fixes#8156
Co-authored-by: Orca <help@stably.ai>
On a fresh session the Won rewrite silently did not fire — real-hardware
capture on macOS 26.5.2 with 2-Set Korean read 3 of 3 Backquote presses as
e2 82 a9 at the PTY, and the feature only started working after the first
press or an input-source round trip.
Cause: the reader returns null for 'no signal' — the IPC is not exposed yet at
startup — as well as for a genuinely absent source, and refreshInputSourceId
committed that as isKoreanInputSourceId(null) === false. An unknown became a
confirmed negative that nothing retried, because a keystroke-triggered refresh
is async and cannot classify the press that triggered it.
Leaves the cache unknown on a null read and retries the startup probe with
bounded backoff, so the gate is warm before the first key. Bounded because each
probe spawns defaults export | plutil | plutil.
Co-authored-by: Orca <help@stably.ai>
`holdForeground` and `coalesceForeground` each cancelled the other's fallback timer on the Windows ConPTY DEC 2026 (synchronized output) path. A continuously repainting TUI such as Codex therefore left the foreground latch stuck open, so every later chunk was held instead of coalesced and output never reached the visible pane until the tab was refreshed.
Break the mutual cancellation and mirror the hidden path's scan on the foreground path, carrying a marker tail so a ConPTY-split DEC 2026 marker is still detected. Nothing was wrong with Flutter — it was simply a long-running command behind a repainting TUI.
Fixes#8754
Co-authored-by: Orca <help@stably.ai>
prefetchKoreanInputSource ran under a bare isMac check, so every macOS user
paid for it. Each refresh shells out to `defaults export | plutil | plutil` —
four processes in the main process — and input-source toggle keys pass
force: true, so one Caps Lock press costs two probes. terminalKoreanWonToBackquote
defaults to false.
Threads the setting through KeyboardHandlersDeps and into the effect's
dependencies, so enabling it mid-session still warms the cache before the
first Backquote.
Co-authored-by: Orca <help@stably.ai>
* feat(terminal): map Korean Won (₩) key to backquote on macOS
Korean keyboard users type markdown code fences and shell backquotes on the key that US layouts reserve for ` — 두벌식 and 세벌식 390 put ₩ there, 세벌식 최종 puts *, so there was no way to type a backquote without switching layouts.
Add a Mac-only terminal setting, "Korean Won (₩) to Backquote (`)", that rewrites the plain backquote-position keystroke to backquote while a Korean input source is active. It sits in Terminal → Advanced, right below the existing JIS Yen (¥) to Backslash (\) mapping.
The rewrite keys on the keystroke position alone — no character or layout-variant knowledge — and follows the live input source through the existing MacNativeTextInputSourceTracker, which refreshes on focus and keyboard activity (Caps Lock / 한영 input switches never blur the window). Modified chords and IME-composed events pass through untouched.
Covered by resolver and input-source tracker unit tests; verified manually in the GUI.
* docs(terminal): clarify Korean Won mapping scope and add docstrings
State in the setting copy that the backquote rewrite applies only while a Korean input source is active, and add JSDoc to the Korean Won resolver exports (addresses CodeRabbit pre-merge docstring coverage and copy-clarity findings).
* test(terminal): pin the recorded Korean commit-before-newline order (STA-3132)
Recorded first-party on Windows 11 + Microsoft Korean (HKL 0412) against the
defect-era v1.4.164 build, with bytes read on the far side of the PTY: the
terminal received ea b0 80 0d, the syllable strictly before the CR.
The capture did not reproduce the suspected deferred-newline inversion. That
route needed a session end carrying dataPendingReconciliation, which plain
compose-then-Enter cannot produce because the IME finalizes first and the
newline is never held; back-to-back arms at 25/60/120 ms did not reach it
either. The test therefore pins the ordering rather than discriminating a fix.
Co-authored-by: Orca <help@stably.ai>
* test(terminal): restore Hangul back-to-back flush coverage deleted with the composition layer
#12278 fixed a Hangul syllable that was not flushed before the next
composition began — the force-end path, and the one that leaves stale glyphs
behind. Returning composition ownership to xterm deleted both that patch and
its test, so nothing guarded the behavior any more.
Replays the recorded back-to-back arms (25/60/120 ms, read as 가\r나 at the
PTY) against a real xterm Terminal. It passes on main: stock xterm flushes the
committed syllable natively, so the removal was safe rather than a silent
regression.
Co-authored-by: Orca <help@stably.ai>
* test(mobile): pin accessory-byte ordering behind a Hangul commit
Returning composition ownership to xterm deleted the accessory-input commit
tests along with the hook they targeted, but the guarantee they protected is
user-visible and still applies: an accessory-bar keystroke must not overtake
the syllable being committed, and must be suppressed when that commit fails.
Drives the current hook with an Android composing-region trace rather than
reconstructing the deleted coordinator.
Co-authored-by: Orca <help@stably.ai>
* test(terminal): replay recorded IBus and fcitx5 Hangul traces offline
Commits interleaved with ASCII (한abc글) are the Linux IME gesture users report
on, and its failure modes are a lost syllable and a doubled one. That gesture
was only covered by tests/e2e/terminal-linux-ime-native.spec.ts, which needs a
Linux host running a real input framework.
Fixtures are the recorded captures from the sealed linux-final evidence run,
replayed against a real xterm Terminal: exact onData, exactly-once counts across
five repetitions, and the PTY bytes the recorded run actually received.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* fix(browser): reject Chromium product versions in cookie-import UA
Do not persist Chrome/1.x User-Agents when a fork (e.g. Arc) reports its
product CFBundleShortVersionString. Only engine-scale majors (>=70) are
advertised; otherwise fall back to Electron's default UA.
Preserves Chrome-shaped UAs for real Chromium engine versions.
Fixes#12726
* fix(browser): reject malformed Chromium version tokens in UA
Validate every numeric component before advertising Chrome/… so values
like 70.not-a-version fall back instead of polluting the UA. Also build
macOS app paths with path.join per coding guidelines.
* fix(browser): drop already-persisted Chrome/1.x UAs on session restore
The version gate stops new fork imports from writing Chrome/1.x, but profiles imported before it keep the broken UA in browser-session-meta.json and replay it on every launch, so affected users stay blocked with no in-app recovery.
Move the gate into browser-session-ua (the module that owns UA shape, and the one both callers can import without a cycle) and drop an unadvertisable persisted UA during restore so the profile falls back to Orca's own engine UA.
---------
Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com>