Commit Graph

545 Commits

Author SHA1 Message Date
Jinjing 3b1017c4fb
Add nightly cut (#13410)
* Add daily macOS dev build release channel

Publish once-daily signed macOS builds from main at a dedicated cadence,
separate from hourly (too noisy) and release branches (too infrequent).
Builds are notarized and installable via the updater, but unvetted —
published to stablyai/orca-daily rather than the main repo to avoid
evicting stable/RC entries from the releases feed.

* fix lint

* fix commit

* Add third token mint to daily macOS build workflow

The upload step's 2x45m retry budget can outlive the one-hour token, so a third
is minted after it for verify and cleanup operations. Release notes are moved to
a file to ensure consistency between draft creation and publish. Daily channel
description updated with specific UTC release time.
2026-08-09 19:01:35 -07:00
Jinjing b075a95b06
Strip liveness gate from AI Vault session delete (#13279)
* Strip liveness gate from AI Vault session delete

Delete now requires only path validation + user confirmation — no process
roster, no liveness check, no quiescence, no ownership ledger.

Co-authored-by: Orca <help@stably.ai>

* Remove obsolete AI Vault liveness delete reliability gate

Session delete no longer checks process liveness, so drop the
manifest entry that still referenced the deleted test files.

* minor fix

---------

Co-authored-by: Orca <help@stably.ai>
2026-08-09 18:40:01 -07:00
Brennan Benson 5df2ddbc9c
perf(ai-vault): isolate tab title resolution (#13377)
* perf(ai-vault): isolate tab title resolution

* fix(ai-vault): preserve background scan caches

* fix(ai-vault): resolve nested worker from chunks
2026-08-09 16:45:52 -07:00
Brennan Benson 774bbc788f
fix(terminal): preserve OSC 8 links across cold parking (#13382)
* fix(terminal): preserve OSC 8 links across cold parking

* test(terminal): make OSC 8 e2e cross-platform

* test(terminal): focus OSC restore e2e on activation
2026-08-09 16:36:12 -07:00
Neil cb9aa12fff
test(terminal): pin the macOS key-binding substitution against #11170 (#13315)
* fix(terminal): show a preedit the IME resumes without a compositionstart

Typing 2-Set Korean shows committed syllables but not the in-progress jamo, so
the user composes each syllable blind. Long-standing hole in the vendored
terminal library, not a regression: the same test fails identically against the
bundle this branch starts from.

The `.active` class that CSS keys `display: block` off is added only in
`compositionstart` and dropped in `_finalizeComposition`. Some IMEs (observed on
Windows/WSL Korean) resume a composition with a bare `compositionupdate` and no
second `compositionstart`, by which point `compositionend` has already hidden the
overlay, so the resumed preedit is written into a hidden element and never
positioned. `updateCompositionElements` also early-returned on `!_isComposing`,
so it would not lay the overlay out either.

Re-show the overlay on an update that carries data, and key the layout guard on
the shown overlay instead. `_isComposing` is deliberately left alone, so no
commit bookkeeping changes and `onData` stays byte-identical. The two guards are
equivalent on every pre-existing path: `compositionstart` sets both,
`_finalizeComposition` clears both.

The bundle hunks are the same two edits applied to the shipped minified output;
the sourcemaps are carried through unchanged.

* test(terminal): prove the resumed-preedit fix against a recorded Windows capture

The synthetic test pins the shape; this replays events a real Microsoft Korean
IME emitted on Windows/WSL. The capture holds three compositionupdates that
resume a composition with no second compositionstart — the exact ordering that
wrote the preedit into a hidden overlay.

Without the fix all three report shown:false; with it all three are visible.
Fixture derived from the sealed 11919-windows-wsl-current capture, which is
read-only and unmodified.

Co-authored-by: Orca <help@stably.ai>

* test(terminal): stop the recorded Hangul fixture pinning a derivation artifact

The capture logs each event twice — a dispatch record and a batched next-frame
re-log. Deriving from both replayed every event twice, which made three
compositionupdates appear to land after a session had ended. Filtered to
dispatch records the capture holds zero resumes and 11 balanced sessions, so
the previous toHaveLength(3) was pinning an artifact of the derivation.

Re-scoped to what the capture does prove: the preedit stays visible across all
37 real updates. Verified by reverting the patch that this passes either way,
so it is coverage and the synthetic test remains the discriminator. Both facts
are now stated in the file.

Co-authored-by: Orca <help@stably.ai>

* fix(terminal): restore the preedit visibility patch onto its own branch

The previous commit accidentally reverted it: checking main's patch and lockfile
into the worktree to test whether a test discriminates also stages them, so the
commit that followed swept them up.

Co-authored-by: Orca <help@stably.ai>

* fix(terminal): claim printable keydowns structurally so committed text survives

Co-authored-by: Orca <help@stably.ai>

* chore(reliability-gates): retarget the IME forwarding gate after the allowlist removal

The gate listed terminal-ime-input-source.test.ts, which went with the
input-source allowlist. Points at the substituted-text commit test instead,
which covers what the gate is actually protecting: text committed outside a
composition session reaching the pty exactly once.

Co-authored-by: Orca <help@stably.ai>

* docs(terminal): record why withholding a claimed keydown needs no timer

The predicate withholds a keydown's byte until the commit arrives, so a key the
IME eats without committing would be dropped. Measured across the recorded
corpus that case does not occur, and the browser marks IME-owned presses on the
keydown itself. Both facts belong next to the predicate rather than only in a
handoff note, since the obvious fix for the imagined gap is a timer, and a timer
here once wrote a newline the user never typed.

Co-authored-by: Orca <help@stably.ai>

* test(terminal): pin the kitty all-keys-as-escape-codes hole explicitly

Flag 8 asks for every printable key as an escape code; this path sends the
committed text raw instead. That is a deliberate trade, not an oversight, but it
was untested — the suite only covered the disambiguate flag. Pinning it makes
the choice visible and records the gate to use if it ever needs closing.

Co-authored-by: Orca <help@stably.ai>

* fix(terminal): keep the kitty key-release report for presses that reached the pty

Claiming the keyup unconditionally suppressed xterm's release report. That was
sized for the old design, which claimed only a short punctuation list; the
structural claim takes every printable keydown, so on macOS an app that
negotiated kitty report_event_types stopped seeing releases for ordinary typing
and would treat every printable key as held down.

Suppress the release only when the press put nothing on the wire — swallowed by
the input source, or owned by a composition transaction. xterm emits nothing
from keyup unless kitty report_event_types (or win32 input mode) is on, so
letting it through is inert everywhere else.

Co-authored-by: Orca <help@stably.ai>

* test(terminal): pin the macOS key-binding substitution against #11170

An OS key-binding remap of the character a Korean layout puts on Backquote
is honoured everywhere on macOS except the terminal, which sent the raw
layout character to the PTY. The substitution is applied inside the system
text input path, so it exists only on keypress.charCode and the input
event's data; the keydown still carries the layout character. Nothing needs
to parse the binding file - Chromium has already applied it by the time
`input` fires.

The reported build sent the raw character. A later punctuation table
happened to list that one character, which closed the issue by enumeration
rather than by design, and the structural claim removes the table entirely.
Without a test the fix could regress silently on a change that never
mentions the issue.

Replays the reporter's captured event shape and pairs it with the same
physical key carrying no substitution, so a fix that rewrote the Backquote
position unconditionally would fail. Discrimination checked by mutation:
suppressing the structural claim, and separately removing the single table
character on a pre-rewrite tree, each make the replay send the raw layout
character while both negatives stay green.

Co-authored-by: Orca <help@stably.ai>

* test(terminal): cover the other Korean layout on the remapped key

Korean layouts disagree about what the backquote position produces: two of
them give the currency sign the issue reports, one gives an asterisk. One
key-binding entry has to survive either, but honouring the substitution by
listing characters covers only the ones someone remembered to list - which
is why the reported character worked and this one did not.

This arm discriminates without a mutation: it fails on the pre-rewrite tree
and passes on the structural claim. The harness supplies no input-source
classification, modelling a source the older design did not recognise,
including the window before its async probe resolves. With the source
recognised the older design claimed all ASCII punctuation and covered this
too, so the gap was real but conditional; the header says so rather than
letting the failure read as unconditional.

Co-authored-by: Orca <help@stably.ai>

* test(terminal): drop the Won-setting arm from the keybinding-dict replay

The Won-to-backquote feature was reverted, so the module this replay imported
no longer exists. The #11170 coverage is unaffected: the remaining arms pin the
substitution itself, which never depended on that setting.

Co-authored-by: Orca <help@stably.ai>

* test(terminal): correct the fixture's provenance count

The header said two derived cases when there are four, and counted the
second layout arm as a negative when it is a positive. Each case already
carries its own recorded flag and note; this stops the summary contradicting
them, which matters in the one field whose whole job is provenance.

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
2026-08-09 16:03:52 -07:00
makoto-developer bf406720a0
fix(i18n): restore the Japanese renderings the brand revert left in Latin (#12934)
* fix(i18n): restore the Japanese renderings the brand revert left in Latin

Before #12113 landed the canonical-rendering guard, the brand-mistranslation
revert treated ターミナル/エージェント/コミット/リポジトリ as machine-translation errors
and rewrote them back to English on every repair run. The guard stops new
damage, but ~700 values still carry it, so the Japanese UI reads
"この terminal を閉じると、agent の現在の作業が停止します。"

This heals what the catalog already holds and closes the defects the same
pipeline introduced elsewhere:

- Relocalize the four generic terms inside Japanese sentences. Both sides are
  anchored on an adjacent Japanese character and reject a `-`, word character
  or `.` neighbour, so `--agent`, agents.md and "Agent SDK" keep their spelling,
  and the 和欧間スペース survives in front of an interpolation.
- Preserve style blocks and command/identifier values in English. MT had
  rewritten a CSS selector to [データスラッシュメニュー], `background:` to `背景:` and a
  keyframe name to ブラウザフラッシュ, and had turned `pnpm install` into
  `pnpmインストール` and a toast dedup id into 陳腐なエージェント行. The same values are
  restored in ko, zh and es.
- Drop the phrase fixes that stripped ~してください from validation messages,
  which left 30 prompts ending in a bare noun.
- Settle terminology and typography: 紛争→競合, 資格情報→認証情報, 未知→不明,
  プロフィール→プロファイル, the full-width ellipsis, no separator space inside
  compound katakana, and one long-vowel form per word.
- Fix literal-translation errors (ナメクジ for slug, ミスター for MR, ランニング for
  Running, 高い/中くらい for priority labels) and complete the truncated Kimi
  status-bar description.
- Translate 570 values that were still English, 485 of them fragments the
  catalog had left untouched and 85 newly added strings.

The catalog is regenerated with `repair-locale-catalog.mjs` and the run is
idempotent: a second pass reports 0 leaf updates.

* fix(i18n): keep the leading space in concatenated Japanese fragments

* fix(i18n): close the self-review findings in the Japanese repair rules

Seven defects the relocalization pass introduced or left behind:

- `git commit` and `orca terminal` are two-word commands, and the position
  guards only looked at the character before the term, so the second word was
  katakana-ized ("git コミットが失敗したとき"). A command-head lookbehind covers
  git/gh/glab/orca/npm/pnpm/npx/yarn/docker/kubectl. A following Latin word now
  also blocks the rule, so "Agent SDK" keeps its spelling as the comment claims.
- `on: 'オン'` matched every "on" in the catalog, including the preposition in
  the external-automation delete confirmation, which rendered as
  "外部ソース オン myhost". Moved to a new ja key-override module so the toggle
  states and the preposition can differ; the module keeps
  locale-key-overrides.mjs under max-lines.
- The phrase fixes write Cookie and fast-forward back in Latin, but neither was
  in CJK_LATIN_SPACED_TERMS, so the 和欧間スペース was missing in five values.
- Three overrides spelled a half-width `...` that the ellipsis phrase fix
  rewrites anyway, so the comment described the opposite of the behavior.
- Two descriptions render as [text] <code> [text]. The Japanese closed the
  sentence with 。 and dropped the "such as" / "like" hand-off, leaving the code
  span outside the sentence in the Git and quick-command panes.
- `' vs {{value0}}'` and `' · {{value0}} external'` were missed by the previous
  leading-space fix, so "3 変更されたファイルvs main" rendered without a gap.

Adds three regression cases: two-word commands, the Latin-only label, and the
spacing for terms the phrase fixes reintroduce.

* fix(i18n): correct the Japanese an external review flagged

73 findings from a ChatGPT review of the full changed-value list, each
reproduced against the shipped catalog before being fixed:

- Two commands were translated into text that does not run: `pnpm playwright
  test` became `pnpm プレイライトテスト` and `gh auth login` became `GH 認証ログイン`.
  Both are pinned to English. A third value corrupted an identifier,
  rendering `packages/web` as `package/web`.
- Two descriptions carried a stale translation with no relation to their
  English source, and one had another row's text entirely.
- Syntax was misread in six values: `Command line Orca runs when…` read as
  "the command line runs", `Let programs … copy` as "copy the programs",
  `Dim files matched by .gitignore` kept `Dim` as a noun, and
  `powers live quota reads` became 強化.
- Wording that changed the warning: `diffs may miss recent commits` read as
  the commits being lost, `before merging is unblocked` as un-merging, and
  `the newer disk content` as new content.
- `host` was rendered as サーバー in eight values even though it covers SSH
  hosts, `worktrees` as ワークスペース, and `on this host` as リモート.
- Git vocabulary translated to its everyday sense: `upstream`→上流,
  `staged changes`→段階的な変更; and identifiers `lan`/`deploy` were localized.
- Instructions to the user had lost their imperative (…をインストールします),
  three validation messages still ended in a bare noun, and two completion
  notices read as future tense.
- Assorted breakage: 窓 for a desktop window, 分割分割線, ターミナルパネル for
  Terminal Panes, オプション for the macOS Option key, Herme for Hermes,
  and a reversed noun phrase in the repo-icon import error.

The `Open` action needed a key override: bare "Open" is the PR/issue state in
16 of 18 places, and only the browser download row and the checks panel use it
as the verb, matching what ko/zh/es already do.

Three fixes had to be reworded rather than written literally: the existing
新しい→新規 and 実験的→実験的機能 phrase fixes run after value overrides and
turned 新しい名前 into 新規名前 and 実験的な into 実験的機能な.

Not fixed: `{{value0}} site{{value1}} connected` still shows the plural-suffix
placeholder, which needs the code change already listed in the PR notes.

* fix(i18n): sweep the whole catalog for the defect classes the reviews found

The external review covered a sample. This runs each of its finding classes as
a detector over all 11,857 values and fixes what they turn up, as durable
pipeline rules rather than one-off value edits where the class recurs:

- host は サーバー ではない. Orca's "host" covers SSH hosts and this computer,
  so a phrase fix rewrites サーバー to ホスト whenever the English says host and
  does not also say server, where the two are deliberately distinct (15 values).
- worktree joins the guarded generic-term list, so the seven values still
  reading "worktree を削除" match the 205 that already say ワークツリー.
- Git vocabulary and brands restored from their everyday sense: 上流→upstream,
  起源→origin, 段階的な変更→ステージ済みの変更, エルメス/ヘルメス→Hermes,
  パワーシェル→PowerShell, アヒルアヒル→DuckDuckGo. All five terms are added to
  CJK_LATIN_SPACED_TERMS so the restored Latin keeps its 和欧間スペース.
- 14 more code values pinned to English: Tailwind class strings
  (size-4 text-muted-foreground → サイズ 4 テキストミュート前景), git refs
  (origin/main → 原点/メイン), sample hosts (example.com → 例.com) and spec
  fixtures (dashboard.spec.ts → ダッシュボードの仕様).
- 12 instructions regained their predicate (…を選択。 → …を選択してください。),
  and 窓のぼかし, macOSのオプションキー, 中くらいのセクション見出し are corrected.

Checked and deliberately not changed: toast notifications that end in 〜しました
(237) read correctly for a completion notice, and setting descriptions ending in
〜します (200) describe what the setting does rather than instructing the user.
Sound preset names stay katakana, matching the rest of that list.

Adds three regression cases covering the host rule and its server exception,
the Git/brand restorations, and the newly pinned class strings and refs.

* fix(i18n): keep Agent in Latin in the Japanese catalog

Japanese developer UIs conventionally leave Agent unlocalized — it names Orca's
own concept rather than the everyday word — so the ja catalog now writes it in
Latin and only normalizes the case, so no sentence mixes "agent" and "Agent".
The 和欧間スペース comes from the existing spaced-term list.

This is the one term where ja diverges from locale-generic-ui-terms.mjs, which
lists エージェント as the expected rendering; ターミナル, コミット and リポジトリ
follow it exactly. Three test expectations are updated to match, and the PR
description flags the divergence so a maintainer can ask for it to be reverted
— it is a single rule in locale-ja-phrase-fixes.mjs.

The guards are unchanged, so `--agent`, agents.md, `orca agent` and "Agent SDK"
keep their spelling.

* fix(i18n): preserve selectors with no declaration block, and sharpen progress labels

CodeRabbit was right that `STYLE_BLOCK` only matched a selector when it carried
a declaration block or an attribute selector. `div.pricing-grid >
div.card.starter:nth-of-type(1) > a.cta` was unprotected and only survived
because MT happened to leave it alone.

A value now also counts as style when every whitespace token is selector-shaped
and at least two carry a class, id, pseudo or attribute. The first attempt at
that threshold counted a sentence-final period as a selector join and froze 259
ordinary two-sentence strings in English; the marker must now be followed by a
letter, so `Show live workspace ports. Click it for …` stays translated. Both
directions are pinned by tests.

The predicate moved to locale-style-values.mjs to keep locale-translation-policy
under max-lines.

A DeepL cross-check of the whole changed-value list surfaced six more:

- Progress labels had lost their 〜中: `Creating...` read 作成…, `Reopening...`
  read 再開…, `Thinking…` read 考え…
- `Hide from sidebar` / `Show in sidebar` carried a stale 左サイドバーから Orca
  Mobileを削除 in one of six places, with the 和欧間スペース missing too
- `Recent or tab strip.` was 最近のまたはタブストリップ。, which is not Japanese

Everything else DeepL flagged was this PR's settled terminology (Agent, Issue,
ホスト, 競合, fast-forward) or a fragment where DeepL had no surrounding context.

* fix(i18n): treat proper nouns as brands, and cut the comment noise

Review feedback, all verified against the catalog:

- Hermes, PowerShell, Mermaid, Claude Code and VS Code are proper nouns, so
  they belong in BRAND_MISTRANSLATIONS with the other product names, not in the
  ja phrase fixes. Claude コードセッション, マーメイドダイアグラム and VS コードで開く
  were unfixed until now because nothing covered them.
- Sweeping every proper noun against the catalog found more the same way:
  Orca IDE rendered as OrcaIDE, and Git had no 和欧間スペース in eight values,
  because neither term was in CJK_LATIN_SPACED_TERMS.
- Settings-search keywords are lowercase, so the brand revert (case-sensitive)
  cannot reach them. windows read 窓, gitignore read ギティ無視, component read
  成分 (the chemistry sense), compose read 作曲する, and neovim/hermes/powershell
  were transliterated. Pinned by value.
- The override sources still spelled エージェント in 34 places even though the
  shipped value is Agent, so the file no longer said what it produced. The
  代理人 rule also ran after the Agent rule, so a future MT 代理人 would have
  stayed katakana; it now maps straight to Agent.
- Two of my own overrides had no matching English source left and were dead.

Comment volume is cut from 80 added lines to 25. The locale modules carry 2-8
comment lines each, and this PR was running an order of magnitude over that;
what is left is one line per genuinely non-obvious constraint.

BRAND_MISTRANSLATIONS moves to its own module to keep locale-translation-policy
under max-lines.

* fix(i18n): give every English string one Japanese rendering

215 English sources had two or more Japanese forms in the catalog, so the same
button read 削除 in one place and 削除する in another. Several of the variants
were also wrong outright: Hide read 隠れる, Sort read 選別, Run read 走る, and
"Don't ask again" read 二度と聞かないでください.

Picks follow the catalog majority — action labels drop する, completion notices
use 〜しました, status labels are 体言, and デフォルト / フィルター / スコープ /
ディストリビューション / 並べ替え win their pairs. A value override is keyed on the
English string, so one entry makes every occurrence agree.

Three key overrides contradicted the value they now share and were realigned;
seven pairs remain and are deliberate, where the same English is a different
thing per call site (Cursor the product vs the caret, Open the PR state vs the
action, Forward the port vs the browser button).

Grab mode picks a page element and hands it to the AI, but read 掴む as a button
and グラブモード in the web-client notice while the rest of the feature said 取得.

Also from CodeRabbit: a single dotted, colon or bracketed token — button.primary,
a:hover, wsl.exe, localhost:3000 — is code whether it names a selector, a file
or a host, so it is preserved too. That caught localhost:3000 reading
ローカルホスト:3000. And an override still spelled `Agent 、` with a space before
the Japanese comma.

The unified map lives in locale-ja-unified-values.mjs to keep the override file
under max-lines.

* fix(i18n): scope the catalog change to ja, and drop the zh-only Terminal form

The ja brand list carried 端子 as a Terminal mistranslation, but 端子 is the
zh rendering and never appears in ja — the Japanese one is 端末, which the
phrase fixes already rewrite to ターミナル. Listing 端末 here instead would be
wrong in the other direction, because this list reverts to Latin. The round-5
expectation moves to 端末 → ターミナル, which is behaviour that can actually occur.

The ko/zh/es identifier restorations are pulled back out; they are real bugs
(pr-view read PR视图, pnpm install read pnpm 설치) but they belong in their own
PR rather than a Japanese one. One zh line has to stay: without it,
verify-localization-catalog refuses the new stale-agent-row-{{value0}} entry
because repair would rewrite the Chinese text to English. That value is a toast
dedup id, not copy.

* chore(i18n): regenerate the Japanese catalog on the current base

The branch point moved forward 68 commits, which added 45 keys to en.json. The
catalog is rebuilt from that base so the repair run stays idempotent, and the
one string the new keys left in English is translated.

* fix(i18n): preserve the code strings rendered inside <code> and font-mono

@smwbev scanned by call-site context rather than value shape — a translate()
that renders inside <code> or a font-mono element is code — and found values
the shape-based list missed. Reproduced against the catalog and fixed here:

- {prompt} read {プロンプト}. It is the substitution token for the commit-message
  prompt template, so a translated one never substitutes.
- /goal read /ゴール, which is not a slash command.
- npm run dev read npm 実行開発, in the same font-mono placeholder role as
  pnpm install.
- nbformat read nbフォーマット.

upstream and upstream/main were already covered. The remaining values in the
scan break in zh rather than ja, but the entries are locale-agnostic, so
orca.yaml, LIN-329, GH #1799 and orca · zsh are pinned here too and #13124
restores the Chinese catalog.

* fix(i18n): repair Japanese translations of code, CLI, and URLs

Code samples, CLI arguments, URLs, and template variables must remain
executable. Adds to NEVER_TRANSLATE_VALUES to prevent future mistakes,
fixes ja.json mistranslations, and adds regression tests.

---------

Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
2026-08-09 15:40:48 -07:00
Neil 01bcc8dca2
fix(terminal): claim printable keydowns structurally so committed IME text survives (#13288)
* fix(terminal): show a preedit the IME resumes without a compositionstart

Typing 2-Set Korean shows committed syllables but not the in-progress jamo, so
the user composes each syllable blind. Long-standing hole in the vendored
terminal library, not a regression: the same test fails identically against the
bundle this branch starts from.

The `.active` class that CSS keys `display: block` off is added only in
`compositionstart` and dropped in `_finalizeComposition`. Some IMEs (observed on
Windows/WSL Korean) resume a composition with a bare `compositionupdate` and no
second `compositionstart`, by which point `compositionend` has already hidden the
overlay, so the resumed preedit is written into a hidden element and never
positioned. `updateCompositionElements` also early-returned on `!_isComposing`,
so it would not lay the overlay out either.

Re-show the overlay on an update that carries data, and key the layout guard on
the shown overlay instead. `_isComposing` is deliberately left alone, so no
commit bookkeeping changes and `onData` stays byte-identical. The two guards are
equivalent on every pre-existing path: `compositionstart` sets both,
`_finalizeComposition` clears both.

The bundle hunks are the same two edits applied to the shipped minified output;
the sourcemaps are carried through unchanged.

* test(terminal): prove the resumed-preedit fix against a recorded Windows capture

The synthetic test pins the shape; this replays events a real Microsoft Korean
IME emitted on Windows/WSL. The capture holds three compositionupdates that
resume a composition with no second compositionstart — the exact ordering that
wrote the preedit into a hidden overlay.

Without the fix all three report shown:false; with it all three are visible.
Fixture derived from the sealed 11919-windows-wsl-current capture, which is
read-only and unmodified.

Co-authored-by: Orca <help@stably.ai>

* test(terminal): stop the recorded Hangul fixture pinning a derivation artifact

The capture logs each event twice — a dispatch record and a batched next-frame
re-log. Deriving from both replayed every event twice, which made three
compositionupdates appear to land after a session had ended. Filtered to
dispatch records the capture holds zero resumes and 11 balanced sessions, so
the previous toHaveLength(3) was pinning an artifact of the derivation.

Re-scoped to what the capture does prove: the preedit stays visible across all
37 real updates. Verified by reverting the patch that this passes either way,
so it is coverage and the synthetic test remains the discriminator. Both facts
are now stated in the file.

Co-authored-by: Orca <help@stably.ai>

* fix(terminal): restore the preedit visibility patch onto its own branch

The previous commit accidentally reverted it: checking main's patch and lockfile
into the worktree to test whether a test discriminates also stages them, so the
commit that followed swept them up.

Co-authored-by: Orca <help@stably.ai>

* fix(terminal): claim printable keydowns structurally so committed text survives

Co-authored-by: Orca <help@stably.ai>

* chore(reliability-gates): retarget the IME forwarding gate after the allowlist removal

The gate listed terminal-ime-input-source.test.ts, which went with the
input-source allowlist. Points at the substituted-text commit test instead,
which covers what the gate is actually protecting: text committed outside a
composition session reaching the pty exactly once.

Co-authored-by: Orca <help@stably.ai>

* docs(terminal): record why withholding a claimed keydown needs no timer

The predicate withholds a keydown's byte until the commit arrives, so a key the
IME eats without committing would be dropped. Measured across the recorded
corpus that case does not occur, and the browser marks IME-owned presses on the
keydown itself. Both facts belong next to the predicate rather than only in a
handoff note, since the obvious fix for the imagined gap is a timer, and a timer
here once wrote a newline the user never typed.

Co-authored-by: Orca <help@stably.ai>

* test(terminal): pin the kitty all-keys-as-escape-codes hole explicitly

Flag 8 asks for every printable key as an escape code; this path sends the
committed text raw instead. That is a deliberate trade, not an oversight, but it
was untested — the suite only covered the disambiguate flag. Pinning it makes
the choice visible and records the gate to use if it ever needs closing.

Co-authored-by: Orca <help@stably.ai>

* fix(terminal): keep the kitty key-release report for presses that reached the pty

Claiming the keyup unconditionally suppressed xterm's release report. That was
sized for the old design, which claimed only a short punctuation list; the
structural claim takes every printable keydown, so on macOS an app that
negotiated kitty report_event_types stopped seeing releases for ordinary typing
and would treat every printable key as held down.

Suppress the release only when the press put nothing on the wire — swallowed by
the input source, or owned by a composition transaction. xterm emits nothing
from keyup unless kitty report_event_types (or win32 input mode) is on, so
letting it through is inert everywhere else.

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
2026-08-09 15:37:11 -07:00
Neil a47b9d1167
Show a preedit the IME resumes without a compositionstart (#13284)
* fix(terminal): show a preedit the IME resumes without a compositionstart

Typing 2-Set Korean shows committed syllables but not the in-progress jamo, so
the user composes each syllable blind. Long-standing hole in the vendored
terminal library, not a regression: the same test fails identically against the
bundle this branch starts from.

The `.active` class that CSS keys `display: block` off is added only in
`compositionstart` and dropped in `_finalizeComposition`. Some IMEs (observed on
Windows/WSL Korean) resume a composition with a bare `compositionupdate` and no
second `compositionstart`, by which point `compositionend` has already hidden the
overlay, so the resumed preedit is written into a hidden element and never
positioned. `updateCompositionElements` also early-returned on `!_isComposing`,
so it would not lay the overlay out either.

Re-show the overlay on an update that carries data, and key the layout guard on
the shown overlay instead. `_isComposing` is deliberately left alone, so no
commit bookkeeping changes and `onData` stays byte-identical. The two guards are
equivalent on every pre-existing path: `compositionstart` sets both,
`_finalizeComposition` clears both.

The bundle hunks are the same two edits applied to the shipped minified output;
the sourcemaps are carried through unchanged.

* test(terminal): prove the resumed-preedit fix against a recorded Windows capture

The synthetic test pins the shape; this replays events a real Microsoft Korean
IME emitted on Windows/WSL. The capture holds three compositionupdates that
resume a composition with no second compositionstart — the exact ordering that
wrote the preedit into a hidden overlay.

Without the fix all three report shown:false; with it all three are visible.
Fixture derived from the sealed 11919-windows-wsl-current capture, which is
read-only and unmodified.

Co-authored-by: Orca <help@stably.ai>

* test(terminal): stop the recorded Hangul fixture pinning a derivation artifact

The capture logs each event twice — a dispatch record and a batched next-frame
re-log. Deriving from both replayed every event twice, which made three
compositionupdates appear to land after a session had ended. Filtered to
dispatch records the capture holds zero resumes and 11 balanced sessions, so
the previous toHaveLength(3) was pinning an artifact of the derivation.

Re-scoped to what the capture does prove: the preedit stays visible across all
37 real updates. Verified by reverting the patch that this passes either way,
so it is coverage and the synthetic test remains the discriminator. Both facts
are now stated in the file.

Co-authored-by: Orca <help@stably.ai>

* fix(terminal): restore the preedit visibility patch onto its own branch

The previous commit accidentally reverted it: checking main's patch and lockfile
into the worktree to test whether a test discriminates also stages them, so the
commit that followed swept them up.

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
2026-08-09 15:30:26 -07:00
Jinwoo Hong 45c1cb979a
fix(orchestration): release context-only dispatches (#13376)
* fix(orchestration): release context-only dispatches

Refs #13005

* test(orchestration): align PTY readiness timeout

---------

Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com>
2026-08-09 14:49:36 -07:00
OrcaWin 69ca0154b6
fix(git): bypass WSL login shells for status reads (#13207) 2026-08-09 14:41:40 -07:00
Brennan Benson 6858e072cf
fix(terminal): agent pane auto-launch lost under fish + Starship (STA-3417) (#12840)
* fix(terminal): extend the shell-ready startup barrier to fish (STA-3417)

Fish never emitted the OSC 777 shell-ready marker, so agent launch
commands were written into the PTY while fish/Starship were still
initializing: the daemon path wrote them synchronously at session
create and the local path blind-wrote ~30ms after the first output
byte. The command was echoed by the kernel but never executed.

- shell-templates: shared fish --init-command that emits the marker
  once on the first fish_prompt event (the earliest point fish's own
  reader owns the PTY, mirroring zsh's zle-line-init marker)
- daemon shell-ready: fish joins the startup barrier so the launch
  command queues until the marker (timeout fallback unchanged)
- local-pty-shell-ready: fish launch config gains the marker wrapper
- codex-startup-delivery/tui-agent-startup: omp/pi/opencode plans now
  request shell-ready delivery (codex parity) so the SSH renderer path
  also waits for the prompt; plain payload-free codex stays on the
  markerless fast path

* fix(terminal): answer DA1 past the shell-ready barrier

The barrier queues all inbound input until the ready marker, including the
renderer's DA1 reply. A shell that withholds its first prompt until DA1 is
answered — fish waits 10s — therefore never emits the marker that would
release the reply it is waiting for. Measured: 10.37s to launch an agent,
versus 0.35s once the reply lands.

Answer DA1 from the daemon while the barrier holds, writing straight to the
subprocess so the reply bypasses the queue, and consume the query so the
renderer's xterm cannot also reply. Released on ready, timeout, or dispose,
handing DA1 back to the renderer for steady state.

Consolidates the identical DA1 handler the ConPTY override already used.

* fix(terminal): prevent duplicate startup DA1 replies
2026-08-09 14:29:14 -07:00
Hyeonho Lee (Ethan) 850342a3e0
fix(ci): run the root-directory guard on stock macOS bash 3.2 (#12879)
* fix(ci): run the root-directory guard on stock macOS bash 3.2

The guard script builds its base-tree lookup with `declare -A`, which
needs bash 4+. Its test spawns plain `bash` from PATH, and stock macOS
has shipped /bin/bash 3.2 since 2007, so on any Mac without a Homebrew
bash the script exits 2 before asserting anything and the default
`pnpm test` suite fails 3 of the guard's 4 cases. Machines with a
Homebrew bash on PATH never see it, which is why it went unnoticed.

Replace the associative array with a plain-array linear scan. Root
directories number in the dozens, so the O(n^2) membership check is
negligible, and the NUL-delimited reads that protect unusual filenames
stay as they were. The empty-array expansion is guarded for
`set -u` under bash 3.2.

All four guard tests now pass with /bin/bash 3.2; behavior under CI's
bash 5 is unchanged.

* fix(ci): run the root-directory guard under node instead of bash

The guard is the only check in the repo written in shell, and it used
`declare -A`, which stock macOS `/bin/bash` 3.2 does not have — so the
guard's own test suite failed 3 of 4 cases on any Mac without a Homebrew
bash. CI never noticed because runners ship bash 5.

Porting it to node removes the interpreter-version variable instead of
working around one construct: node is what the sibling script in this
directory already uses, it is the runtime that runs the test, and the
NUL-delimited read is the same shape as check-changed-code-quality.mjs.
It also drops a latent false pass — a failing `git ls-tree` inside the
shell's `< <(...)` was not caught by `pipefail`, so the read loop saw
nothing and the guard reported success. `execFileSync` throws instead,
which is why the two `git rev-parse --verify` probes are no longer
needed.

Output and exit codes are otherwise unchanged; the usage line now prints
node's script path where the shell printed `$0`.

Tests pin each guarantee and fail when it is reverted: NUL-delimited
reads so odd paths are reported unmangled, exit 2 on bad usage, and
git's own 128 with no node stack trace when a sha does not resolve.

* fix(ci): keep root entry bytes intact and fence guard output

git pathnames are arbitrary bytes, but the guard read ls-tree with
encoding 'utf8', so every invalid sequence collapsed to U+FFFD. That
mangled the reported name and, because the replacement is not
injective, let two different entries compare equal — a genuinely new
root entry could be waved through as pre-existing. Read the bytes as
latin1 and write them back unchanged.

The blocked-entry list is also attacker-controlled and went straight to
stdout. The runner trims leading whitespace before matching '::', so an
indented entry name still parses as a workflow command, and a pathname
may embed a newline. Wrap the list in ::stop-commands:: with a random
resume token so only the guard's own annotation is acted on.

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-08-09 14:06:25 -07:00
Jinjing 3ec48a74d5
Gate artifact publishing behind off-by-default capability (#13368)
* 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>
2026-08-09 13:19:08 -07:00
Jinjing 2dc172f666
Support live toggle of agent status hooks with WSL relay gating (#13361)
* 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>
2026-08-09 12:53:13 -07:00
Neil 17cfc968cf
Revert the terminal IME composition-ownership change (#13282)
* 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>
2026-08-08 19:06:50 -07:00
Jinjing 3e6b93f0d2
test(wsl): drive hook-relay reattach bench through real PTY spawn (#13260)
* 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
2026-08-08 14:06:34 -07:00
OrcaWin 982570648a
fix(wsl): refresh hook relay on PTY reattach (#13139) 2026-08-08 02:03:52 -07:00
Neil 17b3dff3c4
refactor(terminal): return IME composition ownership to xterm (#13128)
* fix(terminal): return IME composition ownership to xterm

* fix(mobile): derive terminal input from native replacement ranges

* test(mobile): record iOS Japanese IME traces

* fix(mobile): preserve native IME replacement ranges

* fix(xterm): flush queued application input after IME commit

* test(terminal): pin Korean intermediate commit

* test: pin Windows IME shortcut ownership

* test: replay IBus number candidate commit

* fix: preserve native macOS input-method punctuation

* refactor(terminal): remove stale mac focus override

* fix(mobile): preserve soft keyboard deletion ranges

* fix: keep IME-owned palette chords in renderer

* fix: stop carried IME shortcuts at renderer owner

* fix: preserve carried IME shortcut dispatch

* fix: narrow main-owned shortcut actions

* test(mobile): pin Japanese IME replacement traces

* test(terminal): retain paired native IME trace

* fix(chat): preserve browser IME composition ownership

* fix(chat): retain macOS IME confirm gesture

* fix(chat): expire unmatched IME confirm carry

* fix(chat): isolate IME confirmation expiry

* fix(chat): retain active IME confirmation

* refactor(terminal): remove dead composition handler

* feat(ime): add shared Enter-ownership seams for CJK composition

The confirming Enter of a CJK composition arrives as two keydowns and the
orderings differ by platform: Windows/Linux redispatch the unmarked Enter/13
before keyup, macOS delivers keyup first. A guard reading only isComposing or
keyCode 229 misses the redispatch, so surfaces submitted on a confirm.

Adds useImeEnterGestureOwnership (carry token, next-frame expiry), a shared
ImeEnterGuardedForm for native implicit submission, and the cmdk seam covering
18 CommandInput surfaces at one site.

A chorded Enter arms the carry but is never swallowed — the reverse would eat a
user's deliberate Cmd/Ctrl+Enter. Both failure modes are pinned by
ime-enter-gesture-ownership-contract.test.ts.

Co-authored-by: Orca <help@stably.ai>

* refactor(terminal): consolidate native input listeners and parked-screen owner

Extracts the shared native-input listener installer and renames the parked-screen
detector for what it actually does, replacing per-call-site duplication. The
listener installer keeps a forgetOptionKeyLocationOnBlur flag so per-window
semantics are preserved rather than flattened.

Net deletion; no behaviour change intended.

Co-authored-by: Orca <help@stably.ai>

* test(terminal): pin recorded IME shapes as regression tests

Nine regression tests built from hashed affected-platform captures, each with a
paired ordinary negative and a discriminating mutation verified to take the file
from all-passing to exactly one failure.

Covers the Windows MS-Korean Shift family (#12179, #11878, #12151, #11946,
#12152) and the Korean TUI line-break rows (STA-3237, STA-3222, STA-3129).

STA-3237 pins the empirical 3-Shift / 2-active-composition / 2-newline ratio the
device run established — the third Shift produces nothing because Space has
already committed. That ratio is not derivable from a static capture.

Co-authored-by: Orca <help@stably.ai>

* fix(ime): guard Enter-commit surfaces against CJK confirm

Applies the Enter-ownership guards across the surfaces whose Enter commits
something: publishes, clones, pairs, installs, posts, or persists.

Tiered deliberately rather than uniformly. Irreversible and remote-effect sites
take the carry token, which also blocks the unmarked redispatch. Locally
reversible sites take the oracle check with a one-line comment naming the
residual, because a spurious commit there costs one undo.

Three numeric fields are left unguarded with the reason in-code: Chromium blanks
number inputs at compositionstart, so a confirm-Enter only ever reaches an
empty-draft reset. Measured with a CDP probe rather than assumed — a guard that
cannot fire is noise.

Co-authored-by: Orca <help@stably.ai>

* test(ime): teeth-check the Enter guards on every guarded surface

One suite per guarded surface, each verified by deleting the guard and
confirming the test fails. A green guard test without that check is unverified,
not verified.

Two shapes pass vacuously in happy-dom and are avoided here: native implicit
form submission never fires, and blur() is inert on an unfocused element. Both
made "the commit did not happen" assertions pass with the guard removed, so the
suites assert the guard's contract directly instead.

Co-authored-by: Orca <help@stably.ai>

* fix(mobile): keep iOS Korean commits whole through the live-input path

iOS Korean reports isComposing: false on every event, so it bypasses the
composition guard entirely. The strict owner rejected UIKit's transformed
post-change field and sent only the leading jamo — the reported symptom.

Prefers the authoritative same-event field text over the predicted text when the
supplied operation cannot produce it. Generic: no Korean special-case, no locale
classifier, no normalization. Adds the RN-target-keyed submit carry alongside it.

Co-authored-by: Orca <help@stably.ai>

* test(e2e): make IME capture harnesses fail loudly instead of silently

Four instruments recorded silence as success, so a void run scored as a clean
one:

- readTerminalImeBoundaryTrace returned an empty trace when the probe never
  installed, making every "nothing leaked" negative pass vacuously
- summarizeLatencies([]) returned a perfect zero distribution that passed all
  three latency thresholds
- the macOS Vietnamese spec pinned an input-source ID that does not exist, and
  failed as though the operator had chosen the wrong source
- the expectedLineCount=1 prefix property was undocumented and one edit from
  silently downgrading a PTY assertion

Input sources now resolve by enumeration and name the near-matches on failure.

Co-authored-by: Orca <help@stably.ai>

* test(terminal): cover Cangjie cancellation and fix a cross-namespace assertion

Adds #11951's recorded Cangjie cancel shape to the existing cancellation suite,
which covered Pinyin and Sogou but not Cangjie. One keystroke then Backspace
arriving as deleteContentBackward with data: null, so the stale preedit is the
only thing a fallback could replay.

Verified against the historical pre-6cd944c62b3 bundle: the positive fails with
['尸'] where [] is expected, while the ordinary negative stays green.

Also fixes the Vietnamese spec, which asserted a TIS-space input-source ID
against getKeyboardInputSourceId(). Those two Orca APIs report the same source
in different namespaces — TIS nests it under VietnameseIM, the app API does not.
The resolver stays as an installation precondition; the assertion matches the
leaf.

Co-authored-by: Orca <help@stably.ai>

* test(e2e): add a real-IME macOS arm for the Korean chord commit

The existing korean-ime-terminal-shift-enter-commit spec synthesizes composition
over CDP: Input.imeSetComposition sets the preedit directly and Input.insertText
performs the commit. Asserting the IME produced events you injected yourself is
circular, so that spec cannot certify real-IME behaviour.

This arm selects 2-Set Korean via TIS, reads it back live, and injects through
System Events key codes, so the OS owns the preedit, the commit instant, and
isComposing. PTY byte expectations are preserved verbatim.

Covers 2 of the original 4 cases by design. The other two are the Windows/Linux
redispatch-before-keyup ordering, which macOS cannot produce and which cannot be
selected -- the OS decides it. Reintroducing synthesis to "restore coverage"
would reintroduce the circularity.

Co-authored-by: Orca <help@stably.ai>

* test(e2e): assert the macOS chord arm at the PTY boundary, not the renderer

The byte expectations were transcribed from korean-ime-terminal-shift-enter-commit
:364/:383, which assert against onData -- a renderer boundary where the terminator
is CR. This spec reads the PTY child, where the tty has already converted CR to LF.

Names both forms per row rather than swapping the constant, so the conversion reads
as evidence that the capture reached past the renderer, as #11936 and #11951 record.
Ctrl+Enter's CSI-u sequence is unaffected and is identical at both boundaries.

Co-authored-by: Orca <help@stably.ai>

* test(e2e): measure composer-to-onData latency and stop dropping IME keystrokes

Two defects in the echo latency probe.

It hooked onWriteParsed and onRender but never onData, so it measured
key->parse->render echo rather than the composer-vs-onData delta the latency rows
need. Adds a third hook feeding its own sample set.

And `event.key.length !== 1` silently dropped IME keystrokes: Pinyin and Cangjie
keydowns arrive as key:'Process' (length 7). Replayed over the captured corpus,
the old filter accepted 580 of 4137 Chinese IME keydowns -- it was discarding 80%
of them. The new filter matches the shape the owner itself branches on.

Attribution charges each onData to the latest keydown rather than a FIFO head,
because composing jamo emit no onData at all and a queue would credit a whole
composition to its first keystroke. The consumer now asserts sample count before
any percentile, so a zero-sample run cannot render as a flawless distribution.

Co-authored-by: Orca <help@stably.ai>

* test(terminal): pin the WSL shifted-jamo newline shape for #11919

In Korean 2-set, Shift types ordinary letters -- the double consonants and the
compound vowels. Each such keystroke reaches Chromium as key='Process',
keyCode=229, shiftKey=true.

The v1.4.163 classifier matched exactly that pattern with no code guard, so it
called those keystrokes Enter, rewrote them to a synthetic Shift+Enter, and
injected a newline into the middle of the word -- with no Enter key pressed.
That is why the reporters said "no modifier key pressed": they had not chorded
Shift+Enter, but they had pressed Shift, to type the double consonant.

Asserts the row's own recorded capture: 40 immediate keydowns, exactly 3 of them
Shift-carrying inside a single syllable, and an onData stream with one newline
per Enter press and none mid-word. Two ordinary negatives keep it from being a
blanket mute -- the same session's non-IME keydowns still reach shortcut policy,
and an ordinary Shift+Enter still resolves through the real policy.

Co-authored-by: Orca <help@stably.ai>

* test(terminal): pin the composition commit lag that made Korean type one behind

macOS Korean 2-Set commits syllable N only when the first jamo of N+1 arrives, so
compositionend and compositionstart land in the same task. A composition-start
handler cancelled the pending finalizer that was the only path to triggerDataEvent
and ended the session without emitting bytes, so every committed syllable reached
onData exactly one syllable late and the backlog cleared only at a Space or Enter.

Types continuously with no Enter and no Space -- either would flush the backlog and
hide it -- and samples onData at every syllable boundary. Paired with a
length-matched ASCII arm that stays green throughout, so the positive is a fact
about composition rather than about timing in general.

Bisected to a single call site across five builds: pristine, 1.4.155 and 1.4.162
pass, 1.4.163 fails, removing the one call repairs it, restoring it fails
identically. That window is exactly the reporter's "started immediately after
updating".

Co-authored-by: Orca <help@stably.ai>

* test(mobile): cover the send-queue abort that silently drops queued keystrokes

One failed send in use-terminal-live-input-commit aborts every keystroke
queued behind it, with the error swallowed by .catch(() => false). The
existing test resolves(true) on every send, so the failure branch was
uncovered.

Four arms: the abort itself, an ordinary negative on the healthy path, a
throwing sender, and a liveness control proving the queue recovers once
the chain settles. Deleting the abort takes 4 passed to 3 failed, with the
ordinary negative correctly surviving.

Scope is stated in the docblock: this is a transport send-queue abort,
reachable only via a real disconnect or RPC error. REQUEST_TIMEOUT_MS is
30s, so latency alone cannot reach the branch — consistent with #7094's
symptom class, not proven to be its cause.

* test(terminal): pin that daemon snapshot/restore cannot disturb a composition

Two independent reporters attributed broken Korean composition to the
always-on PTY daemon repainting terminal state over the preedit. The
attribution is wrong on ancestry — the daemon shipped three months before
the version both call good — but the boundary was never actually tested.

Runs the real applyMainBufferSnapshot choreography against a live
composition, including the full 2J/3J/H wipe plus the resize and
alt-screen branches. textarea.value, selectionStart/End,
compositionView.textContent and .active all survive byte-identical, and
interleaving a restore between every jamo of 문제 still commits 문제 at
onData. Also pins that the uncommitted preedit is absent from the captured
snapshot: it lives in the textarea, never the buffer, so a restore has
nothing stale to echo back.

Injecting one textarea.value = '' into the restore fails exactly the three
restore-boundary tests.

* test(terminal): pin that Cmd tears down a composition where Ctrl and Shift do not

xterm's composition keydown exempts only keyCode 16/17/18 (Shift/Ctrl/Alt)
plus 20/229. macOS Meta — 91/93/224 — is absent, so a Cmd press mid-composition
takes _finalizeComposition(false): the overlay goes dark and never recovers,
because compositionstart is not re-fired. The user composes the rest of the
word blind. Linux and Windows users press Ctrl and are exempt.

xterm already has a Meta-aware modifier predicate in wasModifierKeyOnlyEvent,
so this is an internal inconsistency rather than a deliberate choice.

Owns no reported row and is version-neutral: 5/5 on both 1.4.162 and 1.4.163.
The branch is unexercised in all 328 recorded traces, so this is a hazard pin,
not a regression guard. Only the teardown is asserted; the likely duplicated
commit needs a compositionend the IME kept alive across the Cmd, which no
capture contains.

Deleting the exemption fails exactly the three paired negatives; adding Meta
to it fails exactly the two Cmd arms.

* test(native-chat): characterize preedit loss when a question card replaces the composer

An AskUserQuestion card fully replaces the composer by design, but the
in-flight composition goes with it: the composer unmounts before
compositionend reaches it, so the preedit is never committed to the draft.
The committed text survives only because the draft is cached and restored
via defaultValue. Node identity changes, value 'abc' is preserved, the 가
is gone.

Drives the real NativeChatView -> SessionGate -> InteractiveCard ->
questionActive swap -> Composer -> ComposerField, flipped by writing the
same store field an AskUserQuestion hook event writes. Flipping
questionActive to false fails exactly this test and nothing else across
639 native-chat tests, so the path was entirely unguarded.

CHARACTERIZATION TEST: it asserts the loss. Fixing the defect — committing
the preedit before the swap, or keeping the composer mounted — will make
this file fail. Update the expectations to the new contract rather than
working around them.

Owns no reported row. #12118/STA-3219 flicker is keyed to token counters,
which provably do not remount, and a question card arrives once per
question.

* test(terminal): pin the duplicated commit when Meta interrupts a composition

_finalizeComposition(false) sends textarea.value.substring(start, end) but
cannot clear the IME-owned textarea, so a later compositionend re-sends the
same range. Meta reaches that path because CompositionHelper exempts only
Shift/Ctrl/Alt; xterm's own wasModifierKeyOnlyEvent covers Meta four ways,
so the omission is an internal inconsistency rather than a choice.

Companion to the modifier-exemption guard, which deliberately pins only the
overlay teardown. This pins the data consequence.

HAZARD PIN: owns no reported row. The trigger is unverified on hardware —
no capture in the corpus contains a Meta-during-composition gesture, and
whether macOS keeps the composition alive across it is unmeasured. The
duplication follows from the code given that sequence; whether users reach
the sequence is the open half.

An earlier premise that Space (keyCode 32) reaches this path was refuted by
a corpus scan: 0 of 731 evidence files carry a keyCode-32 Space while
composing, against 171 at 229, and 229 returns early.

* test(terminal): characterize the syllable lost when the textarea blurs mid-composition

CoreBrowserTerminal._handleTextAreaBlur clears the helper textarea
unconditionally — "Text can safely be removed on blur" — while
CompositionHelper._finalizeComposition reads the committed text back out of
that same value from a deferred timeout. By the time it runs the value is
empty, the substring is '', and triggerDataEvent never sees the syllable.
xterm checks composition state in _syncTextArea and omits the same check
here.

Six cases. Blurring mid-composition loses the syllable in every ordering,
including compositionend-before-blur, which is Chromium's real order — so
it is not an ordering artifact. A bare textarea.blur() with no Orca code
loses it too, which places the owner upstream: Orca's unguarded release on
outside pointerdown is one trigger, not the cause. Committing 한 then
blurring mid-가 yields ['한'] where ['한','가'] is correct: one syllable
gone, surrounding text intact.

Teeth checked by inverting — adding an Orca-side composition guard flips
exactly the three cases that route through the release path and leaves the
bare-blur and no-blur cases green, which is the scope split: a fix in
regular-terminal-focus-ownership alone would not close this.

HAZARD PIN, but unlike the others this one has a real production injector —
clicking outside the terminal mid-composition. Owns no reported row. The
shape matches #9738's report; the injector does not, and a shape match with
a mismatched injector is not an owner.

* test(terminal): say which arm the STA-3237 fixture came from

The recorded keydowns are wave 4's A-shift-unmarked-only — the arm that
emits no PTY bytes. Nothing in the file said so, so two readers concluded
the row's events fail the owner's predicate and that STA-3237 and STA-3222
were different defects. They share an owner; the arm that fires is
Process/229+Shift, absent from this bubble-phase trace because the owner
claims it in the capture phase.

Also corrects "code-blind": the v1.4.163 policy emits \x1b\r only for a
shift-only key:'Enter', and a jamo keydown reaches that branch solely via
the isTerminalImeProcessEnter rewrite. The mock is deliberately wider so
the ownership guard stays under test if that rewrite moves.

Comments only — no assertion, fixture value, or mock behaviour changed.

* test(e2e): track the input-source selector the macOS specs shell out to

Five tracked macOS IME specs ran `swift .tmp/select-input-source.swift`, a
file that is gitignored and existed only on one machine. Anyone else
checking out the repo — or the same machine after .tmp is cleaned — could
not run them, and they are the capture drivers for the macOS rows that are
blocked waiting for exactly those runs.

Moves it to tests/e2e/ beside its callers. The chord spec now resolves it
from __dirname rather than reaching two levels up into .tmp.

* test(terminal): pin the CJK repaint decision against the reporter's own output

#12164 comment 1 and #5921 report agent output with double-width glyphs
rendering duplicated character-by-character while ASCII in the same line
stays clean. No IME, no composition, no keystroke — the user never types
the CJK.

Segmenting all three verbatim samples into maximal same-risk-class runs
gives 33 runs and zero violations of "this run is corrupted iff the
production detector flags it": 17 wide runs all corrupted, 16 narrow runs
all byte-identical. The paired negative is co-located in the same line
rather than in a separate run — the reporter supplied it without knowing.

Doubling is asserted as present, not uniform: 자바스크립트 and 시스템 each
leave a jamo undoubled, which is a repaint-region boundary artifact rather
than a per-character transform.

The discriminating arm is in the test rather than a source mutation:
be3f30e2f8 (#6890) elects a repaint for all 17 corrupted runs when the
agent types nothing, and reverting its disjunct elects none. Both
predicates agree once the user has recently typed, which is the pre-#6890
condition.

Samples inlined with per-sample SHA-256 because .tmp is gitignored and
cannot back a landed test.

* test(terminal): pin macOS period substitution landing after the composition

#11504's reporter published a DOM trace showing insertText ". " arriving
149ms after compositionend, when two spaces are typed with a CJK input
source and NSAutomaticPeriodSubstitutionEnabled is on. This replays that
trace against a real Terminal and asserts what reaches onData — bytes to
the PTY, not anything visual.

The owner is stock upstream CoreBrowserTerminal._inputEvent, not an Orca
module, confirmed at the resolved install and in the shipped bundle Vite
loads rather than in the TypeScript source.

Three mutations against that install, predictions written before the runs,
each failing exactly the arms predicted: dropping the composed/keyDownSeen
guard fails two, dropping Orca's intercept fails the one arm where the
payload arrives before the send window drains, and flipping || to && —
the candidate-fix shape — fails the arm that pins the defect itself.

CHARACTERIZATION: arm 1 asserts the broken behaviour and will fail the
moment #11504 is fixed. Update it to the new contract rather than working
around it.

composed is absent from every recorded bundle, so composed: true is the
spec-required value rather than a captured one; the test asserts it before
dispatching so a harness that dropped the field fails loudly.

* test(terminal): replay the recorded Windows Shift sessions through the IME guard

STA-3179 reports a Shift release sending Enter; #12171 reports delayed
Hangul plus doubled newlines. Both replay their own recorded Windows
MS-Korean keydowns through resolveTerminalKeyboardShortcutAction with the
shortcut policy mocked, so the assertions are about which events reach the
policy and what reaches terminal input.

STA-3179's held-Shift gesture yields exactly one newline, from the unmarked
Enter alone; its release arms nothing for the next composition, asserted
after a precondition check that the release really is keyups with shiftKey
already dropped; and an ordinary Shift press-and-release still routes every
keydown, which is the paired non-IME negative.

Teeth, verified by mutation: bypassing the isImeOwnedKeyboardEvent guard in
keyboard-handlers takes STA-3179 from 3 passed to 2 failed / 1 passed — the
survivor being the ordinary-session negative, which is correct, since a
non-IME session should not depend on that guard — and #12171 from 2 passed
to 2 failed. Source restored byte-identical.

Recorded shapes are inlined and the bundles cited in comments; nothing is
imported from .tmp, which is gitignored.

* test(native-chat): correct 61977d45177 — the preedit survives the question card

61977d45177 claimed a composed syllable vanishes silently when an
AskUserQuestion card replaces the composer, and characterized that loss.
The claim was false. Its premise was an artifact of the harness: the test
simulated a preedit with a silent textarea.value assignment and no input
event, which no IME does.

Real composition fires input with insertCompositionText on every keystroke
— the shape this repo already records in its own observed-event capture —
and React's change handler returns on input/change with no composition
gate, so onChange runs for each frame. The draft cache is written
synchronously inside the updater, so the preedit is already committed
before the card can arrive. Driven that way, it survives.

Renamed to match the contract that actually holds, and extended: Hangul
jamo-per-frame, Japanese kana accumulation followed by per-segment
conversion asserting the candidate the user was looking at survives, and a
pin on the mechanism itself — the draft cache holds the preedit while the
card is up.

Teeth: there is no fix to revert, so the mutation is the plausible wrong
one — gating onChange on isComposing(). That takes 4 passed to 3 failed,
with the English negative correctly surviving, since it has no composition
to gate.

Two consequences remain, recorded rather than fixed: the OS aborts the
composition when the field disappears, so a lone jamo returns as a
compatibility jamo the user cannot compose onto, and the remounted
composer is unfocused because the card owned focus.

* docs(native-chat): name the corrected commit and the degraded-jamo consequence

Records in the file itself that 61977d45177 is pushed and wrong, quoting
the two claims that are false, so a reader who finds it in git log reaches
the correction from the file that replaced it.

Also states the residual as a consequence rather than a curiosity: a lone
leading jamo returns as a standalone compatibility jamo (U+3131), which is
not a composable state — the user cannot resume the syllable, only delete
and retype. Preserved, but degraded into something unusable. That is the
note to find if a reporter ever describes exactly that.

The invariant these tests pin is not "the composer commits on unmount" but
"composition input events must reach React" — which is what a future IME
change would break, and is not visible from the swap site at all.

* test(terminal): replay the recorded macOS Telex commit boundaries

#6905 reports Vietnamese composed characters breaking in the terminal.
Replays the retained macOS built-in Simple Telex capture — recorded
selection and value set before each dispatch, since that is what the
commit range reads — and asserts what reaches onData: the first commit
alone, then through the real Enter, then the ASCII tail of the same run.
A code-point count would catch NFD normalisation.

ENGINE CAVEAT, stated first in the docblock: this is macOS built-in Simple
Telex, Telex only. The reporter's three named engines cannot run on the
platform they declared, and which macOS Vietnamese engine they used is
unconfirmed. This file certifies no engine, and does not imply VNI.

The owner is upstream's — CompositionHelper._finalizeComposition's
waitForPropagation branch — so the arms are copies under .tmp aliased by a
scratch config, with node_modules verified unchanged by shasum after every
run. Collapsing the range end onto its start fails all three; collapsing
the start to zero re-emits the first word into the second commit, which is
the reporter's "duplicated" direction. Different failure sets, so the
mutants are distinguishable rather than merely detectable, and the ASCII
assertion passes under both.

Falsifiability here is by mutation, not by a defective build: #6905 does
not reproduce at HEAD, so this has never been watched going red on a real
reproduction.

* docs(terminal): lead the #6905 test with its engine caveat

Comment-only. Moves the caveat above the source line so a reader meets what
the file does NOT establish before what it does — the capture is macOS
built-in Simple Telex, the reporter's named engines cannot run on the
platform they declared, and which engine they used is what gates this row.

Co-authored-by: Orca <help@stably.ai>

* docs(terminal): record that the swallow eats a keystroke after Japanese conversion

This pin framed the swallowed insertText around Cmd interrupting a
composition. A differential through Japanese multi-segment conversion shows
it is broader: type a segment, convert, then press `a`, and the `a` is
lost. No modifier, no exotic gesture. Korean surfaced it first only because
2-Set composes on nearly every keystroke.

Also records why it cannot simply be fixed. The suppression de-duplicates
IMEs that deliver their commit a task after compositionend, which a sibling
test pins; this swallow is that dedup's false positive, and the two events
differ only in payload, so no flag-timing change separates them. Both a
smaller redesign and a content-aware variant were built and measured — the
first duplicates on IBus, the second costs a reported row's test and is
blocked while the patch cannot be regenerated.

The Japanese arrays behind this are authored, not observed: no Japanese DOM
composition trace exists in the corpus.

* docs(terminal): a Japanese capture does exist — correcting dbeecb11bee

That commit said no Japanese DOM composition trace exists in the corpus.
False. One does, filed under the Linux bundles rather than the bundle named
for Japanese: 30 DOM events, two にほんご->日本語 conversions, with full
selection state per event. It is retained byte-identically in three further
bundles — one capture copied four times, not four observations, checked by
hash rather than by counting files.

The claim came from checking the bundle named for Japanese, finding nothing,
and generalising to the corpus without querying the rest of it.

Replaying it emits 日本語日本語 under both sequencing extremes on all four
arms, matching its own recorded onData. So "repeated conversion is
undisturbed" is now captured rather than authored. It carries no
post-compositionend insertText, so it cannot speak to the swallow: the
a-after-conversion figure stays authored and unobserved.

Also rewords the paragraph opener. It claimed to broaden a Cmd framing, but
hazard 2 was never Cmd-framed — the lines above already say Cmd does not
reach it. The real gap was that hazard 2 named no trigger at all, which
reads as exotic when it is ordinary.

* build(xterm): land the patch regeneration harness

The five dependency patches under config/patches/ shipped with no tracked
way to regenerate any of them. The xterm one is the hard case: it is derived
from an upstream build, so no fix could be made without rebuilding, and the
tooling to rebuild lived only in one machine's scratch directory. That
blocked a measured fix for a live keystroke-loss bug, and the EditContext
reduction an OSS survey identified as the only real one available.

Adds the regenerator, the upstream pin, the hand-written source patch the
bundle hunks derive from, tests, docs, and a PR job that verifies the
shipped patches still match the pinned build. The job caches the shallow
clone keyed on the manifest, so a cold run is minutes and a warm one under
one. Round-trip verified: regenerating from a clean checkout reproduces the
shipped patch byte-for-byte.

Marks the emitted patch -diff -text. pnpm hashes it byte-for-byte, so a
CRLF checkout would break install on Windows, and its minified bundle lines
make a diff nobody can read — review the source patch instead.

Also rejects unknown flags. --check was the fallback for any unrecognised
argument, so a typo, or --help, silently triggered a full upstream build
instead of what the caller asked for.

* fix(xterm): stop swallowing a keystroke typed after an IME commit

Type a Japanese segment, convert it, then press a key one macrotask later
and that key was lost. No modifier, nothing exotic — every user who keeps
typing straight after converting. Korean surfaced it first only because
2-Set composes on nearly every keystroke.

handleCompositionInput discarded the payload unconditionally in the window
after the deferred send: _isSendingComposition stays true for one macrotask
after the timer cleared _pendingCompositionStart, and the branch substituted
'' for whatever arrived. The suppression is not itself wrong — it
de-duplicates IMEs that deliver their commit an event-loop turn late, which
terminal-stock-composition.test.ts pins. It just could not tell a duplicate
from new input, because the two events are identical apart from payload.

Now it compares against _sentComposition, the text the deferred send
actually emitted, and discards only a match. A flag-timing redesign was
measured first and rejected: it fixed this and duplicated on IBus, because
no timing change can separate events that differ only in content.

Edited in config/patches/xterm-src/ and regenerated through the harness, so
the emitted patch and the lockfile hash are derived, not hand-written.

The commit-overlap pin's swallow arm now asserts the repaired contract —
the value its own comment already named as correct and as what stock
beta.287 emits. #11504's arm at :184 flips too; it never covered that
report, as its own prior note recorded, and the reporter's +149ms arm is
untouched and still asserting the defect. Provenance hashes in three test
docblocks are updated, since regenerating changes the patch hash and with it
the resolved install directory.

* docs(terminal): re-measure the #6905 mutation citations against the new bundle

Regenerating the patch moved the resolved install, so this docblock's
patch_hash, line count, two line numbers and three mutation outcomes all
described a bundle that no longer exists. The deferred branch is one the
fix writes into, so the outcomes could not be re-pointed on reasoning.

Line numbers read off both files by diffing anchors rather than derived by
arithmetic: 201 to 205, 159 to 163. Outcomes re-run through the retained
rig, which re-resolves through the module loader and re-derives each arm
from a unique minified anchor: pristine 3 passed, m1 3 failed, m2 2 failed,
m3 3 passed — identical to the old bundle. Guard controls in both
directions exit 1, so the counts are falsifiable.

Comment-only; the assertions and expectations are unchanged.

* fix(xterm): size the preedit overlay to the cells its text will occupy

updateCompositionElements computed the overlay's left edge from the grid
but never its width, so the preedit rendered at the font's natural advance
while the committed text takes two cells per wide glyph. Measured in
Chromium 150: 가나다라 drew 48.45px as a preedit and 69.20px once committed
— the same characters, same font, 30% narrower, and drifting further with
each syllable. Every macOS mono font carrying Hangul measured 0.49–0.72 of
two cells; never 1.0.

Deriving the width from wcwidth and the cell measure moves Korean, Japanese
and Chinese to 1.000 and leaves ASCII at 1.000, which it already was:

  한        12.125 -> 17.297   (17.30 expected)
  가나다라   48.453 -> 69.188   (69.20)
  안녕하세요 60.563 -> 86.500   (86.50)
  日本語     42.000 -> 51.906   (51.90)
  abcdefgh  69.234 -> 69.203   (69.20, unchanged)

Edited in config/patches/xterm-src/ and regenerated through the harness, so
the emitted patch and lockfile hash are derived rather than hand-written.

The unit test asserts the arithmetic, which is what CI can run. The pixel
consequence was measured on macOS with SF Mono in an Electron harness, not
on the Windows font stack STA-3232 reports from — so this demonstrates the
mechanism and does not stand as that row's platform evidence.

* test(e2e): pin the macOS Korean preedit as visible only while composing

#11914 reports the composing text invisible until Space. Its c3 was recorded
as unobtainable, and the reason on file was wrong: the boundary IS
assertable, but not in happy-dom, which reports display:block in BOTH the
active and inactive states and zeros for every rect. A test there passes
with the defect present.

Captured on real hardware instead: hidden and 0x0 before, .active with
display:block, a 15.84x16 rect and checkVisibility() true while composing
그, hidden again after. 39 DOM events, 2 composition starts, onData
["한","그","\r"].

Two mechanism findings are carried in the setup because both are invisible
in the result and fatal if removed. The input source must be selected AFTER
the app takes focus — focusing resets it to ABC. And the IME must be warmed
until an observed keyCode 229; typed cold it emits raw QWERTY (g k s r m)
with no composition at all, which is indistinguishable from an IME that is
not installed. Two runs were voided on exactly that signature before the
warm-up was found.

The has229 and compositionStarts assertions exist to make such a run fail
loudly rather than pass as a clean negative.

Gated on darwin plus ORCA_E2E_NATIVE_MACOS_KOREAN, like its siblings. The
final spec form has not itself been executed — the machine became
unavailable — so it carries the probe's measured values as literals rather
than a run of its own.

* docs(e2e): correct 19a8d133db7 — the Korean preedit spec has been executed

That commit said the landed form had never run and carried the probe's
values as literals. It has now run on real hardware: 1 passed, 9.1s, rc=0,
with the capture and log sealed under a verified hash manifest.

The teeth check was also run rather than reasoned about, and it changes
which assertion matters. Forcing the active overlay to max-width:0 with
overflow:hidden — invisible on screen — leaves the active class, the
textContent, display:block AND checkVisibility() all passing. Only
during.rect.width fails. checkVisibility() is not sufficient against this
defect; the bounding rect is the single load-bearing assertion, which the
docblock already said and this run confirms.

An earlier teeth attempt injected the CSS mid-run and tripped the
hasActiveClass poll instead, failing at the wrong assertion. It is
inconclusive and excluded from the seal rather than counted.

* test(terminal): add #12171's ordinary-English arm from a real Windows capture

c4 was recorded as unmet and the ledger sourced its control to
evidence/windows-current/, which holds 12 captures and not one English one.
The arm here comes from windows-9803-final instead — same probe, same host
geometry, same injector, en-US with no IME, replayed keydown for keydown.

Two limits are stated in the file rather than left for a reader to find. It
is a different bundle and a different run about 3.6 hours later, so it is
not a same-run arm. And it is #9803's range-active MUTANT arm: ordinary
English stays byte-exact even with that saved-range mutation live, which is
why it reads as a negative rather than as a baseline.

Bundle cited by directory with its file SHA-256; MANIFEST.sha256 verifies
21/21, rc=0. Nothing imported from .tmp.

* docs(terminal): correct #12164's grounds — the cited comments say no such thing

The rejection of #12164 from this file's family was recorded as resting on its
comment 1 (output doubling) and comment 2 (filed against 1.4.163). Checked
against the API: the issue has exactly two comments, neither of which says
that, and the string 1.4.163 appears nowhere in the thread.

The conclusion survives on better grounds. The issue BODY's repro is "Run any
CLI agent (Codex, AGY, Claude, etc.) that outputs Korean text into the Orca
terminal" — untyped output, no keystrokes, no composition — so excluding
CompositionHelper is right, and the input-path hunt was looking in the wrong
place. The body is also LLM-authored (it still contains a literal
"## 5. GitHub Submission Draft (Ready to Post)") and its Root Cause section
blames a CJK IME preedit buffer its own repro never engages, so it should not
be read as observation.

Comment-only; suite unchanged at 5/5.

* test(native-chat): make composition frames carry isComposing, not just inputType

This suite's comment claimed "Gating onChange on `isComposing` breaks here."
It did not. composeFrame() fired `input` with `inputType` but never set
`isComposing`, so a gate on `isComposing` passed all four tests untouched —
the suite asserted a discriminator it did not exercise.

Composition frames now carry both, so neither gate is exempt. Verified by
pointing the mutant at it: with an `isComposing` gate on the composer's
onChange, this suite now fails 3 of 4 (it passed 4 of 4 before), and the
ordinary-English arm correctly survives, since a composition gate should not
touch it. Production code is unchanged and stays gate-free; the mutation was
applied, measured, and reverted.

Found while excluding NativeChatView's question-card remount as the owner of
#12118 / STA-3219: the remount is real, but the preedit survives it precisely
because this write path has no composition gate.

* fix(mobile): ship the patched xterm build, matching desktop

mobile pinned @xterm/xterm 6.1.0-beta.285 while the patch is keyed to
6.1.0-beta.287, so mobile shipped stock xterm and neither IME defect fix
reached it: the swallowed keystroke after an IME commit (9506039de72) and
the preedit sized to the font rather than the grid (e04e0c88da5).

Bumps the three xterm packages to the desktop versions and adds the patch
to mobile's own pnpm.patchedDependencies. No copy of the patch: pnpm
accepts the parent-relative path and records it in the lockfile against
hash 8d63166272e9040a…, byte-identical to what desktop resolves, so the
two stay in step by construction rather than by a drift check.

The workspace separation is untouched — root pnpm-workspace.yaml still
declares `packages: []` and mobile keeps its own lockfile, which is what
keeps the root's patches from failing as ERR_PNPM_UNUSED_PATCH.

Verified in the generated webview bundle rather than at the install:
alignPreeditToGrid 0->2, sentComposition 0->3, pendingInput 0->11, and the
stock-only _handleAnyTextareaChanges 2->0 and dataAlreadySent 4->0. pnpm
applies patches during linking before postinstall regenerates the bundle,
confirmed by a revert/reinstall/re-apply cycle in both directions.

Mobile suite 2971 passed, 3 skipped — identical before and after. Bundle
+1,514 B (+0.24%). Lockfile churn is xterm-only; --frozen-lockfile passes.

mobile/src/ime/ime-submit-carry.ts is NOT made redundant and is untouched:
it handles iOS firing onSubmitEditing on a React Native native TextInput
after unmarking a composition, which is outside the WebView entirely.

Known divergence left alone: desktop also patches @xterm/addon-webgl and
mobile now runs that version unpatched. That patch is glyph/texture-atlas
rendering with nothing IME-related, so it affects neither fix.

* test(terminal): pin the preedit overlay against already-committed cells

STA-3132 (arm A), STA-3170 and STA-3232 report a Korean preedit painted on
top of text already on screen. Builds v1.4.163-v1.4.166 cancel the pending
finalizer in compositionstart, so a committed syllable reaches onData one
syllable late and buffer.x is stale — the overlay lands on the cell the
flushed syllable is about to occupy.

Replays a recorded hardware trace rather than an authored one: the ordered
DOM event stream captured on Windows + MS Korean (wave5-r2 evidence, 64
events), echoing onData back as PTY output.

The load-bearing assertion is deliberately not the obvious one. Comparing
overlay style.left against cursorX is tautological — left is computed from
buffer.x. This counts committed syllables from the compositionend events
the IME fired, so the two sides are independently derived.

Discriminated by a historical re-add across seven real bundles, since the
owner is deletion-shaped: pristine beta287, v1.4.155 and v1.4.162 pass;
v1.4.163 fails; v1.4.163 with that single call removed passes; the byte
identical baseline restored fails again; head passes. Every failing arm
fails only this case — the ordinary negative stays green in all seven.

The negative asserts its own category rather than claiming it: zero
composition events, zero isComposing, zero keyCode 229, exactly 16 events,
paired against the Korean arm's 4 starts / 3 ends / 11 updates / 64 events.

Scope: cell indices, not pixels. happy-dom has no layout, so the recorded
8x16 cell metrics are supplied to the render service. This makes no claim
about pixels visually overlapping; that is affected-OS confirmation and
stays open. Covers the overlap arm only — STA-3132's auto-line-break arm
and STA-3232's half-line-capacity and a11y arms are untouched.

* test(e2e): matrix macOS period substitution against the OS preference

#11504 reports macOS inserting ". " after a Hangul Space commit. This
sweeps six arms across both states of NSAutomaticPeriodSubstitutionEnabled,
reading the preference live per run rather than asserting a literal.

Two results worth having on record.

The reporter's stated trigger did not reproduce. Their words are "There is
no second press at all. One space is enough", but korean-single-space emits
zero insertText with the preference on or off. So does word-space-word-space.

Their timing does reproduce, with different content. korean-double-space and
korean-longer-word-double-space emit a delayed insertText at +122.5-122.7ms
after compositionend — squarely the reported +149ms — but the payload is a
space, never ". ". Consistent with the double-space rule seeing two slots
under ABC and only one under Korean, where the IME commit consumes the first.

The substitution itself is real and preference-bound: latin-double-space
gives "ab . " with the preference on and "ab  " with it off, on one build
with the preference as the sole variable, reproduced across two runs.

That also refutes a claim in PR #11506, which states the substitution "is
enforced outside the renderer and never reproduces in dev builds, so changes
here must be verified against a packaged app". It reproduced in the dev build
twice and did not reproduce on the signed packaged app. That claim should not
be used as a verification gate.

Gated @headful behind ORCA_E2E_NATIVE_MACOS_PERIOD, same shape as the Korean
preedit spec, so it does not run in ordinary CI. Evidence is onData and DOM
only — the PTY-child reader aborted and no packaged-app arm was stable.

* test(terminal): actually enforce the recorded jamo progression

The preedit assertion compared sample.overlayText against sample.overlayText
— the same expression on both sides. A lane proved it by mutation: corrupting
seven of the eight recorded preedit values left the suite fully green. So the
docblock's ㄱ→가→간→나→낟→다→달→라, which the matrix also cites as this row's
recorded shape, was cited and unenforced.

The first attempt at a fix was insufficient and is worth recording. Threading
stroke.preedit through to the expectation still passed on a corrupted fixture,
because that value both drives the rig and was the expectation — corrupting it
moved both sides together. Same tautology, one level down.

The expectation is now an independent literal. Verified by mutation rather
than by reading: corrupting two recorded values fails one arm; restoring them
passes 3/3.

overlayCell was never affected — it is compared against a count derived from
the compositionend events, not from the buffer, and remains the load-bearing
assertion for the overlap.

* fix(e2e): select the selectable input source, not the first match

TISCreateInputSourceList can return several entries for one input source
id. A third-party IME publishes a non-selectable parent alongside the
selectable mode, and taking sources.first can return the parent — after
which TISSelectInputSource fails with paramErr (-50) while the caller
reports success from the enable step.

Found with Qingg (com.aodaren.inputmethod.Qingg), which exposes exactly
that pair under one id. Its mode id equals the bundle id, so filtering by
name would not have helped; selectability is the discriminator.

Now filters on kTISPropertyInputSourceIsSelectCapable and falls back to
the old behaviour when nothing advertises it, so single-entry sources are
unaffected. Also enables every entry for the id rather than only the one
being selected: selecting a mode whose parent is still disabled fails the
same way.

Compile-checked, and selecting com.apple.keylayout.ABC still exits 0.

Unrelated to the enable path: on macOS 26.5.2 third-party IMEs are gated
behind a consent sheet in System Settings. TISEnableInputSource returns
noErr immediately regardless, and the enable only lands if that sheet is
answered while the requesting process is still alive.

* test(terminal): discriminate #12171 against the real shortcut policy

The prior candidate mutation for this row was correctly refused: its suite
mocked shortcut policy so Process/229 became actionable, while the real
resolveTerminalShortcutAction has no Process branch — so the kill measured
the mock. This does not mock it.

Replays a capture of this row's own gesture (d, l, Shift+T, e, k, Space,
Enter under MS Korean, committing 있다) taken on Orca 1.4.164, through the
real useTerminalKeyboardShortcuts hook, capturing bytes at terminal.input.
The earlier capture could not discriminate at all because it recorded no
shiftKey; this one records it on 10 of 10 keydowns with code populated.

One physical Shift+T produces two shifted Process/229 keydowns. Under the
pre-#12265 classifier each synthesizes {key:'Enter', shiftKey:true}, which
the real policy resolves to sendInput '\x1b\r' — twice, giving 1b0d1b0d,
the two escapes the known-bad ed96881b0d1b0d contains.

Mutation is the retained pre-12265-process-shift.patch applied to HEAD, not
an authored one: patch -p1 applies clean and diffs identical to the mutant
copy. Arms are copies; shared source hashes the same before and after.

The English arm stays clean under both modules, so the mutation
discriminates by language rather than by harness — and a real Shift+Enter
through the same rig yields exactly ['\x1b\r'] in every arm, so a silent
pristine result means the code is quiet rather than the harness dead.

Scope: 1b0d1b0d is measured at the renderer boundary. The capture recorded
no PTY bytes — window.api.pty is frozen on shipped builds and the onData
channel needs a build-time flag — so this shows the renderer producing the
two escapes that payload contains, not a re-observation of the payload.

* docs(native-chat): narrow this file's disclaimer to what is now true

It said "THIS OWNS NO REPORTED ROW". Half of that is stale: the remount site
is now the attributed owner of #12118 and STA-3219. On real Windows TSF the
questionActive swap aborts a live composition — the old node gets only a
blur and no compositionend, the text returns as committed, and the next jamo
yields 아ㄴ rather than 안.

The other half holds. This file pins the opposite property, that the text
survives, which is the half those reporters already agree with. Mutation
shows the gap rather than asserting it: deleting the unmount entirely leaves
three of four tests green, because every substantive assertion is
after.value === … and a composer that never unmounts keeps its value.

Also records why the abort cannot be asserted here. The DOM exposes no
observable separating committed text from a live preedit — value is the same
string either way, there is no EditContext, and the only composing-ness
state is a per-instance ref discarded with the node. A test pinning "no
compositionend fires" would be an anti-guard: red the day it is fixed.

The cadence objection is kept, since it is now the open question rather than
the reason for exclusion.

* refactor(terminal): drop the unread isComposing field from XtermBypassEvent

Added by #6396 for terminal IME candidate handling that this branch has since
removed. No production or test code reads it, and the policy is safe without it:
during composition `key` is 'Process', so the non-ASCII printable checks that
would care never match.

Co-authored-by: Orca <help@stably.ai>

* fix(native-chat): keep the composer mounted through an in-flight IME composition

A question card replaced the composer outright
(`{questionActive ? null : <NativeChatComposer/>}`). Unmounting the field
mid-composition aborts the composition in the OS: the node is detached before
`compositionend` can fire, the preedit returns as committed text, and a resumed
Hangul syllable degrades — 아 then ㄴ yields `아ㄴ`, never `안`.

Confirmed in rasterised pixels on Windows with a real MS Korean IME, at both
v1.4.171 and the reporter-era v1.4.164 (the swap block is byte-identical
across them): the preedit underline present before the swap, the composer
visibly absent during it, and the same glyph back afterwards WITHOUT the
underline — committed, not composing.

The swap is now deferred while a composition is in flight, which is what
editors that survive IME do: ProseMirror gates DOM work on `view.composing`,
CodeMirror protects the composing subtree from redraws. Hiding instead of
unmounting does not work — `display:none` and `visibility:hidden` both blur the
focused element and abort the composition the same way.

The hold releases on `compositionend`, which browsers also fire on blur, so
clicking into the card's own answer input yields the input region immediately;
with nothing composing the card still replaces the composer at once, so no
stray "Send a message" appears beside a question.

The existing characterization test flips to a regression guard: it pinned the
node being destroyed, which was the defect. Node identity is the load-bearing
assertion — value-only checks are trivially satisfied by a composer that never
unmounts and cannot tell a held composition from a destroyed one.

The typing-redirect handler moves to its own hook. That is not cosmetic: both
touched files sat at the 400-line cap, and `max-lines` suppressions are
forbidden, so the room had to come from a real extraction.

* fix(macos): opt Orca out of AppKit automatic period substitution

macOS "Add period with double-space" (`NSAutomaticPeriodSubstitutionEnabled`,
on by default) is applied by AppKit's text input system. Native terminals never
join that system; Chromium text fields do, so xterm's helper textarea inherits
it and a double space arrives as `". "` — a period nobody typed, handed straight
to the PTY (#11504).

Chromium answers AppKit for quote and dash substitution and defaults both off,
but declares no period accessor at all, so AppKit applies that one without
asking. This user default is the only lever: there is no per-field or
per-webContents opt-out to prefer over it. Writing the key into Orca's own
defaults domain overrides the global value for this app alone and leaves the
user's system-wide setting untouched. It necessarily covers every Orca text
field, not only terminals — AppKit offers no narrower scope, and that tradeoff
is deliberate rather than accidental.

Measured on the reporter's own build v1.4.161: with the preference ON, typing
a,b,space,space yields `onData ["a","b"," ",". "]`; with it OFF the same arm
yields two spaces.

Note the issue's causal model is wrong and this fix does not follow it. It
claims the substitution only fires with a CJK input source and never with ABC.
The measurement is the inverse — every Korean arm is clean and the ABC arm is
the one that fires — so the fix is not conditioned on input source.

NOT YET VERIFIED ON HARDWARE. The unit tests inject the writer, so they prove
the call is made on darwin and skipped elsewhere; they do not prove AppKit
honours an app-domain override for this key. That check is outstanding.

* fix(xterm): keep a live composition across a lone Cmd press on macOS

CompositionHelper.keydown exempted keyCodes 16/17/18 from tearing a composition
down, which covers Shift/Ctrl/Alt but not macOS Meta — 91/93 in Chromium, 224 in
Firefox. A lone Cmd press mid-composition therefore reached
_finalizeComposition(false), which dropped the preedit overlay's `active` class
and committed the live syllable early. macOS keeps the marked text alive across
that press, so no later compositionstart re-arms the overlay and the rest of the
word composes invisibly.

Measured on hardware (m4air, macOS 26.5.2, Apple M4, 2-Set Korean) with the Cmd
posted as a CGEventType.flagsChanged, which is what a physical modifier emits.
AppleScript `key code 55` posts nothing a browser can see — a bare `key code 56`
for Shift is equally silent — which is why no capture in the corpus ever reached
this branch. Three arms, same build otherwise: overlay live throughout with the
exemption, dark and prematurely committed without it, live again with it
restored. Evidence under
.tmp/ime-handoff/swarm-scratch/wave31-cmd-preedit/evidence/.

The fix cannot widen past a lone modifier: only a standalone press reports these
keyCodes, and a Cmd chord during composition is reported by Chromium as 229,
which was already exempt. Cmd+A still ends the composition, via the IME's own
compositionend. Ghostty draws the same line, returning early from flagsChanged
under hasMarkedText() for every modifier including Super.

Orca's terminal pane was never affected — shouldSuppressTerminalModifierKeyboardEvent
drops a standalone Meta keydown before xterm sees it, and deleting only 'Meta'
from that set is what flipped the hardware arm to broken. The popout preview
terminal and mobile's webview install no such guard and did reach the teardown.

terminal-ime-xterm-composition-commit-overlap.test.ts asked its fixer to update
the two Cmd arms to the values it named as correct; both now emit a single ['한'].

* test(native-chat): drop two byte-identical duplicate cases

`4632b86919d` copy-pasted two cases twice into the same describe block:
`retains carry across a same-frame non-Enter keyup before redispatch` and
`expires carry before a deliberate Enter after the next frame`. Each pair is
byte-identical — same title, same body — so the copies asserted nothing the
originals did not.

This is what has been failing `static analysis` on this branch since 2026-08-06:
`oxlint vitest(no-identical-title)` reports both under `--deny-warnings`, and
`verify` fails solely because it requires static analysis to pass. Every other
gate in `verify` was already green, including typecheck, xterm patch sync, the
full test shard set, and both package jobs.

12 cases still pass in the file.

* test(e2e): skip the WebGL arm when no WebGL renderer exists

The #12164 probe runs two arms, webgl and dom, and closes by asserting the
active renderer is the requested one. That assertion is right for the dom arm —
it is what proves the pane actually left WebGL, without which the arm is
meaningless — but headless CI has no GPU, xterm falls back to DOM silently, and
the webgl arm then fails.

The failure reads as a Korean rendering defect and is not one, so the webgl arm
now skips with the active renderer named. The dom arm keeps the assertion
unchanged.

This is the third of three checks that have been red on this branch since
2026-08-06. `static analysis` and `verify` were fixed in c51c6b5837e; the CI log
shows this job as 1 failed / 1 passed, the pass being the dom arm.

* chore(lint): drop five unused no-console disable directives

`check-changed-code-quality` reports unused eslint-disable directives as errors,
and these five sat above diagnostic `console.log` calls in IME test and spec
files where `no-console` is not enabled — so each suppressed nothing.

This is the second of the two static-analysis steps. `c51c6b5837e` fixed
"Enforce focused code-quality plugins" (duplicate test titles); this fixes
"Enforce changed-code quality". Both had been red on this branch since
2026-08-06, and I mistook the first for the whole job.

The diagnostic logs themselves are kept — they are what a failing IME arm prints
for a reader to inspect.

* test(e2e): cover #12164 under fractional device scale factor

Fractional display scaling was #12164's last unexplored branch, and the reason
is worth recording: earlier attempts were BLOCKED, correctly, because they
proposed mutating the Windows display scale on a remote physical machine with no
console recovery. `--force-device-scale-factor` reaches the same renderer state
per process, so nothing outside the Electron instance changes and there is
nothing to restore.

The hypothesis was specific: `프프로로젝젝트트` is what a half-pixel cell boundary
could produce on a 2-column glyph, and nothing else in the suite varies dpr.

Measured at 1.25 and 1.5, both under WebGL: ink extents 25/21/16 with identical
ink groups, matching the scale-1 run. No doubling.

The arm self-certifies before asserting — if the flag does not take, the test
fails rather than silently measuring at dpr 1. That matters here: the sibling
spec's WebGL arm went two days reporting a missing GPU as a Korean rendering
defect precisely because a silent fallback looked like a result.

* fix(terminal): match Mod+letter shortcuts by physical key, not IME-rewritten key

With a CJK input source active, macOS and Windows report the physical key through
`code` but rewrite `key` to the layout's character: Korean 2-Set turns Cmd+C into
`{ key: "ㅊ", code: "KeyC", metaKey: true }`. Every `key.toLowerCase() === 'c'`
match misses it, so the shortcut is not recognised and xterm encodes the chord as
PTY input instead — issue #13033 reports `ESC[12618;9u` and a terminal that jumps
to the bottom, because user input scrolls the viewport.

This is the same key-vs-code confusion that owned #12171, where a `Shift+T`
typing ㅆ was read as Enter for want of a `code` guard, so the fix is the same
shape: trust `code` when it is present, fall back to `key` and then the legacy
`keyCode` when it is not (Chromium omits `code` on synthetic and some keypress
events, and `keyCode` keeps its US value even when `key` is rewritten).

Applied to the four terminal-side sites, including the dashboard pop-out, which
#13033 called out specifically as having its own key handler:
  pty-connection.ts            Cmd/Ctrl+C copy guard
  keyboard-handlers.ts         Cmd+G search navigation
  agent-interrupt-inference.ts interrupt inference
  preview-terminal-key-handler.ts  pop-out paste

Nine further `key.toLowerCase()` letter matches exist outside the terminal
(TaskPage, editor, GitHub composer, browser markup). They have the same defect
and are deliberately left for a separate change rather than widening this one.

An existing case, `matchSearchNavigate > returns null for wrong key`, overrode
only `key` and left `code: 'KeyG'`, so it began passing for the wrong reason. It
now overrides both — which is what "wrong key" means once matching is physical —
and a companion case pins the Korean-rewritten chord still matching.

#13033 was closed NOT_PLANNED; the reporter's event shapes drive the new test.

* fix(renderer): match every Mod+letter shortcut by physical key, not IME-rewritten key

Completes the previous commit. A CJK input source rewrites `event.key` while
`event.code` keeps the physical key, so `key.toLowerCase() === 'z'` and friends
silently stop matching — the shortcut is not recognised and the keystroke falls
through to whatever handles unclaimed input.

The helper moves to `@/lib/ime-latin-shortcut-key` first: it now serves the
editor, GitHub composer and browser markup, and importing terminal-pane
internals into those would be the wrong direction. `lib/` already hosts
`ime-composition-keyboard-event` for the same reason.

Nine remaining sites, all previously unreachable under Korean/Japanese/Chinese/
Vietnamese input:
  TaskPage, ActivityPrototypePage, ProjectViewWrapper   Cmd+F search
  useMarkupKeyboardShortcuts                            Cmd+Z undo
  GitHubMarkdownComposer, RichMarkdownLinkBubble,
    rich-markdown-link-shortcut                         Cmd+K link
  native-chat-shortcut                                  Cmd+J
  rich-markdown-key-handler                             Cmd+Shift+X

Six of the nine test `!== 'letter'` as early-return guards and three test
`=== 'letter'`; the negation is applied per site, since a blind substitution
would have inverted six of them.

Full suite: 4249 files pass. Three files fail locally and none is caused by this
change — the branch touches no file under `src/main/` or `src/relay/`, all four
failures reproduce on an unmodified tree or pass in isolation (the worktree
poller passes 21/21 alone, so it is full-suite parallelism), and all 16 CI test
shards are green.

* docs(ime): scope the IME composition rules to the terminal-pane directory

#11893 proposed adding these to the root `AGENTS.md`, which every agent loads on
every task regardless of what it is doing. They only bind keyboard handling, the
composer and the terminal input path, so they belong next to that code —
`tests/e2e/AGENTS.md` already establishes the nested convention here.

Kept from #11893: range-derived commits, guarding above the key dispatch,
the `attachCustomKeyEventHandler` / `CompositionHelper` interaction, no
normalization at commit, and the recorded-trace evidence bar.

Added from defects found since it was written:
- match shortcuts on `event.code`, not `event.key` (#12171, #13033)
- `keyCode === 229` means an IME owns the press
- do not unmount a field mid-composition, and hiding is not a fix because
  `display:none` blurs and aborts it too (#12118, STA-3219, #11332)

The evidence bar now also names the mutation check, since a test that survives
deleting the code it guards is guarding nothing — a failure this effort hit more
than once.

* fix(terminal): gate Ctrl+Enter CSI-u on a negotiated pane, porting #12462

Found while scoping the rebase onto `main`: #12462 landed on 2026-08-06 and
fixes a real defect this branch does not carry. Ctrl+Enter emitted
`\x1b[13;5u` unconditionally, so a pane that never negotiated the kitty
keyboard protocol — local Windows ConPTY, plain shell — printed the escape
verbatim into the prompt.

This branch deletes `terminal-ime-deferred-newline.ts`, which is one of the
files #12462 touched, so a rebase resolving those conflicts by taking our side
wholesale would silently reintroduce the defect. Porting it forward now means
the fix survives the rebase however the conflicts are resolved.

Mirrors the Shift+Enter guard already here: local ConPTY falls back to the
legacy CR every emulator sends, and a negotiated pane keeps the chord, so the
fallback is scoped to panes that cannot receive CSI-u rather than to Windows.

NARROWER THAN #12462 BY ONE CONDITION, deliberately. `main` also allows CSI-u
via `hasCtrlEnterCsiUAuthority()` (trusted consumer evidence, #12329); that
helper and its plumbing do not exist on this branch. Omitting it is the
conservative direction — an authorised pane gets `\r` instead of the chord,
rather than an unnegotiated pane printing an escape — but it should be restored
when the two histories are reconciled.

Test covers both directions and is mutation-checked: forcing the gate open
fails it, so it cannot pass by construction.

* fix: reconcile two more fixtures main moved while the stack waited

Both caught by CI, not locally, and the reason the local run missed one is
worth recording:

1. `browser-toolbar-profile-dialogs.ime-enter.test.tsx` did not pass
   `useNativeUserAgent` / `onUseNativeUserAgentChange`, which `main` added to
   `BrowserToolbarProfileDialogsProps`.

   Local `pnpm typecheck` reported 0 errors on the same commit CI failed. The
   cause was a stale `config/*.tsbuildinfo` — tsc reused an incremental cache
   from before the merge. Deleting it reproduced CI's error exactly. Any
   "typecheck clean" during this merge should be treated as unverified unless
   the cache was cleared first.

2. Localization keys for `SshDisconnectedDialog` were absent from `en.json`:
   the merge took this branch's component alongside `main`'s catalog.
   Regenerated with `pnpm run sync:localization-catalog` rather than hand-added.

* fix(mobile): regenerate the lockfile the merge resolved by taking one side

CI's `verify` failed with `ERR_PNPM_OUTDATED_LOCKFILE` on `mermaid (lockfile:
11.16.0, manifest: 11.16.1)`. The mismatch was in `mobile/`, not the root — the
root lockfile was consistent throughout, which is why inspecting it (and even
GitHub's merge ref) found nothing wrong.

Cause: during the merge I resolved `mobile/pnpm-lock.yaml` by taking this
branch's side wholesale rather than merging it, so it kept `mermaid 11.16.0`
while `mobile/package.json` came from `main` at `11.16.1`. Taking one side of a
lockfile is only safe when the corresponding manifest comes from the same side.

Regenerated with `pnpm install --lockfile-only`; `--frozen-lockfile` now passes
in `mobile/`. Verified the xterm patch entry survives intact — same hash
`4f1b42d268f3964d…` and the parent-relative path into `config/patches/`, which
is the desktop/mobile coupling that would silently break the mobile build.

Two earlier diagnoses of this failure were wrong and are worth recording: it was
not the root lockfile, and it was not a stale merge ref (a rerun reproduced it
exactly).

---------

Co-authored-by: Orca <help@stably.ai>
2026-08-07 22:58:40 -07:00
Jinwoo Hong cf16eac7f6
fix(agent-hooks): keep Node 18 relay companion loadable (#13135)
Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com>
2026-08-07 22:54:42 -07:00
Neil bba32bd00c
fix(daemon): let the publisher replace a dead endpoint, not a third party (#12882)
Terminals froze app-wide several times daily, needing a manual pkill. libuv
unlinks the pathname a server bound to when it closes, with no ownership check,
so a departing daemon deleted whichever socket then sat at the canonical path —
including a live replacement's. The replacement kept hosting PTYs no client
could reach.

#12709 fixed that mechanism; this replaces the shape around it. Two invariants:
only a daemon publishing itself onto the canonical endpoint may mutate that
entry, and only by replacing one it has itself just proven dead; and no actor
removes a name it did not create.

Publish binds a private name, takes the canonical one with an exclusive link,
and on EEXIST proves the incumbent dead by connecting before replacing it in a
single rename. Only 'connected' means occupied and only refused/missing prove
death — a timeout proves nothing and declines. Deletes the claim sweeper, the
reclaim tail of killStaleDaemon, and three unfenced unlinkSync(socketPath)
calls in the launcher.

Measured: rename exposed no gap across 6,525 darwin / 8,004 linux probes of a
live handover, where unlink-then-link gapped on 200 of 200.

Verified on all three platforms: full suite on macOS and Linux, and daemon
restart e2e on a real windows-2022 host. Contract in src/main/daemon/AGENTS.md.
2026-08-07 22:33:50 -07:00
Asurada de4f272b31
fix(i18n): standardize Chinese status bar usage labels (#12881)
* fix(i18n): standardize Chinese status bar usage labels

Signed-off-by: ousugo <dkzyxh@gmail.com>

* fix(i18n): align Antigravity usage description

Signed-off-by: ousugo <dkzyxh@gmail.com>

* fix(i18n): standardize the zh status bar usage labels the menu actually renders

The status bar item menu renders "<Brand> Usage" for eight providers. Claude,
Codex and Gemini read 使用情况; Antigravity, OpenCode Go, Kimi, MiniMax and Grok
read 使用量, so one dropdown showed two words for one concept.

Register the decision where the repo already keeps it — the zh block of
locale-value-overrides.mjs already pins Claude/Codex/Gemini Usage — so the
repair pass enforces it instead of the catalog drifting again, and add the
missing Kimi entry to BRAND_MISTRANSLATIONS so 基米 can no longer come back.

---------

Signed-off-by: ousugo <dkzyxh@gmail.com>
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
2026-08-07 22:08:22 -07:00
Neil c3bf22b9a8
[P2] perf(windows): stop the capability poll respawning blocking wsl.exe probes (#11698)
* perf(windows): stop the capability poll respawning blocking wsl.exe probes

#11295 added a 30s renderer interval to `useWindowsTerminalCapabilities` whose
early-return only fires when WSL is available with at least one distro, so on the
common Windows host (no WSL) it re-ran a full capability read forever. Each read
IPCs four probes whose main-process handlers were synchronous `execFileSync` calls
to wsl.exe/pwsh.exe, blocking the Electron main event loop for up to 5s a time.

The un-latching intent is kept: a host that answers "no WSL" is still re-checked,
now on an exponential backoff (30s, +60s, +120s) that parks once the answer stops
moving, re-arms on window focus, is shared by all consumers of an owner key, and
stops entirely when the last consumer unmounts. The wsl/pwsh IPC handlers now use
async twins that share the existing caches and back off identically.

* fix(windows): classify async wsl/pwsh probe failures with the execFile error shape

The async twins feed `execFile` callback errors into classifiers written for
`execFileSync`: a non-zero exit lands on `error.code` as a number rather than
`error.status`, and a timeout is a SIGTERM kill rather than ETIMEDOUT.

So a Windows host without WSL (wsl.exe ships in System32, so it exits non-zero
instead of ENOENT) was cached as retryable, shrinking the shared window from
10min to 45s and making the still-sync callers re-pay their blocking spawn ~13x
more often; and a pwsh cold start past 5s cached "pwsh missing" for 30s,
demoting the user's PowerShell 7 preference — the exact case the ETIMEDOUT
branch exists to prevent.

Also drops a literal NUL byte from the new re-probe module's signature
separator, which made the file binary to git, and seeds `lastProbeAt` at
registration so focus churn right after mount cannot defer the first re-probe
indefinitely.

Co-authored-by: Orca <help@stably.ai>

* perf(windows): route relay host-capability probes through the async wsl/pwsh twins

A paired web/mobile client resolves `useWindowsTerminalCapabilities` to a local
target (TabBar's `isWebClient` gate, and `useSettingsNavigationMetadata` forces
`{kind:'local'}`), so the new re-probe arms there too. But `window.api.wsl/pwsh`
on a web client is not the ipc/app.ts channel — it is `host.wsl.*`/`host.pwsh.*`
over the runtime RPC, which still ran the sync probes and blocked the desktop
main event loop on `execFileSync('wsl.exe' | 'pwsh.exe')` for up to 5s per call.

Switch those handlers and the relay preflight capability probe to the async
twins added here; they share the same caches, dedupe and backoff, so remote
callers see no behavior change.

* fix(windows): harden async capability reprobes

* fix(windows): dedupe PowerShell shell probes

---------

Co-authored-by: Orca <help@stably.ai>
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-08-07 21:59:24 -07:00
Jinwoo Hong 4b5157b147
fix(codex): bound state DB recovery retries (#13109)
Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com>
2026-08-07 20:37:50 -07:00
Jinwoo Hong 2f30eb9af5
fix(ai-vault): block deletion of live sessions (#13108)
* fix(ai-vault): block deletion of live sessions

* fix(ai-vault): retain external session authority

---------

Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com>
2026-08-07 20:36:23 -07:00
Jinwoo Hong 2396e5e3e5
fix(browser-pane): reschedule remote stream restart with bounded backoff (STA-3483) (#12787) 2026-08-07 15:01:11 -07:00
Brennan Benson f0443c326a
fix(codex): recover interrupted state DB backfills (#12617)
* fix(codex): recover interrupted state DB backfills

* fix(codex): detect mixed-case backfill timeout

* fix(codex): harden backfill recovery review findings

* fix(codex): keep process identity retries safe
2026-08-07 14:06:29 -07:00
Brennan Benson bc1e049b3f
fix(terminal): defer metric option writes to unmeasurable panes (#12944)
* fix(terminal): defer metric option writes to unmeasurable panes

Writing fontSize/fontFamily/fontWeight/lineHeight makes xterm re-measure
cell size against the pane's current box. A hidden or mid-layout pane can
measure a wrong-but-nonzero size, which latches (hasValidSize) and mis-keys
the shared WebGL glyph atlas until a manual resize — the stuck variant of
the P0 bold/blurry-font reports.

Metric writes now land only on measurable panes; otherwise the latest
values park per-pane and flush on the next safe fit or reveal (with a refit
on the light tab-resume path, which otherwise skips fitting). Measurability
helpers move to pane-fit-measurability.ts to stay under the pane-fit.ts
line cap.

* fix(terminal): key metric deferral by terminal, not pane view

getPanes() returns a fresh toPublicPane() wrapper per call, so a
WeakMap keyed on ManagedPane never matched across call sites: deferred
metric options were dropped, not deferred. Key on pane.terminal, which
is carried by reference and dies with the pane.

Also from review:
- flushDeferredPaneMetricOptionsIfMeasurable checks the pending WeakMap
  before the measurability probe, so the common no-deferral case costs
  zero forced style/layout on every reveal.
- applyTerminalAppearance skips the apply (and the probe) when all five
  values are already live and nothing is parked; any settings write
  re-runs the pass over every mounted pane, and arming a no-op deferral
  would trigger a refit on the next reveal.
- fitRevealedPane flushes first: its pixel/grid checks can both no-op
  and return without fitting, stranding parked options.
- Font zoom folds its direct fontSize write into any pending deferral so
  the flush inside safeFit cannot clobber the user's zoom.

Corrects comments that asserted a cell-size re-measure mechanism xterm
does not have: CharSizeService measures via OffscreenCanvas TextMetrics,
independent of the pane box, and only fontSize/fontFamily re-measure.

Test fixtures now allocate a fresh pane view per getPanes() call, which
is what production does and what hid the keying bug.

* fix(terminal): re-check the fit floor after a metric flush

performSafeFit evaluated the min cols/rows gate with the pre-flush cell
size, then flushed and fit unconditionally. A large font jump on a
narrow pane passes the gate at the old size and lands under it at the
new one, so fit() pinned the PTY to the tiny grid the floor exists to
reject. Re-check after a flush that actually landed.

The parked values still apply, so the pane is never stuck on stale
metrics; only the fit is skipped.

* fix(terminal): route a reveal metric flush through the stable fit

fitRevealedPane's new flush branch called safeFit directly, which is
exactly what the function's contract forbids on reveal: resumeRendering
has just re-attached WebGL, whose cell metrics transiently differ from
the DOM renderer's, so a raw fit can propose a one-column-off grid and
reflow — and xterm's wrap/unwrap is not a perfect inverse, leaving a
diff-painting inline TUI corrupted.

A landed flush leaves pixels unchanged with a diverged grid, the same
shape as a snapshot resize, so it takes the same steady-grid repair.
A real resize still fits synchronously, after the flush.

Reachable via window wake, which calls fitAllRevealedPanes with no
pre-flush loop.

* fix(terminal): gate metric writes on the pixel box, not the fit floor

canApplyPaneMetricOptions reused canMeasurePaneForFit, whose >=8 cols /
>=4 rows floor exists to stop a fit pinning the PTY to a sliver. But the
divider clamp is 50px, which clears the 48px pixel floor and proposes
~5 cols — so a pane dragged to the clamp deferred every font change and
never flushed: it never hides, and its box never changes, so no reveal
and no ResizeObserver entry ever arrives. It rendered a stale font until
widened, where pre-PR the write was unconditional.

Gate metric writes on display plus the pixel box only. Hidden panes and
the transient worktree-switch overlay are near-zero, so they still
defer — the deferral's purpose is unchanged. The cols/rows floor stays
on the fit, including the post-flush re-check in performSafeFit.

Apply and flush share the same predicate, so no "applies but never
flushes" state can open up.

* fix(terminal): flush heavy reveal metrics after WebGL resume
2026-08-07 10:14:37 -07:00
Jinwoo Hong ddf58d6d6a
fix(terminal): restore preserved remote PTYs after host relaunch (#12990)
* fix(terminal): foreground preserved daemon PTYs

* fix(terminal): keep snapshot sequence domains distinct

* test(terminal): use active reconnect control

* test(terminal): await reconnect control activation

* test(terminal): validate reconnect with fresh control

* test(terminal): tighten host restart evidence

* fix(terminal): retry preserved PTY attach after inventory

* fix(terminal): retry attach after overlapping inventory

---------

Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com>
2026-08-07 01:25:18 -07:00
Wooseong Kim f057cbc85f
fix(serve): recognize CLI-form serve args on the Electron process (#12818)
* fix(serve): recognize CLI-form serve args on the Electron process

When the binary is launched as `… serve --port …` without the CLI rewrite
that injects `--serve`, normalize argv so isServeMode, headless GPU flags,
and serve option parsing all engage.

Preserves existing `--serve*` flag behavior for the CLI-spawned path.

Fixes #12677

* fix(serve): treat only CLI subcommand position as serve

Parse bare `serve` as the first positional token after flags/values so an
option value named `serve` cannot enable headless mode.

Addresses CodeRabbit on #12818.

* fix(serve): keep CLI redirects ahead of the serve argv rewrite

Rewriting argv before maybeRedirectAppImageCliLaunch replaced the `serve`
positional with `--serve`, so the redirect's command-name lookup saw a port
number and bailed — dropping AppImage serve launches out of the CLI path.

Also translate `--port=6768` (the CLI accepts it, getServeOptions only reads
the next token) and the mixed `--serve --port` form, so a security-shaped flag
like `--no-pairing` can no longer read as accepted while pairing stays on.
Map lookups replace `in` on object literals, which turned a stray `serve
toString` positional into a function spliced onto argv.

* fix(serve): close the CLI-form serve gaps found in review

second-instance: shouldActivateDesktopForSecondInstance matched only `--serve`,
so a duplicate `<binary> serve --port …` — the ExecStart shape documented in
docs/reference/headless-linux-server.md — promoted the live headless server to a
desktop window, un-fixing #11935 on exactly the launch shape this PR legitimizes.

findServeSubcommandIndex consumed a flag's value unconditionally while the
rewrite consumed it only when the next token was not flag-shaped. The two could
disagree and swallow the `serve` token, leaving `--serve` uninjected: #12677
again in a new shape (`--port --port serve`, `--port -- serve`). Both scans now
share one definition of value consumption.

`<binary> serve --help` / `serve help` bound a network-exposed runtime server
with pairing on and printed nothing; the AppImage redirect already routes those
three tokens to the CLI, so refuse them here too.

`--no-pairing=false` translated to `--serve-no-pairing` with the value dropped,
disabling pairing for an operator who asked for the opposite. The CLI reads its
serve booleans as `flags.get(name) === true`, so a boolean is now translated only
in its bare form and the `=` form rides through as the CLI treats it.

Tests: spec-derived parity between src/cli/specs/serve.ts and the rewrite,
covering both ends of the contract (serveOrcaApp and getServeOptions); a
source-text lock on the index.ts redirect/rewrite ordering, which reverted
silently green before; an exhaustive self-consistency property test; and the
real GUI launch argv shapes that must never enter serve mode.

---------

Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com>
2026-08-06 23:56:34 -07:00
Jinwoo Hong c9485fdded
fix(computer): fence macOS HID coordinate clicks (#12981)
Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com>
2026-08-06 21:18:52 -07:00
Jinwoo Hong 87d768058c
fix(terminal): recover stale persisted owner bindings (#12976)
Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com>
2026-08-06 21:18:41 -07:00
Neil f2d62a7887
fix(i18n): localize the status bar Resource Manager tooltip and remote-host count (#12478)
* fix(i18n): localize the status bar Resource Manager tooltip and remote-host count

The Resource Manager tooltip/aria label and the SSH segment's host count were
assembled from bare English literals inside helper functions, so they stayed
English under every non-English UI language while the labels around them
translated. Route them through the catalog with _one/_other plural keys and
whole-line messages (locales reorder and repunctuate the summary), and add
en/es/ja/ko/zh entries.

Root cause of the miss: audit-localization-coverage bailed on any ancestor
binary expression whose operator was not `+`, which hid every string under a
`cond && <JSX/>` guard or a `?? 'fallback'` — including this segment's
'Connecting…'. Only comparison operands are code, so keep `??`, `||` and
`&&` walking, and localize the four real strings that surfaced.

Co-authored-by: Orca <help@stably.ai>

* fix(status-bar): flag the space-scan tooltip row instead of matching its English text

The tooltip tinted a row with `line === 'Space scan ready'`, so routing that
copy through the catalog silently dropped the tint in every translated build.
Return `{ text, emphasized }` and let the segment read the flag.

Adopted from #12439 by @smwbev.

Co-authored-by: Evgenii <smwbev@users.noreply.github.com>

Co-authored-by: Orca <help@stably.ai>

* fix(status-bar): key Resource Manager tooltip rows by role instead of array index

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
2026-08-06 19:14:15 -07:00
Jinjing 8ddf575fe6
Revert "Remove source control group order preference (#12785)" (#12955)
This reverts commit ae1ed5e886.
2026-08-06 16:33:42 -07:00
Neil a7ffb244e4
perf(terminal): bound the reattach payload agent-signal scan (#12681)
hasCursorAgentReattachPayloadScreenSignal built a char-by-char copy of the
entire reattach payload so it could read the last header plus 5000 chars. On a
2MB daemon snapshot that cost 17.5ms of synchronous renderer main-thread work —
~75% of what xterm then spends parsing the same bytes — and the miss case paid
it in full for a result that is always false.

Two changes, both matching existing in-tree precedent: bound the scan to a
256KB tail (as the kitty tracker already bounds its own scan), and strip via
the shared precompiled CSI_SEQUENCE_PATTERN instead of a hand-rolled loop,
which is also faster in V8 because it copies spans rather than building a rope
per character.

  2MB snapshot, header hit   17.5ms -> 0.80ms  (22x)
  2MB snapshot, miss          8.7ms -> 0.52ms  (17x)
  200KB snapshot, header hit  1.5ms -> 0.62ms  (2.4x)

config/scripts/terminal-reattach-payload-scan-benchmark.mjs reproduces this and
asserts every candidate agrees with the baseline before timing it. It also
records a negative result: porting the daemon mouse mirror's includes()
pre-filter to the kitty tracker makes reattach slower, because snapshots always
contain the introducer.

Adds guards for the two behaviours a future shortcut would silently break: a
CSI-split header must still match, and a header behind the tail bound must not.
Also byte-pins POST_REPLAY_REATTACH_RESET_KEEP_MOUSE, which shipped unpinned.

Co-authored-by: Orca <help@stably.ai>
2026-08-06 00:23:57 -07:00
Brennan Benson c2da0e47f9
fix(computer): deliver macOS coordinate clicks via the HID event tap (STA-3433) (#12839)
Mouse events posted with CGEventPostToPid reach the target app with no
window association, so AppKit never routes the press to a view: hover
states fire but the control is never activated, and the mouseUp is
dropped outright when posted back-to-back. Post click events to the HID
event tap instead (as keyboard synthesis already does), pace them, and
stamp mouseEventClickState so multi-clicks register.

Synthetic clicks now also report verification unverified/synthetic_input
from the helper itself, matching the other synthetic actions.
2026-08-05 22:14:18 -07:00
Jinwoo Hong b0ba51831c
Add per-worker model and effort overrides (#12851) 2026-08-05 21:17:45 -07:00
Neil 7da9368b78
fix(terminal): fence detached daemon endpoint ownership (#12709)
* fix(terminal): fence daemon endpoint ownership

* fix(terminal): clean failed daemon PID claims

* fix(terminal): close daemon ownership review gaps

* test(daemon): release startup IPC in boot smoke

* test(daemon): mirror production stdio in boot smoke

* fix(daemon): exit after rpc shutdown cleanup

* fix(terminal): make the socket name the daemon endpoint authority

The reported failure was a live daemon hosting PTYs that nothing could
reach: terminals acknowledged input and never ran it, listings diverged
from reality, and restarting the app never helped because the detached
helper survived. The ownership fence added for it could not fire in the
sequence that produces the split brain.

libuv unlinks the pathname a server bound to when that server closes,
with no ownership check. A daemon that lost its endpoint name therefore
deleted whichever socket then sat at that path — including a live
replacement's — stranding a daemon that still hosted every session.
Bind a private same-directory name and hard-link it into place instead:
libuv can only ever unlink our own bind name, the exclusive link is a
kernel-enforced endpoint claim, and the canonical name is removed only
under an inode ownership check. The bind name replaces the basename
rather than extending it, so it cannot overflow sun_path.

killStaleDaemon removed the PID record unconditionally immediately
before every fork, so the exclusive PID claim was always uncontested at
bind time. It also unlinked a live daemon's endpoint whenever a connect
probe merely timed out, and treated a `ps` timeout as proof of PID
recycling. Now only positive evidence of a dead endpoint authorizes
reclaiming it, SIGKILL is confirmed rather than assumed, and a daemon
that cannot be proven stopped keeps its record and endpoint while the
launcher refuses to fork beside it.

A daemon whose endpoint was taken over now retires itself, draining
rather than killing, so an unreachable orphan stops being permanent.

A repaired PID record re-derives entryPath, appVersion and the Linux
incarnation markers from the authenticated owner instead of dropping
them; without appVersion a healthy daemon read as a permanently stale
bundle and, on Windows, went unpinned against daemon-host pruning.
Repair failure now fails open — abandoning a healthy daemon over a pid
file write cost every persistent terminal on the machine.

Also: treat only ENOENT as an unclaimed record so a Windows file lock is
not reported as an ownership conflict; settle start() before close() so
an accepted connection cannot defer it forever; sweep abandoned claim
and bind names; and type the endpoint-identity seam so a rename cannot
silently disable the fence.

Adds a real-process handover smoke that reproduces the failure with two
daemons racing one endpoint, and wires it into the native-smoke job.

* fix(daemon): retire only on proven endpoint ownership loss

The ownership watchdog read a null identity for any stat failure, so a
transient EACCES or EIO on the runtime directory would retire a daemon
that was still serving every terminal on the machine. Distinguish "the
entry is gone" from "the probe failed" and act only on the former.

Also require the loss to persist across two polls: a replacement
publishes by unlink-then-link, and a single observation can land in that
gap.

* fix(daemon): source repaired ownership metadata from the authenticated hello

Adversarial review found three defects in the previous two commits.

Re-deriving entryPath from the owner's command line truncated it at the
first space. A command line is a single space-joined string, so
`C:\Program Files\Orca\...` and `/Applications/Orca 2.app/...` came back
as `"C:\Program` and `/Applications/Orca`. getDaemonLaunchIdentity treats
a present entryPath as authoritative, so a healthy daemon read as
`different_app_path` and was killed and re-forked — worse than the
missing-metadata case the derivation was added to fix. Carry entryPath
and appVersion as optional fields on the daemon hello identity instead:
the daemon already has both from its own argv, and per
docs/reference/remote-wire-compatibility.md a new optional field is safe
because every reader falls back when it is absent. This also removes a
synchronous `ps` spawn from the Electron main thread during startup.

`start()` rolled back the PID record even when it never published one.
Losing the endpoint link now runs that path, and the ownership-checked
unlink briefly renames the incumbent's record aside — enough to strand a
live daemon's ownership. Roll back only what we actually wrote.

publishDaemonSocketPath read its identity from the canonical name after
linking, so a concurrent unlink returned null: no ownership watchdog and
no endpoint cleanup on any shutdown path. Read it from the bound name
before linking, which shares the inode.

Refusing to fork beside an unconfirmed daemon left the user with no
daemon at all and no in-app recovery, since restart re-entered the same
fence. We have just proved something answers the endpoint, so adopt it
in degraded mode: live sessions keep working, fresh terminals run
locally. SIGTERM is also individually guarded now — an EPERM fell into
the blanket catch and reported "nothing alive", authorizing the very
duplicate this fence exists to prevent.

Also reset the ownership-loss streak on an inconclusive probe so the
confirmations are consecutive, and sweep scratch names before the launch
so a failed launch still reclaims them.
2026-08-05 21:12:32 -07:00
Jinjing ae1ed5e886
Remove source control group order preference (#12785)
* Reorder source control to show staged changes first by default

Stages are closest to the commit action and most relevant to the
commit workflow. Merges untracked files into Changes visually while
preserving their Git area. Removes the untracked-first preset and
includes migration logic for existing user settings.

* Drop source control group order user preference

Remove the sourceControlGroupOrder setting and related UI, migrations, and persistence logic. The source control view now always displays sections in the order: staged changes, unstaged changes, untracked files.

* Reorder source control to show changes before staged

Aligns with the edit-stage-commit workflow by showing unstaged
changes (active edits) before staged changes (queued for commit).
2026-08-05 15:29:46 -07:00
Kyou 74ac7049ec
fix(windows): make managed grok-hook.cmd safe when GROK_HOME is unset (#11782)
* fix(windows): make managed grok-hook.cmd safe when GROK_HOME is unset

Fixes #9358 and #9941.

cmd.exe expands %VAR:~n,m% at parse time. When GROK_HOME is unset (default
outside Orca terminals), the generated length/trailing-backslash guards
became a syntax error and every Grok hook event failed with exit 255.

- Skip substring work when GROK_HOME is undefined (if defined + goto)
- Replace if "%x:~-1%"=="\" (itself a quote-parser bug) with findstr
- Extract Windows script builder; add template + spawn tests

* fix(windows): harden grok-hook GROK_HOME guards and tests

Address review on #11782:
- Inject grokHome via buildWindowsAgentHookPostCommand extra form lines
  (no fragile string replace of the shared payload line)
- Spawn tests delete GROK_HOME and keep PORT/TOKEN/PANE_KEY set so the
  GROK_HOME path actually runs before curl

* fix(windows): cover Grok hook home boundaries

---------

Co-authored-by: OrcaWin <alpha-eng@stably.ai>
2026-08-05 13:31:26 -07:00
Brennan Benson 4c49989c2e
refactor(codex): delete the unreachable managed shared-mirror lane (#12614)
PR 9501 shipped real-home routing for the host system default, and the
env override that could turn it back off was never a shipped control. The
managed-account half of the shared runtime mirror has been unreachable
since: every host account routes to its own self-contained CODEX_HOME
before that code runs.

Delete the flag module and its env plumbing plus the managed branch of
syncForCurrentSelection and the six helpers only it called. The three
lanes that still use the shared mirror -- Windows, a custom CODEX_HOME,
and a hook-lane gate that reports unusable -- are untouched, as are every
legacy migration and the WSL read-back helpers.
2026-08-05 12:57:02 -07:00
Brennan Benson 38ba22ecd1
fix(browser): align cookie import safeguards (#12607)
* fix(browser): align cookie import safeguards

* fix(browser): preserve sessions on failed cookie imports

* fix(browser): bound single-label cookie replacement

* fix(browser): preserve host-only parent cookies

* fix(build): bundle cookie scope parser
2026-08-05 12:51:05 -07:00
Jinjing fde816e4ee
move folders (#12758) 2026-08-05 12:09:24 -07:00
Jinjing d72daf8153
fix(release): admit PTY consumer in SSH watcher isolation gate (#12754)
#12746 only delivers pty.data after an authenticated openClient grant.
Update the macOS release harness to use a credentialed --connect client
and open a legacy session-owner so the gate still exercises watcher
isolation without timing out on the initial PTY echo.
2026-08-05 10:56:26 -07:00
Jinwoo Hong d15939c5fd
fix(terminal): recover rejected paired-runtime input (STA-2830) (#12675)
With a desktop client paired to a remote Orca runtime, terminal panes could report connected, writable, and `terminal.send` returning accepted — yet keystrokes never reached the agent. No error, no banner, no recovery; input silently vanished.

The ticket was really two bugs. The attach half was already fixed by #12589 (subscriber-driven daemon attach), confirmed by reproducing against current main. This fixes the remaining half: a write the host refuses had no way to tell anyone.

A capability-negotiated `WriteUnavailable` opcode carries that refusal back to the client, where it feeds the pane's pre-existing recovery hook. Capability gating matters because decoders reject unknown opcodes on desktop — and, worse, silently drop them on mobile — so the signal is negotiated in the subscribe handshake. Verified per direction: an old host strips the unknown Subscribe key, an old client omits it so the host never emits, and capability cannot be inherited across resubscribe.

Independent review then found the signal was being delivered and discarded: recovery demanded an authoritative liveness answer, and `pty:hasPty` had no `remote:` guard, so a paired pane's id fell through to the LOCAL provider, which returned false, and recovery bailed before remounting. Every test stopped at the transport boundary, so all of them passed while the pane stayed just as stuck. `pty:kill` already had exactly that guard.

The fix makes main answer LESS rather than claim more: `pty:hasPty` now returns unknown for a `remote:` id instead of a fabricated false, because main cannot speak for another host's PTY. The remount is then authorized by positive evidence — the process that owns the PTY stating it refused this specific write over a live negotiated connection — not by inference from silence. Local and app-SSH ids keep the probe, where a false genuinely means the shell died. Nothing is destroyed on this path; the remount rebuilds the renderer over the session it already had.

An end-to-end test now carries a rejected write from the host through to an actual remount, which no prior test did. A surviving mutant was also killed: the legacy-binary capability gate could previously be deleted with nothing turning red.

The reliability gate stays experimental — live paired journeys and mixed installed-release evidence remain uncollected. Fixes STA-2830.
2026-08-05 01:41:27 -07:00
Jinwoo Hong 06780260c0
test(remote-runtime): run an old client and an old server against current code (#12682)
Mixed versions are the normal state of the remote-server feature: users update clients and servers independently. Until now nothing tested that. Every cross-version claim was made by code reading plus unit tests with hand-written old/new shapes — enough to catch design problems, not enough to catch a real skew regression.

This runs the REAL protocol implementations from two builds against each other in one process: the actual host methods and RPC dispatcher on one side, the actual renderer multiplexer on the other, with a transport that reproduces the production asymmetry — each side decodes with its OWN codec and drops frames whose opcode it does not know. A frame survives only if the RECEIVING build understands it, which is what makes this level sufficient without launching two apps. The old side is a genuine checkout extracted from the release tag; the extracted client was confirmed to lack a symbol that exists only on main.

Journey: subscribe, first snapshot, input reaching the process, live output, hide/reveal snapshot, transport drop, resubscribe, input landing again — across old->new, new->old, and a current/current control. Every step ends on an observed-state barrier; no sleeps. The oracle asserts the recorded step list, the exact 16-frame named sequence, negotiated capabilities, the exact input the host wrote to the PTY, rendered content, and zero decoder-rejected frames. A host method the stub lacks is recorded by name and asserted empty, so a harness gap cannot masquerade as a wire break.

Detection is proven per violation shape, and it attributes each to the correct side: an unnegotiated opcode goes red only where a decoder would reject it, a removed published field goes red only where an old client consumes it, and a legal additive field stays green in all three pairings so the harness will not cry wolf on safe changes.

It also documents the three compatibility rules in docs/reference/remote-wire-compatibility.md, linked from AGENTS.md, since they previously existed only as folklore — notably that "decoders reject unknown opcodes" is true for the desktop decoder but NOT for mobile, which silently drops them.

Deliberately scoped: terminal stream only. The session-tab sync channel is not covered, nor agent-session publications, file/Git RPCs, mobile E2EE framing, or the relay transport. Two version points, so a regression introduced and reverted between them is invisible.

CI selection was verified rather than assumed — `vitest list` confirms 0 matches under the shard's exclude and 4 under the dedicated job — because a lane silently running zero tests is precisely how a host-side defect escaped CI earlier in this series. Closes STA-3469.
2026-08-05 01:31:29 -07:00
Jihwan Kim 0f9caf52b1
fix(ssh): time out stalled remote file streams (#11364)
* fix(ssh): time out stalled remote file streams

* test(ssh): cover stream dispose-listener cleanup

* fix(ssh): pause file stream deadlines during sleep

* fix(ssh): replay suspended state to late streams

* fix(ssh): allow slower file stream progress

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com>
2026-08-05 01:13:13 -07:00
Jinwoo Hong 84df99e2f1
test(serve): pin zero duplicate agent resumes across headless serve desktop promotion (#12666)
STA-1716 reported that a packaged `orca serve` could become the single-instance owner after the desktop app exits, leaving Dock/Finder unable to restore a window — and that forcing a reopen made the headless process hydrate a renderer that interrupted and DUPLICATED live agent sessions.

Verification against main found every criterion already fixed (#8646 for desktop promotion and the fail-closed CLI, #12212 for duplicate serve activation, #12574 + #9729 for the resume/ownership guards). The genuine gap was criterion 6: the ticket's own automated regression never existed. An existing reliability gate asserted PTY identity survives promotion, but nothing asserted what the incident was actually about — how many agents the promoted renderer resumes.

This adds that coverage: a unit/service-level journey that drives the real single-instance lock, activation gate, settle and focus paths, then runs the real resume logic against a store seeded as a renderer freshly mounted inside the serve process, asserting zero duplicate resumes.

`settleServeDesktopActivation` moved from `index.ts` into its own module with identical semantics, so the test drives the real decision rather than re-implementing it — the earlier repro had to mirror that logic locally, which is the "test passes without running the scenario" failure mode.

Proven to be a real oracle: breaking each guard individually turns it red, and reverting the pre-#12574 pane form reproduces the incident exactly (two duplicate `codex resume` tabs).
2026-08-05 00:34:10 -07:00
PLANDER DEV 7 cbc005c8aa
fix(remote-runtime): materialize the host surface when reconnecting a terminal pane (#11542)
Reconnect could never recover a terminal pane whose host-side process was gone (host restarted, or the workspace was never opened there): recovery only polled the tab inventory, which can never create the surface it is waiting for, so Reconnect spun for ~60s and gave up permanently.

Verified with a deterministic reproduction: on main the recovery path issues 51 inventory polls and zero activations across both an automatic online trigger and a manual Reconnect click; with this change the pane re-materializes, rebinds and accepts input.

Review found and fixed three further defects beyond the original change:
- an activation answered with a stale ready handle left the loop polling forever instead of re-activating;
- a non-missing activation failure (e.g. an older host without the method) never fell back to inventory;
- host-side, activating a parked surface permanently deleted the host tab, because an already-absent persisted binding was read as a competing owner *after* the destructive retirement had already run.

Independent review confirmed by mutation testing that every production change is covered by a test that fails when it is reverted, that only an authoritative inventory can retire a pane, that the loop is bounded under every failure mode, and that the unknown-liveness guard (proven death required before retirement) is intact.

Fixes STA-3002.
2026-08-04 22:59:06 -07:00
Brennan Benson 9507cbce0c
fix(terminal): rate-cap WebGL atlas recovery (#12622) 2026-08-04 20:42:11 -07:00
Neil 69ca9f91b3
fix(status-bar): invalidate the CLI session count on kill and restart (#12468)
* fix(status-bar): invalidate the CLI session count on kill and restart

`pty:management:killOne` / `killAll` / `restart` tear sessions down via `adapter.shutdown()` and broadcast nothing — unlike `pty:kill`, which ends in `sendPtyExitToRenderer`. The status-bar count is an event-sourced cache, so killing sessions from Manage Sessions or "Kill all terminals" left the `>_ N` chip frozen until the popover was opened, which itself triggers a refresh.

> [!NOTE]
> The dual-source split described in the issue text was already fixed by merged #9387. This closes a *different* remaining invalidation gap that produces the same reported symptom.

Broadcast the teardown so the chip updates without needing the popover opened.

Fixes #8372

Co-authored-by: Orca <help@stably.ai>

* test(e2e): add recordable proof for status-bar-cli-session-count

Fails on origin/main, passes on this branch.

Test: drops after Manage Sessions kills a foreign daemon session, popover never opened

Co-authored-by: Orca <help@stably.ai>

* fix(status-bar): avoid duplicate inventory refresh after kill all

---------

Co-authored-by: Orca <help@stably.ai>
2026-08-04 17:11:18 -07:00