[upstream #12873] feat: add DeepSeek status bar usage provider - #147
Open
innocarpe wants to merge 1177 commits into
Open
[upstream #12873] feat: add DeepSeek status bar usage provider#147innocarpe wants to merge 1177 commits into
innocarpe wants to merge 1177 commits into
Conversation
…2367) * fix(ssh): handle owner displacement and graceful shutdown SSH connections can reconnect with valid session proof after network loss or device sleep. When the incumbent owner is still half-open, allow the reconnecting client to displace it outright rather than wait for socket closure — a window that may never close. Retain displaced deliveries for the new owner to rotate. During app shutdown, drain SSH sessions without terminating recovery operations, and retry pending owner grants in case a replacement commits mid-drain. * fix(ssh): handle owner displacement and graceful shutdown Make QuitTeardownStartGate a shared singleton so SSH connects use the same shutdown fence as the main quit path. Track test-connection probes to ensure they complete before final teardown. Guard owner displacement to prevent stale owners from clearing recovery state claimed by newer owners. * fix(ssh): fix flaky test sync and add error code safety check Test was using tick-based Promise.resolve() loops which don't guarantee the async operation has started. Replace with signal-based synchronization that waits for the actual lease flush. Also add nullish-coalescing to error code check to prevent crashes if error is null or undefined. * fix(ssh): fence reset transport opens during shutdown * fix(ssh): keep recovery leases stable across reconnects * fix(ssh): close transports owned by cancelled connect attempts When a connect is cancelled after its transport has opened, that cancelled attempt still owns the transport and must close it — otherwise it leaks. Add disconnectConnection() to close by identity (not by target ID) so a cancelled attempt closes only the transport it minted, without tearing down its replacement's live transport. Track priorConnection to detect whether this attempt opened a new transport or reused an existing one, and close only on abandonment if this attempt owns the session. * fix(ssh): fence old owner proofs and close superseded transports When an owner reconnects with a new proof while an old one is still live, the old proof is now fenced with SUPERSEDED_ERROR instead of retrying indefinitely. The relay also closes stale transports to signal that their recovery generation has been overtaken by a newer one. This ensures overlapping reconnect scenarios complete with the newest proof rather than getting blocked by stale recovery attempts. * fix(relay): re-pin stdin/stdout fds after closing to prevent recycling When the relay closes stdin/stdout to signal EOF to the SSH peer, the OS can recycle those fds (0 and 1) for new sockets or files. If Node still treats process.stdin/stdout as those numbers, subsequent operations corrupt socket clients and trigger shutdown errors. Re-pin the fds by opening /dev/null to keep them occupied and prevent recycling.
…yai#12482) The 5-minute liveness tick from stablyai#12432 survived fenceAndCloseNow(), so a tick landing between the pre-sign-out fence and the profile wipe could briefly resurrect a broker (benign but soft — the entitlement check bails afterward). The fence now clears the interval; the next auth mutation re-arms it via refreshDemand, and the safety net otherwise behaves identically. Found in release review of the stablyai#12432 cherry-pick. Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
stablyai#12224) Surfaces the real install-failure cause instead of letting a failed elevation stall silently, and keeps the reconnect wait inside its total budget by recomputing the remaining time after each awaited RPC. Relates to stablyai#11906 — this fixes the observability half. The functional half (a .deb/.rpm host cannot elevate and can never self-update) is unchanged, so the issue stays open.
…stablyai#12217) * fix(ports): keep the app responsive when security software slows process creation Orca ran the workspace port scan's probe commands (lsof/ps on macOS, netstat + powershell.exe on Windows) directly in the Electron main process. libuv performs process creation inline on the calling event loop, which in the main process is the browser UI thread, so an endpoint-security module hooking CreateProcessW froze the whole window for the length of the spawn. The same stall also produced a false diagnosis: the 4s command watchdog was armed before execFile (local-workspace-port-scanner.ts:389 -> :410), so its deadline had already passed by the time the command started. Every scan on a hooked host reported a command timeout, tripping the 60s -> 5min backoff and the "Port scanning is temporarily paused after a command timeout" banner even though the commands themselves were healthy. Probe commands now run on a lazily created, unref'd worker thread with FIFO one-at-a-time dispatch, and the watchdog is armed after execFile returns so it measures the command rather than the spawn. Node's own execFile timeout kill (killed: true) is classified as a command timeout, keeping the backoff working for genuine hangs. A scan that observes a stalled spawn skips its optional metadata commands for that cycle, capping a hooked-host scan at roughly one stall instead of three. Closes stablyai#11161 * fix(ports): keep advertised URLs when a stalled spawn skips port metadata Review follow-up on stablyai#11161. The stalled-spawn early return handed scanWorkspacePorts raw ports with no cwd/commandLine, so every port failed attribution and reconcileAdvertisedUrls told the watcher each worktree's listeners had vanished. shouldEvictAfterScan then deleted every cached advertised URL and broadcast a removal event; those URLs are only ever captured from live PTY output, so the dev-server link was gone until the server restarted. The scanners now report metadataAvailable, and reconciliation is skipped for a scan that never gathered attribution evidence. The skip is also no longer self-perpetuating: on an EDR-hooked host every spawn stalls, so gating purely on the current scan's spawnMs made every port permanently external (Stop refused with 'Only workspace-owned local processes can be stopped here.'). Metadata is now re-probed on the scan after a skip, matching what the comment and test name already claimed. Co-authored-by: Orca <help@stably.ai> * test(windows): stop a temp-dir lock from failing the CLI launcher smoke test The native launcher assertions passed on windows-latest, but teardown's rmSync raced Windows' release of the image handle on the exe the test had just executed and threw EPERM, failing the job. Cleanup now retries and, on Windows only, tolerates a residual lock code instead of reporting it as a launcher regression. Co-authored-by: Orca <help@stably.ai> * fix(ports): scope the metadata skip away from attribution-dependent scans The metadata skip was a process-wide parity flag, so Stop and the localhost-label allowlist could land on a degraded cycle and reject a port the panel had just shown as workspace-owned. Give those callers an explicit requireMetadata option, and carry the previous cycle's listener metadata forward so a skipped background scan no longer republishes workspace ports as external. Also pin the watchdog ordering: the stall in the execution test was shorter than the watchdog budget, so a watchdog armed before execFile still passed. * build: guard worker-thread entries against electron imports (stablyai#11161) Electron's module is not registered on worker threads, so require("electron") throws "Cannot find module 'electron'" inside a main-process worker and kills it at startup (verified on Electron 43.1.0). plain-node-entry-guard covered only forked plain-Node entries, so the five worker entries relied on hand-written "must stay electron-free" comments. The port-scan probe worker is one import away from port-scan-command-client.ts, which deliberately contains require('electron'). A violation there fails closed at runtime while every unit test still passes, because the client's require is try/caught on the main thread. Covers stt-worker, warp-theme-parser-worker, session-scanner-opencode-sqlite-worker-entry, main-thread-hang-watchdog-entry and port-scan-command-worker-entry. The scan is transitive over the emitted chunk graph, so a shared chunk that reaches electron is caught too. Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com> * test(windows): retry teardown for main's duplicate-PATH launcher fixture Main's new csc-compiled harness runs an exe from the temp tree, which is exactly the image-handle/AV lock the merged-in removeFixtureTree retry exists for; its bare rmSync would report a teardown lock as a launcher failure. Co-authored-by: Orca <help@stably.ai> * test(ports): pin the packaged-asar worker entry path resolveWorkerEntryPath's packaged branch never runs in dev or e2e, so the path construction had no coverage. Split the electron read out of it and unit-test both layouts. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai> Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
…ablyai#11889) SidebarToolbar is a React.memo boundary whose props are shallow-equal on a language switch, so without its own useTranslation() subscription it kept the English copy it rendered at boot — the persisted locale is applied asynchronously, after the lazy catalog loads. Co-authored-by: 5Hyeons <ohs2251@naver.com>
…yai#12105) formatTrackingSince() used the OS locale rather than the language chosen in Settings. getIntlLocale() resolves the active i18n language to a BCP-47 tag, mapping the synthetic plugin<hex> resource language that Intl rejects back to the pack's real locale. Co-authored-by: Evgenii <kumiro@me.com>
) protectedTranslation refused every language-pack key under auto.components.settings.plugin*, which caught 104 keys that carry no trust meaning — section titles, empty states, Refresh, Add path. A 35-path exact allowlist opens those while consent, provenance, and every *Failed string stay protected; anything new stays protected until it is added deliberately. PluginsSettingsSection.experimental is held back from the contributed allowlist: the "Experimental" chip is a trust badge, which the module's own boundary comment places out of scope. Co-authored-by: Evgenii <kumiro@me.com>
Every card under Settings -> Integrations built its status pill from bare literals inside the JSX call, so the pill stayed English beside a translated description. audit-localization-coverage.mjs inspects JSX attributes and object properties, not conditional expressions handed to a prop, so the gate stayed green while the strings shipped untranslated. The three token-configured cards share tokenProviderStatusLabel(); Gitea passes optional: true because it works read-only without a token, which is why its unconfigured state reads Optional setup rather than Not configured. Co-authored-by: Evgenii <kumiro@me.com>
…tablyai#9444) The description was assembled from a bare template literal, so it stayed English under every language pack. It now lives in the catalog as two entries — description with a {{shortcut}} placeholder and descriptionBase for the invert-on variant — following the copy-module pattern from stablyai#10991. Co-authored-by: 조재중 <126754298+m-a-king@users.noreply.github.com>
The shared Automation tour copy was rendered without passing through translate(), and the overlay surface hardcoded its default Next and Done labels. Copy is keyed off the step id rather than its position, so inserting a step ahead of them cannot shift the text onto the wrong step. Co-authored-by: 5Hyeons <ohs2251@naver.com>
) SidebarNav.c86d83b5c3 is the onboarding pill rendered beside Orca Mobile, so "New" marks a new feature. Both locales had translated it as a create action — ko 새로 만들기 ("create new"), zh 新建 ("create new") — which reads as a button. The PR's other hunk (zh GH PR) already landed on main and resolved to a no-op. Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
The badge value is pinned in locale-key-overrides.mjs, so the ko/zh fix from stablyai#10664 would have been reverted by the next catalog repair. ja carried the same defect — 新規 reads as "create new" — and is corrected alongside.
185 zh values were still verbatim English, plus the 6 VoiceMicrophoneSetting keys were missing. Placeholders, "X of Y" counts, and key ordering are unchanged; the only edits to already-translated values are punctuation. Applied at key level rather than as a branch merge — the PR was cut from an older base and conflicted only on JSON context, with no value drift against main. Co-authored-by: 闲人 <38777313+qiuyongjin@users.noreply.github.com> Co-authored-by: jake <qiu5630@163.com>
…ai#10672) The browser.loadFailure.* keys were still raw English in es/ja/ko/zh. en.json is untouched; every {{value0}} token and the Orca/HTTPS brand terms are preserved. 13 of the zh keys were already covered by stablyai#12368, so only the 6 it did not reach are taken here. Co-authored-by: MumuTW <42820974+MumuTW@users.noreply.github.com>
The source-control primary action rendered 押す ("press") and 引く ("pull a
physical object") for Push/Pull. プッシュ/プル match the sibling フォースプッシュ
and 同期 labels. A guard test pins both so bootstrap re-translation cannot
silently regress them.
Co-authored-by: Iris-Fla <103801589+Iris-Fla@users.noreply.github.com>
Nine ko values said something other than the English source. The riskiest is
SourceControl.6d7f2a47e5 "Discard folder", rendered as 폴더 삭제 ("delete
folder") next to a sibling delete-untracked action. Others: "Only branches Orca
named itself" read as "branches named Orca"; "staged changes" as 단계적
("phased"); "first-party cloud" as the mojibake 1方클라우드; "discard the
deletion" as "the deletion is deleted"; "Stage all changes" as a sentence
meaning "prepare"; and Recipes as 조리법 (cooking recipes).
EphemeralVmsPane.skillTitle is dropped from the PR's test and override — the
key was renamed to cloudVmSkillTitle on main, so the assertion would resolve to
undefined.
Co-authored-by: ShinSungkyu <kxu4583@naver.com>
…c terms to English (stablyai#12192) Fixes stablyai#12113. shouldPreserveEnglishValue keyed on the English value, so any key whose source string equalled a NEVER_TRANSLATE_VALUES entry was forced back to English on every repair run — agent, commit, repo, terminal and Continue were all on that list. Measured on a clean checkout: ko 279, ja 461, zh 1180, es 479 values rewritten, the large majority destroying translator work. 17 generic terms move into locale-generic-ui-terms.mjs, and the brand revert now skips a term's canonical rendering, so genuinely nonsensical forms (zh 回购, ja/zh 端子, es Comprometerse) still fire while 터미널/커밋/エージェント survive. Brand, path, and code tokens are untouched — MD -> 医学博士 and HEAD -> CABEZA are why that list still earns its keep. No catalog values change; every file is under config/scripts/. Co-authored-by: AnddyAgudelo <44873492+AnddyAgudelo@users.noreply.github.com>
The override for SshTargetForm.137e88ce8d held a truncated relay-TTL sentence ending mid-clause at "최대:", which renders "Timeout after disconnect (seconds)" — the sibling key 55c56cf2c7 — not its own English source. stablyai#12192 only corrected the terminals token inside that wrong sentence. Point it at the value ko.json already ships, so a catalog repair cannot overwrite the correct string with the wrong one.
…stablyai#11728) en.json carries ~190 keys per locale that the locale catalogs have not been bootstrapped with yet, so every repair-locale-catalog run threw a TypeError before doing any work. Skip missing leaves instead. Split out of stablyai#11728 so the crash fix can land without the catalog regeneration, which still needs native-speaker review. Co-authored-by: Turtle-Hwan <turtlehwan@gmail.com> Co-authored-by: Orca <help@stably.ai>
…blyai#8662) Eight renderer call sites built Intl.RelativeTimeFormat(undefined, ...), which resolves to the OS locale, so relative timestamps rendered in Korean on a Korean-locale machine even with the UI language set to English. Unlike DateTimeFormat, RelativeTimeFormat emits language words, so it must follow the UI language. Rebased onto main: the formatter now resolves through getIntlLocale() (stablyai#12105) rather than i18n.resolvedLanguage, so the synthetic plugin<hex> resource language cannot reach the constructor and throw. GitHubItemDialog and PullRequestPage no longer own their formatter — main moved it into work-item-state-presentation, which is converted instead, along with the newly added site there. Co-authored-by: moseoh <azqazq195@gmail.com> Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Co-authored-by: Orca <help@stably.ai>
…tablyai#5640) Most of stablyai#5640 landed independently, but 12 strings were still falling through to English: the ko "Diverged" pull-policy notice, and the ja source-control primary-action blocked reasons. Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Co-authored-by: Orca <help@stably.ai>
…tablyai#8816) Cherry-picked the unambiguous subset of stablyai#8816: - 디렉토리 -> 디렉터리 and 쉘 -> 셸, the standard loanword transcriptions - Milestones: 이정표 (a signpost) -> 마일스톤, the Linear product noun - Permission granted: 허가 (a licence) -> 권한 - "privacy envelope" rendered as 봉투, a paper envelope - commentTooLarge: 너무 커서 (too bulky) -> 너무 길어, across 9 composers The rest of the PR is left out: 83 leaves are agent -> Agent recapitalizations that fight the catalog convention, 17 are reverted by the repair policy, and several change meaning (viewed -> 읽음 "read" on the GitHub file checkbox, which means seen; "resolve PR base" read as fixing a problem; 차이점 -> diff, which also breaks ko search recall). Co-authored-by: tykimseoul <tykimseoul@gmail.com> Co-authored-by: Orca <help@stably.ai>
The build-time repair layer carried overrides that rewrite already-correct CJK values back to English. Most are inert against today's catalogs but fire on the next regeneration, so they read as latent regressions rather than policy: - zh workspace status picker (Play/Flag/Zinc/Rose/Emerald/Amber/Violet/Sky/ Blue/Neutral) and `sheet`/`page` were pinned to English while every sibling option, and ko/ja/es, stay translated — half a Chinese picker. - The zh `蓝色的`/`琥珀色`/`中性的` phrase fixes correctly flagged the adjectival 的 form but replaced it with English instead of the bare color noun. - ja `Play` was pinned to English though the catalog already reads 再生. - A value-wide zh `Open: '进行中'` mapped every "Open" to "in progress", including the button that opens an MCP config file. "Open" is a verb (打开) on buttons and a state (开放) beside 已关闭, so no single mapping fits. Catalog corrections in the same area: - The GitHub/PR state picker key override read 진행 중 / 进行中 for ko and zh while ja already had the correct オープン; now 열림 / 开放, matching 닫힘 / 已关闭 on the sibling entry. - The terminal cursor-color group is the on-screen cursor, not the Cursor editor; ja already had カーソル, ko/zh now get 커서/光标 instead of "Cursor". - Tailwind swatch labels 天空 (the sky) and 锌 (the metal) do not read as colors; now 天蓝/锌灰, and ja 空 becomes 空色. - The ko disk-usage heading was pinned to bare "Space" while its own description says 저장 공간; both now use 저장 공간. Two policy tests pinned the Play de-localization. They diagnosed the input correctly — 玩 / 遊ぶ are wrong for a play icon — so the expectations move to 播放 / 再生 rather than English. Destructive drift (localized -> English on regeneration) drops from 40 to 21 for zh, 6 to 5 for ja, and 5 to 3 for ko. What remains is deliberate: search qualifiers, path and filename literals, and product names.
* Add search by name, project, and prompt for automations Split the monolithic automations page into focused modules: extract dialog logic, list panel rendering, search functionality, and utility helpers into separate files. Introduce deferred search matching to keep the input responsive, with proper bounds checking to reject oversized pastes. The page stays unfiltered when search is inactive or too large, preserving the original list view in those cases. * fix(i18n): add missing automation search localization keys Sync en.json keys used by AutomationListSearchField and the no-matches empty state so static analysis localization catalog check passes. * Localize remaining automation strings and optimize search - Add 12 i18n keys for automation labels, counts, and usage display - Extract AutomationPaneTab and SelectedExternalRunPage types to shared automation-page-state module - Optimize search fingerprint by truncating prompts to indexed prefix for bounded performance per tick - Improve escape-key handling in search field to clear input before blurring - Remove deprecated getAutomationListSearchQuery function
…3333) (stablyai#12501) * fix(mobile): keep repeated-prefix chat replies streaming Text alone can't tell "the transcript caught up with this stream" from "a new reply repeats the previous turn's prefix", so the old suppress-on- prefix rule swallowed genuine repeated replies. A stateful gate remembers which transcript tail predates the current stream segment and hides the bubble only when that tail moved during the segment, scoped to the active host/workspace/tab/session so a swapped chat can't inherit a baseline. Refs STA-3333. * fix(mobile): keep the streaming gate alive across chat/terminal toggles The gate lived in MobileNativeChatView, but MobileNativeChatOverlay returns null whenever the user peeks at the terminal — that unmounts the view and throws the baseline away, so the repeated-prefix reply was swallowed again on the way back. Move the gate (and the fold memo it reads) up to the overlay, which stays mounted across those toggles. While hidden the transcript is empty and the throttled stream reports no text, which the gate would have read as "idle" and re-anchored on. Pass the agent's working state so a textless tick inside a live segment holds the baseline instead. The scope key is now keyed off the tab rather than the view-gated chat resolution, so it survives the toggle too; streamIdentity keeps its exact previous value because the delayed-send guards compare against it. Also drops a dead disjunct in the caught-up test: a null baseline is already unequal to every real tail id. * test(mobile): model the real re-show ordering in the streaming-gate tests The overlay regression test replayed the transcript before the stream text on the way back from the terminal view. That ordering is backwards: the session withholds `messages` until a fresh read settles (an RPC round trip) while the throttled stream text returns in ~50ms — and with the transcript already back, a gate that got discarded on the toggle still passes. Replay the real order, which pins the gate's lifetime as intended. Swaps the hidden-gap duplicate case for the in-view one (a tool frame clears the assistant text mid-turn), which is where the hold actually earns its keep; the hidden-gap direction stays covered at the gate level. * fix(mobile): stop the streaming gate adopting a reply as its own history A textless status tick was re-anchoring the gate's pre-stream baseline, so two paths still rendered wrong: - The reply's transcript push beats its throttled status text whenever the pane stays `working` past the turn (a live subagent or background task). The tick in between adopted the just-landed reply as history, and the status text that followed rendered it a second time — a duplicate bubble, and a regression against main's suppress-on-prefix rule. - Peeking at the terminal between turns empties the transcript. That empty tail was adopted as the baseline, so the next repeated-prefix reply was swallowed again — the bug this PR exists to fix. Only a tick that carries a real tail and sits outside a live turn anchors now, with an exception for a gate that has never anchored: mounted mid-turn, the first real tail it sees is the best history it will ever get. Also drop `buildMobileNativeChatData`, a test-only builder this PR had wired the new gate into; its green test asserted the exact suppression this PR removes. Its fold/pending/image coverage moves to the builder the view calls. * test(mobile): pin the textless anchor's text reset Mutation testing found the `prevText` reset on an anchoring textless tick unpinned: keeping the previous turn's text there reads the next turn's opener as a new segment, re-anchors onto the reply that just landed, and renders it a second time — the same duplicate-bubble class already fixed twice on this branch.
…#12564) Fetch metadata when filters are selected, not only while the popover is open, so chip labels remain readable after closing the dropdown.
…k keystrokes (STA-3333) (stablyai#12502) * fix(mobile): serialize native chat PTY writes Two composed native-chat write sequences into one PTY interleaved their bytes: the per-terminal send-in-flight guard lived inside the image attachments hook, so ask answers, permission choices, and question answers wrote straight past it. Move the guard to a shared module-scope write lock (the terminal outlives any one screen) and take it on all four paths. Ask answers additionally queue behind the prior chain's RPC rather than racing it, and a superseding answer is fenced once a key has actually landed: an accepted or ambiguously-delivered keystroke already moved the remote selector, so a replacement's from-scratch key plan would answer the wrong question. A superseding answer inherits the cancelled chain's hold (refcounted, last chain out releases) so changing your mind mid-answer still works, and Stop/cancel stay unguarded so an interrupt can never deadlock against the send it cancels. Refs STA-3333. * fix(mobile): report a fenced native-chat answer instead of dropping it The fence added for superseding Ask answers returned false with no onSendError, and the card re-enables on a false result — so a queued answer vanished with no banner and no toast, looking exactly like a dead button. Every other bail in answerAsk reports. Keep the generation- mismatch branch silent: a newer answer owns the error surface. Also covers the hold-count release, which had no test at all: replacing it with an unconditional release left all 58 tests green while silently reopening the terminal mid-sequence — the exact interleave this PR fixes. * fix(mobile): stop a superseded answer from clearing the fence banner A chain that finished its key plan reported `true` even after a newer answer superseded it. The route sends through useNativeChatAcceptedAction, whose accepted callback retires the send-error banner — and that callback runs after the successor's fence report, because finishTurn() fires in `finally`, before the chain's own promise settles. So the successful predecessor deterministically wiped the fence message the successor had just raised: every healthy write took that branch, which made the previous commit's report vacuous exactly where it mattered. A superseded chain now reports no success, matching every other supersession checkpoint in this hook. * fix(mobile): fence on the turn slot, not the generation counter The supersession guards added in c36f2ce read `generationRef`, but `cancelPending()` bumps that counter from three callers with no successor chain: Stop, ask-cancel, and the lease effect that fires on every disconnect. An answer whose key had already landed then reported false — `fail()` never runs on an accepted write, so the card stayed up silent with Submit re-enabled, and the retry wrote the same key into a selector that key had already advanced. Test the turn slot instead: a successor takes it synchronously before its first await and cannot resolve ahead of this chain, so it means "a successor took over" exactly, without catching bare cancellations. * fix(mobile): report a fenced answer when a dropped lease bumps the generation The fence-report guard tested generationRef, which answers "was I cancelled", not "did a successor replace me". Stop and ask-cancel both write an Escape that clears the ask card, so their silence is harmless. A dropped input lease bumps the same counter and writes nothing: the card stays up with Submit re-enabled while the predecessor's option key has already advanced the live selector, so the retry double-steps it. Gate on the turn slot, matching the two success returns. * test(mobile): cover the turn-slot release after a landed answer The slot delete in the finally was the one line in this hook no test killed: without it a landed answer's resolved-false turn stays parked, so every later answer on that handle reads a fenced predecessor and the ask card dies after its first use. * test(mobile): keep PTY write locks terminal-scoped
…yai#13105) * Fix Cmd+J recent order when terminal entities hydrate late Unified tabs can appear before tabsByWorktree entities on restore, which latched an all-IDLE ranking and buried blocked chats until reopen. Keep a provisional freeze, then re-capture once entities arrive. * Harden Cmd+J incomplete hydration re-rank latch Clear the provisional order latch when the ranked list goes empty so a brief tab wipe cannot freeze an empty Recent section, and assert that a user-moved selection survives the incomplete→complete re-rank. * Fix Cmd+J ordering to not compare focus ordinals across worktrees focusOrdinal is a per-worktree sequence, so comparing rows from different worktrees corrupts their relative order. Preserve input (positional) order instead. Also: refactor test helpers to use makePaneKey() utility for pane-key construction, and clarify a test description about CJK character handling in relevance scoring.
* fix(ai-vault): contain WSL session deletion * fix(ai-vault): close approved-root traversal race * test(wsl): restore fixture permissions before cleanup --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
* fix(ai-vault): block deletion of live sessions * fix(ai-vault): retain external session authority --------- Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com>
* fix(mobile): allow reachable Hyper-V pairing addresses * fix(mobile): keep host-local Hyper-V addresses filtered * fix(mobile): preserve explicit address on empty refresh --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com>
…i#12710) * Persist the Linear issue list view and per-workspace filters Layout, grouping, ordering, columns, and attribute filters survive a restart. Facet ids are workspace-scoped, so filters are kept per Linear workspace and the active filter is *derived* from the selected workspace rather than reset by an effect on switch — no ordering race can apply workspace A's facets to B, and an unresolved or cross-workspace selection reads as unfiltered without erasing anything. A single shared catalog backs the renderer state, `TaskResumeState`, and the strict `ui.set` schema, so a new view option cannot leave paired web/mobile/relay clients rejecting the whole payload. Persisted values are normalized as untrusted input: a corrupt preference or a single bad workspace entry is dropped without taking the rest of the resume state with it. Deriving the filter also removed the guard that used to make three neighbouring behaviours safe, so they are re-scoped here: - The primary-team facet reset now fires only on an in-workspace team change. A workspace switch also changes the primary team, and clearing there wiped the filter that had just been restored for the workspace being switched *to*. - The list-read force check no longer fires on the session's first read, so a restored filter serves warm cache instead of forcing a network round trip behind a blocking spinner on every cold start. - The filter dropdown derives "no single workspace" from `workspaceId` alone. With an unresolved workspace it previously rendered the statically populated priority section, whose clicks now have nowhere to be stored. * Harden Linear view persistence against the failures review surfaced Five issues, each found by a reviewer and reproduced before fixing: - The filter dropdown's prune effect only ran when the user opened the popover, because the filter was always empty at startup. Restoration makes it run on mount, where `availableTeams` may still be the issue-scraped fallback rather than the real fetch. Metadata complete for a *partial* team set passes every R12 guard, so it pruned facets belonging to teams it simply hadn't seen — and the write persisted, deleting them permanently. Gated on `teamsSettled`. - `canonicalize` dedupes but enforces none of the transport bounds; only the throwing parser does. So `serialize` could emit a 101-label filter that the strict `ui.set` schema rejects, which drops the WHOLE taskResumeState — github, jira and linear query included — on every subsequent write, since the renderer resends the merged object each time. Added `boundLinearIssueAttributeFilter` and a round-trip test built from serializer output rather than a literal, which is the only kind that can catch renderer/schema drift. - `linearIssueView` now carries `.catch(undefined)`: value tolerance stops at the top level, so any future instance of the above is a cosmetic reset of the view instead of silent loss of every other resume field. - A workspace switch forced an uncached list read in both directions. The switch is a later observation, so the null-baseline fix didn't cover it; the cache is already workspace-keyed, making the force pure cost. - Recency for the 20-workspace cap came from object key order, which is wrong twice: re-filtering an existing workspace left it at the head (first evicted, though just used), and an array-index-like key enumerates first regardless of insertion, so a write could evict the very entry it added. Recency is now an explicit ordered key list. Also adds the nested parity assertion — the top-level one compares only TaskResumeState's own keys, so a field added to LinearIssueViewResumeState stayed invisible to it, which is exactly what `.strict()` rejects. The wiring test was blind: deleting the hydration guard outright left all four assertions green. The gate is now `shouldPersistLinearIssueView`, unit-tested directly, and the file is renamed to the repo's `*-boundary.test.ts` convention with an assertion that fails on that mutation. * Log discarded Linear views and fix empty-filter serialization - Schema now logs when linearIssueView is discarded, making validation failures visible - Fixed serialization: filters that become empty after bounding are now omitted - Added AssertNoExtraKeys type check for bidirectional schema/type parity - Refactored view option catalogs to use canonical constants, preventing UI/schema drift * Remove workspace persistence limits and LRU eviction Stop capping persisted Linear workspace filters at 20 and evicting least-recently-used workspaces. Simplify persistence to store all workspace filters, gate persistence only on resume state application, and remove tests that pinned implementation details. Users can now persist filters for all their workspaces without arbitrary limits. * add test for linear persistence * Improve Linear filter test clarity and fix e2e overlay dismissal for CI - Convert parameterized filter-pruning test to sequential assertions - Fix dismissOverlayChrome to toggle overlay triggers instead of force-clicking inert page elements in headless CI * Prevent TaskPage from stealing Escape from Radix menus - Add check to detect open Radix dropdown menus and popovers; return early from Escape handler to respect their capture-phase ownership - Update overlay dismissal in e2e tests to use keyboard.press('Escape'), now that TaskPage no longer interferes * The capture-phase Escape guard in TaskPage bailed out for open dropdown menus and popovers, but an open Radix Select matches none of those selectors: the shared SelectContent wrapper (src/renderer/src/components/ui/select.tsx:60) renders data-slot="select-content" and Radix gives its content role="listbox", not role="menu". So with a select open, the window-level capture handler ran first, called preventDefault() and closeTaskPage() — closing the whole task page instead of just the select. Added [data-slot="select-content"] to the guard, as suggested. I did not add [role="listbox"]; the reviewer explicitly notes it's too broad, and the data-slot selector covers every select rendered through the shared wrapper. --------- Co-authored-by: m4air <m4air@MacBook-Air.localdomain> Co-authored-by: m4air <m4air@m4airs-Air.localdomain> Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
…ai#12474) Folder-project workspace ids (`repoId::/path::workspace:<uuid>`) were compared via the suffix-stripping `splitWorktreeIdForFilesystem`, so every workspace sharing one directory compared equal at ~35 runtime call sites — PTYs leaked between siblings and paired/mobile clients hung on "Loading terminal". Both identity helpers now use the suffix-preserving `splitWorktreeId`, `stopTerminalsForWorktree` routes through the shared helper, and `findResolvedWorktreeIdForPath` gains a `targetWorktreeId` tie-break. Co-authored-by: dgk-dev <dgk-dev@users.noreply.github.com>
…dow from reading Ready (stablyai#12477) A remote Orca server whose workspace window is closed keeps answering status RPC, so Settings > Available Hosts showed "Ready" and the status bar showed "Connected" while every graph-backed operation failed. Adds the shared predicate `isRuntimeWorkspaceWindowClosed` (`graphStatus !== 'ready' && desktopWindowStatus === 'openable'`) and one host-health derivation with a new `workspace-window-closed` state, consumed by both surfaces. Hosts that omit `desktopWindowStatus` are unaffected, so the connected-host count and overall dot do not regress. Fixes stablyai#12350 Co-authored-by: gatsby74 <gatsby74@users.noreply.github.com>
…yai#13120) * Expand Cmd+J palette and interleave tabs/worktrees on query Increase palette dimensions (900x600) and remove redundant secondary labels ("Terminal tab", "Mobile Emulator tab") that crowded rows. When a typed query matches both open tabs and worktrees, use a soft-split layout: leading section preview followed by trailing section floor so neither primary is buried under ~50 rows. Trailing section no longer truncates to hard cap when paired with a larger leading section. * Add type-alias search and fix multi-primary palette ordering Add searchable type aliases (e.g. "terminal tab", "mobile emulator") so users can find items by type without cluttering the row display. Refactor multi-primary palette layout into orderMultiPrimaryPaletteItems to prevent selection/render order drift, simplify selectableItems derivation, and track trailing hard-overflow count separately from scrollable rest. * fix(cmd-j): pin multi-primary layout generic for mixed item types Typecheck failed because the ternary lead/trail arrays inferred a WorktreePaletteItem[] | OpenTabPaletteItem[] union that could not satisfy layoutMultiPrimaryPaletteSections' single T parameter.
…exe probes (stablyai#11698) * perf(windows): stop the capability poll respawning blocking wsl.exe probes stablyai#11295 added a 30s renderer interval to `useWindowsTerminalCapabilities` whose early-return only fires when WSL is available with at least one distro, so on the common Windows host (no WSL) it re-ran a full capability read forever. Each read IPCs four probes whose main-process handlers were synchronous `execFileSync` calls to wsl.exe/pwsh.exe, blocking the Electron main event loop for up to 5s a time. The un-latching intent is kept: a host that answers "no WSL" is still re-checked, now on an exponential backoff (30s, +60s, +120s) that parks once the answer stops moving, re-arms on window focus, is shared by all consumers of an owner key, and stops entirely when the last consumer unmounts. The wsl/pwsh IPC handlers now use async twins that share the existing caches and back off identically. * fix(windows): classify async wsl/pwsh probe failures with the execFile error shape The async twins feed `execFile` callback errors into classifiers written for `execFileSync`: a non-zero exit lands on `error.code` as a number rather than `error.status`, and a timeout is a SIGTERM kill rather than ETIMEDOUT. So a Windows host without WSL (wsl.exe ships in System32, so it exits non-zero instead of ENOENT) was cached as retryable, shrinking the shared window from 10min to 45s and making the still-sync callers re-pay their blocking spawn ~13x more often; and a pwsh cold start past 5s cached "pwsh missing" for 30s, demoting the user's PowerShell 7 preference — the exact case the ETIMEDOUT branch exists to prevent. Also drops a literal NUL byte from the new re-probe module's signature separator, which made the file binary to git, and seeds `lastProbeAt` at registration so focus churn right after mount cannot defer the first re-probe indefinitely. Co-authored-by: Orca <help@stably.ai> * perf(windows): route relay host-capability probes through the async wsl/pwsh twins A paired web/mobile client resolves `useWindowsTerminalCapabilities` to a local target (TabBar's `isWebClient` gate, and `useSettingsNavigationMetadata` forces `{kind:'local'}`), so the new re-probe arms there too. But `window.api.wsl/pwsh` on a web client is not the ipc/app.ts channel — it is `host.wsl.*`/`host.pwsh.*` over the runtime RPC, which still ran the sync probes and blocked the desktop main event loop on `execFileSync('wsl.exe' | 'pwsh.exe')` for up to 5s per call. Switch those handlers and the relay preflight capability probe to the async twins added here; they share the same caches, dedupe and backoff, so remote callers see no behavior change. * fix(windows): harden async capability reprobes * fix(windows): dedupe PowerShell shell probes --------- Co-authored-by: Orca <help@stably.ai> Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
* fix(i18n): standardize Chinese status bar usage labels Signed-off-by: ousugo <dkzyxh@gmail.com> * fix(i18n): align Antigravity usage description Signed-off-by: ousugo <dkzyxh@gmail.com> * fix(i18n): standardize the zh status bar usage labels the menu actually renders The status bar item menu renders "<Brand> Usage" for eight providers. Claude, Codex and Gemini read 使用情况; Antigravity, OpenCode Go, Kimi, MiniMax and Grok read 使用量, so one dropdown showed two words for one concept. Register the decision where the repo already keeps it — the zh block of locale-value-overrides.mjs already pins Claude/Codex/Gemini Usage — so the repair pass enforces it instead of the catalog drifting again, and add the missing Kimi entry to BRAND_MISTRANSLATIONS so 基米 can no longer come back. --------- Signed-off-by: ousugo <dkzyxh@gmail.com> Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
…arty (stablyai#12882) Terminals froze app-wide several times daily, needing a manual pkill. libuv unlinks the pathname a server bound to when it closes, with no ownership check, so a departing daemon deleted whichever socket then sat at the canonical path — including a live replacement's. The replacement kept hosting PTYs no client could reach. stablyai#12709 fixed that mechanism; this replaces the shape around it. Two invariants: only a daemon publishing itself onto the canonical endpoint may mutate that entry, and only by replacing one it has itself just proven dead; and no actor removes a name it did not create. Publish binds a private name, takes the canonical one with an exclusive link, and on EEXIST proves the incumbent dead by connecting before replacing it in a single rename. Only 'connected' means occupied and only refused/missing prove death — a timeout proves nothing and declines. Deletes the claim sweeper, the reclaim tail of killStaleDaemon, and three unfenced unlinkSync(socketPath) calls in the launcher. Measured: rename exposed no gap across 6,525 darwin / 8,004 linux probes of a live handover, where unlink-then-link gapped on 200 of 200. Verified on all three platforms: full suite on macOS and Linux, and daemon restart e2e on a real windows-2022 host. Contract in src/main/daemon/AGENTS.md.
Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com>
…ai#13128) * fix(terminal): return IME composition ownership to xterm * fix(mobile): derive terminal input from native replacement ranges * test(mobile): record iOS Japanese IME traces * fix(mobile): preserve native IME replacement ranges * fix(xterm): flush queued application input after IME commit * test(terminal): pin Korean intermediate commit * test: pin Windows IME shortcut ownership * test: replay IBus number candidate commit * fix: preserve native macOS input-method punctuation * refactor(terminal): remove stale mac focus override * fix(mobile): preserve soft keyboard deletion ranges * fix: keep IME-owned palette chords in renderer * fix: stop carried IME shortcuts at renderer owner * fix: preserve carried IME shortcut dispatch * fix: narrow main-owned shortcut actions * test(mobile): pin Japanese IME replacement traces * test(terminal): retain paired native IME trace * fix(chat): preserve browser IME composition ownership * fix(chat): retain macOS IME confirm gesture * fix(chat): expire unmatched IME confirm carry * fix(chat): isolate IME confirmation expiry * fix(chat): retain active IME confirmation * refactor(terminal): remove dead composition handler * feat(ime): add shared Enter-ownership seams for CJK composition The confirming Enter of a CJK composition arrives as two keydowns and the orderings differ by platform: Windows/Linux redispatch the unmarked Enter/13 before keyup, macOS delivers keyup first. A guard reading only isComposing or keyCode 229 misses the redispatch, so surfaces submitted on a confirm. Adds useImeEnterGestureOwnership (carry token, next-frame expiry), a shared ImeEnterGuardedForm for native implicit submission, and the cmdk seam covering 18 CommandInput surfaces at one site. A chorded Enter arms the carry but is never swallowed — the reverse would eat a user's deliberate Cmd/Ctrl+Enter. Both failure modes are pinned by ime-enter-gesture-ownership-contract.test.ts. Co-authored-by: Orca <help@stably.ai> * refactor(terminal): consolidate native input listeners and parked-screen owner Extracts the shared native-input listener installer and renames the parked-screen detector for what it actually does, replacing per-call-site duplication. The listener installer keeps a forgetOptionKeyLocationOnBlur flag so per-window semantics are preserved rather than flattened. Net deletion; no behaviour change intended. Co-authored-by: Orca <help@stably.ai> * test(terminal): pin recorded IME shapes as regression tests Nine regression tests built from hashed affected-platform captures, each with a paired ordinary negative and a discriminating mutation verified to take the file from all-passing to exactly one failure. Covers the Windows MS-Korean Shift family (#12179, #11878, #12151, #11946, #12152) and the Korean TUI line-break rows (STA-3237, STA-3222, STA-3129). STA-3237 pins the empirical 3-Shift / 2-active-composition / 2-newline ratio the device run established — the third Shift produces nothing because Space has already committed. That ratio is not derivable from a static capture. Co-authored-by: Orca <help@stably.ai> * fix(ime): guard Enter-commit surfaces against CJK confirm Applies the Enter-ownership guards across the surfaces whose Enter commits something: publishes, clones, pairs, installs, posts, or persists. Tiered deliberately rather than uniformly. Irreversible and remote-effect sites take the carry token, which also blocks the unmarked redispatch. Locally reversible sites take the oracle check with a one-line comment naming the residual, because a spurious commit there costs one undo. Three numeric fields are left unguarded with the reason in-code: Chromium blanks number inputs at compositionstart, so a confirm-Enter only ever reaches an empty-draft reset. Measured with a CDP probe rather than assumed — a guard that cannot fire is noise. Co-authored-by: Orca <help@stably.ai> * test(ime): teeth-check the Enter guards on every guarded surface One suite per guarded surface, each verified by deleting the guard and confirming the test fails. A green guard test without that check is unverified, not verified. Two shapes pass vacuously in happy-dom and are avoided here: native implicit form submission never fires, and blur() is inert on an unfocused element. Both made "the commit did not happen" assertions pass with the guard removed, so the suites assert the guard's contract directly instead. Co-authored-by: Orca <help@stably.ai> * fix(mobile): keep iOS Korean commits whole through the live-input path iOS Korean reports isComposing: false on every event, so it bypasses the composition guard entirely. The strict owner rejected UIKit's transformed post-change field and sent only the leading jamo — the reported symptom. Prefers the authoritative same-event field text over the predicted text when the supplied operation cannot produce it. Generic: no Korean special-case, no locale classifier, no normalization. Adds the RN-target-keyed submit carry alongside it. Co-authored-by: Orca <help@stably.ai> * test(e2e): make IME capture harnesses fail loudly instead of silently Four instruments recorded silence as success, so a void run scored as a clean one: - readTerminalImeBoundaryTrace returned an empty trace when the probe never installed, making every "nothing leaked" negative pass vacuously - summarizeLatencies([]) returned a perfect zero distribution that passed all three latency thresholds - the macOS Vietnamese spec pinned an input-source ID that does not exist, and failed as though the operator had chosen the wrong source - the expectedLineCount=1 prefix property was undocumented and one edit from silently downgrading a PTY assertion Input sources now resolve by enumeration and name the near-matches on failure. Co-authored-by: Orca <help@stably.ai> * test(terminal): cover Cangjie cancellation and fix a cross-namespace assertion Adds #11951's recorded Cangjie cancel shape to the existing cancellation suite, which covered Pinyin and Sogou but not Cangjie. One keystroke then Backspace arriving as deleteContentBackward with data: null, so the stale preedit is the only thing a fallback could replay. Verified against the historical pre-6cd944c62b3 bundle: the positive fails with ['尸'] where [] is expected, while the ordinary negative stays green. Also fixes the Vietnamese spec, which asserted a TIS-space input-source ID against getKeyboardInputSourceId(). Those two Orca APIs report the same source in different namespaces — TIS nests it under VietnameseIM, the app API does not. The resolver stays as an installation precondition; the assertion matches the leaf. Co-authored-by: Orca <help@stably.ai> * test(e2e): add a real-IME macOS arm for the Korean chord commit The existing korean-ime-terminal-shift-enter-commit spec synthesizes composition over CDP: Input.imeSetComposition sets the preedit directly and Input.insertText performs the commit. Asserting the IME produced events you injected yourself is circular, so that spec cannot certify real-IME behaviour. This arm selects 2-Set Korean via TIS, reads it back live, and injects through System Events key codes, so the OS owns the preedit, the commit instant, and isComposing. PTY byte expectations are preserved verbatim. Covers 2 of the original 4 cases by design. The other two are the Windows/Linux redispatch-before-keyup ordering, which macOS cannot produce and which cannot be selected -- the OS decides it. Reintroducing synthesis to "restore coverage" would reintroduce the circularity. Co-authored-by: Orca <help@stably.ai> * test(e2e): assert the macOS chord arm at the PTY boundary, not the renderer The byte expectations were transcribed from korean-ime-terminal-shift-enter-commit :364/:383, which assert against onData -- a renderer boundary where the terminator is CR. This spec reads the PTY child, where the tty has already converted CR to LF. Names both forms per row rather than swapping the constant, so the conversion reads as evidence that the capture reached past the renderer, as #11936 and #11951 record. Ctrl+Enter's CSI-u sequence is unaffected and is identical at both boundaries. Co-authored-by: Orca <help@stably.ai> * test(e2e): measure composer-to-onData latency and stop dropping IME keystrokes Two defects in the echo latency probe. It hooked onWriteParsed and onRender but never onData, so it measured key->parse->render echo rather than the composer-vs-onData delta the latency rows need. Adds a third hook feeding its own sample set. And `event.key.length !== 1` silently dropped IME keystrokes: Pinyin and Cangjie keydowns arrive as key:'Process' (length 7). Replayed over the captured corpus, the old filter accepted 580 of 4137 Chinese IME keydowns -- it was discarding 80% of them. The new filter matches the shape the owner itself branches on. Attribution charges each onData to the latest keydown rather than a FIFO head, because composing jamo emit no onData at all and a queue would credit a whole composition to its first keystroke. The consumer now asserts sample count before any percentile, so a zero-sample run cannot render as a flawless distribution. Co-authored-by: Orca <help@stably.ai> * test(terminal): pin the WSL shifted-jamo newline shape for #11919 In Korean 2-set, Shift types ordinary letters -- the double consonants and the compound vowels. Each such keystroke reaches Chromium as key='Process', keyCode=229, shiftKey=true. The v1.4.163 classifier matched exactly that pattern with no code guard, so it called those keystrokes Enter, rewrote them to a synthetic Shift+Enter, and injected a newline into the middle of the word -- with no Enter key pressed. That is why the reporters said "no modifier key pressed": they had not chorded Shift+Enter, but they had pressed Shift, to type the double consonant. Asserts the row's own recorded capture: 40 immediate keydowns, exactly 3 of them Shift-carrying inside a single syllable, and an onData stream with one newline per Enter press and none mid-word. Two ordinary negatives keep it from being a blanket mute -- the same session's non-IME keydowns still reach shortcut policy, and an ordinary Shift+Enter still resolves through the real policy. Co-authored-by: Orca <help@stably.ai> * test(terminal): pin the composition commit lag that made Korean type one behind macOS Korean 2-Set commits syllable N only when the first jamo of N+1 arrives, so compositionend and compositionstart land in the same task. A composition-start handler cancelled the pending finalizer that was the only path to triggerDataEvent and ended the session without emitting bytes, so every committed syllable reached onData exactly one syllable late and the backlog cleared only at a Space or Enter. Types continuously with no Enter and no Space -- either would flush the backlog and hide it -- and samples onData at every syllable boundary. Paired with a length-matched ASCII arm that stays green throughout, so the positive is a fact about composition rather than about timing in general. Bisected to a single call site across five builds: pristine, 1.4.155 and 1.4.162 pass, 1.4.163 fails, removing the one call repairs it, restoring it fails identically. That window is exactly the reporter's "started immediately after updating". Co-authored-by: Orca <help@stably.ai> * test(mobile): cover the send-queue abort that silently drops queued keystrokes One failed send in use-terminal-live-input-commit aborts every keystroke queued behind it, with the error swallowed by .catch(() => false). The existing test resolves(true) on every send, so the failure branch was uncovered. Four arms: the abort itself, an ordinary negative on the healthy path, a throwing sender, and a liveness control proving the queue recovers once the chain settles. Deleting the abort takes 4 passed to 3 failed, with the ordinary negative correctly surviving. Scope is stated in the docblock: this is a transport send-queue abort, reachable only via a real disconnect or RPC error. REQUEST_TIMEOUT_MS is 30s, so latency alone cannot reach the branch — consistent with #7094's symptom class, not proven to be its cause. * test(terminal): pin that daemon snapshot/restore cannot disturb a composition Two independent reporters attributed broken Korean composition to the always-on PTY daemon repainting terminal state over the preedit. The attribution is wrong on ancestry — the daemon shipped three months before the version both call good — but the boundary was never actually tested. Runs the real applyMainBufferSnapshot choreography against a live composition, including the full 2J/3J/H wipe plus the resize and alt-screen branches. textarea.value, selectionStart/End, compositionView.textContent and .active all survive byte-identical, and interleaving a restore between every jamo of 문제 still commits 문제 at onData. Also pins that the uncommitted preedit is absent from the captured snapshot: it lives in the textarea, never the buffer, so a restore has nothing stale to echo back. Injecting one textarea.value = '' into the restore fails exactly the three restore-boundary tests. * test(terminal): pin that Cmd tears down a composition where Ctrl and Shift do not xterm's composition keydown exempts only keyCode 16/17/18 (Shift/Ctrl/Alt) plus 20/229. macOS Meta — 91/93/224 — is absent, so a Cmd press mid-composition takes _finalizeComposition(false): the overlay goes dark and never recovers, because compositionstart is not re-fired. The user composes the rest of the word blind. Linux and Windows users press Ctrl and are exempt. xterm already has a Meta-aware modifier predicate in wasModifierKeyOnlyEvent, so this is an internal inconsistency rather than a deliberate choice. Owns no reported row and is version-neutral: 5/5 on both 1.4.162 and 1.4.163. The branch is unexercised in all 328 recorded traces, so this is a hazard pin, not a regression guard. Only the teardown is asserted; the likely duplicated commit needs a compositionend the IME kept alive across the Cmd, which no capture contains. Deleting the exemption fails exactly the three paired negatives; adding Meta to it fails exactly the two Cmd arms. * test(native-chat): characterize preedit loss when a question card replaces the composer An AskUserQuestion card fully replaces the composer by design, but the in-flight composition goes with it: the composer unmounts before compositionend reaches it, so the preedit is never committed to the draft. The committed text survives only because the draft is cached and restored via defaultValue. Node identity changes, value 'abc' is preserved, the 가 is gone. Drives the real NativeChatView -> SessionGate -> InteractiveCard -> questionActive swap -> Composer -> ComposerField, flipped by writing the same store field an AskUserQuestion hook event writes. Flipping questionActive to false fails exactly this test and nothing else across 639 native-chat tests, so the path was entirely unguarded. CHARACTERIZATION TEST: it asserts the loss. Fixing the defect — committing the preedit before the swap, or keeping the composer mounted — will make this file fail. Update the expectations to the new contract rather than working around them. Owns no reported row. #12118/STA-3219 flicker is keyed to token counters, which provably do not remount, and a question card arrives once per question. * test(terminal): pin the duplicated commit when Meta interrupts a composition _finalizeComposition(false) sends textarea.value.substring(start, end) but cannot clear the IME-owned textarea, so a later compositionend re-sends the same range. Meta reaches that path because CompositionHelper exempts only Shift/Ctrl/Alt; xterm's own wasModifierKeyOnlyEvent covers Meta four ways, so the omission is an internal inconsistency rather than a choice. Companion to the modifier-exemption guard, which deliberately pins only the overlay teardown. This pins the data consequence. HAZARD PIN: owns no reported row. The trigger is unverified on hardware — no capture in the corpus contains a Meta-during-composition gesture, and whether macOS keeps the composition alive across it is unmeasured. The duplication follows from the code given that sequence; whether users reach the sequence is the open half. An earlier premise that Space (keyCode 32) reaches this path was refuted by a corpus scan: 0 of 731 evidence files carry a keyCode-32 Space while composing, against 171 at 229, and 229 returns early. * test(terminal): characterize the syllable lost when the textarea blurs mid-composition CoreBrowserTerminal._handleTextAreaBlur clears the helper textarea unconditionally — "Text can safely be removed on blur" — while CompositionHelper._finalizeComposition reads the committed text back out of that same value from a deferred timeout. By the time it runs the value is empty, the substring is '', and triggerDataEvent never sees the syllable. xterm checks composition state in _syncTextArea and omits the same check here. Six cases. Blurring mid-composition loses the syllable in every ordering, including compositionend-before-blur, which is Chromium's real order — so it is not an ordering artifact. A bare textarea.blur() with no Orca code loses it too, which places the owner upstream: Orca's unguarded release on outside pointerdown is one trigger, not the cause. Committing 한 then blurring mid-가 yields ['한'] where ['한','가'] is correct: one syllable gone, surrounding text intact. Teeth checked by inverting — adding an Orca-side composition guard flips exactly the three cases that route through the release path and leaves the bare-blur and no-blur cases green, which is the scope split: a fix in regular-terminal-focus-ownership alone would not close this. HAZARD PIN, but unlike the others this one has a real production injector — clicking outside the terminal mid-composition. Owns no reported row. The shape matches #9738's report; the injector does not, and a shape match with a mismatched injector is not an owner. * test(terminal): say which arm the STA-3237 fixture came from The recorded keydowns are wave 4's A-shift-unmarked-only — the arm that emits no PTY bytes. Nothing in the file said so, so two readers concluded the row's events fail the owner's predicate and that STA-3237 and STA-3222 were different defects. They share an owner; the arm that fires is Process/229+Shift, absent from this bubble-phase trace because the owner claims it in the capture phase. Also corrects "code-blind": the v1.4.163 policy emits \x1b\r only for a shift-only key:'Enter', and a jamo keydown reaches that branch solely via the isTerminalImeProcessEnter rewrite. The mock is deliberately wider so the ownership guard stays under test if that rewrite moves. Comments only — no assertion, fixture value, or mock behaviour changed. * test(e2e): track the input-source selector the macOS specs shell out to Five tracked macOS IME specs ran `swift .tmp/select-input-source.swift`, a file that is gitignored and existed only on one machine. Anyone else checking out the repo — or the same machine after .tmp is cleaned — could not run them, and they are the capture drivers for the macOS rows that are blocked waiting for exactly those runs. Moves it to tests/e2e/ beside its callers. The chord spec now resolves it from __dirname rather than reaching two levels up into .tmp. * test(terminal): pin the CJK repaint decision against the reporter's own output #12164 comment 1 and #5921 report agent output with double-width glyphs rendering duplicated character-by-character while ASCII in the same line stays clean. No IME, no composition, no keystroke — the user never types the CJK. Segmenting all three verbatim samples into maximal same-risk-class runs gives 33 runs and zero violations of "this run is corrupted iff the production detector flags it": 17 wide runs all corrupted, 16 narrow runs all byte-identical. The paired negative is co-located in the same line rather than in a separate run — the reporter supplied it without knowing. Doubling is asserted as present, not uniform: 자바스크립트 and 시스템 each leave a jamo undoubled, which is a repaint-region boundary artifact rather than a per-character transform. The discriminating arm is in the test rather than a source mutation: be3f30e2f8d (#6890) elects a repaint for all 17 corrupted runs when the agent types nothing, and reverting its disjunct elects none. Both predicates agree once the user has recently typed, which is the pre-#6890 condition. Samples inlined with per-sample SHA-256 because .tmp is gitignored and cannot back a landed test. * test(terminal): pin macOS period substitution landing after the composition #11504's reporter published a DOM trace showing insertText ". " arriving 149ms after compositionend, when two spaces are typed with a CJK input source and NSAutomaticPeriodSubstitutionEnabled is on. This replays that trace against a real Terminal and asserts what reaches onData — bytes to the PTY, not anything visual. The owner is stock upstream CoreBrowserTerminal._inputEvent, not an Orca module, confirmed at the resolved install and in the shipped bundle Vite loads rather than in the TypeScript source. Three mutations against that install, predictions written before the runs, each failing exactly the arms predicted: dropping the composed/keyDownSeen guard fails two, dropping Orca's intercept fails the one arm where the payload arrives before the send window drains, and flipping || to && — the candidate-fix shape — fails the arm that pins the defect itself. CHARACTERIZATION: arm 1 asserts the broken behaviour and will fail the moment #11504 is fixed. Update it to the new contract rather than working around it. composed is absent from every recorded bundle, so composed: true is the spec-required value rather than a captured one; the test asserts it before dispatching so a harness that dropped the field fails loudly. * test(terminal): replay the recorded Windows Shift sessions through the IME guard STA-3179 reports a Shift release sending Enter; #12171 reports delayed Hangul plus doubled newlines. Both replay their own recorded Windows MS-Korean keydowns through resolveTerminalKeyboardShortcutAction with the shortcut policy mocked, so the assertions are about which events reach the policy and what reaches terminal input. STA-3179's held-Shift gesture yields exactly one newline, from the unmarked Enter alone; its release arms nothing for the next composition, asserted after a precondition check that the release really is keyups with shiftKey already dropped; and an ordinary Shift press-and-release still routes every keydown, which is the paired non-IME negative. Teeth, verified by mutation: bypassing the isImeOwnedKeyboardEvent guard in keyboard-handlers takes STA-3179 from 3 passed to 2 failed / 1 passed — the survivor being the ordinary-session negative, which is correct, since a non-IME session should not depend on that guard — and #12171 from 2 passed to 2 failed. Source restored byte-identical. Recorded shapes are inlined and the bundles cited in comments; nothing is imported from .tmp, which is gitignored. * test(native-chat): correct 61977d45177 — the preedit survives the question card 61977d45177 claimed a composed syllable vanishes silently when an AskUserQuestion card replaces the composer, and characterized that loss. The claim was false. Its premise was an artifact of the harness: the test simulated a preedit with a silent textarea.value assignment and no input event, which no IME does. Real composition fires input with insertCompositionText on every keystroke — the shape this repo already records in its own observed-event capture — and React's change handler returns on input/change with no composition gate, so onChange runs for each frame. The draft cache is written synchronously inside the updater, so the preedit is already committed before the card can arrive. Driven that way, it survives. Renamed to match the contract that actually holds, and extended: Hangul jamo-per-frame, Japanese kana accumulation followed by per-segment conversion asserting the candidate the user was looking at survives, and a pin on the mechanism itself — the draft cache holds the preedit while the card is up. Teeth: there is no fix to revert, so the mutation is the plausible wrong one — gating onChange on isComposing(). That takes 4 passed to 3 failed, with the English negative correctly surviving, since it has no composition to gate. Two consequences remain, recorded rather than fixed: the OS aborts the composition when the field disappears, so a lone jamo returns as a compatibility jamo the user cannot compose onto, and the remounted composer is unfocused because the card owned focus. * docs(native-chat): name the corrected commit and the degraded-jamo consequence Records in the file itself that 61977d45177 is pushed and wrong, quoting the two claims that are false, so a reader who finds it in git log reaches the correction from the file that replaced it. Also states the residual as a consequence rather than a curiosity: a lone leading jamo returns as a standalone compatibility jamo (U+3131), which is not a composable state — the user cannot resume the syllable, only delete and retype. Preserved, but degraded into something unusable. That is the note to find if a reporter ever describes exactly that. The invariant these tests pin is not "the composer commits on unmount" but "composition input events must reach React" — which is what a future IME change would break, and is not visible from the swap site at all. * test(terminal): replay the recorded macOS Telex commit boundaries #6905 reports Vietnamese composed characters breaking in the terminal. Replays the retained macOS built-in Simple Telex capture — recorded selection and value set before each dispatch, since that is what the commit range reads — and asserts what reaches onData: the first commit alone, then through the real Enter, then the ASCII tail of the same run. A code-point count would catch NFD normalisation. ENGINE CAVEAT, stated first in the docblock: this is macOS built-in Simple Telex, Telex only. The reporter's three named engines cannot run on the platform they declared, and which macOS Vietnamese engine they used is unconfirmed. This file certifies no engine, and does not imply VNI. The owner is upstream's — CompositionHelper._finalizeComposition's waitForPropagation branch — so the arms are copies under .tmp aliased by a scratch config, with node_modules verified unchanged by shasum after every run. Collapsing the range end onto its start fails all three; collapsing the start to zero re-emits the first word into the second commit, which is the reporter's "duplicated" direction. Different failure sets, so the mutants are distinguishable rather than merely detectable, and the ASCII assertion passes under both. Falsifiability here is by mutation, not by a defective build: #6905 does not reproduce at HEAD, so this has never been watched going red on a real reproduction. * docs(terminal): lead the #6905 test with its engine caveat Comment-only. Moves the caveat above the source line so a reader meets what the file does NOT establish before what it does — the capture is macOS built-in Simple Telex, the reporter's named engines cannot run on the platform they declared, and which engine they used is what gates this row. Co-authored-by: Orca <help@stably.ai> * docs(terminal): record that the swallow eats a keystroke after Japanese conversion This pin framed the swallowed insertText around Cmd interrupting a composition. A differential through Japanese multi-segment conversion shows it is broader: type a segment, convert, then press `a`, and the `a` is lost. No modifier, no exotic gesture. Korean surfaced it first only because 2-Set composes on nearly every keystroke. Also records why it cannot simply be fixed. The suppression de-duplicates IMEs that deliver their commit a task after compositionend, which a sibling test pins; this swallow is that dedup's false positive, and the two events differ only in payload, so no flag-timing change separates them. Both a smaller redesign and a content-aware variant were built and measured — the first duplicates on IBus, the second costs a reported row's test and is blocked while the patch cannot be regenerated. The Japanese arrays behind this are authored, not observed: no Japanese DOM composition trace exists in the corpus. * docs(terminal): a Japanese capture does exist — correcting dbeecb11bee That commit said no Japanese DOM composition trace exists in the corpus. False. One does, filed under the Linux bundles rather than the bundle named for Japanese: 30 DOM events, two にほんご->日本語 conversions, with full selection state per event. It is retained byte-identically in three further bundles — one capture copied four times, not four observations, checked by hash rather than by counting files. The claim came from checking the bundle named for Japanese, finding nothing, and generalising to the corpus without querying the rest of it. Replaying it emits 日本語日本語 under both sequencing extremes on all four arms, matching its own recorded onData. So "repeated conversion is undisturbed" is now captured rather than authored. It carries no post-compositionend insertText, so it cannot speak to the swallow: the a-after-conversion figure stays authored and unobserved. Also rewords the paragraph opener. It claimed to broaden a Cmd framing, but hazard 2 was never Cmd-framed — the lines above already say Cmd does not reach it. The real gap was that hazard 2 named no trigger at all, which reads as exotic when it is ordinary. * build(xterm): land the patch regeneration harness The five dependency patches under config/patches/ shipped with no tracked way to regenerate any of them. The xterm one is the hard case: it is derived from an upstream build, so no fix could be made without rebuilding, and the tooling to rebuild lived only in one machine's scratch directory. That blocked a measured fix for a live keystroke-loss bug, and the EditContext reduction an OSS survey identified as the only real one available. Adds the regenerator, the upstream pin, the hand-written source patch the bundle hunks derive from, tests, docs, and a PR job that verifies the shipped patches still match the pinned build. The job caches the shallow clone keyed on the manifest, so a cold run is minutes and a warm one under one. Round-trip verified: regenerating from a clean checkout reproduces the shipped patch byte-for-byte. Marks the emitted patch -diff -text. pnpm hashes it byte-for-byte, so a CRLF checkout would break install on Windows, and its minified bundle lines make a diff nobody can read — review the source patch instead. Also rejects unknown flags. --check was the fallback for any unrecognised argument, so a typo, or --help, silently triggered a full upstream build instead of what the caller asked for. * fix(xterm): stop swallowing a keystroke typed after an IME commit Type a Japanese segment, convert it, then press a key one macrotask later and that key was lost. No modifier, nothing exotic — every user who keeps typing straight after converting. Korean surfaced it first only because 2-Set composes on nearly every keystroke. handleCompositionInput discarded the payload unconditionally in the window after the deferred send: _isSendingComposition stays true for one macrotask after the timer cleared _pendingCompositionStart, and the branch substituted '' for whatever arrived. The suppression is not itself wrong — it de-duplicates IMEs that deliver their commit an event-loop turn late, which terminal-stock-composition.test.ts pins. It just could not tell a duplicate from new input, because the two events are identical apart from payload. Now it compares against _sentComposition, the text the deferred send actually emitted, and discards only a match. A flag-timing redesign was measured first and rejected: it fixed this and duplicated on IBus, because no timing change can separate events that differ only in content. Edited in config/patches/xterm-src/ and regenerated through the harness, so the emitted patch and the lockfile hash are derived, not hand-written. The commit-overlap pin's swallow arm now asserts the repaired contract — the value its own comment already named as correct and as what stock beta.287 emits. #11504's arm at :184 flips too; it never covered that report, as its own prior note recorded, and the reporter's +149ms arm is untouched and still asserting the defect. Provenance hashes in three test docblocks are updated, since regenerating changes the patch hash and with it the resolved install directory. * docs(terminal): re-measure the #6905 mutation citations against the new bundle Regenerating the patch moved the resolved install, so this docblock's patch_hash, line count, two line numbers and three mutation outcomes all described a bundle that no longer exists. The deferred branch is one the fix writes into, so the outcomes could not be re-pointed on reasoning. Line numbers read off both files by diffing anchors rather than derived by arithmetic: 201 to 205, 159 to 163. Outcomes re-run through the retained rig, which re-resolves through the module loader and re-derives each arm from a unique minified anchor: pristine 3 passed, m1 3 failed, m2 2 failed, m3 3 passed — identical to the old bundle. Guard controls in both directions exit 1, so the counts are falsifiable. Comment-only; the assertions and expectations are unchanged. * fix(xterm): size the preedit overlay to the cells its text will occupy updateCompositionElements computed the overlay's left edge from the grid but never its width, so the preedit rendered at the font's natural advance while the committed text takes two cells per wide glyph. Measured in Chromium 150: 가나다라 drew 48.45px as a preedit and 69.20px once committed — the same characters, same font, 30% narrower, and drifting further with each syllable. Every macOS mono font carrying Hangul measured 0.49–0.72 of two cells; never 1.0. Deriving the width from wcwidth and the cell measure moves Korean, Japanese and Chinese to 1.000 and leaves ASCII at 1.000, which it already was: 한 12.125 -> 17.297 (17.30 expected) 가나다라 48.453 -> 69.188 (69.20) 안녕하세요 60.563 -> 86.500 (86.50) 日本語 42.000 -> 51.906 (51.90) abcdefgh 69.234 -> 69.203 (69.20, unchanged) Edited in config/patches/xterm-src/ and regenerated through the harness, so the emitted patch and lockfile hash are derived rather than hand-written. The unit test asserts the arithmetic, which is what CI can run. The pixel consequence was measured on macOS with SF Mono in an Electron harness, not on the Windows font stack STA-3232 reports from — so this demonstrates the mechanism and does not stand as that row's platform evidence. * test(e2e): pin the macOS Korean preedit as visible only while composing #11914 reports the composing text invisible until Space. Its c3 was recorded as unobtainable, and the reason on file was wrong: the boundary IS assertable, but not in happy-dom, which reports display:block in BOTH the active and inactive states and zeros for every rect. A test there passes with the defect present. Captured on real hardware instead: hidden and 0x0 before, .active with display:block, a 15.84x16 rect and checkVisibility() true while composing 그, hidden again after. 39 DOM events, 2 composition starts, onData ["한","그","\r"]. Two mechanism findings are carried in the setup because both are invisible in the result and fatal if removed. The input source must be selected AFTER the app takes focus — focusing resets it to ABC. And the IME must be warmed until an observed keyCode 229; typed cold it emits raw QWERTY (g k s r m) with no composition at all, which is indistinguishable from an IME that is not installed. Two runs were voided on exactly that signature before the warm-up was found. The has229 and compositionStarts assertions exist to make such a run fail loudly rather than pass as a clean negative. Gated on darwin plus ORCA_E2E_NATIVE_MACOS_KOREAN, like its siblings. The final spec form has not itself been executed — the machine became unavailable — so it carries the probe's measured values as literals rather than a run of its own. * docs(e2e): correct 19a8d133db7 — the Korean preedit spec has been executed That commit said the landed form had never run and carried the probe's values as literals. It has now run on real hardware: 1 passed, 9.1s, rc=0, with the capture and log sealed under a verified hash manifest. The teeth check was also run rather than reasoned about, and it changes which assertion matters. Forcing the active overlay to max-width:0 with overflow:hidden — invisible on screen — leaves the active class, the textContent, display:block AND checkVisibility() all passing. Only during.rect.width fails. checkVisibility() is not sufficient against this defect; the bounding rect is the single load-bearing assertion, which the docblock already said and this run confirms. An earlier teeth attempt injected the CSS mid-run and tripped the hasActiveClass poll instead, failing at the wrong assertion. It is inconclusive and excluded from the seal rather than counted. * test(terminal): add #12171's ordinary-English arm from a real Windows capture c4 was recorded as unmet and the ledger sourced its control to evidence/windows-current/, which holds 12 captures and not one English one. The arm here comes from windows-9803-final instead — same probe, same host geometry, same injector, en-US with no IME, replayed keydown for keydown. Two limits are stated in the file rather than left for a reader to find. It is a different bundle and a different run about 3.6 hours later, so it is not a same-run arm. And it is #9803's range-active MUTANT arm: ordinary English stays byte-exact even with that saved-range mutation live, which is why it reads as a negative rather than as a baseline. Bundle cited by directory with its file SHA-256; MANIFEST.sha256 verifies 21/21, rc=0. Nothing imported from .tmp. * docs(terminal): correct #12164's grounds — the cited comments say no such thing The rejection of #12164 from this file's family was recorded as resting on its comment 1 (output doubling) and comment 2 (filed against 1.4.163). Checked against the API: the issue has exactly two comments, neither of which says that, and the string 1.4.163 appears nowhere in the thread. The conclusion survives on better grounds. The issue BODY's repro is "Run any CLI agent (Codex, AGY, Claude, etc.) that outputs Korean text into the Orca terminal" — untyped output, no keystrokes, no composition — so excluding CompositionHelper is right, and the input-path hunt was looking in the wrong place. The body is also LLM-authored (it still contains a literal "## 5. GitHub Submission Draft (Ready to Post)") and its Root Cause section blames a CJK IME preedit buffer its own repro never engages, so it should not be read as observation. Comment-only; suite unchanged at 5/5. * test(native-chat): make composition frames carry isComposing, not just inputType This suite's comment claimed "Gating onChange on `isComposing` breaks here." It did not. composeFrame() fired `input` with `inputType` but never set `isComposing`, so a gate on `isComposing` passed all four tests untouched — the suite asserted a discriminator it did not exercise. Composition frames now carry both, so neither gate is exempt. Verified by pointing the mutant at it: with an `isComposing` gate on the composer's onChange, this suite now fails 3 of 4 (it passed 4 of 4 before), and the ordinary-English arm correctly survives, since a composition gate should not touch it. Production code is unchanged and stays gate-free; the mutation was applied, measured, and reverted. Found while excluding NativeChatView's question-card remount as the owner of #12118 / STA-3219: the remount is real, but the preedit survives it precisely because this write path has no composition gate. * fix(mobile): ship the patched xterm build, matching desktop mobile pinned @xterm/xterm 6.1.0-beta.285 while the patch is keyed to 6.1.0-beta.287, so mobile shipped stock xterm and neither IME defect fix reached it: the swallowed keystroke after an IME commit (9506039de72) and the preedit sized to the font rather than the grid (e04e0c88da5). Bumps the three xterm packages to the desktop versions and adds the patch to mobile's own pnpm.patchedDependencies. No copy of the patch: pnpm accepts the parent-relative path and records it in the lockfile against hash 8d63166272e9040a…, byte-identical to what desktop resolves, so the two stay in step by construction rather than by a drift check. The workspace separation is untouched — root pnpm-workspace.yaml still declares `packages: []` and mobile keeps its own lockfile, which is what keeps the root's patches from failing as ERR_PNPM_UNUSED_PATCH. Verified in the generated webview bundle rather than at the install: alignPreeditToGrid 0->2, sentComposition 0->3, pendingInput 0->11, and the stock-only _handleAnyTextareaChanges 2->0 and dataAlreadySent 4->0. pnpm applies patches during linking before postinstall regenerates the bundle, confirmed by a revert/reinstall/re-apply cycle in both directions. Mobile suite 2971 passed, 3 skipped — identical before and after. Bundle +1,514 B (+0.24%). Lockfile churn is xterm-only; --frozen-lockfile passes. mobile/src/ime/ime-submit-carry.ts is NOT made redundant and is untouched: it handles iOS firing onSubmitEditing on a React Native native TextInput after unmarking a composition, which is outside the WebView entirely. Known divergence left alone: desktop also patches @xterm/addon-webgl and mobile now runs that version unpatched. That patch is glyph/texture-atlas rendering with nothing IME-related, so it affects neither fix. * test(terminal): pin the preedit overlay against already-committed cells STA-3132 (arm A), STA-3170 and STA-3232 report a Korean preedit painted on top of text already on screen. Builds v1.4.163-v1.4.166 cancel the pending finalizer in compositionstart, so a committed syllable reaches onData one syllable late and buffer.x is stale — the overlay lands on the cell the flushed syllable is about to occupy. Replays a recorded hardware trace rather than an authored one: the ordered DOM event stream captured on Windows + MS Korean (wave5-r2 evidence, 64 events), echoing onData back as PTY output. The load-bearing assertion is deliberately not the obvious one. Comparing overlay style.left against cursorX is tautological — left is computed from buffer.x. This counts committed syllables from the compositionend events the IME fired, so the two sides are independently derived. Discriminated by a historical re-add across seven real bundles, since the owner is deletion-shaped: pristine beta287, v1.4.155 and v1.4.162 pass; v1.4.163 fails; v1.4.163 with that single call removed passes; the byte identical baseline restored fails again; head passes. Every failing arm fails only this case — the ordinary negative stays green in all seven. The negative asserts its own category rather than claiming it: zero composition events, zero isComposing, zero keyCode 229, exactly 16 events, paired against the Korean arm's 4 starts / 3 ends / 11 updates / 64 events. Scope: cell indices, not pixels. happy-dom has no layout, so the recorded 8x16 cell metrics are supplied to the render service. This makes no claim about pixels visually overlapping; that is affected-OS confirmation and stays open. Covers the overlap arm only — STA-3132's auto-line-break arm and STA-3232's half-line-capacity and a11y arms are untouched. * test(e2e): matrix macOS period substitution against the OS preference #11504 reports macOS inserting ". " after a Hangul Space commit. This sweeps six arms across both states of NSAutomaticPeriodSubstitutionEnabled, reading the preference live per run rather than asserting a literal. Two results worth having on record. The reporter's stated trigger did not reproduce. Their words are "There is no second press at all. One space is enough", but korean-single-space emits zero insertText with the preference on or off. So does word-space-word-space. Their timing does reproduce, with different content. korean-double-space and korean-longer-word-double-space emit a delayed insertText at +122.5-122.7ms after compositionend — squarely the reported +149ms — but the payload is a space, never ". ". Consistent with the double-space rule seeing two slots under ABC and only one under Korean, where the IME commit consumes the first. The substitution itself is real and preference-bound: latin-double-space gives "ab . " with the preference on and "ab " with it off, on one build with the preference as the sole variable, reproduced across two runs. That also refutes a claim in PR #11506, which states the substitution "is enforced outside the renderer and never reproduces in dev builds, so changes here must be verified against a packaged app". It reproduced in the dev build twice and did not reproduce on the signed packaged app. That claim should not be used as a verification gate. Gated @headful behind ORCA_E2E_NATIVE_MACOS_PERIOD, same shape as the Korean preedit spec, so it does not run in ordinary CI. Evidence is onData and DOM only — the PTY-child reader aborted and no packaged-app arm was stable. * test(terminal): actually enforce the recorded jamo progression The preedit assertion compared sample.overlayText against sample.overlayText — the same expression on both sides. A lane proved it by mutation: corrupting seven of the eight recorded preedit values left the suite fully green. So the docblock's ㄱ→가→간→나→낟→다→달→라, which the matrix also cites as this row's recorded shape, was cited and unenforced. The first attempt at a fix was insufficient and is worth recording. Threading stroke.preedit through to the expectation still passed on a corrupted fixture, because that value both drives the rig and was the expectation — corrupting it moved both sides together. Same tautology, one level down. The expectation is now an independent literal. Verified by mutation rather than by reading: corrupting two recorded values fails one arm; restoring them passes 3/3. overlayCell was never affected — it is compared against a count derived from the compositionend events, not from the buffer, and remains the load-bearing assertion for the overlap. * fix(e2e): select the selectable input source, not the first match TISCreateInputSourceList can return several entries for one input source id. A third-party IME publishes a non-selectable parent alongside the selectable mode, and taking sources.first can return the parent — after which TISSelectInputSource fails with paramErr (-50) while the caller reports success from the enable step. Found with Qingg (com.aodaren.inputmethod.Qingg), which exposes exactly that pair under one id. Its mode id equals the bundle id, so filtering by name would not have helped; selectability is the discriminator. Now filters on kTISPropertyInputSourceIsSelectCapable and falls back to the old behaviour when nothing advertises it, so single-entry sources are unaffected. Also enables every entry for the id rather than only the one being selected: selecting a mode whose parent is still disabled fails the same way. Compile-checked, and selecting com.apple.keylayout.ABC still exits 0. Unrelated to the enable path: on macOS 26.5.2 third-party IMEs are gated behind a consent sheet in System Settings. TISEnableInputSource returns noErr immediately regardless, and the enable only lands if that sheet is answered while the requesting process is still alive. * test(terminal): discriminate #12171 against the real shortcut policy The prior candidate mutation for this row was correctly refused: its suite mocked shortcut policy so Process/229 became actionable, while the real resolveTerminalShortcutAction has no Process branch — so the kill measured the mock. This does not mock it. Replays a capture of this row's own gesture (d, l, Shift+T, e, k, Space, Enter under MS Korean, committing 있다) taken on Orca 1.4.164, through the real useTerminalKeyboardShortcuts hook, capturing bytes at terminal.input. The earlier capture could not discriminate at all because it recorded no shiftKey; this one records it on 10 of 10 keydowns with code populated. One physical Shift+T produces two shifted Process/229 keydowns. Under the pre-#12265 classifier each synthesizes {key:'Enter', shiftKey:true}, which the real policy resolves to sendInput '\x1b\r' — twice, giving 1b0d1b0d, the two escapes the known-bad ed96881b0d1b0d contains. Mutation is the retained pre-12265-process-shift.patch applied to HEAD, not an authored one: patch -p1 applies clean and diffs identical to the mutant copy. Arms are copies; shared source hashes the same before and after. The English arm stays clean under both modules, so the mutation discriminates by language rather than by harness — and a real Shift+Enter through the same rig yields exactly ['\x1b\r'] in every arm, so a silent pristine result means the code is quiet rather than the harness dead. Scope: 1b0d1b0d is measured at the renderer boundary. The capture recorded no PTY bytes — window.api.pty is frozen on shipped builds and the onData channel needs a build-time flag — so this shows the renderer producing the two escapes that payload contains, not a re-observation of the payload. * docs(native-chat): narrow this file's disclaimer to what is now true It said "THIS OWNS NO REPORTED ROW". Half of that is stale: the remount site is now the attributed owner of #12118 and STA-3219. On real Windows TSF the questionActive swap aborts a live composition — the old node gets only a blur and no compositionend, the text returns as committed, and the next jamo yields 아ㄴ rather than 안. The other half holds. This file pins the opposite property, that the text survives, which is the half those reporters already agree with. Mutation shows the gap rather than asserting it: deleting the unmount entirely leaves three of four tests green, because every substantive assertion is after.value === … and a composer that never unmounts keeps its value. Also records why the abort cannot be asserted here. The DOM exposes no observable separating committed text from a live preedit — value is the same string either way, there is no EditContext, and the only composing-ness state is a per-instance ref discarded with the node. A test pinning "no compositionend fires" would be an anti-guard: red the day it is fixed. The cadence objection is kept, since it is now the open question rather than the reason for exclusion. * refactor(terminal): drop the unread isComposing field from XtermBypassEvent Added by #6396 for terminal IME candidate handling that this branch has since removed. No production or test code reads it, and the policy is safe without it: during composition `key` is 'Process', so the non-ASCII printable checks that would care never match. Co-authored-by: Orca <help@stably.ai> * fix(native-chat): keep the composer mounted through an in-flight IME composition A question card replaced the composer outright (`{questionActive ? null : <NativeChatComposer/>}`). Unmounting the field mid-composition aborts the composition in the OS: the node is detached before `compositionend` can fire, the preedit returns as committed text, and a resumed Hangul syllable degrades — 아 then ㄴ yields `아ㄴ`, never `안`. Confirmed in rasterised pixels on Windows with a real MS Korean IME, at both v1.4.171 and the reporter-era v1.4.164 (the swap block is byte-identical across them): the preedit underline present before the swap, the composer visibly absent during it, and the same glyph back afterwards WITHOUT the underline — committed, not composing. The swap is now deferred while a composition is in flight, which is what editors that survive IME do: ProseMirror gates DOM work on `view.composing`, CodeMirror protects the composing subtree from redraws. Hiding instead of unmounting does not work — `display:none` and `visibility:hidden` both blur the focused element and abort the composition the same way. The hold releases on `compositionend`, which browsers also fire on blur, so clicking into the card's own answer input yields the input region immediately; with nothing composing the card still replaces the composer at once, so no stray "Send a message" appears beside a question. The existing characterization test flips to a regression guard: it pinned the node being destroyed, which was the defect. Node identity is the load-bearing assertion — value-only checks are trivially satisfied by a composer that never unmounts and cannot tell a held composition from a destroyed one. The typing-redirect handler moves to its own hook. That is not cosmetic: both touched files sat at the 400-line cap, and `max-lines` suppressions are forbidden, so the room had to come from a real extraction. * fix(macos): opt Orca out of AppKit automatic period substitution macOS "Add period with double-space" (`NSAutomaticPeriodSubstitutionEnabled`, on by default) is applied by AppKit's text input system. Native terminals never join that system; Chromium text fields do, so xterm's helper textarea inherits it and a double space arrives as `". "` — a period nobody typed, handed straight to the PTY (#11504). Chromium answers AppKit for quote and dash substitution and defaults both off, but declares no period accessor at all, so AppKit applies that one without asking. This user default is the only lever: there is no per-field or per-webContents opt-out to prefer over it. Writing the key into Orca's own defaults domain overrides the global value for this app alone and leaves the user's system-wide setting untouched. It necessarily covers every Orca text field, not only terminals — AppKit offers no narrower scope, and that tradeoff is deliberate rather than accidental. Measured on the reporter's own build v1.4.161: with the preference ON, typing a,b,space,space yields `onData ["a","b"," ",". "]`; with it OFF the same arm yields two spaces. Note the issue's causal model is wrong and this fix does not follow it. It claims the substitution only fires with a CJK input source and never with ABC. The measurement is the inverse — every Korean arm is clean and the ABC arm is the one that fires — so the fix is not conditioned on input source. NOT YET VERIFIED ON HARDWARE. The unit tests inject the writer, so they prove the call is made on darwin and skipped elsewhere; they do not prove AppKit honours an app-domain override for this key. That check is outstanding. * fix(xterm): keep a live composition across a lone Cmd press on macOS CompositionHelper.keydown exempted keyCodes 16/17/18 from tearing a composition down, which covers Shift/Ctrl/Alt but not macOS Meta — 91/93 in Chromium, 224 in Firefox. A lone Cmd press mid-composition therefore reached _finalizeComposition(false), which dropped the preedit overlay's `active` class and committed the live syllable early. macOS keeps the marked text alive across that press, so no later compositionstart re-arms the overlay and the rest of the word composes invisibly. Measured on hardware (m4air, macOS 26.5.2, Apple M4, 2-Set Korean) with the Cmd posted as a CGEventType.flagsChanged, which is what a physical modifier emits. AppleScript `key code 55` posts nothing a browser can see — a bare `key code 56` for Shift is equally silent — which is why no capture in the corpus ever reached this branch. Three arms, same build otherwise: overlay live throughout with the exemption, dark and prematurely committed without it, live again with it restored. Evidence under .tmp/ime-handoff/swarm-scratch/wave31-cmd-preedit/evidence/. The fix cannot widen past a lone modifier: only a standalone press reports these keyCodes, and a Cmd chord during composition is reported by Chromium as 229, which was already exempt. Cmd+A still ends the composition, via the IME's own compositionend. Ghostty draws the same line, returning early from flagsChanged under hasMarkedText() for every modifier including Super. Orca's terminal pane was never affected — shouldSuppressTerminalModifierKeyboardEvent drops a standalone Meta keydown before xterm sees it, and deleting only 'Meta' from that set is what flipped the hardware arm to broken. The popout preview terminal and mobile's webview install no such guard and did reach the teardown. terminal-ime-xterm-composition-commit-overlap.test.ts asked its fixer to update the two Cmd arms to the values it named as correct; both now emit a single ['한']. * test(native-chat): drop two byte-identical duplicate cases `4632b86919d` copy-pasted two cases twice into the same describe block: `retains carry across a same-frame non-Enter keyup before redispatch` and `expires carry before a deliberate Enter after the next frame`. Each pair is byte-identical — same title, same body — so the copies asserted nothing the originals did not. This is what has been failing `static analysis` on this branch since 2026-08-06: `oxlint vitest(no-identical-title)` reports both under `--deny-warnings`, and `verify` fails solely because it requires static analysis to pass. Every other gate in `verify` was already green, including typecheck, xterm patch sync, the full test shard set, and both package jobs. 12 cases still pass in the file. * test(e2e): skip the WebGL arm when no WebGL renderer exists The #12164 probe runs two arms, webgl and dom, and closes by asserting the active renderer is the requested one. That assertion is right for the dom arm — it is what proves the pane actually left WebGL, without which the arm is meaningless — but headless CI has no GPU, xterm falls back to DOM silently, and the webgl arm then fails. The failure reads as a Korean rendering defect and is not one, so the webgl arm now skips with the active renderer named. The dom arm keeps the assertion unchanged. This is the third of three checks that have been red on this branch since 2026-08-06. `static analysis` and `verify` were fixed in c51c6b5837e; the CI log shows this job as 1 failed / 1 passed, the pass being the dom arm. * chore(lint): drop five unused no-console disable directives `check-changed-code-quality` reports unused eslint-disable directives as errors, and these five sat above diagnostic `console.log` calls in IME test and spec files where `no-console` is not enabled — so each suppressed nothing. This is the second of the two static-analysis steps. `c51c6b5837e` fixed "Enforce focused code-quality plugins" (duplicate test titles); this fixes "Enforce changed-code quality". Both had been red on this branch since 2026-08-06, and I mistook the first for the whole job. The diagnostic logs themselves are kept — they are what a failing IME arm prints for a reader to inspect. * test(e2e): cover #12164 under fractional device scale factor Fractional display scaling was #12164's last unexplored branch, and the reason is worth recording: earlier attempts were BLOCKED, correctly, because they proposed mutating the Windows display scale on a remote physical machine with no console recovery. `--force-device-scale-factor` reaches the same renderer state per process, so nothing outside the Electron instance changes and there is nothing to restore. The hypothesis was specific: `프프로로젝젝트트` is what a half-pixel cell boundary could produce on a 2-column glyph, and nothing else in the suite varies dpr. Measured at 1.25 and 1.5, both under WebGL: ink extents 25/21/16 with identical ink groups, matching the scale-1 run. No doubling. The arm self-certifies before asserting — if the flag does not take, the test fails rather than silently measuring at dpr 1. That matters here: the sibling spec's WebGL arm went two days reporting a missing GPU as a Korean rendering defect precisely because a silent fallback looked like a result. * fix(terminal): match Mod+letter shortcuts by physical key, not IME-rewritten key With a CJK input source active, macOS and Windows report the physical key through `code` but rewrite `key` to the layout's character: Korean 2-Set turns Cmd+C into `{ key: "ㅊ", code: "KeyC", metaKey: true }`. Every `key.toLowerCase() === 'c'` match misses it, so the shortcut is not recognised and xterm encodes the chord as PTY input instead — issue #13033 reports `ESC[12618;9u` and a terminal that jumps to the bottom, because user input scrolls the viewport. This is the same key-vs-code confusion that owned #12171, where a `Shift+T` typing ㅆ was read as Enter for want of a `code` guard, so the fix is the same shape: trust `code` when it is present, fall back to `key` and then the legacy `keyCode` when it is not (Chromium omits `code` on synthetic and some keypress events, and `keyCode` keeps its US value even when `key` is rewritten). Applied to the four terminal-side sites, including the dashboard pop-out, which #13033 called out specifically as having its own key handler: pty-connection.ts Cmd/Ctrl+C copy guard keyboard-handlers.ts Cmd+G search navigation agent-interrupt-inference.ts interrupt inference preview-terminal-key-handler.ts pop-out paste Nine further `key.toLowerCase()` letter matches exist outside the terminal (TaskPage, editor, GitHub composer, browser markup). They have the same defect and are deliberately left for a separate change rather than widening this one. An existing case, `matchSearchNavigate > returns null for wrong key`, overrode only `key` and left `code: 'KeyG'`, so it began passing for the wrong reason. It now overrides both — which is what "wrong key" means once matching is physical — and a companion case pins the Korean-rewritten chord still matching. #13033 was closed NOT_PLANNED; the reporter's event shapes drive the new test. * fix(renderer): match every Mod+letter shortcut by physical key, not IME-rewritten key Completes the previous commit. A CJK input source rewrites `event.key` while `event.code` keeps the physical key, so `key.toLowerCase() === 'z'` and friends silently stop matching — the shortcut is not recognised and the keystroke falls through to whatever handles unclaimed input. The helper moves to `@/lib/ime-latin-shortcut-key` first: it now serves the editor, GitHub composer and browser markup, and importing terminal-pane internals into those would be the wrong direction. `lib/` already hosts `ime-composition-keyboard-event` for the same reason. Nine remaining sites, all previously unreachable under Korean/Japanese/Chinese/ Vietnamese input: TaskPage, ActivityPrototypePage, ProjectViewWrapper Cmd+F search useMarkupKeyboardShortcuts Cmd+Z undo GitHubMarkdownComposer, RichMarkdownLinkBubble, rich-markdown-link-shortcut Cmd+K link native-chat-shortcut Cmd+J rich-markdown-key-handler Cmd+Shift+X Six of the nine test `!== 'letter'` as early-return guards and three test `=== 'letter'`; the negation is applied per site, since a blind substitution would have inverted six of them. Full suite: 4249 files pass. Three files fail locally and none is caused by this change — the branch touches no file under `src/main/` or `src/relay/`, all four failures reproduce on an unmodified tree or pass in isolation (the worktree poller passes 21/21 alone, so it is full-suite parallelism), and all 16 CI test shards are green. * docs(ime): scope the IME composition rules to the terminal-pane directory #11893 proposed adding these to the root `AGENTS.md`, which every agent loads on every task regardless of what it is doing. They only bind keyboard handling, the composer and the terminal input path, so they belong next to that code — `tests/e2e/AGENTS.md` already establishes the nested convention here. Kept from #11893: range-derived commits, guarding above the key dispatch, the `attachCustomKeyEventHandler` / `CompositionHelper` interaction, no normalization at commit, and the recorded-trace evidence bar. Added from defects found since it was written: - match shortcuts on `event.code`, not `event.key` (#12171, #13033) - `keyCode === 229` means an IME owns the press - do not unmount a field mid-composition, and hiding is not a fix because `display:none` blurs and aborts it too (#12118, STA-3219, #11332) The evidence bar now also names the mutation check, since a test that survives deleting the code it guards is guarding nothing — a failure this effort hit more than once. * fix(terminal): gate Ctrl+Enter CSI-u on a negotiated pane, porting #12462 Found while scoping the rebase onto `main`: #12462 landed on 2026-08-06 and fixes a real defect this branch does not carry. Ctrl+Enter emitted `\x1b[13;5u` unconditionally, so a pane that never negotiated the kitty keyboard protocol — local Windows ConPTY, plain shell — printed the escape verbatim into the prompt.…
…ai#13154) Three guards in daemon-health.test.ts skipped tests that assert PID-record and start-time logic. The file's daemonTestSocketPath already returns a real named pipe on win32, so none of them needed a Unix socket — the skips cost coverage for nothing. Verified on a Windows host: the file goes from 29 passed / 4 skipped to 32 passed / 1 skipped, restoring Windows coverage of killStaleDaemon's ownership decisions, which stablyai#12882 changed. The fourth guard stays: that test spawns a real child and binds a filesystem socket path, which fails with listen EACCES on Windows.
…tablyai#12811) * fix(browser): reject Chromium product versions in cookie-import UA Do not persist Chrome/1.x User-Agents when a fork (e.g. Arc) reports its product CFBundleShortVersionString. Only engine-scale majors (>=70) are advertised; otherwise fall back to Electron's default UA. Preserves Chrome-shaped UAs for real Chromium engine versions. Fixes stablyai#12726 * fix(browser): reject malformed Chromium version tokens in UA Validate every numeric component before advertising Chrome/… so values like 70.not-a-version fall back instead of polluting the UA. Also build macOS app paths with path.join per coding guidelines. * fix(browser): drop already-persisted Chrome/1.x UAs on session restore The version gate stops new fork imports from writing Chrome/1.x, but profiles imported before it keep the broken UA in browser-session-meta.json and replay it on every launch, so affected users stay blocked with no in-app recovery. Move the gate into browser-session-ua (the module that owns UA shape, and the one both callers can import without a cycle) and drop an unadvertisable persisted UA during restore so the profile falls back to Orca's own engine UA. --------- Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com>
…tablyai#13168) * test(terminal): pin the recorded Korean commit-before-newline order (STA-3132) Recorded first-party on Windows 11 + Microsoft Korean (HKL 0412) against the defect-era v1.4.164 build, with bytes read on the far side of the PTY: the terminal received ea b0 80 0d, the syllable strictly before the CR. The capture did not reproduce the suspected deferred-newline inversion. That route needed a session end carrying dataPendingReconciliation, which plain compose-then-Enter cannot produce because the IME finalizes first and the newline is never held; back-to-back arms at 25/60/120 ms did not reach it either. The test therefore pins the ordering rather than discriminating a fix. Co-authored-by: Orca <help@stably.ai> * test(terminal): restore Hangul back-to-back flush coverage deleted with the composition layer stablyai#12278 fixed a Hangul syllable that was not flushed before the next composition began — the force-end path, and the one that leaves stale glyphs behind. Returning composition ownership to xterm deleted both that patch and its test, so nothing guarded the behavior any more. Replays the recorded back-to-back arms (25/60/120 ms, read as 가\r나 at the PTY) against a real xterm Terminal. It passes on main: stock xterm flushes the committed syllable natively, so the removal was safe rather than a silent regression. Co-authored-by: Orca <help@stably.ai> * test(mobile): pin accessory-byte ordering behind a Hangul commit Returning composition ownership to xterm deleted the accessory-input commit tests along with the hook they targeted, but the guarantee they protected is user-visible and still applies: an accessory-bar keystroke must not overtake the syllable being committed, and must be suppressed when that commit fails. Drives the current hook with an Android composing-region trace rather than reconstructing the deleted coordinator. Co-authored-by: Orca <help@stably.ai> * test(terminal): replay recorded IBus and fcitx5 Hangul traces offline Commits interleaved with ASCII (한abc글) are the Linux IME gesture users report on, and its failure modes are a lost syllable and a doubled one. That gesture was only covered by tests/e2e/terminal-linux-ime-native.spec.ts, which needs a Linux host running a real input framework. Fixtures are the recorded captures from the sealed linux-final evidence run, replayed against a real xterm Terminal: exact onData, exactly-once counts across five repetitions, and the PTY bytes the recorded run actually received. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai>
…boards (stablyai#13104) * feat(terminal): map Korean Won (₩) key to backquote on macOS Korean keyboard users type markdown code fences and shell backquotes on the key that US layouts reserve for ` — 두벌식 and 세벌식 390 put ₩ there, 세벌식 최종 puts *, so there was no way to type a backquote without switching layouts. Add a Mac-only terminal setting, "Korean Won (₩) to Backquote (`)", that rewrites the plain backquote-position keystroke to backquote while a Korean input source is active. It sits in Terminal → Advanced, right below the existing JIS Yen (¥) to Backslash (\) mapping. The rewrite keys on the keystroke position alone — no character or layout-variant knowledge — and follows the live input source through the existing MacNativeTextInputSourceTracker, which refreshes on focus and keyboard activity (Caps Lock / 한영 input switches never blur the window). Modified chords and IME-composed events pass through untouched. Covered by resolver and input-source tracker unit tests; verified manually in the GUI. * docs(terminal): clarify Korean Won mapping scope and add docstrings State in the setting copy that the backquote rewrite applies only while a Korean input source is active, and add JSDoc to the Korean Won resolver exports (addresses CodeRabbit pre-merge docstring coverage and copy-clarity findings).
…ablyai#13181) prefetchKoreanInputSource ran under a bare isMac check, so every macOS user paid for it. Each refresh shells out to `defaults export | plutil | plutil` — four processes in the main process — and input-source toggle keys pass force: true, so one Caps Lock press costs two probes. terminalKoreanWonToBackquote defaults to false. Threads the setting through KeyboardHandlersDeps and into the effect's dependencies, so enabling it mid-session still warms the cache before the first Backquote. Co-authored-by: Orca <help@stably.ai>
…inting TUI (stablyai#12463) `holdForeground` and `coalesceForeground` each cancelled the other's fallback timer on the Windows ConPTY DEC 2026 (synchronized output) path. A continuously repainting TUI such as Codex therefore left the foreground latch stuck open, so every later chunk was held instead of coalesced and output never reached the visible pane until the tab was refreshed. Break the mutual cancellation and mirror the hidden path's scan on the foreground path, carrying a marker tail so a ConPTY-split DEC 2026 marker is still detected. Nothing was wrong with Flutter — it was simply a long-running command behind a repainting TUI. Fixes stablyai#8754 Co-authored-by: Orca <help@stably.ai>
…s negative (stablyai#13182) On a fresh session the Won rewrite silently did not fire — real-hardware capture on macOS 26.5.2 with 2-Set Korean read 3 of 3 Backquote presses as e2 82 a9 at the PTY, and the feature only started working after the first press or an input-source round trip. Cause: the reader returns null for 'no signal' — the IPC is not exposed yet at startup — as well as for a genuinely absent source, and refreshInputSourceId committed that as isKoreanInputSourceId(null) === false. An unknown became a confirmed negative that nothing retried, because a keystroke-triggered refresh is async and cannot classify the press that triggered it. Leaves the cache unknown on a null read and retries the startup probe with bounded backoff, so the gate is warm before the first key. Bounded because each probe spawns defaults export | plutil | plutil. Co-authored-by: Orca <help@stably.ai>
… the worktree path (stablyai#12465) `mapTerminalFilePath` derived the WSL distro only from the *shape* of `worktreePath`. A worktree on a native Windows drive whose project runs under the WSL runtime gets a shell whose paths are POSIX, so no distro was found, the path went verbatim to a Win32 stat probe, and the candidate was dropped — no underline, no tooltip, inert Ctrl+click. Resolve the pane's distro from the execution runtime, falling back to the old worktree-shape derivation so existing behaviour is unchanged. Note the half of stablyai#8156 covered by merged stablyai#8215 (worktree on the WSL filesystem) was already fixed; this closes the remaining gap. Fixes stablyai#8156 Co-authored-by: Orca <help@stably.ai>
…sponses (stablyai#12473) * fix(native-chat): resolve WSL Codex transcripts so Chat UI renders responses Codex reports a guest Linux transcript path. On a Windows host `existsSync` resolved it against the current drive (`C:\home\...`) and discarded it, and the id-based fallback only searched host roots. `resolveSessionFilePath` returned null forever while the watcher reported `watching: true`, leaving Chat UI permanently empty. Translate the guest path to its host-readable UNC twin, classifying **before** any `existsSync` probe on win32 so the `C:\home` false positive cannot fire. Adapted from stablyai#10639 with one required correction: it uses the **async cached** WSL seams (`listWslDistrosAsync` / `getWslHomeAsync`) rather than the `execFileSync` ones, which would stall the Electron main thread for up to 5s per tick of the resolve-poll loop on a cold distro. Fixes stablyai#10326 Co-authored-by: Orca <help@stably.ai> * fix(native-chat): stop the WSL transcript probe firing every poll tick Three follow-ups from review of the WSL Codex transcript fix: - The UNC translation was retried on every fast resolve-poll tick (measured 10 sync UNC stats per 100ms) because only a successful result was memoized. Gate the retry to the slow fallback cadence. - A non-empty WSL home list was cached for the process lifetime, so a distro that was still booting during the first probe stayed excluded forever. Expire both branches; getWslHomeAsync caches successes, so a refresh only re-spawns wsl.exe for the distros that actually failed. - codexSessionsDirs enumerated every distro's home eagerly, waking distros the user left stopped even for native-Windows panes. Make the WSL roots a lazy tier consulted only after the host's own Codex roots miss. The resolve-poll suite became platform-dependent and only passed off Windows; pin the platform and add explicit win32 coverage. --------- Co-authored-by: Orca <help@stably.ai> Co-authored-by: OrcaWin <alpha-eng@stably.ai>
* feat(quick-commands): support remote host collections * fix(quick-commands): address remote host review * Preserve local Quick Commands UI --------- Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com>
Map DEEPSEEK_API_KEY balance (GET /user/balance) into the status-bar rate-limit pipeline so prepaid credit is visible alongside other providers. Preserves: existing provider poll/stale policy and agent gating contracts. Evidence: vitest deepseek-fetcher + provider-visibility + service suites. Refs stablyai#12869
innocarpe
force-pushed
the
fix/deepseek-status-bar-usage
branch
from
August 8, 2026 15:38
713c674 to
e33f594
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.
Summary
Portfolio mirror of upstream stablyai#12873 — DeepSeek status-bar usage via
DEEPSEEK_API_KEY+/user/balance.See upstream PR body for full template (Summary, Testing, AI Review, Security Audit, Notes) and explicit Accounts UI deferral.
Fixes stablyai#12869 (upstream issue)