[upstream #13228] fix(claude): guard resume restore commands - #165
Open
innocarpe wants to merge 1454 commits into
Open
[upstream #13228] fix(claude): guard resume restore commands#165innocarpe wants to merge 1454 commits into
innocarpe wants to merge 1454 commits into
Conversation
…tablyai#12963) * fix(terminal): wait for shell-ready before every SSH startup command Renderer-delivered SSH launches wrote the startup command after a flat 50ms, so a remote shell still reaching its prompt could drop it. Only Codex plans opted into waiting. Gate on whether a startup command exists rather than on which agent it names: an unready shell drops whatever is written to it regardless. The relay already gates its own delivery on the marker it armed; this mirrors that for the renderer path, bounded by the existing 1.5s fallback. * fix(terminal): arm SSH marker for every startup * fix(terminal): cancel stale SSH startup writes
…-3417) (stablyai#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
* refactor(task-page): extract pure task-kind, jira, and pagination helpers Moves seven closed sets of pure helpers out of TaskPage.tsx (13485 -> 13308 lines) into domain-named sibling modules. Function bodies are byte-identical cut/paste; the only production edits in TaskPage.tsx are the removed blocks and the new import statements. - task-page-github-task-kind.ts: isPRFocusedTaskView, normalizeGitHubTaskPreset, getGitHubTaskKind, getDefaultPresetForGitHubTaskKind, scopeGitHubTaskSearch - task-page-jira-create-fields.ts: the Jira create-field visibility, allowed-value, and payload builders - task-page-jira-project-selection.ts: getJiraProjectSelectionKey, compareJiraProjectsByDisplayLabel - task-page-jira-status-tone.ts: getJiraStatusTone - task-page-pr-delta-summary.ts: formatPRDelta - task-page-pagination-page-numbers.ts: getPageNumbers - task-page-string-set-equality.ts: areStringSetsEqual Each module gets a characterization test suite that pins current behavior, including the quirks (case-sensitive matching, truthiness-based option payload fallbacks, allowedValues winning over schema type). Quirks are documented, not fixed. github-enterprise-slug-routing-boundary.test.ts anchored its source-text sections on `function formatPRDelta` and `function getPageNumbers`; both moved, so the sentinels advance to the next declarations. The bounded sections and their assertions are unchanged. No intentional behavior change. * refactor(task-page): clarify helper logic and consolidate tests - Add comment explaining quoted-form parsing in GitHub task scope - Refine test descriptions and characterization comments for clarity - Add test case for quoted `is:"issue"` form in task scoping - Consolidate test files as part of pure-function extraction * improvements * Remove obvious comment from getPageNumbers The function name and implementation are self-documenting; the comment restates what the code already expresses clearly.
…tablyai#13155) * fix(terminal): keep the reattach grid push alive for a hidden pane A restored Floating Workspace pane is display:none for its whole PTY reattach, because the panel always mounts closed after a restart. The display:none guard in safeFitAndThen dropped the reattach continuation outright, so the PTY never received the client grid and no explicit SIGWINCH was sent to repaint the restored TUI after the replay. Parking the continuation is not an option: the structural replay coordinator holds live PTY bytes behind its promise, so a closed panel would stall output indefinitely. Settle the promise immediately instead and move the continuation to a per-pane deferred bucket that the first measurable fit drains. Deferral is opt-in so the bounded-degradation contract still covers every other caller, and the continuation re-checks mobile PTY ownership at fire time now that the wait is unbounded. * fix(terminal): preserve replacement deferred grid push
* fix(orchestration): release context-only dispatches Refs stablyai#13005 * test(orchestration): align PTY readiness timeout --------- Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com>
…istory (stablyai#12935) * feat(agents): add Prime Agent as a supported TUI agent with session history Wire Prime Intellect's prime-agent CLI (a Pi fork) into the desktop and mobile agent catalogs following the Trae registration pattern, and into the Agent Session History browser following the OMP pattern: - types.ts, tui-agent-config.ts: register 'prime-agent' with argv prompt injection behind a `--` separator (its own help documents `--` as "treat all following arguments as messages"; without it, prompts starting with `help`/`agents`/`-…` dispatch as subcommands or flags), plus csi-u Shift+Enter encoding matching the Pi TUI it embeds. - agent-kind.ts, telemetry-events.ts, agent-status-types.ts, agent-type-label.ts, tui-agent-display-names.ts, tui-agent-selection.ts, skills-cli-agent-keys.ts: standard per-agent registrations. - agent-headless-command.ts: `-p/--print` one-shot runs share the print-mode matcher with Claude/Trae so they are not mistaken for live interactive panes. - agent-process-recognition: the npm shim launches a generic bundled cli.js, so only the exact package path is an authoritative identity (same as Pi and cursor-agent). The three per-agent regex branches are now one table in agent-node-entrypoint-identities.ts — the module was at its max-lines budget and a table makes the next agent one entry. - AI Vault: sessions are Pi's message-graph JSONL under ~/.prime/agent/sessions (override PRIME_AGENT_CODING_AGENT_DIR — Prime Agent brands Pi's env contract instead of sharing PI_CODING_AGENT_DIR); parsed by the shared message-graph parser with incremental append-resume; discovered locally, in WSL homes, and over remote SSH; resumes by absolute transcript path (`prime-agent --resume <path>`) like OMP, with session-id fallback. - skill-discovery-sources.ts: ~/.prime/agent/skills home source. - Catalog, i18n (en/es/ja/ko/zh), mobile registries, and a bundled 64x64 favicon (required by mobile's offline-icon invariant). Scanner-test fixtures for OMP and Prime Agent move into session-scanner-test-fixtures.ts and the incremental fixture into its own module, keeping every touched file inside its max-lines budget without ratchet bumps. * fix(ai-vault): map custom Prime Agent roots to their sessions child PRIME_AGENT_CODING_AGENT_DIR is consumed verbatim by the CLI as its agent config dir, with transcripts always in <agentDir>/sessions — unlike PI_CODING_AGENT_DIR's <home>/agent/sessions shape the shared normalizer models. A custom root with a non-special basename (or a `.prime` leaf) was therefore scanned as-is instead of its sessions child. Dedicated normalizePrimeAgentSessionsDir appends `sessions` to every configured root, taking only an explicit `.../sessions` path as-is; the shared Pi/OMP normalizer drops the `.prime` widening it no longer needs. Raised in review on stablyai#12935. * fix(ai-vault): guard degenerate Prime Agent roots and cover the remote source normalizePrimeAgentSessionsDir stripped a filesystem-root value ('/' or '//') to '', which then joined into the relative root 'sessions' and would walk the main-process cwd. session-scanner-roots.ts already carries this guard for the OMP variant; apply the same fallback here. The remote SSH source had no test: deleting jsonlSource('prime-agent', ...) left the suite green, unlike the local path which is pinned by the AI_VAULT_AGENTS exhaustiveness assertion in session-scanner.test.ts. Add a case that fixes the .prime/agent/sessions root segments, the .jsonl extension, and parser routing. Raised in review on stablyai#12935. * fix(ai-vault): honor Prime Agent's sessions-root env and non-interactive modes Verified against upstream PrimeIntellect-ai/prime-agent source rather than inferred from the CLI's help text. config.ts getSessionsDir() reads PRIME_AGENT_SESSION_DIR (and its legacy PRIME_AGENT_CODING_AGENT_SESSION_DIR alias) ahead of the agent dir and uses it verbatim; setting either left the vault silently empty. It also appends `sessions` to the agent dir unconditionally, with no basename escape hatch, so PRIME_AGENT_CODING_AGENT_DIR=/data/sessions writes to /data/sessions/sessions while Orca scanned /data/sessions. getAgentDir() and the session-dir override both run through expandTildePath, so a `~` value set outside a shell resolves. cli/args.ts also spells the non-interactive runs `--mode json|rpc|acp|daemon`, which the shared print-mode matcher does not know, so those panes were counted as live interactive agents and the paste-submit path would write user text into a JSON-RPC/ACP stream. Match upstream exactly: only the space-separated form, since `--mode=json` is not parsed by the CLI and does start the TUI. Raised in review on stablyai#12935. * fix(ai-vault): keep Prime Agent roots absolute and remote segments posix Two holes in the previous commit. The degenerate-root guard only rejected pure-separator values, so a relative env value still resolved against the main-process cwd: PRIME_AGENT_CODING_AGENT_DIR='.' scanned '<cwd>/sessions' and, worse, PRIME_AGENT_SESSION_DIR='.' scanned the cwd itself. Require an absolute path in both branches and fall back to the default otherwise. remotePrimeAgentSessionsSegments() built its segments with the local-platform join, so on a Windows client scanning a posix SSH host it produced '\.prime\agent\sessions' and split('/') collapsed it to one bogus segment — remote discovery would have found nothing. Remote roots are posix regardless of client platform, so keep them literal. Pi and OMP are unaffected: their normalizer returns a '.../sessions' input unchanged and never joins. Raised in review on stablyai#12935. * test(ai-vault): pin Windows drive roots to the Prime Agent default fallback 'C:\' and 'C:/' strip to the drive-relative 'C:', which isAbsolute rejects on every platform — assert they land in the default fallback so a looser truthiness check can't reintroduce a 'C:sessions' scan root. Raised in review on stablyai#12935. * test(ai-vault): pin the drive-relative root form and state what the posix runner can assert --------- Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
…blyai#13132) * fix(terminal): restore canonical terminal rows from unified sessions During session hydration, non-canonical terminal rows (closed/stale tabs) were being restored alongside canonical ones. Filter to only canonical terminal rows from unified sessions, treating legacy rows as terminal backing data only. * fix(terminal): preserve independent legacy PTY rows * fix(terminal): stop canonical PTY filter from evicting live legacy rows Two regressions in the hydrateWorkspaceSession canonical-PTY filter: 1. canonicalPtyIds was built before isValidTerminalTabId ran, so a web-client mirror row with a "::" id seeded its PTY into the set and then evicted the valid local row owning the same PTY - wiping every terminal in the worktree and orphaning the daemon PTY, persisted back to disk on the next write. 2. `.some()` dropped an entire legacy split tab for a single overlapping leaf, discarding panes whose PTYs nothing else referenced. Only rows that survive the id check now claim ownership, and only pure duplicates (every PTY already canonical, guarded against PTY-less rows) drop. * fix(terminal): deduplicate rows with correct orphaned PTY handling - Extract row hydration to terminal-session-row-hydration.ts - Distinguish mounted vs orphaned PTY bindings when scoring ownership - Strip shared PTYs from retained rows to prevent duplicate reconnection - Follow dropped row focus to canonical twin; clean up orphaned agent sessions * fix(terminal): clear sleeping-agent records for invalid-tab-id rows Rows with invalid tab IDs are dropped like subsumed rows but were not getting their sleeping-agent records cleaned up. Extract common test mocks into a bootstrap file to reduce duplication. * fix(terminal): anchor retained rows on remaining leaf PTYs When a canonical row claims a retained row's tab-level PTY, the row still owns leaf-level PTYs in split panes. Anchor on the surviving leaf PTY so the orphan sweep doesn't delete the row before its pane reattaches (stablyai#10486). Also cleans up sleeping agent sessions for released leaves and improves PTY categorization. * persisted ptyIdsByLeafId keys are unvalidated strings, and makePaneKey throws on non-UUIDs.
Refresh README star history graphic to the latest ~40K stars curve (through August).
* fix(cursor): accept BOM-prefixed hook JSON * test(cursor): pin the hook BOM allowance to one leading U+FEFF Document why the BOM strip exists and cover the narrowness the fix claims: a double BOM, a whitespace-then-BOM prefix, and a BOM inside the JSON body are all still rejected. --------- Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
* fix: address pr-bug-scan validated finding from stablyai#4213 On per-file read/size error, skip the file and continue merging instead of returning found:false; preserve first error for the all-failed case. * add tests --------- Co-authored-by: orca-bug-scan-bot <orca-bug-scan-bot@stably.ai> Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
…3284) * 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>
… text survives (stablyai#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>
…atin (stablyai#12934) * fix(i18n): restore the Japanese renderings the brand revert left in Latin Before stablyai#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 stablyai#1799 and orca · zsh are pinned here too and stablyai#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>
…ai#11170 (stablyai#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 stablyai#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 stablyai#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>
…flag (stablyai#13310) * 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(e2e): assert IME preedit geometry headlessly for Korean and CJK input Both IME defects that shipped and were reverted walked through a suite of ~3000 passing assertions, because every one of them was about bytes reaching the PTY. A preedit rendered into a hidden overlay satisfies all of them while the user composes blind. The real-geometry coverage that would have caught it existed but was headful, env-gated and macOS-only, so it never ran in CI. Drives composition through CDP Input.imeSetComposition instead of a native input source, which removes the accessibility grant and the system input source that forced the headful gate. The suite runs in the normal headless project in about 55s serially, and asserts the composition overlay's real bounding rect — the one property an overlay clipped to max-width:0 cannot fake and a DOM emulator cannot produce. Three tests are red on main and marked test.fail() so they stay visible in CI and flip loud when their fix lands: the preedit resumed by a bare compositionupdate, and full-width punctuation and digits committed from a keydown that still carries the ASCII layout key. Co-authored-by: Orca <help@stably.ai> * test(e2e): drop the known-broken markers now the stack closes all three Validated on real macOS hardware: with the two fixes below this layer, all three report "Expected to fail, but passed". Korean preedit renders at non-zero geometry through every jamo, and an Apple pinyin source sends ef bc 8c e3 80 82 to the pty where main sends ASCII. Worth recording why the punctuation case looked green on main once: an input source whose id happens to contain an allowlist term, as Sogou's does, satisfies the old gate. Correctness there depended on which IME the user had selected. Co-authored-by: Orca <help@stably.ai> * test(e2e): cover the Linux and Windows IME ownership shapes headlessly The ten headless IME specs on this branch all decided ownership through a macOS user-agent override, so the two platforms whose failure mode is a *dropped* character rather than a downgraded one had no coverage at all, and the one Windows-recorded trace already in the suite was replayed under whichever policy the runner happened to report — macOS locally, Linux on the CI shards. Adds four Linux specs and two Windows specs, every one of them replaying a native capture rather than a hand-authored ordering: - IBus/X11 Hangul mixed with literal ASCII. Its `compositionend` is EMPTY and the syllable arrives afterwards as a bare `insertText`, so reading the commit off `compositionend.data` — which the Windows capture rewards — drops every syllable on this framework. - fcitx5/Wayland Hangul. No keydown at all for a composing key, not even 229, and physically wrong `code` values on the literal keys. Any ownership rule reading 229 or `code` fails here. - Numeric pinyin candidate selection under both frameworks, with the ordinary digit kept as the negative control, so the two directions are pinned against each other rather than separately. - Windows Microsoft Korean captured with real scan codes, including the two lines committed with Shift held. Each asserts both sides of the boundary: the preedit's real geometry at every frame the user would see, and the exact byte stream the native run put on the PTY. The recorded `onData` the Windows/WSL fixture already carried is now asserted instead of sitting unused. Chinese moves up to first-class alongside Korean: pinyin preedit width is now pinned the way the Japanese phrase already was, and full-width punctuation is covered in the composition-session shape the Windows and Linux frameworks use, not only the macOS insertText shape. Two harness fixes fell out of the recorded traces and are why the IBus one passes. The replay applied each event's recorded textarea state *after* dispatch, one event too late for the handlers that read `textarea.value`; and it left a task boundary between `compositionend` and the `input` carrying the commit, which Chromium never inserts, letting xterm's deferred finalizer settle against a textarea the committed text had not reached yet. Co-authored-by: Orca <help@stably.ai> * test(e2e): replay the recorded macOS IME shapes instead of only synthesising them The macOS coverage on this branch drives Chromium composition through CDP, which is genuine but hand-ordered, so it could not assert the one property that decides the macOS rule: a composing keydown arrives with keyCode 229 while `key` is still the single translated character the input source produced — `ㅎ`, not `Process` — which is indistinguishable by length from an ordinary printable key. Three native captures were sitting unused in the evidence set. Adds a recorded 2-Set Korean session, including the syllable boundary where one composition closes and the next opens with no keydown between them, asserted against its own recorded byte stream. Adds the third failure mode, which had no coverage in any shape: an abandoned preedit leaking to the shell. Pinyin and Cangjie both backspace a composition away to nothing, and the assertion is not "the right bytes" but "no bytes". Both cancellation captures continue with a literal `ordinary` typed as bare keydowns, the recorder's own negative control. That tail carries no `input` events because the build it was captured on produced the byte from the keydown itself, so replaying it would measure the recorder rather than the product; the specs cut at the `compositionend` and say so. Co-authored-by: Orca <help@stably.ai> * test(e2e): promote the non-allowlist input-source punctuation spec to the suite Qingg matches none of the terms the pre-structural build enumerated, so on that build its bypass never installs. It is the one arm no headless spec can express, and the only test here that the pre-structural build cannot pass. Promoted from scratch with four changes, each forced by a measurement rather than by taste: - A non-attached input method is a refusal, not a negative result. macOS attaches per app instance and the attach can simply fail — 3 of 6 instances under exclusive host access, and re-selecting the source did not recover one of them across 9 keystrokes. That is now `test.skip()` with a reason naming the rerun, not a thrown error, and the suite does not gate on a fully green session. - Attachment is probed with a LETTER. Punctuation substitution emits no compositionstart and no keyCode 229 even under a fully attached source, so at the keydown it is indistinguishable from having no source at all. The punctuation arms are judged on PTY bytes alone. - The ASCII-layout control is now part of the spec rather than a side experiment. Without it a build that rewrote every `.` into `。` unconditionally would pass the Qingg arm and be badly wrong. - The assertion runs by default instead of behind a strict-mode flag, and gained a non-vacuity check: the input source must have committed something. That is the sharp end of the mechanism — on the old build the DOM carries only keydown and keyup, so nothing is committed at all and the character is destroyed before the source is asked. The verdict stays an equality between two measurements, never a comparison against a hardcoded glyph, so it holds whatever punctuation mode the operator's input source happens to be in. It reads `beforeinput`, not `input`: the forwarder consumes `input` in the capture phase on the pane element, so a probe on the helper textarea never sees it and a strict run fails with correct bytes underneath. Co-authored-by: Orca <help@stably.ai> * feat(terminal): encode IME commits as CSI-u under the all-keys kitty flag A pane that negotiates `report_all_keys_as_escape_codes` (bit 3) asked for every printable key as a CSI-u report. The commit path wrote IME-committed text raw, so such a pane got a legacy byte stream it had declined. That predicate has no IME-specific condition, so it affected every macOS user in such a pane, not just CJK users. Encode the press that produced the commit instead, reusing xterm's own kitty encoder rather than hand-rolling CSI-u. `claimKeyEvent` is untouched: still unconditional, still structural, still no kitty read on the keydown. The flag read happens once per commit. The gate is bit 3 alone. Flags 1/2/4/16 leave printable keys as text, so panes negotiating only those keep receiving substituted characters; gating on "kitty active" would strip the substitution from every pane that negotiates anything. Known limit, pinned by test: the report carries the physical key's codepoint, not the committed glyph. Bit 3 is the app declaring it does not want text, and bit 4 is how it asks for text back — but xterm's encoder derives that text field from the same `key` it derives the keycode from, so carrying the committed glyph needs an encoder change, not a wider gate. * fix(terminal): report a held key's repeats as REPEAT under the kitty flags The commit encoder never passed an event type, so xterm's encoder applied its PRESS default to every auto-repeat keydown. A pane negotiating report_event_types alongside bit 3 saw one held key as N separate strikes. Carry the keydown's `repeat` on the claimed press and map it to the protocol's REPEAT. The event type only reaches the wire when report_event_types is negotiated, so this is inert for panes that asked only for bit 3. Co-authored-by: Orca <help@stably.ai> * chore: drop non-mergeable IME e2e scratch files --------- Co-authored-by: Orca <help@stably.ai>
… on Windows (stablyai#13313) * 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(e2e): assert IME preedit geometry headlessly for Korean and CJK input Both IME defects that shipped and were reverted walked through a suite of ~3000 passing assertions, because every one of them was about bytes reaching the PTY. A preedit rendered into a hidden overlay satisfies all of them while the user composes blind. The real-geometry coverage that would have caught it existed but was headful, env-gated and macOS-only, so it never ran in CI. Drives composition through CDP Input.imeSetComposition instead of a native input source, which removes the accessibility grant and the system input source that forced the headful gate. The suite runs in the normal headless project in about 55s serially, and asserts the composition overlay's real bounding rect — the one property an overlay clipped to max-width:0 cannot fake and a DOM emulator cannot produce. Three tests are red on main and marked test.fail() so they stay visible in CI and flip loud when their fix lands: the preedit resumed by a bare compositionupdate, and full-width punctuation and digits committed from a keydown that still carries the ASCII layout key. Co-authored-by: Orca <help@stably.ai> * test(e2e): drop the known-broken markers now the stack closes all three Validated on real macOS hardware: with the two fixes below this layer, all three report "Expected to fail, but passed". Korean preedit renders at non-zero geometry through every jamo, and an Apple pinyin source sends ef bc 8c e3 80 82 to the pty where main sends ASCII. Worth recording why the punctuation case looked green on main once: an input source whose id happens to contain an allowlist term, as Sogou's does, satisfies the old gate. Correctness there depended on which IME the user had selected. Co-authored-by: Orca <help@stably.ai> * test(e2e): cover the Linux and Windows IME ownership shapes headlessly The ten headless IME specs on this branch all decided ownership through a macOS user-agent override, so the two platforms whose failure mode is a *dropped* character rather than a downgraded one had no coverage at all, and the one Windows-recorded trace already in the suite was replayed under whichever policy the runner happened to report — macOS locally, Linux on the CI shards. Adds four Linux specs and two Windows specs, every one of them replaying a native capture rather than a hand-authored ordering: - IBus/X11 Hangul mixed with literal ASCII. Its `compositionend` is EMPTY and the syllable arrives afterwards as a bare `insertText`, so reading the commit off `compositionend.data` — which the Windows capture rewards — drops every syllable on this framework. - fcitx5/Wayland Hangul. No keydown at all for a composing key, not even 229, and physically wrong `code` values on the literal keys. Any ownership rule reading 229 or `code` fails here. - Numeric pinyin candidate selection under both frameworks, with the ordinary digit kept as the negative control, so the two directions are pinned against each other rather than separately. - Windows Microsoft Korean captured with real scan codes, including the two lines committed with Shift held. Each asserts both sides of the boundary: the preedit's real geometry at every frame the user would see, and the exact byte stream the native run put on the PTY. The recorded `onData` the Windows/WSL fixture already carried is now asserted instead of sitting unused. Chinese moves up to first-class alongside Korean: pinyin preedit width is now pinned the way the Japanese phrase already was, and full-width punctuation is covered in the composition-session shape the Windows and Linux frameworks use, not only the macOS insertText shape. Two harness fixes fell out of the recorded traces and are why the IBus one passes. The replay applied each event's recorded textarea state *after* dispatch, one event too late for the handlers that read `textarea.value`; and it left a task boundary between `compositionend` and the `input` carrying the commit, which Chromium never inserts, letting xterm's deferred finalizer settle against a textarea the committed text had not reached yet. Co-authored-by: Orca <help@stably.ai> * test(e2e): replay the recorded macOS IME shapes instead of only synthesising them The macOS coverage on this branch drives Chromium composition through CDP, which is genuine but hand-ordered, so it could not assert the one property that decides the macOS rule: a composing keydown arrives with keyCode 229 while `key` is still the single translated character the input source produced — `ㅎ`, not `Process` — which is indistinguishable by length from an ordinary printable key. Three native captures were sitting unused in the evidence set. Adds a recorded 2-Set Korean session, including the syllable boundary where one composition closes and the next opens with no keydown between them, asserted against its own recorded byte stream. Adds the third failure mode, which had no coverage in any shape: an abandoned preedit leaking to the shell. Pinyin and Cangjie both backspace a composition away to nothing, and the assertion is not "the right bytes" but "no bytes". Both cancellation captures continue with a literal `ordinary` typed as bare keydowns, the recorder's own negative control. That tail carries no `input` events because the build it was captured on produced the byte from the keydown itself, so replaying it would measure the recorder rather than the product; the specs cut at the `compositionend` and say so. Co-authored-by: Orca <help@stably.ai> * test(e2e): promote the non-allowlist input-source punctuation spec to the suite Qingg matches none of the terms the pre-structural build enumerated, so on that build its bypass never installs. It is the one arm no headless spec can express, and the only test here that the pre-structural build cannot pass. Promoted from scratch with four changes, each forced by a measurement rather than by taste: - A non-attached input method is a refusal, not a negative result. macOS attaches per app instance and the attach can simply fail — 3 of 6 instances under exclusive host access, and re-selecting the source did not recover one of them across 9 keystrokes. That is now `test.skip()` with a reason naming the rerun, not a thrown error, and the suite does not gate on a fully green session. - Attachment is probed with a LETTER. Punctuation substitution emits no compositionstart and no keyCode 229 even under a fully attached source, so at the keydown it is indistinguishable from having no source at all. The punctuation arms are judged on PTY bytes alone. - The ASCII-layout control is now part of the spec rather than a side experiment. Without it a build that rewrote every `.` into `。` unconditionally would pass the Qingg arm and be badly wrong. - The assertion runs by default instead of behind a strict-mode flag, and gained a non-vacuity check: the input source must have committed something. That is the sharp end of the mechanism — on the old build the DOM carries only keydown and keyup, so nothing is committed at all and the character is destroyed before the source is asked. The verdict stays an equality between two measurements, never a comparison against a hardcoded glyph, so it holds whatever punctuation mode the operator's input source happens to be in. It reads `beforeinput`, not `input`: the forwarder consumes `input` in the capture phase on the pane element, so a probe on the helper textarea never sees it and a strict run fails with correct bytes underneath. Co-authored-by: Orca <help@stably.ai> * test(e2e): normalise the pty line terminator so IME specs can run on Windows A Unix pty's line discipline turns the terminal's CR into a bare LF, but Windows ConPTY hands the reading process CRLF. Every spec compares against a recorded line ending in LF, so on Windows 16 of 21 failed on the terminator alone while the IME payload bytes ahead of it were byte-for-byte correct. Verified on real Windows: the renderer-to-pty boundary assertion passed there, so the app writes a bare CR and the LF is added by the console downstream of anything we control. Normalising in the reader keeps the specs asserting the IME bytes unchanged rather than loosening them. Co-authored-by: Orca <help@stably.ai> * chore: drop non-mergeable IME e2e scratch files --------- Co-authored-by: Orca <help@stably.ai>
…lyai#11170) (stablyai#13314) * 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(e2e): assert IME preedit geometry headlessly for Korean and CJK input Both IME defects that shipped and were reverted walked through a suite of ~3000 passing assertions, because every one of them was about bytes reaching the PTY. A preedit rendered into a hidden overlay satisfies all of them while the user composes blind. The real-geometry coverage that would have caught it existed but was headful, env-gated and macOS-only, so it never ran in CI. Drives composition through CDP Input.imeSetComposition instead of a native input source, which removes the accessibility grant and the system input source that forced the headful gate. The suite runs in the normal headless project in about 55s serially, and asserts the composition overlay's real bounding rect — the one property an overlay clipped to max-width:0 cannot fake and a DOM emulator cannot produce. Three tests are red on main and marked test.fail() so they stay visible in CI and flip loud when their fix lands: the preedit resumed by a bare compositionupdate, and full-width punctuation and digits committed from a keydown that still carries the ASCII layout key. Co-authored-by: Orca <help@stably.ai> * test(e2e): drop the known-broken markers now the stack closes all three Validated on real macOS hardware: with the two fixes below this layer, all three report "Expected to fail, but passed". Korean preedit renders at non-zero geometry through every jamo, and an Apple pinyin source sends ef bc 8c e3 80 82 to the pty where main sends ASCII. Worth recording why the punctuation case looked green on main once: an input source whose id happens to contain an allowlist term, as Sogou's does, satisfies the old gate. Correctness there depended on which IME the user had selected. Co-authored-by: Orca <help@stably.ai> * test(e2e): cover the Linux and Windows IME ownership shapes headlessly The ten headless IME specs on this branch all decided ownership through a macOS user-agent override, so the two platforms whose failure mode is a *dropped* character rather than a downgraded one had no coverage at all, and the one Windows-recorded trace already in the suite was replayed under whichever policy the runner happened to report — macOS locally, Linux on the CI shards. Adds four Linux specs and two Windows specs, every one of them replaying a native capture rather than a hand-authored ordering: - IBus/X11 Hangul mixed with literal ASCII. Its `compositionend` is EMPTY and the syllable arrives afterwards as a bare `insertText`, so reading the commit off `compositionend.data` — which the Windows capture rewards — drops every syllable on this framework. - fcitx5/Wayland Hangul. No keydown at all for a composing key, not even 229, and physically wrong `code` values on the literal keys. Any ownership rule reading 229 or `code` fails here. - Numeric pinyin candidate selection under both frameworks, with the ordinary digit kept as the negative control, so the two directions are pinned against each other rather than separately. - Windows Microsoft Korean captured with real scan codes, including the two lines committed with Shift held. Each asserts both sides of the boundary: the preedit's real geometry at every frame the user would see, and the exact byte stream the native run put on the PTY. The recorded `onData` the Windows/WSL fixture already carried is now asserted instead of sitting unused. Chinese moves up to first-class alongside Korean: pinyin preedit width is now pinned the way the Japanese phrase already was, and full-width punctuation is covered in the composition-session shape the Windows and Linux frameworks use, not only the macOS insertText shape. Two harness fixes fell out of the recorded traces and are why the IBus one passes. The replay applied each event's recorded textarea state *after* dispatch, one event too late for the handlers that read `textarea.value`; and it left a task boundary between `compositionend` and the `input` carrying the commit, which Chromium never inserts, letting xterm's deferred finalizer settle against a textarea the committed text had not reached yet. Co-authored-by: Orca <help@stably.ai> * test(e2e): replay the recorded macOS IME shapes instead of only synthesising them The macOS coverage on this branch drives Chromium composition through CDP, which is genuine but hand-ordered, so it could not assert the one property that decides the macOS rule: a composing keydown arrives with keyCode 229 while `key` is still the single translated character the input source produced — `ㅎ`, not `Process` — which is indistinguishable by length from an ordinary printable key. Three native captures were sitting unused in the evidence set. Adds a recorded 2-Set Korean session, including the syllable boundary where one composition closes and the next opens with no keydown between them, asserted against its own recorded byte stream. Adds the third failure mode, which had no coverage in any shape: an abandoned preedit leaking to the shell. Pinyin and Cangjie both backspace a composition away to nothing, and the assertion is not "the right bytes" but "no bytes". Both cancellation captures continue with a literal `ordinary` typed as bare keydowns, the recorder's own negative control. That tail carries no `input` events because the build it was captured on produced the byte from the keydown itself, so replaying it would measure the recorder rather than the product; the specs cut at the `compositionend` and say so. Co-authored-by: Orca <help@stably.ai> * test(e2e): promote the non-allowlist input-source punctuation spec to the suite Qingg matches none of the terms the pre-structural build enumerated, so on that build its bypass never installs. It is the one arm no headless spec can express, and the only test here that the pre-structural build cannot pass. Promoted from scratch with four changes, each forced by a measurement rather than by taste: - A non-attached input method is a refusal, not a negative result. macOS attaches per app instance and the attach can simply fail — 3 of 6 instances under exclusive host access, and re-selecting the source did not recover one of them across 9 keystrokes. That is now `test.skip()` with a reason naming the rerun, not a thrown error, and the suite does not gate on a fully green session. - Attachment is probed with a LETTER. Punctuation substitution emits no compositionstart and no keyCode 229 even under a fully attached source, so at the keydown it is indistinguishable from having no source at all. The punctuation arms are judged on PTY bytes alone. - The ASCII-layout control is now part of the spec rather than a side experiment. Without it a build that rewrote every `.` into `。` unconditionally would pass the Qingg arm and be badly wrong. - The assertion runs by default instead of behind a strict-mode flag, and gained a non-vacuity check: the input source must have committed something. That is the sharp end of the mechanism — on the old build the DOM carries only keydown and keyup, so nothing is committed at all and the character is destroyed before the source is asked. The verdict stays an equality between two measurements, never a comparison against a hardcoded glyph, so it holds whatever punctuation mode the operator's input source happens to be in. It reads `beforeinput`, not `input`: the forwarder consumes `input` in the capture phase on the pane element, so a probe on the helper textarea never sees it and a strict run fails with correct bytes underneath. Co-authored-by: Orca <help@stably.ai> * feat(terminal): encode IME commits as CSI-u under the all-keys kitty flag A pane that negotiates `report_all_keys_as_escape_codes` (bit 3) asked for every printable key as a CSI-u report. The commit path wrote IME-committed text raw, so such a pane got a legacy byte stream it had declined. That predicate has no IME-specific condition, so it affected every macOS user in such a pane, not just CJK users. Encode the press that produced the commit instead, reusing xterm's own kitty encoder rather than hand-rolling CSI-u. `claimKeyEvent` is untouched: still unconditional, still structural, still no kitty read on the keydown. The flag read happens once per commit. The gate is bit 3 alone. Flags 1/2/4/16 leave printable keys as text, so panes negotiating only those keep receiving substituted characters; gating on "kitty active" would strip the substitution from every pane that negotiates anything. Known limit, pinned by test: the report carries the physical key's codepoint, not the committed glyph. Bit 3 is the app declaring it does not want text, and bit 4 is how it asks for text back — but xterm's encoder derives that text field from the same `key` it derives the keycode from, so carrying the committed glyph needs an encoder change, not a wider gate. * fix(terminal): report a held key's repeats as REPEAT under the kitty flags The commit encoder never passed an event type, so xterm's encoder applied its PRESS default to every auto-repeat keydown. A pane negotiating report_event_types alongside bit 3 saw one held key as N separate strikes. Carry the keydown's `repeat` on the claimed press and map it to the protocol's REPEAT. The event type only reaches the wire when report_event_types is negotiated, so this is inert for panes that asked only for bit 3. Co-authored-by: Orca <help@stably.ai> * test(e2e): cover a macOS system key remap reaching the terminal (stablyai#11170) Co-authored-by: Orca <help@stably.ai> * chore: drop non-mergeable IME e2e scratch files --------- Co-authored-by: Orca <help@stably.ai>
Prevent older paired hosts from discarding the entire taskResumeState when they encounter the strict linearIssueView field, which was causing silent loss of github and jira queries during remote pairing. Layout, grouping, ordering, and per-workspace filters are now device-local.
…i#12057) * fix(repo-icon): center emoji glyph within its sized icon box The emoji glyph span in RepoIconGlyph received a fixed-size box via iconClassName but had no self-centering, so the outer flex only centered the box itself while the glyph sat top-left inside it — most visible in the settings preview (size-10 box, size-5 icon). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013ewCWxSox2WHN8T78AT7T4 * test(repo-icon): cover emoji centering in its sized icon box --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
* 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
* perf(ai-vault): isolate tab title resolution * fix(ai-vault): preserve background scan caches * fix(ai-vault): resolve nested worker from chunks
* fix(terminal): handle select-all in Kitty TUIs * fix(terminal): preserve popout native selection
Co-authored-by: OrcaWin <alpha-eng@stably.ai>
* fix(claude): hide managed hook consoles on Windows * fix(claude): harden managed hook invocation --------- Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
* fix: separate WSL skill copy and setup commands * fix: keep repair fallback on Windows host * fix: pin skill setup terminal runtime * fix: align skill setup retries with runtime --------- Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
* fix(cli): preserve WSL --deps quotes and parse task ids strictly PowerShell 5.1 native splat was stripping ASCII double quotes on the WSL bridge, so non-empty JSON --deps arrays failed while [] still worked. Pre-escape quotes before launching orca.exe, and recover quote-stripped task-id arrays while rejecting non-task-id and malformed CSV input (stablyai#12188). * fix(orchestration): narrow WSL deps recovery * fix(wsl): forward native CLI arguments losslessly * ci(windows): exercise WSL PowerShell argv boundary --------- Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
Forward-slash //wsl.localhost links opened as a different path string than the Files sidebar's backslash UNC, so file watchers did not refresh the editor. Canonicalize WSL UNC to \\wsl.localhost\... at map time. Fixes stablyai#13349
Persisted Claude default args or a custom command can carry their own --resume/-r/--continue/-c selectors (a bare picker default or a stale id). Cold restore appended the authoritative --resume <id> after them, typing a command with competing selectors into the restored pane (stablyai#12982). buildAgentResumeStartupPlan now routes Claude through a selector guard that tokenizes the base with the existing startup tokenizer, strips selectors in option position only (value-taking options keep dash-leading values), and appends exactly one authoritative selector, inserting before Claude's own -- terminator when present. Splicing is span-based so untouched bytes stay verbatim, wrapper commands are left alone, and any tokenization failure falls back to the previous append-only behavior. Launch paths, other agents, persistence, and the wire are unchanged.
Round-1 review findings: locate the claude executable by command position (index 0, after a wrapper --, or behind NAME=value assignments) so an argument merely ending in /claude can never be mistaken for it; stop matching the joined -r<id> form, which was ambiguous with dash-leading option values and forced an unmaintainable arity table (now deleted). Ambiguous shapes degrade to the pre-guard append-only behavior.
Round-2 review findings: an unquoted operator or newline after the claude token means the base chains other commands, and splicing across that boundary handed the selector to the wrong command — detect it and fall back to plain appending. Also recognize claude behind PowerShell's & call operator, decouple the test oracle from the implementation's selector predicate, add Windows tokenizer span tests, and rename the module after its public API.
Round-3 review findings: the guard's operator scan compared raw source to token value, so one quote or escape anywhere in a token hid a shell-active operator outside the quotes and the splice crossed a live command boundary, losing the resume entirely. Both tokenizers now flag tokens carrying an unquoted, unescaped operator byte (or a word-leading # comment on posix/powershell) on their spans, where quote state actually lives, and the guard fails open on that flag. Also strengthens the redirect fail-open test to carry a stale selector, re-tokenizes each raw span in the shell span tests, and documents agent-resume-argv-drop as codex-only.
Round-4 review findings: unquoted multi-token expansions (backtick, $(, ${)
split across whitespace, so removing only the recognized selector token left
a broken construct tail — both tokenizers now raise the span flag (renamed
bareShellSyntax) for those openers, on cmd also for operators between
single quotes, which cmd does not treat as quoting. The separator backoff is
clamped to the previous token's span end so a token ending in an escaped
space can no longer donate its escape to the appended selector.
Round-5 review finding: cmd.exe has no single-quote syntax, so the Windows tokenizer's grouping of a single-quoted region diverges from what cmd parses — literal argv like 'claude ...--resume... old' was being read as a real selector and stripped, and a literal '--' as claude's terminator. Flag any cmd single-quoted token as bareShellSyntax so the guard fails open.
Round-6 review findings: the span flag was only evaluated in the unquoted branch, so an expansion opener inside double quotes went unflagged — and inside $(…)/backticks a nested quote re-opens a context this tokenizer does not model, so the splice could cut mid-construct (syntax error, or a silently mutated substitution body). Both tokenizers now flag those, and the flag is renamed divergesFromShell to say what it means. Restrict the NAME=value command-position prefix to posix, where that syntax exists. Drops two branches proven dead.
Round-7 review findings: (1) the divergence scan started after the claude token, so an expansion opened in a prefix — $(x; npx -- claude --resume s) — had its closer spliced away, producing a base bash cannot parse; it now covers every token including the executable, exempting only PowerShell's leading call operator. (2) posix drops a double-quoted backslash the shell keeps literal, and the Windows escape branch ran inside quoted regions where cmd/PowerShell keep the escape byte literal — both now flagged, so a literal can never be misread as a selector. (3) an unquoted line continuation hid a selector inside a token and skipped the newline gap check. Also removes a third provably dead branch and collapses the cut floor into the cut itself.
Round-8 review findings, all one family — escapes whose token value hides a selector the shell would see: a double-quoted line continuation (bash deletes both bytes), posix $'…'/$"…" quoting, a windows escaped newline, and a trailing unpaired escape. The last one was previously written off as pre-fix-identical, but once stripping happens the dangling escape swallows the separator and no exact --resume reaches claude at all — strictly worse than appending, so it must fail open. Also folds the three gap predicates into one scan.
Round-9 review findings from both lanes: inside double quotes only $( and
${ open an expansion — $' and $" are literal there — and a trailing $
was flagged unconditionally because JS ''.includes('') is true. Both made
the guard fail open on modelable bases, leaving the stale selector to
compete, so stablyai#12982 went unfixed for them. Separately, cmd strips ^ before
the child re-splits on the bare whitespace, so an escaped separator hides
two real arguments and must fail open rather than drop one.
Round-10 review findings, both Windows-only (a bash oracle cannot reach
them): cmd strips a caret before a quote and the child's parser then reads
a bare quote delimiter, so the tokenizer's word boundaries stop matching
argv — one case turned a working resume into no resume at all, another let
a stale selector survive the splice. And bare (…)/{…} are live PowerShell
syntax in argument position, so splicing through them emitted unbalanced
output that PowerShell cannot parse.
Round-11 review finding: after a bare --%, PowerShell passes the rest of the line to the child literally, so the guard stripped a real selector and then appended quoting that arrives as literal bytes — claude ends up with no exact --resume at all, worse than leaving the stale one. Quoted "--%" and cmd, where the token is ordinary, still splice.
Round-11 review finding: an odd run of backslashes before a quote makes it a literal byte to the child's CommandLineToArgvW parser, not a delimiter, so the tokenizer's word boundaries stopped matching argv. Orca manufactures that pattern itself — quoteStartupArg wraps every token in quotes without escaping a trailing backslash — so a pasted Windows path was enough to move the selector into a desynced region and leave claude with no resume flag. Also replaces a caret test case that was byte-identical before and after its own fix, and merges two stacked comment blocks.
Round-12 finding: PowerShell expands backtick escapes only inside double quotes, so a sequence there produces a token value argv never sees — the guard could strip "-`r" plus the argument after it. Also narrows the stop-parsing comment: a quoted --% can engage stop-parsing before a parameter token, where the base is already mangled either way.
Round-13 finding: the previous commit gated on quote === '"', but PowerShell's tokenizer calls Backtick() from ScanGenericToken, so it expands these sequences in unquoted arguments as well — bare -`r really is a control character, not -r. The guard read it as a selector and dropped it plus the argument after it. Widening to all PowerShell contexts measures 0 under-flag and 0 over-flag across the full printable matrix; the backtick-escaped-space idiom still splices. Also swaps a test case that was byte-identical with and without its own fix.
Round-14 observations, all pre-existing and measured: PowerShell drops a token-leading backtick together with the whitespace after it, emitting no token, so the tokenizer's extra token shifted the locator; and a backtick before a bare CR is a line continuation too. Flagging both takes the lane's 329k-base sweep from 87 bad to 0 with no new failures and the must-splice list byte-unchanged. Also corrects a comment that no longer listed every PowerShell divergence.
Round-15 verified against a real PowerShell 7.6.4 engine: a backtick before a bare CR is not a line continuation there — pwsh keeps the CR in the token. The flag stays because 5.1 is unverified and failing open costs nothing, but the comment now says that rather than claiming continuation.
brennanb2025
force-pushed
the
fix/claude-resume-session-id
branch
from
August 11, 2026 22:40
76439d1 to
38ad976
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Upstream
Summary
Description Claude cold restore no longer replays a dangling
--resumeflag when the provider session id is missing. The startup planner removes invalid captured resume arguments before appending a verified provider session and rejects a command that consists only of the danNote
innocarpe/orcamainuntil the upstream PR is merged.