diff --git a/.github/workflows/performance-contracts.yml b/.github/workflows/performance-contracts.yml index c2c330ea00f..e8132e2aa79 100644 --- a/.github/workflows/performance-contracts.yml +++ b/.github/workflows/performance-contracts.yml @@ -19,7 +19,6 @@ on: # Contracts without the .performance suffix. Keep in sync with config/vitest.performance.config.ts; # without these a rename lands green and only breaks the next nightly. - 'src/main/runtime/session-tabs/session-tabs-phone-layout-moves.test.ts' - - 'src/renderer/src/components/terminal-pane/pty-connection/startup/saved-resume-cancellation.test.ts' - 'src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-terminal-writer-acquire.test.ts' - 'src/shared/agent-launch/permissions/tui-agent-permission-choice.test.ts' - 'src/main/runtime/rpc/methods/projects/repo.test.ts' diff --git a/config/build-plugins/react-compiler-uncompiled-files.mjs b/config/build-plugins/react-compiler-uncompiled-files.mjs new file mode 100644 index 00000000000..9c7826c0f19 --- /dev/null +++ b/config/build-plugins/react-compiler-uncompiled-files.mjs @@ -0,0 +1,36 @@ +import { readFileSync } from 'node:fs' +import { resolve } from 'node:path' +import { fileURLToPath } from 'node:url' + +// Why: import.meta.url, not dirname — electron-vite's config bundle rewrites only the former. +const REPO_ROOT = fileURLToPath(new URL('../..', import.meta.url)) + +export const UNCOMPILED_LIST = 'config/react-compiler-uncompiled-files.txt' +// Hand-kept list plus every file still in a hazard ratchet; compiled, these would run differently. +export const UNCOMPILED_FILE_LISTS = [ + UNCOMPILED_LIST, + 'config/render-path-translate-baseline.txt', + 'config/react-compiler-hazard-baseline.txt' +] + +/** Repo-relative paths from a list or count baseline: `[count ]path[#detail][ # note]`. */ +export function parseListedFiles(text) { + return text + .split('\n') + .map((line) => line.trim()) + .filter((line) => line && !line.startsWith('#')) + .map((line) => + line + .replace(/^\d+\s+/, '') + .split('#')[0] + .trim() + ) +} + +export function readListedFiles(list, root = REPO_ROOT) { + return parseListedFiles(readFileSync(resolve(root, list), 'utf8')) +} + +export function reactCompilerExcludedFiles(root = REPO_ROOT) { + return [...new Set(UNCOMPILED_FILE_LISTS.flatMap((list) => readListedFiles(list, root)))] +} diff --git a/config/build-plugins/react-compiler.ts b/config/build-plugins/react-compiler.ts index fcdb314daa8..33ec76cc24b 100644 --- a/config/build-plugins/react-compiler.ts +++ b/config/build-plugins/react-compiler.ts @@ -1,25 +1,5 @@ -import { readFileSync } from 'node:fs' -import { resolve } from 'node:path' import type { Options } from '@vitejs/plugin-react' - -// Hand-kept list plus every file still in a hazard ratchet; compiled, these would run differently. -const UNCOMPILED_FILE_LISTS = [ - 'config/react-compiler-uncompiled-files.txt', - 'config/render-path-translate-baseline.txt', - 'config/react-compiler-hazard-baseline.txt' -] - -function listedFiles(list: string): string[] { - return readFileSync(resolve(list), 'utf8') - .split('\n') - .map((line) => line.trim()) - .filter((line) => line && !line.startsWith('#')) - .map((line) => line.replace(/^\d+\s+/, '').split('#')[0]) -} - -export function reactCompilerExcludedFiles(): string[] { - return [...new Set(UNCOMPILED_FILE_LISTS.flatMap(listedFiles))] -} +import { reactCompilerExcludedFiles } from './react-compiler-uncompiled-files.mjs' export function isReactCompilerEnabled(): boolean { return process.env.ORCA_REACT_COMPILER !== '0' diff --git a/config/focus-setting-read-baseline.txt b/config/focus-setting-read-baseline.txt index c516a2d6722..60cbc8028f2 100644 --- a/config/focus-setting-read-baseline.txt +++ b/config/focus-setting-read-baseline.txt @@ -12,7 +12,7 @@ 3 src/renderer/src/components/LinearIssueTextEditor.tsx 3 src/renderer/src/components/LinearIssueWorkspace.tsx 5 src/renderer/src/components/LinearItemDrawer.tsx -1 src/renderer/src/components/NativeChatResumeOnRestartGroups.tsx +1 src/renderer/src/components/native-chat-recovery/tree/NativeChatResumeOnRestartGroups.tsx 1 src/renderer/src/components/activity/ActivityPrototypePage.tsx 1 src/renderer/src/components/activity/activity-scope-filter-controls.tsx 2 src/renderer/src/components/activity/activity-thread-actions.ts diff --git a/config/react-compiler-bailout-baseline.txt b/config/react-compiler-bailout-baseline.txt index d1bc3f33bea..7eca34d1494 100644 --- a/config/react-compiler-bailout-baseline.txt +++ b/config/react-compiler-bailout-baseline.txt @@ -206,7 +206,7 @@ 1 src/renderer/src/components/mobile/WindowsFirewallNotice.tsx 2 src/renderer/src/components/mobile/use-mobile-page-paired-devices.ts 1 src/renderer/src/components/mobile/use-mobile-pairing-generation.ts -1 src/renderer/src/components/native-chat-restart-runtimes.ts +1 src/renderer/src/components/native-chat-recovery/offers/native-chat-restart-runtimes.ts 1 src/renderer/src/components/native-chat/NativeChatStructuredSession.tsx 8 src/renderer/src/components/native-chat/attachments/chips/use-native-chat-composer-attachments.ts 2 src/renderer/src/components/native-chat/attachments/intake/use-native-chat-resolved-path-attachments.ts diff --git a/config/react-compiler-hazard-baseline.txt b/config/react-compiler-hazard-baseline.txt index 27196222a7d..3f359d3ae77 100644 --- a/config/react-compiler-hazard-baseline.txt +++ b/config/react-compiler-hazard-baseline.txt @@ -1,5 +1,5 @@ # Grandfathered code React Compiler would run differently (#:). # Generated; never hand-edit. Lower after a fix: pnpm check:react-compiler-compat-ratchet --prune 1 src/renderer/src/components/agent/agent-launch-picker-drafts.ts#useAgentPickerSetup:discarded-subscription -1 src/renderer/src/components/native-chat-restart-runtimes.ts#useNativeChatRestartRuntimes:discarded-subscription +1 src/renderer/src/components/native-chat-recovery/offers/native-chat-restart-runtimes.ts#useNativeChatRestartRuntimes:discarded-subscription 1 src/renderer/src/components/right-sidebar/source-control/ai/use-source-control-action-agent-choice.ts#useSourceControlActionAgentChoice:store-read-in-memo-block diff --git a/config/reliability-gates.jsonc b/config/reliability-gates.jsonc index 66ecfbe88cb..0726542716e 100644 --- a/config/reliability-gates.jsonc +++ b/config/reliability-gates.jsonc @@ -2175,30 +2175,23 @@ "providers": ["local", "daemon", "wsl", "ssh", "remote-runtime"], "coveredPlatforms": ["linux"], "coveredProviders": [], - "coverageNotes": "Audit2694 source-only review: Remove14 authored/14 expanded cases in the renderer completion wrapper family, including the complete bell/query-reply/stale-input factory, without replacement. Retain29 authored/34 expanded cases with unchanged callback bodies. Three removals are the existing pending2203 overlap and must be adopted/count-reconciled once; eleven are fresh removals. No current-source runtime or gate execution is supplied by this packet; dated evidence retains its original source scope and does not validate the reduced factory bytes. No equivalent replacement, physical provider, mixed-version, mobile, rendered UI, cross-platform, soak or measured-performance result is claimed. The retained directory-level discovery command continues to discover the reduced family; historical wrapper mentions are not fresh import-closure or execution evidence. Historical prior account (preserved verbatim): Audit2133 current-parent preparation: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2065 (sources2063/2064): Intentionally retire these low-value alarms without replacement: src/renderer/src/components/terminal-pane/pty-connection/startup/pty-connection-startup-cwd-fallback-notice.spec.ts: prints a terminal notice when the startup cwd fell back to the workspace root. src/renderer/src/components/terminal-pane/pty-transport/ipc/pty-transport-pi-coalesce.test.ts: surfaces working frames when many spinner frames arrive in one chunk. src/renderer/src/components/terminal-pane/pty-transport/ipc/pty-transport-pi-spinner.test.ts: surfaces normalized working and idle labels across a spinner cycle; surfaces working even when a single chunk contains multiple spinner frames. src/renderer/src/components/terminal-pane/pty-connection/tests/pty-connection-spawn-and-startup.test.ts: ordinary registration cleanup removes only import { registerPtyConnectionStartupCwdFallbackNoticeTests } from '../startup/pty-connection-startup-cwd-fallback-notice.spec'; registerPtyConnectionStartupCwdFallbackNoticeTests(). Existing unrelated evidenceRuns retain their original scope; unchanged dated receipt objects do not validate changed source bytes or reduced selectors. No fresh gate execution or historical import-closure equivalence is claimed. Frozen current literal-import graph reaches these changed test bytes; unresolved local and computed edges remain debt, and current reachability is not historical execution or equivalent replacement evidence. Historical prior account (preserved verbatim): Audit2061 (sources2059/2060): Intentionally retire these low-value alarms without replacement: src/renderer/src/components/terminal-pane/pty-transport/ipc/pty-transport-agent-trust-hold.test.ts: spawns only after the answer, carrying \"let agents ask\" to main for this launch; reattaches a section terminal without asking or changing trust; spawns with today's default when the prompt is released without an answer. src/renderer/src/components/terminal-pane/pty-connection/host-ownership/pty-connection-ssh-hook-status-authority.spec.ts: leaves SSH IPC OSC 9999 status ownership in the main runtime. src/renderer/src/components/terminal-pane/pty-connection/tests/pty-connection-completion-and-notification.test.ts: ordinary registration cleanup removes only import { registerPtyConnectionSshHookStatusAuthorityTests } from '../host-ownership/pty-connection-ssh-hook-status-authority.spec'; registerPtyConnectionSshHookStatusAuthorityTests(). Existing unrelated evidenceRuns retain their original scope; unchanged dated receipt objects do not validate changed source bytes or reduced selectors. No fresh gate execution or historical import-closure equivalence is claimed. Historical prior account (preserved verbatim): Audit2056 (sources2054/2055): Intentionally retire these low-value alarms without replacement: src/renderer/src/components/terminal-pane/pty-transport/ipc/pty-transport-spawn-intent.test.ts: declares the intent only when the spawn resumes an agent; reports a deferral to the pane instead of an error toast. src/renderer/src/components/terminal-pane/pty-connection/startup/in-place-resume-hold.test.ts: owns the record while the start runs, through a nested start, and releases on settle; holds nothing for a start not resuming a record, or once the pane is disposed. Existing unrelated evidenceRuns retain their original scope; unchanged dated receipt objects do not validate changed source bytes or reduced selectors. No fresh gate execution or historical import-closure equivalence is claimed. Historical prior account (preserved verbatim): Audit1939 (sources1934/1935): The existing directory-level pnpm test selector includes eleven changed source files and automatically omits the three whole deletions. Keep this nonempty discovery command unchanged; no dated receipt uses this broad path. Removed agent admission fields, mock transport routing and terminal-id projection alarms are lost. Retained foreground state, actual headless xterm, redraw, abort/rebind reader and bridge claim progression remain scoped to their unchanged callbacks. No current-source execution, independent replacement, native transport or physical provider result is assigned. Preserve meaningful remaining selectors/assertions and unchanged dated receipts within their original scope. Reduced selectors have no fresh gate execution evidence in this metadata packet. No equivalent replacement, physical watcher/PTY/provider, live SSH/remote/mobile, rendered, cross-platform, soak or measured-budget result is claimed. Historical prior account (preserved verbatim): Retained exact unchanged-source receipts remain the only current execution evidence. Reduced selectors have no fresh execution result. Audit1417 intentionally reduces src/renderer/src/components/terminal-pane/pty-connection/main-pane-start-wait.test.ts: Loses joined waiter invocation/failure notification and raw terminal error/paired-host readable sentence alarms; survivors preserve hold detection and card clearing. deletes src/renderer/src/components/terminal-pane/pty-connection-hidden-tui-snapshot-replay.spec.ts: Loses rich hidden snapshot sequence replay/grid resize and forced repaint after hidden snapshot alarms, including live-trigger deduplication. deletes src/renderer/src/components/terminal-pane/pty-connection-pi-synchronized-input.test.ts: Loses typed-input forced synchronized-frame presentation alarm; setup implements the parser behavior it claims to exercise. deletes src/renderer/src/components/terminal-pane/pty-connection-unserved-ssh-pane.test.ts: Loses automatic SSH dial deduplication, passphrase gating, stale pane exclusion, and disposal cancellation alarms. deletes src/renderer/src/components/terminal-pane/pty-connection-remote-snapshot-source-grid.test.ts: Loses source grid before frame and destination grid restoration ordering alarm. deletes src/renderer/src/components/terminal-pane/pty-connection-restart-replacement.spec.ts: Loses wrapper-level hidden/visible restart resume, dispose/rebind suppression and finished-turn replay alarms; manual pipeline and mock-implemented binding invalidate the whole callbacks. No equivalent replacement coverage or fresh result is claimed. Historical coverage account: Audit1391 changes referenced sources. Retained unchanged-source receipts apply only to their unchanged commands; the reduced selectors have no new execution result. Historical coverage account (not a current claim for changed sources): Audit1361 changes referenced sources. Retained unchanged-source receipts apply only to their unchanged commands; the reduced selectors have no new execution result. Historical coverage account (not a current claim for changed sources): Audit1342 changes referenced sources. Retained unchanged-source receipts apply only to their unchanged commands; the reduced selectors have no new execution result. Historical coverage account (not a current claim for changed sources): macOS daemon-backed Electron journey verifies same PID and terminal identity plus rendered output. Focused renderer contracts pass on Linux, Windows and WSL. Neighboring SSH model and replay contracts pass locally; no new live SSH or paired-runtime journey. Audit1342: src/renderer/src/components/terminal-pane/pty-connection-unanswered-command-finish.spec.ts removed. Unanswered SSH shell finish keeps/drops mocked agent row matrix disappears. src/renderer/src/components/terminal-pane/pty-connection-split-cwd-resolution.test.ts reduced. Happy inherited-cwd wait and transport-option forwarding sample loses its individual alarm. src/renderer/src/components/terminal-pane/pty-connection-command-input-agent-identity.spec.ts removed. Typed Droid versus process/launch identity and SSH/runtime process-evidence matrices disappear. src/renderer/src/components/terminal-pane/pty-connection/reattach-success-settle.test.ts removed. Success/no-ID/expired/superseded attach-settlement bag alarms disappear. src/renderer/src/components/terminal-pane/pty-transport-user-input.test.ts reduced. Every-write kind forwarding and synchronous driving-attempt observer/rejection/ACK call counts disappear. src/renderer/src/components/terminal-pane/pty-connection-codex-restart-input-lookup.spec.ts removed. No global tab enumeration on ordinary/stale input and known restart notice input-block mock-call alarms. src/renderer/src/components/terminal-pane/pty-connection-layout-owner-wake.test.ts removed. Hidden-to-visible single-pane start request, attach-only argument and negative local wake-record alarms are intentionally dropped; no real server wake/ack occurs. src/renderer/src/components/terminal-pane/pty-connection-completion-and-notification.test.ts reduced. Driver registration/import of a whole-deleted selected spec is removed; directly authored cases remain unchanged. No extra authored removal credit. src/renderer/src/components/terminal-pane/pty-connection-command-finished-cleanup.spec.ts reduced. Driver registration/import of a whole-deleted selected spec is removed; directly authored cases remain unchanged. No extra authored removal credit. src/renderer/src/components/terminal-pane/pty-connection-terminal-input-gating.spec.ts reduced. Driver registration/import of a whole-deleted selected spec is removed; directly authored cases remain unchanged. No extra authored removal credit. No equivalent replacement coverage is claimed. Audit1361: src/renderer/src/components/terminal-pane/pty-connection/foreground-output-scan.test.ts reduced. Internal searched-character budget and ordinary/malformed no-op controls. No equivalent replacement coverage is claimed. Audit1365: focused Linux unit selection passed on source head 8880357cb420c45f1b808ae545a5bc609324624d. This new result has its own exact command; historical results keep their prior scope. Intentional test losses remain recorded in knownGaps. No physical-provider, native input, rendered E2E, p95, soak or replacement-coverage claim. Audit1369: src/renderer/src/components/terminal-pane/pty-connection/disposed-spawn-retention.test.ts removed. Rootless/removed/moved tabs, missing leaves, queued deletion and host-qualified/legacy deletion eligibility fixtures. No equivalent replacement coverage is claimed. Audit1391: src/renderer/src/components/terminal-pane/pty-connection-remote-pause-drop-notice.spec.ts reduced. Lose repeated quiet switches avoiding snapshots and legacy host without drop-report capability requesting a snapshot. Retained imported registrar uses fake transport/xterm writes, exercising real gap/restore lifecycle rather than actual terminal rendering. No equivalent replacement coverage is claimed. Audit1396: src/renderer/src/components/terminal-pane/pty-connection-pending-spawn.spec.ts reduced. Lose standalone debug-state retention/log silence alarm. React StrictMode is not mounted; bindings manually simulate remount over mocked transport. No equivalent replacement coverage is claimed. The existing primary-selector receipt and its claims remain scoped to their unchanged sources; the broad optional command has no new execution result. Audit1400: src/renderer/src/components/terminal-pane/pty-connection/terminal-input-echo-delay.test.ts reduced. Removes two fabricated-binding pipeline registrations; retains timestamp-policy outcomes without wall-clock speed assertions. No equivalent replacement coverage is claimed. The existing primary-selector receipt and its claims remain scoped to their unchanged sources; the broad optional command has no new execution result. Audit1402: src/renderer/src/components/terminal-pane/pty-connection-park-reveal-model-snapshot.spec.ts reduced. Loses basic paired serialize/initial replay forwarding check; hidden alt-frame grid/precedence policy remains. No real terminal renderer claim. src/renderer/src/components/terminal-pane/pty-connection/main-pane-resume-banner.test.ts reduced. Loses subscription-budget and attach-start generation predicate alarms; actual restored-banner renderer is not exercised. No equivalent replacement coverage is claimed. The existing primary-selector receipt and its claims remain scoped to their unchanged sources; the broad optional command has no new execution result.", + "coverageNotes": "Audit3086: the transport filename selector loses IPC suspension/input/reattach cases and the entire spawn-errors suite. Unique classification, ownership, fallback retirement, reply-limit, write-failure and teardown-branch alarms are intentionally retired without replacement. Remaining registrations are unchanged; no fresh execution or real provider result is claimed. Audit2694 source-only review: Remove14 authored/14 expanded cases in the renderer completion wrapper family, including the complete bell/query-reply/stale-input factory, without replacement. Retain29 authored/34 expanded cases with unchanged callback bodies. Three removals are the existing pending2203 overlap and must be adopted/count-reconciled once; eleven are fresh removals. No current-source runtime or gate execution is supplied by this packet; dated evidence retains its original source scope and does not validate the reduced factory bytes. No equivalent replacement, physical provider, mixed-version, mobile, rendered UI, cross-platform, soak or measured-performance result is claimed. The retained directory-level discovery command continues to discover the reduced family; historical wrapper mentions are not fresh import-closure or execution evidence. Historical prior account (preserved verbatim): Audit2133 current-parent preparation: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2065 (sources2063/2064): Intentionally retire these low-value alarms without replacement: src/renderer/src/components/terminal-pane/pty-connection/startup/pty-connection-startup-cwd-fallback-notice.spec.ts: prints a terminal notice when the startup cwd fell back to the workspace root. src/renderer/src/components/terminal-pane/pty-transport/ipc/pty-transport-pi-coalesce.test.ts: surfaces working frames when many spinner frames arrive in one chunk. src/renderer/src/components/terminal-pane/pty-transport/ipc/pty-transport-pi-spinner.test.ts: surfaces normalized working and idle labels across a spinner cycle; surfaces working even when a single chunk contains multiple spinner frames. src/renderer/src/components/terminal-pane/pty-connection/tests/pty-connection-spawn-and-startup.test.ts: ordinary registration cleanup removes only import { registerPtyConnectionStartupCwdFallbackNoticeTests } from '../startup/pty-connection-startup-cwd-fallback-notice.spec'; registerPtyConnectionStartupCwdFallbackNoticeTests(). Existing unrelated evidenceRuns retain their original scope; unchanged dated receipt objects do not validate changed source bytes or reduced selectors. No fresh gate execution or historical import-closure equivalence is claimed. Frozen current literal-import graph reaches these changed test bytes; unresolved local and computed edges remain debt, and current reachability is not historical execution or equivalent replacement evidence. Historical prior account (preserved verbatim): Audit2061 (sources2059/2060): Intentionally retire these low-value alarms without replacement: src/renderer/src/components/terminal-pane/pty-transport/ipc/pty-transport-agent-trust-hold.test.ts: spawns only after the answer, carrying \"let agents ask\" to main for this launch; reattaches a section terminal without asking or changing trust; spawns with today's default when the prompt is released without an answer. src/renderer/src/components/terminal-pane/pty-connection/host-ownership/pty-connection-ssh-hook-status-authority.spec.ts: leaves SSH IPC OSC 9999 status ownership in the main runtime. src/renderer/src/components/terminal-pane/pty-connection/tests/pty-connection-completion-and-notification.test.ts: ordinary registration cleanup removes only import { registerPtyConnectionSshHookStatusAuthorityTests } from '../host-ownership/pty-connection-ssh-hook-status-authority.spec'; registerPtyConnectionSshHookStatusAuthorityTests(). Existing unrelated evidenceRuns retain their original scope; unchanged dated receipt objects do not validate changed source bytes or reduced selectors. No fresh gate execution or historical import-closure equivalence is claimed. Historical prior account (preserved verbatim): Audit2056 (sources2054/2055): Intentionally retire these low-value alarms without replacement: src/renderer/src/components/terminal-pane/pty-transport/ipc/pty-transport-spawn-intent.test.ts: declares the intent only when the spawn resumes an agent; reports a deferral to the pane instead of an error toast. src/renderer/src/components/terminal-pane/pty-connection/startup/in-place-resume-hold.test.ts: owns the record while the start runs, through a nested start, and releases on settle; holds nothing for a start not resuming a record, or once the pane is disposed. Existing unrelated evidenceRuns retain their original scope; unchanged dated receipt objects do not validate changed source bytes or reduced selectors. No fresh gate execution or historical import-closure equivalence is claimed. Historical prior account (preserved verbatim): Audit1939 (sources1934/1935): The existing directory-level pnpm test selector includes eleven changed source files and automatically omits the three whole deletions. Keep this nonempty discovery command unchanged; no dated receipt uses this broad path. Removed agent admission fields, mock transport routing and terminal-id projection alarms are lost. Retained foreground state, actual headless xterm, redraw, abort/rebind reader and bridge claim progression remain scoped to their unchanged callbacks. No current-source execution, independent replacement, native transport or physical provider result is assigned. Preserve meaningful remaining selectors/assertions and unchanged dated receipts within their original scope. Reduced selectors have no fresh gate execution evidence in this metadata packet. No equivalent replacement, physical watcher/PTY/provider, live SSH/remote/mobile, rendered, cross-platform, soak or measured-budget result is claimed. Historical prior account (preserved verbatim): Retained exact unchanged-source receipts remain the only current execution evidence. Reduced selectors have no fresh execution result. Audit1417 intentionally reduces src/renderer/src/components/terminal-pane/pty-connection/main-pane-start-wait.test.ts: Loses joined waiter invocation/failure notification and raw terminal error/paired-host readable sentence alarms; survivors preserve hold detection and card clearing. deletes src/renderer/src/components/terminal-pane/pty-connection-hidden-tui-snapshot-replay.spec.ts: Loses rich hidden snapshot sequence replay/grid resize and forced repaint after hidden snapshot alarms, including live-trigger deduplication. deletes src/renderer/src/components/terminal-pane/pty-connection-pi-synchronized-input.test.ts: Loses typed-input forced synchronized-frame presentation alarm; setup implements the parser behavior it claims to exercise. deletes src/renderer/src/components/terminal-pane/pty-connection-unserved-ssh-pane.test.ts: Loses automatic SSH dial deduplication, passphrase gating, stale pane exclusion, and disposal cancellation alarms. deletes src/renderer/src/components/terminal-pane/pty-connection-remote-snapshot-source-grid.test.ts: Loses source grid before frame and destination grid restoration ordering alarm. deletes src/renderer/src/components/terminal-pane/pty-connection-restart-replacement.spec.ts: Loses wrapper-level hidden/visible restart resume, dispose/rebind suppression and finished-turn replay alarms; manual pipeline and mock-implemented binding invalidate the whole callbacks. No equivalent replacement coverage or fresh result is claimed. Historical coverage account: Audit1391 changes referenced sources. Retained unchanged-source receipts apply only to their unchanged commands; the reduced selectors have no new execution result. Historical coverage account (not a current claim for changed sources): Audit1361 changes referenced sources. Retained unchanged-source receipts apply only to their unchanged commands; the reduced selectors have no new execution result. Historical coverage account (not a current claim for changed sources): Audit1342 changes referenced sources. Retained unchanged-source receipts apply only to their unchanged commands; the reduced selectors have no new execution result. Historical coverage account (not a current claim for changed sources): macOS daemon-backed Electron journey verifies same PID and terminal identity plus rendered output. Focused renderer contracts pass on Linux, Windows and WSL. Neighboring SSH model and replay contracts pass locally; no new live SSH or paired-runtime journey. Audit1342: src/renderer/src/components/terminal-pane/pty-connection-unanswered-command-finish.spec.ts removed. Unanswered SSH shell finish keeps/drops mocked agent row matrix disappears. src/renderer/src/components/terminal-pane/pty-connection-split-cwd-resolution.test.ts reduced. Happy inherited-cwd wait and transport-option forwarding sample loses its individual alarm. src/renderer/src/components/terminal-pane/pty-connection-command-input-agent-identity.spec.ts removed. Typed Droid versus process/launch identity and SSH/runtime process-evidence matrices disappear. src/renderer/src/components/terminal-pane/pty-connection/reattach-success-settle.test.ts removed. Success/no-ID/expired/superseded attach-settlement bag alarms disappear. src/renderer/src/components/terminal-pane/pty-transport-user-input.test.ts reduced. Every-write kind forwarding and synchronous driving-attempt observer/rejection/ACK call counts disappear. src/renderer/src/components/terminal-pane/pty-connection-codex-restart-input-lookup.spec.ts removed. No global tab enumeration on ordinary/stale input and known restart notice input-block mock-call alarms. src/renderer/src/components/terminal-pane/pty-connection-layout-owner-wake.test.ts removed. Hidden-to-visible single-pane start request, attach-only argument and negative local wake-record alarms are intentionally dropped; no real server wake/ack occurs. src/renderer/src/components/terminal-pane/pty-connection-completion-and-notification.test.ts reduced. Driver registration/import of a whole-deleted selected spec is removed; directly authored cases remain unchanged. No extra authored removal credit. src/renderer/src/components/terminal-pane/pty-connection-command-finished-cleanup.spec.ts reduced. Driver registration/import of a whole-deleted selected spec is removed; directly authored cases remain unchanged. No extra authored removal credit. src/renderer/src/components/terminal-pane/pty-connection-terminal-input-gating.spec.ts reduced. Driver registration/import of a whole-deleted selected spec is removed; directly authored cases remain unchanged. No extra authored removal credit. No equivalent replacement coverage is claimed. Audit1361: src/renderer/src/components/terminal-pane/pty-connection/foreground-output-scan.test.ts reduced. Internal searched-character budget and ordinary/malformed no-op controls. No equivalent replacement coverage is claimed. Audit1365: focused Linux unit selection passed on source head 8880357cb420c45f1b808ae545a5bc609324624d. This new result has its own exact command; historical results keep their prior scope. Intentional test losses remain recorded in knownGaps. No physical-provider, native input, rendered E2E, p95, soak or replacement-coverage claim. Audit1369: src/renderer/src/components/terminal-pane/pty-connection/disposed-spawn-retention.test.ts removed. Rootless/removed/moved tabs, missing leaves, queued deletion and host-qualified/legacy deletion eligibility fixtures. No equivalent replacement coverage is claimed. Audit1391: src/renderer/src/components/terminal-pane/pty-connection-remote-pause-drop-notice.spec.ts reduced. Lose repeated quiet switches avoiding snapshots and legacy host without drop-report capability requesting a snapshot. Retained imported registrar uses fake transport/xterm writes, exercising real gap/restore lifecycle rather than actual terminal rendering. No equivalent replacement coverage is claimed. Audit1396: src/renderer/src/components/terminal-pane/pty-connection-pending-spawn.spec.ts reduced. Lose standalone debug-state retention/log silence alarm. React StrictMode is not mounted; bindings manually simulate remount over mocked transport. No equivalent replacement coverage is claimed. The existing primary-selector receipt and its claims remain scoped to their unchanged sources; the broad optional command has no new execution result. Audit1400: src/renderer/src/components/terminal-pane/pty-connection/terminal-input-echo-delay.test.ts reduced. Removes two fabricated-binding pipeline registrations; retains timestamp-policy outcomes without wall-clock speed assertions. No equivalent replacement coverage is claimed. The existing primary-selector receipt and its claims remain scoped to their unchanged sources; the broad optional command has no new execution result. Audit1402: src/renderer/src/components/terminal-pane/pty-connection-park-reveal-model-snapshot.spec.ts reduced. Loses basic paired serialize/initial replay forwarding check; hidden alt-frame grid/precedence policy remains. No real terminal renderer claim. src/renderer/src/components/terminal-pane/pty-connection/main-pane-resume-banner.test.ts reduced. Loses subscription-budget and attach-start generation predicate alarms; actual restored-banner renderer is not exercised. No equivalent replacement coverage is claimed. The existing primary-selector receipt and its claims remain scoped to their unchanged sources; the broad optional command has no new execution result.", "motivatingLinks": [ "https://github.com/user-attachments/assets/e8c6d1dc-6150-4c3d-b55a-3d12efefdd04", "https://github.com/user-attachments/assets/b0328f88-34ac-4d51-8119-9efe17072435" ], "invariant": "Adopting a prestarted terminal preserves its existing process and paints snapshot-covered startup output once while retaining subsequent live output. Missing sequence proof or blank snapshots must not authorize dropping output.", - "oracle": "Audit2694 source-only review: Remove14 authored/14 expanded cases in the renderer completion wrapper family, including the complete bell/query-reply/stale-input factory, without replacement. Retain29 authored/34 expanded cases with unchanged callback bodies. Three removals are the existing pending2203 overlap and must be adopted/count-reconciled once; eleven are fresh removals. No current-source runtime or gate execution is supplied by this packet; dated evidence retains its original source scope and does not validate the reduced factory bytes. No equivalent replacement, physical provider, mixed-version, mobile, rendered UI, cross-platform, soak or measured-performance result is claimed. The retained directory-level discovery command continues to discover the reduced family; historical wrapper mentions are not fresh import-closure or execution evidence. Historical prior account (preserved verbatim): Audit2133 current-parent preparation: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2065 (sources2063/2064): Intentionally retire these low-value alarms without replacement: src/renderer/src/components/terminal-pane/pty-connection/startup/pty-connection-startup-cwd-fallback-notice.spec.ts: prints a terminal notice when the startup cwd fell back to the workspace root. src/renderer/src/components/terminal-pane/pty-transport/ipc/pty-transport-pi-coalesce.test.ts: surfaces working frames when many spinner frames arrive in one chunk. src/renderer/src/components/terminal-pane/pty-transport/ipc/pty-transport-pi-spinner.test.ts: surfaces normalized working and idle labels across a spinner cycle; surfaces working even when a single chunk contains multiple spinner frames. src/renderer/src/components/terminal-pane/pty-connection/tests/pty-connection-spawn-and-startup.test.ts: ordinary registration cleanup removes only import { registerPtyConnectionStartupCwdFallbackNoticeTests } from '../startup/pty-connection-startup-cwd-fallback-notice.spec'; registerPtyConnectionStartupCwdFallbackNoticeTests(). Existing unrelated evidenceRuns retain their original scope; unchanged dated receipt objects do not validate changed source bytes or reduced selectors. No fresh gate execution or historical import-closure equivalence is claimed. Frozen current literal-import graph reaches these changed test bytes; unresolved local and computed edges remain debt, and current reachability is not historical execution or equivalent replacement evidence. Historical prior account (preserved verbatim): Audit2061 (sources2059/2060): Intentionally retire these low-value alarms without replacement: src/renderer/src/components/terminal-pane/pty-transport/ipc/pty-transport-agent-trust-hold.test.ts: spawns only after the answer, carrying \"let agents ask\" to main for this launch; reattaches a section terminal without asking or changing trust; spawns with today's default when the prompt is released without an answer. src/renderer/src/components/terminal-pane/pty-connection/host-ownership/pty-connection-ssh-hook-status-authority.spec.ts: leaves SSH IPC OSC 9999 status ownership in the main runtime. src/renderer/src/components/terminal-pane/pty-connection/tests/pty-connection-completion-and-notification.test.ts: ordinary registration cleanup removes only import { registerPtyConnectionSshHookStatusAuthorityTests } from '../host-ownership/pty-connection-ssh-hook-status-authority.spec'; registerPtyConnectionSshHookStatusAuthorityTests(). Existing unrelated evidenceRuns retain their original scope; unchanged dated receipt objects do not validate changed source bytes or reduced selectors. No fresh gate execution or historical import-closure equivalence is claimed. Historical prior account (preserved verbatim): Audit2056 (sources2054/2055): Intentionally retire these low-value alarms without replacement: src/renderer/src/components/terminal-pane/pty-transport/ipc/pty-transport-spawn-intent.test.ts: declares the intent only when the spawn resumes an agent; reports a deferral to the pane instead of an error toast. src/renderer/src/components/terminal-pane/pty-connection/startup/in-place-resume-hold.test.ts: owns the record while the start runs, through a nested start, and releases on settle; holds nothing for a start not resuming a record, or once the pane is disposed. Existing unrelated evidenceRuns retain their original scope; unchanged dated receipt objects do not validate changed source bytes or reduced selectors. No fresh gate execution or historical import-closure equivalence is claimed. Historical prior account (preserved verbatim): Audit1939 (sources1934/1935): The existing directory-level pnpm test selector includes eleven changed source files and automatically omits the three whole deletions. Keep this nonempty discovery command unchanged; no dated receipt uses this broad path. Removed agent admission fields, mock transport routing and terminal-id projection alarms are lost. Retained foreground state, actual headless xterm, redraw, abort/rebind reader and bridge claim progression remain scoped to their unchanged callbacks. No current-source execution, independent replacement, native transport or physical provider result is assigned. Preserve meaningful remaining selectors/assertions and unchanged dated receipts within their original scope. Reduced selectors have no fresh gate execution evidence in this metadata packet. No equivalent replacement, physical watcher/PTY/provider, live SSH/remote/mobile, rendered, cross-platform, soak or measured-budget result is claimed. Historical prior account (preserved verbatim): Retained exact unchanged-source receipts remain the only current execution evidence. Reduced selectors have no fresh execution result. Historical pre-audit oracle account for changed sources: The retained unchanged-source receipts are the only current execution evidence; reduced-source assertions await validation. Historical oracle account (not a current claim for changed sources): The retained unchanged-source receipts are the only current execution evidence; reduced-source assertions await validation. Historical oracle account (not a current claim for changed sources): The retained unchanged-source receipts are the only current execution evidence; reduced-source assertions await validation. Historical oracle account (not a current claim for changed sources): Pass snapshot sequence and proven zero keyboard flags through real IPC transport projection. Deliver snapshot-covered and newer output before reattach resolves; drain replay parse callbacks and require one startup marker and the newer output. Repeat with no sequence and blank snapshot to retain unproven bytes. In Electron select a prestarted workspace, type a generated marker and compare PID and stable terminal identities before and after.", + "oracle": "Audit3086: the transport filename selector loses IPC suspension/input/reattach cases and the entire spawn-errors suite. Unique classification, ownership, fallback retirement, reply-limit, write-failure and teardown-branch alarms are intentionally retired without replacement. Remaining registrations are unchanged; no fresh execution or real provider result is claimed. Audit2694 source-only review: Remove14 authored/14 expanded cases in the renderer completion wrapper family, including the complete bell/query-reply/stale-input factory, without replacement. Retain29 authored/34 expanded cases with unchanged callback bodies. Three removals are the existing pending2203 overlap and must be adopted/count-reconciled once; eleven are fresh removals. No current-source runtime or gate execution is supplied by this packet; dated evidence retains its original source scope and does not validate the reduced factory bytes. No equivalent replacement, physical provider, mixed-version, mobile, rendered UI, cross-platform, soak or measured-performance result is claimed. The retained directory-level discovery command continues to discover the reduced family; historical wrapper mentions are not fresh import-closure or execution evidence. Historical prior account (preserved verbatim): Audit2133 current-parent preparation: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2065 (sources2063/2064): Intentionally retire these low-value alarms without replacement: src/renderer/src/components/terminal-pane/pty-connection/startup/pty-connection-startup-cwd-fallback-notice.spec.ts: prints a terminal notice when the startup cwd fell back to the workspace root. src/renderer/src/components/terminal-pane/pty-transport/ipc/pty-transport-pi-coalesce.test.ts: surfaces working frames when many spinner frames arrive in one chunk. src/renderer/src/components/terminal-pane/pty-transport/ipc/pty-transport-pi-spinner.test.ts: surfaces normalized working and idle labels across a spinner cycle; surfaces working even when a single chunk contains multiple spinner frames. src/renderer/src/components/terminal-pane/pty-connection/tests/pty-connection-spawn-and-startup.test.ts: ordinary registration cleanup removes only import { registerPtyConnectionStartupCwdFallbackNoticeTests } from '../startup/pty-connection-startup-cwd-fallback-notice.spec'; registerPtyConnectionStartupCwdFallbackNoticeTests(). Existing unrelated evidenceRuns retain their original scope; unchanged dated receipt objects do not validate changed source bytes or reduced selectors. No fresh gate execution or historical import-closure equivalence is claimed. Frozen current literal-import graph reaches these changed test bytes; unresolved local and computed edges remain debt, and current reachability is not historical execution or equivalent replacement evidence. Historical prior account (preserved verbatim): Audit2061 (sources2059/2060): Intentionally retire these low-value alarms without replacement: src/renderer/src/components/terminal-pane/pty-transport/ipc/pty-transport-agent-trust-hold.test.ts: spawns only after the answer, carrying \"let agents ask\" to main for this launch; reattaches a section terminal without asking or changing trust; spawns with today's default when the prompt is released without an answer. src/renderer/src/components/terminal-pane/pty-connection/host-ownership/pty-connection-ssh-hook-status-authority.spec.ts: leaves SSH IPC OSC 9999 status ownership in the main runtime. src/renderer/src/components/terminal-pane/pty-connection/tests/pty-connection-completion-and-notification.test.ts: ordinary registration cleanup removes only import { registerPtyConnectionSshHookStatusAuthorityTests } from '../host-ownership/pty-connection-ssh-hook-status-authority.spec'; registerPtyConnectionSshHookStatusAuthorityTests(). Existing unrelated evidenceRuns retain their original scope; unchanged dated receipt objects do not validate changed source bytes or reduced selectors. No fresh gate execution or historical import-closure equivalence is claimed. Historical prior account (preserved verbatim): Audit2056 (sources2054/2055): Intentionally retire these low-value alarms without replacement: src/renderer/src/components/terminal-pane/pty-transport/ipc/pty-transport-spawn-intent.test.ts: declares the intent only when the spawn resumes an agent; reports a deferral to the pane instead of an error toast. src/renderer/src/components/terminal-pane/pty-connection/startup/in-place-resume-hold.test.ts: owns the record while the start runs, through a nested start, and releases on settle; holds nothing for a start not resuming a record, or once the pane is disposed. Existing unrelated evidenceRuns retain their original scope; unchanged dated receipt objects do not validate changed source bytes or reduced selectors. No fresh gate execution or historical import-closure equivalence is claimed. Historical prior account (preserved verbatim): Audit1939 (sources1934/1935): The existing directory-level pnpm test selector includes eleven changed source files and automatically omits the three whole deletions. Keep this nonempty discovery command unchanged; no dated receipt uses this broad path. Removed agent admission fields, mock transport routing and terminal-id projection alarms are lost. Retained foreground state, actual headless xterm, redraw, abort/rebind reader and bridge claim progression remain scoped to their unchanged callbacks. No current-source execution, independent replacement, native transport or physical provider result is assigned. Preserve meaningful remaining selectors/assertions and unchanged dated receipts within their original scope. Reduced selectors have no fresh gate execution evidence in this metadata packet. No equivalent replacement, physical watcher/PTY/provider, live SSH/remote/mobile, rendered, cross-platform, soak or measured-budget result is claimed. Historical prior account (preserved verbatim): Retained exact unchanged-source receipts remain the only current execution evidence. Reduced selectors have no fresh execution result. Historical pre-audit oracle account for changed sources: The retained unchanged-source receipts are the only current execution evidence; reduced-source assertions await validation. Historical oracle account (not a current claim for changed sources): The retained unchanged-source receipts are the only current execution evidence; reduced-source assertions await validation. Historical oracle account (not a current claim for changed sources): The retained unchanged-source receipts are the only current execution evidence; reduced-source assertions await validation. Historical oracle account (not a current claim for changed sources): Pass snapshot sequence and proven zero keyboard flags through real IPC transport projection. Deliver snapshot-covered and newer output before reattach resolves; drain replay parse callbacks and require one startup marker and the newer output. Repeat with no sequence and blank snapshot to retain unproven bytes. In Electron select a prestarted workspace, type a generated marker and compare PID and stable terminal identities before and after.", "commands": [ - "pnpm test src/renderer/src/components/terminal-pane/pty-transport/ipc/pty-transport-connect-spawn.test.ts src/renderer/src/components/terminal-pane/pty-connection/tests/pty-connection-hidden-replay.test.ts", - "pnpm test src/renderer/src/components/terminal-pane/pty-transport/ipc/pty-transport-connect-spawn.test.ts src/renderer/src/components/terminal-pane/pty-connection/tests/pty-connection-hidden-replay.test.ts src/renderer/src/components/terminal-pane/pty-connection/tests/pty-connection-visibility-and-exit.test.ts", + "pnpm test src/renderer/src/components/terminal-pane/pty-connection/tests/pty-connection-hidden-replay.test.ts", + "pnpm test src/renderer/src/components/terminal-pane/pty-connection/tests/pty-connection-hidden-replay.test.ts src/renderer/src/components/terminal-pane/pty-connection/tests/pty-connection-visibility-and-exit.test.ts", "pnpm test src/renderer/src/components/terminal-pane/pty-connection src/renderer/src/components/terminal-pane/pty-transport/ipc/pty-transport", "pnpm test src/shared/terminal/protocol/escape/terminal-synchronized-output-socket-splits.test.ts" ], "testFiles": [ - "src/renderer/src/components/terminal-pane/pty-transport/ipc/pty-transport-connect-spawn.test.ts", "src/renderer/src/components/terminal-pane/pty-connection/tests/pty-connection-hidden-replay.test.ts" ], "assertionRefs": [ - { - "file": "src/renderer/src/components/terminal-pane/pty-transport/ipc/pty-transport-connect-spawn.test.ts", - "assertions": [ - "zero and nonzero snapshot sequence and proven zero keyboard flags survive IPC projection" - ] - }, { "file": "src/renderer/src/components/terminal-pane/pty-connection/tests/pty-connection-hidden-replay.test.ts", "assertions": [ @@ -2242,6 +2235,7 @@ "Exercise live SSH and paired-runtime snapshot adoption before claiming full provider coverage." ], "knownGaps": [ + "Audit3077 retires the IPC zero/nonzero snapshot-sequence and zero-keyboard-proof examples and their direct selectors. The retained join projects only sequence 7 and flags 3. No equivalent replacement or fresh gate execution is claimed.", "Audit3069 retires nominal spawn/resume/banner wiring, first-fit and lookup-budget variants, private pending-map checks, the split-CWD disposal suite and startup command/draft baseline variants. Distinct regression alarms are intentionally lost without equivalent replacement; retained ownership, cancellation and recovery cases remain. Existing selectors and dated evidence retain their original scope.", "Audit3061 retires the park-model supplied snapshot suite and nominal mode, cursor, focus, geometry and remote resume metadata variants. Their distinct alarms are intentionally lost; six retained mode/remote cases preserve selected tail-order, authority and parse-readiness boundaries without equivalent replacement. Existing selectors and dated evidence retain their original scope.", "Audit3053 reduces hidden-replay coverage by retiring nominal overlap, resize, cursor metadata and standalone stalled-restore variants. Timeout, overflow, identity/generation fences and retained live-output coordination remain; the removed alarms are intentionally lost without equivalent replacement. Existing commands, assertion references and dated evidence retain their original scope.", @@ -2266,7 +2260,8 @@ "Audit1417 intentionally reduces src/renderer/src/components/terminal-pane/pty-connection/main-pane-start-wait.test.ts: Loses joined waiter invocation/failure notification and raw terminal error/paired-host readable sentence alarms; survivors preserve hold detection and card clearing. deletes src/renderer/src/components/terminal-pane/pty-connection-hidden-tui-snapshot-replay.spec.ts: Loses rich hidden snapshot sequence replay/grid resize and forced repaint after hidden snapshot alarms, including live-trigger deduplication. deletes src/renderer/src/components/terminal-pane/pty-connection-pi-synchronized-input.test.ts: Loses typed-input forced synchronized-frame presentation alarm; setup implements the parser behavior it claims to exercise. deletes src/renderer/src/components/terminal-pane/pty-connection-unserved-ssh-pane.test.ts: Loses automatic SSH dial deduplication, passphrase gating, stale pane exclusion, and disposal cancellation alarms. deletes src/renderer/src/components/terminal-pane/pty-connection-remote-snapshot-source-grid.test.ts: Loses source grid before frame and destination grid restoration ordering alarm. deletes src/renderer/src/components/terminal-pane/pty-connection-restart-replacement.spec.ts: Loses wrapper-level hidden/visible restart resume, dispose/rebind suppression and finished-turn replay alarms; manual pipeline and mock-implemented binding invalidate the whole callbacks. No equivalent replacement coverage or fresh result is claimed.", "Audit1939 (sources1934/1935): The existing directory-level pnpm test selector includes eleven changed source files and automatically omits the three whole deletions. Keep this nonempty discovery command unchanged; no dated receipt uses this broad path. Removed agent admission fields, mock transport routing and terminal-id projection alarms are lost. Retained foreground state, actual headless xterm, redraw, abort/rebind reader and bridge claim progression remain scoped to their unchanged callbacks. No current-source execution, independent replacement, native transport or physical provider result is assigned. Preserve meaningful remaining selectors/assertions and unchanged dated receipts within their original scope. Reduced selectors have no fresh gate execution evidence in this metadata packet. No equivalent replacement, physical watcher/PTY/provider, live SSH/remote/mobile, rendered, cross-platform, soak or measured-budget result is claimed. Exact retired authored alarms: 'bounds a startup title storm to one extra read ladder and never adopts the title as identity'; 'leaves untrackable providers free of client process scans'; 'keeps a legacy remote process hint unverifiable after a startup title'; 'releases a retained capability when disposed mid-read'; 'releases a retained capability when an exit schedules no successor read'; 'releases a retained capability when an untrackable $exit ends the read'; 'does not release when the cancelled read has a successor'; 'settles the visible confirmation when the pty id is gone before the read runs'; 'reports a read in flight when a command-finished read owns the pane'; 'reads the foreground once after a command starts and publishes the recognized agent'; 'does not replace known foreground identity with typed command inference'; 'revokes stale routing immediately while a known pane confirms a new command'; 'reads the foreground for a visible PTY so restored running Codex panes regain identity'; 'does not retry or publish visible PTY reads for an idle shell foreground'; 'retries visible PTY reads only while a foreground wrapper may resolve to an agent'; 'stops after the ladder and publishes no identity for a persistent unknown process'; 'does not treat a nested shell seen mid-command as prompt proof'; 'recognizes an agent started from a nested shell on a ladder re-read'; 'marks shell foreground on command finished without any foreground read'; 'trusts a rapid ordinary command finish without forcing a fresh scan'; 'still marks shell without a read for a duplicate D pair on an idle pane'; 'marks shell on a rapid command start->finish when the foreground is a shell'; 'marks shell foreground when the confirming read shows the agent is gone'; 'confirms a command finished for a launch-known agent pane before any read'; 'accepts a fresh shell result for a launch-known Droid pane'; 'marks a confirmed shell without repeating a post-boundary scan'; 'never reads or publishes for remote or ssh panes'; 'does not let forged OSC 133/777 output assert remote identity'; 'keeps command-only pane inference bounded for large pasted scripts'; 'skips jcode colors for command-only startup %s'; 'keeps explicit pane ownership ahead of a delayed command hint'; 'does not infer a TUI from %s'; 'survives a launch config registered before its identity lands'; 'prefers the tab launch agent over every later signal'; 'falls back through startup, then initial status, then the registered config'; 'ignores a registered agent name that is not a known TUI agent'; 'skips the OSC color answer for jcode and no one else'; 'drops a Done that landed before the command-finished fact'; 'keeps a program Done reported before the command-finished fact'; 'dismisses the kept result when the next command starts'; 'keeps a facet-less OSC Done from a non-agent program'; 'still drops a real agent Done'; 'still drops an interrupted program Done'; 'retires a hook-identified row the foreground tracker never read'; 'leaves a pane with an armed command-finished drop to its own confirming read'; 'leaves a remote pane to its execution host'; 'keeps the row when the host can verify the owner process itself'; 'keeps a row that belongs to a different agent'; 'does not watch the store for an owner it can never name'; 'control: a fresh host image repaints the pane without resizing the program'; 'asks the program to repaint when the image is %s'; 'asks the program to repaint when the host never answers the snapshot request'; 'repaints a full-screen TUI on the alternate screen the same way'; 'leaves the size alone while a phone or another desktop holds it'; 'grows a pane at the host minimum height instead of asking for the same clamped size'; 'does not nudge while the stream is still reconnecting'; 'mounts and remounts the tab owner while an unrelated workspace is active (%s)'; 'continues a recorded live folder owner despite ambiguous inventory (%s)'; 'resolves an encoded split surface using its parent tab and active leaf'; 'does not capture a remote revision for a local terminal'; 'omits incarnation and length for older runtime responses'; 'refuses %s owner before RPC'; 'refuses a resolved terminal belonging to another native PTY'; 'refuses a foreign remote terminal handle despite a matching native PTY ID'; 'refuses a remote handle owned by another environment'; 'refuses a remote terminal with no live native PTY'; 'does not fall back locally when remote lacks RPC'; 'rejects malformed or oversized encoded response %#'; 'maps a unified terminal tab id to its legacy owner id'; 'keeps non-terminal and unknown ids unchanged'; 'keeps an exact terminal owner ahead of a stale unified alias'; 'stays inert on a host that was not kept for the bridge'; 'stays inert for a pane main does not list, so it still dials the host'.", "Audit2056 (sources2054/2055): Intentionally retire these low-value alarms without replacement: src/renderer/src/components/terminal-pane/pty-transport/ipc/pty-transport-spawn-intent.test.ts: declares the intent only when the spawn resumes an agent; reports a deferral to the pane instead of an error toast. src/renderer/src/components/terminal-pane/pty-connection/startup/in-place-resume-hold.test.ts: owns the record while the start runs, through a nested start, and releases on settle; holds nothing for a start not resuming a record, or once the pane is disposed. Existing unrelated evidenceRuns retain their original scope; unchanged dated receipt objects do not validate changed source bytes or reduced selectors. No fresh gate execution or historical import-closure equivalence is claimed.", - "Audit2061 (sources2059/2060): Intentionally retire these low-value alarms without replacement: src/renderer/src/components/terminal-pane/pty-transport/ipc/pty-transport-agent-trust-hold.test.ts: spawns only after the answer, carrying \"let agents ask\" to main for this launch; reattaches a section terminal without asking or changing trust; spawns with today's default when the prompt is released without an answer. src/renderer/src/components/terminal-pane/pty-connection/host-ownership/pty-connection-ssh-hook-status-authority.spec.ts: leaves SSH IPC OSC 9999 status ownership in the main runtime. src/renderer/src/components/terminal-pane/pty-connection/tests/pty-connection-completion-and-notification.test.ts: ordinary registration cleanup removes only import { registerPtyConnectionSshHookStatusAuthorityTests } from '../host-ownership/pty-connection-ssh-hook-status-authority.spec'; registerPtyConnectionSshHookStatusAuthorityTests(). Existing unrelated evidenceRuns retain their original scope; unchanged dated receipt objects do not validate changed source bytes or reduced selectors. No fresh gate execution or historical import-closure equivalence is claimed." + "Audit2061 (sources2059/2060): Intentionally retire these low-value alarms without replacement: src/renderer/src/components/terminal-pane/pty-transport/ipc/pty-transport-agent-trust-hold.test.ts: spawns only after the answer, carrying \"let agents ask\" to main for this launch; reattaches a section terminal without asking or changing trust; spawns with today's default when the prompt is released without an answer. src/renderer/src/components/terminal-pane/pty-connection/host-ownership/pty-connection-ssh-hook-status-authority.spec.ts: leaves SSH IPC OSC 9999 status ownership in the main runtime. src/renderer/src/components/terminal-pane/pty-connection/tests/pty-connection-completion-and-notification.test.ts: ordinary registration cleanup removes only import { registerPtyConnectionSshHookStatusAuthorityTests } from '../host-ownership/pty-connection-ssh-hook-status-authority.spec'; registerPtyConnectionSshHookStatusAuthorityTests(). Existing unrelated evidenceRuns retain their original scope; unchanged dated receipt objects do not validate changed source bytes or reduced selectors. No fresh gate execution or historical import-closure equivalence is claimed.", + "Audit3086: the transport filename selector loses IPC suspension/input/reattach cases and the entire spawn-errors suite. Unique classification, ownership, fallback retirement, reply-limit, write-failure and teardown-branch alarms are intentionally retired without replacement. Remaining registrations are unchanged; no fresh execution or real provider result is claimed." ], "demotionRule": "Keep experimental or demote if adoption duplicates covered output, drops newer or unproven output, changes terminal ownership, or flakes without explanation." }, @@ -4235,10 +4230,10 @@ "providers": ["remote-runtime"], "coveredPlatforms": ["macos"], "coveredProviders": ["remote-runtime"], - "coverageNotes": "Audit2432: The broad unit command loses the isolated BranchPicker open-change notification sample. Its unique true/false callback alarm is intentionally retired; reopening/status-refresh behavior was not exercised. No equivalent replacement or fresh validation of the reduced selector is claimed. Audit2425: The broad unit command loses the workspace-copy-status supplied readError forwarding sample. It did not exercise the branch-switch blocking promised by its title. This alarm is intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2417: The broad unit command loses two ordinary clean Git cherry-pick/revert success samples and two terminal startup foreground-read work-budget samples. These unique alarms are intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2412: The broad unit command loses the whole six-case fake-React-hook scroll-anchor suite, two generic draft-retention samples, a simple new-agent order sample, a real history-publication batching-count sample, an unmounted handoff abort/timer sample and three basic WSL preview/error samples. These alarms are intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2404: The broad unit command loses the standalone share-name lookup 503 propagation suite, initial deletion placement-kind sample, direct changed-mtime stale sample, first-pass rollback existence example and abandoned-preparation directory sample. These exact alarms are intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2397 review amendment: The broad unit command loses three standalone suites, two partial-case definitions and two URL table rows across package/download/placement tests. Keep initial wrong-port authorization, modified independent-copy digest refusal and abort-after-begin cancellation dispatch. The remaining alarms are deliberately retired; no equivalent complete replacement or fresh validation of the reduced selector is claimed. Audit2393 current-parent refresh2401: Remove the basic skill-install service alias sample and original public managed-remove receiptless refusal case. Preserve the upstream request ownership suite and transaction interruption coverage. No equivalent complete replacement or fresh validation of the reduced selector is claimed. Audit2389: The broad unit command loses the missing-preview suite, plain-global destination sample and prepublished-receipt recovery example. No equivalent replacement or fresh validation of the reduced selector is claimed. Audit2385: The broad unit command loses four authored walker/staging/protocol examples (six expanded cases); no equivalent replacement or fresh validation is claimed for the reduced suite. Audit2383: The unscoped Vitest command no longer includes the removed Antigravity catalogue suite; its exact provider-root/owner example has no equivalent replacement or fresh validation claim. Audit2377/2381: The unscoped Vitest command includes the reduced bundled-skill and native-discovery suites; the eight removed backup/legacy/catalogue/symlink/SSH-builder examples have no replacement-equivalence or fresh validation claim here. Audit2133 current-parent preparation: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2095 current-parent preparation2126: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit1640 removes low-value IPC, SSH, workspace and transcript-watcher tests without equivalent replacement coverage. Per-source reviews record retained public refusal, cancellation, execution-host and real-filesystem outcomes with injected/native/remote limits. Affected historical receipts are retired; gates without active current evidence are downgraded. Historical scope before this audit: Deterministic main-IPC contract tests cover disconnect-driven close delivery, exactly-once close, per-subscription teardown isolation against a failing socket close and a throwing liveness probe, containment of a throwing renderer send on the unguarded host-close path, and continued suppression of stale payloads from a retired transport. A headed paired-server journey (real Orca host plus a separate paired Orca desktop client) covers hidden-but-mounted reveal, cold-parked reveal, and cold-parked reveal across a disconnect/reconnect. Live Linux and Windows paired-server evidence and real sleep/wake transport loss remain uncollected.", + "coverageNotes": "Audit3128 scoped coverage reduction: Audit3121 removes VoiceDictationLanguageSetting.test.tsx (Spanish-to-es callback and unsupported-model Korean option presence) and voice-speech-model-rows.test.ts (provider/language-order/retired-on-disk list policy); these real narrow alarms are deliberately retired without replacement. VoiceSpeechModelSection per-model concurrent deletion remains unchanged. Audit3122 removes the direct absolute/parent-segment sparse-directory exception-type sample and the five-row IPC sparse-preset error-code/non-save table (two authored/six expanded cases). Those real typed-validation and IPC refusal/error-code alarms are deliberately retired. Keep both real symlink/junction floating-directory trust cases and the minimal RPC structured validation-reason serialization case. The RPC case does not replace IPC save/refusal behavior, typed normalization for both path inputs, actual Git sparse checkout or rendered localization. No equivalent replacement or fresh test/runtime/platform/remote/performance validation is claimed. Audit3124 removes ExperimentalPane.test.tsx (ordinary Pet switch callback), WorkspaceDirectorySetting.test.tsx (padded absolute saved-path normalization/no-write blur), and repository-search.perf.test.ts (navigation/selected-pane builder-call policy, fixture cardinality/nonempty entries and renamed project keyword inclusion). These real narrow alarms are retired without replacement; RepositoryPane.search.test.tsx search-to-sparse-editor request remains unchanged. Audit3129: resolver Windows case-insensitive and POSIX case-sensitive configured-env composition plus mocked-base launch-preflight Windows composition are intentionally no longer checked by the two deleted suites. No equivalent coverage claimed. Keep runtime-structured-agent-host-env.test.ts unchanged: configured Windows Claude-home account identity and host-account precedence remain checked. Audit3114 incorporates source3101: retire three structured Claude child-environment wrapper cases covering inherited stamp isolation/default and explicit entrypoint, Windows SDK-key canonicalization/default/duplicate-key order, and configured lookup/final environment/config-dir conjunction. Keep direct auth/session sanitation and Windows-versus-Linux overlay checks, actual executable lookup from supplied inherited PATH, native Windows child environment delivery, and stale option-mutation fencing. These are not equivalent replacements for the retired wrapper alarms. No fresh test/runtime, real login-shell probe, Claude SDK startup, remote/mixed-version, UI or performance result is claimed. Audit3114 incorporates source3105: retire 17 authored/20 expanded daemon launch-note cases, including the entire two-case SessionLaunchNote suite. Deliberately lose split-marker carry across same-token re-arm, stale-token retirement refusal, foreground tri-state and serial settlement, omitted-note/no-incarnation mixed-version policy, sticky retirement, first-incarnation/replacement launch preservation and held-launch precedence. Keep only the unchanged unresolved foreground-read race against a later finished attach. Supplied notes/foreground and runtime calls do not establish actual daemon/main restart, SSH, native Windows observation, mobile UI, equivalent replacement or fresh runtime/performance validation. Audit3114 incorporates source3109: retire six authored/35 expanded pricing cases: 21 accepted model-name examples, 10 unsupported/future-model fail-closed examples, malformed TTL clamp, unknown-model estimate refusal, flat-model long-context disregard and oversized long-context clamp. Keep mixed five-minute/one-hour write allocation and short/all-long/mixed long-context allocation with original fixed oracles. Real parser/refusal/bounds alarms are knowingly lost without equivalent replacement; retained tests do not validate provider rates, actual billing or request threshold classification. Preserve the archived usage-scan-append source-freeze receipt without transferring historical execution/performance credit. No fresh runtime result is claimed. Audit3108 source-only reduction (3094/3095/3100): retire 27 authored/30 expanded cases, including complete markdown view-state, file-preview link-open and CLI agent-server-route suites, without replacement. Deliberate losses include preview visibility/reference lifetime and same-path owners; SSH stat, directory/missing-target, explicit-null owner, nonmarkdown line/column, local URI routing, remote-to-client guard and same-file reuse; document-preview source-owner/folder/reuse routing, stable grant location and outside-workspace POSIX/Windows request projection; and verified remote CLI send/check refusal, suggested --on argv/copy, shell no-probe bypass, unverified-launch admission and older-peer refusal. Retain two markdown source-runtime/qualified-reveal alarms, one stale grant-rejection fence and two remote read/mutation evidence policies, with original callbacks. Final last-wins reveal did not prove transient cancellation; CLI prerequisite did not prove no real send, and envelope tests do not execute server refusal. No fresh runtime, rendered UI, actual file/SSH/paired transport, authorization, mixed-version or performance result is claimed. Audit3096 source-only reduction (3087/3088): retire 69 authored/76 expanded cases, including complete recovery-portals and reconnect-banner suites. Intentionally lose dead/default TUI replay, generated alternate-frame style composition, reset/mode/dimension and legacy missing-mode-boundary alarms, UI notice/action/reveal policies, and isolated recovery lifecycle variants, without equivalent replacement. Retained registrations and callbacks are unchanged; headless cells are not pixels, and no fresh runtime, host, SSH, WSL, rendered-app or performance result is claimed. Audit3086 source-only reduction (3079/3080/3081): intentionally retire 93 authored/100 expanded mock-boundary cases, including the complete spawn-errors and SSH reconnect overlay suites, without replacement. Retired alarms include spawn classification/ownership, reply ceilings and renderer-write failure recovery, disconnect/detach fencing branches, authority facts, and overlay retry/visibility wiring. Retained callbacks are unchanged; no fresh execution, real PTY/SSH, rendered UI, or performance result is claimed. Audit2432: The broad unit command loses the isolated BranchPicker open-change notification sample. Its unique true/false callback alarm is intentionally retired; reopening/status-refresh behavior was not exercised. No equivalent replacement or fresh validation of the reduced selector is claimed. Audit2425: The broad unit command loses the workspace-copy-status supplied readError forwarding sample. It did not exercise the branch-switch blocking promised by its title. This alarm is intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2417: The broad unit command loses two ordinary clean Git cherry-pick/revert success samples and two terminal startup foreground-read work-budget samples. These unique alarms are intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2412: The broad unit command loses the whole six-case fake-React-hook scroll-anchor suite, two generic draft-retention samples, a simple new-agent order sample, a real history-publication batching-count sample, an unmounted handoff abort/timer sample and three basic WSL preview/error samples. These alarms are intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2404: The broad unit command loses the standalone share-name lookup 503 propagation suite, initial deletion placement-kind sample, direct changed-mtime stale sample, first-pass rollback existence example and abandoned-preparation directory sample. These exact alarms are intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2397 review amendment: The broad unit command loses three standalone suites, two partial-case definitions and two URL table rows across package/download/placement tests. Keep initial wrong-port authorization, modified independent-copy digest refusal and abort-after-begin cancellation dispatch. The remaining alarms are deliberately retired; no equivalent complete replacement or fresh validation of the reduced selector is claimed. Audit2393 current-parent refresh2401: Remove the basic skill-install service alias sample and original public managed-remove receiptless refusal case. Preserve the upstream request ownership suite and transaction interruption coverage. No equivalent complete replacement or fresh validation of the reduced selector is claimed. Audit2389: The broad unit command loses the missing-preview suite, plain-global destination sample and prepublished-receipt recovery example. No equivalent replacement or fresh validation of the reduced selector is claimed. Audit2385: The broad unit command loses four authored walker/staging/protocol examples (six expanded cases); no equivalent replacement or fresh validation is claimed for the reduced suite. Audit2383: The unscoped Vitest command no longer includes the removed Antigravity catalogue suite; its exact provider-root/owner example has no equivalent replacement or fresh validation claim. Audit2377/2381: The unscoped Vitest command includes the reduced bundled-skill and native-discovery suites; the eight removed backup/legacy/catalogue/symlink/SSH-builder examples have no replacement-equivalence or fresh validation claim here. Audit2133 current-parent preparation: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2095 current-parent preparation2126: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit1640 removes low-value IPC, SSH, workspace and transcript-watcher tests without equivalent replacement coverage. Per-source reviews record retained public refusal, cancellation, execution-host and real-filesystem outcomes with injected/native/remote limits. Affected historical receipts are retired; gates without active current evidence are downgraded. Historical scope before this audit: Deterministic main-IPC contract tests cover disconnect-driven close delivery, exactly-once close, per-subscription teardown isolation against a failing socket close and a throwing liveness probe, containment of a throwing renderer send on the unguarded host-close path, and continued suppression of stale payloads from a retired transport. A headed paired-server journey (real Orca host plus a separate paired Orca desktop client) covers hidden-but-mounted reveal, cold-parked reveal, and cold-parked reveal across a disconnect/reconnect. Live Linux and Windows paired-server evidence and real sleep/wake transport loss remain uncollected.", "motivatingLinks": ["tests/e2e/paired-remote-terminal-parked-reveal-interactivity.spec.ts"], "invariant": "Every renderer-held runtime subscription receives exactly one terminal close event when its transport is retired, including when the retirement advanced the transport generation first, and a single failing teardown never abandons that environment's remaining subscriptions nor escapes into the transport that reported the close. Payload frames from a retired transport stay suppressed. A revealed remote terminal therefore reattaches over a live multiplex connection: its buffer restores, typed input reaches the host PTY, the echo paints without a tab flip, and the PTY converges on the revealed pane grid.", - "oracle": "Audit2432: The broad unit command loses the isolated BranchPicker open-change notification sample. Its unique true/false callback alarm is intentionally retired; reopening/status-refresh behavior was not exercised. No equivalent replacement or fresh validation of the reduced selector is claimed. Audit2425: The broad unit command loses the workspace-copy-status supplied readError forwarding sample. It did not exercise the branch-switch blocking promised by its title. This alarm is intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2417: The broad unit command loses two ordinary clean Git cherry-pick/revert success samples and two terminal startup foreground-read work-budget samples. These unique alarms are intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2412: The broad unit command loses the whole six-case fake-React-hook scroll-anchor suite, two generic draft-retention samples, a simple new-agent order sample, a real history-publication batching-count sample, an unmounted handoff abort/timer sample and three basic WSL preview/error samples. These alarms are intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2404: The broad unit command loses the standalone share-name lookup 503 propagation suite, initial deletion placement-kind sample, direct changed-mtime stale sample, first-pass rollback existence example and abandoned-preparation directory sample. These exact alarms are intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2397 review amendment: The broad unit command loses three standalone suites, two partial-case definitions and two URL table rows across package/download/placement tests. Keep initial wrong-port authorization, modified independent-copy digest refusal and abort-after-begin cancellation dispatch. The remaining alarms are deliberately retired; no equivalent complete replacement or fresh validation of the reduced selector is claimed. Audit2393 current-parent refresh2401: Remove the basic skill-install service alias sample and original public managed-remove receiptless refusal case. Preserve the upstream request ownership suite and transaction interruption coverage. No equivalent complete replacement or fresh validation of the reduced selector is claimed. Audit2389: The broad unit command loses the missing-preview suite, plain-global destination sample and prepublished-receipt recovery example. No equivalent replacement or fresh validation of the reduced selector is claimed. Audit2385: The broad unit command loses four authored walker/staging/protocol examples (six expanded cases); no equivalent replacement or fresh validation is claimed for the reduced suite. Audit2383: The unscoped Vitest command no longer includes the removed Antigravity catalogue suite; its exact provider-root/owner example has no equivalent replacement or fresh validation claim. Audit2377/2381: The unscoped Vitest command includes the reduced bundled-skill and native-discovery suites; the eight removed backup/legacy/catalogue/symlink/SSH-builder examples have no replacement-equivalence or fresh validation claim here. Audit2133 current-parent preparation: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2095 current-parent preparation2126: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): The main IPC contract test subscribes terminal.multiplex through the real handler, disconnects the environment, and asserts the renderer received exactly one {type: close} subscription event. Two isolation tests subscribe a second stream to the same environment and make the first one fail -- in its socket close, and in the liveness probe inside notifyClosed -- then assert the disconnect does not throw, both transports closed, and every close the renderer could still receive was delivered. A third drives a host-initiated close through the transport callback, which is the one notifyClosed call site with no surrounding guard, with a renderer send that throws, and asserts it cannot escape into the WebSocket close handler. A fourth test asserts that after retirement a late response frame is not forwarded and a late transport close does not re-send. The paired-server journey runs three reveal scenarios against one real host and one real paired desktop client, and for each records buffer restore, host-side receipt of the typed marker through an out-of-band host sink file, live paint without a tab flip, and PTY-versus-pane grid convergence.", + "oracle": "Audit3128 scoped coverage reduction: Audit3121 removes VoiceDictationLanguageSetting.test.tsx (Spanish-to-es callback and unsupported-model Korean option presence) and voice-speech-model-rows.test.ts (provider/language-order/retired-on-disk list policy); these real narrow alarms are deliberately retired without replacement. VoiceSpeechModelSection per-model concurrent deletion remains unchanged. Audit3122 removes the direct absolute/parent-segment sparse-directory exception-type sample and the five-row IPC sparse-preset error-code/non-save table (two authored/six expanded cases). Those real typed-validation and IPC refusal/error-code alarms are deliberately retired. Keep both real symlink/junction floating-directory trust cases and the minimal RPC structured validation-reason serialization case. The RPC case does not replace IPC save/refusal behavior, typed normalization for both path inputs, actual Git sparse checkout or rendered localization. No equivalent replacement or fresh test/runtime/platform/remote/performance validation is claimed. Audit3124 removes ExperimentalPane.test.tsx (ordinary Pet switch callback), WorkspaceDirectorySetting.test.tsx (padded absolute saved-path normalization/no-write blur), and repository-search.perf.test.ts (navigation/selected-pane builder-call policy, fixture cardinality/nonempty entries and renamed project keyword inclusion). These real narrow alarms are retired without replacement; RepositoryPane.search.test.tsx search-to-sparse-editor request remains unchanged. Audit3129: resolver Windows case-insensitive and POSIX case-sensitive configured-env composition plus mocked-base launch-preflight Windows composition are intentionally no longer checked by the two deleted suites. No equivalent coverage claimed. Keep runtime-structured-agent-host-env.test.ts unchanged: configured Windows Claude-home account identity and host-account precedence remain checked. Audit3114 incorporates source3101: retire three structured Claude child-environment wrapper cases covering inherited stamp isolation/default and explicit entrypoint, Windows SDK-key canonicalization/default/duplicate-key order, and configured lookup/final environment/config-dir conjunction. Keep direct auth/session sanitation and Windows-versus-Linux overlay checks, actual executable lookup from supplied inherited PATH, native Windows child environment delivery, and stale option-mutation fencing. These are not equivalent replacements for the retired wrapper alarms. No fresh test/runtime, real login-shell probe, Claude SDK startup, remote/mixed-version, UI or performance result is claimed. Audit3114 incorporates source3105: retire 17 authored/20 expanded daemon launch-note cases, including the entire two-case SessionLaunchNote suite. Deliberately lose split-marker carry across same-token re-arm, stale-token retirement refusal, foreground tri-state and serial settlement, omitted-note/no-incarnation mixed-version policy, sticky retirement, first-incarnation/replacement launch preservation and held-launch precedence. Keep only the unchanged unresolved foreground-read race against a later finished attach. Supplied notes/foreground and runtime calls do not establish actual daemon/main restart, SSH, native Windows observation, mobile UI, equivalent replacement or fresh runtime/performance validation. Audit3114 incorporates source3109: retire six authored/35 expanded pricing cases: 21 accepted model-name examples, 10 unsupported/future-model fail-closed examples, malformed TTL clamp, unknown-model estimate refusal, flat-model long-context disregard and oversized long-context clamp. Keep mixed five-minute/one-hour write allocation and short/all-long/mixed long-context allocation with original fixed oracles. Real parser/refusal/bounds alarms are knowingly lost without equivalent replacement; retained tests do not validate provider rates, actual billing or request threshold classification. Preserve the archived usage-scan-append source-freeze receipt without transferring historical execution/performance credit. No fresh runtime result is claimed. Audit3108 source-only reduction (3094/3095/3100): retire 27 authored/30 expanded cases, including complete markdown view-state, file-preview link-open and CLI agent-server-route suites, without replacement. Deliberate losses include preview visibility/reference lifetime and same-path owners; SSH stat, directory/missing-target, explicit-null owner, nonmarkdown line/column, local URI routing, remote-to-client guard and same-file reuse; document-preview source-owner/folder/reuse routing, stable grant location and outside-workspace POSIX/Windows request projection; and verified remote CLI send/check refusal, suggested --on argv/copy, shell no-probe bypass, unverified-launch admission and older-peer refusal. Retain two markdown source-runtime/qualified-reveal alarms, one stale grant-rejection fence and two remote read/mutation evidence policies, with original callbacks. Final last-wins reveal did not prove transient cancellation; CLI prerequisite did not prove no real send, and envelope tests do not execute server refusal. No fresh runtime, rendered UI, actual file/SSH/paired transport, authorization, mixed-version or performance result is claimed. Audit3096 source-only reduction (3087/3088): retire 69 authored/76 expanded cases, including complete recovery-portals and reconnect-banner suites. Intentionally lose dead/default TUI replay, generated alternate-frame style composition, reset/mode/dimension and legacy missing-mode-boundary alarms, UI notice/action/reveal policies, and isolated recovery lifecycle variants, without equivalent replacement. Retained registrations and callbacks are unchanged; headless cells are not pixels, and no fresh runtime, host, SSH, WSL, rendered-app or performance result is claimed. Audit3086 source-only reduction (3079/3080/3081): intentionally retire 93 authored/100 expanded mock-boundary cases, including the complete spawn-errors and SSH reconnect overlay suites, without replacement. Retired alarms include spawn classification/ownership, reply ceilings and renderer-write failure recovery, disconnect/detach fencing branches, authority facts, and overlay retry/visibility wiring. Retained callbacks are unchanged; no fresh execution, real PTY/SSH, rendered UI, or performance result is claimed. Audit2432: The broad unit command loses the isolated BranchPicker open-change notification sample. Its unique true/false callback alarm is intentionally retired; reopening/status-refresh behavior was not exercised. No equivalent replacement or fresh validation of the reduced selector is claimed. Audit2425: The broad unit command loses the workspace-copy-status supplied readError forwarding sample. It did not exercise the branch-switch blocking promised by its title. This alarm is intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2417: The broad unit command loses two ordinary clean Git cherry-pick/revert success samples and two terminal startup foreground-read work-budget samples. These unique alarms are intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2412: The broad unit command loses the whole six-case fake-React-hook scroll-anchor suite, two generic draft-retention samples, a simple new-agent order sample, a real history-publication batching-count sample, an unmounted handoff abort/timer sample and three basic WSL preview/error samples. These alarms are intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2404: The broad unit command loses the standalone share-name lookup 503 propagation suite, initial deletion placement-kind sample, direct changed-mtime stale sample, first-pass rollback existence example and abandoned-preparation directory sample. These exact alarms are intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2397 review amendment: The broad unit command loses three standalone suites, two partial-case definitions and two URL table rows across package/download/placement tests. Keep initial wrong-port authorization, modified independent-copy digest refusal and abort-after-begin cancellation dispatch. The remaining alarms are deliberately retired; no equivalent complete replacement or fresh validation of the reduced selector is claimed. Audit2393 current-parent refresh2401: Remove the basic skill-install service alias sample and original public managed-remove receiptless refusal case. Preserve the upstream request ownership suite and transaction interruption coverage. No equivalent complete replacement or fresh validation of the reduced selector is claimed. Audit2389: The broad unit command loses the missing-preview suite, plain-global destination sample and prepublished-receipt recovery example. No equivalent replacement or fresh validation of the reduced selector is claimed. Audit2385: The broad unit command loses four authored walker/staging/protocol examples (six expanded cases); no equivalent replacement or fresh validation is claimed for the reduced suite. Audit2383: The unscoped Vitest command no longer includes the removed Antigravity catalogue suite; its exact provider-root/owner example has no equivalent replacement or fresh validation claim. Audit2377/2381: The unscoped Vitest command includes the reduced bundled-skill and native-discovery suites; the eight removed backup/legacy/catalogue/symlink/SSH-builder examples have no replacement-equivalence or fresh validation claim here. Audit2133 current-parent preparation: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2095 current-parent preparation2126: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): The main IPC contract test subscribes terminal.multiplex through the real handler, disconnects the environment, and asserts the renderer received exactly one {type: close} subscription event. Two isolation tests subscribe a second stream to the same environment and make the first one fail -- in its socket close, and in the liveness probe inside notifyClosed -- then assert the disconnect does not throw, both transports closed, and every close the renderer could still receive was delivered. A third drives a host-initiated close through the transport callback, which is the one notifyClosed call site with no surrounding guard, with a renderer send that throws, and asserts it cannot escape into the WebSocket close handler. A fourth test asserts that after retirement a late response frame is not forwarded and a late transport close does not re-send. The paired-server journey runs three reveal scenarios against one real host and one real paired desktop client, and for each records buffer restore, host-side receipt of the typed marker through an out-of-band host sink file, live paint without a tab flip, and PTY-versus-pane grid convergence.", "commands": [ "pnpm exec vitest run --config config/vitest.config.ts", "pnpm exec playwright test tests/e2e/paired-remote-terminal-parked-reveal-interactivity.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1" @@ -4322,7 +4317,14 @@ "Audit2412 removes whole useVirtualizedScrollAnchor.marks-restore-signal.test.ts (six fake-hook geometry/mark/convergence/user-takeover/clamp alarms), use-task-creation-draft-retention original3/4 (immediate discard mock-call count/unopened no-write), use-terminal-bulk-close-actions.placement original6/7 (absent new-agent append and one real history-publication count at consumed3/30 scale), PendingNativeChatCreation original6 (unmounted abort completion/timer cleanup), and wsl-move-dialog-store original1/9/13 (default whole-distro count/reference/reuse and rejected-preview/start refusal recovery). The history counter measures genuine publication work without entries/order/reopen assertions; its unique batching alarm is intentionally retired. Fake React scroll tests ignore dependency scheduling and cleanup; unique simulated arithmetic alarms are deliberately lost. Retained existing-terminal reorder is not new-agent append coverage; mounted handoff timeout is not abort coverage. No equivalent complete replacement or fresh validation is claimed.", "Audit2417 removes commit-actions-real-git original3/4 (ordinary clean cherry-pick and revert content, subject and current-branch samples), and pty-startup-ingress-exited-asker original8/12 (actual foreground-group read budget across100queued queries, and zero foreground reads across100normal chunks plus final emitted/accepted raw cursor equality). Counters measure actual callback work and scale is consumed; these internal-work alarms are deliberately retired. Retained unborn, conflict and sequencer cases are not equivalent ordinary-success coverage. No equivalent complete replacement or fresh validation is claimed.", "Audit2425 removes use-workspace-copy-status original5, which returns a fixture-supplied readError string through statusReadError. No consumer blocker or attempted branch switch established refusal; the unique error-forwarding sample is intentionally retired. Other copy ownership and stale-refresh cases are not equivalent error-path coverage. No equivalent replacement or fresh validation is claimed.", - "Audit2432 removes BranchPicker.test.tsx original3, which only asserted onOpenChange(true/false) with an always-mounted mocked Popover. It never reopened the picker or consumed the callback in the parent status-refresh owner, despite its title. Lose that unique open/close notification alarm. Retained blocker/collision recovery and asynchronous post-unmount outcome cases are not equivalent replacement coverage. No fresh validation of the reduced selector is claimed." + "Audit2432 removes BranchPicker.test.tsx original3, which only asserted onOpenChange(true/false) with an always-mounted mocked Popover. It never reopened the picker or consumed the callback in the parent status-refresh owner, despite its title. Lose that unique open/close notification alarm. Retained blocker/collision recovery and asynchronous post-unmount outcome cases are not equivalent replacement coverage. No fresh validation of the reduced selector is claimed.", + "Audit3086 source-only reduction (3079/3080/3081): intentionally retire 93 authored/100 expanded mock-boundary cases, including the complete spawn-errors and SSH reconnect overlay suites, without replacement. Retired alarms include spawn classification/ownership, reply ceilings and renderer-write failure recovery, disconnect/detach fencing branches, authority facts, and overlay retry/visibility wiring. Retained callbacks are unchanged; no fresh execution, real PTY/SSH, rendered UI, or performance result is claimed.", + "Audit3096 source-only reduction (3087/3088): retire 69 authored/76 expanded cases, including complete recovery-portals and reconnect-banner suites. Intentionally lose dead/default TUI replay, generated alternate-frame style composition, reset/mode/dimension and legacy missing-mode-boundary alarms, UI notice/action/reveal policies, and isolated recovery lifecycle variants, without equivalent replacement. Retained registrations and callbacks are unchanged; headless cells are not pixels, and no fresh runtime, host, SSH, WSL, rendered-app or performance result is claimed.", + "Audit3108 source-only reduction (3094/3095/3100): retire 27 authored/30 expanded cases, including complete markdown view-state, file-preview link-open and CLI agent-server-route suites, without replacement. Deliberate losses include preview visibility/reference lifetime and same-path owners; SSH stat, directory/missing-target, explicit-null owner, nonmarkdown line/column, local URI routing, remote-to-client guard and same-file reuse; document-preview source-owner/folder/reuse routing, stable grant location and outside-workspace POSIX/Windows request projection; and verified remote CLI send/check refusal, suggested --on argv/copy, shell no-probe bypass, unverified-launch admission and older-peer refusal. Retain two markdown source-runtime/qualified-reveal alarms, one stale grant-rejection fence and two remote read/mutation evidence policies, with original callbacks. Final last-wins reveal did not prove transient cancellation; CLI prerequisite did not prove no real send, and envelope tests do not execute server refusal. No fresh runtime, rendered UI, actual file/SSH/paired transport, authorization, mixed-version or performance result is claimed.", + "Audit3114 incorporates source3101: retire three structured Claude child-environment wrapper cases covering inherited stamp isolation/default and explicit entrypoint, Windows SDK-key canonicalization/default/duplicate-key order, and configured lookup/final environment/config-dir conjunction. Keep direct auth/session sanitation and Windows-versus-Linux overlay checks, actual executable lookup from supplied inherited PATH, native Windows child environment delivery, and stale option-mutation fencing. These are not equivalent replacements for the retired wrapper alarms. No fresh test/runtime, real login-shell probe, Claude SDK startup, remote/mixed-version, UI or performance result is claimed.", + "Audit3114 incorporates source3105: retire 17 authored/20 expanded daemon launch-note cases, including the entire two-case SessionLaunchNote suite. Deliberately lose split-marker carry across same-token re-arm, stale-token retirement refusal, foreground tri-state and serial settlement, omitted-note/no-incarnation mixed-version policy, sticky retirement, first-incarnation/replacement launch preservation and held-launch precedence. Keep only the unchanged unresolved foreground-read race against a later finished attach. Supplied notes/foreground and runtime calls do not establish actual daemon/main restart, SSH, native Windows observation, mobile UI, equivalent replacement or fresh runtime/performance validation.", + "Audit3114 incorporates source3109: retire six authored/35 expanded pricing cases: 21 accepted model-name examples, 10 unsupported/future-model fail-closed examples, malformed TTL clamp, unknown-model estimate refusal, flat-model long-context disregard and oversized long-context clamp. Keep mixed five-minute/one-hour write allocation and short/all-long/mixed long-context allocation with original fixed oracles. Real parser/refusal/bounds alarms are knowingly lost without equivalent replacement; retained tests do not validate provider rates, actual billing or request threshold classification. Preserve the archived usage-scan-append source-freeze receipt without transferring historical execution/performance credit. No fresh runtime result is claimed.", + "Audit3128 scoped coverage reduction: Audit3121 removes VoiceDictationLanguageSetting.test.tsx (Spanish-to-es callback and unsupported-model Korean option presence) and voice-speech-model-rows.test.ts (provider/language-order/retired-on-disk list policy); these real narrow alarms are deliberately retired without replacement. VoiceSpeechModelSection per-model concurrent deletion remains unchanged. Audit3122 removes the direct absolute/parent-segment sparse-directory exception-type sample and the five-row IPC sparse-preset error-code/non-save table (two authored/six expanded cases). Those real typed-validation and IPC refusal/error-code alarms are deliberately retired. Keep both real symlink/junction floating-directory trust cases and the minimal RPC structured validation-reason serialization case. The RPC case does not replace IPC save/refusal behavior, typed normalization for both path inputs, actual Git sparse checkout or rendered localization. No equivalent replacement or fresh test/runtime/platform/remote/performance validation is claimed. Audit3124 removes ExperimentalPane.test.tsx (ordinary Pet switch callback), WorkspaceDirectorySetting.test.tsx (padded absolute saved-path normalization/no-write blur), and repository-search.perf.test.ts (navigation/selected-pane builder-call policy, fixture cardinality/nonempty entries and renamed project keyword inclusion). These real narrow alarms are retired without replacement; RepositoryPane.search.test.tsx search-to-sparse-editor request remains unchanged. Audit3129: resolver Windows case-insensitive and POSIX case-sensitive configured-env composition plus mocked-base launch-preflight Windows composition are intentionally no longer checked by the two deleted suites. No equivalent coverage claimed. Keep runtime-structured-agent-host-env.test.ts unchanged: configured Windows Claude-home account identity and host-account precedence remain checked." ], "demotionRule": "Demote if a retired runtime transport can leave a renderer subscription without a close event, if a close is delivered more than once, if one failing teardown strands its sibling subscriptions, if payload frames from a retired transport reach the renderer, or if a revealed remote terminal can stay blank or reject input after a reconnect." }, @@ -4440,7 +4442,7 @@ "providers": ["remote-runtime"], "coveredPlatforms": ["macos"], "coveredProviders": [], - "coverageNotes": "Audit2133 current-parent preparation: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2095 current-parent preparation2126: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2090 (sources2088/2089): Current reduced suites intentionally retire the alarms listed in knownGaps; prior dated execution and replacement claims do not validate changed source bytes or selectors. Historical prior account (preserved verbatim): Audit2086 (sources2082/2083): Current reduced suites intentionally retire the alarms listed in knownGaps; prior dated execution and replacement claims do not validate changed source bytes or selectors. Historical prior account (preserved verbatim): Audit2076 (sources2073/2074): Intentionally retire these low-value alarms without replacement: src/main/runtime/orca-runtime-tests/terminal-handles-part-02.spec.ts: recovers a moved SSH pane through the tab it now sits in, not the one its lease froze; reports a plain expired orphan; does not report a pane whose only recent lease was superseded; does not report a pane whose only recent lease had its relay id recycled; picks the eligible orphan over a superseded predecessor that matches first; collects the same tabs the per-tab reader reports, in one sweep of the leases; reports no candidate when no lease names any of the worktree tabs. src/main/runtime/orca-runtime-tests/runtime-availability.spec.ts: starts unavailable with no authoritative window; reports the configured Windows terminal shell on status; reports floating workspace availability from settings on status; polls floating tabs with targeted PTY liveness and no repo/provider inventory; hydrates persisted tabs when the store cannot report repos; advertises browser screencast only when a renderer window is available; routes mobile Codex reset consumption through the account mutation coordinator; maps a definite pre-provider rejection into an authoritative current snapshot. Existing unrelated evidenceRuns retain their original scope; unchanged dated receipt objects do not validate changed source bytes or reduced selectors. No fresh gate execution or historical import-closure equivalence is claimed. Frozen current literal-import graph reaches these changed test bytes; unresolved local and computed edges remain debt, and current reachability is not historical execution or equivalent replacement evidence. Historical prior account (preserved verbatim): Audit2043: Remove the registration-wait, webview-security, preview-authorization, guest-lifecycle and grant-IPC unit suites and the sibling-directory assertion reference. Runtime and browser capability consumers also lose removed imported examples. Remaining document-policy, file-reader and grant-registry assertions do not replace those alarms. Preserve the 27 independent dated receipts at their original unchanged selectors; reduced prospective unit selectors have no fresh result or equivalent replacement claim. Historical prior account (preserved verbatim): Audit1973: Delete the browser RPC forwarding/schema/scalar suite and its prospective selector. Preserve all27 unrelated dated receipts; no actual browser operation alarm is replaced. Whole original changed dated receipts are retired, never narrowed or transferred. This metadata review claims no fresh execution, equivalent replacement, provider/PTY, live remote/mobile, rendered, cross-platform, soak or measured-budget result. Historical prior account (preserved verbatim): Audit1925 (sources1922/1923): The complete terminal-file-link-actions suite is removed. Lose supplied local/remote menu-row inventories, browser label/order/download/reveal callback forwarding, platform-copy and source-group pinning examples. No OS reveal/download, real remote preview or browser navigation was exercised; the other real document-preview sources are not equivalent replacements. Preserve meaningful remaining selectors/assertions and unchanged dated receipts within their original scope. Reduced selectors have no fresh gate execution evidence in this metadata packet. No equivalent replacement, physical watcher/PTY/provider, live SSH/remote/mobile, rendered, cross-platform, soak or measured-budget result is claimed. Historical prior account (preserved verbatim): Audit1899 replays sealed Audit1895: workspace-doc-history.test.ts is entirely removed from its current command and testFiles. Supplied recency/dedupe/cap/title/identity examples and the10000-row getter-read budget alarm are intentionally lost. No current assertionRef or active receipt selects that file. Unrelated active receipts and all prior gap literals remain verbatim; no replacement history/index-work coverage or new browser/paired-preview gate execution is claimed. Historical coverage account before Audit1899: Audit1853 reduces LinkActionPopover.test.tsx and archives its complete 2026-09-06 mixed-selector receipt. The lost copy/open-state and Settings callback-order/omission alarms are accepted without equivalent replacement coverage. No fresh execution of this gate is credited. Audit1709 removes low-value test definitions and retires affected assertion or execution-receipt claims without equivalent replacement coverage. Per-source reviews bound retained behavior and injected/native/remote limits. Unchanged assertions and receipts keep their original scope; gates lacking current active evidence are downgraded. Historical scope before this audit: Audit1687 removes low-value test projections, mock wiring, private-state assertions, inventories and duplicate examples without equivalent replacement coverage. Per-source reviews record retained public behavior and refusal boundaries with injected/native/remote limits. Affected historical receipts are retired; gates without active current evidence are downgraded. Historical scope before this audit: Audit1630 removes low-value Git, GitHub/GitLab and IPC projections, argument inventories and duplicate examples without equivalent replacement coverage. Per-source reviews record retained cancellation, ownership, authorization, filesystem and SQLite boundaries with injected/native/remote limits. Affected historical receipts are retired; gates without active current evidence are downgraded. Historical scope before this audit: Audit1571 removes mock forwarding, capability/schema inventories, private predicates, supplied error/result replays, and filesystem-mocked persistence checks without equivalent replacement claims. Retained changed tests also exercise real local permission-file reread, registry proxy rollback/credential retirement, site display recovery, and pending sender/credential/document admission with injected Electron/UI producers. Retained network tests exercise owned stream credit/cleanup, generation and retirement fences, real loopback proxy/SOCKS bytes and file-backed binding state. SSH/WSL/paired runtime, native sessions, authority producers and storage clears remain injected; simulated relaunch uses filesystem mocks. The unchanged profile-retirement suite was statically reviewed and not rerun. No actual SSH/WSL runtime, native Chromium jar deletion, durable process restart, remote DNS or external site proof. Receipts selecting changed tests are retired and unaffected receipts remain unchanged. Historical scope before this audit: Audit1568 removes API inventories, supplied mock-value/forwarding assertions, implementation-shape tables and negative/setup cases that do not reach their intended boundary, without equivalent replacement claims. Retained changed definitions exercise owned manager lifetime/queue/refcount/clamp and real synthetic process/files/staging boundaries. Seventeen unchanged native Electron definitions across this combined audit are statically reviewed and not rerun. Changed native suites exercise local popup wire/cookie/session and clean/native process-identity behavior; known popup-admission flaws remain unfixed. Unit native/event/session/registry inputs remain injected. No actual OS focus, registry/host execution, CRX verification or durable restart proof. Receipts selecting changed tests are retired and unaffected receipts remain unchanged. Historical scope before this audit: Audit1564 retires copied cookie key recipes, mock bridge/preview forwarding and flag tables, source-spelling/script inventories and setup-only alarms without equivalent replacement coverage. Retained changed cases exercise actual scoped SQLite merge/rollback/schema refusal, owned cancellation and grant authority/canonical containment, ranged response consistency, replacement guest ownership and executed guest-script promise/DOM cleanup behavior. Session, host response, geometry/observer and transport producers remain injected where documented. One unchanged native extension definition is not rerun locally; no actual remote filesystem, native layout/focus or authentication claim. Receipts selecting modified tests are retired; unaffected exact-selector receipts remain unchanged. Historical scope before this audit: Audit1412 changes referenced sources. Retained unchanged-source receipts apply only to their unchanged commands; the reduced selectors have no new execution result. Historical coverage account (not a current claim for changed sources): Audit1386 changes referenced sources; no new execution result validates reduced selectors. Retained unchanged receipts apply only to unchanged commands. Historical coverage: Audit1308 changes referenced sources. Retained unchanged-source receipts apply only to their unchanged commands; the reduced selectors have no new execution result. Historical coverage account (not a current claim for changed sources): A macOS desktop host and a separate paired Electron client exercise the Explorer preview action end to end: the document renders inside the client-local orca-preview guest, the client gains exactly one browser workspace and it is the document one, the host's own page registry and tab snapshot gain nothing, the split does not take focus from the source editor, an explicit click activates the preview, and closing it with its X leaves the editor and terminal intact. Both host oracles are read in a run where an ordinary URL browser tab from the same client does arrive in them, because a host that cannot project browser pages at all would otherwise answer the absence question the same way. A second journey quits that client with a document tab open and relaunches it on the same profile, which is the restore the user's original report turned on: the same workspace row comes back blank and named by the document, renders again, and does so over a grant URL that differs from the one that was quit. The same journey pins the guest URL to the orca-preview scheme, so a regression back to file:// or to a host browser page is visible. The rewritten journey caught a live gap the old one could not see: since previews became browser pages they had no chrome to hand focus to their guest, so main refused every reported link click as coming from an unfocused guest and the one route out of a preview was dead. Main's own log answered it — registered true, grant live, focused false — after the embedder reported the anchor really taking six real presses. The pane now hands focus over when it is the reader's surface, and the journey routes. The preview's own components moved out of the editor folder into browser-pane/workspace-doc/ with this change, since a preview is a browser pane and no longer an editor one; their i18n keys keep the historical auto.components.editor.HtmlDocPreview prefix rather than dropping thirty translated strings across four locales for a tidier key path. Seven gate commands named those files by their old paths and were re-issued at the new ones, and the nine dated runs behind them were retired rather than left naming files that are no longer there, on the same principle this gate already applied twice: vitest treats a missing named file as a filter and exits 0, so such a command reports green while silently running only the half that still exists. Each retired record's finding is carried into the replacement run recorded below it, including the red half of the entry/subresource pair, which was re-measured by re-applying its mutant at the new path rather than being dropped. The fence over the page-management channels was decided channel by channel before it was built, and that decision record is: of the 23 channels that can name a page, 6 are allowed for a workspace-document page — the tool channels the reader drives, which reach it through the one authority that reads both halves of the registry — 9 refuse (download accept and cancel, devtools, certificate proceed, client page metadata publish, viewport override, WebAuthn account response, registration probe, registration repair), 3 cannot name a page at all because they take a profile or a worktree instead, and the rest are the registration and activation doors, refused on membership. What shipped is not the per-channel guard that classification implies: eleven call sites read the browsing map directly, so a guard per channel is exactly the caller-flag seam that has to be remembered at every future call site. The registry was split in two instead, which makes the classification structural — a document guest is absent from the browsing map, so every refusing channel refuses by absence rather than by a guard, and a channel added later inherits the refusal without being classified at all. Unit contracts cover grant scoping and path containment, per-owner read routing, refusal of a truncated or over-cap read, the old-server binary degradation, scheme admission on will-attach-webview, the guest navigation policy, the plan layer's local/SSH/paired/out-of-worktree routing, and the distinct sentence the reader sees for each failure reason. The paired browser-creation surfaces this gate still owns — capability rejection cleanup, canonical page identity, reconciliation rollback, close convergence — keep their existing headed and headless journeys. Linux and Windows live journeys remain gaps. A refused subresource is a notice on a rendered document rather than a failure panel, since only the entry document's failure leaves the reader with nothing; the entry document is served as text by every owner and cannot be refused for its format at all. Grants die with the renderer document that minted them, at window creation and on every reload, because the renderer holds the only record of which preview owns which grant. The mint and revoke channels answer only the trusted renderer, and a preview tab is never republished to mobile clients, which have no grant to read it with. External URLs leave the preview only via a trusted user click on an anchor while the preview is focused; script-driven navigation and popups reach nothing. The guest's navigation sinks are deny-only and route no URL at all, and the guest carries a preview-only preload, pinned by will-attach-webview and denied to every other attach path, whose capture-phase click handler reports an anchor's URL to main; main answers only a report from a preview guest with a live bound grant that is the focused contents, and only for http(s). Two earlier reader-facing records from 2026-08-26, which measured a shape where a subresource failure was filtered out and the panel still carried an unsupported-asset sentence, were retired with that shape rather than left contradicting the 10/10 runs of the command they share. Two more were retired on the same principle when the external-link design changed: a record showing that removing a recent-input gate let the unattended document open two browser tabs, and a record showing that dropping the webview's allowpopups attribute left a target=_blank press with nothing to route. Both measured machinery that no longer exists — the input gate could not tell a script's navigation from the click it followed, which is the case now recorded below, and the preload intercepts the press before Chromium considers a popup at all, so allowpopups was removed. WebRTC is fenced by the guest's IP-handling policy alone: this Chromium answers a webrtc content-security-policy directive as unrecognized and gathers candidates anyway, so the directive is deliberately absent from the served policy and a unit assertion keeps it out. A standalone Electron probe on the same version gathered 7 candidates including a STUN reflexive one without the policy and 0 with it. Two command paths this gate used to carry no longer exist, and their structured evidence was retired with them rather than left naming files that are no longer there: vitest treats a missing named file as a filter and exits 0, so such a command reports green while silently running only the half that still exists. The first is src/renderer/src/lib/file-preview-capability-cleanup.test.ts, deleted in STA-5557 when HTML previews stopped creating paired browser tabs: on 2026-08-12 its five-assertion STA-4063 oracle ran red 5/5 on scan e4c278eb52 and on main bc2e30000b, partially green 2/5 on pre-fix PR #14100, and green 5/5 on candidate 14b5877a75; the surviving half of that oracle is web-runtime-browser-capability-cleanup.test.ts, still commanded above. The second is tests/e2e/paired-remote-html-browser-focus.spec.ts, renamed in STA-5557 to paired-remote-html-preview-local-render.spec.ts because the journey no longer proves anything about browser focus: its 2026-08-12 runs established the owning-runtime routing and no-focus-steal behavior of the then-current browser-tab preview (red on main 346e59c879 and a90a18d43d, green on candidates d05217c40d and b7de9c0961, red again with only the side-preview focus or routing seam disabled), and its 2026-08-26 runs are recorded below under the current path. An adversarial pass after the journey landed found four coverage gaps rather than shipping defects, and all four are pinned here. The page url had a third door: the navigation commit wrote whatever url was reported, unfenced, and while nothing reaches it for a document page today it sat one report away from giving a document page a URL that persistence, the publish boundary and the address bar all read. The group projection was the load-bearing half of the mobile publish boundary that no test had: removing it alone left the whole suite green, because no test in it had tab groups, while the phone payload gained a group whose order and active tab named a document tab it was never sent. The focus handoff was covered only in the narrowing direction, so widening it — dropping the page-activity check, or either half of the reader-surface check — survived both suites. And the handoff itself was offered once and never again, so another app taking the front left the preview's one route out shut until the pane remounted, which is a desktop gap rather than a test-only one. The standalone capability-cleanup and browser-placement ledger suites were removed, including their mixed-version rejection, cleanup transfer, capacity and partition alarms. web-runtime-session.test.ts no longer supplies browser creation, rollback or recovery assertions. The grouped workspace-document exclusion remains; the separate no-group publication example was removed. Existing live/browser-store evidence does not establish equivalent replacement alarms. Earlier descriptions of the removed fixtures and their run results are historical. The client-owned-placement and layout-groups suites are removed. The broad mirror fixture no longer supplies preview placement/reservation/move assertions. Browser rows retain pre-response replay and real local-workspace reuse; focus intent retains response ordering and exact-leaf sibling ordering. Supplied browser-layout and placement/focus projection alarms are lost; retained evidence is narrower and not equivalent. The test-value audit retires renderer grant-release mock ledgers, direct creation/title/session-field copies, notification/onboarding calls and several placement/per-page projections. Canonical-ID rejection, document reopen/restore and preview URL fences remain; they do not prove close-to-registry revocation or replace every lost alarm. Historical execution evidence is unchanged. The test-value audit removed the direct legacy editor-preview hydration example from tabs-hydration.test.ts. Dropping legacy preview chrome while preserving the same-document ordinary editor no longer has that alarm. The surviving corrupt-id deduplication case does not cover that migration, and retained preview journeys are not equivalent replacement coverage. Historical run counts and migration mutation findings describe the earlier suite. Audit1308: src/renderer/src/components/editor/EditorPanelShell.header.test.tsx removed. Header visibility and mocked editor-surface inventory alarms disappear. No equivalent replacement coverage is claimed. Audit1312 removes low-value imported specs from aggregate test drivers; old whole-driver execution receipts are archived. Retained registrations are unchanged. No equivalent replacement coverage is claimed. Audit1383: src/main/runtime/orca-runtime-tests/worktree-native-chat-setup.spec.ts reduced. Lose command-list/receipt/default/background activation matrices whose provider spawn returns canned IDs. Retained test proves actual tab allocation separation, not terminal startup. No equivalent replacement coverage is claimed. Existing exact unaffected receipts and their platform/provider/budget claims remain scoped to unchanged selectors. Changed optional selectors have no new execution result. Audit1386: src/renderer/src/lib/doc-preview-grants.test.ts reduced. Repeated-mount mint count, ordinary release/remint and failed-mint cache retry standalone alarms removed; no actual grant server/access test. No equivalent replacement coverage is claimed. Audit1401: src/main/runtime/orca-runtime-tests/mobile-session-tabs-duplicate-pty-repair.spec.ts removed. Loses listing-on-already-repaired-data alarm; it never established runtime repair invocation. src/main/runtime/orca-runtime-tests/mobile-session-tabs-split-agent-tab.spec.ts removed. Loses actual mobile split sibling shell versus recorded-agent resume command alarms; no equivalent coverage asserted. src/main/runtime/orca-runtime-tests/worktree-native-chat-setup.spec.ts removed. Loses separate setup/startup tab result in that manually assembled path. src/main/runtime/orca-runtime.test.ts reduced. Driver registration/import of a whole-deleted selected spec is removed; directly authored cases remain unchanged. No extra authored removal credit. No equivalent replacement coverage is claimed. The existing primary-selector receipt and its claims remain scoped to their unchanged sources; the broad optional command has no new execution result. Audit1412: src/main/browser/guest-lifecycle/manager/browser-manager-guest-policy-profile.test.ts removed. Loses workspace-document in-grant navigation/popup refusal, cleanup/re-attach, cross-registry guest authorization and browsing-map separation alarms. No equivalent browser policy coverage is claimed. No equivalent replacement coverage is claimed. Audit1421 removes browser-address-bar-suggestions.test.ts. Document suggestion title/path matching, recency merging and path-prefix ranking alarms are lost. No equivalent replacement coverage is claimed. Historical receipts remain scoped to unchanged selectors; the reduced command has no new execution result. Audit2452 removes the whole fake-effect browser-close routing suite. Active selectors and assertion references no longer select it. Its requested-worktree default close, sibling-page close and explicit missing-page typed reply alarms are intentionally retired without equivalent replacement. Prior dated receipts remain unchanged and scoped to their original sources; no fresh execution of the reduced gate, rendered browser, live remote, mobile or cross-platform validation is claimed.", + "coverageNotes": "Audit3108: doc-preview-grants.test.ts now retains only the stale grant-rejection race: a late rejected grant request must not evict a newer grant registry entry, witnessed by later revocation of grant-2. Retire stable minted-location and outside-workspace POSIX/Windows request-projection alarms without replacement. The direct nonempty grant selector remains unchanged; no real grant-server/file authorization, SSH/HTML delivery or native-focus result is claimed. Audit2133 current-parent preparation: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2095 current-parent preparation2126: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2090 (sources2088/2089): Current reduced suites intentionally retire the alarms listed in knownGaps; prior dated execution and replacement claims do not validate changed source bytes or selectors. Historical prior account (preserved verbatim): Audit2086 (sources2082/2083): Current reduced suites intentionally retire the alarms listed in knownGaps; prior dated execution and replacement claims do not validate changed source bytes or selectors. Historical prior account (preserved verbatim): Audit2076 (sources2073/2074): Intentionally retire these low-value alarms without replacement: src/main/runtime/orca-runtime-tests/terminal-handles-part-02.spec.ts: recovers a moved SSH pane through the tab it now sits in, not the one its lease froze; reports a plain expired orphan; does not report a pane whose only recent lease was superseded; does not report a pane whose only recent lease had its relay id recycled; picks the eligible orphan over a superseded predecessor that matches first; collects the same tabs the per-tab reader reports, in one sweep of the leases; reports no candidate when no lease names any of the worktree tabs. src/main/runtime/orca-runtime-tests/runtime-availability.spec.ts: starts unavailable with no authoritative window; reports the configured Windows terminal shell on status; reports floating workspace availability from settings on status; polls floating tabs with targeted PTY liveness and no repo/provider inventory; hydrates persisted tabs when the store cannot report repos; advertises browser screencast only when a renderer window is available; routes mobile Codex reset consumption through the account mutation coordinator; maps a definite pre-provider rejection into an authoritative current snapshot. Existing unrelated evidenceRuns retain their original scope; unchanged dated receipt objects do not validate changed source bytes or reduced selectors. No fresh gate execution or historical import-closure equivalence is claimed. Frozen current literal-import graph reaches these changed test bytes; unresolved local and computed edges remain debt, and current reachability is not historical execution or equivalent replacement evidence. Historical prior account (preserved verbatim): Audit2043: Remove the registration-wait, webview-security, preview-authorization, guest-lifecycle and grant-IPC unit suites and the sibling-directory assertion reference. Runtime and browser capability consumers also lose removed imported examples. Remaining document-policy, file-reader and grant-registry assertions do not replace those alarms. Preserve the 27 independent dated receipts at their original unchanged selectors; reduced prospective unit selectors have no fresh result or equivalent replacement claim. Historical prior account (preserved verbatim): Audit1973: Delete the browser RPC forwarding/schema/scalar suite and its prospective selector. Preserve all27 unrelated dated receipts; no actual browser operation alarm is replaced. Whole original changed dated receipts are retired, never narrowed or transferred. This metadata review claims no fresh execution, equivalent replacement, provider/PTY, live remote/mobile, rendered, cross-platform, soak or measured-budget result. Historical prior account (preserved verbatim): Audit1925 (sources1922/1923): The complete terminal-file-link-actions suite is removed. Lose supplied local/remote menu-row inventories, browser label/order/download/reveal callback forwarding, platform-copy and source-group pinning examples. No OS reveal/download, real remote preview or browser navigation was exercised; the other real document-preview sources are not equivalent replacements. Preserve meaningful remaining selectors/assertions and unchanged dated receipts within their original scope. Reduced selectors have no fresh gate execution evidence in this metadata packet. No equivalent replacement, physical watcher/PTY/provider, live SSH/remote/mobile, rendered, cross-platform, soak or measured-budget result is claimed. Historical prior account (preserved verbatim): Audit1899 replays sealed Audit1895: workspace-doc-history.test.ts is entirely removed from its current command and testFiles. Supplied recency/dedupe/cap/title/identity examples and the10000-row getter-read budget alarm are intentionally lost. No current assertionRef or active receipt selects that file. Unrelated active receipts and all prior gap literals remain verbatim; no replacement history/index-work coverage or new browser/paired-preview gate execution is claimed. Historical coverage account before Audit1899: Audit1853 reduces LinkActionPopover.test.tsx and archives its complete 2026-09-06 mixed-selector receipt. The lost copy/open-state and Settings callback-order/omission alarms are accepted without equivalent replacement coverage. No fresh execution of this gate is credited. Audit1709 removes low-value test definitions and retires affected assertion or execution-receipt claims without equivalent replacement coverage. Per-source reviews bound retained behavior and injected/native/remote limits. Unchanged assertions and receipts keep their original scope; gates lacking current active evidence are downgraded. Historical scope before this audit: Audit1687 removes low-value test projections, mock wiring, private-state assertions, inventories and duplicate examples without equivalent replacement coverage. Per-source reviews record retained public behavior and refusal boundaries with injected/native/remote limits. Affected historical receipts are retired; gates without active current evidence are downgraded. Historical scope before this audit: Audit1630 removes low-value Git, GitHub/GitLab and IPC projections, argument inventories and duplicate examples without equivalent replacement coverage. Per-source reviews record retained cancellation, ownership, authorization, filesystem and SQLite boundaries with injected/native/remote limits. Affected historical receipts are retired; gates without active current evidence are downgraded. Historical scope before this audit: Audit1571 removes mock forwarding, capability/schema inventories, private predicates, supplied error/result replays, and filesystem-mocked persistence checks without equivalent replacement claims. Retained changed tests also exercise real local permission-file reread, registry proxy rollback/credential retirement, site display recovery, and pending sender/credential/document admission with injected Electron/UI producers. Retained network tests exercise owned stream credit/cleanup, generation and retirement fences, real loopback proxy/SOCKS bytes and file-backed binding state. SSH/WSL/paired runtime, native sessions, authority producers and storage clears remain injected; simulated relaunch uses filesystem mocks. The unchanged profile-retirement suite was statically reviewed and not rerun. No actual SSH/WSL runtime, native Chromium jar deletion, durable process restart, remote DNS or external site proof. Receipts selecting changed tests are retired and unaffected receipts remain unchanged. Historical scope before this audit: Audit1568 removes API inventories, supplied mock-value/forwarding assertions, implementation-shape tables and negative/setup cases that do not reach their intended boundary, without equivalent replacement claims. Retained changed definitions exercise owned manager lifetime/queue/refcount/clamp and real synthetic process/files/staging boundaries. Seventeen unchanged native Electron definitions across this combined audit are statically reviewed and not rerun. Changed native suites exercise local popup wire/cookie/session and clean/native process-identity behavior; known popup-admission flaws remain unfixed. Unit native/event/session/registry inputs remain injected. No actual OS focus, registry/host execution, CRX verification or durable restart proof. Receipts selecting changed tests are retired and unaffected receipts remain unchanged. Historical scope before this audit: Audit1564 retires copied cookie key recipes, mock bridge/preview forwarding and flag tables, source-spelling/script inventories and setup-only alarms without equivalent replacement coverage. Retained changed cases exercise actual scoped SQLite merge/rollback/schema refusal, owned cancellation and grant authority/canonical containment, ranged response consistency, replacement guest ownership and executed guest-script promise/DOM cleanup behavior. Session, host response, geometry/observer and transport producers remain injected where documented. One unchanged native extension definition is not rerun locally; no actual remote filesystem, native layout/focus or authentication claim. Receipts selecting modified tests are retired; unaffected exact-selector receipts remain unchanged. Historical scope before this audit: Audit1412 changes referenced sources. Retained unchanged-source receipts apply only to their unchanged commands; the reduced selectors have no new execution result. Historical coverage account (not a current claim for changed sources): Audit1386 changes referenced sources; no new execution result validates reduced selectors. Retained unchanged receipts apply only to unchanged commands. Historical coverage: Audit1308 changes referenced sources. Retained unchanged-source receipts apply only to their unchanged commands; the reduced selectors have no new execution result. Historical coverage account (not a current claim for changed sources): A macOS desktop host and a separate paired Electron client exercise the Explorer preview action end to end: the document renders inside the client-local orca-preview guest, the client gains exactly one browser workspace and it is the document one, the host's own page registry and tab snapshot gain nothing, the split does not take focus from the source editor, an explicit click activates the preview, and closing it with its X leaves the editor and terminal intact. Both host oracles are read in a run where an ordinary URL browser tab from the same client does arrive in them, because a host that cannot project browser pages at all would otherwise answer the absence question the same way. A second journey quits that client with a document tab open and relaunches it on the same profile, which is the restore the user's original report turned on: the same workspace row comes back blank and named by the document, renders again, and does so over a grant URL that differs from the one that was quit. The same journey pins the guest URL to the orca-preview scheme, so a regression back to file:// or to a host browser page is visible. The rewritten journey caught a live gap the old one could not see: since previews became browser pages they had no chrome to hand focus to their guest, so main refused every reported link click as coming from an unfocused guest and the one route out of a preview was dead. Main's own log answered it — registered true, grant live, focused false — after the embedder reported the anchor really taking six real presses. The pane now hands focus over when it is the reader's surface, and the journey routes. The preview's own components moved out of the editor folder into browser-pane/workspace-doc/ with this change, since a preview is a browser pane and no longer an editor one; their i18n keys keep the historical auto.components.editor.HtmlDocPreview prefix rather than dropping thirty translated strings across four locales for a tidier key path. Seven gate commands named those files by their old paths and were re-issued at the new ones, and the nine dated runs behind them were retired rather than left naming files that are no longer there, on the same principle this gate already applied twice: vitest treats a missing named file as a filter and exits 0, so such a command reports green while silently running only the half that still exists. Each retired record's finding is carried into the replacement run recorded below it, including the red half of the entry/subresource pair, which was re-measured by re-applying its mutant at the new path rather than being dropped. The fence over the page-management channels was decided channel by channel before it was built, and that decision record is: of the 23 channels that can name a page, 6 are allowed for a workspace-document page — the tool channels the reader drives, which reach it through the one authority that reads both halves of the registry — 9 refuse (download accept and cancel, devtools, certificate proceed, client page metadata publish, viewport override, WebAuthn account response, registration probe, registration repair), 3 cannot name a page at all because they take a profile or a worktree instead, and the rest are the registration and activation doors, refused on membership. What shipped is not the per-channel guard that classification implies: eleven call sites read the browsing map directly, so a guard per channel is exactly the caller-flag seam that has to be remembered at every future call site. The registry was split in two instead, which makes the classification structural — a document guest is absent from the browsing map, so every refusing channel refuses by absence rather than by a guard, and a channel added later inherits the refusal without being classified at all. Unit contracts cover grant scoping and path containment, per-owner read routing, refusal of a truncated or over-cap read, the old-server binary degradation, scheme admission on will-attach-webview, the guest navigation policy, the plan layer's local/SSH/paired/out-of-worktree routing, and the distinct sentence the reader sees for each failure reason. The paired browser-creation surfaces this gate still owns — capability rejection cleanup, canonical page identity, reconciliation rollback, close convergence — keep their existing headed and headless journeys. Linux and Windows live journeys remain gaps. A refused subresource is a notice on a rendered document rather than a failure panel, since only the entry document's failure leaves the reader with nothing; the entry document is served as text by every owner and cannot be refused for its format at all. Grants die with the renderer document that minted them, at window creation and on every reload, because the renderer holds the only record of which preview owns which grant. The mint and revoke channels answer only the trusted renderer, and a preview tab is never republished to mobile clients, which have no grant to read it with. External URLs leave the preview only via a trusted user click on an anchor while the preview is focused; script-driven navigation and popups reach nothing. The guest's navigation sinks are deny-only and route no URL at all, and the guest carries a preview-only preload, pinned by will-attach-webview and denied to every other attach path, whose capture-phase click handler reports an anchor's URL to main; main answers only a report from a preview guest with a live bound grant that is the focused contents, and only for http(s). Two earlier reader-facing records from 2026-08-26, which measured a shape where a subresource failure was filtered out and the panel still carried an unsupported-asset sentence, were retired with that shape rather than left contradicting the 10/10 runs of the command they share. Two more were retired on the same principle when the external-link design changed: a record showing that removing a recent-input gate let the unattended document open two browser tabs, and a record showing that dropping the webview's allowpopups attribute left a target=_blank press with nothing to route. Both measured machinery that no longer exists — the input gate could not tell a script's navigation from the click it followed, which is the case now recorded below, and the preload intercepts the press before Chromium considers a popup at all, so allowpopups was removed. WebRTC is fenced by the guest's IP-handling policy alone: this Chromium answers a webrtc content-security-policy directive as unrecognized and gathers candidates anyway, so the directive is deliberately absent from the served policy and a unit assertion keeps it out. A standalone Electron probe on the same version gathered 7 candidates including a STUN reflexive one without the policy and 0 with it. Two command paths this gate used to carry no longer exist, and their structured evidence was retired with them rather than left naming files that are no longer there: vitest treats a missing named file as a filter and exits 0, so such a command reports green while silently running only the half that still exists. The first is src/renderer/src/lib/file-preview-capability-cleanup.test.ts, deleted in STA-5557 when HTML previews stopped creating paired browser tabs: on 2026-08-12 its five-assertion STA-4063 oracle ran red 5/5 on scan e4c278eb52 and on main bc2e30000b, partially green 2/5 on pre-fix PR #14100, and green 5/5 on candidate 14b5877a75; the surviving half of that oracle is web-runtime-browser-capability-cleanup.test.ts, still commanded above. The second is tests/e2e/paired-remote-html-browser-focus.spec.ts, renamed in STA-5557 to paired-remote-html-preview-local-render.spec.ts because the journey no longer proves anything about browser focus: its 2026-08-12 runs established the owning-runtime routing and no-focus-steal behavior of the then-current browser-tab preview (red on main 346e59c879 and a90a18d43d, green on candidates d05217c40d and b7de9c0961, red again with only the side-preview focus or routing seam disabled), and its 2026-08-26 runs are recorded below under the current path. An adversarial pass after the journey landed found four coverage gaps rather than shipping defects, and all four are pinned here. The page url had a third door: the navigation commit wrote whatever url was reported, unfenced, and while nothing reaches it for a document page today it sat one report away from giving a document page a URL that persistence, the publish boundary and the address bar all read. The group projection was the load-bearing half of the mobile publish boundary that no test had: removing it alone left the whole suite green, because no test in it had tab groups, while the phone payload gained a group whose order and active tab named a document tab it was never sent. The focus handoff was covered only in the narrowing direction, so widening it — dropping the page-activity check, or either half of the reader-surface check — survived both suites. And the handoff itself was offered once and never again, so another app taking the front left the preview's one route out shut until the pane remounted, which is a desktop gap rather than a test-only one. The standalone capability-cleanup and browser-placement ledger suites were removed, including their mixed-version rejection, cleanup transfer, capacity and partition alarms. web-runtime-session.test.ts no longer supplies browser creation, rollback or recovery assertions. The grouped workspace-document exclusion remains; the separate no-group publication example was removed. Existing live/browser-store evidence does not establish equivalent replacement alarms. Earlier descriptions of the removed fixtures and their run results are historical. The client-owned-placement and layout-groups suites are removed. The broad mirror fixture no longer supplies preview placement/reservation/move assertions. Browser rows retain pre-response replay and real local-workspace reuse; focus intent retains response ordering and exact-leaf sibling ordering. Supplied browser-layout and placement/focus projection alarms are lost; retained evidence is narrower and not equivalent. The test-value audit retires renderer grant-release mock ledgers, direct creation/title/session-field copies, notification/onboarding calls and several placement/per-page projections. Canonical-ID rejection, document reopen/restore and preview URL fences remain; they do not prove close-to-registry revocation or replace every lost alarm. Historical execution evidence is unchanged. The test-value audit removed the direct legacy editor-preview hydration example from tabs-hydration.test.ts. Dropping legacy preview chrome while preserving the same-document ordinary editor no longer has that alarm. The surviving corrupt-id deduplication case does not cover that migration, and retained preview journeys are not equivalent replacement coverage. Historical run counts and migration mutation findings describe the earlier suite. Audit1308: src/renderer/src/components/editor/EditorPanelShell.header.test.tsx removed. Header visibility and mocked editor-surface inventory alarms disappear. No equivalent replacement coverage is claimed. Audit1312 removes low-value imported specs from aggregate test drivers; old whole-driver execution receipts are archived. Retained registrations are unchanged. No equivalent replacement coverage is claimed. Audit1383: src/main/runtime/orca-runtime-tests/worktree-native-chat-setup.spec.ts reduced. Lose command-list/receipt/default/background activation matrices whose provider spawn returns canned IDs. Retained test proves actual tab allocation separation, not terminal startup. No equivalent replacement coverage is claimed. Existing exact unaffected receipts and their platform/provider/budget claims remain scoped to unchanged selectors. Changed optional selectors have no new execution result. Audit1386: src/renderer/src/lib/doc-preview-grants.test.ts reduced. Repeated-mount mint count, ordinary release/remint and failed-mint cache retry standalone alarms removed; no actual grant server/access test. No equivalent replacement coverage is claimed. Audit1401: src/main/runtime/orca-runtime-tests/mobile-session-tabs-duplicate-pty-repair.spec.ts removed. Loses listing-on-already-repaired-data alarm; it never established runtime repair invocation. src/main/runtime/orca-runtime-tests/mobile-session-tabs-split-agent-tab.spec.ts removed. Loses actual mobile split sibling shell versus recorded-agent resume command alarms; no equivalent coverage asserted. src/main/runtime/orca-runtime-tests/worktree-native-chat-setup.spec.ts removed. Loses separate setup/startup tab result in that manually assembled path. src/main/runtime/orca-runtime.test.ts reduced. Driver registration/import of a whole-deleted selected spec is removed; directly authored cases remain unchanged. No extra authored removal credit. No equivalent replacement coverage is claimed. The existing primary-selector receipt and its claims remain scoped to their unchanged sources; the broad optional command has no new execution result. Audit1412: src/main/browser/guest-lifecycle/manager/browser-manager-guest-policy-profile.test.ts removed. Loses workspace-document in-grant navigation/popup refusal, cleanup/re-attach, cross-registry guest authorization and browsing-map separation alarms. No equivalent browser policy coverage is claimed. No equivalent replacement coverage is claimed. Audit1421 removes browser-address-bar-suggestions.test.ts. Document suggestion title/path matching, recency merging and path-prefix ranking alarms are lost. No equivalent replacement coverage is claimed. Historical receipts remain scoped to unchanged selectors; the reduced command has no new execution result. Audit2452 removes the whole fake-effect browser-close routing suite. Active selectors and assertion references no longer select it. Its requested-worktree default close, sibling-page close and explicit missing-page typed reply alarms are intentionally retired without equivalent replacement. Prior dated receipts remain unchanged and scoped to their original sources; no fresh execution of the reduced gate, rendered browser, live remote, mobile or cross-platform validation is claimed.", "motivatingLinks": [ "https://linear.app/stably/issue/STA-5557/remote-html-preview-render-workspace-files-locally-over-the-file", "https://linear.app/stably/issue/STA-3872/unable-to-open-html-files-in-remote-server", @@ -4451,7 +4453,7 @@ "https://github.com/stablyai/orca/pull/13876" ], "invariant": "Opening one HTML preview from a paired client renders the workspace document in exactly one client-local browser tab, located by that document and served over the orca-preview scheme. The client gains exactly that one browser workspace and it is the document one — blank where a URL page carries a URL, named by the document, with the chip naming the file — while the host gains no browser page at all, neither in its own page registry nor in the tab snapshot its clients publish into. The preview occupies its own split without taking focus from the source editor; an explicit click activates it, and closing it removes only the preview. Following a document from a file link is the other half of that switch and does move the reader to it, tab group included, whether the preview is new or already open, because opening a file is a request to look at it. A document tab quit with the client comes back as the same row on a grant the relaunched client mints afresh. A preview is named by the browser page it is open in, not by a namespace of its own, and the page registry has two halves: a workspace-document guest is registered in its own map and is absent from the browsing one entirely. That absence is the fence. Page, session and profile management, agent tab enumeration and command targeting, download routing and certificate attribution all read the browsing map directly, in more places than a per-channel guard could be remembered in, so none of them can name a document page and none of them carries a guard. Browser tools the reader drives (element grab, hover describe, selection capture, the annotation viewport bridge) are the one operation that legitimately spans the halves, and they go through the single authority that reads both, keyed by the page and its hosting renderer. The halves are disjoint in both directions: browsing registration refuses a page the document half already holds, and minting a grant refuses a page the browsing half already holds, so one id can never name a surface in both. The headless backend acts on that refusal by destroying the window it had already opened rather than leaving a policy-less page behind an id nothing can drive, keeping nothing under that id for its own shutdown to hand back. Registration refuses on the same terms when the guest it was asked about is already gone. The exit door is guarded in both its halves: a preview withdraws by revoking its grant and never through the unregister channel, so a page the document half holds arriving there is refused before either the registration teardown or the grab-state disposal beside it, which would otherwise drop the intent an in-flight preview grab compares by identity and leave that grab answering ok without ever arming its guest. A bridge request whose guest does not resolve is refused without tearing down the page it named, so a misaddressed request cannot cancel a healthy page's in-flight downloads and grabs. The annotation viewport bridge resolves its guest when its serialized op actually runs rather than when the request arrived, so a cross-process navigation while it waited cannot leave the bridge installed in a retired guest while the reader looks at a new one. State main keys by a preview's page is disposed when that page's grant is revoked, which is the only signal a preview's surface is gone. A tool asking for a page whose guest has not attached yet waits for that registration and arms when it arrives, rather than answering not-ready at the reader; that wait resolves only the request already naming this page, never the worktree-wide or any-tab waits the CLI and agents use to ask for a browser tab to drive. Handing the previewed document to the reader's own machine routes on the owners its grant was minted against — the file's own connection owner and the worktree's own runtime owner, neither read from the tab's stored fields. Only a document proven to live on this machine reaches the client OS; one with a resolved remote owner is downloaded first; and one whose owner cannot be resolved at all, workspace root included, is refused with a message naming that, because the download route would otherwise read the same absolute path on the client and hand back a same-named local file under the remote document's name. A runtime-owned path that falls outside its worktree root is refused by that route itself and surfaces as a failure toast rather than a download. Nothing the document does writes a file to this machine either: the preview partition denies downloads outright instead of routing them through the browser download flow, which has no page to attribute a preview's bytes to and would otherwise reserve a name in this desktop's Downloads folder and write them there unprompted. That refusal is visible to the reader and invisible to the document: the preview's shell carries a fixed sentence saying downloads are off, published at most once per preview per interval so a document asking in a loop cannot fill Orca's chrome, while the page itself gets back exactly what it got before, which is nothing. The sentence names no file, because the document chooses the name it offers; and a refusal never takes the document away the way an entry document's own failure does, whatever it names. A preview is a browser tab, not an editor tab in a preview mode: it is named the way a browser tab is named — by the document it shows when that document declares a title, and by the file it shows when it does not — while the chip goes on naming the file and the host whatever the document calls itself. A title is refused on the same terms the url is: a document that declares none has Chromium report the grant URL as its title, and that title is stored, mirrored onto the tab and written to disk, so anything carrying the scheme falls back to the file instead. It is created by the preview action as a page located by its document, it carries the workspace-relative path copy the editor's path header owned, and closing it revokes the grant that made the document readable while a URL tab closing beside it revokes nothing. Chrome persisted by builds that made previews editor tabs is dropped on restore rather than coming back naming a surface no restore can produce, and the ordinary editor tab for the same document is left alone. A document tab is held back at the mobile publish boundary — no client holds its grant, and the wire has no tab kind for it — while an ordinary browser tab beside it still publishes. It is held back from the group projection that publishes tab order, recency and group activity as well as from the tab list itself, so no published group names a tab the phone is never sent. A browser page can be located by a workspace document instead of a URL, and the document is the whole of its stored identity. The grant and the orca-preview URL that document is served over are minted when the page mounts and replaced by a hard reload, so neither is ever written to the page's url, mirrored onto its tab, persisted or published: such a page's url is the blank URL from creation through restore, including when a session written elsewhere carries a grant URL in, and what the session carries is the worktree and path a restored page mints afresh against today's owners. Every door onto a page's url holds that line — creation, the title update, and the navigation commit alike — so a report about a document page cannot give it a URL it never had, and the title fallback and the loading affordance follow the url each door actually wrote. The mirror carries the document too, so a tab entry cannot go on naming a document its active page has left. Every guest in the app is policy-attached through one door: a workspace document takes a restricted profile there rather than a separate installer beside it, so the attachment bookkeeping that door owns — what registration refuses, and what teardown frees — covers a preview on the same terms as a browsing page, and a preview takes none of the browsing machinery that door installs. That authority answers from the moment the embedder hands the guest over rather than only after a later navigation: the guest binds to the grant it is already showing, so the tools reach the document the reader opened and not just one they navigated to. A read the host reports as truncated or over-cap is refused rather than served partially, and a document outside the paired worktree is refused with a message naming that boundary instead of a bare read failure. The rendered document reaches nothing off-machine on its own: every served response carries a self-only content security policy, the preview session cancels any request that is not in-document, subframes cannot navigate outside the grant, a guest no document has yet bound to a grant may not navigate at all, the guest gathers no ICE candidates, and an SSH path that canonicalizes outside the grant root is refused before it is read. The one route out is a link the reader presses: a trusted click on an anchor, reported by the preview's own preload from a guest still bound to a live grant, leaves through the configured browser destination and captured source route — and only after the reader confirms the exact destination URL, so a document cannot spend a single stray press exfiltrating what it can read into a link it authored. The preview hands its guest that focus itself whenever it is the surface the reader is in — a browsing page gets it from the chrome around it, and a preview has no chrome to get it from — and it does so only then, so a preview mounted behind a terminal or an editor never takes the keyboard from what the reader is actually in. It offers again when the window itself takes focus back and nothing in the embedder has claimed that focus, because another app coming to the front lands focus on the embedder rather than the guest and the route out would otherwise stay shut until something remounted the pane — while the same window focus also arrives when the reader presses a tab, that being the guest's own blur returning, and taking focus back from there would fight the reader for their own click. Nothing else does. A navigation or popup the document starts by itself is swallowed whatever else is happening, including immediately after a genuine press elsewhere in the document, so a page that can read its grant cannot hand it to a browser tab; a middle click opens nothing; and a fragment link is answered inside the document. A preview attach carries the preview preload and no renderer-supplied one, and no other attach path can acquire it. A subresource the workspace will not send degrades the document to a notice naming that file, never to a failure panel over a page that rendered. A grant outlives neither the tab that owns it nor the renderer document that minted it, and only the trusted renderer can mint or revoke one. For the browser creations this gate still owns, owner-pinned creation returns the canonical host page identity before navigation readiness; delayed navigation cannot turn a created page into an unidentifiable failure or a duplicate retry. Capability rejection before host mutation must preserve the original error, issue no RPC, surface a failure toast, and remove only a caller-declared newly-created empty split. Post-create reconciliation failure requires exact rollback; ambiguous rollback rejects without local fallback.", - "oracle": "Audit2133 current-parent preparation: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2095 current-parent preparation2126: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2090 (sources2088/2089): Current reduced suites intentionally retire the alarms listed in knownGaps; prior dated execution and replacement claims do not validate changed source bytes or selectors. Historical prior account (preserved verbatim): Audit2086 (sources2082/2083): Current reduced suites intentionally retire the alarms listed in knownGaps; prior dated execution and replacement claims do not validate changed source bytes or selectors. Historical prior account (preserved verbatim): Audit2076 (sources2073/2074): Intentionally retire these low-value alarms without replacement: src/main/runtime/orca-runtime-tests/terminal-handles-part-02.spec.ts: recovers a moved SSH pane through the tab it now sits in, not the one its lease froze; reports a plain expired orphan; does not report a pane whose only recent lease was superseded; does not report a pane whose only recent lease had its relay id recycled; picks the eligible orphan over a superseded predecessor that matches first; collects the same tabs the per-tab reader reports, in one sweep of the leases; reports no candidate when no lease names any of the worktree tabs. src/main/runtime/orca-runtime-tests/runtime-availability.spec.ts: starts unavailable with no authoritative window; reports the configured Windows terminal shell on status; reports floating workspace availability from settings on status; polls floating tabs with targeted PTY liveness and no repo/provider inventory; hydrates persisted tabs when the store cannot report repos; advertises browser screencast only when a renderer window is available; routes mobile Codex reset consumption through the account mutation coordinator; maps a definite pre-provider rejection into an authoritative current snapshot. Existing unrelated evidenceRuns retain their original scope; unchanged dated receipt objects do not validate changed source bytes or reduced selectors. No fresh gate execution or historical import-closure equivalence is claimed. Frozen current literal-import graph reaches these changed test bytes; unresolved local and computed edges remain debt, and current reachability is not historical execution or equivalent replacement evidence. Historical prior account (preserved verbatim): Audit2043: Remove the registration-wait, webview-security, preview-authorization, guest-lifecycle and grant-IPC unit suites and the sibling-directory assertion reference. Runtime and browser capability consumers also lose removed imported examples. Remaining document-policy, file-reader and grant-registry assertions do not replace those alarms. Preserve the 27 independent dated receipts at their original unchanged selectors; reduced prospective unit selectors have no fresh result or equivalent replacement claim. Historical prior account (preserved verbatim): Audit1973: Delete the browser RPC forwarding/schema/scalar suite and its prospective selector. Preserve all27 unrelated dated receipts; no actual browser operation alarm is replaced. Whole original changed dated receipts are retired, never narrowed or transferred. This metadata review claims no fresh execution, equivalent replacement, provider/PTY, live remote/mobile, rendered, cross-platform, soak or measured-budget result. Historical prior account (preserved verbatim): Audit1925 (sources1922/1923): The complete terminal-file-link-actions suite is removed. Lose supplied local/remote menu-row inventories, browser label/order/download/reveal callback forwarding, platform-copy and source-group pinning examples. No OS reveal/download, real remote preview or browser navigation was exercised; the other real document-preview sources are not equivalent replacements. Preserve meaningful remaining selectors/assertions and unchanged dated receipts within their original scope. Reduced selectors have no fresh gate execution evidence in this metadata packet. No equivalent replacement, physical watcher/PTY/provider, live SSH/remote/mobile, rendered, cross-platform, soak or measured-budget result is claimed. Historical prior account (preserved verbatim): Audit1709 retires changed-source assertion and execution scope without equivalent replacement coverage. Existing unchanged-selector receipts remain scoped to their original sources. Historical prior-source oracle: Audit1687 retires changed-source assertion and execution scope without equivalent replacement coverage. Existing unchanged-selector receipts remain scoped to their original sources. Historical prior-source oracle: Audit1421 narrows renderer unit coverage as recorded in coverageNotes. Prior oracle descriptions are historical for the changed suite. The retained unchanged-source receipts are the only current execution evidence; reduced-source assertions await validation. Historical oracle account (not a current claim for changed sources): Retained unchanged-selector receipts are the only current execution evidence. Reduced-source assertions await validation. Historical prior oracle: The retained unchanged-source receipts are the only current execution evidence; reduced-source assertions await validation. Historical oracle account (not a current claim for changed sources): In paired Electron, write an HTML fixture that declares its own title on the host, invoke the Explorer preview action on the client, and require the document text to be readable out of the orca-preview guest before judging any absence. With that presence established, require the client to hold exactly one browser workspace more than its baseline and that workspace to be the document one: page and tab url blank, the document path mirrored onto both, the tab named by the document's title, the chip naming the file, no editor row of the retired preview species anywhere, and the guest URL carrying the orca-preview scheme. Ask the host through its own page registry as well as through the tab snapshot, and in the same run open an ordinary URL browser tab from the same client and require that one to arrive in both — the presence precondition without which “the host gained nothing” is satisfied just as well by an oracle that cannot see browser pages at all. Require the preview to sit in a group other than the source editor's while the active group and tab remain the source editor's. Then click away to the terminal, click the preview tab, require it to reactivate and still render, close it with its own X, and require the document tab to be gone while the host still holds only the URL tab and the source group, source editor and terminal survive. Quit the client with a document tab open and relaunch it on the same profile: require the same workspace row to come back, blank and named by the document, rendering the document again over a grant URL that differs from the one that was quit, with no preview-scheme or document-named page anywhere in what the host holds. Prove the halves are live by flipping one product property at a time and requiring the run to fail: publish document workspaces to the host like ordinary ones, and stop mirroring the document onto the workspace row. Have the fixture document attempt its own egress on every load — an unattended window.open and location.href to an off-machine URL, plus an inline ICE gathering probe — and require the same baseline counts and a candidate count of zero, so the document's own attempts are measured rather than assumed. Then, as a separate phase after the close oracle has already run, bring the client window to the front, press the document's heading with a real mouse event, and require the document to report that the same press drove it to attempt a second window.open and location.href while both browser counts stay at that phase's baseline and nothing routes — the case a recent-input gate cannot distinguish from the press's own effect. Only then press the target=_blank link with a real mouse event and require both a recorded routing call that returned success and a browser count above that baseline, with the preview tab still open. Drive the preload's click policy as a unit oracle over a real document: a dispatched click, a trusted press on an external anchor, an anchor reached through what it wraps, an SVG animated href, a sibling preview link, fragment and percent-encoded fragment targets, a bare hash, and a middle click. Create a browser page located by a workspace document, handing creation a live grant URL, and require its stored url, its mirrored tab url and the written session payload all to be blank with no orca-preview string anywhere in what was written, while an ordinary page created the same way keeps the URL it was given and asks for the address bar the document page never does. Parse the written page and tab through the session schema and require the document to survive both halves. Hydrate them back and require the document page to return blank and still named — including when its page row was salvaged away and only the tab's own copy remains, and when a foreign session carried a grant URL into both rows. Drive the mirror across a page switch out of the document and back, and across a repair in which the document is the only mirrored field that differs. Name a document page from its document and require the tab to take that name, name it with an empty title and require the file, name it with a live grant URL and require the file again with no preview scheme anywhere in the written session, and require an ordinary blank browser tab beside it to still be called New Tab. Dispatch a title update out of a rendered preview's own guest and require it to reach the page state while the identity chip still reads the document's workspace-relative path. Attach a browsing guest and a workspace-document guest through the same method in one run and require the browsing one to take clicked-link routing, popup handling and auth-identity detach tracking while the document guest takes none of them, stays inside the grant it is showing, denies every window it asks for, and is dropped from the page-keyed document registry by the same teardown that frees its id for a later attach. Register a browsing guest and attach a workspace-document guest in one run against the real manager, require the one door to answer each page with the guest of its own half, and require the document page to be absent from the browsing map and from its enumeration. Drive both browsing registration entry points with a page the document half already holds and require them to register nothing, and drive the mint channel with a page the browsing half already holds and require it to refuse; and drive the offscreen one with a guest that is missing and with one already destroyed, requiring the same refusal. Arm a grab on a live preview target, drive the unregister channel at that same target in the window before the queued operation runs, and require the grab to reach the guest anyway — then drive the same sequence for an ordinary browser page and require its grab state to be disposed after all. Hold one viewport-bridge op open, queue a second behind it, swap the page's guest while that second op waits, and require the injection to land in the guest the page has then. Revoke a grant after a tool has run against its page and require that page's grab state to be cancelled and disposed. Ask a tool for a document page whose guest has not attached, require the request to park in the registration wait, attach the guest, and require the same request to arm on it; require a page nothing ever renders to answer not-ready once that wait elapses. With a document open, ask a tool for a browsing page id and require it to be answered by the browsing half or not at all, with the same channel reaching the document guest under the page it really renders. Drive open-externally for a document whose per-file owner is remote while the workspace-scoped owner is unresolved, for a runtime-owned worktree whose preview tab carries no runtime id of its own, and for a worktree that resolves no runtime owner while the tab still carries one, requiring the download route in each; and for an owner that cannot be resolved at all, and for an unknown workspace root while nothing names another host, requiring a refusal that neither opens nor downloads. Drive the headless backend with a page the document half already holds and require it to reject, destroy the window, and unregister nothing — then shut the backend down and require it still to have unregistered nothing. Navigate a bound preview guest at a second grant through both latch events and require it to stay on the grant it bound to. Mount a preview while a renderer drag is already in flight and require its guest to be click-through at the moment it is appended, not a turn later. Render the editor panel shell in each remaining tab mode and require the path header exactly where the surface does not already name itself. Drive the preview action and require a browser tab located by the document rather than an editor tab, require a second open of the same document to activate the tab it is already in, and require closing that tab to revoke its grant while a URL tab closed beside it revokes none. Publish a worktree holding a document tab and a URL tab and require only the URL tab to reach the mobile snapshot. Install the shared partition policies for a preview partition and for an ordinary browsing partition in the same run, fire each one's own will-download listener, and require the preview's to cancel while the browsing one still reaches the download router — then require the preview protocol installer to be what asks for that deny. In the same run, require the cancelled download to raise a reader-facing notice and the routed one to raise none. require the guest registry to name the bound grant for a live preview guest and nothing for a contents that is not one, has committed no document, or is gone. Drive the shell with a refusal and require one fixed sentence, still one row after three more refusals, standing beside an asset failure rather than being counted with it, gone behind the failure panel, and ignored when it names another preview's grant. Drive the main-side report gate directly for a sender that is no preview guest, a guest with no bound or a revoked grant, a genuine press Electron's webview focus flag misreports as unfocused, and non-web URLs; drive the reader-facing confirmation for accept, cancel, and a confirmed tab the browser refuses; and drive will-attach-webview in both preload directions. Run the per-owner reader, grant-containment, scheme-admission, guest-policy, and plan-routing contracts as unit oracles, including a host-reported truncation, an over-cap binary, and an out-of-worktree paired path. Drive the reader-facing component with the payloads the reader can actually produce — the entry document fails only as truncated or unreadable, a subresource additionally as a refused format — and require the asset case to leave the guest mounted. Drive the closed-tab cleanup hook, the window installer, and the grant IPC handlers directly, requiring the grant to be released when the preview tab closes, cleared at window creation and on a cross-document main-frame navigation, and refused to any sender that is not the trusted renderer. For the browser creations this gate still owns, run the unchanged contract oracle for direct create and side-preview callers with absent status, unknown capabilities, and a mixed-version host, requiring the original unsupported error or visible toast, zero RPCs, and no retained new split; hold a real navigation response beyond the 15-second client deadline after host creation and require the first RPC to return the exact host inventory page ID, one host page, and no retry; repeat reconciliation faults against headless serve and retain the separate exact-page reconciliation rollback oracle. Audit2452 removes the whole fake-effect browser-close routing suite. Active selectors and assertion references no longer select it. Its requested-worktree default close, sibling-page close and explicit missing-page typed reply alarms are intentionally retired without equivalent replacement. Prior dated receipts remain unchanged and scoped to their original sources; no fresh execution of the reduced gate, rendered browser, live remote, mobile or cross-platform validation is claimed.", + "oracle": "Audit3108: doc-preview-grants.test.ts now retains only the stale grant-rejection race: a late rejected grant request must not evict a newer grant registry entry, witnessed by later revocation of grant-2. Retire stable minted-location and outside-workspace POSIX/Windows request-projection alarms without replacement. The direct nonempty grant selector remains unchanged; no real grant-server/file authorization, SSH/HTML delivery or native-focus result is claimed. Audit2133 current-parent preparation: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2095 current-parent preparation2126: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2090 (sources2088/2089): Current reduced suites intentionally retire the alarms listed in knownGaps; prior dated execution and replacement claims do not validate changed source bytes or selectors. Historical prior account (preserved verbatim): Audit2086 (sources2082/2083): Current reduced suites intentionally retire the alarms listed in knownGaps; prior dated execution and replacement claims do not validate changed source bytes or selectors. Historical prior account (preserved verbatim): Audit2076 (sources2073/2074): Intentionally retire these low-value alarms without replacement: src/main/runtime/orca-runtime-tests/terminal-handles-part-02.spec.ts: recovers a moved SSH pane through the tab it now sits in, not the one its lease froze; reports a plain expired orphan; does not report a pane whose only recent lease was superseded; does not report a pane whose only recent lease had its relay id recycled; picks the eligible orphan over a superseded predecessor that matches first; collects the same tabs the per-tab reader reports, in one sweep of the leases; reports no candidate when no lease names any of the worktree tabs. src/main/runtime/orca-runtime-tests/runtime-availability.spec.ts: starts unavailable with no authoritative window; reports the configured Windows terminal shell on status; reports floating workspace availability from settings on status; polls floating tabs with targeted PTY liveness and no repo/provider inventory; hydrates persisted tabs when the store cannot report repos; advertises browser screencast only when a renderer window is available; routes mobile Codex reset consumption through the account mutation coordinator; maps a definite pre-provider rejection into an authoritative current snapshot. Existing unrelated evidenceRuns retain their original scope; unchanged dated receipt objects do not validate changed source bytes or reduced selectors. No fresh gate execution or historical import-closure equivalence is claimed. Frozen current literal-import graph reaches these changed test bytes; unresolved local and computed edges remain debt, and current reachability is not historical execution or equivalent replacement evidence. Historical prior account (preserved verbatim): Audit2043: Remove the registration-wait, webview-security, preview-authorization, guest-lifecycle and grant-IPC unit suites and the sibling-directory assertion reference. Runtime and browser capability consumers also lose removed imported examples. Remaining document-policy, file-reader and grant-registry assertions do not replace those alarms. Preserve the 27 independent dated receipts at their original unchanged selectors; reduced prospective unit selectors have no fresh result or equivalent replacement claim. Historical prior account (preserved verbatim): Audit1973: Delete the browser RPC forwarding/schema/scalar suite and its prospective selector. Preserve all27 unrelated dated receipts; no actual browser operation alarm is replaced. Whole original changed dated receipts are retired, never narrowed or transferred. This metadata review claims no fresh execution, equivalent replacement, provider/PTY, live remote/mobile, rendered, cross-platform, soak or measured-budget result. Historical prior account (preserved verbatim): Audit1925 (sources1922/1923): The complete terminal-file-link-actions suite is removed. Lose supplied local/remote menu-row inventories, browser label/order/download/reveal callback forwarding, platform-copy and source-group pinning examples. No OS reveal/download, real remote preview or browser navigation was exercised; the other real document-preview sources are not equivalent replacements. Preserve meaningful remaining selectors/assertions and unchanged dated receipts within their original scope. Reduced selectors have no fresh gate execution evidence in this metadata packet. No equivalent replacement, physical watcher/PTY/provider, live SSH/remote/mobile, rendered, cross-platform, soak or measured-budget result is claimed. Historical prior account (preserved verbatim): Audit1709 retires changed-source assertion and execution scope without equivalent replacement coverage. Existing unchanged-selector receipts remain scoped to their original sources. Historical prior-source oracle: Audit1687 retires changed-source assertion and execution scope without equivalent replacement coverage. Existing unchanged-selector receipts remain scoped to their original sources. Historical prior-source oracle: Audit1421 narrows renderer unit coverage as recorded in coverageNotes. Prior oracle descriptions are historical for the changed suite. The retained unchanged-source receipts are the only current execution evidence; reduced-source assertions await validation. Historical oracle account (not a current claim for changed sources): Retained unchanged-selector receipts are the only current execution evidence. Reduced-source assertions await validation. Historical prior oracle: The retained unchanged-source receipts are the only current execution evidence; reduced-source assertions await validation. Historical oracle account (not a current claim for changed sources): In paired Electron, write an HTML fixture that declares its own title on the host, invoke the Explorer preview action on the client, and require the document text to be readable out of the orca-preview guest before judging any absence. With that presence established, require the client to hold exactly one browser workspace more than its baseline and that workspace to be the document one: page and tab url blank, the document path mirrored onto both, the tab named by the document's title, the chip naming the file, no editor row of the retired preview species anywhere, and the guest URL carrying the orca-preview scheme. Ask the host through its own page registry as well as through the tab snapshot, and in the same run open an ordinary URL browser tab from the same client and require that one to arrive in both — the presence precondition without which “the host gained nothing” is satisfied just as well by an oracle that cannot see browser pages at all. Require the preview to sit in a group other than the source editor's while the active group and tab remain the source editor's. Then click away to the terminal, click the preview tab, require it to reactivate and still render, close it with its own X, and require the document tab to be gone while the host still holds only the URL tab and the source group, source editor and terminal survive. Quit the client with a document tab open and relaunch it on the same profile: require the same workspace row to come back, blank and named by the document, rendering the document again over a grant URL that differs from the one that was quit, with no preview-scheme or document-named page anywhere in what the host holds. Prove the halves are live by flipping one product property at a time and requiring the run to fail: publish document workspaces to the host like ordinary ones, and stop mirroring the document onto the workspace row. Have the fixture document attempt its own egress on every load — an unattended window.open and location.href to an off-machine URL, plus an inline ICE gathering probe — and require the same baseline counts and a candidate count of zero, so the document's own attempts are measured rather than assumed. Then, as a separate phase after the close oracle has already run, bring the client window to the front, press the document's heading with a real mouse event, and require the document to report that the same press drove it to attempt a second window.open and location.href while both browser counts stay at that phase's baseline and nothing routes — the case a recent-input gate cannot distinguish from the press's own effect. Only then press the target=_blank link with a real mouse event and require both a recorded routing call that returned success and a browser count above that baseline, with the preview tab still open. Drive the preload's click policy as a unit oracle over a real document: a dispatched click, a trusted press on an external anchor, an anchor reached through what it wraps, an SVG animated href, a sibling preview link, fragment and percent-encoded fragment targets, a bare hash, and a middle click. Create a browser page located by a workspace document, handing creation a live grant URL, and require its stored url, its mirrored tab url and the written session payload all to be blank with no orca-preview string anywhere in what was written, while an ordinary page created the same way keeps the URL it was given and asks for the address bar the document page never does. Parse the written page and tab through the session schema and require the document to survive both halves. Hydrate them back and require the document page to return blank and still named — including when its page row was salvaged away and only the tab's own copy remains, and when a foreign session carried a grant URL into both rows. Drive the mirror across a page switch out of the document and back, and across a repair in which the document is the only mirrored field that differs. Name a document page from its document and require the tab to take that name, name it with an empty title and require the file, name it with a live grant URL and require the file again with no preview scheme anywhere in the written session, and require an ordinary blank browser tab beside it to still be called New Tab. Dispatch a title update out of a rendered preview's own guest and require it to reach the page state while the identity chip still reads the document's workspace-relative path. Attach a browsing guest and a workspace-document guest through the same method in one run and require the browsing one to take clicked-link routing, popup handling and auth-identity detach tracking while the document guest takes none of them, stays inside the grant it is showing, denies every window it asks for, and is dropped from the page-keyed document registry by the same teardown that frees its id for a later attach. Register a browsing guest and attach a workspace-document guest in one run against the real manager, require the one door to answer each page with the guest of its own half, and require the document page to be absent from the browsing map and from its enumeration. Drive both browsing registration entry points with a page the document half already holds and require them to register nothing, and drive the mint channel with a page the browsing half already holds and require it to refuse; and drive the offscreen one with a guest that is missing and with one already destroyed, requiring the same refusal. Arm a grab on a live preview target, drive the unregister channel at that same target in the window before the queued operation runs, and require the grab to reach the guest anyway — then drive the same sequence for an ordinary browser page and require its grab state to be disposed after all. Hold one viewport-bridge op open, queue a second behind it, swap the page's guest while that second op waits, and require the injection to land in the guest the page has then. Revoke a grant after a tool has run against its page and require that page's grab state to be cancelled and disposed. Ask a tool for a document page whose guest has not attached, require the request to park in the registration wait, attach the guest, and require the same request to arm on it; require a page nothing ever renders to answer not-ready once that wait elapses. With a document open, ask a tool for a browsing page id and require it to be answered by the browsing half or not at all, with the same channel reaching the document guest under the page it really renders. Drive open-externally for a document whose per-file owner is remote while the workspace-scoped owner is unresolved, for a runtime-owned worktree whose preview tab carries no runtime id of its own, and for a worktree that resolves no runtime owner while the tab still carries one, requiring the download route in each; and for an owner that cannot be resolved at all, and for an unknown workspace root while nothing names another host, requiring a refusal that neither opens nor downloads. Drive the headless backend with a page the document half already holds and require it to reject, destroy the window, and unregister nothing — then shut the backend down and require it still to have unregistered nothing. Navigate a bound preview guest at a second grant through both latch events and require it to stay on the grant it bound to. Mount a preview while a renderer drag is already in flight and require its guest to be click-through at the moment it is appended, not a turn later. Render the editor panel shell in each remaining tab mode and require the path header exactly where the surface does not already name itself. Drive the preview action and require a browser tab located by the document rather than an editor tab, require a second open of the same document to activate the tab it is already in, and require closing that tab to revoke its grant while a URL tab closed beside it revokes none. Publish a worktree holding a document tab and a URL tab and require only the URL tab to reach the mobile snapshot. Install the shared partition policies for a preview partition and for an ordinary browsing partition in the same run, fire each one's own will-download listener, and require the preview's to cancel while the browsing one still reaches the download router — then require the preview protocol installer to be what asks for that deny. In the same run, require the cancelled download to raise a reader-facing notice and the routed one to raise none. require the guest registry to name the bound grant for a live preview guest and nothing for a contents that is not one, has committed no document, or is gone. Drive the shell with a refusal and require one fixed sentence, still one row after three more refusals, standing beside an asset failure rather than being counted with it, gone behind the failure panel, and ignored when it names another preview's grant. Drive the main-side report gate directly for a sender that is no preview guest, a guest with no bound or a revoked grant, a genuine press Electron's webview focus flag misreports as unfocused, and non-web URLs; drive the reader-facing confirmation for accept, cancel, and a confirmed tab the browser refuses; and drive will-attach-webview in both preload directions. Run the per-owner reader, grant-containment, scheme-admission, guest-policy, and plan-routing contracts as unit oracles, including a host-reported truncation, an over-cap binary, and an out-of-worktree paired path. Drive the reader-facing component with the payloads the reader can actually produce — the entry document fails only as truncated or unreadable, a subresource additionally as a refused format — and require the asset case to leave the guest mounted. Drive the closed-tab cleanup hook, the window installer, and the grant IPC handlers directly, requiring the grant to be released when the preview tab closes, cleared at window creation and on a cross-document main-frame navigation, and refused to any sender that is not the trusted renderer. For the browser creations this gate still owns, run the unchanged contract oracle for direct create and side-preview callers with absent status, unknown capabilities, and a mixed-version host, requiring the original unsupported error or visible toast, zero RPCs, and no retained new split; hold a real navigation response beyond the 15-second client deadline after host creation and require the first RPC to return the exact host inventory page ID, one host page, and no retry; repeat reconciliation faults against headless serve and retain the separate exact-page reconciliation rollback oracle. Audit2452 removes the whole fake-effect browser-close routing suite. Active selectors and assertion references no longer select it. Its requested-worktree default close, sibling-page close and explicit missing-page typed reply alarms are intentionally retired without equivalent replacement. Prior dated receipts remain unchanged and scoped to their original sources; no fresh execution of the reduced gate, rendered browser, live remote, mobile or cross-platform validation is claimed.", "commands": [ "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/browser/client-pages/orca-runtime-browser.test.ts src/renderer/src/runtime/web-session-tabs-sync/navigation/web-session-tabs-sync-focus-intent.test.ts src/renderer/src/runtime/web-session-tabs-sync/placement/web-session-tabs-sync-browser-tabs.test.ts", "pnpm exec vitest run --config config/vitest.config.ts src/main/browser/document-preview/doc-preview-grant-registry.test.ts src/main/browser/document-preview/doc-preview-file-reader.test.ts src/main/browser/document-preview/doc-preview-guest-policy.test.ts src/main/browser/document-preview/doc-preview-failure-notice.test.ts src/shared/document-preview/doc-preview-scheme.test.ts src/renderer/src/lib/browser/documents/doc-preview-grants.test.ts src/renderer/src/components/browser-pane/workspace-doc/doc-preview-external-link-confirmation.test.ts", @@ -4952,7 +4954,8 @@ "D01 candidate audit retires this complete dated receipt because a named test source changes in the candidate overlay. It supplies no new execution or equivalent-coverage claim. Original receipt verbatim:\n{\n \"date\": \"2026-08-27\",\n \"runner\": \"local\",\n \"platform\": \"macos\",\n \"command\": \"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.failure-message.test.tsx\",\n \"result\": \"failed\",\n \"durationSeconds\": 1.27,\n \"summary\": \"Candidate with the sole delta that every failure sets the blocking reason, asset or not, failed 4/14 on the asset-notice assertions — the same mutant the retired 2026-08-26 record measured at 3/10, re-run at the relocated path. A refused subresource must name itself over a document that stays mounted; only the entry document may take the page away.\",\n \"currentCommand\": \"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.failure-message.test.tsx\"\n }", "D01 candidate audit retires this complete dated receipt because a named test source changes in the candidate overlay. It supplies no new execution or equivalent-coverage claim. Original receipt verbatim:\n{\n \"date\": \"2026-08-27\",\n \"runner\": \"local\",\n \"platform\": \"macos\",\n \"command\": \"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.failure-message.test.tsx\",\n \"result\": \"passed\",\n \"durationSeconds\": 1.54,\n \"summary\": \"14/14 on the reader-facing failure suite at its relocated path. This and the red run below replace the 2026-08-26 pair recorded against the pre-relocation path (10/10 green, 3/10 red), re-measured today so the entry/subresource split keeps a red half rather than only a green one.\",\n \"currentCommand\": \"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.failure-message.test.tsx\"\n }", "D01 final candidate whole-registration policy removes retired module selectors and obsolete assertion descriptions prospectively. Existing unaffected receipts retain their original scope; reduced selectors have no new execution or equivalent replacement claim. Retired named modules: [\"src/preload/doc-preview-link-interception.test.ts\",\"src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.failure-message.test.tsx\"]. Retired compound assertion refs, preserved as historical data: [{\"file\":\"src/renderer/src/components/browser-pane/workspace-doc/doc-preview-external-link-confirmation.test.ts\",\"assertions\":[\"an external preview link shows the reader its exact destination and becomes a browser tab only after confirmation\",\"a declined confirmation opens nothing, and a confirmed tab that is refused or fails outright surfaces instead of being dropped\"]}]", - "Audit2452 removes the whole fake-effect browser-close routing suite. Active selectors and assertion references no longer select it. Its requested-worktree default close, sibling-page close and explicit missing-page typed reply alarms are intentionally retired without equivalent replacement. Prior dated receipts remain unchanged and scoped to their original sources; no fresh execution of the reduced gate, rendered browser, live remote, mobile or cross-platform validation is claimed. Retired assertion reference: {\"file\":\"src/renderer/src/hooks/useIpcEvents-close-routing-browser-pages.test.ts\",\"assertions\":[\"closing an absent renderer page returns browser_tab_not_found instead of an untyped failure\"]}" + "Audit2452 removes the whole fake-effect browser-close routing suite. Active selectors and assertion references no longer select it. Its requested-worktree default close, sibling-page close and explicit missing-page typed reply alarms are intentionally retired without equivalent replacement. Prior dated receipts remain unchanged and scoped to their original sources; no fresh execution of the reduced gate, rendered browser, live remote, mobile or cross-platform validation is claimed. Retired assertion reference: {\"file\":\"src/renderer/src/hooks/useIpcEvents-close-routing-browser-pages.test.ts\",\"assertions\":[\"closing an absent renderer page returns browser_tab_not_found instead of an untyped failure\"]}", + "Audit3108: doc-preview-grants.test.ts now retains only the stale grant-rejection race: a late rejected grant request must not evict a newer grant registry entry, witnessed by later revocation of grant-2. Retire stable minted-location and outside-workspace POSIX/Windows request-projection alarms without replacement. The direct nonempty grant selector remains unchanged; no real grant-server/file authorization, SSH/HTML delivery or native-focus result is claimed." ], "demotionRule": "Demote if a paired HTML preview creates a browser workspace on either side, renders from anything other than the orca-preview scheme, serves a read the host reported as truncated or over-cap, serves a document without the self-only content security policy, lets the preview session carry an off-machine request, reads an SSH path that canonicalizes outside its grant root, steals focus from the source editor when it opens, removes unrelated editor or terminal state on close, hides a document that rendered because one subresource was refused, leaves a grant readable after the tab or renderer that owns it is gone, mints one for a sender that is not the trusted renderer, opens a browser tab for a navigation the document started with no one at the keyboard, or lets the guest gather an ICE candidate; or if a paired browser creation this gate still owns produces duplicate client or host identities or loses its requested split." }, @@ -6089,6 +6092,7 @@ "Keep missing-proof mixed-version behavior identical to the incumbent client path." ], "knownGaps": [ + "Audit3077 deletes the Kitty restore/reset integration suite and its registrar. The input-and-repaint driver remains populated; the retired emission/mirror/host-request alarms have no equivalent replacement.", "Audit3061 retires the park-model supplied snapshot suite and nominal mode, cursor, focus, geometry and remote resume metadata variants. Their distinct alarms are intentionally lost; six retained mode/remote cases preserve selected tail-order, authority and parse-readiness boundaries without equivalent replacement. Existing selectors and dated evidence retain their original scope.", "Audit3053 reduces hidden-replay coverage by retiring nominal overlap, resize, cursor metadata and standalone stalled-restore variants. Timeout, overflow, identity/generation fences and retained live-output coordination remain; the removed alarms are intentionally lost without equivalent replacement. Existing commands, assertion references and dated evidence retain their original scope.", "Audit3037 removes seven supplied-input baseline cases from the terminal-input-gating registrar. Input-and-repaint wrapper selectors and unrelated assertions remain; historical receipts do not validate the reduced source bytes. Pane authority, acknowledgment refusal and quarantine recovery cases remain, without native or RPC execution claims.", @@ -6775,7 +6779,7 @@ "id": "terminal-session.startup-cwd-missing-dir-recovery", "title": "Terminal working-directory recovery stays scoped and provider-safe", "maturity": "experimental", - "protection": "partial", + "protection": "none", "owner": "terminal-runtime", "layer": "shared-main-renderer-and-posix-shell-contract", "surfaces": [ @@ -6787,9 +6791,9 @@ ], "platforms": ["macos", "linux", "windows", "mobile"], "providers": ["local", "daemon", "ssh", "wsl", "remote-runtime"], - "coveredPlatforms": ["linux"], + "coveredPlatforms": [], "coveredProviders": [], - "coverageNotes": "Audit2133 current-parent preparation: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit1760 removes the standalone startup-cwd policy suite without equivalent replacement. One fresh Linux run passed21 retained IPC transport unit cases; cwd-specific coverage returns mocked fallback metadata only. Original receipts and scope are archived in knownGaps; current platform scope reflects this unit run, not a native provider. Historical coverage account (not current evidence): Audit1683 removes low-value test projections, mock wiring, private-state assertions, inventories and duplicate examples without equivalent replacement coverage. Per-source reviews record retained public behavior and refusal boundaries with injected/native/remote limits. Affected historical receipts are retired; gates without active current evidence are downgraded. Historical scope before this audit: Audit1640 removes low-value IPC, SSH, workspace and transcript-watcher tests without equivalent replacement coverage. Per-source reviews record retained public refusal, cancellation, execution-host and real-filesystem outcomes with injected/native/remote limits. Affected historical receipts are retired; gates without active current evidence are downgraded. Historical scope before this audit: Audit1396 changes referenced sources. Retained unchanged-source receipts apply only to their unchanged commands; the reduced selectors have no new execution result. Historical coverage account (not a current claim for changed sources): Local macOS evidence covers the shared missing-dir fallback policy, main pty:spawn recovery and metadata, no-flag and reattach strictness, renderer IPC flag routing, SSH-tagged and remote-runtime omission, and the visibility-gated terminal notice. Real Bash and Zsh node-pty tests replace the active cwd inode and exercise the shared POSIX OMP wrapper; audit829 removes generated-file snapshots and their current byte-parity claim. No equivalent replacement for generated-script byte checks is claimed. Daemon shares the same pre-provider main cwd decision but lacks a live daemon-provider run; WSL UNC paths are exempt from the spawn probe and lack a live run; native PowerShell is unaffected; Linux/Windows and mobile/API strictness remain gaps. Audit1396: src/renderer/src/components/terminal-pane/pty-connection-pending-spawn.spec.ts reduced. Lose standalone debug-state retention/log silence alarm. React StrictMode is not mounted; bindings manually simulate remount over mocked transport. No equivalent replacement coverage is claimed.", + "coverageNotes": "Audit3077 retires the sole active passed IPC receipt; no current execution evidence remains for this gate. Historical account (not current coverage): Audit2133 current-parent preparation: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit1760 removes the standalone startup-cwd policy suite without equivalent replacement. One fresh Linux run passed21 retained IPC transport unit cases; cwd-specific coverage returns mocked fallback metadata only. Original receipts and scope are archived in knownGaps; current platform scope reflects this unit run, not a native provider. Historical coverage account (not current evidence): Audit1683 removes low-value test projections, mock wiring, private-state assertions, inventories and duplicate examples without equivalent replacement coverage. Per-source reviews record retained public behavior and refusal boundaries with injected/native/remote limits. Affected historical receipts are retired; gates without active current evidence are downgraded. Historical scope before this audit: Audit1640 removes low-value IPC, SSH, workspace and transcript-watcher tests without equivalent replacement coverage. Per-source reviews record retained public refusal, cancellation, execution-host and real-filesystem outcomes with injected/native/remote limits. Affected historical receipts are retired; gates without active current evidence are downgraded. Historical scope before this audit: Audit1396 changes referenced sources. Retained unchanged-source receipts apply only to their unchanged commands; the reduced selectors have no new execution result. Historical coverage account (not a current claim for changed sources): Local macOS evidence covers the shared missing-dir fallback policy, main pty:spawn recovery and metadata, no-flag and reattach strictness, renderer IPC flag routing, SSH-tagged and remote-runtime omission, and the visibility-gated terminal notice. Real Bash and Zsh node-pty tests replace the active cwd inode and exercise the shared POSIX OMP wrapper; audit829 removes generated-file snapshots and their current byte-parity claim. No equivalent replacement for generated-script byte checks is claimed. Daemon shares the same pre-provider main cwd decision but lacks a live daemon-provider run; WSL UNC paths are exempt from the spawn probe and lack a live run; native PowerShell is unaffected; Linux/Windows and mobile/API strictness remain gaps. Audit1396: src/renderer/src/components/terminal-pane/pty-connection-pending-spawn.spec.ts reduced. Lose standalone debug-state retention/log silence alarm. React StrictMode is not mounted; bindings manually simulate remount over mocked transport. No equivalent replacement coverage is claimed.", "motivatingLinks": [ "https://github.com/stablyai/orca/issues/7239", "https://github.com/stablyai/orca/pull/7750", @@ -6799,49 +6803,17 @@ ], "invariant": "A fresh local renderer terminal spawn may recover from a saved startup cwd whose directory no longer exists only by spawning at the selected workspace root and printing a generic in-terminal notice; existing directories — including ones outside the worktree (#7685) — spawn as requested, and reattach, SSH, remote-runtime, runtime/API, and mobile callers keep exact cwd semantics. A long-lived POSIX shell whose cwd inode was deleted and replaced may run a wrapped OMP command from the live path named by its logical PWD only in a subshell; the parent shell cwd, OMP argv, status extension, and exit status remain unchanged, while a genuinely unavailable path fails visibly without invoking OMP. A usable physical cwd remains authoritative when PWD is unset or names a removed logical symlink instead of being remapped to a workspace fallback.", "oracle": "Audit2133 current-parent preparation: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit1683 retires changed-source assertion and execution scope without equivalent replacement coverage. Existing unchanged-selector receipts remain scoped to their original sources. Historical prior-source oracle: The retained unchanged-source receipts are the only current execution evidence; reduced-source assertions await validation. Historical oracle account (not a current claim for changed sources): The shared resolver falls back to the workspace root only when the injected existence probe reports the resolved cwd missing and the workspace root present, and never probes floating terminals or a cwd equal to the root. The renderer sends cwdFallback only for fresh local IPC spawns, main honors it only when connectionId and sessionId are absent, WSL UNC paths never engage the probe-based fallback, main returns fallback metadata only after an actual fallback, the IPC transport preserves that metadata, and the connection layer writes a generic notice that omits the missing path. In real interactive Bash and Zsh PTYs, unset PWD and remove a logical symlink while the physical cwd stays usable, requiring OMP to remain there; then delete and recreate the active project path, require extension-enabled and extension-skipping invocations to observe the replacement inode with byte-exact argv and nonzero status preserved, require the parent shell to remain on the stale inode, unset PWD and require recovery through ORCA_WORKTREE_PATH, clear every logical fallback and require an actionable failure without invoking OMP, and finally delete the replacement and require the same for the missing PWD path.", - "commands": [ - "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/pty/pty-spawn-cwd-fallback.test.ts", - "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/pty-transport/ipc/pty-transport-connect-spawn.test.ts --maxWorkers=2", - "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/pty-connection/tests/pty-connection-spawn-and-startup.test.ts", - "pnpm exec vitest run --config config/vitest.config.ts src/main/pty/shell-startup/omp-shell-wrapper.node-pty.test.ts src/main/pty/shell-startup/omp-shell-wrapper-alias-safety.test.ts" - ], - "testFiles": [ - "src/main/ipc/pty/pty-spawn-cwd-fallback.test.ts", - "src/renderer/src/components/terminal-pane/pty-transport/ipc/pty-transport-connect-spawn.test.ts", - "src/renderer/src/components/terminal-pane/pty-connection/tests/pty-connection-spawn-and-startup.test.ts", - "src/main/pty/shell-startup/omp-shell-wrapper.node-pty.test.ts", - "src/main/pty/shell-startup/omp-shell-wrapper-alias-safety.test.ts" - ], - "assertionRefs": [ - { - "file": "src/renderer/src/components/terminal-pane/pty-transport/ipc/pty-transport-connect-spawn.test.ts", - "assertions": [ - "IPC transport returns startup cwd fallback metadata to the connection layer" - ] - }, - { - "file": "src/renderer/src/components/terminal-pane/pty-connection/tests/pty-connection-spawn-and-startup.test.ts", - "assertions": ["fresh local IPC worktree spawns are marked with cwdFallback worktree"] - } - ], - "evidenceRuns": [ - { - "date": "2026-11-07", - "runner": "local", - "platform": "linux", - "command": "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/pty-transport/ipc/pty-transport-connect-spawn.test.ts --maxWorkers=2", - "result": "passed", - "durationSeconds": 38.8, - "summary": "21 retained IPC transport unit cases passed on Linux, including return of mocked startupCwdFallback metadata. No live PTY, real missing directory, recovery notice, native shell, SSH/WSL, phone, p95 or soak evidence. Executed via pinned Node24 Vitest entry, same root config and selected file; no actual provider coverage claimed." - } - ], + "commands": [], + "testFiles": [], + "assertionRefs": [], + "evidenceRuns": [], "runtimeBudget": { "p95Seconds": 40, "scope": "Historical pre-audit selector budget; changed selectors have no new runtime measurement. Unchanged receipts remain scoped to their own commands." }, "flakeHistory": { "status": "unknown", - "evidence": "Audit1760: one Linux IPC transport unit run passed21 cases; no current soak evidence. Original flake history archived in knownGaps." + "evidence": "Audit3077: no current execution or soak result remains after retiring the reduced IPC source receipt. Historical account: Audit1760: one Linux IPC transport unit run passed21 cases; no current soak evidence. Original flake history archived in knownGaps." }, "redGreenEvidence": { "status": "missing", @@ -6858,6 +6830,9 @@ "Add WSL/mobile/API spawn provider-contract coverage or explicitly narrow their risk scope." ], "knownGaps": [ + "Audit3077 archives the remaining startup-CWD selectors and aliases because protection none permits no active selectors. These unchanged sources are unvalidated candidates, not current execution evidence. The worktree-marker alias remains mapping debt; this metadata retirement adds no test-removal credit. Original candidate fields verbatim:\ncommands: [\n \"pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/pty/pty-spawn-cwd-fallback.test.ts\",\n \"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/pty-connection/tests/pty-connection-spawn-and-startup.test.ts\",\n \"pnpm exec vitest run --config config/vitest.config.ts src/main/pty/shell-startup/omp-shell-wrapper.node-pty.test.ts src/main/pty/shell-startup/omp-shell-wrapper-alias-safety.test.ts\"\n ]\ntestFiles: [\n \"src/main/ipc/pty/pty-spawn-cwd-fallback.test.ts\",\n \"src/renderer/src/components/terminal-pane/pty-connection/tests/pty-connection-spawn-and-startup.test.ts\",\n \"src/main/pty/shell-startup/omp-shell-wrapper.node-pty.test.ts\",\n \"src/main/pty/shell-startup/omp-shell-wrapper-alias-safety.test.ts\"\n ]\nassertionRefs: [\n {\n \"file\": \"src/renderer/src/components/terminal-pane/pty-connection/tests/pty-connection-spawn-and-startup.test.ts\",\n \"assertions\": [\"fresh local IPC worktree spawns are marked with cwdFallback worktree\"]\n }\n ]", + "Audit3077 retires the direct IPC startup-CWD metadata example, selector and alias. Other startup-CWD selectors and the existing worktree-marker mapping debt remain; no equivalent replacement is claimed.", + "Audit3077 retires this complete dated receipt because its selected source was reduced. It does not validate the reduced selector. Original receipt verbatim:\n{\n \"date\": \"2026-11-07\",\n \"runner\": \"local\",\n \"platform\": \"linux\",\n \"command\": \"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/pty-transport/ipc/pty-transport-connect-spawn.test.ts --maxWorkers=2\",\n \"result\": \"passed\",\n \"durationSeconds\": 38.8,\n \"summary\": \"21 retained IPC transport unit cases passed on Linux, including return of mocked startupCwdFallback metadata. No live PTY, real missing directory, recovery notice, native shell, SSH/WSL, phone, p95 or soak evidence. Executed via pinned Node24 Vitest entry, same root config and selected file; no actual provider coverage claimed.\"\n }", "Audit3069 retires nominal spawn/resume/banner wiring, first-fit and lookup-budget variants, private pending-map checks, the split-CWD disposal suite and startup command/draft baseline variants. Distinct regression alarms are intentionally lost without equivalent replacement; retained ownership, cancellation and recovery cases remain. Existing selectors and dated evidence retain their original scope. The earlier generic-notice alias was already retired by audit3061; the remaining cwdFallback marker alias is unchanged mapping debt.", "Audit3061 retires the park-model supplied snapshot suite and nominal mode, cursor, focus, geometry and remote resume metadata variants. Their distinct alarms are intentionally lost; six retained mode/remote cases preserve selected tail-order, authority and parse-readiness boundaries without equivalent replacement. Existing selectors and dated evidence retain their original scope. The generic startup-cwd notice alias was already stale and is removed as pre-existing metadata debt; this batch receives no new removal credit for that alias. The separate cwdFallback marker alias remains outside this bounded reconciliation.", "Audit3037 retires the runtime-owner spawn-routing suite and its remote cwdFallback-absence assertion claim. Actual routing-composition alarms are intentionally lost; no replacement or renewed execution credit. Other historical claims and receipts retain their original scope.", @@ -8588,13 +8563,13 @@ "providers": ["paired-runtime", "ssh"], "coveredPlatforms": ["macos", "linux"], "coveredProviders": [], - "coverageNotes": "Audit2432: The broad unit command loses the isolated BranchPicker open-change notification sample. Its unique true/false callback alarm is intentionally retired; reopening/status-refresh behavior was not exercised. No equivalent replacement or fresh validation of the reduced selector is claimed. Audit2425: The broad unit command loses the workspace-copy-status supplied readError forwarding sample. It did not exercise the branch-switch blocking promised by its title. This alarm is intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2417: The broad unit command loses two ordinary clean Git cherry-pick/revert success samples and two terminal startup foreground-read work-budget samples. These unique alarms are intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2412: The broad unit command loses the whole six-case fake-React-hook scroll-anchor suite, two generic draft-retention samples, a simple new-agent order sample, a real history-publication batching-count sample, an unmounted handoff abort/timer sample and three basic WSL preview/error samples. These alarms are intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2404: The broad unit command loses the standalone share-name lookup 503 propagation suite, initial deletion placement-kind sample, direct changed-mtime stale sample, first-pass rollback existence example and abandoned-preparation directory sample. These exact alarms are intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2397 review amendment: The broad unit command loses three standalone suites, two partial-case definitions and two URL table rows across package/download/placement tests. Keep initial wrong-port authorization, modified independent-copy digest refusal and abort-after-begin cancellation dispatch. The remaining alarms are deliberately retired; no equivalent complete replacement or fresh validation of the reduced selector is claimed. Audit2393 current-parent refresh2401: Remove the basic skill-install service alias sample and original public managed-remove receiptless refusal case. Preserve the upstream request ownership suite and transaction interruption coverage. No equivalent complete replacement or fresh validation of the reduced selector is claimed. Audit2389: The broad unit command loses the missing-preview suite, plain-global destination sample and prepublished-receipt recovery example. No equivalent replacement or fresh validation of the reduced selector is claimed. Audit2385: The broad unit command loses four authored walker/staging/protocol examples (six expanded cases); no equivalent replacement or fresh validation is claimed for the reduced suite. Audit2383: The unscoped Vitest command no longer includes the removed Antigravity catalogue suite; its exact provider-root/owner example has no equivalent replacement or fresh validation claim. Audit2377/2381: The unscoped Vitest command includes the reduced bundled-skill and native-discovery suites; the eight removed backup/legacy/catalogue/symlink/SSH-builder examples have no replacement-equivalence or fresh validation claim here. Audit2162: Reduced inventory-authority source intentionally loses the examples recorded in knownGaps; historical execution is not validation of changed bytes. Historical prior account (preserved verbatim): Audit2133 current-parent preparation: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2095 current-parent preparation2126: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2090 (sources2088/2089): Current reduced suites intentionally retire the alarms listed in knownGaps; prior dated execution and replacement claims do not validate changed source bytes or selectors. Historical prior account (preserved verbatim): Audit2086 (sources2082/2083): Current reduced suites intentionally retire the alarms listed in knownGaps; prior dated execution and replacement claims do not validate changed source bytes or selectors. Historical prior account (preserved verbatim): Audit2076 (sources2073/2074): Intentionally retire these low-value alarms without replacement: src/main/runtime/orca-runtime-tests/terminal-handles-part-02.spec.ts: recovers a moved SSH pane through the tab it now sits in, not the one its lease froze; reports a plain expired orphan; does not report a pane whose only recent lease was superseded; does not report a pane whose only recent lease had its relay id recycled; picks the eligible orphan over a superseded predecessor that matches first; collects the same tabs the per-tab reader reports, in one sweep of the leases; reports no candidate when no lease names any of the worktree tabs. src/main/runtime/orca-runtime-tests/runtime-availability.spec.ts: starts unavailable with no authoritative window; reports the configured Windows terminal shell on status; reports floating workspace availability from settings on status; polls floating tabs with targeted PTY liveness and no repo/provider inventory; hydrates persisted tabs when the store cannot report repos; advertises browser screencast only when a renderer window is available; routes mobile Codex reset consumption through the account mutation coordinator; maps a definite pre-provider rejection into an authoritative current snapshot. Existing unrelated evidenceRuns retain their original scope; unchanged dated receipt objects do not validate changed source bytes or reduced selectors. No fresh gate execution or historical import-closure equivalence is claimed. Frozen current literal-import graph reaches these changed test bytes; unresolved local and computed edges remain debt, and current reachability is not historical execution or equivalent replacement evidence. Historical prior account (preserved verbatim): Audit2003: Remove SSH/absent-inventory attach refusal, later-subscriber retry, no-view adoption/takeback, provider-tail/unknown-read projections, failed-keystroke retry and simpler loss reattach examples. Keep the existing-subscriber inventory-ready retry alias only at its retained callback, plus retained held-input cancellation/replacement and overlapping attach/loss fences. The inventory driver also loses the whole empty-worktree-publication registration and folder-host/unverifiable scalar examples. Retire the entire dated10-case attach receipt; retain16 independent receipts only in their original scopes. No retained-memory or real-daemon measurement follows from supplied provider inventories. Changed-source prior results do not validate reduced-source bytes. No fresh execution, equivalent replacement, red/green, measured budget, soak, physical provider/PTY, rendered, live remote/mobile or cross-platform result is claimed. Historical prior account (preserved verbatim): Audit1991: Remove initial-snapshot sequence-domain, limited-tail/truncation and unknown-output-sequence aliases whose callbacks are removed. The source retains partial-chunk replay and requested-snapshot retry/admission at supplied snapshot/data seams, not equivalent replacement or measured retained-memory proof. Preserve the existing stream/pending-wait capacity aliases only to their retained owning multiplex callbacks with supplied PTY resolutions. Removing the sibling-PTY heartbeat dedup work-count sample does not establish a new decorative fanout budget; the unrelated-PTY cancellation fence survives. Preserve17 unrelated dated receipts. Changed-source prior results do not validate reduced-source bytes. No fresh execution, equivalent replacement, red/green, measured budget, soak, physical provider/PTY, rendered, live remote/mobile or cross-platform result is claimed. Historical prior account (preserved verbatim): Audit1983: Remove the reasonless-close selector and narrow census aliases to actual retained runtime and injected-inventory callbacks. The removed synthetic census-dedup and isolated pre-boundary timing cases receive no replacement credit; preserve17 unrelated dated receipts. Changed-source prior results do not validate reduced-source bytes. No fresh execution, equivalent replacement, red/green, measured budget, soak, provider/PTY, rendered, live remote/mobile or cross-platform result is claimed. Historical prior account (preserved verbatim): Audit1939 (sources1934/1935): Delete the synthetic watcher-synchronization and partial-reconciliation suites. Retire all affected host-fact, capability-owner, activation-residue and retention-cap aliases because the remaining callbacks do not establish those full claims. Keep exact local hook/timer, byte-stream side effects, exit/disposal, scroll-intent cleanup and deterministic local rank/TTL progression callbacks within their mocked-host scope. No physical paired-host retention, resource budget or equivalent replacement evidence is assigned. The generic broad test command remains a runnable discovery command, never a dated execution receipt. Preserve meaningful remaining selectors/assertions and unchanged dated receipts within their original scope. Reduced selectors have no fresh gate execution evidence in this metadata packet. No equivalent replacement, physical watcher/PTY/provider, live SSH/remote/mobile, rendered, cross-platform, soak or measured-budget result is claimed. Historical prior account (preserved verbatim): Audit1931 (sources1928/1929): Delete the complete hook-agent-status projection suite and its two behavioral assertion aliases. Supplied stored hook/title/resume/staleness/rename/question/generation/PTY-retirement/snapshot-version projections are intentionally lost. The separate osc-status-scan gate mentions this source only in existing historical notes and receives no mutation. No measured hidden-output retention, CPU/RSS or resource budget result is claimed by the deleted source or this audit. Preserve meaningful remaining selectors/assertions and unchanged dated receipts within their original scope. Reduced selectors have no fresh gate execution evidence in this metadata packet. No equivalent replacement, physical watcher/PTY/provider, live SSH/remote/mobile, rendered, cross-platform, soak or measured-budget result is claimed. Historical prior account (preserved verbatim): Module ownership cleanup retires the modeled cold-park private watcher/render/work-count suites under the current test policy. Their assertions are no longer active evidence. Retained lifecycle tests and headed/headless journeys do not establish equivalent coverage for those removed harness alarms. Audit1640 removes low-value IPC, SSH, workspace and transcript-watcher tests without equivalent replacement coverage. Per-source reviews record retained public refusal, cancellation, execution-host and real-filesystem outcomes with injected/native/remote limits. Affected historical receipts are retired; gates without active current evidence are downgraded. Historical scope before this audit: Audit1634 removes low-value IPC, filesystem and runtime projections, argument inventories and duplicate examples without equivalent replacement coverage. Per-source reviews record retained cancellation, ownership, authorization, filesystem and SQLite boundaries with injected/native/remote limits. Affected historical receipts are retired; gates without active current evidence are downgraded. Historical scope before this audit: Audit1627 removes low-value mocked daemon projections, scalar tables and duplicate terminal examples without equivalent replacement coverage. Per-source reviews record retained terminal content preservation, process ownership refusal and recovery boundaries together with injected/native/remote limits. Affected historical receipts are retired; gates without active current evidence are downgraded. Historical scope before this audit: Retained exact unchanged-source receipts remain the only current execution evidence. Reduced selectors have no fresh execution result. Audit1417 intentionally deletes src/renderer/src/components/terminal-pane/pty-connection-hidden-tui-snapshot-replay.spec.ts: Loses rich hidden snapshot sequence replay/grid resize and forced repaint after hidden snapshot alarms, including live-trigger deduplication. deletes src/renderer/src/components/terminal-pane/terminal-cold-park-verdict-loop.test.tsx: Loses modeled React oscillation alarm; real pane/watchers are not exercised. deletes src/renderer/src/runtime/remote-runtime-terminal-parse-backpressure.test.ts: Loses delayed credit until parsing, malformed frame/unknown stream handling, discarded transformed output recovery and failed ACK rearm alarms. No equivalent replacement coverage or fresh result is claimed. Historical coverage account: Audit1412 changes referenced sources. Retained unchanged-source receipts apply only to their unchanged commands; the reduced selectors have no new execution result. Historical coverage account (not a current claim for changed sources): This audit removes the browser response-after-manual-disconnect example. Its distinct in-flight response fencing alarm is lost; retained queued-call disconnect tests are not an equivalent replacement. Historical commands and results remain historical. Deterministic headed macOS runs launch an isolated Orca desktop server and a separate paired web client. A byte-identical headless run uses an isolated `orca serve` host. Both create six real paired host PTYs with bounded high-output scrollback, prove sustained output while all six client xterms are warm-mounted but hidden causes zero renderer scheduler work, then drive 180 timed real-PTY spinner frames through a second production `session.tabs.subscribeAll` transport and the real Zustand store. Each worktree must publish and apply only its exact initial-working and final-idle titles. Both topologies ordinary-park five xterms without enabling the lossy retention budget, assert bounded cells/heap/timer lag, then restore the bounded authoritative tail exactly once on the original PTY including output produced while parked and continued input/output. The headed oracle additionally keeps every remote target workspace and terminal unmounted in the host renderer before and after client recovery. Production-parity React contracts reproduce and remove the independent cold-park watcher/title feedback loop, including memo-cache discard, StrictMode cleanup replay, detached split-PTY replacement, and the zero-park fast path. A deterministic cross-boundary contract drives 24 real PTY trackers in distinct worktrees through 2,880 decorative spinner and bare-native redraw chunks and counts host publications, serialized bytes, renderer apply calls, and store mutations while preserving exact per-PTY raw chunks and per-worktree semantic transitions. Fake-clock continuation beyond the 30-minute stale boundary requires one deduplicated refresh per worktree every 15 minutes, globally spaced by at least 50 ms, with every mirrored working status still fresh. Exact-identical Pi plus rotating normalized Pi/Grok frames renew coarse working/permission state; rich prompt/tool/question fields retain their original 30-minute lease, and `stateStartedAt`/acknowledgement identity remain stable across title/hook source swaps. A separate headed desktop-client journey relaunches the host main process while retaining the daemon and a daemon-backgrounded PTY, then requires first-observation foreground sync and continued input/output on the same PTY. Unit coverage keeps layout versions out of snapshot output high-waters when a restored snapshot has no output sequence and retries an existing subscriber only after authoritative provider inventory proves the retained PTY exists. Separate headed/headless ACK-starvation tests recover one stalled stream without replacing its PTY. A headed output-drop oracle proves a successful snapshot probe cannot certify a stale live stream when the authoritative PTY sequence advanced beyond the client high-water. Unit coverage requires an unsequenced client to establish a first-probe baseline, remain attached at the same sequence, and recover only after a later probe advances. A capability-disabled run proves legacy hosts retain the prior lossy limit/TTL fallback. Unit tests cover exact-owner capability routing, raw-stream release, singleton side-effect facts with timed handoff cleanup, 64 simultaneous synthetic-title sources with zero decorative client events after convergence, per-client late-subscribe recovery, semantic bell/title transitions, local animation-frame preservation, legacy no-output-pause delivery gating with exact snapshot restore, provider-authoritative snapshots, 128 active streams plus retry after a 129th-stream capacity rejection, capacity-pressure backoff, full split-leaf remint reconciliation, truncation, and manual-disconnect queue fencing. Linux, Windows, live SSH, and production-scale paired hosts remain gaps. The audit removed the E2E parking override/debug-handle suite. Override exposure guards, delay/retention independence, TTL filtering and debug-handle forwarding alarms are lost. Retained real parking journeys do not establish equivalent test-harness configuration coverage. The test-value audit removes separate responsive-stream, intervening-control-frame and capacity-error classification examples. Retained multiplexer checks use fake clocks and captured transport callbacks; their per-stream isolation, deadline and probe-baseline assertions do not prove an actual host restart or equivalent replacement coverage. Historical run commands and results remain historical. The test-value audit removes the standalone retained/live metadata projection and additive-preference negative example from runtime-client-events.test.ts. Its remaining case checks partial-authority event admission through a captured callback; it does not establish full-pair projection, actual peer delivery or equivalent replacement coverage. Historical commands and results remain historical. The isolated accepted-handle-events observer suite is removed. Its runtime/worktree/pane membership, once-only delivery and listener-release ledger alarms are lost. Live hidden-retention evidence does not establish equivalent observer coverage. Audit1292: The entire host-session mirror empty-inventory settle suite is removed. Live-unpublished versus genuinely-empty hydration, same-ID revision response/in-flight fencing and cleared-tracking pending-probe invalidation alarms are lost. The singleton store and inventories were mocked; no real host census or resume delivery or equivalent replacement coverage is claimed. Audit1302: src/main/runtime/session-tabs-inventory-publication.test.ts: Keep actual runtime public inventory authority across renderer epochs/resync, fallback, aborts and invalid graph publishers. Remove copied nonempty publication, private scan counters/collector and known-host replacement, fake refresh that performs the restoration itself, environment capability flag, and private resume-store characterization whose fixture supplies reservation/rollback ordering. Lose isolated refresh/churn/receiver/capability and copied-payload wiring alarms; no equivalent replacement. Reduced selectors require fresh validation; prior full-suite receipts do not validate removed or changed coverage. No equivalent replacement or physical-provider claim. Audit1312 removes low-value imported specs from aggregate test drivers; old whole-driver execution receipts are archived. Retained registrations are unchanged. No equivalent replacement coverage is claimed. Audit1318: src/shared/remote-runtime/control/remote-runtime-shared-control-connection.test.ts: Retain real local encrypted WebSocket regressions for pre-ready socket replacement preserving a request, close-before-ready logical subscription cleanup, unrelated RPC surviving another deadline, and pending setup abort leaving a sibling pending. Host server is synthetic and uses production crypto; kept scenarios also retain internal diagnostics/memory assertions unchanged. Remove happy routing, authority-envelope, unknown-ID/log, cleanup enum, binary, keepalive/liveness and production-budget/private socket inventories; intentionally lose those alarms, no physical host authorization, independent crypto or remote provider claim. Reduced selectors need fresh validation; historical full-suite receipts do not validate the changed coverage. No equivalent replacement or physical-provider claim. Audit1346 removes the entire private-state-injected session-tabs rich-status boundary suite. Its stale task/question, neutral-title preservation, owner-change and provider-generation alarms are intentionally lost. Retained real terminal journeys do not establish equivalent replacement coverage. The prior mixed 42-contract execution receipt is historical and must not validate the reduced current selector; fresh validation remains required. Audit1352: focused Linux unit selections validated on source head b638e07d395b76af3ba6b03761a1a0fe7e5e026e. Historical statements above retain their original scope. Intentional test losses are recorded in knownGaps; these runs claim no physical-provider, p95, rendered E2E or cross-platform validation. Audit1360: focused Linux unit selections validated on source head 415c9cffc2d10024131ec4b6721767a22195de6d. Historical statements above retain their original scope. Intentional test losses are recorded in knownGaps; these runs claim no physical-provider, p95, rendered E2E or cross-platform validation. Audit1383: src/main/runtime/orca-runtime-tests/worktree-native-chat-setup.spec.ts reduced. Lose command-list/receipt/default/background activation matrices whose provider spawn returns canned IDs. Retained test proves actual tab allocation separation, not terminal startup. No equivalent replacement coverage is claimed. Existing exact unaffected receipts and their platform/provider/budget claims remain scoped to unchanged selectors. Changed optional selectors have no new execution result. Audit1391: src/renderer/src/components/terminal-pane/pty-connection-remote-pause-drop-notice.spec.ts reduced. Lose repeated quiet switches avoiding snapshots and legacy host without drop-report capability requesting a snapshot. Retained imported registrar uses fake transport/xterm writes, exercising real gap/restore lifecycle rather than actual terminal rendering. No equivalent replacement coverage is claimed. The existing primary-selector receipt and its claims remain scoped to their unchanged sources; the broad optional command has no new execution result. Audit1396: src/renderer/src/components/terminal-pane/terminal-parked-watcher-reconciliation.test.ts reduced. Retire standalone reconcile set output and positive untouchedFreshSpawn/workspaceOwner copying alarms; no native watcher execution claim. No equivalent replacement coverage is claimed. Existing exact unaffected receipts remain unchanged and apply only to their unchanged selectors. Changed optional selectors have no new execution result. Audit1401: src/main/runtime/orca-runtime-tests/mobile-session-tabs-duplicate-pty-repair.spec.ts removed. Loses listing-on-already-repaired-data alarm; it never established runtime repair invocation. src/main/runtime/orca-runtime-tests/mobile-session-tabs-split-agent-tab.spec.ts removed. Loses actual mobile split sibling shell versus recorded-agent resume command alarms; no equivalent coverage asserted. src/main/runtime/orca-runtime-tests/worktree-native-chat-setup.spec.ts removed. Loses separate setup/startup tab result in that manually assembled path. src/main/runtime/orca-runtime.test.ts reduced. Driver registration/import of a whole-deleted selected spec is removed; directly authored cases remain unchanged. No extra authored removal credit. No equivalent replacement coverage is claimed. Existing exact unaffected receipts remain unchanged and apply only to their unchanged selectors. Changed optional selectors have no new execution result. Audit1402: src/renderer/src/components/terminal-pane/pty-connection-park-reveal-model-snapshot.spec.ts reduced. Loses basic paired serialize/initial replay forwarding check; hidden alt-frame grid/precedence policy remains. No real terminal renderer claim. No equivalent replacement coverage is claimed. The existing primary-selector receipt and its claims remain scoped to their unchanged sources; the broad optional command has no new execution result. Audit1412: src/main/runtime/mobile-session-tabs-agent-status-heartbeat.test.ts reduced. Loses FIFO fairness/spacing, semantic bypass/cancellation, delayed-boundary scheduling and teardown timer-cleanup alarms. Retained fixed emission cases do not replace those outcomes. No equivalent replacement coverage is claimed.", + "coverageNotes": "Audit3128 scoped coverage reduction: Audit3121 removes VoiceDictationLanguageSetting.test.tsx (Spanish-to-es callback and unsupported-model Korean option presence) and voice-speech-model-rows.test.ts (provider/language-order/retired-on-disk list policy); these real narrow alarms are deliberately retired without replacement. VoiceSpeechModelSection per-model concurrent deletion remains unchanged. Audit3122 removes the direct absolute/parent-segment sparse-directory exception-type sample and the five-row IPC sparse-preset error-code/non-save table (two authored/six expanded cases). Those real typed-validation and IPC refusal/error-code alarms are deliberately retired. Keep both real symlink/junction floating-directory trust cases and the minimal RPC structured validation-reason serialization case. The RPC case does not replace IPC save/refusal behavior, typed normalization for both path inputs, actual Git sparse checkout or rendered localization. No equivalent replacement or fresh test/runtime/platform/remote/performance validation is claimed. Audit3124 removes ExperimentalPane.test.tsx (ordinary Pet switch callback), WorkspaceDirectorySetting.test.tsx (padded absolute saved-path normalization/no-write blur), and repository-search.perf.test.ts (navigation/selected-pane builder-call policy, fixture cardinality/nonempty entries and renamed project keyword inclusion). These real narrow alarms are retired without replacement; RepositoryPane.search.test.tsx search-to-sparse-editor request remains unchanged. Audit3129: resolver Windows case-insensitive and POSIX case-sensitive configured-env composition plus mocked-base launch-preflight Windows composition are intentionally no longer checked by the two deleted suites. No equivalent coverage claimed. Keep runtime-structured-agent-host-env.test.ts unchanged: configured Windows Claude-home account identity and host-account precedence remain checked. Audit3114 incorporates source3101: retire three structured Claude child-environment wrapper cases covering inherited stamp isolation/default and explicit entrypoint, Windows SDK-key canonicalization/default/duplicate-key order, and configured lookup/final environment/config-dir conjunction. Keep direct auth/session sanitation and Windows-versus-Linux overlay checks, actual executable lookup from supplied inherited PATH, native Windows child environment delivery, and stale option-mutation fencing. These are not equivalent replacements for the retired wrapper alarms. No fresh test/runtime, real login-shell probe, Claude SDK startup, remote/mixed-version, UI or performance result is claimed. Audit3114 incorporates source3105: retire 17 authored/20 expanded daemon launch-note cases, including the entire two-case SessionLaunchNote suite. Deliberately lose split-marker carry across same-token re-arm, stale-token retirement refusal, foreground tri-state and serial settlement, omitted-note/no-incarnation mixed-version policy, sticky retirement, first-incarnation/replacement launch preservation and held-launch precedence. Keep only the unchanged unresolved foreground-read race against a later finished attach. Supplied notes/foreground and runtime calls do not establish actual daemon/main restart, SSH, native Windows observation, mobile UI, equivalent replacement or fresh runtime/performance validation. Audit3114 incorporates source3109: retire six authored/35 expanded pricing cases: 21 accepted model-name examples, 10 unsupported/future-model fail-closed examples, malformed TTL clamp, unknown-model estimate refusal, flat-model long-context disregard and oversized long-context clamp. Keep mixed five-minute/one-hour write allocation and short/all-long/mixed long-context allocation with original fixed oracles. Real parser/refusal/bounds alarms are knowingly lost without equivalent replacement; retained tests do not validate provider rates, actual billing or request threshold classification. Preserve the archived usage-scan-append source-freeze receipt without transferring historical execution/performance credit. No fresh runtime result is claimed. Audit3108 source-only reduction (3094/3095/3100): retire 27 authored/30 expanded cases, including complete markdown view-state, file-preview link-open and CLI agent-server-route suites, without replacement. Deliberate losses include preview visibility/reference lifetime and same-path owners; SSH stat, directory/missing-target, explicit-null owner, nonmarkdown line/column, local URI routing, remote-to-client guard and same-file reuse; document-preview source-owner/folder/reuse routing, stable grant location and outside-workspace POSIX/Windows request projection; and verified remote CLI send/check refusal, suggested --on argv/copy, shell no-probe bypass, unverified-launch admission and older-peer refusal. Retain two markdown source-runtime/qualified-reveal alarms, one stale grant-rejection fence and two remote read/mutation evidence policies, with original callbacks. Final last-wins reveal did not prove transient cancellation; CLI prerequisite did not prove no real send, and envelope tests do not execute server refusal. No fresh runtime, rendered UI, actual file/SSH/paired transport, authorization, mixed-version or performance result is claimed. Audit3096 source-only reduction (3087/3088): retire 69 authored/76 expanded cases, including complete recovery-portals and reconnect-banner suites. Intentionally lose dead/default TUI replay, generated alternate-frame style composition, reset/mode/dimension and legacy missing-mode-boundary alarms, UI notice/action/reveal policies, and isolated recovery lifecycle variants, without equivalent replacement. Retained registrations and callbacks are unchanged; headless cells are not pixels, and no fresh runtime, host, SSH, WSL, rendered-app or performance result is claimed. Audit3086 source-only reduction (3079/3080/3081): intentionally retire 93 authored/100 expanded mock-boundary cases, including the complete spawn-errors and SSH reconnect overlay suites, without replacement. Retired alarms include spawn classification/ownership, reply ceilings and renderer-write failure recovery, disconnect/detach fencing branches, authority facts, and overlay retry/visibility wiring. Retained callbacks are unchanged; no fresh execution, real PTY/SSH, rendered UI, or performance result is claimed. The direct side-effect-facts selector retains six cases: replay exclusion, live sequence handling, stale disposer ownership, one handoff drain, drain-before-snapshot, and stale replacement-snapshot refusal. Successful current-entry snapshot application is not witnessed by the retained snapshot case. Audit2432: The broad unit command loses the isolated BranchPicker open-change notification sample. Its unique true/false callback alarm is intentionally retired; reopening/status-refresh behavior was not exercised. No equivalent replacement or fresh validation of the reduced selector is claimed. Audit2425: The broad unit command loses the workspace-copy-status supplied readError forwarding sample. It did not exercise the branch-switch blocking promised by its title. This alarm is intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2417: The broad unit command loses two ordinary clean Git cherry-pick/revert success samples and two terminal startup foreground-read work-budget samples. These unique alarms are intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2412: The broad unit command loses the whole six-case fake-React-hook scroll-anchor suite, two generic draft-retention samples, a simple new-agent order sample, a real history-publication batching-count sample, an unmounted handoff abort/timer sample and three basic WSL preview/error samples. These alarms are intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2404: The broad unit command loses the standalone share-name lookup 503 propagation suite, initial deletion placement-kind sample, direct changed-mtime stale sample, first-pass rollback existence example and abandoned-preparation directory sample. These exact alarms are intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2397 review amendment: The broad unit command loses three standalone suites, two partial-case definitions and two URL table rows across package/download/placement tests. Keep initial wrong-port authorization, modified independent-copy digest refusal and abort-after-begin cancellation dispatch. The remaining alarms are deliberately retired; no equivalent complete replacement or fresh validation of the reduced selector is claimed. Audit2393 current-parent refresh2401: Remove the basic skill-install service alias sample and original public managed-remove receiptless refusal case. Preserve the upstream request ownership suite and transaction interruption coverage. No equivalent complete replacement or fresh validation of the reduced selector is claimed. Audit2389: The broad unit command loses the missing-preview suite, plain-global destination sample and prepublished-receipt recovery example. No equivalent replacement or fresh validation of the reduced selector is claimed. Audit2385: The broad unit command loses four authored walker/staging/protocol examples (six expanded cases); no equivalent replacement or fresh validation is claimed for the reduced suite. Audit2383: The unscoped Vitest command no longer includes the removed Antigravity catalogue suite; its exact provider-root/owner example has no equivalent replacement or fresh validation claim. Audit2377/2381: The unscoped Vitest command includes the reduced bundled-skill and native-discovery suites; the eight removed backup/legacy/catalogue/symlink/SSH-builder examples have no replacement-equivalence or fresh validation claim here. Audit2162: Reduced inventory-authority source intentionally loses the examples recorded in knownGaps; historical execution is not validation of changed bytes. Historical prior account (preserved verbatim): Audit2133 current-parent preparation: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2095 current-parent preparation2126: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2090 (sources2088/2089): Current reduced suites intentionally retire the alarms listed in knownGaps; prior dated execution and replacement claims do not validate changed source bytes or selectors. Historical prior account (preserved verbatim): Audit2086 (sources2082/2083): Current reduced suites intentionally retire the alarms listed in knownGaps; prior dated execution and replacement claims do not validate changed source bytes or selectors. Historical prior account (preserved verbatim): Audit2076 (sources2073/2074): Intentionally retire these low-value alarms without replacement: src/main/runtime/orca-runtime-tests/terminal-handles-part-02.spec.ts: recovers a moved SSH pane through the tab it now sits in, not the one its lease froze; reports a plain expired orphan; does not report a pane whose only recent lease was superseded; does not report a pane whose only recent lease had its relay id recycled; picks the eligible orphan over a superseded predecessor that matches first; collects the same tabs the per-tab reader reports, in one sweep of the leases; reports no candidate when no lease names any of the worktree tabs. src/main/runtime/orca-runtime-tests/runtime-availability.spec.ts: starts unavailable with no authoritative window; reports the configured Windows terminal shell on status; reports floating workspace availability from settings on status; polls floating tabs with targeted PTY liveness and no repo/provider inventory; hydrates persisted tabs when the store cannot report repos; advertises browser screencast only when a renderer window is available; routes mobile Codex reset consumption through the account mutation coordinator; maps a definite pre-provider rejection into an authoritative current snapshot. Existing unrelated evidenceRuns retain their original scope; unchanged dated receipt objects do not validate changed source bytes or reduced selectors. No fresh gate execution or historical import-closure equivalence is claimed. Frozen current literal-import graph reaches these changed test bytes; unresolved local and computed edges remain debt, and current reachability is not historical execution or equivalent replacement evidence. Historical prior account (preserved verbatim): Audit2003: Remove SSH/absent-inventory attach refusal, later-subscriber retry, no-view adoption/takeback, provider-tail/unknown-read projections, failed-keystroke retry and simpler loss reattach examples. Keep the existing-subscriber inventory-ready retry alias only at its retained callback, plus retained held-input cancellation/replacement and overlapping attach/loss fences. The inventory driver also loses the whole empty-worktree-publication registration and folder-host/unverifiable scalar examples. Retire the entire dated10-case attach receipt; retain16 independent receipts only in their original scopes. No retained-memory or real-daemon measurement follows from supplied provider inventories. Changed-source prior results do not validate reduced-source bytes. No fresh execution, equivalent replacement, red/green, measured budget, soak, physical provider/PTY, rendered, live remote/mobile or cross-platform result is claimed. Historical prior account (preserved verbatim): Audit1991: Remove initial-snapshot sequence-domain, limited-tail/truncation and unknown-output-sequence aliases whose callbacks are removed. The source retains partial-chunk replay and requested-snapshot retry/admission at supplied snapshot/data seams, not equivalent replacement or measured retained-memory proof. Preserve the existing stream/pending-wait capacity aliases only to their retained owning multiplex callbacks with supplied PTY resolutions. Removing the sibling-PTY heartbeat dedup work-count sample does not establish a new decorative fanout budget; the unrelated-PTY cancellation fence survives. Preserve17 unrelated dated receipts. Changed-source prior results do not validate reduced-source bytes. No fresh execution, equivalent replacement, red/green, measured budget, soak, physical provider/PTY, rendered, live remote/mobile or cross-platform result is claimed. Historical prior account (preserved verbatim): Audit1983: Remove the reasonless-close selector and narrow census aliases to actual retained runtime and injected-inventory callbacks. The removed synthetic census-dedup and isolated pre-boundary timing cases receive no replacement credit; preserve17 unrelated dated receipts. Changed-source prior results do not validate reduced-source bytes. No fresh execution, equivalent replacement, red/green, measured budget, soak, provider/PTY, rendered, live remote/mobile or cross-platform result is claimed. Historical prior account (preserved verbatim): Audit1939 (sources1934/1935): Delete the synthetic watcher-synchronization and partial-reconciliation suites. Retire all affected host-fact, capability-owner, activation-residue and retention-cap aliases because the remaining callbacks do not establish those full claims. Keep exact local hook/timer, byte-stream side effects, exit/disposal, scroll-intent cleanup and deterministic local rank/TTL progression callbacks within their mocked-host scope. No physical paired-host retention, resource budget or equivalent replacement evidence is assigned. The generic broad test command remains a runnable discovery command, never a dated execution receipt. Preserve meaningful remaining selectors/assertions and unchanged dated receipts within their original scope. Reduced selectors have no fresh gate execution evidence in this metadata packet. No equivalent replacement, physical watcher/PTY/provider, live SSH/remote/mobile, rendered, cross-platform, soak or measured-budget result is claimed. Historical prior account (preserved verbatim): Audit1931 (sources1928/1929): Delete the complete hook-agent-status projection suite and its two behavioral assertion aliases. Supplied stored hook/title/resume/staleness/rename/question/generation/PTY-retirement/snapshot-version projections are intentionally lost. The separate osc-status-scan gate mentions this source only in existing historical notes and receives no mutation. No measured hidden-output retention, CPU/RSS or resource budget result is claimed by the deleted source or this audit. Preserve meaningful remaining selectors/assertions and unchanged dated receipts within their original scope. Reduced selectors have no fresh gate execution evidence in this metadata packet. No equivalent replacement, physical watcher/PTY/provider, live SSH/remote/mobile, rendered, cross-platform, soak or measured-budget result is claimed. Historical prior account (preserved verbatim): Module ownership cleanup retires the modeled cold-park private watcher/render/work-count suites under the current test policy. Their assertions are no longer active evidence. Retained lifecycle tests and headed/headless journeys do not establish equivalent coverage for those removed harness alarms. Audit1640 removes low-value IPC, SSH, workspace and transcript-watcher tests without equivalent replacement coverage. Per-source reviews record retained public refusal, cancellation, execution-host and real-filesystem outcomes with injected/native/remote limits. Affected historical receipts are retired; gates without active current evidence are downgraded. Historical scope before this audit: Audit1634 removes low-value IPC, filesystem and runtime projections, argument inventories and duplicate examples without equivalent replacement coverage. Per-source reviews record retained cancellation, ownership, authorization, filesystem and SQLite boundaries with injected/native/remote limits. Affected historical receipts are retired; gates without active current evidence are downgraded. Historical scope before this audit: Audit1627 removes low-value mocked daemon projections, scalar tables and duplicate terminal examples without equivalent replacement coverage. Per-source reviews record retained terminal content preservation, process ownership refusal and recovery boundaries together with injected/native/remote limits. Affected historical receipts are retired; gates without active current evidence are downgraded. Historical scope before this audit: Retained exact unchanged-source receipts remain the only current execution evidence. Reduced selectors have no fresh execution result. Audit1417 intentionally deletes src/renderer/src/components/terminal-pane/pty-connection-hidden-tui-snapshot-replay.spec.ts: Loses rich hidden snapshot sequence replay/grid resize and forced repaint after hidden snapshot alarms, including live-trigger deduplication. deletes src/renderer/src/components/terminal-pane/terminal-cold-park-verdict-loop.test.tsx: Loses modeled React oscillation alarm; real pane/watchers are not exercised. deletes src/renderer/src/runtime/remote-runtime-terminal-parse-backpressure.test.ts: Loses delayed credit until parsing, malformed frame/unknown stream handling, discarded transformed output recovery and failed ACK rearm alarms. No equivalent replacement coverage or fresh result is claimed. Historical coverage account: Audit1412 changes referenced sources. Retained unchanged-source receipts apply only to their unchanged commands; the reduced selectors have no new execution result. Historical coverage account (not a current claim for changed sources): This audit removes the browser response-after-manual-disconnect example. Its distinct in-flight response fencing alarm is lost; retained queued-call disconnect tests are not an equivalent replacement. Historical commands and results remain historical. Deterministic headed macOS runs launch an isolated Orca desktop server and a separate paired web client. A byte-identical headless run uses an isolated `orca serve` host. Both create six real paired host PTYs with bounded high-output scrollback, prove sustained output while all six client xterms are warm-mounted but hidden causes zero renderer scheduler work, then drive 180 timed real-PTY spinner frames through a second production `session.tabs.subscribeAll` transport and the real Zustand store. Each worktree must publish and apply only its exact initial-working and final-idle titles. Both topologies ordinary-park five xterms without enabling the lossy retention budget, assert bounded cells/heap/timer lag, then restore the bounded authoritative tail exactly once on the original PTY including output produced while parked and continued input/output. The headed oracle additionally keeps every remote target workspace and terminal unmounted in the host renderer before and after client recovery. Production-parity React contracts reproduce and remove the independent cold-park watcher/title feedback loop, including memo-cache discard, StrictMode cleanup replay, detached split-PTY replacement, and the zero-park fast path. A deterministic cross-boundary contract drives 24 real PTY trackers in distinct worktrees through 2,880 decorative spinner and bare-native redraw chunks and counts host publications, serialized bytes, renderer apply calls, and store mutations while preserving exact per-PTY raw chunks and per-worktree semantic transitions. Fake-clock continuation beyond the 30-minute stale boundary requires one deduplicated refresh per worktree every 15 minutes, globally spaced by at least 50 ms, with every mirrored working status still fresh. Exact-identical Pi plus rotating normalized Pi/Grok frames renew coarse working/permission state; rich prompt/tool/question fields retain their original 30-minute lease, and `stateStartedAt`/acknowledgement identity remain stable across title/hook source swaps. A separate headed desktop-client journey relaunches the host main process while retaining the daemon and a daemon-backgrounded PTY, then requires first-observation foreground sync and continued input/output on the same PTY. Unit coverage keeps layout versions out of snapshot output high-waters when a restored snapshot has no output sequence and retries an existing subscriber only after authoritative provider inventory proves the retained PTY exists. Separate headed/headless ACK-starvation tests recover one stalled stream without replacing its PTY. A headed output-drop oracle proves a successful snapshot probe cannot certify a stale live stream when the authoritative PTY sequence advanced beyond the client high-water. Unit coverage requires an unsequenced client to establish a first-probe baseline, remain attached at the same sequence, and recover only after a later probe advances. A capability-disabled run proves legacy hosts retain the prior lossy limit/TTL fallback. Unit tests cover exact-owner capability routing, raw-stream release, singleton side-effect facts with timed handoff cleanup, 64 simultaneous synthetic-title sources with zero decorative client events after convergence, per-client late-subscribe recovery, semantic bell/title transitions, local animation-frame preservation, legacy no-output-pause delivery gating with exact snapshot restore, provider-authoritative snapshots, 128 active streams plus retry after a 129th-stream capacity rejection, capacity-pressure backoff, full split-leaf remint reconciliation, truncation, and manual-disconnect queue fencing. Linux, Windows, live SSH, and production-scale paired hosts remain gaps. The audit removed the E2E parking override/debug-handle suite. Override exposure guards, delay/retention independence, TTL filtering and debug-handle forwarding alarms are lost. Retained real parking journeys do not establish equivalent test-harness configuration coverage. The test-value audit removes separate responsive-stream, intervening-control-frame and capacity-error classification examples. Retained multiplexer checks use fake clocks and captured transport callbacks; their per-stream isolation, deadline and probe-baseline assertions do not prove an actual host restart or equivalent replacement coverage. Historical run commands and results remain historical. The test-value audit removes the standalone retained/live metadata projection and additive-preference negative example from runtime-client-events.test.ts. Its remaining case checks partial-authority event admission through a captured callback; it does not establish full-pair projection, actual peer delivery or equivalent replacement coverage. Historical commands and results remain historical. The isolated accepted-handle-events observer suite is removed. Its runtime/worktree/pane membership, once-only delivery and listener-release ledger alarms are lost. Live hidden-retention evidence does not establish equivalent observer coverage. Audit1292: The entire host-session mirror empty-inventory settle suite is removed. Live-unpublished versus genuinely-empty hydration, same-ID revision response/in-flight fencing and cleared-tracking pending-probe invalidation alarms are lost. The singleton store and inventories were mocked; no real host census or resume delivery or equivalent replacement coverage is claimed. Audit1302: src/main/runtime/session-tabs-inventory-publication.test.ts: Keep actual runtime public inventory authority across renderer epochs/resync, fallback, aborts and invalid graph publishers. Remove copied nonempty publication, private scan counters/collector and known-host replacement, fake refresh that performs the restoration itself, environment capability flag, and private resume-store characterization whose fixture supplies reservation/rollback ordering. Lose isolated refresh/churn/receiver/capability and copied-payload wiring alarms; no equivalent replacement. Reduced selectors require fresh validation; prior full-suite receipts do not validate removed or changed coverage. No equivalent replacement or physical-provider claim. Audit1312 removes low-value imported specs from aggregate test drivers; old whole-driver execution receipts are archived. Retained registrations are unchanged. No equivalent replacement coverage is claimed. Audit1318: src/shared/remote-runtime/control/remote-runtime-shared-control-connection.test.ts: Retain real local encrypted WebSocket regressions for pre-ready socket replacement preserving a request, close-before-ready logical subscription cleanup, unrelated RPC surviving another deadline, and pending setup abort leaving a sibling pending. Host server is synthetic and uses production crypto; kept scenarios also retain internal diagnostics/memory assertions unchanged. Remove happy routing, authority-envelope, unknown-ID/log, cleanup enum, binary, keepalive/liveness and production-budget/private socket inventories; intentionally lose those alarms, no physical host authorization, independent crypto or remote provider claim. Reduced selectors need fresh validation; historical full-suite receipts do not validate the changed coverage. No equivalent replacement or physical-provider claim. Audit1346 removes the entire private-state-injected session-tabs rich-status boundary suite. Its stale task/question, neutral-title preservation, owner-change and provider-generation alarms are intentionally lost. Retained real terminal journeys do not establish equivalent replacement coverage. The prior mixed 42-contract execution receipt is historical and must not validate the reduced current selector; fresh validation remains required. Audit1352: focused Linux unit selections validated on source head b638e07d395b76af3ba6b03761a1a0fe7e5e026e. Historical statements above retain their original scope. Intentional test losses are recorded in knownGaps; these runs claim no physical-provider, p95, rendered E2E or cross-platform validation. Audit1360: focused Linux unit selections validated on source head 415c9cffc2d10024131ec4b6721767a22195de6d. Historical statements above retain their original scope. Intentional test losses are recorded in knownGaps; these runs claim no physical-provider, p95, rendered E2E or cross-platform validation. Audit1383: src/main/runtime/orca-runtime-tests/worktree-native-chat-setup.spec.ts reduced. Lose command-list/receipt/default/background activation matrices whose provider spawn returns canned IDs. Retained test proves actual tab allocation separation, not terminal startup. No equivalent replacement coverage is claimed. Existing exact unaffected receipts and their platform/provider/budget claims remain scoped to unchanged selectors. Changed optional selectors have no new execution result. Audit1391: src/renderer/src/components/terminal-pane/pty-connection-remote-pause-drop-notice.spec.ts reduced. Lose repeated quiet switches avoiding snapshots and legacy host without drop-report capability requesting a snapshot. Retained imported registrar uses fake transport/xterm writes, exercising real gap/restore lifecycle rather than actual terminal rendering. No equivalent replacement coverage is claimed. The existing primary-selector receipt and its claims remain scoped to their unchanged sources; the broad optional command has no new execution result. Audit1396: src/renderer/src/components/terminal-pane/terminal-parked-watcher-reconciliation.test.ts reduced. Retire standalone reconcile set output and positive untouchedFreshSpawn/workspaceOwner copying alarms; no native watcher execution claim. No equivalent replacement coverage is claimed. Existing exact unaffected receipts remain unchanged and apply only to their unchanged selectors. Changed optional selectors have no new execution result. Audit1401: src/main/runtime/orca-runtime-tests/mobile-session-tabs-duplicate-pty-repair.spec.ts removed. Loses listing-on-already-repaired-data alarm; it never established runtime repair invocation. src/main/runtime/orca-runtime-tests/mobile-session-tabs-split-agent-tab.spec.ts removed. Loses actual mobile split sibling shell versus recorded-agent resume command alarms; no equivalent coverage asserted. src/main/runtime/orca-runtime-tests/worktree-native-chat-setup.spec.ts removed. Loses separate setup/startup tab result in that manually assembled path. src/main/runtime/orca-runtime.test.ts reduced. Driver registration/import of a whole-deleted selected spec is removed; directly authored cases remain unchanged. No extra authored removal credit. No equivalent replacement coverage is claimed. Existing exact unaffected receipts remain unchanged and apply only to their unchanged selectors. Changed optional selectors have no new execution result. Audit1402: src/renderer/src/components/terminal-pane/pty-connection-park-reveal-model-snapshot.spec.ts reduced. Loses basic paired serialize/initial replay forwarding check; hidden alt-frame grid/precedence policy remains. No real terminal renderer claim. No equivalent replacement coverage is claimed. The existing primary-selector receipt and its claims remain scoped to their unchanged sources; the broad optional command has no new execution result. Audit1412: src/main/runtime/mobile-session-tabs-agent-status-heartbeat.test.ts reduced. Loses FIFO fairness/spacing, semantic bypass/cancellation, delayed-boundary scheduling and teardown timer-cleanup alarms. Retained fixed emission cases do not replace those outcomes. No equivalent replacement coverage is claimed.", "motivatingLinks": [ "https://github.com/stablyai/orca/issues/8652", "https://github.com/stablyai/orca/pull/10625" ], "invariant": "A host advertising terminal.paired-parking.v1 keeps the PTY and bounded authoritative history alive while an ordinary hidden-view park destroys the client xterm and releases its raw per-PTY stream. Reveal must restore up to the requested 5,000 rows, parked-time side effects/output, the same PTY identity, and continued input/output. Parked watcher synchronization compares parking semantics rather than fresh object identity, owns setup and cleanup across StrictMode replay, reconciles real PTY/layout changes, and performs no terminal-state scan when nothing is parked. After a host main-process relaunch, unknown local delivery-sync state must not be treated as proof that a surviving daemon PTY is already foregrounded. An empty headed session-tab inventory is authoritative only after the current renderer graph generation publishes a complete inventory and every execution host answers the PTY census; unpublished, resync-pending, transport-failed, and partially scoped inventories remain unverifiable, while genuinely authoritative empty and non-empty inventories settle and a scope-less legacy empty keeps its pre-authority best-effort settle so outdated hosts retain agent auto-resume. The unpublished-empty regression must keep the resume dispatch parked with exactly one daemon process/writer; restoring unconditional empty hydration must release one dispatch and create two live writers. If subscription precedes provider readiness, authoritative inventory must reconsider that existing subscriber without spawning, resizing, or changing reconnect state. A snapshot without an output high-water must keep it unknown rather than borrowing a layout version that can suppress newer live output. Hosts without the capability must gate hidden raw output before xterm scheduling and repaint from the authoritative snapshot on reveal while retaining the existing limit/TTL force-parking fallback. After the first semantic title state, decorative spinner frequency must add zero paired client-event frames and zero full session-tabs work between status-freshness leases regardless of hidden worktree count. Continuous working or permission evidence may refresh each affected worktree once per 15 minutes through one globally spaced FIFO so current and legacy viewers do not decay before 30 minutes; sibling PTYs share that worktree refresh, while local title animation, raw output, and semantic title, status, bell, completion, and query facts remain intact. A paired terminal stream whose delivery credits stop progressing must replace only that stream; command silence first probes authoritative state and replaces the stream if the probe times out or proves the PTY advanced beyond the client's delivered output sequence. When no comparable delivery high-water exists, the first sequenced probe establishes a baseline and only later advancement proves staleness. A same-sequence snapshot remains valid proof of a responsive silent command. A successful status probe may replace a pre-ready shared-control socket without rejecting or duplicating calls already waiting for that transport. Manual disconnect must retain pairing while preventing queued or passive calls and subscriptions from recreating transport until explicit Connect. A paired pane reconnecting to a relaunched host that has not yet published its window graph (an unpublished frame, bare or client-projected) must stay in recovery rather than retire, and the host's first published snapshot carrying the surface must reattach it; a published frame lacking the surface still retires it. The placeholder is only ever sent before the host's graph first publishes; afterwards a worktree with no tabs gets a real empty answer, pushed once to any client that asked during startup, so a paired client opening a never-opened host worktree still gets its first terminal.", - "oracle": "Audit2432: The broad unit command loses the isolated BranchPicker open-change notification sample. Its unique true/false callback alarm is intentionally retired; reopening/status-refresh behavior was not exercised. No equivalent replacement or fresh validation of the reduced selector is claimed. Audit2425: The broad unit command loses the workspace-copy-status supplied readError forwarding sample. It did not exercise the branch-switch blocking promised by its title. This alarm is intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2417: The broad unit command loses two ordinary clean Git cherry-pick/revert success samples and two terminal startup foreground-read work-budget samples. These unique alarms are intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2412: The broad unit command loses the whole six-case fake-React-hook scroll-anchor suite, two generic draft-retention samples, a simple new-agent order sample, a real history-publication batching-count sample, an unmounted handoff abort/timer sample and three basic WSL preview/error samples. These alarms are intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2404: The broad unit command loses the standalone share-name lookup 503 propagation suite, initial deletion placement-kind sample, direct changed-mtime stale sample, first-pass rollback existence example and abandoned-preparation directory sample. These exact alarms are intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2397 review amendment: The broad unit command loses three standalone suites, two partial-case definitions and two URL table rows across package/download/placement tests. Keep initial wrong-port authorization, modified independent-copy digest refusal and abort-after-begin cancellation dispatch. The remaining alarms are deliberately retired; no equivalent complete replacement or fresh validation of the reduced selector is claimed. Audit2393 current-parent refresh2401: Remove the basic skill-install service alias sample and original public managed-remove receiptless refusal case. Preserve the upstream request ownership suite and transaction interruption coverage. No equivalent complete replacement or fresh validation of the reduced selector is claimed. Audit2389: The broad unit command loses the missing-preview suite, plain-global destination sample and prepublished-receipt recovery example. No equivalent replacement or fresh validation of the reduced selector is claimed. Audit2385: The broad unit command loses four authored walker/staging/protocol examples (six expanded cases); no equivalent replacement or fresh validation is claimed for the reduced suite. Audit2383: The unscoped Vitest command no longer includes the removed Antigravity catalogue suite; its exact provider-root/owner example has no equivalent replacement or fresh validation claim. Audit2377/2381: The unscoped Vitest command includes the reduced bundled-skill and native-discovery suites; the eight removed backup/legacy/catalogue/symlink/SSH-builder examples have no replacement-equivalence or fresh validation claim here. Audit2133 current-parent preparation: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2095 current-parent preparation2126: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2090 (sources2088/2089): Current reduced suites intentionally retire the alarms listed in knownGaps; prior dated execution and replacement claims do not validate changed source bytes or selectors. Historical prior account (preserved verbatim): Audit2086 (sources2082/2083): Current reduced suites intentionally retire the alarms listed in knownGaps; prior dated execution and replacement claims do not validate changed source bytes or selectors. Historical prior account (preserved verbatim): Audit2076 (sources2073/2074): Intentionally retire these low-value alarms without replacement: src/main/runtime/orca-runtime-tests/terminal-handles-part-02.spec.ts: recovers a moved SSH pane through the tab it now sits in, not the one its lease froze; reports a plain expired orphan; does not report a pane whose only recent lease was superseded; does not report a pane whose only recent lease had its relay id recycled; picks the eligible orphan over a superseded predecessor that matches first; collects the same tabs the per-tab reader reports, in one sweep of the leases; reports no candidate when no lease names any of the worktree tabs. src/main/runtime/orca-runtime-tests/runtime-availability.spec.ts: starts unavailable with no authoritative window; reports the configured Windows terminal shell on status; reports floating workspace availability from settings on status; polls floating tabs with targeted PTY liveness and no repo/provider inventory; hydrates persisted tabs when the store cannot report repos; advertises browser screencast only when a renderer window is available; routes mobile Codex reset consumption through the account mutation coordinator; maps a definite pre-provider rejection into an authoritative current snapshot. Existing unrelated evidenceRuns retain their original scope; unchanged dated receipt objects do not validate changed source bytes or reduced selectors. No fresh gate execution or historical import-closure equivalence is claimed. Frozen current literal-import graph reaches these changed test bytes; unresolved local and computed edges remain debt, and current reachability is not historical execution or equivalent replacement evidence. Historical prior account (preserved verbatim): Audit2003: Remove SSH/absent-inventory attach refusal, later-subscriber retry, no-view adoption/takeback, provider-tail/unknown-read projections, failed-keystroke retry and simpler loss reattach examples. Keep the existing-subscriber inventory-ready retry alias only at its retained callback, plus retained held-input cancellation/replacement and overlapping attach/loss fences. The inventory driver also loses the whole empty-worktree-publication registration and folder-host/unverifiable scalar examples. Retire the entire dated10-case attach receipt; retain16 independent receipts only in their original scopes. No retained-memory or real-daemon measurement follows from supplied provider inventories. Changed-source prior results do not validate reduced-source bytes. No fresh execution, equivalent replacement, red/green, measured budget, soak, physical provider/PTY, rendered, live remote/mobile or cross-platform result is claimed. Historical prior account (preserved verbatim): Audit1991: Remove initial-snapshot sequence-domain, limited-tail/truncation and unknown-output-sequence aliases whose callbacks are removed. The source retains partial-chunk replay and requested-snapshot retry/admission at supplied snapshot/data seams, not equivalent replacement or measured retained-memory proof. Preserve the existing stream/pending-wait capacity aliases only to their retained owning multiplex callbacks with supplied PTY resolutions. Removing the sibling-PTY heartbeat dedup work-count sample does not establish a new decorative fanout budget; the unrelated-PTY cancellation fence survives. Preserve17 unrelated dated receipts. Changed-source prior results do not validate reduced-source bytes. No fresh execution, equivalent replacement, red/green, measured budget, soak, physical provider/PTY, rendered, live remote/mobile or cross-platform result is claimed. Historical prior account (preserved verbatim): Audit1983: Remove the reasonless-close selector and narrow census aliases to actual retained runtime and injected-inventory callbacks. The removed synthetic census-dedup and isolated pre-boundary timing cases receive no replacement credit; preserve17 unrelated dated receipts. Changed-source prior results do not validate reduced-source bytes. No fresh execution, equivalent replacement, red/green, measured budget, soak, provider/PTY, rendered, live remote/mobile or cross-platform result is claimed. Historical prior account (preserved verbatim): Audit1939 (sources1934/1935): Delete the synthetic watcher-synchronization and partial-reconciliation suites. Retire all affected host-fact, capability-owner, activation-residue and retention-cap aliases because the remaining callbacks do not establish those full claims. Keep exact local hook/timer, byte-stream side effects, exit/disposal, scroll-intent cleanup and deterministic local rank/TTL progression callbacks within their mocked-host scope. No physical paired-host retention, resource budget or equivalent replacement evidence is assigned. The generic broad test command remains a runnable discovery command, never a dated execution receipt. Preserve meaningful remaining selectors/assertions and unchanged dated receipts within their original scope. Reduced selectors have no fresh gate execution evidence in this metadata packet. No equivalent replacement, physical watcher/PTY/provider, live SSH/remote/mobile, rendered, cross-platform, soak or measured-budget result is claimed. Historical prior account (preserved verbatim): Audit1931 (sources1928/1929): Delete the complete hook-agent-status projection suite and its two behavioral assertion aliases. Supplied stored hook/title/resume/staleness/rename/question/generation/PTY-retirement/snapshot-version projections are intentionally lost. The separate osc-status-scan gate mentions this source only in existing historical notes and receives no mutation. No measured hidden-output retention, CPU/RSS or resource budget result is claimed by the deleted source or this audit. Preserve meaningful remaining selectors/assertions and unchanged dated receipts within their original scope. Reduced selectors have no fresh gate execution evidence in this metadata packet. No equivalent replacement, physical watcher/PTY/provider, live SSH/remote/mobile, rendered, cross-platform, soak or measured-budget result is claimed. Historical prior account (preserved verbatim): Retained exact unchanged-source receipts remain the only current execution evidence. Reduced selectors have no fresh execution result. Historical pre-audit oracle account for changed sources: The retained unchanged-source receipts are the only current execution evidence; reduced-source assertions await validation. Historical oracle account (not a current claim for changed sources): Existing exact unchanged-selector receipts remain the only current execution evidence. Changed source selectors have no fresh execution result. Historical prior-source oracle account: Existing exact unchanged-selector receipts remain the only current execution evidence. Changed source selectors have no fresh execution result. Historical prior-source oracle account: Run one byte-identical six-terminal oracle against an isolated headed desktop host and an isolated headless `orca serve` host. Stage at least 1,000,000 xterm cells, hide all six warm managers, emit an initial working title followed by 30 timed real-PTY spinner frames and a final idle title per worktree, and observe a second real session-tabs subscription plus the production renderer store. Require exactly two transport envelopes and two title mutations per worktree, no decorative title crossing either boundary, and positive decoded-envelope bytes. Enable ordinary parking with the lossy retention budget disabled, require exactly one mounted manager and five parked tabs, at most 45% retained cells, no more than 16 MiB heap growth, and under 500 ms timer drift. In headed mode, require the host renderer to remain on its original workspace with zero target terminal managers mounted throughout client park, reveal, and live I/O. While a tab is parked, require authoritative terminal.read to observe new PTY output; reveal it and require the original PTY, a marker within the requested 5,000-row history, the parked marker, and post-reveal input/output. Drive 24 real PTY title trackers in distinct worktrees through 40 decorative spinner frames each, then 40 spinner frames alternating with Cursor's bare native identity redraw, using fake clocks. Require all 2,880 raw chunks exactly and zero host session-tabs publications, serialized bytes, renderer apply calls, and renderer store mutations. Continue decorative evidence beyond 30 minutes, require exactly one refresh per worktree per 15-minute lease through a FIFO spaced by at least 50 ms, and require all 24 viewer statuses to remain working and fresh; then require one exact semantic idle transition and one visible-output chunk per PTY. Require ordinary and interior-Braille title changes to publish. Drive 64 host PTYs through ten 80 ms synthetic title frames, require zero paired client events after the first frame per PTY while all 640 local frames remain observable, attach a late client and require one current frame per PTY, then require semantic bell and idle transitions on both clients. Reconstruct a legacy subscribed stream with no outputPause capability, hide a chatty pane, require no hidden xterm writes, reveal it, and require one authoritative snapshot plus continued live output. Drop and acknowledge output only for one original paired-client stream, prove the fixture process consumed input while the host model advanced and the client stayed stale, then require the command snapshot probe to replace that stream, repaint exact fixture output, preserve authoritative PTY identity and target-tab cardinality, and resume live I/O. For a client without a delivered sequence, require the first numeric probe to establish a baseline without replacement, a same-sequence probe to remain attached, and a later advanced probe to replace only that stream. Withhold the first encrypted shared-control ready frame, start one RPC, trigger a status-probe refresh, and require two connections, one host delivery, successful response, and zero retained request bytes. Admit 128 active streams, reject the 129th as retryable, release one stream, then require the retry to attach and publish a snapshot without multiplying retained subscribers. Disable terminal.paired-parking.v1 and require the same oracle to fail before parking, while the legacy limit-one fallback separately passes. Also preserve truncated first paint, ACK-starved same-PTY recovery, responsive silent-command snapshot probes, dead-stream probe timeout recovery, and queued manual-disconnect fencing.", + "oracle": "Audit3128 scoped coverage reduction: Audit3121 removes VoiceDictationLanguageSetting.test.tsx (Spanish-to-es callback and unsupported-model Korean option presence) and voice-speech-model-rows.test.ts (provider/language-order/retired-on-disk list policy); these real narrow alarms are deliberately retired without replacement. VoiceSpeechModelSection per-model concurrent deletion remains unchanged. Audit3122 removes the direct absolute/parent-segment sparse-directory exception-type sample and the five-row IPC sparse-preset error-code/non-save table (two authored/six expanded cases). Those real typed-validation and IPC refusal/error-code alarms are deliberately retired. Keep both real symlink/junction floating-directory trust cases and the minimal RPC structured validation-reason serialization case. The RPC case does not replace IPC save/refusal behavior, typed normalization for both path inputs, actual Git sparse checkout or rendered localization. No equivalent replacement or fresh test/runtime/platform/remote/performance validation is claimed. Audit3124 removes ExperimentalPane.test.tsx (ordinary Pet switch callback), WorkspaceDirectorySetting.test.tsx (padded absolute saved-path normalization/no-write blur), and repository-search.perf.test.ts (navigation/selected-pane builder-call policy, fixture cardinality/nonempty entries and renamed project keyword inclusion). These real narrow alarms are retired without replacement; RepositoryPane.search.test.tsx search-to-sparse-editor request remains unchanged. Audit3129: resolver Windows case-insensitive and POSIX case-sensitive configured-env composition plus mocked-base launch-preflight Windows composition are intentionally no longer checked by the two deleted suites. No equivalent coverage claimed. Keep runtime-structured-agent-host-env.test.ts unchanged: configured Windows Claude-home account identity and host-account precedence remain checked. Audit3114 incorporates source3101: retire three structured Claude child-environment wrapper cases covering inherited stamp isolation/default and explicit entrypoint, Windows SDK-key canonicalization/default/duplicate-key order, and configured lookup/final environment/config-dir conjunction. Keep direct auth/session sanitation and Windows-versus-Linux overlay checks, actual executable lookup from supplied inherited PATH, native Windows child environment delivery, and stale option-mutation fencing. These are not equivalent replacements for the retired wrapper alarms. No fresh test/runtime, real login-shell probe, Claude SDK startup, remote/mixed-version, UI or performance result is claimed. Audit3114 incorporates source3105: retire 17 authored/20 expanded daemon launch-note cases, including the entire two-case SessionLaunchNote suite. Deliberately lose split-marker carry across same-token re-arm, stale-token retirement refusal, foreground tri-state and serial settlement, omitted-note/no-incarnation mixed-version policy, sticky retirement, first-incarnation/replacement launch preservation and held-launch precedence. Keep only the unchanged unresolved foreground-read race against a later finished attach. Supplied notes/foreground and runtime calls do not establish actual daemon/main restart, SSH, native Windows observation, mobile UI, equivalent replacement or fresh runtime/performance validation. Audit3114 incorporates source3109: retire six authored/35 expanded pricing cases: 21 accepted model-name examples, 10 unsupported/future-model fail-closed examples, malformed TTL clamp, unknown-model estimate refusal, flat-model long-context disregard and oversized long-context clamp. Keep mixed five-minute/one-hour write allocation and short/all-long/mixed long-context allocation with original fixed oracles. Real parser/refusal/bounds alarms are knowingly lost without equivalent replacement; retained tests do not validate provider rates, actual billing or request threshold classification. Preserve the archived usage-scan-append source-freeze receipt without transferring historical execution/performance credit. No fresh runtime result is claimed. Audit3108 source-only reduction (3094/3095/3100): retire 27 authored/30 expanded cases, including complete markdown view-state, file-preview link-open and CLI agent-server-route suites, without replacement. Deliberate losses include preview visibility/reference lifetime and same-path owners; SSH stat, directory/missing-target, explicit-null owner, nonmarkdown line/column, local URI routing, remote-to-client guard and same-file reuse; document-preview source-owner/folder/reuse routing, stable grant location and outside-workspace POSIX/Windows request projection; and verified remote CLI send/check refusal, suggested --on argv/copy, shell no-probe bypass, unverified-launch admission and older-peer refusal. Retain two markdown source-runtime/qualified-reveal alarms, one stale grant-rejection fence and two remote read/mutation evidence policies, with original callbacks. Final last-wins reveal did not prove transient cancellation; CLI prerequisite did not prove no real send, and envelope tests do not execute server refusal. No fresh runtime, rendered UI, actual file/SSH/paired transport, authorization, mixed-version or performance result is claimed. Audit3096 source-only reduction (3087/3088): retire 69 authored/76 expanded cases, including complete recovery-portals and reconnect-banner suites. Intentionally lose dead/default TUI replay, generated alternate-frame style composition, reset/mode/dimension and legacy missing-mode-boundary alarms, UI notice/action/reveal policies, and isolated recovery lifecycle variants, without equivalent replacement. Retained registrations and callbacks are unchanged; headless cells are not pixels, and no fresh runtime, host, SSH, WSL, rendered-app or performance result is claimed. Audit3086 source-only reduction (3079/3080/3081): intentionally retire 93 authored/100 expanded mock-boundary cases, including the complete spawn-errors and SSH reconnect overlay suites, without replacement. Retired alarms include spawn classification/ownership, reply ceilings and renderer-write failure recovery, disconnect/detach fencing branches, authority facts, and overlay retry/visibility wiring. Retained callbacks are unchanged; no fresh execution, real PTY/SSH, rendered UI, or performance result is claimed. The direct side-effect-facts selector retains six cases: replay exclusion, live sequence handling, stale disposer ownership, one handoff drain, drain-before-snapshot, and stale replacement-snapshot refusal. Successful current-entry snapshot application is not witnessed by the retained snapshot case. Audit2432: The broad unit command loses the isolated BranchPicker open-change notification sample. Its unique true/false callback alarm is intentionally retired; reopening/status-refresh behavior was not exercised. No equivalent replacement or fresh validation of the reduced selector is claimed. Audit2425: The broad unit command loses the workspace-copy-status supplied readError forwarding sample. It did not exercise the branch-switch blocking promised by its title. This alarm is intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2417: The broad unit command loses two ordinary clean Git cherry-pick/revert success samples and two terminal startup foreground-read work-budget samples. These unique alarms are intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2412: The broad unit command loses the whole six-case fake-React-hook scroll-anchor suite, two generic draft-retention samples, a simple new-agent order sample, a real history-publication batching-count sample, an unmounted handoff abort/timer sample and three basic WSL preview/error samples. These alarms are intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2404: The broad unit command loses the standalone share-name lookup 503 propagation suite, initial deletion placement-kind sample, direct changed-mtime stale sample, first-pass rollback existence example and abandoned-preparation directory sample. These exact alarms are intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2397 review amendment: The broad unit command loses three standalone suites, two partial-case definitions and two URL table rows across package/download/placement tests. Keep initial wrong-port authorization, modified independent-copy digest refusal and abort-after-begin cancellation dispatch. The remaining alarms are deliberately retired; no equivalent complete replacement or fresh validation of the reduced selector is claimed. Audit2393 current-parent refresh2401: Remove the basic skill-install service alias sample and original public managed-remove receiptless refusal case. Preserve the upstream request ownership suite and transaction interruption coverage. No equivalent complete replacement or fresh validation of the reduced selector is claimed. Audit2389: The broad unit command loses the missing-preview suite, plain-global destination sample and prepublished-receipt recovery example. No equivalent replacement or fresh validation of the reduced selector is claimed. Audit2385: The broad unit command loses four authored walker/staging/protocol examples (six expanded cases); no equivalent replacement or fresh validation is claimed for the reduced suite. Audit2383: The unscoped Vitest command no longer includes the removed Antigravity catalogue suite; its exact provider-root/owner example has no equivalent replacement or fresh validation claim. Audit2377/2381: The unscoped Vitest command includes the reduced bundled-skill and native-discovery suites; the eight removed backup/legacy/catalogue/symlink/SSH-builder examples have no replacement-equivalence or fresh validation claim here. Audit2133 current-parent preparation: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2095 current-parent preparation2126: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2090 (sources2088/2089): Current reduced suites intentionally retire the alarms listed in knownGaps; prior dated execution and replacement claims do not validate changed source bytes or selectors. Historical prior account (preserved verbatim): Audit2086 (sources2082/2083): Current reduced suites intentionally retire the alarms listed in knownGaps; prior dated execution and replacement claims do not validate changed source bytes or selectors. Historical prior account (preserved verbatim): Audit2076 (sources2073/2074): Intentionally retire these low-value alarms without replacement: src/main/runtime/orca-runtime-tests/terminal-handles-part-02.spec.ts: recovers a moved SSH pane through the tab it now sits in, not the one its lease froze; reports a plain expired orphan; does not report a pane whose only recent lease was superseded; does not report a pane whose only recent lease had its relay id recycled; picks the eligible orphan over a superseded predecessor that matches first; collects the same tabs the per-tab reader reports, in one sweep of the leases; reports no candidate when no lease names any of the worktree tabs. src/main/runtime/orca-runtime-tests/runtime-availability.spec.ts: starts unavailable with no authoritative window; reports the configured Windows terminal shell on status; reports floating workspace availability from settings on status; polls floating tabs with targeted PTY liveness and no repo/provider inventory; hydrates persisted tabs when the store cannot report repos; advertises browser screencast only when a renderer window is available; routes mobile Codex reset consumption through the account mutation coordinator; maps a definite pre-provider rejection into an authoritative current snapshot. Existing unrelated evidenceRuns retain their original scope; unchanged dated receipt objects do not validate changed source bytes or reduced selectors. No fresh gate execution or historical import-closure equivalence is claimed. Frozen current literal-import graph reaches these changed test bytes; unresolved local and computed edges remain debt, and current reachability is not historical execution or equivalent replacement evidence. Historical prior account (preserved verbatim): Audit2003: Remove SSH/absent-inventory attach refusal, later-subscriber retry, no-view adoption/takeback, provider-tail/unknown-read projections, failed-keystroke retry and simpler loss reattach examples. Keep the existing-subscriber inventory-ready retry alias only at its retained callback, plus retained held-input cancellation/replacement and overlapping attach/loss fences. The inventory driver also loses the whole empty-worktree-publication registration and folder-host/unverifiable scalar examples. Retire the entire dated10-case attach receipt; retain16 independent receipts only in their original scopes. No retained-memory or real-daemon measurement follows from supplied provider inventories. Changed-source prior results do not validate reduced-source bytes. No fresh execution, equivalent replacement, red/green, measured budget, soak, physical provider/PTY, rendered, live remote/mobile or cross-platform result is claimed. Historical prior account (preserved verbatim): Audit1991: Remove initial-snapshot sequence-domain, limited-tail/truncation and unknown-output-sequence aliases whose callbacks are removed. The source retains partial-chunk replay and requested-snapshot retry/admission at supplied snapshot/data seams, not equivalent replacement or measured retained-memory proof. Preserve the existing stream/pending-wait capacity aliases only to their retained owning multiplex callbacks with supplied PTY resolutions. Removing the sibling-PTY heartbeat dedup work-count sample does not establish a new decorative fanout budget; the unrelated-PTY cancellation fence survives. Preserve17 unrelated dated receipts. Changed-source prior results do not validate reduced-source bytes. No fresh execution, equivalent replacement, red/green, measured budget, soak, physical provider/PTY, rendered, live remote/mobile or cross-platform result is claimed. Historical prior account (preserved verbatim): Audit1983: Remove the reasonless-close selector and narrow census aliases to actual retained runtime and injected-inventory callbacks. The removed synthetic census-dedup and isolated pre-boundary timing cases receive no replacement credit; preserve17 unrelated dated receipts. Changed-source prior results do not validate reduced-source bytes. No fresh execution, equivalent replacement, red/green, measured budget, soak, provider/PTY, rendered, live remote/mobile or cross-platform result is claimed. Historical prior account (preserved verbatim): Audit1939 (sources1934/1935): Delete the synthetic watcher-synchronization and partial-reconciliation suites. Retire all affected host-fact, capability-owner, activation-residue and retention-cap aliases because the remaining callbacks do not establish those full claims. Keep exact local hook/timer, byte-stream side effects, exit/disposal, scroll-intent cleanup and deterministic local rank/TTL progression callbacks within their mocked-host scope. No physical paired-host retention, resource budget or equivalent replacement evidence is assigned. The generic broad test command remains a runnable discovery command, never a dated execution receipt. Preserve meaningful remaining selectors/assertions and unchanged dated receipts within their original scope. Reduced selectors have no fresh gate execution evidence in this metadata packet. No equivalent replacement, physical watcher/PTY/provider, live SSH/remote/mobile, rendered, cross-platform, soak or measured-budget result is claimed. Historical prior account (preserved verbatim): Audit1931 (sources1928/1929): Delete the complete hook-agent-status projection suite and its two behavioral assertion aliases. Supplied stored hook/title/resume/staleness/rename/question/generation/PTY-retirement/snapshot-version projections are intentionally lost. The separate osc-status-scan gate mentions this source only in existing historical notes and receives no mutation. No measured hidden-output retention, CPU/RSS or resource budget result is claimed by the deleted source or this audit. Preserve meaningful remaining selectors/assertions and unchanged dated receipts within their original scope. Reduced selectors have no fresh gate execution evidence in this metadata packet. No equivalent replacement, physical watcher/PTY/provider, live SSH/remote/mobile, rendered, cross-platform, soak or measured-budget result is claimed. Historical prior account (preserved verbatim): Retained exact unchanged-source receipts remain the only current execution evidence. Reduced selectors have no fresh execution result. Historical pre-audit oracle account for changed sources: The retained unchanged-source receipts are the only current execution evidence; reduced-source assertions await validation. Historical oracle account (not a current claim for changed sources): Existing exact unchanged-selector receipts remain the only current execution evidence. Changed source selectors have no fresh execution result. Historical prior-source oracle account: Existing exact unchanged-selector receipts remain the only current execution evidence. Changed source selectors have no fresh execution result. Historical prior-source oracle account: Run one byte-identical six-terminal oracle against an isolated headed desktop host and an isolated headless `orca serve` host. Stage at least 1,000,000 xterm cells, hide all six warm managers, emit an initial working title followed by 30 timed real-PTY spinner frames and a final idle title per worktree, and observe a second real session-tabs subscription plus the production renderer store. Require exactly two transport envelopes and two title mutations per worktree, no decorative title crossing either boundary, and positive decoded-envelope bytes. Enable ordinary parking with the lossy retention budget disabled, require exactly one mounted manager and five parked tabs, at most 45% retained cells, no more than 16 MiB heap growth, and under 500 ms timer drift. In headed mode, require the host renderer to remain on its original workspace with zero target terminal managers mounted throughout client park, reveal, and live I/O. While a tab is parked, require authoritative terminal.read to observe new PTY output; reveal it and require the original PTY, a marker within the requested 5,000-row history, the parked marker, and post-reveal input/output. Drive 24 real PTY title trackers in distinct worktrees through 40 decorative spinner frames each, then 40 spinner frames alternating with Cursor's bare native identity redraw, using fake clocks. Require all 2,880 raw chunks exactly and zero host session-tabs publications, serialized bytes, renderer apply calls, and renderer store mutations. Continue decorative evidence beyond 30 minutes, require exactly one refresh per worktree per 15-minute lease through a FIFO spaced by at least 50 ms, and require all 24 viewer statuses to remain working and fresh; then require one exact semantic idle transition and one visible-output chunk per PTY. Require ordinary and interior-Braille title changes to publish. Drive 64 host PTYs through ten 80 ms synthetic title frames, require zero paired client events after the first frame per PTY while all 640 local frames remain observable, attach a late client and require one current frame per PTY, then require semantic bell and idle transitions on both clients. Reconstruct a legacy subscribed stream with no outputPause capability, hide a chatty pane, require no hidden xterm writes, reveal it, and require one authoritative snapshot plus continued live output. Drop and acknowledge output only for one original paired-client stream, prove the fixture process consumed input while the host model advanced and the client stayed stale, then require the command snapshot probe to replace that stream, repaint exact fixture output, preserve authoritative PTY identity and target-tab cardinality, and resume live I/O. For a client without a delivered sequence, require the first numeric probe to establish a baseline without replacement, a same-sequence probe to remain attached, and a later advanced probe to replace only that stream. Withhold the first encrypted shared-control ready frame, start one RPC, trigger a status-probe refresh, and require two connections, one host delivery, successful response, and zero retained request bytes. Admit 128 active streams, reject the 129th as retryable, release one stream, then require the retry to attach and publish a snapshot without multiplying retained subscribers. Disable terminal.paired-parking.v1 and require the same oracle to fail before parking, while the legacy limit-one fallback separately passes. Also preserve truncated first paint, ACK-starved same-PTY recovery, responsive silent-command snapshot probes, dead-stream probe timeout recovery, and queued manual-disconnect fencing.", "commands": [ "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/pty-transport/remote-runtime/remote-runtime-pty-transport-unpublished-host-graph.test.ts", "ORCA_BACKGROUND_LAUNCH=1 pnpm exec playwright test tests/e2e/paired-remote-terminal-host-quit-reconnect-input.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1", @@ -9063,7 +9038,14 @@ "Audit2412 removes whole useVirtualizedScrollAnchor.marks-restore-signal.test.ts (six fake-hook geometry/mark/convergence/user-takeover/clamp alarms), use-task-creation-draft-retention original3/4 (immediate discard mock-call count/unopened no-write), use-terminal-bulk-close-actions.placement original6/7 (absent new-agent append and one real history-publication count at consumed3/30 scale), PendingNativeChatCreation original6 (unmounted abort completion/timer cleanup), and wsl-move-dialog-store original1/9/13 (default whole-distro count/reference/reuse and rejected-preview/start refusal recovery). The history counter measures genuine publication work without entries/order/reopen assertions; its unique batching alarm is intentionally retired. Fake React scroll tests ignore dependency scheduling and cleanup; unique simulated arithmetic alarms are deliberately lost. Retained existing-terminal reorder is not new-agent append coverage; mounted handoff timeout is not abort coverage. No equivalent complete replacement or fresh validation is claimed.", "Audit2417 removes commit-actions-real-git original3/4 (ordinary clean cherry-pick and revert content, subject and current-branch samples), and pty-startup-ingress-exited-asker original8/12 (actual foreground-group read budget across100queued queries, and zero foreground reads across100normal chunks plus final emitted/accepted raw cursor equality). Counters measure actual callback work and scale is consumed; these internal-work alarms are deliberately retired. Retained unborn, conflict and sequencer cases are not equivalent ordinary-success coverage. No equivalent complete replacement or fresh validation is claimed.", "Audit2425 removes use-workspace-copy-status original5, which returns a fixture-supplied readError string through statusReadError. No consumer blocker or attempted branch switch established refusal; the unique error-forwarding sample is intentionally retired. Other copy ownership and stale-refresh cases are not equivalent error-path coverage. No equivalent replacement or fresh validation is claimed.", - "Audit2432 removes BranchPicker.test.tsx original3, which only asserted onOpenChange(true/false) with an always-mounted mocked Popover. It never reopened the picker or consumed the callback in the parent status-refresh owner, despite its title. Lose that unique open/close notification alarm. Retained blocker/collision recovery and asynchronous post-unmount outcome cases are not equivalent replacement coverage. No fresh validation of the reduced selector is claimed." + "Audit2432 removes BranchPicker.test.tsx original3, which only asserted onOpenChange(true/false) with an always-mounted mocked Popover. It never reopened the picker or consumed the callback in the parent status-refresh owner, despite its title. Lose that unique open/close notification alarm. Retained blocker/collision recovery and asynchronous post-unmount outcome cases are not equivalent replacement coverage. No fresh validation of the reduced selector is claimed.", + "Audit3086 source-only reduction (3079/3080/3081): intentionally retire 93 authored/100 expanded mock-boundary cases, including the complete spawn-errors and SSH reconnect overlay suites, without replacement. Retired alarms include spawn classification/ownership, reply ceilings and renderer-write failure recovery, disconnect/detach fencing branches, authority facts, and overlay retry/visibility wiring. Retained callbacks are unchanged; no fresh execution, real PTY/SSH, rendered UI, or performance result is claimed. The direct side-effect-facts selector retains six cases: replay exclusion, live sequence handling, stale disposer ownership, one handoff drain, drain-before-snapshot, and stale replacement-snapshot refusal. Successful current-entry snapshot application is not witnessed by the retained snapshot case.", + "Audit3096 source-only reduction (3087/3088): retire 69 authored/76 expanded cases, including complete recovery-portals and reconnect-banner suites. Intentionally lose dead/default TUI replay, generated alternate-frame style composition, reset/mode/dimension and legacy missing-mode-boundary alarms, UI notice/action/reveal policies, and isolated recovery lifecycle variants, without equivalent replacement. Retained registrations and callbacks are unchanged; headless cells are not pixels, and no fresh runtime, host, SSH, WSL, rendered-app or performance result is claimed.", + "Audit3108 source-only reduction (3094/3095/3100): retire 27 authored/30 expanded cases, including complete markdown view-state, file-preview link-open and CLI agent-server-route suites, without replacement. Deliberate losses include preview visibility/reference lifetime and same-path owners; SSH stat, directory/missing-target, explicit-null owner, nonmarkdown line/column, local URI routing, remote-to-client guard and same-file reuse; document-preview source-owner/folder/reuse routing, stable grant location and outside-workspace POSIX/Windows request projection; and verified remote CLI send/check refusal, suggested --on argv/copy, shell no-probe bypass, unverified-launch admission and older-peer refusal. Retain two markdown source-runtime/qualified-reveal alarms, one stale grant-rejection fence and two remote read/mutation evidence policies, with original callbacks. Final last-wins reveal did not prove transient cancellation; CLI prerequisite did not prove no real send, and envelope tests do not execute server refusal. No fresh runtime, rendered UI, actual file/SSH/paired transport, authorization, mixed-version or performance result is claimed.", + "Audit3114 incorporates source3101: retire three structured Claude child-environment wrapper cases covering inherited stamp isolation/default and explicit entrypoint, Windows SDK-key canonicalization/default/duplicate-key order, and configured lookup/final environment/config-dir conjunction. Keep direct auth/session sanitation and Windows-versus-Linux overlay checks, actual executable lookup from supplied inherited PATH, native Windows child environment delivery, and stale option-mutation fencing. These are not equivalent replacements for the retired wrapper alarms. No fresh test/runtime, real login-shell probe, Claude SDK startup, remote/mixed-version, UI or performance result is claimed.", + "Audit3114 incorporates source3105: retire 17 authored/20 expanded daemon launch-note cases, including the entire two-case SessionLaunchNote suite. Deliberately lose split-marker carry across same-token re-arm, stale-token retirement refusal, foreground tri-state and serial settlement, omitted-note/no-incarnation mixed-version policy, sticky retirement, first-incarnation/replacement launch preservation and held-launch precedence. Keep only the unchanged unresolved foreground-read race against a later finished attach. Supplied notes/foreground and runtime calls do not establish actual daemon/main restart, SSH, native Windows observation, mobile UI, equivalent replacement or fresh runtime/performance validation.", + "Audit3114 incorporates source3109: retire six authored/35 expanded pricing cases: 21 accepted model-name examples, 10 unsupported/future-model fail-closed examples, malformed TTL clamp, unknown-model estimate refusal, flat-model long-context disregard and oversized long-context clamp. Keep mixed five-minute/one-hour write allocation and short/all-long/mixed long-context allocation with original fixed oracles. Real parser/refusal/bounds alarms are knowingly lost without equivalent replacement; retained tests do not validate provider rates, actual billing or request threshold classification. Preserve the archived usage-scan-append source-freeze receipt without transferring historical execution/performance credit. No fresh runtime result is claimed.", + "Audit3128 scoped coverage reduction: Audit3121 removes VoiceDictationLanguageSetting.test.tsx (Spanish-to-es callback and unsupported-model Korean option presence) and voice-speech-model-rows.test.ts (provider/language-order/retired-on-disk list policy); these real narrow alarms are deliberately retired without replacement. VoiceSpeechModelSection per-model concurrent deletion remains unchanged. Audit3122 removes the direct absolute/parent-segment sparse-directory exception-type sample and the five-row IPC sparse-preset error-code/non-save table (two authored/six expanded cases). Those real typed-validation and IPC refusal/error-code alarms are deliberately retired. Keep both real symlink/junction floating-directory trust cases and the minimal RPC structured validation-reason serialization case. The RPC case does not replace IPC save/refusal behavior, typed normalization for both path inputs, actual Git sparse checkout or rendered localization. No equivalent replacement or fresh test/runtime/platform/remote/performance validation is claimed. Audit3124 removes ExperimentalPane.test.tsx (ordinary Pet switch callback), WorkspaceDirectorySetting.test.tsx (padded absolute saved-path normalization/no-write blur), and repository-search.perf.test.ts (navigation/selected-pane builder-call policy, fixture cardinality/nonempty entries and renamed project keyword inclusion). These real narrow alarms are retired without replacement; RepositoryPane.search.test.tsx search-to-sparse-editor request remains unchanged. Audit3129: resolver Windows case-insensitive and POSIX case-sensitive configured-env composition plus mocked-base launch-preflight Windows composition are intentionally no longer checked by the two deleted suites. No equivalent coverage claimed. Keep runtime-structured-agent-host-env.test.ts unchanged: configured Windows Claude-home account identity and host-account precedence remain checked." ], "demotionRule": "Keep experimental or demote to protection none if decorative title frequency changes paired client-event or session-tabs publication volume after convergence, a semantic fact is dropped, paired first paint is blank, passive work reconnects after manual disconnect, a capable hidden paired manager survives ordinary parking, a legacy hidden pane schedules raw xterm output or bypasses its fallback, reveal loses requested bounded history or live PTY identity, retained buffer cells do not fall, or either headed/headless scenario flakes." }, @@ -9241,7 +9223,7 @@ "providers": ["local", "daemon", "ssh", "remote-runtime"], "coveredPlatforms": ["macos"], "coveredProviders": [], - "coverageNotes": "Module layout SSH policy: Retire the multiplexer private-clock saturation registration, the writer saturation transition-count registration and the changed-source dated writer/backpressure receipt. Retained byte delivery, lane order, fairness, settlement and cancellation cases are unvalidated by that historical receipt. No equivalent replacement, new execution, measured performance, live SSH/WSL or cross-platform proof is claimed. The unchanged WSL protocol receipt retains only its original command scope. Historical prior account (preserved verbatim): Audit2432: The broad unit command loses the isolated BranchPicker open-change notification sample. Its unique true/false callback alarm is intentionally retired; reopening/status-refresh behavior was not exercised. No equivalent replacement or fresh validation of the reduced selector is claimed. Audit2425: The broad unit command loses the workspace-copy-status supplied readError forwarding sample. It did not exercise the branch-switch blocking promised by its title. This alarm is intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2417: The broad unit command loses two ordinary clean Git cherry-pick/revert success samples and two terminal startup foreground-read work-budget samples. These unique alarms are intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2412: The broad unit command loses the whole six-case fake-React-hook scroll-anchor suite, two generic draft-retention samples, a simple new-agent order sample, a real history-publication batching-count sample, an unmounted handoff abort/timer sample and three basic WSL preview/error samples. These alarms are intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2404: The broad unit command loses the standalone share-name lookup 503 propagation suite, initial deletion placement-kind sample, direct changed-mtime stale sample, first-pass rollback existence example and abandoned-preparation directory sample. These exact alarms are intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2397 review amendment: The broad unit command loses three standalone suites, two partial-case definitions and two URL table rows across package/download/placement tests. Keep initial wrong-port authorization, modified independent-copy digest refusal and abort-after-begin cancellation dispatch. The remaining alarms are deliberately retired; no equivalent complete replacement or fresh validation of the reduced selector is claimed. Audit2393 current-parent refresh2401: Remove the basic skill-install service alias sample and original public managed-remove receiptless refusal case. Preserve the upstream request ownership suite and transaction interruption coverage. No equivalent complete replacement or fresh validation of the reduced selector is claimed. Audit2389: The broad unit command loses the missing-preview suite, plain-global destination sample and prepublished-receipt recovery example. No equivalent replacement or fresh validation of the reduced selector is claimed. Audit2385: The broad unit command loses four authored walker/staging/protocol examples (six expanded cases); no equivalent replacement or fresh validation is claimed for the reduced suite. Audit2383: The unscoped Vitest command no longer includes the removed Antigravity catalogue suite; its exact provider-root/owner example has no equivalent replacement or fresh validation claim. Audit2377/2381: The unscoped Vitest command includes the reduced bundled-skill and native-discovery suites; the eight removed backup/legacy/catalogue/symlink/SSH-builder examples have no replacement-equivalence or fresh validation claim here. Audit2133 current-parent preparation: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2095 current-parent preparation2126: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2090 (sources2088/2089): Current reduced suites intentionally retire the alarms listed in knownGaps; prior dated execution and replacement claims do not validate changed source bytes or selectors. Historical prior account (preserved verbatim): Audit2086 (sources2082/2083): Current reduced suites intentionally retire the alarms listed in knownGaps; prior dated execution and replacement claims do not validate changed source bytes or selectors. Historical prior account (preserved verbatim): Audit2076 (sources2073/2074): Intentionally retire these low-value alarms without replacement: src/main/runtime/orca-runtime-tests/terminal-handles-part-02.spec.ts: recovers a moved SSH pane through the tab it now sits in, not the one its lease froze; reports a plain expired orphan; does not report a pane whose only recent lease was superseded; does not report a pane whose only recent lease had its relay id recycled; picks the eligible orphan over a superseded predecessor that matches first; collects the same tabs the per-tab reader reports, in one sweep of the leases; reports no candidate when no lease names any of the worktree tabs. src/main/runtime/orca-runtime-tests/runtime-availability.spec.ts: starts unavailable with no authoritative window; reports the configured Windows terminal shell on status; reports floating workspace availability from settings on status; polls floating tabs with targeted PTY liveness and no repo/provider inventory; hydrates persisted tabs when the store cannot report repos; advertises browser screencast only when a renderer window is available; routes mobile Codex reset consumption through the account mutation coordinator; maps a definite pre-provider rejection into an authoritative current snapshot. Existing unrelated evidenceRuns retain their original scope; unchanged dated receipt objects do not validate changed source bytes or reduced selectors. No fresh gate execution or historical import-closure equivalence is claimed. Frozen current literal-import graph reaches these changed test bytes; unresolved local and computed edges remain debt, and current reachability is not historical execution or equivalent replacement evidence. Historical prior account (preserved verbatim): Audit2070 (sources2068/2069): Intentionally retire these low-value alarms without replacement: src/main/runtime/orca-runtime.test.ts: ordinary registration cleanup removes only await import('./orca-runtime-tests/hooks-and-hosted-review.spec'). src/main/ssh/ssh-multiplexer-transport-writer.test.ts: reports saturated epochs and their drain boundary exactly once. src/main/runtime/orca-runtime-tests/worktree-removal-archive-hook-gate.spec.ts: removes and records the waiver when the failure is explicitly overridden; removes without an override record when the hook succeeds; removes when the hook is configured but not requested; removes when no archive hook is configured; holds a desktop removal in the same repo until this one is accepted; joins a desktop removal of the same worktree accepted while this one waited. src/main/runtime/orca-runtime-tests/worktree-ps-agent-row-dismissal.spec.ts: drops the row as soon as the user dismisses it, without waiting for the PTY to exit; tells paired clients to republish on the transition and on the dismissal; rejoins a row through its %s handle after pane ownership clears. src/main/runtime/orca-runtime-tests/hooks-and-hosted-review.spec.ts: reports an error when the SSH filesystem provider is unavailable; reports an SSH orca.yaml as an error without reading any filesystem; reports ok for a local repo hook check; reads SSH issue commands as absent and drops writes without touching any filesystem; allows host integration slug helpers for SSH repos through provider-aware GitHub clients; routes runtime GitHub repo identity helpers through the selected WSL project runtime; routes runtime GitHub issue and work-item actions through the selected WSL project runtime; pins explicit origin preference on runtime open-by-number work item lookups; forwards check-details cancellation without local Git overrides. Existing unrelated evidenceRuns retain their original scope; unchanged dated receipt objects do not validate changed source bytes or reduced selectors. No fresh gate execution or historical import-closure equivalence is claimed. Frozen current literal-import graph reaches these changed test bytes; unresolved local and computed edges remain debt, and current reachability is not historical execution or equivalent replacement evidence. The historical receipt/currentCommand selecting src/main/ssh/ssh-multiplexer-transport-writer.test.ts remains historical and is not a passing result for the reduced current command. Historical prior account (preserved verbatim): Audit1931 (sources1928/1929): Delete the complete tail-wait memo suite: its sole current authored callback checks supplied fromTail/blocked field values. Retire all three broader memoization/equivalence/eviction assertion aliases; they are not backed by retained source callbacks. No scan-work budget, sustained output, physical terminal or resource measurement is assigned to this removed test. Preserve meaningful remaining selectors/assertions and unchanged dated receipts within their original scope. Reduced selectors have no fresh gate execution evidence in this metadata packet. No equivalent replacement, physical watcher/PTY/provider, live SSH/remote/mobile, rendered, cross-platform, soak or measured-budget result is claimed. Historical prior account (preserved verbatim): Audit1674 removes low-value test projections, mock wiring, private-state assertions, inventories and duplicate examples without equivalent replacement coverage. Per-source reviews record retained public behavior and refusal boundaries with injected/native/remote limits. Affected historical receipts are retired; gates without active current evidence are downgraded. Historical scope before this audit: Audit1634 removes low-value IPC, filesystem and runtime projections, argument inventories and duplicate examples without equivalent replacement coverage. Per-source reviews record retained cancellation, ownership, authorization, filesystem and SQLite boundaries with injected/native/remote limits. Affected historical receipts are retired; gates without active current evidence are downgraded. Historical scope before this audit: Local macOS evidence covers main-process pending-output and projection-admission caps, saturated-tail wait detection, direct-SSH source retention/accounting, always-on V1 negotiation with legacy peer fallback, exact provider-generation pause ownership, renderer exit/data ordering, decoder input bounds, bounded reconnect scheduling, and WSL stdio transport settlement through deterministic unit contracts. A joined main/runtime oracle exercises renderer-sourced headed semantics and headless-model snapshot semantics without launching a live paired server. Separately, a macOS-hosted Docker OpenSSH run exercises only the deployed Linux relay and direct SSH provider with an exact 256 KiB source-credit plateau, concurrent PTY typing, fixed-size filesystem frames, Git churn, and owner-lease reconnect. Neither deterministic topology labels nor Docker SSH constitute live headed desktop, headless orca serve, or physical WSL evidence; local/daemon, prior-version daemons, Windows named pipes/ConPTY, folder workspaces, and mixed-version clients also remain uncovered. Audit1312 removes low-value imported specs from aggregate test drivers; old whole-driver execution receipts are archived. Retained registrations are unchanged. No equivalent replacement coverage is claimed. Audit1383: src/main/runtime/orca-runtime-tests/worktree-native-chat-setup.spec.ts reduced. Lose command-list/receipt/default/background activation matrices whose provider spawn returns canned IDs. Retained test proves actual tab allocation separation, not terminal startup. No equivalent replacement coverage is claimed. Existing exact unaffected receipts and their platform/provider/budget claims remain scoped to unchanged selectors. Changed optional selectors have no new execution result. Audit1401: src/main/runtime/orca-runtime-tests/mobile-session-tabs-duplicate-pty-repair.spec.ts removed. Loses listing-on-already-repaired-data alarm; it never established runtime repair invocation. src/main/runtime/orca-runtime-tests/mobile-session-tabs-split-agent-tab.spec.ts removed. Loses actual mobile split sibling shell versus recorded-agent resume command alarms; no equivalent coverage asserted. src/main/runtime/orca-runtime-tests/worktree-native-chat-setup.spec.ts removed. Loses separate setup/startup tab result in that manually assembled path. src/main/runtime/orca-runtime.test.ts reduced. Driver registration/import of a whole-deleted selected spec is removed; directly authored cases remain unchanged. No extra authored removal credit. No equivalent replacement coverage is claimed. Existing exact unaffected receipts remain unchanged and apply only to their unchanged selectors. Changed optional selectors have no new execution result.", + "coverageNotes": "Audit3128 scoped coverage reduction: Audit3121 removes VoiceDictationLanguageSetting.test.tsx (Spanish-to-es callback and unsupported-model Korean option presence) and voice-speech-model-rows.test.ts (provider/language-order/retired-on-disk list policy); these real narrow alarms are deliberately retired without replacement. VoiceSpeechModelSection per-model concurrent deletion remains unchanged. Audit3122 removes the direct absolute/parent-segment sparse-directory exception-type sample and the five-row IPC sparse-preset error-code/non-save table (two authored/six expanded cases). Those real typed-validation and IPC refusal/error-code alarms are deliberately retired. Keep both real symlink/junction floating-directory trust cases and the minimal RPC structured validation-reason serialization case. The RPC case does not replace IPC save/refusal behavior, typed normalization for both path inputs, actual Git sparse checkout or rendered localization. No equivalent replacement or fresh test/runtime/platform/remote/performance validation is claimed. Audit3124 removes ExperimentalPane.test.tsx (ordinary Pet switch callback), WorkspaceDirectorySetting.test.tsx (padded absolute saved-path normalization/no-write blur), and repository-search.perf.test.ts (navigation/selected-pane builder-call policy, fixture cardinality/nonempty entries and renamed project keyword inclusion). These real narrow alarms are retired without replacement; RepositoryPane.search.test.tsx search-to-sparse-editor request remains unchanged. Audit3129: resolver Windows case-insensitive and POSIX case-sensitive configured-env composition plus mocked-base launch-preflight Windows composition are intentionally no longer checked by the two deleted suites. No equivalent coverage claimed. Keep runtime-structured-agent-host-env.test.ts unchanged: configured Windows Claude-home account identity and host-account precedence remain checked. Audit3114 incorporates source3101: retire three structured Claude child-environment wrapper cases covering inherited stamp isolation/default and explicit entrypoint, Windows SDK-key canonicalization/default/duplicate-key order, and configured lookup/final environment/config-dir conjunction. Keep direct auth/session sanitation and Windows-versus-Linux overlay checks, actual executable lookup from supplied inherited PATH, native Windows child environment delivery, and stale option-mutation fencing. These are not equivalent replacements for the retired wrapper alarms. No fresh test/runtime, real login-shell probe, Claude SDK startup, remote/mixed-version, UI or performance result is claimed. Audit3114 incorporates source3105: retire 17 authored/20 expanded daemon launch-note cases, including the entire two-case SessionLaunchNote suite. Deliberately lose split-marker carry across same-token re-arm, stale-token retirement refusal, foreground tri-state and serial settlement, omitted-note/no-incarnation mixed-version policy, sticky retirement, first-incarnation/replacement launch preservation and held-launch precedence. Keep only the unchanged unresolved foreground-read race against a later finished attach. Supplied notes/foreground and runtime calls do not establish actual daemon/main restart, SSH, native Windows observation, mobile UI, equivalent replacement or fresh runtime/performance validation. Audit3114 incorporates source3109: retire six authored/35 expanded pricing cases: 21 accepted model-name examples, 10 unsupported/future-model fail-closed examples, malformed TTL clamp, unknown-model estimate refusal, flat-model long-context disregard and oversized long-context clamp. Keep mixed five-minute/one-hour write allocation and short/all-long/mixed long-context allocation with original fixed oracles. Real parser/refusal/bounds alarms are knowingly lost without equivalent replacement; retained tests do not validate provider rates, actual billing or request threshold classification. Preserve the archived usage-scan-append source-freeze receipt without transferring historical execution/performance credit. No fresh runtime result is claimed. Audit3108 source-only reduction (3094/3095/3100): retire 27 authored/30 expanded cases, including complete markdown view-state, file-preview link-open and CLI agent-server-route suites, without replacement. Deliberate losses include preview visibility/reference lifetime and same-path owners; SSH stat, directory/missing-target, explicit-null owner, nonmarkdown line/column, local URI routing, remote-to-client guard and same-file reuse; document-preview source-owner/folder/reuse routing, stable grant location and outside-workspace POSIX/Windows request projection; and verified remote CLI send/check refusal, suggested --on argv/copy, shell no-probe bypass, unverified-launch admission and older-peer refusal. Retain two markdown source-runtime/qualified-reveal alarms, one stale grant-rejection fence and two remote read/mutation evidence policies, with original callbacks. Final last-wins reveal did not prove transient cancellation; CLI prerequisite did not prove no real send, and envelope tests do not execute server refusal. No fresh runtime, rendered UI, actual file/SSH/paired transport, authorization, mixed-version or performance result is claimed. Audit3096 source-only reduction (3087/3088): retire 69 authored/76 expanded cases, including complete recovery-portals and reconnect-banner suites. Intentionally lose dead/default TUI replay, generated alternate-frame style composition, reset/mode/dimension and legacy missing-mode-boundary alarms, UI notice/action/reveal policies, and isolated recovery lifecycle variants, without equivalent replacement. Retained registrations and callbacks are unchanged; headless cells are not pixels, and no fresh runtime, host, SSH, WSL, rendered-app or performance result is claimed. Audit3086 source-only reduction (3079/3080/3081): intentionally retire 93 authored/100 expanded mock-boundary cases, including the complete spawn-errors and SSH reconnect overlay suites, without replacement. Retired alarms include spawn classification/ownership, reply ceilings and renderer-write failure recovery, disconnect/detach fencing branches, authority facts, and overlay retry/visibility wiring. Retained callbacks are unchanged; no fresh execution, real PTY/SSH, rendered UI, or performance result is claimed. Module layout SSH policy: Retire the multiplexer private-clock saturation registration, the writer saturation transition-count registration and the changed-source dated writer/backpressure receipt. Retained byte delivery, lane order, fairness, settlement and cancellation cases are unvalidated by that historical receipt. No equivalent replacement, new execution, measured performance, live SSH/WSL or cross-platform proof is claimed. The unchanged WSL protocol receipt retains only its original command scope. Historical prior account (preserved verbatim): Audit2432: The broad unit command loses the isolated BranchPicker open-change notification sample. Its unique true/false callback alarm is intentionally retired; reopening/status-refresh behavior was not exercised. No equivalent replacement or fresh validation of the reduced selector is claimed. Audit2425: The broad unit command loses the workspace-copy-status supplied readError forwarding sample. It did not exercise the branch-switch blocking promised by its title. This alarm is intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2417: The broad unit command loses two ordinary clean Git cherry-pick/revert success samples and two terminal startup foreground-read work-budget samples. These unique alarms are intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2412: The broad unit command loses the whole six-case fake-React-hook scroll-anchor suite, two generic draft-retention samples, a simple new-agent order sample, a real history-publication batching-count sample, an unmounted handoff abort/timer sample and three basic WSL preview/error samples. These alarms are intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2404: The broad unit command loses the standalone share-name lookup 503 propagation suite, initial deletion placement-kind sample, direct changed-mtime stale sample, first-pass rollback existence example and abandoned-preparation directory sample. These exact alarms are intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2397 review amendment: The broad unit command loses three standalone suites, two partial-case definitions and two URL table rows across package/download/placement tests. Keep initial wrong-port authorization, modified independent-copy digest refusal and abort-after-begin cancellation dispatch. The remaining alarms are deliberately retired; no equivalent complete replacement or fresh validation of the reduced selector is claimed. Audit2393 current-parent refresh2401: Remove the basic skill-install service alias sample and original public managed-remove receiptless refusal case. Preserve the upstream request ownership suite and transaction interruption coverage. No equivalent complete replacement or fresh validation of the reduced selector is claimed. Audit2389: The broad unit command loses the missing-preview suite, plain-global destination sample and prepublished-receipt recovery example. No equivalent replacement or fresh validation of the reduced selector is claimed. Audit2385: The broad unit command loses four authored walker/staging/protocol examples (six expanded cases); no equivalent replacement or fresh validation is claimed for the reduced suite. Audit2383: The unscoped Vitest command no longer includes the removed Antigravity catalogue suite; its exact provider-root/owner example has no equivalent replacement or fresh validation claim. Audit2377/2381: The unscoped Vitest command includes the reduced bundled-skill and native-discovery suites; the eight removed backup/legacy/catalogue/symlink/SSH-builder examples have no replacement-equivalence or fresh validation claim here. Audit2133 current-parent preparation: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2095 current-parent preparation2126: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2090 (sources2088/2089): Current reduced suites intentionally retire the alarms listed in knownGaps; prior dated execution and replacement claims do not validate changed source bytes or selectors. Historical prior account (preserved verbatim): Audit2086 (sources2082/2083): Current reduced suites intentionally retire the alarms listed in knownGaps; prior dated execution and replacement claims do not validate changed source bytes or selectors. Historical prior account (preserved verbatim): Audit2076 (sources2073/2074): Intentionally retire these low-value alarms without replacement: src/main/runtime/orca-runtime-tests/terminal-handles-part-02.spec.ts: recovers a moved SSH pane through the tab it now sits in, not the one its lease froze; reports a plain expired orphan; does not report a pane whose only recent lease was superseded; does not report a pane whose only recent lease had its relay id recycled; picks the eligible orphan over a superseded predecessor that matches first; collects the same tabs the per-tab reader reports, in one sweep of the leases; reports no candidate when no lease names any of the worktree tabs. src/main/runtime/orca-runtime-tests/runtime-availability.spec.ts: starts unavailable with no authoritative window; reports the configured Windows terminal shell on status; reports floating workspace availability from settings on status; polls floating tabs with targeted PTY liveness and no repo/provider inventory; hydrates persisted tabs when the store cannot report repos; advertises browser screencast only when a renderer window is available; routes mobile Codex reset consumption through the account mutation coordinator; maps a definite pre-provider rejection into an authoritative current snapshot. Existing unrelated evidenceRuns retain their original scope; unchanged dated receipt objects do not validate changed source bytes or reduced selectors. No fresh gate execution or historical import-closure equivalence is claimed. Frozen current literal-import graph reaches these changed test bytes; unresolved local and computed edges remain debt, and current reachability is not historical execution or equivalent replacement evidence. Historical prior account (preserved verbatim): Audit2070 (sources2068/2069): Intentionally retire these low-value alarms without replacement: src/main/runtime/orca-runtime.test.ts: ordinary registration cleanup removes only await import('./orca-runtime-tests/hooks-and-hosted-review.spec'). src/main/ssh/ssh-multiplexer-transport-writer.test.ts: reports saturated epochs and their drain boundary exactly once. src/main/runtime/orca-runtime-tests/worktree-removal-archive-hook-gate.spec.ts: removes and records the waiver when the failure is explicitly overridden; removes without an override record when the hook succeeds; removes when the hook is configured but not requested; removes when no archive hook is configured; holds a desktop removal in the same repo until this one is accepted; joins a desktop removal of the same worktree accepted while this one waited. src/main/runtime/orca-runtime-tests/worktree-ps-agent-row-dismissal.spec.ts: drops the row as soon as the user dismisses it, without waiting for the PTY to exit; tells paired clients to republish on the transition and on the dismissal; rejoins a row through its %s handle after pane ownership clears. src/main/runtime/orca-runtime-tests/hooks-and-hosted-review.spec.ts: reports an error when the SSH filesystem provider is unavailable; reports an SSH orca.yaml as an error without reading any filesystem; reports ok for a local repo hook check; reads SSH issue commands as absent and drops writes without touching any filesystem; allows host integration slug helpers for SSH repos through provider-aware GitHub clients; routes runtime GitHub repo identity helpers through the selected WSL project runtime; routes runtime GitHub issue and work-item actions through the selected WSL project runtime; pins explicit origin preference on runtime open-by-number work item lookups; forwards check-details cancellation without local Git overrides. Existing unrelated evidenceRuns retain their original scope; unchanged dated receipt objects do not validate changed source bytes or reduced selectors. No fresh gate execution or historical import-closure equivalence is claimed. Frozen current literal-import graph reaches these changed test bytes; unresolved local and computed edges remain debt, and current reachability is not historical execution or equivalent replacement evidence. The historical receipt/currentCommand selecting src/main/ssh/ssh-multiplexer-transport-writer.test.ts remains historical and is not a passing result for the reduced current command. Historical prior account (preserved verbatim): Audit1931 (sources1928/1929): Delete the complete tail-wait memo suite: its sole current authored callback checks supplied fromTail/blocked field values. Retire all three broader memoization/equivalence/eviction assertion aliases; they are not backed by retained source callbacks. No scan-work budget, sustained output, physical terminal or resource measurement is assigned to this removed test. Preserve meaningful remaining selectors/assertions and unchanged dated receipts within their original scope. Reduced selectors have no fresh gate execution evidence in this metadata packet. No equivalent replacement, physical watcher/PTY/provider, live SSH/remote/mobile, rendered, cross-platform, soak or measured-budget result is claimed. Historical prior account (preserved verbatim): Audit1674 removes low-value test projections, mock wiring, private-state assertions, inventories and duplicate examples without equivalent replacement coverage. Per-source reviews record retained public behavior and refusal boundaries with injected/native/remote limits. Affected historical receipts are retired; gates without active current evidence are downgraded. Historical scope before this audit: Audit1634 removes low-value IPC, filesystem and runtime projections, argument inventories and duplicate examples without equivalent replacement coverage. Per-source reviews record retained cancellation, ownership, authorization, filesystem and SQLite boundaries with injected/native/remote limits. Affected historical receipts are retired; gates without active current evidence are downgraded. Historical scope before this audit: Local macOS evidence covers main-process pending-output and projection-admission caps, saturated-tail wait detection, direct-SSH source retention/accounting, always-on V1 negotiation with legacy peer fallback, exact provider-generation pause ownership, renderer exit/data ordering, decoder input bounds, bounded reconnect scheduling, and WSL stdio transport settlement through deterministic unit contracts. A joined main/runtime oracle exercises renderer-sourced headed semantics and headless-model snapshot semantics without launching a live paired server. Separately, a macOS-hosted Docker OpenSSH run exercises only the deployed Linux relay and direct SSH provider with an exact 256 KiB source-credit plateau, concurrent PTY typing, fixed-size filesystem frames, Git churn, and owner-lease reconnect. Neither deterministic topology labels nor Docker SSH constitute live headed desktop, headless orca serve, or physical WSL evidence; local/daemon, prior-version daemons, Windows named pipes/ConPTY, folder workspaces, and mixed-version clients also remain uncovered. Audit1312 removes low-value imported specs from aggregate test drivers; old whole-driver execution receipts are archived. Retained registrations are unchanged. No equivalent replacement coverage is claimed. Audit1383: src/main/runtime/orca-runtime-tests/worktree-native-chat-setup.spec.ts reduced. Lose command-list/receipt/default/background activation matrices whose provider spawn returns canned IDs. Retained test proves actual tab allocation separation, not terminal startup. No equivalent replacement coverage is claimed. Existing exact unaffected receipts and their platform/provider/budget claims remain scoped to unchanged selectors. Changed optional selectors have no new execution result. Audit1401: src/main/runtime/orca-runtime-tests/mobile-session-tabs-duplicate-pty-repair.spec.ts removed. Loses listing-on-already-repaired-data alarm; it never established runtime repair invocation. src/main/runtime/orca-runtime-tests/mobile-session-tabs-split-agent-tab.spec.ts removed. Loses actual mobile split sibling shell versus recorded-agent resume command alarms; no equivalent coverage asserted. src/main/runtime/orca-runtime-tests/worktree-native-chat-setup.spec.ts removed. Loses separate setup/startup tab result in that manually assembled path. src/main/runtime/orca-runtime.test.ts reduced. Driver registration/import of a whole-deleted selected spec is removed; directly authored cases remain unchanged. No extra authored removal credit. No equivalent replacement coverage is claimed. Existing exact unaffected receipts remain unchanged and apply only to their unchanged selectors. Changed optional selectors have no new execution result.", "motivatingLinks": [ "https://github.com/stablyai/orca/pull/6836", "https://github.com/stablyai/orca/pull/6858", @@ -9249,7 +9231,7 @@ "https://github.com/stablyai/orca/pull/7054" ], "invariant": "High-volume terminal output must stay bounded across daemon socket writes, SSH relay writer/decoder/source retention/reconnect, main model and projection admission, runtime metadata, detectors, and tail checks, main-to-renderer in-flight bytes, renderer scheduler queues, and hidden-output restore without starving focused input. Every new SSH session offers V1 and every same-build relay supports it; legacy delivery is reachable only when capability negotiation proves the peer cannot use V1. Negotiated SSH spans remain exact and contiguous through activation, exit, cancellation, recovery, desktop projection, and required remote replacement; provisional activation data cannot project, reconnect may release it only into the exact attempt's private recovery quarantine after contract validation, and exit seals that quarantine against later same-token frames until ordered admission or exact cancellation proof. Reconnect checkpoint capture freezes exact provider-generation/PTy admission, cancels queued old work, and waits for the running raw completion before exposing its accepted checkpoint; timeout or failure detaches only that PTy's old model, makes the checkpoint unavailable, and releases the bounded fence without closing the shared provider, while the same failure outside an active exact migration remains generation-fatal and overlapping reconnects preserve any earlier outstanding fence. Pending renderer projection IDs are capped and compacted across split remainders, exit preparation owns its renderer fence through finalization, exit-time cancellation transfers published projections before proof commit, generation close fences late proof while draining exact projection waiters, closed-generation identity compacts without weakening stale rejection, canceled source-delivery retirement retains at most one ordered token per PTY, and pause/resume targets only the exact provider generation. Additional subscribers cannot stall the owner, while the required legacy primary retains backpressure.", - "oracle": "Module layout SSH policy: Retire the multiplexer private-clock saturation registration, the writer saturation transition-count registration and the changed-source dated writer/backpressure receipt. Retained byte delivery, lane order, fairness, settlement and cancellation cases are unvalidated by that historical receipt. No equivalent replacement, new execution, measured performance, live SSH/WSL or cross-platform proof is claimed. The unchanged WSL protocol receipt retains only its original command scope. Historical prior account (preserved verbatim): Audit2432: The broad unit command loses the isolated BranchPicker open-change notification sample. Its unique true/false callback alarm is intentionally retired; reopening/status-refresh behavior was not exercised. No equivalent replacement or fresh validation of the reduced selector is claimed. Audit2425: The broad unit command loses the workspace-copy-status supplied readError forwarding sample. It did not exercise the branch-switch blocking promised by its title. This alarm is intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2417: The broad unit command loses two ordinary clean Git cherry-pick/revert success samples and two terminal startup foreground-read work-budget samples. These unique alarms are intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2412: The broad unit command loses the whole six-case fake-React-hook scroll-anchor suite, two generic draft-retention samples, a simple new-agent order sample, a real history-publication batching-count sample, an unmounted handoff abort/timer sample and three basic WSL preview/error samples. These alarms are intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2404: The broad unit command loses the standalone share-name lookup 503 propagation suite, initial deletion placement-kind sample, direct changed-mtime stale sample, first-pass rollback existence example and abandoned-preparation directory sample. These exact alarms are intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2397 review amendment: The broad unit command loses three standalone suites, two partial-case definitions and two URL table rows across package/download/placement tests. Keep initial wrong-port authorization, modified independent-copy digest refusal and abort-after-begin cancellation dispatch. The remaining alarms are deliberately retired; no equivalent complete replacement or fresh validation of the reduced selector is claimed. Audit2393 current-parent refresh2401: Remove the basic skill-install service alias sample and original public managed-remove receiptless refusal case. Preserve the upstream request ownership suite and transaction interruption coverage. No equivalent complete replacement or fresh validation of the reduced selector is claimed. Audit2389: The broad unit command loses the missing-preview suite, plain-global destination sample and prepublished-receipt recovery example. No equivalent replacement or fresh validation of the reduced selector is claimed. Audit2385: The broad unit command loses four authored walker/staging/protocol examples (six expanded cases); no equivalent replacement or fresh validation is claimed for the reduced suite. Audit2383: The unscoped Vitest command no longer includes the removed Antigravity catalogue suite; its exact provider-root/owner example has no equivalent replacement or fresh validation claim. Audit2377/2381: The unscoped Vitest command includes the reduced bundled-skill and native-discovery suites; the eight removed backup/legacy/catalogue/symlink/SSH-builder examples have no replacement-equivalence or fresh validation claim here. Audit2133 current-parent preparation: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2095 current-parent preparation2126: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2090 (sources2088/2089): Current reduced suites intentionally retire the alarms listed in knownGaps; prior dated execution and replacement claims do not validate changed source bytes or selectors. Historical prior account (preserved verbatim): Audit2086 (sources2082/2083): Current reduced suites intentionally retire the alarms listed in knownGaps; prior dated execution and replacement claims do not validate changed source bytes or selectors. Historical prior account (preserved verbatim): Audit2076 (sources2073/2074): Intentionally retire these low-value alarms without replacement: src/main/runtime/orca-runtime-tests/terminal-handles-part-02.spec.ts: recovers a moved SSH pane through the tab it now sits in, not the one its lease froze; reports a plain expired orphan; does not report a pane whose only recent lease was superseded; does not report a pane whose only recent lease had its relay id recycled; picks the eligible orphan over a superseded predecessor that matches first; collects the same tabs the per-tab reader reports, in one sweep of the leases; reports no candidate when no lease names any of the worktree tabs. src/main/runtime/orca-runtime-tests/runtime-availability.spec.ts: starts unavailable with no authoritative window; reports the configured Windows terminal shell on status; reports floating workspace availability from settings on status; polls floating tabs with targeted PTY liveness and no repo/provider inventory; hydrates persisted tabs when the store cannot report repos; advertises browser screencast only when a renderer window is available; routes mobile Codex reset consumption through the account mutation coordinator; maps a definite pre-provider rejection into an authoritative current snapshot. Existing unrelated evidenceRuns retain their original scope; unchanged dated receipt objects do not validate changed source bytes or reduced selectors. No fresh gate execution or historical import-closure equivalence is claimed. Frozen current literal-import graph reaches these changed test bytes; unresolved local and computed edges remain debt, and current reachability is not historical execution or equivalent replacement evidence. Historical prior account (preserved verbatim): Audit2070 (sources2068/2069): Intentionally retire these low-value alarms without replacement: src/main/runtime/orca-runtime.test.ts: ordinary registration cleanup removes only await import('./orca-runtime-tests/hooks-and-hosted-review.spec'). src/main/ssh/ssh-multiplexer-transport-writer.test.ts: reports saturated epochs and their drain boundary exactly once. src/main/runtime/orca-runtime-tests/worktree-removal-archive-hook-gate.spec.ts: removes and records the waiver when the failure is explicitly overridden; removes without an override record when the hook succeeds; removes when the hook is configured but not requested; removes when no archive hook is configured; holds a desktop removal in the same repo until this one is accepted; joins a desktop removal of the same worktree accepted while this one waited. src/main/runtime/orca-runtime-tests/worktree-ps-agent-row-dismissal.spec.ts: drops the row as soon as the user dismisses it, without waiting for the PTY to exit; tells paired clients to republish on the transition and on the dismissal; rejoins a row through its %s handle after pane ownership clears. src/main/runtime/orca-runtime-tests/hooks-and-hosted-review.spec.ts: reports an error when the SSH filesystem provider is unavailable; reports an SSH orca.yaml as an error without reading any filesystem; reports ok for a local repo hook check; reads SSH issue commands as absent and drops writes without touching any filesystem; allows host integration slug helpers for SSH repos through provider-aware GitHub clients; routes runtime GitHub repo identity helpers through the selected WSL project runtime; routes runtime GitHub issue and work-item actions through the selected WSL project runtime; pins explicit origin preference on runtime open-by-number work item lookups; forwards check-details cancellation without local Git overrides. Existing unrelated evidenceRuns retain their original scope; unchanged dated receipt objects do not validate changed source bytes or reduced selectors. No fresh gate execution or historical import-closure equivalence is claimed. Frozen current literal-import graph reaches these changed test bytes; unresolved local and computed edges remain debt, and current reachability is not historical execution or equivalent replacement evidence. The historical receipt/currentCommand selecting src/main/ssh/ssh-multiplexer-transport-writer.test.ts remains historical and is not a passing result for the reduced current command. Historical prior account (preserved verbatim): Audit1931 (sources1928/1929): Delete the complete tail-wait memo suite: its sole current authored callback checks supplied fromTail/blocked field values. Retire all three broader memoization/equivalence/eviction assertion aliases; they are not backed by retained source callbacks. No scan-work budget, sustained output, physical terminal or resource measurement is assigned to this removed test. Preserve meaningful remaining selectors/assertions and unchanged dated receipts within their original scope. Reduced selectors have no fresh gate execution evidence in this metadata packet. No equivalent replacement, physical watcher/PTY/provider, live SSH/remote/mobile, rendered, cross-platform, soak or measured-budget result is claimed. Historical prior account (preserved verbatim): Audit1674 retires changed-source assertion and execution scope without equivalent replacement coverage. Existing unchanged-selector receipts remain scoped to their original sources. Historical prior-source oracle: Existing exact unchanged-selector receipts remain the only current execution evidence. Changed source selectors have no fresh execution result. Historical prior-source oracle account: The current executable slice injects main-process renderer backlog pressure, then asserts ordinary terminal-wait detection without a joined-tail allocation and with full prompt-family continuity, per-PTY and total pending-output caps, a 1,024-ID projection cap with split-remainder compaction, preserved sequenced-tail metadata, active-pending protection ahead of background trimming, exact provider-generation pause ownership, renderer exit/data ordering, and ACK-gated in-flight bounds. The negotiation seam proves every initial connection and reconnect offers V1, the same-build relay advertises V1 without launch flags, old clients remain token-free, and method-not-found peers fall back without installing source ACK publication. The joined source-intake/multiplex oracle blocks renderer or headless serialization, admits one snapshot-covered span and one trailing span, requires replacement reservation only after the authoritative sequence is known, advances upstream ACK eligibility exactly to the covered boundary after SnapshotEnd, and delivers then ACKs the trailing span through the ordinary live path without cancellation. Source-range ledger and multiplex seams additionally require cumulative byte ACK 40 then 100 for one 100-byte frame to release exact byte credit without early source settlement, admit a contiguous higher-generation recovered token while the prior token remains unsettled, and reject stale stream or source generations without releasing in-flight byte credit. Replacement reservation rolls back every earlier span on a later failure, commit requires the exact frozen transfer states, and an authoritatively reclaimed covered span rejects commit without local trim authority while rollback removes the reservation idempotently. The exit-deadline oracle publishes a source-backed desktop projection, advances a fake clock to cancellation, and requires one projection transfer, proof commit, preparation, and final exit with no provider close, retained obligation, duplicate finalization, or process-lifetime cancellation tombstone; a second controlled proof remains pending across generation close and must never publish final exit, while both paths release the renderer preparation lease exactly once. The model-migration oracle accepts span A, blocks span B's raw emulator completion, begins disconnect migration, and proves no checkpoint or reconnect attach can remain at A; releasing B advances the exact checkpoint to B and projects B once. Its fake-clock arm reaches the 10-second deadline, requires checkpoint-unavailable, one exact model reset, zero retained admission charge and timers, and no effect from late raw settlement. Its two-PTY failure arm rejects one running raw callback under an active exact migration and requires one checkpoint-unavailable result, one model reset, zero provider closes, retained sibling checkpoint and transport liveness, zero charges and timers, and an unaffected other generation; a non-migrating sibling oracle still requires generation close. The intake additionally closes 2,048 sequential provider generations into one exact range, rejects stale events across that range, and preserves an out-of-order live gap until its own close. Direct-SSH contracts additionally prove activation-response settlement before the first source frame, claim-gated projection under same-decoder-turn response/data delivery, exact early-ACK reservation through send settlement or same-token retry, recovery completion visible before lease-held frames transfers those frames only into the private quarantine and admits no output until exact checkpoint-to-recoveryEnd coverage, invalid-checkpoint restore retention through response settlement followed by fresh-token retry and one live source frame, isolated saturated-subscriber eviction with healthy owner liveness, preserved legacy-primary backpressure, one-write ownership across write(false)/drain, control selection ahead of queued ordinary PTY input with FIFO lanes and bounded fairness, liveness rebasing, failed-exit retention through late ACK or exact recovery, response-settlement cancellation authority, token-scoped exit cancellation, exact checkpoint-to-recoveryEnd continuity, stale-owner one-shot retry, no physical PTY teardown on recovery failure, exact provider-generation closure and once-only cleanup when recovery cancellation publication or proof rejects, 16 MiB-plus-1 MiB decoder caps, charged per-PTY/session retention budgets, and eight-wide isolated reattach. The live Electron perf oracle adds hidden-output floods, renderer scheduler queue depth, dropped-output-zero normal scenarios, and active key latency budgets before promotion.", + "oracle": "Audit3128 scoped coverage reduction: Audit3121 removes VoiceDictationLanguageSetting.test.tsx (Spanish-to-es callback and unsupported-model Korean option presence) and voice-speech-model-rows.test.ts (provider/language-order/retired-on-disk list policy); these real narrow alarms are deliberately retired without replacement. VoiceSpeechModelSection per-model concurrent deletion remains unchanged. Audit3122 removes the direct absolute/parent-segment sparse-directory exception-type sample and the five-row IPC sparse-preset error-code/non-save table (two authored/six expanded cases). Those real typed-validation and IPC refusal/error-code alarms are deliberately retired. Keep both real symlink/junction floating-directory trust cases and the minimal RPC structured validation-reason serialization case. The RPC case does not replace IPC save/refusal behavior, typed normalization for both path inputs, actual Git sparse checkout or rendered localization. No equivalent replacement or fresh test/runtime/platform/remote/performance validation is claimed. Audit3124 removes ExperimentalPane.test.tsx (ordinary Pet switch callback), WorkspaceDirectorySetting.test.tsx (padded absolute saved-path normalization/no-write blur), and repository-search.perf.test.ts (navigation/selected-pane builder-call policy, fixture cardinality/nonempty entries and renamed project keyword inclusion). These real narrow alarms are retired without replacement; RepositoryPane.search.test.tsx search-to-sparse-editor request remains unchanged. Audit3129: resolver Windows case-insensitive and POSIX case-sensitive configured-env composition plus mocked-base launch-preflight Windows composition are intentionally no longer checked by the two deleted suites. No equivalent coverage claimed. Keep runtime-structured-agent-host-env.test.ts unchanged: configured Windows Claude-home account identity and host-account precedence remain checked. Audit3114 incorporates source3101: retire three structured Claude child-environment wrapper cases covering inherited stamp isolation/default and explicit entrypoint, Windows SDK-key canonicalization/default/duplicate-key order, and configured lookup/final environment/config-dir conjunction. Keep direct auth/session sanitation and Windows-versus-Linux overlay checks, actual executable lookup from supplied inherited PATH, native Windows child environment delivery, and stale option-mutation fencing. These are not equivalent replacements for the retired wrapper alarms. No fresh test/runtime, real login-shell probe, Claude SDK startup, remote/mixed-version, UI or performance result is claimed. Audit3114 incorporates source3105: retire 17 authored/20 expanded daemon launch-note cases, including the entire two-case SessionLaunchNote suite. Deliberately lose split-marker carry across same-token re-arm, stale-token retirement refusal, foreground tri-state and serial settlement, omitted-note/no-incarnation mixed-version policy, sticky retirement, first-incarnation/replacement launch preservation and held-launch precedence. Keep only the unchanged unresolved foreground-read race against a later finished attach. Supplied notes/foreground and runtime calls do not establish actual daemon/main restart, SSH, native Windows observation, mobile UI, equivalent replacement or fresh runtime/performance validation. Audit3114 incorporates source3109: retire six authored/35 expanded pricing cases: 21 accepted model-name examples, 10 unsupported/future-model fail-closed examples, malformed TTL clamp, unknown-model estimate refusal, flat-model long-context disregard and oversized long-context clamp. Keep mixed five-minute/one-hour write allocation and short/all-long/mixed long-context allocation with original fixed oracles. Real parser/refusal/bounds alarms are knowingly lost without equivalent replacement; retained tests do not validate provider rates, actual billing or request threshold classification. Preserve the archived usage-scan-append source-freeze receipt without transferring historical execution/performance credit. No fresh runtime result is claimed. Audit3108 source-only reduction (3094/3095/3100): retire 27 authored/30 expanded cases, including complete markdown view-state, file-preview link-open and CLI agent-server-route suites, without replacement. Deliberate losses include preview visibility/reference lifetime and same-path owners; SSH stat, directory/missing-target, explicit-null owner, nonmarkdown line/column, local URI routing, remote-to-client guard and same-file reuse; document-preview source-owner/folder/reuse routing, stable grant location and outside-workspace POSIX/Windows request projection; and verified remote CLI send/check refusal, suggested --on argv/copy, shell no-probe bypass, unverified-launch admission and older-peer refusal. Retain two markdown source-runtime/qualified-reveal alarms, one stale grant-rejection fence and two remote read/mutation evidence policies, with original callbacks. Final last-wins reveal did not prove transient cancellation; CLI prerequisite did not prove no real send, and envelope tests do not execute server refusal. No fresh runtime, rendered UI, actual file/SSH/paired transport, authorization, mixed-version or performance result is claimed. Audit3096 source-only reduction (3087/3088): retire 69 authored/76 expanded cases, including complete recovery-portals and reconnect-banner suites. Intentionally lose dead/default TUI replay, generated alternate-frame style composition, reset/mode/dimension and legacy missing-mode-boundary alarms, UI notice/action/reveal policies, and isolated recovery lifecycle variants, without equivalent replacement. Retained registrations and callbacks are unchanged; headless cells are not pixels, and no fresh runtime, host, SSH, WSL, rendered-app or performance result is claimed. Audit3086 source-only reduction (3079/3080/3081): intentionally retire 93 authored/100 expanded mock-boundary cases, including the complete spawn-errors and SSH reconnect overlay suites, without replacement. Retired alarms include spawn classification/ownership, reply ceilings and renderer-write failure recovery, disconnect/detach fencing branches, authority facts, and overlay retry/visibility wiring. Retained callbacks are unchanged; no fresh execution, real PTY/SSH, rendered UI, or performance result is claimed. Module layout SSH policy: Retire the multiplexer private-clock saturation registration, the writer saturation transition-count registration and the changed-source dated writer/backpressure receipt. Retained byte delivery, lane order, fairness, settlement and cancellation cases are unvalidated by that historical receipt. No equivalent replacement, new execution, measured performance, live SSH/WSL or cross-platform proof is claimed. The unchanged WSL protocol receipt retains only its original command scope. Historical prior account (preserved verbatim): Audit2432: The broad unit command loses the isolated BranchPicker open-change notification sample. Its unique true/false callback alarm is intentionally retired; reopening/status-refresh behavior was not exercised. No equivalent replacement or fresh validation of the reduced selector is claimed. Audit2425: The broad unit command loses the workspace-copy-status supplied readError forwarding sample. It did not exercise the branch-switch blocking promised by its title. This alarm is intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2417: The broad unit command loses two ordinary clean Git cherry-pick/revert success samples and two terminal startup foreground-read work-budget samples. These unique alarms are intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2412: The broad unit command loses the whole six-case fake-React-hook scroll-anchor suite, two generic draft-retention samples, a simple new-agent order sample, a real history-publication batching-count sample, an unmounted handoff abort/timer sample and three basic WSL preview/error samples. These alarms are intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2404: The broad unit command loses the standalone share-name lookup 503 propagation suite, initial deletion placement-kind sample, direct changed-mtime stale sample, first-pass rollback existence example and abandoned-preparation directory sample. These exact alarms are intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2397 review amendment: The broad unit command loses three standalone suites, two partial-case definitions and two URL table rows across package/download/placement tests. Keep initial wrong-port authorization, modified independent-copy digest refusal and abort-after-begin cancellation dispatch. The remaining alarms are deliberately retired; no equivalent complete replacement or fresh validation of the reduced selector is claimed. Audit2393 current-parent refresh2401: Remove the basic skill-install service alias sample and original public managed-remove receiptless refusal case. Preserve the upstream request ownership suite and transaction interruption coverage. No equivalent complete replacement or fresh validation of the reduced selector is claimed. Audit2389: The broad unit command loses the missing-preview suite, plain-global destination sample and prepublished-receipt recovery example. No equivalent replacement or fresh validation of the reduced selector is claimed. Audit2385: The broad unit command loses four authored walker/staging/protocol examples (six expanded cases); no equivalent replacement or fresh validation is claimed for the reduced suite. Audit2383: The unscoped Vitest command no longer includes the removed Antigravity catalogue suite; its exact provider-root/owner example has no equivalent replacement or fresh validation claim. Audit2377/2381: The unscoped Vitest command includes the reduced bundled-skill and native-discovery suites; the eight removed backup/legacy/catalogue/symlink/SSH-builder examples have no replacement-equivalence or fresh validation claim here. Audit2133 current-parent preparation: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2095 current-parent preparation2126: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2090 (sources2088/2089): Current reduced suites intentionally retire the alarms listed in knownGaps; prior dated execution and replacement claims do not validate changed source bytes or selectors. Historical prior account (preserved verbatim): Audit2086 (sources2082/2083): Current reduced suites intentionally retire the alarms listed in knownGaps; prior dated execution and replacement claims do not validate changed source bytes or selectors. Historical prior account (preserved verbatim): Audit2076 (sources2073/2074): Intentionally retire these low-value alarms without replacement: src/main/runtime/orca-runtime-tests/terminal-handles-part-02.spec.ts: recovers a moved SSH pane through the tab it now sits in, not the one its lease froze; reports a plain expired orphan; does not report a pane whose only recent lease was superseded; does not report a pane whose only recent lease had its relay id recycled; picks the eligible orphan over a superseded predecessor that matches first; collects the same tabs the per-tab reader reports, in one sweep of the leases; reports no candidate when no lease names any of the worktree tabs. src/main/runtime/orca-runtime-tests/runtime-availability.spec.ts: starts unavailable with no authoritative window; reports the configured Windows terminal shell on status; reports floating workspace availability from settings on status; polls floating tabs with targeted PTY liveness and no repo/provider inventory; hydrates persisted tabs when the store cannot report repos; advertises browser screencast only when a renderer window is available; routes mobile Codex reset consumption through the account mutation coordinator; maps a definite pre-provider rejection into an authoritative current snapshot. Existing unrelated evidenceRuns retain their original scope; unchanged dated receipt objects do not validate changed source bytes or reduced selectors. No fresh gate execution or historical import-closure equivalence is claimed. Frozen current literal-import graph reaches these changed test bytes; unresolved local and computed edges remain debt, and current reachability is not historical execution or equivalent replacement evidence. Historical prior account (preserved verbatim): Audit2070 (sources2068/2069): Intentionally retire these low-value alarms without replacement: src/main/runtime/orca-runtime.test.ts: ordinary registration cleanup removes only await import('./orca-runtime-tests/hooks-and-hosted-review.spec'). src/main/ssh/ssh-multiplexer-transport-writer.test.ts: reports saturated epochs and their drain boundary exactly once. src/main/runtime/orca-runtime-tests/worktree-removal-archive-hook-gate.spec.ts: removes and records the waiver when the failure is explicitly overridden; removes without an override record when the hook succeeds; removes when the hook is configured but not requested; removes when no archive hook is configured; holds a desktop removal in the same repo until this one is accepted; joins a desktop removal of the same worktree accepted while this one waited. src/main/runtime/orca-runtime-tests/worktree-ps-agent-row-dismissal.spec.ts: drops the row as soon as the user dismisses it, without waiting for the PTY to exit; tells paired clients to republish on the transition and on the dismissal; rejoins a row through its %s handle after pane ownership clears. src/main/runtime/orca-runtime-tests/hooks-and-hosted-review.spec.ts: reports an error when the SSH filesystem provider is unavailable; reports an SSH orca.yaml as an error without reading any filesystem; reports ok for a local repo hook check; reads SSH issue commands as absent and drops writes without touching any filesystem; allows host integration slug helpers for SSH repos through provider-aware GitHub clients; routes runtime GitHub repo identity helpers through the selected WSL project runtime; routes runtime GitHub issue and work-item actions through the selected WSL project runtime; pins explicit origin preference on runtime open-by-number work item lookups; forwards check-details cancellation without local Git overrides. Existing unrelated evidenceRuns retain their original scope; unchanged dated receipt objects do not validate changed source bytes or reduced selectors. No fresh gate execution or historical import-closure equivalence is claimed. Frozen current literal-import graph reaches these changed test bytes; unresolved local and computed edges remain debt, and current reachability is not historical execution or equivalent replacement evidence. The historical receipt/currentCommand selecting src/main/ssh/ssh-multiplexer-transport-writer.test.ts remains historical and is not a passing result for the reduced current command. Historical prior account (preserved verbatim): Audit1931 (sources1928/1929): Delete the complete tail-wait memo suite: its sole current authored callback checks supplied fromTail/blocked field values. Retire all three broader memoization/equivalence/eviction assertion aliases; they are not backed by retained source callbacks. No scan-work budget, sustained output, physical terminal or resource measurement is assigned to this removed test. Preserve meaningful remaining selectors/assertions and unchanged dated receipts within their original scope. Reduced selectors have no fresh gate execution evidence in this metadata packet. No equivalent replacement, physical watcher/PTY/provider, live SSH/remote/mobile, rendered, cross-platform, soak or measured-budget result is claimed. Historical prior account (preserved verbatim): Audit1674 retires changed-source assertion and execution scope without equivalent replacement coverage. Existing unchanged-selector receipts remain scoped to their original sources. Historical prior-source oracle: Existing exact unchanged-selector receipts remain the only current execution evidence. Changed source selectors have no fresh execution result. Historical prior-source oracle account: The current executable slice injects main-process renderer backlog pressure, then asserts ordinary terminal-wait detection without a joined-tail allocation and with full prompt-family continuity, per-PTY and total pending-output caps, a 1,024-ID projection cap with split-remainder compaction, preserved sequenced-tail metadata, active-pending protection ahead of background trimming, exact provider-generation pause ownership, renderer exit/data ordering, and ACK-gated in-flight bounds. The negotiation seam proves every initial connection and reconnect offers V1, the same-build relay advertises V1 without launch flags, old clients remain token-free, and method-not-found peers fall back without installing source ACK publication. The joined source-intake/multiplex oracle blocks renderer or headless serialization, admits one snapshot-covered span and one trailing span, requires replacement reservation only after the authoritative sequence is known, advances upstream ACK eligibility exactly to the covered boundary after SnapshotEnd, and delivers then ACKs the trailing span through the ordinary live path without cancellation. Source-range ledger and multiplex seams additionally require cumulative byte ACK 40 then 100 for one 100-byte frame to release exact byte credit without early source settlement, admit a contiguous higher-generation recovered token while the prior token remains unsettled, and reject stale stream or source generations without releasing in-flight byte credit. Replacement reservation rolls back every earlier span on a later failure, commit requires the exact frozen transfer states, and an authoritatively reclaimed covered span rejects commit without local trim authority while rollback removes the reservation idempotently. The exit-deadline oracle publishes a source-backed desktop projection, advances a fake clock to cancellation, and requires one projection transfer, proof commit, preparation, and final exit with no provider close, retained obligation, duplicate finalization, or process-lifetime cancellation tombstone; a second controlled proof remains pending across generation close and must never publish final exit, while both paths release the renderer preparation lease exactly once. The model-migration oracle accepts span A, blocks span B's raw emulator completion, begins disconnect migration, and proves no checkpoint or reconnect attach can remain at A; releasing B advances the exact checkpoint to B and projects B once. Its fake-clock arm reaches the 10-second deadline, requires checkpoint-unavailable, one exact model reset, zero retained admission charge and timers, and no effect from late raw settlement. Its two-PTY failure arm rejects one running raw callback under an active exact migration and requires one checkpoint-unavailable result, one model reset, zero provider closes, retained sibling checkpoint and transport liveness, zero charges and timers, and an unaffected other generation; a non-migrating sibling oracle still requires generation close. The intake additionally closes 2,048 sequential provider generations into one exact range, rejects stale events across that range, and preserves an out-of-order live gap until its own close. Direct-SSH contracts additionally prove activation-response settlement before the first source frame, claim-gated projection under same-decoder-turn response/data delivery, exact early-ACK reservation through send settlement or same-token retry, recovery completion visible before lease-held frames transfers those frames only into the private quarantine and admits no output until exact checkpoint-to-recoveryEnd coverage, invalid-checkpoint restore retention through response settlement followed by fresh-token retry and one live source frame, isolated saturated-subscriber eviction with healthy owner liveness, preserved legacy-primary backpressure, one-write ownership across write(false)/drain, control selection ahead of queued ordinary PTY input with FIFO lanes and bounded fairness, liveness rebasing, failed-exit retention through late ACK or exact recovery, response-settlement cancellation authority, token-scoped exit cancellation, exact checkpoint-to-recoveryEnd continuity, stale-owner one-shot retry, no physical PTY teardown on recovery failure, exact provider-generation closure and once-only cleanup when recovery cancellation publication or proof rejects, 16 MiB-plus-1 MiB decoder caps, charged per-PTY/session retention budgets, and eight-wide isolated reattach. The live Electron perf oracle adds hidden-output floods, renderer scheduler queue depth, dropped-output-zero normal scenarios, and active key latency budgets before promotion.", "commands": [ "pnpm exec vitest run --config config/vitest.config.ts", "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/construction/orca-runtime.test.ts", @@ -9390,7 +9372,14 @@ "Module layout SSH policy: Retire the multiplexer private-clock saturation registration, the writer saturation transition-count registration and the changed-source dated writer/backpressure receipt. Retained byte delivery, lane order, fairness, settlement and cancellation cases are unvalidated by that historical receipt. No equivalent replacement, new execution, measured performance, live SSH/WSL or cross-platform proof is claimed. The unchanged WSL protocol receipt retains only its original command scope.", "Historical original SSH assertion metadata before policy cuts, preserved verbatim; removed assertions are not current coverage:\n{\n \"file\": \"src/main/ssh/protocol/ssh-multiplexer-transport-writer.test.ts\",\n \"assertions\": [\n \"write(false) owns one frame and later ordinary traffic waits for drain\",\n \"queued control is selected before ordinary PTY backlog at drain while each lane remains FIFO\",\n \"four-control fairness guarantees ordinary progress without starving control\",\n \"one coalesced liveness bypass is allowed per saturated epoch\",\n \"saturation and drain transitions are reported exactly once\"\n ]\n }\n{\n \"file\": \"src/main/ssh/protocol/ssh-channel-multiplexer.test.ts\",\n \"assertions\": [\n \"self-imposed writer saturation suppresses false death and rebases both health clocks on drain\"\n ]\n }", "Historical complete dated writer/backpressure receipt retired after its source changed, preserved verbatim including executed command and path-only currentCommand; not current validation:\n{\n \"date\": \"2026-07-29\",\n \"runner\": \"local\",\n \"platform\": \"macos\",\n \"command\": \"pnpm exec vitest run --config config/vitest.config.ts src/main/ssh/ssh-multiplexer-transport-writer.test.ts src/main/ssh/ssh-channel-multiplexer-backpressure.test.ts --reporter=dot\",\n \"result\": \"passed\",\n \"durationSeconds\": 6.89,\n \"summary\": \"Thirteen current-head deterministic files passed 150 tests across exit-sealed private recovery, stale-transfer cancellation authority, bounded latest-token retirement, restore retirement, fail-closed recovery cancellation, partial ACK and token rotation, reclaimed-span replacement, mux lane fairness, provisional activation, and exit-proof ordering; no live topology was exercised.\",\n \"currentCommand\": \"pnpm exec vitest run --config config/vitest.config.ts src/main/ssh/protocol/ssh-multiplexer-transport-writer.test.ts src/main/ssh/protocol/ssh-channel-multiplexer-backpressure.test.ts --reporter=dot\"\n }", - "Historical provider scope before changed-source receipt retirement: [\"ssh\"]" + "Historical provider scope before changed-source receipt retirement: [\"ssh\"]", + "Audit3086 source-only reduction (3079/3080/3081): intentionally retire 93 authored/100 expanded mock-boundary cases, including the complete spawn-errors and SSH reconnect overlay suites, without replacement. Retired alarms include spawn classification/ownership, reply ceilings and renderer-write failure recovery, disconnect/detach fencing branches, authority facts, and overlay retry/visibility wiring. Retained callbacks are unchanged; no fresh execution, real PTY/SSH, rendered UI, or performance result is claimed.", + "Audit3096 source-only reduction (3087/3088): retire 69 authored/76 expanded cases, including complete recovery-portals and reconnect-banner suites. Intentionally lose dead/default TUI replay, generated alternate-frame style composition, reset/mode/dimension and legacy missing-mode-boundary alarms, UI notice/action/reveal policies, and isolated recovery lifecycle variants, without equivalent replacement. Retained registrations and callbacks are unchanged; headless cells are not pixels, and no fresh runtime, host, SSH, WSL, rendered-app or performance result is claimed.", + "Audit3108 source-only reduction (3094/3095/3100): retire 27 authored/30 expanded cases, including complete markdown view-state, file-preview link-open and CLI agent-server-route suites, without replacement. Deliberate losses include preview visibility/reference lifetime and same-path owners; SSH stat, directory/missing-target, explicit-null owner, nonmarkdown line/column, local URI routing, remote-to-client guard and same-file reuse; document-preview source-owner/folder/reuse routing, stable grant location and outside-workspace POSIX/Windows request projection; and verified remote CLI send/check refusal, suggested --on argv/copy, shell no-probe bypass, unverified-launch admission and older-peer refusal. Retain two markdown source-runtime/qualified-reveal alarms, one stale grant-rejection fence and two remote read/mutation evidence policies, with original callbacks. Final last-wins reveal did not prove transient cancellation; CLI prerequisite did not prove no real send, and envelope tests do not execute server refusal. No fresh runtime, rendered UI, actual file/SSH/paired transport, authorization, mixed-version or performance result is claimed.", + "Audit3114 incorporates source3101: retire three structured Claude child-environment wrapper cases covering inherited stamp isolation/default and explicit entrypoint, Windows SDK-key canonicalization/default/duplicate-key order, and configured lookup/final environment/config-dir conjunction. Keep direct auth/session sanitation and Windows-versus-Linux overlay checks, actual executable lookup from supplied inherited PATH, native Windows child environment delivery, and stale option-mutation fencing. These are not equivalent replacements for the retired wrapper alarms. No fresh test/runtime, real login-shell probe, Claude SDK startup, remote/mixed-version, UI or performance result is claimed.", + "Audit3114 incorporates source3105: retire 17 authored/20 expanded daemon launch-note cases, including the entire two-case SessionLaunchNote suite. Deliberately lose split-marker carry across same-token re-arm, stale-token retirement refusal, foreground tri-state and serial settlement, omitted-note/no-incarnation mixed-version policy, sticky retirement, first-incarnation/replacement launch preservation and held-launch precedence. Keep only the unchanged unresolved foreground-read race against a later finished attach. Supplied notes/foreground and runtime calls do not establish actual daemon/main restart, SSH, native Windows observation, mobile UI, equivalent replacement or fresh runtime/performance validation.", + "Audit3114 incorporates source3109: retire six authored/35 expanded pricing cases: 21 accepted model-name examples, 10 unsupported/future-model fail-closed examples, malformed TTL clamp, unknown-model estimate refusal, flat-model long-context disregard and oversized long-context clamp. Keep mixed five-minute/one-hour write allocation and short/all-long/mixed long-context allocation with original fixed oracles. Real parser/refusal/bounds alarms are knowingly lost without equivalent replacement; retained tests do not validate provider rates, actual billing or request threshold classification. Preserve the archived usage-scan-append source-freeze receipt without transferring historical execution/performance credit. No fresh runtime result is claimed.", + "Audit3128 scoped coverage reduction: Audit3121 removes VoiceDictationLanguageSetting.test.tsx (Spanish-to-es callback and unsupported-model Korean option presence) and voice-speech-model-rows.test.ts (provider/language-order/retired-on-disk list policy); these real narrow alarms are deliberately retired without replacement. VoiceSpeechModelSection per-model concurrent deletion remains unchanged. Audit3122 removes the direct absolute/parent-segment sparse-directory exception-type sample and the five-row IPC sparse-preset error-code/non-save table (two authored/six expanded cases). Those real typed-validation and IPC refusal/error-code alarms are deliberately retired. Keep both real symlink/junction floating-directory trust cases and the minimal RPC structured validation-reason serialization case. The RPC case does not replace IPC save/refusal behavior, typed normalization for both path inputs, actual Git sparse checkout or rendered localization. No equivalent replacement or fresh test/runtime/platform/remote/performance validation is claimed. Audit3124 removes ExperimentalPane.test.tsx (ordinary Pet switch callback), WorkspaceDirectorySetting.test.tsx (padded absolute saved-path normalization/no-write blur), and repository-search.perf.test.ts (navigation/selected-pane builder-call policy, fixture cardinality/nonempty entries and renamed project keyword inclusion). These real narrow alarms are retired without replacement; RepositoryPane.search.test.tsx search-to-sparse-editor request remains unchanged. Audit3129: resolver Windows case-insensitive and POSIX case-sensitive configured-env composition plus mocked-base launch-preflight Windows composition are intentionally no longer checked by the two deleted suites. No equivalent coverage claimed. Keep runtime-structured-agent-host-env.test.ts unchanged: configured Windows Claude-home account identity and host-account precedence remain checked." ], "demotionRule": "Cannot promote without metric artifacts and bounded-byte assertions." }, @@ -9626,7 +9615,7 @@ "providers": ["ssh", "remote-runtime", "wsl"], "coveredPlatforms": ["macos"], "coveredProviders": ["ssh", "remote-runtime"], - "coverageNotes": "Audit2709 removes src/renderer/src/runtime/web-session-tabs-sync/inventory/remote-runtime-session-tabs-inflight.test.ts: intentionally retire waiting out an older inventory and sharing a fresh post-operation request among concurrent callers. No equivalent replacement or fresh execution is claimed. Dated receipts remain historical evidence for their unchanged selectors only; they do not validate the reduced commands or full current gate. Historical prior account (preserved verbatim): Audit2133 current-parent preparation: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2076 (sources2073/2074): Intentionally retire these low-value alarms without replacement: src/main/ssh/connection/ssh-connection-manager.test.ts: tears a failed startup down quietly, so its error is not replaced by a disconnect; marks a dial that never reached the host, so setup reports SSH’s reason; removes the registration when bulk teardown fails; leaves a connection whose own reconnect is asking the user alone; clears the pool entry when the closed connection is still the registered one; still reuses a connection that is already up; dials a user's connection test. Existing unrelated evidenceRuns retain their original scope; unchanged dated receipt objects do not validate changed source bytes or reduced selectors. No fresh gate execution or historical import-closure equivalence is claimed. Frozen current literal-import graph reaches these changed test bytes; unresolved local and computed edges remain debt, and current reachability is not historical execution or equivalent replacement evidence. Historical prior account (preserved verbatim): Audit2052 (sources2049/2050/2051): Remove complete low-value restored-pane-title and pane-visibility-identity suites and their current selectors. Their supplied snapshot/title and nullable identity-comparison alarms are intentionally lost without replacement. Unrelated dated evidenceRuns remain unchanged and do not validate the reduced commands. No fresh gate execution, actual PTY/provider, rendered, remote/mobile or cross-platform result is claimed. Historical prior account (preserved verbatim): Audit1983: Delete the terminal-create idempotency forwarding suite and its prospective selector. Preserve2 unrelated receipts without claiming the lost forwarding alarm was replaced. Changed-source prior results do not validate reduced-source bytes. No fresh execution, equivalent replacement, red/green, measured budget, soak, provider/PTY, rendered, live remote/mobile or cross-platform result is claimed. Historical prior account (preserved verbatim): Audit1931 (sources1928/1929): Delete complete transport-unmount and queued-initial-CWD helper suites: despite lifecycle-shaped fixtures, the unmount owner returns a Boolean PTY-id predicate, while CWD cases check nullish field selection and callback counts. Lose their detach/destroy/teardown and one-shot-CWD supplied examples. Partial queued-pane-startup loses three identical/empty/nonowner identity examples, retaining four exact one-shot/replacement ownership callbacks. No assertion object maps to these three sources, so unrelated alias objects remain byte-exact. Preserve the current81ed prior removal of pane-split-startup; stale bd948 source metadata snapshots are not replayed. No actual process/PTY startup, transport shutdown, live SSH or rendered recovery is proven by this metadata change. Preserve meaningful remaining selectors/assertions and unchanged dated receipts within their original scope. Reduced selectors have no fresh gate execution evidence in this metadata packet. No equivalent replacement, physical watcher/PTY/provider, live SSH/remote/mobile, rendered, cross-platform, soak or measured-budget result is claimed. Historical prior account (preserved verbatim): Audit1925 (sources1922/1923): Delete the sole supplied public-SSH-state spread/sanitized-error projection and its provider-authority-pair alias. Also delete the complete tiny one-shot split-startup wrapper suite: callback-visible temporary field/reset, sequential payload and thrown-sentinel reset alarms disappear. No SSH provider epoch handoff, connection, process startup or actual terminal split was performed by either deleted suite. Preserve meaningful remaining selectors/assertions and unchanged dated receipts within their original scope. Reduced selectors have no fresh gate execution evidence in this metadata packet. No equivalent replacement, physical watcher/PTY/provider, live SSH/remote/mobile, rendered, cross-platform, soak or measured-budget result is claimed. Historical prior account (preserved verbatim): Audit1903 removes whole mocked detach, supplied geometry and/or close-request suites selected by this gate. Their pending-CWD/PTY-binding argument/order, fabricated hit-test and close bookkeeping alarms are intentionally lost without replacement. Valid remaining selectors/assertions and unchanged-selector execution receipts retain their original scope. Reduced unit selections have no fresh broad gate, real PTY/provider, rendered focus, or measured-budget validation. Historical prior account (preserved verbatim): Audit1645 removes low-value test projections, mock wiring, private-state assertions, inventories and duplicate examples without equivalent replacement coverage. Per-source reviews record retained public behavior and refusal boundaries with injected/native/remote limits. Affected historical receipts are retired; gates without active current evidence are downgraded. Historical scope before this audit: Audit1640 removes low-value IPC, SSH, workspace and transcript-watcher tests without equivalent replacement coverage. Per-source reviews record retained public refusal, cancellation, execution-host and real-filesystem outcomes with injected/native/remote limits. Affected historical receipts are retired; gates without active current evidence are downgraded. Historical scope before this audit: Audit1342 changes referenced sources. Retained unchanged-source receipts apply only to their unchanged commands; the reduced selectors have no new execution result. Historical coverage account (not a current claim for changed sources): Audit1301 changes referenced sources. Retained unchanged-source receipts apply only to their unchanged commands; the reduced selectors have no new execution result. Historical coverage account (not a current claim for changed sources): Deterministic renderer coverage proves retained native and runtime SSH payloads are admitted through production routes only with valid complete authority, stale cleanup cannot unregister a replacement runtime terminal, direct SSH Git and folder panes keep their PTY bindings through disconnect and reconnect, one authority chain stops after two automatic attempts even when each timeout exceeds the rolling window, rejected acknowledgements mutate no store maps, and one shared exact attempt admits every concurrent split-pane spawn and reattach while preserving the first PTY as the tab fallback. A later sibling failure rotates the tab once, stale callbacks from the prior attempt mutate no state, split remount activity suppression is counted per leaf, primary PTY exit promotes a bound survivor or preserves an empty continuation gap for a late sibling, and primary, non-primary, or null-PTY detach preserves exact authority on both resulting tabs. Intentional pane disposal cancels its settlement timer without breaking StrictMode remount timeout ownership. A mounted remote-runtime terminal survives repeated transport partitions without changing PTY identity. A real encrypted-WebSocket oracle proves a successful reachability probe can replace a pre-ready shared-control socket without rejecting or duplicating the waiting RPC. Direct SSH coordinator tests cover immediate terminal finalization, hydration correction, damping, bounded retry, and telemetry non-interference. Client/server heartbeat tests cover timer suspension, socket generations fence stale callbacks, cold restored-terminal attachment retries, cached pixels remain unhealthy until authoritative replay, automatic retries stop after one minute, manual reconnect preserves the PTY, and pane closure releases recovery UI state. Current-transport recovery clears every error that transport surfaced without clearing or displaying a sibling pane's errors, while transport suppression, pane retention, and the rendered surface are independently bounded. Current macOS Electron journeys against an ephemeral Linux Docker SSH target cover exact-authority repo/worktree hydration, live terminal recovery after disconnect/reconnect, and eager six-terminal remount after renderer reload. A Windows remote-runtime smoke covers reachability and PTY round-trip. Multi-target live fanout, paired-close, WSL, and patched live partition journeys remain gaps. The test-value audit narrowed resolved-worktree-execution-host.test.ts to cache reuse and invalidation. Its direct hydrated-worktree, missing-folder, restored-owner, host-stamp and empty-runtime selection alarms are lost. Other routing and recovery suites do not establish equivalent selector coverage; historical run commands and results remain historical. The test-value audit removes standalone unwritten/released renderer-authority and cross-scope request-count examples. The retained synthetic mirror/store/sidebar and pending-inventory checks do not establish real PTY byte ownership, SSH reconnection or equivalent replacement coverage; historical commands and results remain historical. The test-value audit removes direct join/label ledgers, immediate failure and negative-only stale/removal/partial-authority examples, plus the standalone retained/live event projection. Retained synthetic store/hydration compositions do not establish real SSH continuity, remote authority delivery or equivalent replacement alarms. Historical commands and results remain historical. The renderer scheduling fixture retains in-flight coalescing only. Its immediate publication and negative stale-SSH-cleanup call ledgers were removed; the retained SSH recovery evidence does not replace those alarms. The accepted-handle-events observer suite is removed, including its scoped listener and settlement-release assertions. These observer alarms are lost; retained SSH/transport journeys are not equivalent replacement coverage. The test-value audit removed repeated remote-handle rotation inventories and snapshot-first pending-restart migration examples from terminal-pty-identity-replacement.test.ts, plus direct foreground metadata setter, confirmation, evidence-enum, and prefix/worktree cleanup examples. Those rotation, pending-restart, snapshot-first migration, and foreground scalar-update alarms are lost. The retained replacement case observes pane and tab identities in one subscriber publication; it does not establish equivalent migration, process-evidence, or input-routing coverage. Historical commands and results remain historical. The test-value audit narrows terminals-hydration.test.ts to a real reconnect batch with one measured store publication and hydration-seeded history consumed by a subsequent visit/Back check. Direct split-layout, runtime/folder/floating restoration, dead-leaf focus, cold/known-absent SSH deferral, title, default-tab marker, navigation initialization, and hydrationSucceeded scalar examples are removed; those specific alarms are lost. No actual target-removal transition, live SSH connection, session writer, or persisted restart is exercised by the retained cases. The old target-scoped/snapshot assertion references are retired because the current file does not contain those oracles. Other suites and historical runs do not establish equivalent replacement coverage. Audit1299 removes tests/e2e/terminal-cold-activation-deferral.spec.ts. Private-state/work-budget/assembled-fixture alarms are intentionally lost; retained suites are not equivalent replacement coverage. No new runtime/platform evidence is claimed. Audit1301: src/renderer/src/runtime/remote-runtime-session-tabs-inflight.test.ts reduced. Direct dedupe eviction/count and equal receipt-position sample loses authored alarm. No equivalent replacement coverage is claimed. Audit1318: src/shared/remote-runtime/control/remote-runtime-shared-control-connection.test.ts: Retain real local encrypted WebSocket regressions for pre-ready socket replacement preserving a request, close-before-ready logical subscription cleanup, unrelated RPC surviving another deadline, and pending setup abort leaving a sibling pending. Host server is synthetic and uses production crypto; kept scenarios also retain internal diagnostics/memory assertions unchanged. Remove happy routing, authority-envelope, unknown-ID/log, cleanup enum, binary, keepalive/liveness and production-budget/private socket inventories; intentionally lose those alarms, no physical host authorization, independent crypto or remote provider claim. Reduced selectors need fresh validation; historical full-suite receipts do not validate the changed coverage. No equivalent replacement or physical-provider claim. Audit1342: src/renderer/src/hooks/ipc-events/direct-ssh-connect-reply-routing.test.ts removed. Initial and event-triggered partial-authority hydration fixture alarms disappear. src/renderer/src/components/terminal-pane/terminal-error-accumulation.test.ts removed. Multiline dedup/storm bounds, resource classification/collision, scoped clear/reconnect strip and1000-pane map count alarms disappear. No equivalent replacement coverage is claimed. Audit1396: src/renderer/src/components/terminal-pane/pty-connection-pending-spawn.spec.ts reduced. Lose standalone debug-state retention/log silence alarm. React StrictMode is not mounted; bindings manually simulate remount over mocked transport. No equivalent replacement coverage is claimed. The existing primary-selector receipt and its claims remain scoped to their unchanged sources; the broad optional command has no new execution result. Audit1400: src/main/ipc/ssh/ssh.test.ts reduced. Lose trivial missing target and mocked publication inventory. Preexisting unresolved ../ssh/ssh-port-scanner mock is recorded, not fixed. src/renderer/src/runtime/sync-runtime-graph-scheduling.test.ts removed. Removes whole coalescing call-count suite; no newest-state delivery or replacement claim. src/renderer/src/store/slices/terminal-pty-identity-replacement.test.ts reduced. Removes three no-op/work-count registrations; retains externally observed ownership consistency. No equivalent replacement coverage is claimed. Existing exact unaffected receipts remain unchanged and apply only to their unchanged selectors. Changed optional selectors have no new execution result. Audit1412: src/main/ssh/ssh-provider-authority.test.ts removed. Loses real AbortController revocation/target isolation/reentrancy outcomes, recreated-target generation-floor/session rollover behavior, stale mutation refusal and settled-registration cleanup alarms. These useful alarms are deliberately lost; no equivalent cancellation coverage is claimed. No equivalent replacement coverage is claimed. Existing exact unaffected receipts remain unchanged and apply only to their unchanged selectors. Changed optional selectors have no new execution result. Audit1743 removes the standalone line-height clamp/default and pane cwd fallback helper tests (5 authored cases). These trivial-helper alarms are intentionally lost, with no equivalent replacement. Only current commands/testFiles lose the deleted selectors; historical receipts and evidenceRuns remain unchanged and do not validate the reduced commands.", + "coverageNotes": "Audit3096: remove the deleted reconnect-banner selector and its two UI aliases. The reduced recovery-state selector retains only the deadline cancelling pending backoff and explicit retry owning a new epoch alias; healthy-replacement stale-schedule and independent caller-cutoff cancellation aliases retire. Other retained state cases cover reentrant retry, owner settlement and same-epoch parked revival, without replacing those removed alarms. Prior admission-selector retirement, archived July 28 receipt, unrelated selectors/claims/receipts and old gaps remain unchanged; no fresh execution is claimed. Audit3086: retire the destroyed fresh-session fallback settlement/non-destructive reattach alias and its direct selector; retain the detach/attach selector and assertion unchanged. The retained admission cases cover adopt-before-data and explicitly buffered-death admission without a new spawn, without replacing fallback retirement. July 28 evidence was already archived by audit3077; no duplicate retirement or new execution is claimed. Audit2709 removes src/renderer/src/runtime/web-session-tabs-sync/inventory/remote-runtime-session-tabs-inflight.test.ts: intentionally retire waiting out an older inventory and sharing a fresh post-operation request among concurrent callers. No equivalent replacement or fresh execution is claimed. Dated receipts remain historical evidence for their unchanged selectors only; they do not validate the reduced commands or full current gate. Historical prior account (preserved verbatim): Audit2133 current-parent preparation: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2076 (sources2073/2074): Intentionally retire these low-value alarms without replacement: src/main/ssh/connection/ssh-connection-manager.test.ts: tears a failed startup down quietly, so its error is not replaced by a disconnect; marks a dial that never reached the host, so setup reports SSH’s reason; removes the registration when bulk teardown fails; leaves a connection whose own reconnect is asking the user alone; clears the pool entry when the closed connection is still the registered one; still reuses a connection that is already up; dials a user's connection test. Existing unrelated evidenceRuns retain their original scope; unchanged dated receipt objects do not validate changed source bytes or reduced selectors. No fresh gate execution or historical import-closure equivalence is claimed. Frozen current literal-import graph reaches these changed test bytes; unresolved local and computed edges remain debt, and current reachability is not historical execution or equivalent replacement evidence. Historical prior account (preserved verbatim): Audit2052 (sources2049/2050/2051): Remove complete low-value restored-pane-title and pane-visibility-identity suites and their current selectors. Their supplied snapshot/title and nullable identity-comparison alarms are intentionally lost without replacement. Unrelated dated evidenceRuns remain unchanged and do not validate the reduced commands. No fresh gate execution, actual PTY/provider, rendered, remote/mobile or cross-platform result is claimed. Historical prior account (preserved verbatim): Audit1983: Delete the terminal-create idempotency forwarding suite and its prospective selector. Preserve2 unrelated receipts without claiming the lost forwarding alarm was replaced. Changed-source prior results do not validate reduced-source bytes. No fresh execution, equivalent replacement, red/green, measured budget, soak, provider/PTY, rendered, live remote/mobile or cross-platform result is claimed. Historical prior account (preserved verbatim): Audit1931 (sources1928/1929): Delete complete transport-unmount and queued-initial-CWD helper suites: despite lifecycle-shaped fixtures, the unmount owner returns a Boolean PTY-id predicate, while CWD cases check nullish field selection and callback counts. Lose their detach/destroy/teardown and one-shot-CWD supplied examples. Partial queued-pane-startup loses three identical/empty/nonowner identity examples, retaining four exact one-shot/replacement ownership callbacks. No assertion object maps to these three sources, so unrelated alias objects remain byte-exact. Preserve the current81ed prior removal of pane-split-startup; stale bd948 source metadata snapshots are not replayed. No actual process/PTY startup, transport shutdown, live SSH or rendered recovery is proven by this metadata change. Preserve meaningful remaining selectors/assertions and unchanged dated receipts within their original scope. Reduced selectors have no fresh gate execution evidence in this metadata packet. No equivalent replacement, physical watcher/PTY/provider, live SSH/remote/mobile, rendered, cross-platform, soak or measured-budget result is claimed. Historical prior account (preserved verbatim): Audit1925 (sources1922/1923): Delete the sole supplied public-SSH-state spread/sanitized-error projection and its provider-authority-pair alias. Also delete the complete tiny one-shot split-startup wrapper suite: callback-visible temporary field/reset, sequential payload and thrown-sentinel reset alarms disappear. No SSH provider epoch handoff, connection, process startup or actual terminal split was performed by either deleted suite. Preserve meaningful remaining selectors/assertions and unchanged dated receipts within their original scope. Reduced selectors have no fresh gate execution evidence in this metadata packet. No equivalent replacement, physical watcher/PTY/provider, live SSH/remote/mobile, rendered, cross-platform, soak or measured-budget result is claimed. Historical prior account (preserved verbatim): Audit1903 removes whole mocked detach, supplied geometry and/or close-request suites selected by this gate. Their pending-CWD/PTY-binding argument/order, fabricated hit-test and close bookkeeping alarms are intentionally lost without replacement. Valid remaining selectors/assertions and unchanged-selector execution receipts retain their original scope. Reduced unit selections have no fresh broad gate, real PTY/provider, rendered focus, or measured-budget validation. Historical prior account (preserved verbatim): Audit1645 removes low-value test projections, mock wiring, private-state assertions, inventories and duplicate examples without equivalent replacement coverage. Per-source reviews record retained public behavior and refusal boundaries with injected/native/remote limits. Affected historical receipts are retired; gates without active current evidence are downgraded. Historical scope before this audit: Audit1640 removes low-value IPC, SSH, workspace and transcript-watcher tests without equivalent replacement coverage. Per-source reviews record retained public refusal, cancellation, execution-host and real-filesystem outcomes with injected/native/remote limits. Affected historical receipts are retired; gates without active current evidence are downgraded. Historical scope before this audit: Audit1342 changes referenced sources. Retained unchanged-source receipts apply only to their unchanged commands; the reduced selectors have no new execution result. Historical coverage account (not a current claim for changed sources): Audit1301 changes referenced sources. Retained unchanged-source receipts apply only to their unchanged commands; the reduced selectors have no new execution result. Historical coverage account (not a current claim for changed sources): Deterministic renderer coverage proves retained native and runtime SSH payloads are admitted through production routes only with valid complete authority, stale cleanup cannot unregister a replacement runtime terminal, direct SSH Git and folder panes keep their PTY bindings through disconnect and reconnect, one authority chain stops after two automatic attempts even when each timeout exceeds the rolling window, rejected acknowledgements mutate no store maps, and one shared exact attempt admits every concurrent split-pane spawn and reattach while preserving the first PTY as the tab fallback. A later sibling failure rotates the tab once, stale callbacks from the prior attempt mutate no state, split remount activity suppression is counted per leaf, primary PTY exit promotes a bound survivor or preserves an empty continuation gap for a late sibling, and primary, non-primary, or null-PTY detach preserves exact authority on both resulting tabs. Intentional pane disposal cancels its settlement timer without breaking StrictMode remount timeout ownership. A mounted remote-runtime terminal survives repeated transport partitions without changing PTY identity. A real encrypted-WebSocket oracle proves a successful reachability probe can replace a pre-ready shared-control socket without rejecting or duplicating the waiting RPC. Direct SSH coordinator tests cover immediate terminal finalization, hydration correction, damping, bounded retry, and telemetry non-interference. Client/server heartbeat tests cover timer suspension, socket generations fence stale callbacks, cold restored-terminal attachment retries, cached pixels remain unhealthy until authoritative replay, automatic retries stop after one minute, manual reconnect preserves the PTY, and pane closure releases recovery UI state. Current-transport recovery clears every error that transport surfaced without clearing or displaying a sibling pane's errors, while transport suppression, pane retention, and the rendered surface are independently bounded. Current macOS Electron journeys against an ephemeral Linux Docker SSH target cover exact-authority repo/worktree hydration, live terminal recovery after disconnect/reconnect, and eager six-terminal remount after renderer reload. A Windows remote-runtime smoke covers reachability and PTY round-trip. Multi-target live fanout, paired-close, WSL, and patched live partition journeys remain gaps. The test-value audit narrowed resolved-worktree-execution-host.test.ts to cache reuse and invalidation. Its direct hydrated-worktree, missing-folder, restored-owner, host-stamp and empty-runtime selection alarms are lost. Other routing and recovery suites do not establish equivalent selector coverage; historical run commands and results remain historical. The test-value audit removes standalone unwritten/released renderer-authority and cross-scope request-count examples. The retained synthetic mirror/store/sidebar and pending-inventory checks do not establish real PTY byte ownership, SSH reconnection or equivalent replacement coverage; historical commands and results remain historical. The test-value audit removes direct join/label ledgers, immediate failure and negative-only stale/removal/partial-authority examples, plus the standalone retained/live event projection. Retained synthetic store/hydration compositions do not establish real SSH continuity, remote authority delivery or equivalent replacement alarms. Historical commands and results remain historical. The renderer scheduling fixture retains in-flight coalescing only. Its immediate publication and negative stale-SSH-cleanup call ledgers were removed; the retained SSH recovery evidence does not replace those alarms. The accepted-handle-events observer suite is removed, including its scoped listener and settlement-release assertions. These observer alarms are lost; retained SSH/transport journeys are not equivalent replacement coverage. The test-value audit removed repeated remote-handle rotation inventories and snapshot-first pending-restart migration examples from terminal-pty-identity-replacement.test.ts, plus direct foreground metadata setter, confirmation, evidence-enum, and prefix/worktree cleanup examples. Those rotation, pending-restart, snapshot-first migration, and foreground scalar-update alarms are lost. The retained replacement case observes pane and tab identities in one subscriber publication; it does not establish equivalent migration, process-evidence, or input-routing coverage. Historical commands and results remain historical. The test-value audit narrows terminals-hydration.test.ts to a real reconnect batch with one measured store publication and hydration-seeded history consumed by a subsequent visit/Back check. Direct split-layout, runtime/folder/floating restoration, dead-leaf focus, cold/known-absent SSH deferral, title, default-tab marker, navigation initialization, and hydrationSucceeded scalar examples are removed; those specific alarms are lost. No actual target-removal transition, live SSH connection, session writer, or persisted restart is exercised by the retained cases. The old target-scoped/snapshot assertion references are retired because the current file does not contain those oracles. Other suites and historical runs do not establish equivalent replacement coverage. Audit1299 removes tests/e2e/terminal-cold-activation-deferral.spec.ts. Private-state/work-budget/assembled-fixture alarms are intentionally lost; retained suites are not equivalent replacement coverage. No new runtime/platform evidence is claimed. Audit1301: src/renderer/src/runtime/remote-runtime-session-tabs-inflight.test.ts reduced. Direct dedupe eviction/count and equal receipt-position sample loses authored alarm. No equivalent replacement coverage is claimed. Audit1318: src/shared/remote-runtime/control/remote-runtime-shared-control-connection.test.ts: Retain real local encrypted WebSocket regressions for pre-ready socket replacement preserving a request, close-before-ready logical subscription cleanup, unrelated RPC surviving another deadline, and pending setup abort leaving a sibling pending. Host server is synthetic and uses production crypto; kept scenarios also retain internal diagnostics/memory assertions unchanged. Remove happy routing, authority-envelope, unknown-ID/log, cleanup enum, binary, keepalive/liveness and production-budget/private socket inventories; intentionally lose those alarms, no physical host authorization, independent crypto or remote provider claim. Reduced selectors need fresh validation; historical full-suite receipts do not validate the changed coverage. No equivalent replacement or physical-provider claim. Audit1342: src/renderer/src/hooks/ipc-events/direct-ssh-connect-reply-routing.test.ts removed. Initial and event-triggered partial-authority hydration fixture alarms disappear. src/renderer/src/components/terminal-pane/terminal-error-accumulation.test.ts removed. Multiline dedup/storm bounds, resource classification/collision, scoped clear/reconnect strip and1000-pane map count alarms disappear. No equivalent replacement coverage is claimed. Audit1396: src/renderer/src/components/terminal-pane/pty-connection-pending-spawn.spec.ts reduced. Lose standalone debug-state retention/log silence alarm. React StrictMode is not mounted; bindings manually simulate remount over mocked transport. No equivalent replacement coverage is claimed. The existing primary-selector receipt and its claims remain scoped to their unchanged sources; the broad optional command has no new execution result. Audit1400: src/main/ipc/ssh/ssh.test.ts reduced. Lose trivial missing target and mocked publication inventory. Preexisting unresolved ../ssh/ssh-port-scanner mock is recorded, not fixed. src/renderer/src/runtime/sync-runtime-graph-scheduling.test.ts removed. Removes whole coalescing call-count suite; no newest-state delivery or replacement claim. src/renderer/src/store/slices/terminal-pty-identity-replacement.test.ts reduced. Removes three no-op/work-count registrations; retains externally observed ownership consistency. No equivalent replacement coverage is claimed. Existing exact unaffected receipts remain unchanged and apply only to their unchanged selectors. Changed optional selectors have no new execution result. Audit1412: src/main/ssh/ssh-provider-authority.test.ts removed. Loses real AbortController revocation/target isolation/reentrancy outcomes, recreated-target generation-floor/session rollover behavior, stale mutation refusal and settled-registration cleanup alarms. These useful alarms are deliberately lost; no equivalent cancellation coverage is claimed. No equivalent replacement coverage is claimed. Existing exact unaffected receipts remain unchanged and apply only to their unchanged selectors. Changed optional selectors have no new execution result. Audit1743 removes the standalone line-height clamp/default and pane cwd fallback helper tests (5 authored cases). These trivial-helper alarms are intentionally lost, with no equivalent replacement. Only current commands/testFiles lose the deleted selectors; historical receipts and evidenceRuns remain unchanged and do not validate the reduced commands.", "motivatingLinks": [ "https://linear.app/stably/issue/STA-3107", "https://github.com/stablyai/orca/pull/12664", @@ -9641,13 +9630,13 @@ "https://github.com/stablyai/orca/issues/12902" ], "invariant": "SSH, WSL, and remote-runtime restore paths must treat provider listing failures and unknown liveness as unknown, not dead, while still avoiding duplicate spawn and clearing expired relay leases exactly once. Direct SSH disconnect, reconnect, and wake must keep PTY bindings and relay identity, and target snapshot hydration/reconnect cannot reset sibling SSH, local, WSL, or runtime-owned state. Every restored remote terminal must preserve its provider PTY identity, including the authoritative incarnation returned by a successful session-ID reattach. After a recoverable partition the same authenticated runtime must reattach the same PTY, reject detached input, apply the latest viewport, and report healthy only after authoritative replay. A successful one-shot reachability probe may replace a pre-ready shared-control socket, but waiting RPCs must continue onto the replacement under their original deadline without duplicate host delivery or retained request bytes. Automatic PTY recovery stops after one bounded minute without a fatal terminal error; a manual reconnect starts a newly fenced epoch against the same PTY, and closed panes retain no recovery UI state. Successful current-transport recovery must clear every stale error surfaced for that pane without clearing or displaying a sibling pane's error; a later genuine failure must remain visible. Each transport retains at most eight suppressed messages, each pane retains at most eight distinct messages, and the rendered surface retains at most 24 lines and 4,000 characters. One capability-gated terminal-create mutation must produce at most one host PTY across an unknown response outcome, remain manually retryable after cutoff, and never let a stale completion replace a newer pane lifecycle. Reconnect must alternate exact activation with authoritative inventory so neither a stale activation response nor an activation failure can strand or retire a pane, and activating a parked surface whose persisted binding was already retired must respawn it rather than report a changed owner after signalling its exit.", - "oracle": "Audit2133 current-parent preparation: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2076 (sources2073/2074): Intentionally retire these low-value alarms without replacement: src/main/ssh/connection/ssh-connection-manager.test.ts: tears a failed startup down quietly, so its error is not replaced by a disconnect; marks a dial that never reached the host, so setup reports SSH’s reason; removes the registration when bulk teardown fails; leaves a connection whose own reconnect is asking the user alone; clears the pool entry when the closed connection is still the registered one; still reuses a connection that is already up; dials a user's connection test. Existing unrelated evidenceRuns retain their original scope; unchanged dated receipt objects do not validate changed source bytes or reduced selectors. No fresh gate execution or historical import-closure equivalence is claimed. Frozen current literal-import graph reaches these changed test bytes; unresolved local and computed edges remain debt, and current reachability is not historical execution or equivalent replacement evidence. Historical prior account (preserved verbatim): Audit2052 (sources2049/2050/2051): Remove complete low-value restored-pane-title and pane-visibility-identity suites and their current selectors. Their supplied snapshot/title and nullable identity-comparison alarms are intentionally lost without replacement. Unrelated dated evidenceRuns remain unchanged and do not validate the reduced commands. No fresh gate execution, actual PTY/provider, rendered, remote/mobile or cross-platform result is claimed. Historical prior account (preserved verbatim): Audit1983: Delete the terminal-create idempotency forwarding suite and its prospective selector. Preserve2 unrelated receipts without claiming the lost forwarding alarm was replaced. Changed-source prior results do not validate reduced-source bytes. No fresh execution, equivalent replacement, red/green, measured budget, soak, provider/PTY, rendered, live remote/mobile or cross-platform result is claimed. Historical prior account (preserved verbatim): Audit1931 (sources1928/1929): Delete complete transport-unmount and queued-initial-CWD helper suites: despite lifecycle-shaped fixtures, the unmount owner returns a Boolean PTY-id predicate, while CWD cases check nullish field selection and callback counts. Lose their detach/destroy/teardown and one-shot-CWD supplied examples. Partial queued-pane-startup loses three identical/empty/nonowner identity examples, retaining four exact one-shot/replacement ownership callbacks. No assertion object maps to these three sources, so unrelated alias objects remain byte-exact. Preserve the current81ed prior removal of pane-split-startup; stale bd948 source metadata snapshots are not replayed. No actual process/PTY startup, transport shutdown, live SSH or rendered recovery is proven by this metadata change. Preserve meaningful remaining selectors/assertions and unchanged dated receipts within their original scope. Reduced selectors have no fresh gate execution evidence in this metadata packet. No equivalent replacement, physical watcher/PTY/provider, live SSH/remote/mobile, rendered, cross-platform, soak or measured-budget result is claimed. Historical prior account (preserved verbatim): Audit1925 (sources1922/1923): Delete the sole supplied public-SSH-state spread/sanitized-error projection and its provider-authority-pair alias. Also delete the complete tiny one-shot split-startup wrapper suite: callback-visible temporary field/reset, sequential payload and thrown-sentinel reset alarms disappear. No SSH provider epoch handoff, connection, process startup or actual terminal split was performed by either deleted suite. Preserve meaningful remaining selectors/assertions and unchanged dated receipts within their original scope. Reduced selectors have no fresh gate execution evidence in this metadata packet. No equivalent replacement, physical watcher/PTY/provider, live SSH/remote/mobile, rendered, cross-platform, soak or measured-budget result is claimed. Historical prior account (preserved verbatim): Audit1903 removes whole mocked detach, supplied geometry and/or close-request suites selected by this gate. Their pending-CWD/PTY-binding argument/order, fabricated hit-test and close bookkeeping alarms are intentionally lost without replacement. Valid remaining selectors/assertions and unchanged-selector execution receipts retain their original scope. Reduced unit selections have no fresh broad gate, real PTY/provider, rendered focus, or measured-budget validation. Historical prior account (preserved verbatim): Existing exact unchanged-selector receipts remain the only current execution evidence. Changed source selectors have no fresh execution result. Historical prior-source oracle account: Existing exact unchanged-selector receipts remain the only current execution evidence. Changed source selectors have no fresh execution result. Historical prior-source oracle account: The retained unchanged-source receipts are the only current execution evidence; reduced-source assertions await validation. Historical oracle account (not a current claim for changed sources): The retained unchanged-source receipts are the only current execution evidence; reduced-source assertions await validation. Historical oracle account (not a current claim for changed sources): Deterministic tests cover bounded stale-handle replacement, suspended heartbeat clocks, cold and established subscription failure, ten partition/recovery cycles, automatic-recovery cutoff, manual reconnect, and direct SSH binding retention. They assert SSH state changes leave PTY bindings in place, relay identity survives, and another target/local/WSL/runtime panes remain byte-identical through target snapshot hydration and reconnect. Both remount callbacks consume split-count activity suppression, intentional dispose emits no failure/timeout, and a same-attempt StrictMode remount still owns one timeout. Hydration clears an untrusted PTY hint without clearing its current pending owner, healthy current-authority bindings suppress correction, hydration finalizes once, and reconnect emits no paired close lifecycle. A provider-level session-ID reattach returns an incarnation, then a legacy exit without an incarnation must resolve to that returned identity rather than minting a fallback identity. The shared-control oracle withholds the first encrypted ready frame, starts one RPC, triggers the successful-probe refresh, then requires exactly two client connections, one host request, a successful response, zero pending calls, and zero retained request bytes. Tests also assert one unsubscribe per remote-runtime epoch, observable recovery phases, stable PTY identity, resumed snapshot/output/input, no healthy state before replay, no retry or input after cutoff, a new manual epoch against the same PTY, quiet recovery UI with an explicit Reconnect action, pane-close state cleanup, one stable create mutation id, old-runtime no-retry behavior, cross-process PTY adoption, and bounded in-flight coordination. A focused recovery oracle surfaces two distinct failures through one transport, completes authoritative replay, and requires both matching clear callbacks; pane-state tests require only the active pane's error to render, matching-pane recovery to preserve sibling and unrelated errors, dismissal to re-admit later failures, and distinct storms to retain only the newest eight messages within 24 lines and 4,000 characters. The retained store replacement assertion observes one atomic pane/tab subscriber publication; repeated rotations and snapshot-first pending-restart migration no longer have their direct store examples.", + "oracle": "Audit3096: remove the deleted reconnect-banner selector and its two UI aliases. The reduced recovery-state selector retains only the deadline cancelling pending backoff and explicit retry owning a new epoch alias; healthy-replacement stale-schedule and independent caller-cutoff cancellation aliases retire. Other retained state cases cover reentrant retry, owner settlement and same-epoch parked revival, without replacing those removed alarms. Prior admission-selector retirement, archived July 28 receipt, unrelated selectors/claims/receipts and old gaps remain unchanged; no fresh execution is claimed. Audit3086: retire the destroyed fresh-session fallback settlement/non-destructive reattach alias and its direct selector; retain the detach/attach selector and assertion unchanged. The retained admission cases cover adopt-before-data and explicitly buffered-death admission without a new spawn, without replacing fallback retirement. July 28 evidence was already archived by audit3077; no duplicate retirement or new execution is claimed. Audit2133 current-parent preparation: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2076 (sources2073/2074): Intentionally retire these low-value alarms without replacement: src/main/ssh/connection/ssh-connection-manager.test.ts: tears a failed startup down quietly, so its error is not replaced by a disconnect; marks a dial that never reached the host, so setup reports SSH’s reason; removes the registration when bulk teardown fails; leaves a connection whose own reconnect is asking the user alone; clears the pool entry when the closed connection is still the registered one; still reuses a connection that is already up; dials a user's connection test. Existing unrelated evidenceRuns retain their original scope; unchanged dated receipt objects do not validate changed source bytes or reduced selectors. No fresh gate execution or historical import-closure equivalence is claimed. Frozen current literal-import graph reaches these changed test bytes; unresolved local and computed edges remain debt, and current reachability is not historical execution or equivalent replacement evidence. Historical prior account (preserved verbatim): Audit2052 (sources2049/2050/2051): Remove complete low-value restored-pane-title and pane-visibility-identity suites and their current selectors. Their supplied snapshot/title and nullable identity-comparison alarms are intentionally lost without replacement. Unrelated dated evidenceRuns remain unchanged and do not validate the reduced commands. No fresh gate execution, actual PTY/provider, rendered, remote/mobile or cross-platform result is claimed. Historical prior account (preserved verbatim): Audit1983: Delete the terminal-create idempotency forwarding suite and its prospective selector. Preserve2 unrelated receipts without claiming the lost forwarding alarm was replaced. Changed-source prior results do not validate reduced-source bytes. No fresh execution, equivalent replacement, red/green, measured budget, soak, provider/PTY, rendered, live remote/mobile or cross-platform result is claimed. Historical prior account (preserved verbatim): Audit1931 (sources1928/1929): Delete complete transport-unmount and queued-initial-CWD helper suites: despite lifecycle-shaped fixtures, the unmount owner returns a Boolean PTY-id predicate, while CWD cases check nullish field selection and callback counts. Lose their detach/destroy/teardown and one-shot-CWD supplied examples. Partial queued-pane-startup loses three identical/empty/nonowner identity examples, retaining four exact one-shot/replacement ownership callbacks. No assertion object maps to these three sources, so unrelated alias objects remain byte-exact. Preserve the current81ed prior removal of pane-split-startup; stale bd948 source metadata snapshots are not replayed. No actual process/PTY startup, transport shutdown, live SSH or rendered recovery is proven by this metadata change. Preserve meaningful remaining selectors/assertions and unchanged dated receipts within their original scope. Reduced selectors have no fresh gate execution evidence in this metadata packet. No equivalent replacement, physical watcher/PTY/provider, live SSH/remote/mobile, rendered, cross-platform, soak or measured-budget result is claimed. Historical prior account (preserved verbatim): Audit1925 (sources1922/1923): Delete the sole supplied public-SSH-state spread/sanitized-error projection and its provider-authority-pair alias. Also delete the complete tiny one-shot split-startup wrapper suite: callback-visible temporary field/reset, sequential payload and thrown-sentinel reset alarms disappear. No SSH provider epoch handoff, connection, process startup or actual terminal split was performed by either deleted suite. Preserve meaningful remaining selectors/assertions and unchanged dated receipts within their original scope. Reduced selectors have no fresh gate execution evidence in this metadata packet. No equivalent replacement, physical watcher/PTY/provider, live SSH/remote/mobile, rendered, cross-platform, soak or measured-budget result is claimed. Historical prior account (preserved verbatim): Audit1903 removes whole mocked detach, supplied geometry and/or close-request suites selected by this gate. Their pending-CWD/PTY-binding argument/order, fabricated hit-test and close bookkeeping alarms are intentionally lost without replacement. Valid remaining selectors/assertions and unchanged-selector execution receipts retain their original scope. Reduced unit selections have no fresh broad gate, real PTY/provider, rendered focus, or measured-budget validation. Historical prior account (preserved verbatim): Existing exact unchanged-selector receipts remain the only current execution evidence. Changed source selectors have no fresh execution result. Historical prior-source oracle account: Existing exact unchanged-selector receipts remain the only current execution evidence. Changed source selectors have no fresh execution result. Historical prior-source oracle account: The retained unchanged-source receipts are the only current execution evidence; reduced-source assertions await validation. Historical oracle account (not a current claim for changed sources): The retained unchanged-source receipts are the only current execution evidence; reduced-source assertions await validation. Historical oracle account (not a current claim for changed sources): Deterministic tests cover bounded stale-handle replacement, suspended heartbeat clocks, cold and established subscription failure, ten partition/recovery cycles, automatic-recovery cutoff, manual reconnect, and direct SSH binding retention. They assert SSH state changes leave PTY bindings in place, relay identity survives, and another target/local/WSL/runtime panes remain byte-identical through target snapshot hydration and reconnect. Both remount callbacks consume split-count activity suppression, intentional dispose emits no failure/timeout, and a same-attempt StrictMode remount still owns one timeout. Hydration clears an untrusted PTY hint without clearing its current pending owner, healthy current-authority bindings suppress correction, hydration finalizes once, and reconnect emits no paired close lifecycle. A provider-level session-ID reattach returns an incarnation, then a legacy exit without an incarnation must resolve to that returned identity rather than minting a fallback identity. The shared-control oracle withholds the first encrypted ready frame, starts one RPC, triggers the successful-probe refresh, then requires exactly two client connections, one host request, a successful response, zero pending calls, and zero retained request bytes. Tests also assert one unsubscribe per remote-runtime epoch, observable recovery phases, stable PTY identity, resumed snapshot/output/input, no healthy state before replay, no retry or input after cutoff, a new manual epoch against the same PTY, quiet recovery UI with an explicit Reconnect action, pane-close state cleanup, one stable create mutation id, old-runtime no-retry behavior, cross-process PTY adoption, and bounded in-flight coordination. A focused recovery oracle surfaces two distinct failures through one transport, completes authoritative replay, and requires both matching clear callbacks; pane-state tests require only the active pane's error to render, matching-pane recovery to preserve sibling and unrelated errors, dismissal to re-admit later failures, and distinct storms to retain only the newest eight messages within 24 lines and 4,000 characters. The retained store replacement assertion observes one atomic pane/tab subscriber publication; repeated rotations and snapshot-first pending-restart migration no longer have their direct store examples.", "commands": [ "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/pty-transport/remote-runtime/paired-reconnect-multi-pane-materialization.test.ts src/renderer/src/runtime/web-session-tabs-sync/agent-status/paired-reconnect-sidebar-agent-count.test.ts --reporter=dot", "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/lib/resolved-worktree-execution-host.test.ts src/renderer/src/components/terminal/background-terminal-worktree-mount.test.ts src/renderer/src/components/terminal-pane/mounting/queued-pane-startup.test.ts src/renderer/src/components/terminal-pane/parking/one-shot-park-replay.test.ts src/renderer/src/components/terminal-pane/pty-connection/tests/pty-connection-spawn-and-startup.test.ts src/renderer/src/components/terminal-pane/pty-transport/remote-runtime/remote-runtime-pty-transport-host-surface-replacement.test.ts src/renderer/src/components/terminal-pane/pty-transport/remote-runtime/remote-runtime-pty-transport-stream-reconnect.test.ts src/renderer/src/store/terminals/terminal-pty-identity-replacement.test.ts", - "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/pty-transport/ipc/pty-transport-reattach-admission.test.ts src/renderer/src/components/terminal-pane/pty-transport/ipc/pty-transport-detach-attach-handoff.test.ts", + "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/pty-transport/ipc/pty-transport-detach-attach-handoff.test.ts", "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/pty-transport/remote-runtime/remote-runtime-pty-transport-attach-subscription.test.ts --reporter=dot", - "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/pty-transport/remote-runtime/remote-runtime-pty-transport-attach-subscription.test.ts src/renderer/src/components/terminal-pane/pty-transport/remote-runtime/remote-runtime-pty-transport-host-surface-replacement.test.ts src/renderer/src/components/terminal-pane/pty-transport/remote-runtime/remote-runtime-pty-transport-stream-reconnect.test.ts src/renderer/src/components/terminal-pane/pty-transport/remote-runtime/remote-runtime-pty-transport-expired-pane-recovery.test.ts src/renderer/src/components/terminal-pane/pty-transport/remote-runtime/remote-runtime-pty-transport-create-outcome-recovery.test.ts src/renderer/src/components/terminal-pane/pty-transport/remote-runtime/remote-runtime-pty-transport-create-handoff.test.ts src/renderer/src/components/terminal-pane/pty-transport/remote-runtime/recovery/remote-runtime-pty-recovery-state.test.ts src/renderer/src/components/terminal-pane/pty-transport/remote-runtime/recovery/TerminalRemoteRuntimeReconnectBanner.test.tsx src/shared/remote-runtime/remote-runtime-socket-liveness.test.ts src/shared/remote-runtime/control/remote-runtime-shared-control-connection.test.ts src/shared/remote-runtime/control/remote-runtime-shared-control-socket-generation.test.ts src/shared/remote-runtime/remote-runtime-client-error-classification.test.ts src/main/runtime/rpc/remote-runtime-server-heartbeat.test.ts src/main/runtime/terminal/launch/orca-runtime-terminal-create-idempotency.test.ts", + "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/pty-transport/remote-runtime/remote-runtime-pty-transport-attach-subscription.test.ts src/renderer/src/components/terminal-pane/pty-transport/remote-runtime/remote-runtime-pty-transport-host-surface-replacement.test.ts src/renderer/src/components/terminal-pane/pty-transport/remote-runtime/remote-runtime-pty-transport-stream-reconnect.test.ts src/renderer/src/components/terminal-pane/pty-transport/remote-runtime/remote-runtime-pty-transport-expired-pane-recovery.test.ts src/renderer/src/components/terminal-pane/pty-transport/remote-runtime/remote-runtime-pty-transport-create-outcome-recovery.test.ts src/renderer/src/components/terminal-pane/pty-transport/remote-runtime/remote-runtime-pty-transport-create-handoff.test.ts src/renderer/src/components/terminal-pane/pty-transport/remote-runtime/recovery/remote-runtime-pty-recovery-state.test.ts src/shared/remote-runtime/remote-runtime-socket-liveness.test.ts src/shared/remote-runtime/control/remote-runtime-shared-control-connection.test.ts src/shared/remote-runtime/control/remote-runtime-shared-control-socket-generation.test.ts src/shared/remote-runtime/remote-runtime-client-error-classification.test.ts src/main/runtime/rpc/remote-runtime-server-heartbeat.test.ts src/main/runtime/terminal/launch/orca-runtime-terminal-create-idempotency.test.ts", "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/store/slices/ui/workspace-navigation-history-hydration.test.ts src/renderer/src/store/repos/repos-ssh-host-reconciliation.test.ts src/renderer/src/components/terminal-pane/pty-connection/tests/pty-connection-spawn-and-startup.test.ts --reporter=dot", "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/ssh/ssh.test.ts src/main/ipc/worktrees-ssh-repo-owner-resolution.test.ts src/main/ssh/connection/ssh-connection-manager.test.ts src/main/ssh/connection/ssh-connection.test.ts src/preload/ssh-authority-forwarding.test.ts src/renderer/src/runtime/rpc/subscriptions/runtime-client-events.test.ts src/renderer/src/runtime/hosts/ssh/runtime-environment-ssh-state.test.ts src/shared/ssh-retained-payload-admission.test.ts --reporter=dot", "pnpm exec electron-vite build --mode e2e", @@ -9662,7 +9651,6 @@ "src/renderer/src/components/terminal-pane/mounting/queued-pane-startup.test.ts", "src/renderer/src/components/terminal-pane/parking/one-shot-park-replay.test.ts", "src/renderer/src/components/terminal-pane/pty-connection/tests/pty-connection-spawn-and-startup.test.ts", - "src/renderer/src/components/terminal-pane/pty-transport/ipc/pty-transport-reattach-admission.test.ts", "src/renderer/src/components/terminal-pane/pty-transport/ipc/pty-transport-detach-attach-handoff.test.ts", "src/renderer/src/components/terminal-pane/pty-transport/remote-runtime/remote-runtime-pty-transport-attach-subscription.test.ts", "src/renderer/src/components/terminal-pane/pty-transport/remote-runtime/remote-runtime-pty-transport-host-surface-replacement.test.ts", @@ -9671,7 +9659,6 @@ "src/renderer/src/components/terminal-pane/pty-transport/remote-runtime/remote-runtime-pty-transport-create-outcome-recovery.test.ts", "src/renderer/src/components/terminal-pane/pty-transport/remote-runtime/remote-runtime-pty-transport-create-handoff.test.ts", "src/renderer/src/components/terminal-pane/pty-transport/remote-runtime/recovery/remote-runtime-pty-recovery-state.test.ts", - "src/renderer/src/components/terminal-pane/pty-transport/remote-runtime/recovery/TerminalRemoteRuntimeReconnectBanner.test.tsx", "src/renderer/src/store/terminals/terminal-pty-identity-replacement.test.ts", "src/shared/remote-runtime/remote-runtime-socket-liveness.test.ts", "src/shared/remote-runtime/control/remote-runtime-shared-control-connection.test.ts", @@ -9747,16 +9734,7 @@ { "file": "src/renderer/src/components/terminal-pane/pty-transport/remote-runtime/recovery/remote-runtime-pty-recovery-state.test.ts", "assertions": [ - "a healthy replacement invalidates the prior recovery epoch so a slower failure cannot re-arm retry", - "the automatic-recovery deadline cancels pending backoff and a manual retry owns a new epoch", - "a caller-owned recovery cutoff cancels scheduled work and remains disconnected" - ] - }, - { - "file": "src/renderer/src/components/terminal-pane/pty-transport/remote-runtime/recovery/TerminalRemoteRuntimeReconnectBanner.test.tsx", - "assertions": [ - "automatic recovery renders as a quiet bounded status without a button", - "the disconnected state exposes one explicit Reconnect action" + "the automatic-recovery deadline cancels pending backoff and a manual retry owns a new epoch" ] }, { @@ -9828,12 +9806,6 @@ "a StrictMode remount joins the pending spawn instead of starting a second one" ] }, - { - "file": "src/renderer/src/components/terminal-pane/pty-transport/ipc/pty-transport-reattach-admission.test.ts", - "assertions": [ - "a destroyed fresh session fallback settles retirement before it can publish handlers, and reattach remains non-destructive" - ] - }, { "file": "src/renderer/src/components/terminal-pane/pty-transport/ipc/pty-transport-detach-attach-handoff.test.ts", "assertions": [ @@ -9851,16 +9823,6 @@ "durationSeconds": 2.54, "summary": "Seven deterministic STA-3107 contracts passed across six-pane multiplex rematerialization and client-owned sidebar-row retention after sleep/wake.", "currentCommand": "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/pty-transport/remote-runtime/paired-reconnect-multi-pane-materialization.test.ts src/renderer/src/runtime/web-session-tabs-sync/agent-status/paired-reconnect-sidebar-agent-count.test.ts --reporter=dot" - }, - { - "date": "2026-07-28", - "runner": "local", - "platform": "macos", - "command": "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/pty-transport-reattach-admission.test.ts src/renderer/src/components/terminal-pane/pty-transport-detach-attach-handoff.test.ts", - "result": "passed", - "durationSeconds": 1.48, - "summary": "All 90 transport tests passed, including pre-publication admission rejection, handler-complete non-destructive detach for obsolete SSH reattach transports, settled retirement of rejected or destroyed fresh fallbacks, and reported shutdown refusal.", - "currentCommand": "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/pty-transport/ipc/pty-transport-reattach-admission.test.ts src/renderer/src/components/terminal-pane/pty-transport/ipc/pty-transport-detach-attach-handoff.test.ts" } ], "runtimeBudget": { @@ -9885,6 +9847,8 @@ "Include reconnect/polling artifacts that identify provider, connection id, and lease state." ], "knownGaps": [ + "Audit3077 reduces the IPC handoff suite while retaining the obsolete-reattach detach observer case and its alias. Removed nominal no-kill and gauge-cleanup alarms have no equivalent replacement.", + "Audit3077 retires this complete dated receipt because its selected source was reduced. It does not validate the reduced selector. Original receipt verbatim:\n{\n \"date\": \"2026-07-28\",\n \"runner\": \"local\",\n \"platform\": \"macos\",\n \"command\": \"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/pty-transport-reattach-admission.test.ts src/renderer/src/components/terminal-pane/pty-transport-detach-attach-handoff.test.ts\",\n \"result\": \"passed\",\n \"durationSeconds\": 1.48,\n \"summary\": \"All 90 transport tests passed, including pre-publication admission rejection, handler-complete non-destructive detach for obsolete SSH reattach transports, settled retirement of rejected or destroyed fresh fallbacks, and reported shutdown refusal.\",\n \"currentCommand\": \"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/pty-transport/ipc/pty-transport-reattach-admission.test.ts src/renderer/src/components/terminal-pane/pty-transport/ipc/pty-transport-detach-attach-handoff.test.ts\"\n }", "Audit3069 retires nominal spawn/resume/banner wiring, first-fit and lookup-budget variants, private pending-map checks, the split-CWD disposal suite and startup command/draft baseline variants. Distinct regression alarms are intentionally lost without equivalent replacement; retained ownership, cancellation and recovery cases remain. Existing selectors and dated evidence retain their original scope.", "Audit3061 retires the park-model supplied snapshot suite and nominal mode, cursor, focus, geometry and remote resume metadata variants. Their distinct alarms are intentionally lost; six retained mode/remote cases preserve selected tail-order, authority and parse-readiness boundaries without equivalent replacement. Existing selectors and dated evidence retain their original scope.", "Audit2133 current-parent preparation (988 authored removals; independently reviewed2099/2107/2115/2116 decisions; upstream clear-buffer and recovery-image regressions retained): Intentionally retire these low-value alarms without replacement: src/renderer/src/components/terminal-pane/pty-connection/tests/pty-connection-spawn-and-startup.test.ts: remove only literal registration import/call pairs import { registerPtyConnectionTerminalBellAttentionTests } from '../notifications/pty-connection-terminal-bell-attention.spec', registerPtyConnectionTerminalBellAttentionTests(); no additional authored credit. src/renderer/src/store/terminals/workspace-terminal-reconnect-hydration.test.ts: restores daemon terminal wake hints before declaring the session ready. src/main/ssh/connection/ssh-connection.test.ts: transitions to connected on successful connect; captures the negotiated SSH server key fingerprint; allows concurrent exec commands for ssh2 transport; removes startup listeners after ssh2 connect succeeds; scopes lifecycle events and pending handshake timers to one mock client; enables TCP_NODELAY on the new ssh2 client after a reconnect cycle; transitions through connecting → connected states; reports error state on connection failure; disconnect cleans up and sets state to disconnected; getTarget returns a copy of the target; getState returns a copy of the state; throws when connecting a disposed connection. src/renderer/src/components/terminal-pane/pty-connection/notifications/pty-connection-terminal-bell-attention.spec.ts: forwards onBell to pane attention and notification policy; hands the bell to the attention module with its pane even when pane attention is off. Current changed source bytes, wrapper registrations and reduced selectors have no fresh execution or red/green credit. Unrelated surviving receipt objects keep their original narrow scope; current literal graph reachability does not establish historical import closure or equivalent replacement coverage. Current conservative command/import graph reaches src/renderer/src/components/terminal-pane/pty-connection/tests/pty-connection-spawn-and-startup.test.ts, src/renderer/src/components/terminal-pane/pty-connection/notifications/pty-connection-terminal-bell-attention.spec.ts, src/renderer/src/store/terminals/workspace-terminal-reconnect-hydration.test.ts, src/main/ssh/connection/ssh-connection.test.ts. Unresolved local/computed edges remain explicit debt. Retired active assertionRefs (raw original objects retained as historical only): {\n \"file\": \"src/renderer/src/store/terminals/workspace-terminal-reconnect-hydration.test.ts\",\n \"assertions\": [\"restores daemon terminal wake hints before declaring the session ready\"]\n }", @@ -9933,7 +9897,9 @@ "Audit1983 historical original testFiles object, preserved verbatim; retired objects are not active execution evidence or replacement coverage:\n\"src/main/runtime/rpc/methods/terminal/terminal-create-idempotency.test.ts\"", "Audit2052 (sources2049/2050/2051): Remove complete low-value restored-pane-title and pane-visibility-identity suites and their current selectors. Their supplied snapshot/title and nullable identity-comparison alarms are intentionally lost without replacement. Unrelated dated evidenceRuns remain unchanged and do not validate the reduced commands. No fresh gate execution, actual PTY/provider, rendered, remote/mobile or cross-platform result is claimed.", "Audit2195 removes the low-value startup admission boolean and host deferral predicate inventories from background-terminal-worktree-mount.test.ts. The reduced source retains concrete targeted mount, prune and reveal ownership transitions, not those oracle assertions. Existing selectors remain unexecuted here; no equivalent replacement or new runtime/provider/platform/budget result is claimed. Original retired assertion object verbatim:\n{\n \"file\": \"src/renderer/src/components/terminal/background-terminal-worktree-mount.test.ts\",\n \"assertions\": [\n \"startup waits for hydration before mounting terminal panes while degraded mode remains interactive\",\n \"only an explicit local execution host can defer cold activation\",\n \"SSH, remote-runtime, and unresolved owners remain eager\"\n ]\n }", - "Audit2709 removes src/renderer/src/runtime/web-session-tabs-sync/inventory/remote-runtime-session-tabs-inflight.test.ts: intentionally retire waiting out an older inventory and sharing a fresh post-operation request among concurrent callers. No equivalent replacement or fresh execution is claimed. Dated receipts remain historical evidence for their unchanged selectors only; they do not validate the reduced commands or full current gate. Original prospective command before removal: pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/lib/resolved-worktree-execution-host.test.ts src/renderer/src/components/terminal/background-terminal-worktree-mount.test.ts src/renderer/src/components/terminal-pane/mounting/queued-pane-startup.test.ts src/renderer/src/components/terminal-pane/parking/one-shot-park-replay.test.ts src/renderer/src/components/terminal-pane/pty-connection/tests/pty-connection-spawn-and-startup.test.ts src/renderer/src/components/terminal-pane/pty-transport/remote-runtime/remote-runtime-pty-transport-host-surface-replacement.test.ts src/renderer/src/components/terminal-pane/pty-transport/remote-runtime/remote-runtime-pty-transport-stream-reconnect.test.ts src/renderer/src/runtime/web-session-tabs-sync/inventory/remote-runtime-session-tabs-inflight.test.ts src/renderer/src/store/terminals/terminal-pty-identity-replacement.test.ts" + "Audit2709 removes src/renderer/src/runtime/web-session-tabs-sync/inventory/remote-runtime-session-tabs-inflight.test.ts: intentionally retire waiting out an older inventory and sharing a fresh post-operation request among concurrent callers. No equivalent replacement or fresh execution is claimed. Dated receipts remain historical evidence for their unchanged selectors only; they do not validate the reduced commands or full current gate. Original prospective command before removal: pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/lib/resolved-worktree-execution-host.test.ts src/renderer/src/components/terminal/background-terminal-worktree-mount.test.ts src/renderer/src/components/terminal-pane/mounting/queued-pane-startup.test.ts src/renderer/src/components/terminal-pane/parking/one-shot-park-replay.test.ts src/renderer/src/components/terminal-pane/pty-connection/tests/pty-connection-spawn-and-startup.test.ts src/renderer/src/components/terminal-pane/pty-transport/remote-runtime/remote-runtime-pty-transport-host-surface-replacement.test.ts src/renderer/src/components/terminal-pane/pty-transport/remote-runtime/remote-runtime-pty-transport-stream-reconnect.test.ts src/renderer/src/runtime/web-session-tabs-sync/inventory/remote-runtime-session-tabs-inflight.test.ts src/renderer/src/store/terminals/terminal-pty-identity-replacement.test.ts", + "Audit3086: retire the destroyed fresh-session fallback settlement/non-destructive reattach alias and its direct selector; retain the detach/attach selector and assertion unchanged. The retained admission cases cover adopt-before-data and explicitly buffered-death admission without a new spawn, without replacing fallback retirement. July 28 evidence was already archived by audit3077; no duplicate retirement or new execution is claimed.", + "Audit3096: remove the deleted reconnect-banner selector and its two UI aliases. The reduced recovery-state selector retains only the deadline cancelling pending backoff and explicit retry owning a new epoch alias; healthy-replacement stale-schedule and independent caller-cutoff cancellation aliases retire. Other retained state cases cover reentrant retry, owner settlement and same-epoch parked revival, without replacing those removed alarms. Prior admission-selector retirement, archived July 28 receipt, unrelated selectors/claims/receipts and old gaps remain unchanged; no fresh execution is claimed." ], "demotionRule": "Cannot promote if provider failure can close panes or if the oracle is screenshot-only." }, @@ -11886,14 +11852,14 @@ "providers": ["local", "daemon", "ssh"], "coveredPlatforms": ["macos"], "coveredProviders": ["local"], - "coverageNotes": "Audit1878 retires the dated reduced-source unit receipt. Only the unchanged macOS local Electron receipts remain current, scoped to their exact commands; reduced unit selectors and SSH have no current execution receipt. Historical pre-audit coverageNotes: Deterministic renderer and IPC-transport tests prove count and text ceilings, oldest-reply shedding, explicit query-reply source routing, ordinary-input preservation, acknowledged-write FIFO barriers, single-worker drain reentrancy, one-reply-per-write delivery for OSC, DA1, and CPR replies, real xterm OSC reply generation, drain-failure containment, teardown settlement, and clear/reuse generation fencing. Remote-runtime tests preserve separate query-reply writes across pending input, async validation, and viewport-claim buffering. Host-contract tests prove a later DA1/CPR reply cannot overtake a deferred OSC reply, including a coalesced legacy-client payload. Live macOS Electron tests cover local PTY OSC replies and interactive typing; a macOS-hosted Docker OpenSSH test proves an upstream-node-pty Linux relay keeps OSC/DA1 replies out of the next fish child's stdin. No live daemon, paired-runtime, WSL, physical Linux/Windows client, or binary mixed-version run is registered.", + "coverageNotes": "Audit3086: retire the immediate-reply ceiling and thrown renderer-write recovery aliases. Narrow FIFO coverage to deferred preconnect validation and teardown coverage to natural-exit stale ordinary/accepted tails; live acknowledged barriers and disconnect/detach branches are intentionally retired. Prior queue cleanup and unchanged Electron receipts remain exact; no replacement or fresh execution is claimed. Audit1878 retires the dated reduced-source unit receipt. Only the unchanged macOS local Electron receipts remain current, scoped to their exact commands; reduced unit selectors and SSH have no current execution receipt. Historical pre-audit coverageNotes: Deterministic renderer and IPC-transport tests prove count and text ceilings, oldest-reply shedding, explicit query-reply source routing, ordinary-input preservation, acknowledged-write FIFO barriers, single-worker drain reentrancy, one-reply-per-write delivery for OSC, DA1, and CPR replies, real xterm OSC reply generation, drain-failure containment, teardown settlement, and clear/reuse generation fencing. Remote-runtime tests preserve separate query-reply writes across pending input, async validation, and viewport-claim buffering. Host-contract tests prove a later DA1/CPR reply cannot overtake a deferred OSC reply, including a coalesced legacy-client payload. Live macOS Electron tests cover local PTY OSC replies and interactive typing; a macOS-hosted Docker OpenSSH test proves an upstream-node-pty Linux relay keeps OSC/DA1 replies out of the next fish child's stdin. No live daemon, paired-runtime, WSL, physical Linux/Windows client, or binary mixed-version run is registered.", "motivatingLinks": [ "https://github.com/stablyai/orca/issues/13137", "https://github.com/stablyai/orca/issues/7329", "https://github.com/stablyai/orca/issues/13892" ], "invariant": "The desktop PTY input queue retains at most 64 explicitly sourced pending terminal query replies and 4096 UTF-16 code units. Every retained reply reaches the provider as one atomic write, and ordinary, acknowledged, and reply input share one invocation-ordered FIFO so no later write overtakes an acknowledged write. Reentrant write callbacks cannot start a second drain worker or strand input admitted after clear/reuse. A reply's own echo is contained on the output side by projecting its known echo shapes; the ESC-initial verbatim shape is matched only when complete, never held as a partial, so a query torn at its own ESC is still answered. Overflow removes only the oldest query replies, never ordinary input except the documented modified-F3/CPR byte collision, and failure or teardown cannot clear a newer queue generation or strand an acknowledged promise.", - "oracle": "Audit1878 retires the dated reduced-source unit receipt. Only the unchanged macOS local Electron receipts remain current, scoped to their exact commands; reduced unit selectors and SSH have no current execution receipt. Historical pre-audit oracle: Synchronously enqueue separate 10,000-entry OSC and DA1 reply floods before the scheduled drain and assert that only the initial immediate reply and newest 64 pending replies are written, each as one provider write, before a trailing keystroke. Stall an acknowledged write between earlier and later ordinary/reply input, then require teardown to settle it and same-id reuse to receive no stale tail. Reenter the queue synchronously from an acknowledged write with both enqueue and clear/reuse, requiring one drain and fresh input delivery only after the stale acknowledged write settles false. At the host boundary, defer an OSC reply and assert that separate or legacy-coalesced DA1/CPR replies flush after it in observed query order. At the remote-runtime boundary, preserve separate writes around pending ordinary input, async validation, and viewport-claim buffering. Repeat behind 10,000 ordinary inputs and exercise the text ceiling, real xterm generation, provider-write failure, rejected yield, and clear/reuse generation fencing.", + "oracle": "Audit3086: retire the immediate-reply ceiling and thrown renderer-write recovery aliases. Narrow FIFO coverage to deferred preconnect validation and teardown coverage to natural-exit stale ordinary/accepted tails; live acknowledged barriers and disconnect/detach branches are intentionally retired. Prior queue cleanup and unchanged Electron receipts remain exact; no replacement or fresh execution is claimed. Audit1878 retires the dated reduced-source unit receipt. Only the unchanged macOS local Electron receipts remain current, scoped to their exact commands; reduced unit selectors and SSH have no current execution receipt. Historical pre-audit oracle: Synchronously enqueue separate 10,000-entry OSC and DA1 reply floods before the scheduled drain and assert that only the initial immediate reply and newest 64 pending replies are written, each as one provider write, before a trailing keystroke. Stall an acknowledged write between earlier and later ordinary/reply input, then require teardown to settle it and same-id reuse to receive no stale tail. Reenter the queue synchronously from an acknowledged write with both enqueue and clear/reuse, requiring one drain and fresh input delivery only after the stale acknowledged write settles false. At the host boundary, defer an OSC reply and assert that separate or legacy-coalesced DA1/CPR replies flush after it in observed query order. At the remote-runtime boundary, preserve separate writes around pending ordinary input, async validation, and viewport-claim buffering. Repeat behind 10,000 ordinary inputs and exercise the text ceiling, real xterm generation, provider-write failure, rejected yield, and clear/reuse generation fencing.", "commands": [ "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/pty-transport/input/pty-input-write-queue.test.ts src/renderer/src/components/terminal-pane/pty-transport/ipc/pty-transport-input-write.test.ts src/shared/terminal/protocol/queries/terminal-query-reply.test.ts src/shared/terminal/pty/ingress/pty-startup-ingress-live-query-reply.test.ts src/shared/terminal/pty/ingress/pty-startup-reply-echo-shapes.test.ts", "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/pty-transport/remote-runtime/output/remote-runtime-pty-batching.test.ts src/renderer/src/components/terminal-pane/pty-transport/remote-runtime/remote-runtime-pty-query-reply-immediate.test.ts src/renderer/src/components/terminal-pane/pty-transport/remote-runtime/remote-runtime-pty-transport-input-coalescing.test.ts", @@ -11916,14 +11882,8 @@ { "file": "src/renderer/src/components/terminal-pane/pty-transport/input/pty-input-write-queue.test.ts", "assertions": [ - "a synchronous 10,000-reply flood writes only the immediate first and newest 64 pending replies before a trailing keystroke", - "10,000 ordinary queued inputs survive a later reply flood while reply retention remains bounded", - "reply-shaped bytes from the ordinary input path never enter the shed-able reply budget", - "reply count and text overflow remove only the oldest pending replies", - "real xterm OSC 10/11 query handlers remain subject to the same retention ceiling", - "retained OSC, DA1, and CPR replies stay one provider write each and both OSC and DA1 floods remain bounded", "provider-write and yield failures settle without unhandled rejection, repeated same-generation admission, or stale-generation clearing", - "clear releases saturated reply accounting and fences in-flight validation before later input", + "clear fences in-flight validation before later input", "acknowledged input is serialized between earlier and later ordinary/reply writes", "clear settles active and pending acknowledged input before same-id queue reuse", "reentrant enqueue cannot start a second drain worker past an unacknowledged write", @@ -11933,10 +11893,8 @@ { "file": "src/renderer/src/components/terminal-pane/pty-transport/ipc/pty-transport-input-write.test.ts", "assertions": [ - "sendInputImmediate applies the reply ceiling while sendInput preserves a reply-shaped ordinary payload in exact IPC write order", - "a thrown renderer write triggers one owning-transport recovery callback and rejects later input in that queue generation", - "live and preconnect acknowledged writes remain FIFO barriers for later ordinary and immediate input", - "disconnect, detach, and natural exit settle acknowledged writes and fence same-id stale chunks" + "deferred preconnect validation preserves queued bytes before later accepted, ordinary, and immediate input", + "natural exit settles active acknowledged input and fences ordinary and accepted stale chunks before same-id reuse" ] }, { @@ -12026,11 +11984,13 @@ "Accumulate the normal soak window with zero unexplained flakes." ], "knownGaps": [ + "Audit3077 deletes the delivery-watchdog suite and retires queue flood, conservation, atomicity, retention, echo and heap examples plus preconnect cap/seed/abandon examples. The remaining queue failure/FIFO/reentrancy/generation and preconnect lifetime cases are narrower; no equivalent replacement is claimed.", "No live daemon, paired-runtime, WSL, physical Linux/Windows client, mixed-version, or network-fault run is registered.", "Ordinary-input queue growth predates this change and is outside this reply-only gate.", "Reply overload intentionally sheds the oldest synthetic replies; the modified-F3/CPR byte collision shares that budget, and live application behavior under a pathological query flood is not measured.", "Audit1878 retires this complete dated receipt because a source in its selector was reduced. It does not validate the reduced selector. Original receipt verbatim:\n{\n \"date\": \"2026-08-30\",\n \"runner\": \"local\",\n \"platform\": \"macos\",\n \"command\": \"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/pty-input-write-queue.test.ts src/renderer/src/components/terminal-pane/pty-transport-input-write.test.ts src/shared/terminal-query-reply.test.ts src/shared/pty-startup-ingress-live-query-reply.test.ts src/shared/pty-startup-reply-echo-shapes.test.ts\",\n \"currentCommand\": \"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/pty-transport/input/pty-input-write-queue.test.ts src/renderer/src/components/terminal-pane/pty-transport/ipc/pty-transport-input-write.test.ts src/shared/terminal/protocol/queries/terminal-query-reply.test.ts src/shared/terminal/pty/ingress/pty-startup-ingress-live-query-reply.test.ts src/shared/terminal/pty/ingress/pty-startup-reply-echo-shapes.test.ts\",\n \"result\": \"passed\",\n \"durationSeconds\": 15,\n \"summary\": \"Five files and 149 tests passed in 15.16 seconds, including explicit IPC reply-source routing, the 10,000-reply count and text ceilings, 10,000-entry ordinary backlog preservation, acknowledged-write FIFO barriers, synchronous reentrancy fencing, prompt teardown settlement, same-id generation fencing, real xterm OSC query floods, single-shot drain-failure recovery, and one-reply-per-write echo containment.\"\n }", - "Audit1878 removes standalone reply-classifier inventory, ordinary-keystroke helper/no-write checks and a latency-order title that never executes the host path from pty-startup-ingress-live-query-reply.test.ts. It also removes projection needle/refusal/holdPartial and ConPTY/WSL string/array inventories from pty-startup-reply-echo-shapes.test.ts, including the direct short-needle and ESC-hold guard alarms. Captured echo matching and actual ingress writes/output state remain, without equivalent replacement coverage. Only the two unchanged macOS local Electron receipts remain active; neither proves the reduced five-file unit selector, SSH, a new soak result, or a new measured budget." + "Audit1878 removes standalone reply-classifier inventory, ordinary-keystroke helper/no-write checks and a latency-order title that never executes the host path from pty-startup-ingress-live-query-reply.test.ts. It also removes projection needle/refusal/holdPartial and ConPTY/WSL string/array inventories from pty-startup-reply-echo-shapes.test.ts, including the direct short-needle and ESC-hold guard alarms. Captured echo matching and actual ingress writes/output state remain, without equivalent replacement coverage. Only the two unchanged macOS local Electron receipts remain active; neither proves the reduced five-file unit selector, SSH, a new soak result, or a new measured budget.", + "Audit3086: retire the immediate-reply ceiling and thrown renderer-write recovery aliases. Narrow FIFO coverage to deferred preconnect validation and teardown coverage to natural-exit stale ordinary/accepted tails; live acknowledged barriers and disconnect/detach branches are intentionally retired. Prior queue cleanup and unchanged Electron receipts remain exact; no replacement or fresh execution is claimed." ], "demotionRule": "Keep experimental or revert if ordinary input is evicted, pending replies exceed either ceiling, retained replies are coalesced, a trailing key sits behind more than the bounded reply window, or the gate develops unexplained flakes." }, @@ -12689,7 +12649,7 @@ "https://github.com/stablyai/orca/pull/8034" ], "invariant": "After a renderer lifecycle reset (main-frame did-start-navigation / render-process-gone / destroyed), no surviving PTY remains delivery-gated by pre-reset unacked bytes: main's in-flight counters and pending backlog equal the true state of the new page (zero in-flight, zero pending). Delivery then resumes only once the reloaded page's pty:data dispatcher re-registers and signals pty:rendererDispatcherReady; during the boot window before that handshake main holds all sends (data accrues losslessly in the capped pending backlog) so bytes cannot be dropped into a listener-less page and re-pin the gate. The hold itself cannot become a permanent freeze: a one-shot ~10s watchdog armed on each reset force-opens the gate (incrementing rendererDispatcherReadyForcedCount) if the handshake is lost, and the real handshake or a re-registration cancels it. The reset fires only for a new-document main-frame navigation: did-start-navigation carries exact frame and same-document details, so overlapping subframe or in-page navigation never clears accounting or holds the gate on the still-alive page. If a renderer lifecycle edge is otherwise missed, a backstop still recovers: because the handshake is one-shot per page load, receiving pty:rendererDispatcherReady while the gate is already open proves a reset was missed (or the watchdog force-opened the gate), so the handler reconciles by clearing the stale accounting before re-opening. The renderer sends that handshake exactly once per page load, after its pty:data listener registers.", - "oracle": "Ingest more than 512 KB of PTY output with no renderer ACKs and assert the per-PTY gate closes (sends stop at the 512 KB high-water, remainder accrues as pending). Fire the registered main-frame did-start-navigation listener and assert rendererInFlightChars and pendingChars are zero and the new diagnostics record the reset (rendererLifecycleResetCount 1, lastLifecycleResetClearedChars 512 KB). Then, before any dispatcher-ready handshake, ingest another chunk and assert it is NOT sent and NOT counted in-flight (held for the boot window, accruing in pending). Finally fire the pty:rendererDispatcherReady handshake and assert the held chunk is delivered to the renderer. Counters-zero without proving both the boot-window hold and that delivery resumes is insufficient. Additional cases prove the boot-window hold also covers the interactive direct-send fast path (input-primed keystroke echo is held, not sent, until the handshake) and that the self-heal watchdog force-opens the gate (rendererDispatcherReadyForcedCount 1) when no handshake arrives, while a timely handshake cancels the watchdog and leaves no orphaned timer. A further case opens the new page with a main-frame navigation and dispatcher handshake, then fires an overlapping subframe navigation and asserts the gate stays ready, the reset count stays at exactly one, and fresh output delivers without the watchdog — proving an iframe cannot reclose the live page. A backstop case saturates the gate, then fires pty:rendererDispatcherReady while the gate is still open (ready true) with no preceding reset — modeling a missed lifecycle edge — and asserts the handler reconciles: in-flight and pending clear, rendererLifecycleResetCount increments, and fresh output flows immediately (a straggler ACK is clamped and cannot underflow). A renderer-side case (pty-dispatcher-pi-routing.test.ts) asserts ensurePtyDispatcher() sends pty:rendererDispatcherReady exactly once across two attach calls — proving the send fires (it is optional-chained) and the one-shot guard holds.", + "oracle": "Ingest more than 512 KB of PTY output with no renderer ACKs and assert the per-PTY gate closes (sends stop at the 512 KB high-water, remainder accrues as pending). Fire the registered main-frame did-start-navigation listener and assert rendererInFlightChars and pendingChars are zero and the new diagnostics record the reset (rendererLifecycleResetCount 1, lastLifecycleResetClearedChars 512 KB). Then, before any dispatcher-ready handshake, ingest another chunk and assert it is NOT sent and NOT counted in-flight (held for the boot window, accruing in pending). Finally fire the pty:rendererDispatcherReady handshake and assert the held chunk is delivered to the renderer. Counters-zero without proving both the boot-window hold and that delivery resumes is insufficient. Additional cases prove the boot-window hold also covers the interactive direct-send fast path (input-primed keystroke echo is held, not sent, until the handshake) and that the self-heal watchdog force-opens the gate (rendererDispatcherReadyForcedCount 1) when no handshake arrives, while a timely handshake cancels the watchdog and leaves no orphaned timer. A further case opens the new page with a main-frame navigation and dispatcher handshake, then fires an overlapping subframe navigation and asserts the gate stays ready, the reset count stays at exactly one, and fresh output delivers without the watchdog — proving an iframe cannot reclose the live page. A backstop case saturates the gate, then fires pty:rendererDispatcherReady while the gate is still open (ready true) with no preceding reset — modeling a missed lifecycle edge — and asserts the handler reconciles: in-flight and pending clear, rendererLifecycleResetCount increments, and fresh output flows immediately (a straggler ACK is clamped and cannot underflow).", "commands": [], "testFiles": [], "assertionRefs": [], @@ -12716,6 +12676,7 @@ "Add an Electron-level reload survival test that proves a saturated pane resumes rendering and input after Cmd+R / crash recovery." ], "knownGaps": [ + "Audit3077 retires the renderer once-per-page dispatcher-ready request example. It called ensurePtyDispatcher twice, without proving two attach calls or handshake ordering. Main-side lifecycle/hold/watchdog evidence and the desired invariant retain their original scope.", "Audit1634 removes low-value IPC, filesystem and runtime projections, argument inventories and duplicate examples without equivalent replacement coverage. Per-source reviews record retained cancellation, ownership, authorization, filesystem and SQLite boundaries with injected/native/remote limits. Affected historical receipts are retired; gates without active current evidence are downgraded.", "The unit oracle drives a local mock PTY; daemon and WSL coverage rides the same pipeline but has no dedicated provider-contract test yet.", "The dev repro covers the output-delivery freeze only; the arrow-key input symptom and mouse-mode flood observed in the same incident are out of scope and need their own repros.", @@ -12794,14 +12755,14 @@ "providers": ["local", "remote-runtime", "ssh"], "coveredPlatforms": ["macos", "linux"], "coveredProviders": ["local", "remote-runtime", "ssh"], - "coverageNotes": "Audit2432: The broad unit command loses the isolated BranchPicker open-change notification sample. Its unique true/false callback alarm is intentionally retired; reopening/status-refresh behavior was not exercised. No equivalent replacement or fresh validation of the reduced selector is claimed. Audit2425: The broad unit command loses the workspace-copy-status supplied readError forwarding sample. It did not exercise the branch-switch blocking promised by its title. This alarm is intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2417: The broad unit command loses two ordinary clean Git cherry-pick/revert success samples and two terminal startup foreground-read work-budget samples. These unique alarms are intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2412: The broad unit command loses the whole six-case fake-React-hook scroll-anchor suite, two generic draft-retention samples, a simple new-agent order sample, a real history-publication batching-count sample, an unmounted handoff abort/timer sample and three basic WSL preview/error samples. These alarms are intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2404: The broad unit command loses the standalone share-name lookup 503 propagation suite, initial deletion placement-kind sample, direct changed-mtime stale sample, first-pass rollback existence example and abandoned-preparation directory sample. These exact alarms are intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2397 review amendment: The broad unit command loses three standalone suites, two partial-case definitions and two URL table rows across package/download/placement tests. Keep initial wrong-port authorization, modified independent-copy digest refusal and abort-after-begin cancellation dispatch. The remaining alarms are deliberately retired; no equivalent complete replacement or fresh validation of the reduced selector is claimed. Audit2393 current-parent refresh2401: Remove the basic skill-install service alias sample and original public managed-remove receiptless refusal case. Preserve the upstream request ownership suite and transaction interruption coverage. No equivalent complete replacement or fresh validation of the reduced selector is claimed. Audit2389: The broad unit command loses the missing-preview suite, plain-global destination sample and prepublished-receipt recovery example. No equivalent replacement or fresh validation of the reduced selector is claimed. Audit2385: The broad unit command loses four authored walker/staging/protocol examples (six expanded cases); no equivalent replacement or fresh validation is claimed for the reduced suite. Audit2383: The unscoped Vitest command no longer includes the removed Antigravity catalogue suite; its exact provider-root/owner example has no equivalent replacement or fresh validation claim. Audit2377/2381: The unscoped Vitest command includes the reduced bundled-skill and native-discovery suites; the eight removed backup/legacy/catalogue/symlink/SSH-builder examples have no replacement-equivalence or fresh validation claim here. Audit2133 current-parent preparation: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2095 current-parent preparation2126: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2090 (sources2088/2089): Current reduced suites intentionally retire the alarms listed in knownGaps; prior dated execution and replacement claims do not validate changed source bytes or selectors. Historical prior account (preserved verbatim): Audit2086 (sources2082/2083): Current reduced suites intentionally retire the alarms listed in knownGaps; prior dated execution and replacement claims do not validate changed source bytes or selectors. Historical prior account (preserved verbatim): Audit2076 (sources2073/2074): Intentionally retire these low-value alarms without replacement: src/main/runtime/orca-runtime-tests/terminal-handles-part-02.spec.ts: recovers a moved SSH pane through the tab it now sits in, not the one its lease froze; reports a plain expired orphan; does not report a pane whose only recent lease was superseded; does not report a pane whose only recent lease had its relay id recycled; picks the eligible orphan over a superseded predecessor that matches first; collects the same tabs the per-tab reader reports, in one sweep of the leases; reports no candidate when no lease names any of the worktree tabs. src/main/runtime/orca-runtime-tests/runtime-availability.spec.ts: starts unavailable with no authoritative window; reports the configured Windows terminal shell on status; reports floating workspace availability from settings on status; polls floating tabs with targeted PTY liveness and no repo/provider inventory; hydrates persisted tabs when the store cannot report repos; advertises browser screencast only when a renderer window is available; routes mobile Codex reset consumption through the account mutation coordinator; maps a definite pre-provider rejection into an authoritative current snapshot. Existing unrelated evidenceRuns retain their original scope; unchanged dated receipt objects do not validate changed source bytes or reduced selectors. No fresh gate execution or historical import-closure equivalence is claimed. Frozen current literal-import graph reaches these changed test bytes; unresolved local and computed edges remain debt, and current reachability is not historical execution or equivalent replacement evidence. Historical prior account (preserved verbatim): Audit2056 (sources2054/2055): Intentionally retire these low-value alarms without replacement: src/renderer/src/runtime/files/watching/runtime-file-client-watch.test.ts: delivers remote file changes to listeners for the same worktree. src/main/runtime/orca-runtime.test.ts: ordinary registration cleanup removes only await import('./orca-runtime-tests/mobile-session-tabs-part-14.spec'). src/main/runtime/orca-runtime-tests/terminal-side-effect-facts-part-03.spec.ts: arms the stale-title timer for a seeded working title; arms the Command Code scrape from the noted spawn command; prefers the tracked title over a stale renderer lastTitle in the hydration seed; writes recent output into the current runtime output map; uses a later title handler replacement with the runtime receiver; calls the current injected listener with the runtime receiver; keeps optional reconciliation lazy after its callback is removed. src/main/runtime/orca-runtime-tests/mobile-session-tabs-part-14.spec.ts: lists them, so a paired client can mirror a migrated editor; closes one by retiring it from the host session; keeps an unsaved draft unless the close is forced; shares aggregate work but leaves a replacement pending promise alone; lets a targeted refresh finish while an aggregate refresh waits; refuses a close when the store lacks %s before the guard; keeps a later missing %s on the original caught failure path. Existing unrelated evidenceRuns retain their original scope; unchanged dated receipt objects do not validate changed source bytes or reduced selectors. No fresh gate execution or historical import-closure equivalence is claimed. Exact retired assertionRefs, preserved as historical objects only: {\n \"file\": \"src/renderer/src/runtime/files/watching/runtime-file-client-watch.test.ts\",\n \"assertions\": [\n \"terminal watcher error and setup failure evict the shared entry before retrying callbacks run\",\n \"an old stream end cannot evict the replacement stream, which still delivers to its listener\",\n \"two same-worktree listeners share a synthetic subscription until the final stop unsubscribes and sends unwatch\"\n ]\n } Historical prior account (preserved verbatim): Audit1973: Delete the standalone mocked watcher cleanup suite and its entire sole-file prospective command. Retire the removed file-command transport-abort alias; preserve the independent cancellation suite and final-overflow/error/end cleanup callback, and13 unrelated receipt objects. Whole original changed dated receipts are retired, never narrowed or transferred. This metadata review claims no fresh execution, equivalent replacement, provider/PTY, live remote/mobile, rendered, cross-platform, soak or measured-budget result. Historical prior account (preserved verbatim): Audit1931 (sources1928/1929): Partial runtime files-watch suite retains controlled Windows close-event waiting, shared-client survival, late physical-exit ownership/deadline cleanup and shutdown joining. Its combined Windows close/deadline/late-exit alias remains backed by unchanged callbacks. Retire native-error-close, pre-publication setup ownership and failed-delete rearm aliases; associated delegation, failure forwarding, removal routing and recovery examples are lost. These tests inject watch/subscription seams and do not observe an actual native watcher or physical process. Preserve meaningful remaining selectors/assertions and unchanged dated receipts within their original scope. Reduced selectors have no fresh gate execution evidence in this metadata packet. No equivalent replacement, physical watcher/PTY/provider, live SSH/remote/mobile, rendered, cross-platform, soak or measured-budget result is claimed. Historical prior account (preserved verbatim): Audit1925 (sources1922/1923): Partial runtime watcher-host suite loses mocked option/event/overflow/resubscription forwarding, supplied teardown-error propagation, aborted-signal/no-call and private ten-thousand-waiter-count inventories. Retire the 10,000-caller bound and failed-recovery-subscriber-ending assertion aliases. Keep same-root owner cancellation, pre-ready replacement, held root-close join, stale-generation recovery delivery and initial failed-root ownership mappings backed by retained real-host callbacks with injected subscription/error/physical-exit promises. No actual native watcher, process termination or resource bound was observed. Preserve meaningful remaining selectors/assertions and unchanged dated receipts within their original scope. Reduced selectors have no fresh gate execution evidence in this metadata packet. No equivalent replacement, physical watcher/PTY/provider, live SSH/remote/mobile, rendered, cross-platform, soak or measured-budget result is claimed. Historical prior account (preserved verbatim): Audit1683 removes low-value test projections, mock wiring, private-state assertions, inventories and duplicate examples without equivalent replacement coverage. Per-source reviews record retained public behavior and refusal boundaries with injected/native/remote limits. Affected historical receipts are retired; gates without active current evidence are downgraded. Historical scope before this audit: Audit1645 removes low-value test projections, mock wiring, private-state assertions, inventories and duplicate examples without equivalent replacement coverage. Per-source reviews record retained public behavior and refusal boundaries with injected/native/remote limits. Affected historical receipts are retired; gates without active current evidence are downgraded. Historical scope before this audit: Audit1640 removes low-value IPC, SSH, workspace and transcript-watcher tests without equivalent replacement coverage. Per-source reviews record retained public refusal, cancellation, execution-host and real-filesystem outcomes with injected/native/remote limits. Affected historical receipts are retired; gates without active current evidence are downgraded. Historical scope before this audit: Audit1634 removes low-value IPC, filesystem and runtime projections, argument inventories and duplicate examples without equivalent replacement coverage. Per-source reviews record retained cancellation, ownership, authorization, filesystem and SQLite boundaries with injected/native/remote limits. Affected historical receipts are retired; gates without active current evidence are downgraded. Historical scope before this audit: Audit1627 removes low-value mocked daemon projections, scalar tables and duplicate terminal examples without equivalent replacement coverage. Per-source reviews record retained terminal content preservation, process ownership refusal and recovery boundaries together with injected/native/remote limits. Affected historical receipts are retired; gates without active current evidence are downgraded. Historical scope before this audit: Audit1623 removes low-value mocked daemon projections, scalar tables and duplicate terminal examples without equivalent replacement coverage. Per-source reviews record retained terminal content preservation, process ownership refusal and recovery boundaries together with injected/native/remote limits. Affected historical receipts are retired; gates without active current evidence are downgraded. Historical scope before this audit: Audit1516 removes package-electron-runtime-contract.test.mjs, a copied release-workflow platform and marker matrix. Its workflow wiring alarm is intentionally retired; no actual packaged watcher delivery was exercised and no equivalent replacement is claimed. Unaffected dated execution receipts retain their original scope; changed optional selectors have no fresh full-gate execution result. This audit removes the paired-web stopped-during-outage and late-ready direct-branch examples. The retained pre-ready cancellation, failed-unwatch retry and replay-continuity cases are not equivalent replacements for those ordering alarms. Current web-runtime-client.test.ts assertions below name only its retained pre-ready callback case; replay cases remain in their separate suite. Historical commands and results remain historical. Deterministic tests cover one shared healthy child per runtime process plus at most four bounded fault-quarantine children, native desktop, paired-runtime, and WSL snapshot processes sharing the same eight-physical-child reservation with typed serialized event-driven capacity recovery, including recursive re-wait when crash recovery reclaims an announced slot, one quarantine attempt per watch lifetime, generation-scoped cancellation, bounded termination with removable deadline waiters, replacement-crawl errors that cannot be blessed by a late readiness ack, pre-ready paired-web cancellation registered before an unbounded capacity wait, already-resolved and late physical-exit retry of a rejected paired-web teardown, physical desktop-install and local/SSH PTY teardown, stale-generation rejection, per-child stat bounds, final-overflow RPC delivery, and renderer eviction before terminal callbacks can retry. Destructive local and SSH removal fences both ID-derived and resolved-cwd terminal roots, including sibling-root/cwd combinations with reverse admission rollback, closes descendant relay watches before parent deletion, enumerates authoritative provider/cwd ownership, falls back to daemon spawn cwd before OSC 7, and keeps daemon/relay immediate shutdown pending until native exit or a bounded fail-closed error; a dead relay PID discovered during attach also settles concurrent shutdown before its stale entry is reaped, while Windows runtime watcher deadlines retain one removable physical-close waiter, treat Node's error-close path as positive physical-exit proof, and clear root ownership on late close. The relay policy test maps a standard repository's base, Git common directory, and worktree roots to one healthy supervisor, then proves a shared-child failure recovers those roots in separate quarantine supervisors with overflow and resumed events. Abortable capacity, quarantine, runtime-root, relay, SSH, and relay pre-install setup waits attach one reaction to each shared promise and explicitly remove 10,000 cancelled caller closures while one anchor remains. Built-entry macOS harnesses kill the desktop/runtime and SSH relay watcher children, then require automatic resubscription and later events while the host process and relay PTY survive. SSH bundles the child boundary beside relay.js, requires both artifacts for install completeness, and preserves registration-owned same-root cancellation. The test-value audit corrects the active renderer watch command/file reference to the existing runtime-file-client-watch.test.ts, whose three cases are retained unchanged. They observe synthetic subscription retry/fanout and unsubscribe/unwatch calls, not actual preload/main bookkeeping or physical watcher-process cleanup. The separate read/preview client suite loses forwarding, supplied-error and negative-only examples, retaining binary detection and Unicode SVG decoding. Historical evidence commands/results are unchanged. Test-value audit1293 removes src/main/providers/local-pty/local-pty-provider-session-inventory.test.ts (2 authored cases). Their isolated assertions no longer run; surviving tests are not claimed as equivalent coverage. Fresh execution against the candidate SHA is required. Audit1312 removes low-value imported specs from aggregate test drivers; old whole-driver execution receipts are archived. Retained registrations are unchanged. No equivalent replacement coverage is claimed. Audit1383: src/main/runtime/orca-runtime-tests/worktree-native-chat-setup.spec.ts reduced. Lose command-list/receipt/default/background activation matrices whose provider spawn returns canned IDs. Retained test proves actual tab allocation separation, not terminal startup. No equivalent replacement coverage is claimed. Existing exact unaffected receipts and their platform/provider/budget claims remain scoped to unchanged selectors. Changed optional selectors have no new execution result. Audit1401: src/main/runtime/orca-runtime-tests/mobile-session-tabs-duplicate-pty-repair.spec.ts removed. Loses listing-on-already-repaired-data alarm; it never established runtime repair invocation. src/main/runtime/orca-runtime-tests/mobile-session-tabs-split-agent-tab.spec.ts removed. Loses actual mobile split sibling shell versus recorded-agent resume command alarms; no equivalent coverage asserted. src/main/runtime/orca-runtime-tests/worktree-native-chat-setup.spec.ts removed. Loses separate setup/startup tab result in that manually assembled path. src/main/runtime/orca-runtime.test.ts reduced. Driver registration/import of a whole-deleted selected spec is removed; directly authored cases remain unchanged. No extra authored removal credit. No equivalent replacement coverage is claimed. Existing exact unaffected receipts remain unchanged and apply only to their unchanged selectors. Changed optional selectors have no new execution result. Audit1412: src/main/workspaces/removal/watcher-removal-gate.test.ts reduced. Loses exact removal-error compatibility with renderer predicate under shared message constants. Kept public refusal outcomes do not establish that renderer classification. No equivalent replacement coverage is claimed. Existing exact unaffected receipts remain unchanged and apply only to their unchanged selectors. Changed optional selectors have no new execution result.", + "coverageNotes": "Audit3128 scoped coverage reduction: Audit3121 removes VoiceDictationLanguageSetting.test.tsx (Spanish-to-es callback and unsupported-model Korean option presence) and voice-speech-model-rows.test.ts (provider/language-order/retired-on-disk list policy); these real narrow alarms are deliberately retired without replacement. VoiceSpeechModelSection per-model concurrent deletion remains unchanged. Audit3122 removes the direct absolute/parent-segment sparse-directory exception-type sample and the five-row IPC sparse-preset error-code/non-save table (two authored/six expanded cases). Those real typed-validation and IPC refusal/error-code alarms are deliberately retired. Keep both real symlink/junction floating-directory trust cases and the minimal RPC structured validation-reason serialization case. The RPC case does not replace IPC save/refusal behavior, typed normalization for both path inputs, actual Git sparse checkout or rendered localization. No equivalent replacement or fresh test/runtime/platform/remote/performance validation is claimed. Audit3124 removes ExperimentalPane.test.tsx (ordinary Pet switch callback), WorkspaceDirectorySetting.test.tsx (padded absolute saved-path normalization/no-write blur), and repository-search.perf.test.ts (navigation/selected-pane builder-call policy, fixture cardinality/nonempty entries and renamed project keyword inclusion). These real narrow alarms are retired without replacement; RepositoryPane.search.test.tsx search-to-sparse-editor request remains unchanged. Audit3129: resolver Windows case-insensitive and POSIX case-sensitive configured-env composition plus mocked-base launch-preflight Windows composition are intentionally no longer checked by the two deleted suites. No equivalent coverage claimed. Keep runtime-structured-agent-host-env.test.ts unchanged: configured Windows Claude-home account identity and host-account precedence remain checked. Audit3114 incorporates source3101: retire three structured Claude child-environment wrapper cases covering inherited stamp isolation/default and explicit entrypoint, Windows SDK-key canonicalization/default/duplicate-key order, and configured lookup/final environment/config-dir conjunction. Keep direct auth/session sanitation and Windows-versus-Linux overlay checks, actual executable lookup from supplied inherited PATH, native Windows child environment delivery, and stale option-mutation fencing. These are not equivalent replacements for the retired wrapper alarms. No fresh test/runtime, real login-shell probe, Claude SDK startup, remote/mixed-version, UI or performance result is claimed. Audit3114 incorporates source3105: retire 17 authored/20 expanded daemon launch-note cases, including the entire two-case SessionLaunchNote suite. Deliberately lose split-marker carry across same-token re-arm, stale-token retirement refusal, foreground tri-state and serial settlement, omitted-note/no-incarnation mixed-version policy, sticky retirement, first-incarnation/replacement launch preservation and held-launch precedence. Keep only the unchanged unresolved foreground-read race against a later finished attach. Supplied notes/foreground and runtime calls do not establish actual daemon/main restart, SSH, native Windows observation, mobile UI, equivalent replacement or fresh runtime/performance validation. Audit3114 incorporates source3109: retire six authored/35 expanded pricing cases: 21 accepted model-name examples, 10 unsupported/future-model fail-closed examples, malformed TTL clamp, unknown-model estimate refusal, flat-model long-context disregard and oversized long-context clamp. Keep mixed five-minute/one-hour write allocation and short/all-long/mixed long-context allocation with original fixed oracles. Real parser/refusal/bounds alarms are knowingly lost without equivalent replacement; retained tests do not validate provider rates, actual billing or request threshold classification. Preserve the archived usage-scan-append source-freeze receipt without transferring historical execution/performance credit. No fresh runtime result is claimed. Audit3108 source-only reduction (3094/3095/3100): retire 27 authored/30 expanded cases, including complete markdown view-state, file-preview link-open and CLI agent-server-route suites, without replacement. Deliberate losses include preview visibility/reference lifetime and same-path owners; SSH stat, directory/missing-target, explicit-null owner, nonmarkdown line/column, local URI routing, remote-to-client guard and same-file reuse; document-preview source-owner/folder/reuse routing, stable grant location and outside-workspace POSIX/Windows request projection; and verified remote CLI send/check refusal, suggested --on argv/copy, shell no-probe bypass, unverified-launch admission and older-peer refusal. Retain two markdown source-runtime/qualified-reveal alarms, one stale grant-rejection fence and two remote read/mutation evidence policies, with original callbacks. Final last-wins reveal did not prove transient cancellation; CLI prerequisite did not prove no real send, and envelope tests do not execute server refusal. No fresh runtime, rendered UI, actual file/SSH/paired transport, authorization, mixed-version or performance result is claimed. Audit3096 source-only reduction (3087/3088): retire 69 authored/76 expanded cases, including complete recovery-portals and reconnect-banner suites. Intentionally lose dead/default TUI replay, generated alternate-frame style composition, reset/mode/dimension and legacy missing-mode-boundary alarms, UI notice/action/reveal policies, and isolated recovery lifecycle variants, without equivalent replacement. Retained registrations and callbacks are unchanged; headless cells are not pixels, and no fresh runtime, host, SSH, WSL, rendered-app or performance result is claimed. Audit3086 source-only reduction (3079/3080/3081): intentionally retire 93 authored/100 expanded mock-boundary cases, including the complete spawn-errors and SSH reconnect overlay suites, without replacement. Retired alarms include spawn classification/ownership, reply ceilings and renderer-write failure recovery, disconnect/detach fencing branches, authority facts, and overlay retry/visibility wiring. Retained callbacks are unchanged; no fresh execution, real PTY/SSH, rendered UI, or performance result is claimed. Audit2432: The broad unit command loses the isolated BranchPicker open-change notification sample. Its unique true/false callback alarm is intentionally retired; reopening/status-refresh behavior was not exercised. No equivalent replacement or fresh validation of the reduced selector is claimed. Audit2425: The broad unit command loses the workspace-copy-status supplied readError forwarding sample. It did not exercise the branch-switch blocking promised by its title. This alarm is intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2417: The broad unit command loses two ordinary clean Git cherry-pick/revert success samples and two terminal startup foreground-read work-budget samples. These unique alarms are intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2412: The broad unit command loses the whole six-case fake-React-hook scroll-anchor suite, two generic draft-retention samples, a simple new-agent order sample, a real history-publication batching-count sample, an unmounted handoff abort/timer sample and three basic WSL preview/error samples. These alarms are intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2404: The broad unit command loses the standalone share-name lookup 503 propagation suite, initial deletion placement-kind sample, direct changed-mtime stale sample, first-pass rollback existence example and abandoned-preparation directory sample. These exact alarms are intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2397 review amendment: The broad unit command loses three standalone suites, two partial-case definitions and two URL table rows across package/download/placement tests. Keep initial wrong-port authorization, modified independent-copy digest refusal and abort-after-begin cancellation dispatch. The remaining alarms are deliberately retired; no equivalent complete replacement or fresh validation of the reduced selector is claimed. Audit2393 current-parent refresh2401: Remove the basic skill-install service alias sample and original public managed-remove receiptless refusal case. Preserve the upstream request ownership suite and transaction interruption coverage. No equivalent complete replacement or fresh validation of the reduced selector is claimed. Audit2389: The broad unit command loses the missing-preview suite, plain-global destination sample and prepublished-receipt recovery example. No equivalent replacement or fresh validation of the reduced selector is claimed. Audit2385: The broad unit command loses four authored walker/staging/protocol examples (six expanded cases); no equivalent replacement or fresh validation is claimed for the reduced suite. Audit2383: The unscoped Vitest command no longer includes the removed Antigravity catalogue suite; its exact provider-root/owner example has no equivalent replacement or fresh validation claim. Audit2377/2381: The unscoped Vitest command includes the reduced bundled-skill and native-discovery suites; the eight removed backup/legacy/catalogue/symlink/SSH-builder examples have no replacement-equivalence or fresh validation claim here. Audit2133 current-parent preparation: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2095 current-parent preparation2126: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2090 (sources2088/2089): Current reduced suites intentionally retire the alarms listed in knownGaps; prior dated execution and replacement claims do not validate changed source bytes or selectors. Historical prior account (preserved verbatim): Audit2086 (sources2082/2083): Current reduced suites intentionally retire the alarms listed in knownGaps; prior dated execution and replacement claims do not validate changed source bytes or selectors. Historical prior account (preserved verbatim): Audit2076 (sources2073/2074): Intentionally retire these low-value alarms without replacement: src/main/runtime/orca-runtime-tests/terminal-handles-part-02.spec.ts: recovers a moved SSH pane through the tab it now sits in, not the one its lease froze; reports a plain expired orphan; does not report a pane whose only recent lease was superseded; does not report a pane whose only recent lease had its relay id recycled; picks the eligible orphan over a superseded predecessor that matches first; collects the same tabs the per-tab reader reports, in one sweep of the leases; reports no candidate when no lease names any of the worktree tabs. src/main/runtime/orca-runtime-tests/runtime-availability.spec.ts: starts unavailable with no authoritative window; reports the configured Windows terminal shell on status; reports floating workspace availability from settings on status; polls floating tabs with targeted PTY liveness and no repo/provider inventory; hydrates persisted tabs when the store cannot report repos; advertises browser screencast only when a renderer window is available; routes mobile Codex reset consumption through the account mutation coordinator; maps a definite pre-provider rejection into an authoritative current snapshot. Existing unrelated evidenceRuns retain their original scope; unchanged dated receipt objects do not validate changed source bytes or reduced selectors. No fresh gate execution or historical import-closure equivalence is claimed. Frozen current literal-import graph reaches these changed test bytes; unresolved local and computed edges remain debt, and current reachability is not historical execution or equivalent replacement evidence. Historical prior account (preserved verbatim): Audit2056 (sources2054/2055): Intentionally retire these low-value alarms without replacement: src/renderer/src/runtime/files/watching/runtime-file-client-watch.test.ts: delivers remote file changes to listeners for the same worktree. src/main/runtime/orca-runtime.test.ts: ordinary registration cleanup removes only await import('./orca-runtime-tests/mobile-session-tabs-part-14.spec'). src/main/runtime/orca-runtime-tests/terminal-side-effect-facts-part-03.spec.ts: arms the stale-title timer for a seeded working title; arms the Command Code scrape from the noted spawn command; prefers the tracked title over a stale renderer lastTitle in the hydration seed; writes recent output into the current runtime output map; uses a later title handler replacement with the runtime receiver; calls the current injected listener with the runtime receiver; keeps optional reconciliation lazy after its callback is removed. src/main/runtime/orca-runtime-tests/mobile-session-tabs-part-14.spec.ts: lists them, so a paired client can mirror a migrated editor; closes one by retiring it from the host session; keeps an unsaved draft unless the close is forced; shares aggregate work but leaves a replacement pending promise alone; lets a targeted refresh finish while an aggregate refresh waits; refuses a close when the store lacks %s before the guard; keeps a later missing %s on the original caught failure path. Existing unrelated evidenceRuns retain their original scope; unchanged dated receipt objects do not validate changed source bytes or reduced selectors. No fresh gate execution or historical import-closure equivalence is claimed. Exact retired assertionRefs, preserved as historical objects only: {\n \"file\": \"src/renderer/src/runtime/files/watching/runtime-file-client-watch.test.ts\",\n \"assertions\": [\n \"terminal watcher error and setup failure evict the shared entry before retrying callbacks run\",\n \"an old stream end cannot evict the replacement stream, which still delivers to its listener\",\n \"two same-worktree listeners share a synthetic subscription until the final stop unsubscribes and sends unwatch\"\n ]\n } Historical prior account (preserved verbatim): Audit1973: Delete the standalone mocked watcher cleanup suite and its entire sole-file prospective command. Retire the removed file-command transport-abort alias; preserve the independent cancellation suite and final-overflow/error/end cleanup callback, and13 unrelated receipt objects. Whole original changed dated receipts are retired, never narrowed or transferred. This metadata review claims no fresh execution, equivalent replacement, provider/PTY, live remote/mobile, rendered, cross-platform, soak or measured-budget result. Historical prior account (preserved verbatim): Audit1931 (sources1928/1929): Partial runtime files-watch suite retains controlled Windows close-event waiting, shared-client survival, late physical-exit ownership/deadline cleanup and shutdown joining. Its combined Windows close/deadline/late-exit alias remains backed by unchanged callbacks. Retire native-error-close, pre-publication setup ownership and failed-delete rearm aliases; associated delegation, failure forwarding, removal routing and recovery examples are lost. These tests inject watch/subscription seams and do not observe an actual native watcher or physical process. Preserve meaningful remaining selectors/assertions and unchanged dated receipts within their original scope. Reduced selectors have no fresh gate execution evidence in this metadata packet. No equivalent replacement, physical watcher/PTY/provider, live SSH/remote/mobile, rendered, cross-platform, soak or measured-budget result is claimed. Historical prior account (preserved verbatim): Audit1925 (sources1922/1923): Partial runtime watcher-host suite loses mocked option/event/overflow/resubscription forwarding, supplied teardown-error propagation, aborted-signal/no-call and private ten-thousand-waiter-count inventories. Retire the 10,000-caller bound and failed-recovery-subscriber-ending assertion aliases. Keep same-root owner cancellation, pre-ready replacement, held root-close join, stale-generation recovery delivery and initial failed-root ownership mappings backed by retained real-host callbacks with injected subscription/error/physical-exit promises. No actual native watcher, process termination or resource bound was observed. Preserve meaningful remaining selectors/assertions and unchanged dated receipts within their original scope. Reduced selectors have no fresh gate execution evidence in this metadata packet. No equivalent replacement, physical watcher/PTY/provider, live SSH/remote/mobile, rendered, cross-platform, soak or measured-budget result is claimed. Historical prior account (preserved verbatim): Audit1683 removes low-value test projections, mock wiring, private-state assertions, inventories and duplicate examples without equivalent replacement coverage. Per-source reviews record retained public behavior and refusal boundaries with injected/native/remote limits. Affected historical receipts are retired; gates without active current evidence are downgraded. Historical scope before this audit: Audit1645 removes low-value test projections, mock wiring, private-state assertions, inventories and duplicate examples without equivalent replacement coverage. Per-source reviews record retained public behavior and refusal boundaries with injected/native/remote limits. Affected historical receipts are retired; gates without active current evidence are downgraded. Historical scope before this audit: Audit1640 removes low-value IPC, SSH, workspace and transcript-watcher tests without equivalent replacement coverage. Per-source reviews record retained public refusal, cancellation, execution-host and real-filesystem outcomes with injected/native/remote limits. Affected historical receipts are retired; gates without active current evidence are downgraded. Historical scope before this audit: Audit1634 removes low-value IPC, filesystem and runtime projections, argument inventories and duplicate examples without equivalent replacement coverage. Per-source reviews record retained cancellation, ownership, authorization, filesystem and SQLite boundaries with injected/native/remote limits. Affected historical receipts are retired; gates without active current evidence are downgraded. Historical scope before this audit: Audit1627 removes low-value mocked daemon projections, scalar tables and duplicate terminal examples without equivalent replacement coverage. Per-source reviews record retained terminal content preservation, process ownership refusal and recovery boundaries together with injected/native/remote limits. Affected historical receipts are retired; gates without active current evidence are downgraded. Historical scope before this audit: Audit1623 removes low-value mocked daemon projections, scalar tables and duplicate terminal examples without equivalent replacement coverage. Per-source reviews record retained terminal content preservation, process ownership refusal and recovery boundaries together with injected/native/remote limits. Affected historical receipts are retired; gates without active current evidence are downgraded. Historical scope before this audit: Audit1516 removes package-electron-runtime-contract.test.mjs, a copied release-workflow platform and marker matrix. Its workflow wiring alarm is intentionally retired; no actual packaged watcher delivery was exercised and no equivalent replacement is claimed. Unaffected dated execution receipts retain their original scope; changed optional selectors have no fresh full-gate execution result. This audit removes the paired-web stopped-during-outage and late-ready direct-branch examples. The retained pre-ready cancellation, failed-unwatch retry and replay-continuity cases are not equivalent replacements for those ordering alarms. Current web-runtime-client.test.ts assertions below name only its retained pre-ready callback case; replay cases remain in their separate suite. Historical commands and results remain historical. Deterministic tests cover one shared healthy child per runtime process plus at most four bounded fault-quarantine children, native desktop, paired-runtime, and WSL snapshot processes sharing the same eight-physical-child reservation with typed serialized event-driven capacity recovery, including recursive re-wait when crash recovery reclaims an announced slot, one quarantine attempt per watch lifetime, generation-scoped cancellation, bounded termination with removable deadline waiters, replacement-crawl errors that cannot be blessed by a late readiness ack, pre-ready paired-web cancellation registered before an unbounded capacity wait, already-resolved and late physical-exit retry of a rejected paired-web teardown, physical desktop-install and local/SSH PTY teardown, stale-generation rejection, per-child stat bounds, final-overflow RPC delivery, and renderer eviction before terminal callbacks can retry. Destructive local and SSH removal fences both ID-derived and resolved-cwd terminal roots, including sibling-root/cwd combinations with reverse admission rollback, closes descendant relay watches before parent deletion, enumerates authoritative provider/cwd ownership, falls back to daemon spawn cwd before OSC 7, and keeps daemon/relay immediate shutdown pending until native exit or a bounded fail-closed error; a dead relay PID discovered during attach also settles concurrent shutdown before its stale entry is reaped, while Windows runtime watcher deadlines retain one removable physical-close waiter, treat Node's error-close path as positive physical-exit proof, and clear root ownership on late close. The relay policy test maps a standard repository's base, Git common directory, and worktree roots to one healthy supervisor, then proves a shared-child failure recovers those roots in separate quarantine supervisors with overflow and resumed events. Abortable capacity, quarantine, runtime-root, relay, SSH, and relay pre-install setup waits attach one reaction to each shared promise and explicitly remove 10,000 cancelled caller closures while one anchor remains. Built-entry macOS harnesses kill the desktop/runtime and SSH relay watcher children, then require automatic resubscription and later events while the host process and relay PTY survive. SSH bundles the child boundary beside relay.js, requires both artifacts for install completeness, and preserves registration-owned same-root cancellation. The test-value audit corrects the active renderer watch command/file reference to the existing runtime-file-client-watch.test.ts, whose three cases are retained unchanged. They observe synthetic subscription retry/fanout and unsubscribe/unwatch calls, not actual preload/main bookkeeping or physical watcher-process cleanup. The separate read/preview client suite loses forwarding, supplied-error and negative-only examples, retaining binary detection and Unicode SVG decoding. Historical evidence commands/results are unchanged. Test-value audit1293 removes src/main/providers/local-pty/local-pty-provider-session-inventory.test.ts (2 authored cases). Their isolated assertions no longer run; surviving tests are not claimed as equivalent coverage. Fresh execution against the candidate SHA is required. Audit1312 removes low-value imported specs from aggregate test drivers; old whole-driver execution receipts are archived. Retained registrations are unchanged. No equivalent replacement coverage is claimed. Audit1383: src/main/runtime/orca-runtime-tests/worktree-native-chat-setup.spec.ts reduced. Lose command-list/receipt/default/background activation matrices whose provider spawn returns canned IDs. Retained test proves actual tab allocation separation, not terminal startup. No equivalent replacement coverage is claimed. Existing exact unaffected receipts and their platform/provider/budget claims remain scoped to unchanged selectors. Changed optional selectors have no new execution result. Audit1401: src/main/runtime/orca-runtime-tests/mobile-session-tabs-duplicate-pty-repair.spec.ts removed. Loses listing-on-already-repaired-data alarm; it never established runtime repair invocation. src/main/runtime/orca-runtime-tests/mobile-session-tabs-split-agent-tab.spec.ts removed. Loses actual mobile split sibling shell versus recorded-agent resume command alarms; no equivalent coverage asserted. src/main/runtime/orca-runtime-tests/worktree-native-chat-setup.spec.ts removed. Loses separate setup/startup tab result in that manually assembled path. src/main/runtime/orca-runtime.test.ts reduced. Driver registration/import of a whole-deleted selected spec is removed; directly authored cases remain unchanged. No extra authored removal credit. No equivalent replacement coverage is claimed. Existing exact unaffected receipts remain unchanged and apply only to their unchanged selectors. Changed optional selectors have no new execution result. Audit1412: src/main/workspaces/removal/watcher-removal-gate.test.ts reduced. Loses exact removal-error compatibility with renderer predicate under shared message constants. Kept public refusal outcomes do not establish that renderer classification. No equivalent replacement coverage is claimed. Existing exact unaffected receipts remain unchanged and apply only to their unchanged selectors. Changed optional selectors have no new execution result.", "motivatingLinks": [ "https://github.com/stablyai/orca/issues/5308", "https://github.com/stablyai/orca/issues/7547", "https://github.com/stablyai/orca/issues/8212" ], "invariant": "Local runtime and SSH relay roots share one watcher child per host process while healthy and use at most four quarantine children after shard failure; native desktop, paired-runtime, and direct WSL snapshot children consume the same process-wide eight-child reservation and retry transient exhaustion without permanently disabling a root. Cancellation remains owned by its child generation and settles only after acknowledgement or bounded physical exit; paired-web retains cleanup ownership and can cancel before readiness even while capacity is exhausted, retries a failed unwatch after already-resolved or late physical exit without duplicating stream termination, replacements never overlap a still-retiring generation, and aborted or timed-out callers detach immediately even while queued behind same-root relay teardown. Destructive local and SSH cleanup holds one containment-aware watcher/terminal removal fence through descendant watcher and PTY physical exit plus parent Git/filesystem deletion; daemon and relay kill requests retain ownership until the actual subprocess exit callback or an equivalent positive dead-PID proof and propagate rejected signals or a bounded failure otherwise. A crash-fused shared shard moves affected roots into the bounded quarantine pool, a second fused quarantine ends those watch lifetimes, interrupted setup emits overflow after recovery, metadata work stays bounded, and the SSH relay, PTYs, Git, and other filesystem operations never load the watcher native addon or share its fault boundary.", - "oracle": "Audit2432: The broad unit command loses the isolated BranchPicker open-change notification sample. Its unique true/false callback alarm is intentionally retired; reopening/status-refresh behavior was not exercised. No equivalent replacement or fresh validation of the reduced selector is claimed. Audit2425: The broad unit command loses the workspace-copy-status supplied readError forwarding sample. It did not exercise the branch-switch blocking promised by its title. This alarm is intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2417: The broad unit command loses two ordinary clean Git cherry-pick/revert success samples and two terminal startup foreground-read work-budget samples. These unique alarms are intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2412: The broad unit command loses the whole six-case fake-React-hook scroll-anchor suite, two generic draft-retention samples, a simple new-agent order sample, a real history-publication batching-count sample, an unmounted handoff abort/timer sample and three basic WSL preview/error samples. These alarms are intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2404: The broad unit command loses the standalone share-name lookup 503 propagation suite, initial deletion placement-kind sample, direct changed-mtime stale sample, first-pass rollback existence example and abandoned-preparation directory sample. These exact alarms are intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2397 review amendment: The broad unit command loses three standalone suites, two partial-case definitions and two URL table rows across package/download/placement tests. Keep initial wrong-port authorization, modified independent-copy digest refusal and abort-after-begin cancellation dispatch. The remaining alarms are deliberately retired; no equivalent complete replacement or fresh validation of the reduced selector is claimed. Audit2393 current-parent refresh2401: Remove the basic skill-install service alias sample and original public managed-remove receiptless refusal case. Preserve the upstream request ownership suite and transaction interruption coverage. No equivalent complete replacement or fresh validation of the reduced selector is claimed. Audit2389: The broad unit command loses the missing-preview suite, plain-global destination sample and prepublished-receipt recovery example. No equivalent replacement or fresh validation of the reduced selector is claimed. Audit2385: The broad unit command loses four authored walker/staging/protocol examples (six expanded cases); no equivalent replacement or fresh validation is claimed for the reduced suite. Audit2383: The unscoped Vitest command no longer includes the removed Antigravity catalogue suite; its exact provider-root/owner example has no equivalent replacement or fresh validation claim. Audit2377/2381: The unscoped Vitest command includes the reduced bundled-skill and native-discovery suites; the eight removed backup/legacy/catalogue/symlink/SSH-builder examples have no replacement-equivalence or fresh validation claim here. Audit2133 current-parent preparation: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2095 current-parent preparation2126: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2090 (sources2088/2089): Current reduced suites intentionally retire the alarms listed in knownGaps; prior dated execution and replacement claims do not validate changed source bytes or selectors. Historical prior account (preserved verbatim): Audit2086 (sources2082/2083): Current reduced suites intentionally retire the alarms listed in knownGaps; prior dated execution and replacement claims do not validate changed source bytes or selectors. Historical prior account (preserved verbatim): Audit2076 (sources2073/2074): Intentionally retire these low-value alarms without replacement: src/main/runtime/orca-runtime-tests/terminal-handles-part-02.spec.ts: recovers a moved SSH pane through the tab it now sits in, not the one its lease froze; reports a plain expired orphan; does not report a pane whose only recent lease was superseded; does not report a pane whose only recent lease had its relay id recycled; picks the eligible orphan over a superseded predecessor that matches first; collects the same tabs the per-tab reader reports, in one sweep of the leases; reports no candidate when no lease names any of the worktree tabs. src/main/runtime/orca-runtime-tests/runtime-availability.spec.ts: starts unavailable with no authoritative window; reports the configured Windows terminal shell on status; reports floating workspace availability from settings on status; polls floating tabs with targeted PTY liveness and no repo/provider inventory; hydrates persisted tabs when the store cannot report repos; advertises browser screencast only when a renderer window is available; routes mobile Codex reset consumption through the account mutation coordinator; maps a definite pre-provider rejection into an authoritative current snapshot. Existing unrelated evidenceRuns retain their original scope; unchanged dated receipt objects do not validate changed source bytes or reduced selectors. No fresh gate execution or historical import-closure equivalence is claimed. Frozen current literal-import graph reaches these changed test bytes; unresolved local and computed edges remain debt, and current reachability is not historical execution or equivalent replacement evidence. Historical prior account (preserved verbatim): Audit2056 (sources2054/2055): Intentionally retire these low-value alarms without replacement: src/renderer/src/runtime/files/watching/runtime-file-client-watch.test.ts: delivers remote file changes to listeners for the same worktree. src/main/runtime/orca-runtime.test.ts: ordinary registration cleanup removes only await import('./orca-runtime-tests/mobile-session-tabs-part-14.spec'). src/main/runtime/orca-runtime-tests/terminal-side-effect-facts-part-03.spec.ts: arms the stale-title timer for a seeded working title; arms the Command Code scrape from the noted spawn command; prefers the tracked title over a stale renderer lastTitle in the hydration seed; writes recent output into the current runtime output map; uses a later title handler replacement with the runtime receiver; calls the current injected listener with the runtime receiver; keeps optional reconciliation lazy after its callback is removed. src/main/runtime/orca-runtime-tests/mobile-session-tabs-part-14.spec.ts: lists them, so a paired client can mirror a migrated editor; closes one by retiring it from the host session; keeps an unsaved draft unless the close is forced; shares aggregate work but leaves a replacement pending promise alone; lets a targeted refresh finish while an aggregate refresh waits; refuses a close when the store lacks %s before the guard; keeps a later missing %s on the original caught failure path. Existing unrelated evidenceRuns retain their original scope; unchanged dated receipt objects do not validate changed source bytes or reduced selectors. No fresh gate execution or historical import-closure equivalence is claimed. Exact retired assertionRefs, preserved as historical objects only: {\n \"file\": \"src/renderer/src/runtime/files/watching/runtime-file-client-watch.test.ts\",\n \"assertions\": [\n \"terminal watcher error and setup failure evict the shared entry before retrying callbacks run\",\n \"an old stream end cannot evict the replacement stream, which still delivers to its listener\",\n \"two same-worktree listeners share a synthetic subscription until the final stop unsubscribes and sends unwatch\"\n ]\n } Historical prior account (preserved verbatim): Audit1973: Delete the standalone mocked watcher cleanup suite and its entire sole-file prospective command. Retire the removed file-command transport-abort alias; preserve the independent cancellation suite and final-overflow/error/end cleanup callback, and13 unrelated receipt objects. Whole original changed dated receipts are retired, never narrowed or transferred. This metadata review claims no fresh execution, equivalent replacement, provider/PTY, live remote/mobile, rendered, cross-platform, soak or measured-budget result. Historical prior account (preserved verbatim): Audit1931 (sources1928/1929): Partial runtime files-watch suite retains controlled Windows close-event waiting, shared-client survival, late physical-exit ownership/deadline cleanup and shutdown joining. Its combined Windows close/deadline/late-exit alias remains backed by unchanged callbacks. Retire native-error-close, pre-publication setup ownership and failed-delete rearm aliases; associated delegation, failure forwarding, removal routing and recovery examples are lost. These tests inject watch/subscription seams and do not observe an actual native watcher or physical process. Preserve meaningful remaining selectors/assertions and unchanged dated receipts within their original scope. Reduced selectors have no fresh gate execution evidence in this metadata packet. No equivalent replacement, physical watcher/PTY/provider, live SSH/remote/mobile, rendered, cross-platform, soak or measured-budget result is claimed. Historical prior account (preserved verbatim): Audit1925 (sources1922/1923): Partial runtime watcher-host suite loses mocked option/event/overflow/resubscription forwarding, supplied teardown-error propagation, aborted-signal/no-call and private ten-thousand-waiter-count inventories. Retire the 10,000-caller bound and failed-recovery-subscriber-ending assertion aliases. Keep same-root owner cancellation, pre-ready replacement, held root-close join, stale-generation recovery delivery and initial failed-root ownership mappings backed by retained real-host callbacks with injected subscription/error/physical-exit promises. No actual native watcher, process termination or resource bound was observed. Preserve meaningful remaining selectors/assertions and unchanged dated receipts within their original scope. Reduced selectors have no fresh gate execution evidence in this metadata packet. No equivalent replacement, physical watcher/PTY/provider, live SSH/remote/mobile, rendered, cross-platform, soak or measured-budget result is claimed. Historical prior account (preserved verbatim): Audit1683 retires changed-source assertion and execution scope without equivalent replacement coverage. Existing unchanged-selector receipts remain scoped to their original sources. Historical prior-source oracle: Existing exact unchanged-selector receipts remain the only current execution evidence. Changed source selectors have no fresh execution result. Historical prior-source oracle account: Existing exact unchanged-selector receipts remain the only current execution evidence. Changed source selectors have no fresh execution result. Historical prior-source oracle account: Fill the global watcher-child reservation, require native desktop, paired-runtime, and WSL setup to retain typed transient capacity and retry after one serialized release, then cancel pending desktop and paired-client watches and require no retry. Hold one live anchor while aborting 10,000 callers at the capacity, quarantine-assignment, runtime-root, relay-setup, and SSH-registration layers; require exactly one explicit waiter and one base-promise reaction to remain, then zero after settlement. Hold an old relay unsubscribe, queue one same-root setup leader and one anchor, then abort 10,000 joiners; require every cancellation to settle before teardown releases, exactly one anchor waiter, and only one later physical install. Advance the former paired-web callback deadline while capacity remains full, require cancellation ownership to stay live, publish the server token before ready, and require files.unwatch to abort setup before a later slot release can install a native root. Reject the first ready-watch unwatch with a physical-exit promise, resolve that exit later, and require a fresh physical unwatch attempt, registry release, and exactly one stream end. Fill one healthy shared child with multiple roots, map a standard SSH repository's base, Git common directory, and worktree roots to one relay supervisor, then inject IPC disconnect-before-exit, queued and active cancellation, operations during termination, kill-without-exit, repeated unkillable generations, initial and recovery timeouts, and shard failure. During local and SSH deletion, attempt a late terminal spawn whose worktree identity and cwd name different roots, enumerate a non-prefix remote PTY by authoritative worktree ownership, withhold daemon/relay exit callbacks, make attach positively prove a concurrent-shutdown PID dead, and make the provider unavailable; require every acquired admission to roll back on rejection, dead-PID proof to settle physical exit before reaping, timeout to retain ownership, and Git/filesystem deletion never to start. Require bounded watcher process count, physical-exit settlement, healthy-root recovery, stale-generation rejection, overflow plus later events, and fail-closed destructive cleanup. Verify relay build hashes and remote install probes require both relay.js and relay-watcher.js. Launch the built relay with a real PTY and Parcel watch, kill only the watcher child, and require the same relay PID and PTY to answer before a replacement child delivers overflow and a post-crash event.", + "oracle": "Audit3128 scoped coverage reduction: Audit3121 removes VoiceDictationLanguageSetting.test.tsx (Spanish-to-es callback and unsupported-model Korean option presence) and voice-speech-model-rows.test.ts (provider/language-order/retired-on-disk list policy); these real narrow alarms are deliberately retired without replacement. VoiceSpeechModelSection per-model concurrent deletion remains unchanged. Audit3122 removes the direct absolute/parent-segment sparse-directory exception-type sample and the five-row IPC sparse-preset error-code/non-save table (two authored/six expanded cases). Those real typed-validation and IPC refusal/error-code alarms are deliberately retired. Keep both real symlink/junction floating-directory trust cases and the minimal RPC structured validation-reason serialization case. The RPC case does not replace IPC save/refusal behavior, typed normalization for both path inputs, actual Git sparse checkout or rendered localization. No equivalent replacement or fresh test/runtime/platform/remote/performance validation is claimed. Audit3124 removes ExperimentalPane.test.tsx (ordinary Pet switch callback), WorkspaceDirectorySetting.test.tsx (padded absolute saved-path normalization/no-write blur), and repository-search.perf.test.ts (navigation/selected-pane builder-call policy, fixture cardinality/nonempty entries and renamed project keyword inclusion). These real narrow alarms are retired without replacement; RepositoryPane.search.test.tsx search-to-sparse-editor request remains unchanged. Audit3129: resolver Windows case-insensitive and POSIX case-sensitive configured-env composition plus mocked-base launch-preflight Windows composition are intentionally no longer checked by the two deleted suites. No equivalent coverage claimed. Keep runtime-structured-agent-host-env.test.ts unchanged: configured Windows Claude-home account identity and host-account precedence remain checked. Audit3114 incorporates source3101: retire three structured Claude child-environment wrapper cases covering inherited stamp isolation/default and explicit entrypoint, Windows SDK-key canonicalization/default/duplicate-key order, and configured lookup/final environment/config-dir conjunction. Keep direct auth/session sanitation and Windows-versus-Linux overlay checks, actual executable lookup from supplied inherited PATH, native Windows child environment delivery, and stale option-mutation fencing. These are not equivalent replacements for the retired wrapper alarms. No fresh test/runtime, real login-shell probe, Claude SDK startup, remote/mixed-version, UI or performance result is claimed. Audit3114 incorporates source3105: retire 17 authored/20 expanded daemon launch-note cases, including the entire two-case SessionLaunchNote suite. Deliberately lose split-marker carry across same-token re-arm, stale-token retirement refusal, foreground tri-state and serial settlement, omitted-note/no-incarnation mixed-version policy, sticky retirement, first-incarnation/replacement launch preservation and held-launch precedence. Keep only the unchanged unresolved foreground-read race against a later finished attach. Supplied notes/foreground and runtime calls do not establish actual daemon/main restart, SSH, native Windows observation, mobile UI, equivalent replacement or fresh runtime/performance validation. Audit3114 incorporates source3109: retire six authored/35 expanded pricing cases: 21 accepted model-name examples, 10 unsupported/future-model fail-closed examples, malformed TTL clamp, unknown-model estimate refusal, flat-model long-context disregard and oversized long-context clamp. Keep mixed five-minute/one-hour write allocation and short/all-long/mixed long-context allocation with original fixed oracles. Real parser/refusal/bounds alarms are knowingly lost without equivalent replacement; retained tests do not validate provider rates, actual billing or request threshold classification. Preserve the archived usage-scan-append source-freeze receipt without transferring historical execution/performance credit. No fresh runtime result is claimed. Audit3108 source-only reduction (3094/3095/3100): retire 27 authored/30 expanded cases, including complete markdown view-state, file-preview link-open and CLI agent-server-route suites, without replacement. Deliberate losses include preview visibility/reference lifetime and same-path owners; SSH stat, directory/missing-target, explicit-null owner, nonmarkdown line/column, local URI routing, remote-to-client guard and same-file reuse; document-preview source-owner/folder/reuse routing, stable grant location and outside-workspace POSIX/Windows request projection; and verified remote CLI send/check refusal, suggested --on argv/copy, shell no-probe bypass, unverified-launch admission and older-peer refusal. Retain two markdown source-runtime/qualified-reveal alarms, one stale grant-rejection fence and two remote read/mutation evidence policies, with original callbacks. Final last-wins reveal did not prove transient cancellation; CLI prerequisite did not prove no real send, and envelope tests do not execute server refusal. No fresh runtime, rendered UI, actual file/SSH/paired transport, authorization, mixed-version or performance result is claimed. Audit3096 source-only reduction (3087/3088): retire 69 authored/76 expanded cases, including complete recovery-portals and reconnect-banner suites. Intentionally lose dead/default TUI replay, generated alternate-frame style composition, reset/mode/dimension and legacy missing-mode-boundary alarms, UI notice/action/reveal policies, and isolated recovery lifecycle variants, without equivalent replacement. Retained registrations and callbacks are unchanged; headless cells are not pixels, and no fresh runtime, host, SSH, WSL, rendered-app or performance result is claimed. Audit3086 source-only reduction (3079/3080/3081): intentionally retire 93 authored/100 expanded mock-boundary cases, including the complete spawn-errors and SSH reconnect overlay suites, without replacement. Retired alarms include spawn classification/ownership, reply ceilings and renderer-write failure recovery, disconnect/detach fencing branches, authority facts, and overlay retry/visibility wiring. Retained callbacks are unchanged; no fresh execution, real PTY/SSH, rendered UI, or performance result is claimed. Audit2432: The broad unit command loses the isolated BranchPicker open-change notification sample. Its unique true/false callback alarm is intentionally retired; reopening/status-refresh behavior was not exercised. No equivalent replacement or fresh validation of the reduced selector is claimed. Audit2425: The broad unit command loses the workspace-copy-status supplied readError forwarding sample. It did not exercise the branch-switch blocking promised by its title. This alarm is intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2417: The broad unit command loses two ordinary clean Git cherry-pick/revert success samples and two terminal startup foreground-read work-budget samples. These unique alarms are intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2412: The broad unit command loses the whole six-case fake-React-hook scroll-anchor suite, two generic draft-retention samples, a simple new-agent order sample, a real history-publication batching-count sample, an unmounted handoff abort/timer sample and three basic WSL preview/error samples. These alarms are intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2404: The broad unit command loses the standalone share-name lookup 503 propagation suite, initial deletion placement-kind sample, direct changed-mtime stale sample, first-pass rollback existence example and abandoned-preparation directory sample. These exact alarms are intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2397 review amendment: The broad unit command loses three standalone suites, two partial-case definitions and two URL table rows across package/download/placement tests. Keep initial wrong-port authorization, modified independent-copy digest refusal and abort-after-begin cancellation dispatch. The remaining alarms are deliberately retired; no equivalent complete replacement or fresh validation of the reduced selector is claimed. Audit2393 current-parent refresh2401: Remove the basic skill-install service alias sample and original public managed-remove receiptless refusal case. Preserve the upstream request ownership suite and transaction interruption coverage. No equivalent complete replacement or fresh validation of the reduced selector is claimed. Audit2389: The broad unit command loses the missing-preview suite, plain-global destination sample and prepublished-receipt recovery example. No equivalent replacement or fresh validation of the reduced selector is claimed. Audit2385: The broad unit command loses four authored walker/staging/protocol examples (six expanded cases); no equivalent replacement or fresh validation is claimed for the reduced suite. Audit2383: The unscoped Vitest command no longer includes the removed Antigravity catalogue suite; its exact provider-root/owner example has no equivalent replacement or fresh validation claim. Audit2377/2381: The unscoped Vitest command includes the reduced bundled-skill and native-discovery suites; the eight removed backup/legacy/catalogue/symlink/SSH-builder examples have no replacement-equivalence or fresh validation claim here. Audit2133 current-parent preparation: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2095 current-parent preparation2126: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2090 (sources2088/2089): Current reduced suites intentionally retire the alarms listed in knownGaps; prior dated execution and replacement claims do not validate changed source bytes or selectors. Historical prior account (preserved verbatim): Audit2086 (sources2082/2083): Current reduced suites intentionally retire the alarms listed in knownGaps; prior dated execution and replacement claims do not validate changed source bytes or selectors. Historical prior account (preserved verbatim): Audit2076 (sources2073/2074): Intentionally retire these low-value alarms without replacement: src/main/runtime/orca-runtime-tests/terminal-handles-part-02.spec.ts: recovers a moved SSH pane through the tab it now sits in, not the one its lease froze; reports a plain expired orphan; does not report a pane whose only recent lease was superseded; does not report a pane whose only recent lease had its relay id recycled; picks the eligible orphan over a superseded predecessor that matches first; collects the same tabs the per-tab reader reports, in one sweep of the leases; reports no candidate when no lease names any of the worktree tabs. src/main/runtime/orca-runtime-tests/runtime-availability.spec.ts: starts unavailable with no authoritative window; reports the configured Windows terminal shell on status; reports floating workspace availability from settings on status; polls floating tabs with targeted PTY liveness and no repo/provider inventory; hydrates persisted tabs when the store cannot report repos; advertises browser screencast only when a renderer window is available; routes mobile Codex reset consumption through the account mutation coordinator; maps a definite pre-provider rejection into an authoritative current snapshot. Existing unrelated evidenceRuns retain their original scope; unchanged dated receipt objects do not validate changed source bytes or reduced selectors. No fresh gate execution or historical import-closure equivalence is claimed. Frozen current literal-import graph reaches these changed test bytes; unresolved local and computed edges remain debt, and current reachability is not historical execution or equivalent replacement evidence. Historical prior account (preserved verbatim): Audit2056 (sources2054/2055): Intentionally retire these low-value alarms without replacement: src/renderer/src/runtime/files/watching/runtime-file-client-watch.test.ts: delivers remote file changes to listeners for the same worktree. src/main/runtime/orca-runtime.test.ts: ordinary registration cleanup removes only await import('./orca-runtime-tests/mobile-session-tabs-part-14.spec'). src/main/runtime/orca-runtime-tests/terminal-side-effect-facts-part-03.spec.ts: arms the stale-title timer for a seeded working title; arms the Command Code scrape from the noted spawn command; prefers the tracked title over a stale renderer lastTitle in the hydration seed; writes recent output into the current runtime output map; uses a later title handler replacement with the runtime receiver; calls the current injected listener with the runtime receiver; keeps optional reconciliation lazy after its callback is removed. src/main/runtime/orca-runtime-tests/mobile-session-tabs-part-14.spec.ts: lists them, so a paired client can mirror a migrated editor; closes one by retiring it from the host session; keeps an unsaved draft unless the close is forced; shares aggregate work but leaves a replacement pending promise alone; lets a targeted refresh finish while an aggregate refresh waits; refuses a close when the store lacks %s before the guard; keeps a later missing %s on the original caught failure path. Existing unrelated evidenceRuns retain their original scope; unchanged dated receipt objects do not validate changed source bytes or reduced selectors. No fresh gate execution or historical import-closure equivalence is claimed. Exact retired assertionRefs, preserved as historical objects only: {\n \"file\": \"src/renderer/src/runtime/files/watching/runtime-file-client-watch.test.ts\",\n \"assertions\": [\n \"terminal watcher error and setup failure evict the shared entry before retrying callbacks run\",\n \"an old stream end cannot evict the replacement stream, which still delivers to its listener\",\n \"two same-worktree listeners share a synthetic subscription until the final stop unsubscribes and sends unwatch\"\n ]\n } Historical prior account (preserved verbatim): Audit1973: Delete the standalone mocked watcher cleanup suite and its entire sole-file prospective command. Retire the removed file-command transport-abort alias; preserve the independent cancellation suite and final-overflow/error/end cleanup callback, and13 unrelated receipt objects. Whole original changed dated receipts are retired, never narrowed or transferred. This metadata review claims no fresh execution, equivalent replacement, provider/PTY, live remote/mobile, rendered, cross-platform, soak or measured-budget result. Historical prior account (preserved verbatim): Audit1931 (sources1928/1929): Partial runtime files-watch suite retains controlled Windows close-event waiting, shared-client survival, late physical-exit ownership/deadline cleanup and shutdown joining. Its combined Windows close/deadline/late-exit alias remains backed by unchanged callbacks. Retire native-error-close, pre-publication setup ownership and failed-delete rearm aliases; associated delegation, failure forwarding, removal routing and recovery examples are lost. These tests inject watch/subscription seams and do not observe an actual native watcher or physical process. Preserve meaningful remaining selectors/assertions and unchanged dated receipts within their original scope. Reduced selectors have no fresh gate execution evidence in this metadata packet. No equivalent replacement, physical watcher/PTY/provider, live SSH/remote/mobile, rendered, cross-platform, soak or measured-budget result is claimed. Historical prior account (preserved verbatim): Audit1925 (sources1922/1923): Partial runtime watcher-host suite loses mocked option/event/overflow/resubscription forwarding, supplied teardown-error propagation, aborted-signal/no-call and private ten-thousand-waiter-count inventories. Retire the 10,000-caller bound and failed-recovery-subscriber-ending assertion aliases. Keep same-root owner cancellation, pre-ready replacement, held root-close join, stale-generation recovery delivery and initial failed-root ownership mappings backed by retained real-host callbacks with injected subscription/error/physical-exit promises. No actual native watcher, process termination or resource bound was observed. Preserve meaningful remaining selectors/assertions and unchanged dated receipts within their original scope. Reduced selectors have no fresh gate execution evidence in this metadata packet. No equivalent replacement, physical watcher/PTY/provider, live SSH/remote/mobile, rendered, cross-platform, soak or measured-budget result is claimed. Historical prior account (preserved verbatim): Audit1683 retires changed-source assertion and execution scope without equivalent replacement coverage. Existing unchanged-selector receipts remain scoped to their original sources. Historical prior-source oracle: Existing exact unchanged-selector receipts remain the only current execution evidence. Changed source selectors have no fresh execution result. Historical prior-source oracle account: Existing exact unchanged-selector receipts remain the only current execution evidence. Changed source selectors have no fresh execution result. Historical prior-source oracle account: Fill the global watcher-child reservation, require native desktop, paired-runtime, and WSL setup to retain typed transient capacity and retry after one serialized release, then cancel pending desktop and paired-client watches and require no retry. Hold one live anchor while aborting 10,000 callers at the capacity, quarantine-assignment, runtime-root, relay-setup, and SSH-registration layers; require exactly one explicit waiter and one base-promise reaction to remain, then zero after settlement. Hold an old relay unsubscribe, queue one same-root setup leader and one anchor, then abort 10,000 joiners; require every cancellation to settle before teardown releases, exactly one anchor waiter, and only one later physical install. Advance the former paired-web callback deadline while capacity remains full, require cancellation ownership to stay live, publish the server token before ready, and require files.unwatch to abort setup before a later slot release can install a native root. Reject the first ready-watch unwatch with a physical-exit promise, resolve that exit later, and require a fresh physical unwatch attempt, registry release, and exactly one stream end. Fill one healthy shared child with multiple roots, map a standard SSH repository's base, Git common directory, and worktree roots to one relay supervisor, then inject IPC disconnect-before-exit, queued and active cancellation, operations during termination, kill-without-exit, repeated unkillable generations, initial and recovery timeouts, and shard failure. During local and SSH deletion, attempt a late terminal spawn whose worktree identity and cwd name different roots, enumerate a non-prefix remote PTY by authoritative worktree ownership, withhold daemon/relay exit callbacks, make attach positively prove a concurrent-shutdown PID dead, and make the provider unavailable; require every acquired admission to roll back on rejection, dead-PID proof to settle physical exit before reaping, timeout to retain ownership, and Git/filesystem deletion never to start. Require bounded watcher process count, physical-exit settlement, healthy-root recovery, stale-generation rejection, overflow plus later events, and fail-closed destructive cleanup. Verify relay build hashes and remote install probes require both relay.js and relay-watcher.js. Launch the built relay with a real PTY and Parcel watch, kill only the watcher child, and require the same relay PID and PTY to answer before a replacement child delivers overflow and a post-crash event.", "commands": [ "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/filesystem/watcher-process/parcel-watcher-unsubscribe-timeout.test.ts --reporter=dot", "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/filesystem/watchers/filesystem-watcher.test.ts src/main/ipc/filesystem/watchers/filesystem-watcher-native-capacity.test.ts src/main/ipc/filesystem/watcher-process/parcel-watcher-child-registry.test.ts src/main/ipc/filesystem/watcher-process/parcel-watcher-child-launch.test.ts src/main/ipc/filesystem/watcher-process/parcel-watcher-process.test.ts src/main/ipc/filesystem/watcher-process/parcel-watcher-supervisor-capacity.test.ts src/main/ipc/filesystem/watcher-process/parcel-watcher-disconnect-termination.test.ts src/main/ipc/filesystem/watcher-process/runtime-watcher-pending-assignment.test.ts src/main/ipc/filesystem/watcher-process/parcel-watcher-event-delivery.test.ts src/main/ipc/filesystem/watchers/filesystem-watcher-local-unsubscribe.test.ts src/main/ipc/filesystem/watchers/filesystem-watcher-wsl.test.ts src/main/ipc/worktrees-delete-pty-teardown.test.ts src/main/ipc/worktrees-orphan-directory-cleanup.test.ts src/main/ipc/worktrees-remove-preflight.test.ts src/main/workspaces/removal/watcher-removal-gate.test.ts src/main/runtime/files/watch/orca-runtime-files-watch.test.ts src/main/runtime/workspaces/removal/worktree-teardown.test.ts src/main/runtime/construction/orca-runtime.test.ts src/main/runtime/rpc/methods/files/files.test.ts src/main/runtime/rpc/methods/files/files-watch-cancellation.test.ts src/renderer/src/web/web-runtime-client.test.ts src/shared/cross-platform-path.test.ts src/shared/physical-exit-tracker.test.ts src/shared/promise-settlement-waiters.test.ts src/main/ipc/pty/pty-spawn-cwd-fallback.test.ts src/main/providers/local-pty/local-pty-provider-shutdown.test.ts src/main/daemon/providers/adapter/testing/daemon-pty-adapter.test.ts src/main/daemon/pty-subprocess/testing/pty-subprocess.test.ts src/main/daemon/sessions/testing/session.test.ts src/main/daemon/terminal-host/testing/terminal-host.test.ts src/main/daemon/terminal-host/testing/terminal-host-session-reaping-leak.test.ts --reporter=dot", @@ -13218,7 +13179,14 @@ "Audit2412 removes whole useVirtualizedScrollAnchor.marks-restore-signal.test.ts (six fake-hook geometry/mark/convergence/user-takeover/clamp alarms), use-task-creation-draft-retention original3/4 (immediate discard mock-call count/unopened no-write), use-terminal-bulk-close-actions.placement original6/7 (absent new-agent append and one real history-publication count at consumed3/30 scale), PendingNativeChatCreation original6 (unmounted abort completion/timer cleanup), and wsl-move-dialog-store original1/9/13 (default whole-distro count/reference/reuse and rejected-preview/start refusal recovery). The history counter measures genuine publication work without entries/order/reopen assertions; its unique batching alarm is intentionally retired. Fake React scroll tests ignore dependency scheduling and cleanup; unique simulated arithmetic alarms are deliberately lost. Retained existing-terminal reorder is not new-agent append coverage; mounted handoff timeout is not abort coverage. No equivalent complete replacement or fresh validation is claimed.", "Audit2417 removes commit-actions-real-git original3/4 (ordinary clean cherry-pick and revert content, subject and current-branch samples), and pty-startup-ingress-exited-asker original8/12 (actual foreground-group read budget across100queued queries, and zero foreground reads across100normal chunks plus final emitted/accepted raw cursor equality). Counters measure actual callback work and scale is consumed; these internal-work alarms are deliberately retired. Retained unborn, conflict and sequencer cases are not equivalent ordinary-success coverage. No equivalent complete replacement or fresh validation is claimed.", "Audit2425 removes use-workspace-copy-status original5, which returns a fixture-supplied readError string through statusReadError. No consumer blocker or attempted branch switch established refusal; the unique error-forwarding sample is intentionally retired. Other copy ownership and stale-refresh cases are not equivalent error-path coverage. No equivalent replacement or fresh validation is claimed.", - "Audit2432 removes BranchPicker.test.tsx original3, which only asserted onOpenChange(true/false) with an always-mounted mocked Popover. It never reopened the picker or consumed the callback in the parent status-refresh owner, despite its title. Lose that unique open/close notification alarm. Retained blocker/collision recovery and asynchronous post-unmount outcome cases are not equivalent replacement coverage. No fresh validation of the reduced selector is claimed." + "Audit2432 removes BranchPicker.test.tsx original3, which only asserted onOpenChange(true/false) with an always-mounted mocked Popover. It never reopened the picker or consumed the callback in the parent status-refresh owner, despite its title. Lose that unique open/close notification alarm. Retained blocker/collision recovery and asynchronous post-unmount outcome cases are not equivalent replacement coverage. No fresh validation of the reduced selector is claimed.", + "Audit3086 source-only reduction (3079/3080/3081): intentionally retire 93 authored/100 expanded mock-boundary cases, including the complete spawn-errors and SSH reconnect overlay suites, without replacement. Retired alarms include spawn classification/ownership, reply ceilings and renderer-write failure recovery, disconnect/detach fencing branches, authority facts, and overlay retry/visibility wiring. Retained callbacks are unchanged; no fresh execution, real PTY/SSH, rendered UI, or performance result is claimed.", + "Audit3096 source-only reduction (3087/3088): retire 69 authored/76 expanded cases, including complete recovery-portals and reconnect-banner suites. Intentionally lose dead/default TUI replay, generated alternate-frame style composition, reset/mode/dimension and legacy missing-mode-boundary alarms, UI notice/action/reveal policies, and isolated recovery lifecycle variants, without equivalent replacement. Retained registrations and callbacks are unchanged; headless cells are not pixels, and no fresh runtime, host, SSH, WSL, rendered-app or performance result is claimed.", + "Audit3108 source-only reduction (3094/3095/3100): retire 27 authored/30 expanded cases, including complete markdown view-state, file-preview link-open and CLI agent-server-route suites, without replacement. Deliberate losses include preview visibility/reference lifetime and same-path owners; SSH stat, directory/missing-target, explicit-null owner, nonmarkdown line/column, local URI routing, remote-to-client guard and same-file reuse; document-preview source-owner/folder/reuse routing, stable grant location and outside-workspace POSIX/Windows request projection; and verified remote CLI send/check refusal, suggested --on argv/copy, shell no-probe bypass, unverified-launch admission and older-peer refusal. Retain two markdown source-runtime/qualified-reveal alarms, one stale grant-rejection fence and two remote read/mutation evidence policies, with original callbacks. Final last-wins reveal did not prove transient cancellation; CLI prerequisite did not prove no real send, and envelope tests do not execute server refusal. No fresh runtime, rendered UI, actual file/SSH/paired transport, authorization, mixed-version or performance result is claimed.", + "Audit3114 incorporates source3101: retire three structured Claude child-environment wrapper cases covering inherited stamp isolation/default and explicit entrypoint, Windows SDK-key canonicalization/default/duplicate-key order, and configured lookup/final environment/config-dir conjunction. Keep direct auth/session sanitation and Windows-versus-Linux overlay checks, actual executable lookup from supplied inherited PATH, native Windows child environment delivery, and stale option-mutation fencing. These are not equivalent replacements for the retired wrapper alarms. No fresh test/runtime, real login-shell probe, Claude SDK startup, remote/mixed-version, UI or performance result is claimed.", + "Audit3114 incorporates source3105: retire 17 authored/20 expanded daemon launch-note cases, including the entire two-case SessionLaunchNote suite. Deliberately lose split-marker carry across same-token re-arm, stale-token retirement refusal, foreground tri-state and serial settlement, omitted-note/no-incarnation mixed-version policy, sticky retirement, first-incarnation/replacement launch preservation and held-launch precedence. Keep only the unchanged unresolved foreground-read race against a later finished attach. Supplied notes/foreground and runtime calls do not establish actual daemon/main restart, SSH, native Windows observation, mobile UI, equivalent replacement or fresh runtime/performance validation.", + "Audit3114 incorporates source3109: retire six authored/35 expanded pricing cases: 21 accepted model-name examples, 10 unsupported/future-model fail-closed examples, malformed TTL clamp, unknown-model estimate refusal, flat-model long-context disregard and oversized long-context clamp. Keep mixed five-minute/one-hour write allocation and short/all-long/mixed long-context allocation with original fixed oracles. Real parser/refusal/bounds alarms are knowingly lost without equivalent replacement; retained tests do not validate provider rates, actual billing or request threshold classification. Preserve the archived usage-scan-append source-freeze receipt without transferring historical execution/performance credit. No fresh runtime result is claimed.", + "Audit3128 scoped coverage reduction: Audit3121 removes VoiceDictationLanguageSetting.test.tsx (Spanish-to-es callback and unsupported-model Korean option presence) and voice-speech-model-rows.test.ts (provider/language-order/retired-on-disk list policy); these real narrow alarms are deliberately retired without replacement. VoiceSpeechModelSection per-model concurrent deletion remains unchanged. Audit3122 removes the direct absolute/parent-segment sparse-directory exception-type sample and the five-row IPC sparse-preset error-code/non-save table (two authored/six expanded cases). Those real typed-validation and IPC refusal/error-code alarms are deliberately retired. Keep both real symlink/junction floating-directory trust cases and the minimal RPC structured validation-reason serialization case. The RPC case does not replace IPC save/refusal behavior, typed normalization for both path inputs, actual Git sparse checkout or rendered localization. No equivalent replacement or fresh test/runtime/platform/remote/performance validation is claimed. Audit3124 removes ExperimentalPane.test.tsx (ordinary Pet switch callback), WorkspaceDirectorySetting.test.tsx (padded absolute saved-path normalization/no-write blur), and repository-search.perf.test.ts (navigation/selected-pane builder-call policy, fixture cardinality/nonempty entries and renamed project keyword inclusion). These real narrow alarms are retired without replacement; RepositoryPane.search.test.tsx search-to-sparse-editor request remains unchanged. Audit3129: resolver Windows case-insensitive and POSIX case-sensitive configured-env composition plus mocked-base launch-preflight Windows composition are intentionally no longer checked by the two deleted suites. No equivalent coverage claimed. Keep runtime-structured-agent-host-env.test.ts unchanged: configured Windows Claude-home account identity and host-account precedence remain checked." ], "demotionRule": "Demote or quarantine if the fault harness flakes without a product or harness bug, if healthy operation exceeds one runtime watcher child, if total physical operation exceeds eight children including retiring generations, if quarantine children outlive their roots or repeat after fusing, if event delivery becomes unbounded, or if metadata/stat work returns to the serve process." }, @@ -14462,10 +14430,10 @@ "providers": ["paired-runtime"], "coveredPlatforms": ["linux"], "coveredProviders": ["paired-runtime"], - "coverageNotes": "Audit2432: The broad unit command loses the isolated BranchPicker open-change notification sample. Its unique true/false callback alarm is intentionally retired; reopening/status-refresh behavior was not exercised. No equivalent replacement or fresh validation of the reduced selector is claimed. Audit2425: The broad unit command loses the workspace-copy-status supplied readError forwarding sample. It did not exercise the branch-switch blocking promised by its title. This alarm is intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2417: The broad unit command loses two ordinary clean Git cherry-pick/revert success samples and two terminal startup foreground-read work-budget samples. These unique alarms are intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2412: The broad unit command loses the whole six-case fake-React-hook scroll-anchor suite, two generic draft-retention samples, a simple new-agent order sample, a real history-publication batching-count sample, an unmounted handoff abort/timer sample and three basic WSL preview/error samples. These alarms are intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2404: The broad unit command loses the standalone share-name lookup 503 propagation suite, initial deletion placement-kind sample, direct changed-mtime stale sample, first-pass rollback existence example and abandoned-preparation directory sample. These exact alarms are intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2397 review amendment: The broad unit command loses three standalone suites, two partial-case definitions and two URL table rows across package/download/placement tests. Keep initial wrong-port authorization, modified independent-copy digest refusal and abort-after-begin cancellation dispatch. The remaining alarms are deliberately retired; no equivalent complete replacement or fresh validation of the reduced selector is claimed. Audit2393 current-parent refresh2401: Remove the basic skill-install service alias sample and original public managed-remove receiptless refusal case. Preserve the upstream request ownership suite and transaction interruption coverage. No equivalent complete replacement or fresh validation of the reduced selector is claimed. Audit2389: The broad unit command loses the missing-preview suite, plain-global destination sample and prepublished-receipt recovery example. No equivalent replacement or fresh validation of the reduced selector is claimed. Audit2385: The broad unit command loses four authored walker/staging/protocol examples (six expanded cases); no equivalent replacement or fresh validation is claimed for the reduced suite. Audit2383: The unscoped Vitest command no longer includes the removed Antigravity catalogue suite; its exact provider-root/owner example has no equivalent replacement or fresh validation claim. Audit2377/2381: The unscoped Vitest command includes the reduced bundled-skill and native-discovery suites; the eight removed backup/legacy/catalogue/symlink/SSH-builder examples have no replacement-equivalence or fresh validation claim here. Audit2133 current-parent preparation: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2095 current-parent preparation2126: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit1579 removes workflow/manifest/regex/private binary-header and diagnostic metric inventories, fixture selectors, injected forwarding/platform/signature/process examples, unproven overlap and internal churn counters without equivalent replacement coverage. Retains actual Git trust/diffs; filesystem hash/ELF-region/native discovery; child CLI module repair; compiler/lock/publication/rollback and synthetic Electron install/cache ownership; executable shell quoting/exit; controlled notary/timer cleanup; local patched extractor file/AST freshness; real Chromium/WebKit CSP/sandbox/HTTPS image/referrer/link/preservation controls under parsed Android headers. Five unchanged setup-pnpm/VM request-cleanup definitions reviewed and not rerun. GitHub, installer/archive, OS metric, client, native delegate, compressor, builder and Apple boundaries injected or absent where recorded. No actual native PTY, shipped full/native app, real Actions/cache/npm signature authority, downloaded Electron archive, real objdump/linker/native CLI/AppImage, signing/NSIS/notary/upload/updater or live freeze proof. Historical scope before this audit: Published Linux 1.4.188 desktop against current source in both directions; scheduled weekly and manually runnable. No required PR check.", + "coverageNotes": "Audit3128 scoped coverage reduction: Audit3121 removes VoiceDictationLanguageSetting.test.tsx (Spanish-to-es callback and unsupported-model Korean option presence) and voice-speech-model-rows.test.ts (provider/language-order/retired-on-disk list policy); these real narrow alarms are deliberately retired without replacement. VoiceSpeechModelSection per-model concurrent deletion remains unchanged. Audit3122 removes the direct absolute/parent-segment sparse-directory exception-type sample and the five-row IPC sparse-preset error-code/non-save table (two authored/six expanded cases). Those real typed-validation and IPC refusal/error-code alarms are deliberately retired. Keep both real symlink/junction floating-directory trust cases and the minimal RPC structured validation-reason serialization case. The RPC case does not replace IPC save/refusal behavior, typed normalization for both path inputs, actual Git sparse checkout or rendered localization. No equivalent replacement or fresh test/runtime/platform/remote/performance validation is claimed. Audit3124 removes ExperimentalPane.test.tsx (ordinary Pet switch callback), WorkspaceDirectorySetting.test.tsx (padded absolute saved-path normalization/no-write blur), and repository-search.perf.test.ts (navigation/selected-pane builder-call policy, fixture cardinality/nonempty entries and renamed project keyword inclusion). These real narrow alarms are retired without replacement; RepositoryPane.search.test.tsx search-to-sparse-editor request remains unchanged. Audit3129: resolver Windows case-insensitive and POSIX case-sensitive configured-env composition plus mocked-base launch-preflight Windows composition are intentionally no longer checked by the two deleted suites. No equivalent coverage claimed. Keep runtime-structured-agent-host-env.test.ts unchanged: configured Windows Claude-home account identity and host-account precedence remain checked. Audit3114 incorporates source3101: retire three structured Claude child-environment wrapper cases covering inherited stamp isolation/default and explicit entrypoint, Windows SDK-key canonicalization/default/duplicate-key order, and configured lookup/final environment/config-dir conjunction. Keep direct auth/session sanitation and Windows-versus-Linux overlay checks, actual executable lookup from supplied inherited PATH, native Windows child environment delivery, and stale option-mutation fencing. These are not equivalent replacements for the retired wrapper alarms. No fresh test/runtime, real login-shell probe, Claude SDK startup, remote/mixed-version, UI or performance result is claimed. Audit3114 incorporates source3105: retire 17 authored/20 expanded daemon launch-note cases, including the entire two-case SessionLaunchNote suite. Deliberately lose split-marker carry across same-token re-arm, stale-token retirement refusal, foreground tri-state and serial settlement, omitted-note/no-incarnation mixed-version policy, sticky retirement, first-incarnation/replacement launch preservation and held-launch precedence. Keep only the unchanged unresolved foreground-read race against a later finished attach. Supplied notes/foreground and runtime calls do not establish actual daemon/main restart, SSH, native Windows observation, mobile UI, equivalent replacement or fresh runtime/performance validation. Audit3114 incorporates source3109: retire six authored/35 expanded pricing cases: 21 accepted model-name examples, 10 unsupported/future-model fail-closed examples, malformed TTL clamp, unknown-model estimate refusal, flat-model long-context disregard and oversized long-context clamp. Keep mixed five-minute/one-hour write allocation and short/all-long/mixed long-context allocation with original fixed oracles. Real parser/refusal/bounds alarms are knowingly lost without equivalent replacement; retained tests do not validate provider rates, actual billing or request threshold classification. Preserve the archived usage-scan-append source-freeze receipt without transferring historical execution/performance credit. No fresh runtime result is claimed. Audit3108 source-only reduction (3094/3095/3100): retire 27 authored/30 expanded cases, including complete markdown view-state, file-preview link-open and CLI agent-server-route suites, without replacement. Deliberate losses include preview visibility/reference lifetime and same-path owners; SSH stat, directory/missing-target, explicit-null owner, nonmarkdown line/column, local URI routing, remote-to-client guard and same-file reuse; document-preview source-owner/folder/reuse routing, stable grant location and outside-workspace POSIX/Windows request projection; and verified remote CLI send/check refusal, suggested --on argv/copy, shell no-probe bypass, unverified-launch admission and older-peer refusal. Retain two markdown source-runtime/qualified-reveal alarms, one stale grant-rejection fence and two remote read/mutation evidence policies, with original callbacks. Final last-wins reveal did not prove transient cancellation; CLI prerequisite did not prove no real send, and envelope tests do not execute server refusal. No fresh runtime, rendered UI, actual file/SSH/paired transport, authorization, mixed-version or performance result is claimed. Audit3096 source-only reduction (3087/3088): retire 69 authored/76 expanded cases, including complete recovery-portals and reconnect-banner suites. Intentionally lose dead/default TUI replay, generated alternate-frame style composition, reset/mode/dimension and legacy missing-mode-boundary alarms, UI notice/action/reveal policies, and isolated recovery lifecycle variants, without equivalent replacement. Retained registrations and callbacks are unchanged; headless cells are not pixels, and no fresh runtime, host, SSH, WSL, rendered-app or performance result is claimed. Audit3086 source-only reduction (3079/3080/3081): intentionally retire 93 authored/100 expanded mock-boundary cases, including the complete spawn-errors and SSH reconnect overlay suites, without replacement. Retired alarms include spawn classification/ownership, reply ceilings and renderer-write failure recovery, disconnect/detach fencing branches, authority facts, and overlay retry/visibility wiring. Retained callbacks are unchanged; no fresh execution, real PTY/SSH, rendered UI, or performance result is claimed. Audit2432: The broad unit command loses the isolated BranchPicker open-change notification sample. Its unique true/false callback alarm is intentionally retired; reopening/status-refresh behavior was not exercised. No equivalent replacement or fresh validation of the reduced selector is claimed. Audit2425: The broad unit command loses the workspace-copy-status supplied readError forwarding sample. It did not exercise the branch-switch blocking promised by its title. This alarm is intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2417: The broad unit command loses two ordinary clean Git cherry-pick/revert success samples and two terminal startup foreground-read work-budget samples. These unique alarms are intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2412: The broad unit command loses the whole six-case fake-React-hook scroll-anchor suite, two generic draft-retention samples, a simple new-agent order sample, a real history-publication batching-count sample, an unmounted handoff abort/timer sample and three basic WSL preview/error samples. These alarms are intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2404: The broad unit command loses the standalone share-name lookup 503 propagation suite, initial deletion placement-kind sample, direct changed-mtime stale sample, first-pass rollback existence example and abandoned-preparation directory sample. These exact alarms are intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2397 review amendment: The broad unit command loses three standalone suites, two partial-case definitions and two URL table rows across package/download/placement tests. Keep initial wrong-port authorization, modified independent-copy digest refusal and abort-after-begin cancellation dispatch. The remaining alarms are deliberately retired; no equivalent complete replacement or fresh validation of the reduced selector is claimed. Audit2393 current-parent refresh2401: Remove the basic skill-install service alias sample and original public managed-remove receiptless refusal case. Preserve the upstream request ownership suite and transaction interruption coverage. No equivalent complete replacement or fresh validation of the reduced selector is claimed. Audit2389: The broad unit command loses the missing-preview suite, plain-global destination sample and prepublished-receipt recovery example. No equivalent replacement or fresh validation of the reduced selector is claimed. Audit2385: The broad unit command loses four authored walker/staging/protocol examples (six expanded cases); no equivalent replacement or fresh validation is claimed for the reduced suite. Audit2383: The unscoped Vitest command no longer includes the removed Antigravity catalogue suite; its exact provider-root/owner example has no equivalent replacement or fresh validation claim. Audit2377/2381: The unscoped Vitest command includes the reduced bundled-skill and native-discovery suites; the eight removed backup/legacy/catalogue/symlink/SSH-builder examples have no replacement-equivalence or fresh validation claim here. Audit2133 current-parent preparation: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2095 current-parent preparation2126: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit1579 removes workflow/manifest/regex/private binary-header and diagnostic metric inventories, fixture selectors, injected forwarding/platform/signature/process examples, unproven overlap and internal churn counters without equivalent replacement coverage. Retains actual Git trust/diffs; filesystem hash/ELF-region/native discovery; child CLI module repair; compiler/lock/publication/rollback and synthetic Electron install/cache ownership; executable shell quoting/exit; controlled notary/timer cleanup; local patched extractor file/AST freshness; real Chromium/WebKit CSP/sandbox/HTTPS image/referrer/link/preservation controls under parsed Android headers. Five unchanged setup-pnpm/VM request-cleanup definitions reviewed and not rerun. GitHub, installer/archive, OS metric, client, native delegate, compressor, builder and Apple boundaries injected or absent where recorded. No actual native PTY, shipped full/native app, real Actions/cache/npm signature authority, downloaded Electron archive, real objdump/linker/native CLI/AppImage, signing/NSIS/notary/upload/updater or live freeze proof. Historical scope before this audit: Published Linux 1.4.188 desktop against current source in both directions; scheduled weekly and manually runnable. No required PR check.", "motivatingLinks": ["https://github.com/stablyai/orca/actions/runs/34069063016"], "invariant": "A paired client and host without client-hosted browser capabilities retain server-hosted browser placement across supported version skew.", - "oracle": "Audit2432: The broad unit command loses the isolated BranchPicker open-change notification sample. Its unique true/false callback alarm is intentionally retired; reopening/status-refresh behavior was not exercised. No equivalent replacement or fresh validation of the reduced selector is claimed. Audit2425: The broad unit command loses the workspace-copy-status supplied readError forwarding sample. It did not exercise the branch-switch blocking promised by its title. This alarm is intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2417: The broad unit command loses two ordinary clean Git cherry-pick/revert success samples and two terminal startup foreground-read work-budget samples. These unique alarms are intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2412: The broad unit command loses the whole six-case fake-React-hook scroll-anchor suite, two generic draft-retention samples, a simple new-agent order sample, a real history-publication batching-count sample, an unmounted handoff abort/timer sample and three basic WSL preview/error samples. These alarms are intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2404: The broad unit command loses the standalone share-name lookup 503 propagation suite, initial deletion placement-kind sample, direct changed-mtime stale sample, first-pass rollback existence example and abandoned-preparation directory sample. These exact alarms are intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2397 review amendment: The broad unit command loses three standalone suites, two partial-case definitions and two URL table rows across package/download/placement tests. Keep initial wrong-port authorization, modified independent-copy digest refusal and abort-after-begin cancellation dispatch. The remaining alarms are deliberately retired; no equivalent complete replacement or fresh validation of the reduced selector is claimed. Audit2393 current-parent refresh2401: Remove the basic skill-install service alias sample and original public managed-remove receiptless refusal case. Preserve the upstream request ownership suite and transaction interruption coverage. No equivalent complete replacement or fresh validation of the reduced selector is claimed. Audit2389: The broad unit command loses the missing-preview suite, plain-global destination sample and prepublished-receipt recovery example. No equivalent replacement or fresh validation of the reduced selector is claimed. Audit2385: The broad unit command loses four authored walker/staging/protocol examples (six expanded cases); no equivalent replacement or fresh validation is claimed for the reduced suite. Audit2383: The unscoped Vitest command no longer includes the removed Antigravity catalogue suite; its exact provider-root/owner example has no equivalent replacement or fresh validation claim. Audit2377/2381: The unscoped Vitest command includes the reduced bundled-skill and native-discovery suites; the eight removed backup/legacy/catalogue/symlink/SSH-builder examples have no replacement-equivalence or fresh validation claim here. Audit2133 current-parent preparation: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2095 current-parent preparation2126: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Require both existing named browser placement scenarios to pass three times with one attempt, zero skips, zero failures, and no report errors.", + "oracle": "Audit3128 scoped coverage reduction: Audit3121 removes VoiceDictationLanguageSetting.test.tsx (Spanish-to-es callback and unsupported-model Korean option presence) and voice-speech-model-rows.test.ts (provider/language-order/retired-on-disk list policy); these real narrow alarms are deliberately retired without replacement. VoiceSpeechModelSection per-model concurrent deletion remains unchanged. Audit3122 removes the direct absolute/parent-segment sparse-directory exception-type sample and the five-row IPC sparse-preset error-code/non-save table (two authored/six expanded cases). Those real typed-validation and IPC refusal/error-code alarms are deliberately retired. Keep both real symlink/junction floating-directory trust cases and the minimal RPC structured validation-reason serialization case. The RPC case does not replace IPC save/refusal behavior, typed normalization for both path inputs, actual Git sparse checkout or rendered localization. No equivalent replacement or fresh test/runtime/platform/remote/performance validation is claimed. Audit3124 removes ExperimentalPane.test.tsx (ordinary Pet switch callback), WorkspaceDirectorySetting.test.tsx (padded absolute saved-path normalization/no-write blur), and repository-search.perf.test.ts (navigation/selected-pane builder-call policy, fixture cardinality/nonempty entries and renamed project keyword inclusion). These real narrow alarms are retired without replacement; RepositoryPane.search.test.tsx search-to-sparse-editor request remains unchanged. Audit3129: resolver Windows case-insensitive and POSIX case-sensitive configured-env composition plus mocked-base launch-preflight Windows composition are intentionally no longer checked by the two deleted suites. No equivalent coverage claimed. Keep runtime-structured-agent-host-env.test.ts unchanged: configured Windows Claude-home account identity and host-account precedence remain checked. Audit3114 incorporates source3101: retire three structured Claude child-environment wrapper cases covering inherited stamp isolation/default and explicit entrypoint, Windows SDK-key canonicalization/default/duplicate-key order, and configured lookup/final environment/config-dir conjunction. Keep direct auth/session sanitation and Windows-versus-Linux overlay checks, actual executable lookup from supplied inherited PATH, native Windows child environment delivery, and stale option-mutation fencing. These are not equivalent replacements for the retired wrapper alarms. No fresh test/runtime, real login-shell probe, Claude SDK startup, remote/mixed-version, UI or performance result is claimed. Audit3114 incorporates source3105: retire 17 authored/20 expanded daemon launch-note cases, including the entire two-case SessionLaunchNote suite. Deliberately lose split-marker carry across same-token re-arm, stale-token retirement refusal, foreground tri-state and serial settlement, omitted-note/no-incarnation mixed-version policy, sticky retirement, first-incarnation/replacement launch preservation and held-launch precedence. Keep only the unchanged unresolved foreground-read race against a later finished attach. Supplied notes/foreground and runtime calls do not establish actual daemon/main restart, SSH, native Windows observation, mobile UI, equivalent replacement or fresh runtime/performance validation. Audit3114 incorporates source3109: retire six authored/35 expanded pricing cases: 21 accepted model-name examples, 10 unsupported/future-model fail-closed examples, malformed TTL clamp, unknown-model estimate refusal, flat-model long-context disregard and oversized long-context clamp. Keep mixed five-minute/one-hour write allocation and short/all-long/mixed long-context allocation with original fixed oracles. Real parser/refusal/bounds alarms are knowingly lost without equivalent replacement; retained tests do not validate provider rates, actual billing or request threshold classification. Preserve the archived usage-scan-append source-freeze receipt without transferring historical execution/performance credit. No fresh runtime result is claimed. Audit3108 source-only reduction (3094/3095/3100): retire 27 authored/30 expanded cases, including complete markdown view-state, file-preview link-open and CLI agent-server-route suites, without replacement. Deliberate losses include preview visibility/reference lifetime and same-path owners; SSH stat, directory/missing-target, explicit-null owner, nonmarkdown line/column, local URI routing, remote-to-client guard and same-file reuse; document-preview source-owner/folder/reuse routing, stable grant location and outside-workspace POSIX/Windows request projection; and verified remote CLI send/check refusal, suggested --on argv/copy, shell no-probe bypass, unverified-launch admission and older-peer refusal. Retain two markdown source-runtime/qualified-reveal alarms, one stale grant-rejection fence and two remote read/mutation evidence policies, with original callbacks. Final last-wins reveal did not prove transient cancellation; CLI prerequisite did not prove no real send, and envelope tests do not execute server refusal. No fresh runtime, rendered UI, actual file/SSH/paired transport, authorization, mixed-version or performance result is claimed. Audit3096 source-only reduction (3087/3088): retire 69 authored/76 expanded cases, including complete recovery-portals and reconnect-banner suites. Intentionally lose dead/default TUI replay, generated alternate-frame style composition, reset/mode/dimension and legacy missing-mode-boundary alarms, UI notice/action/reveal policies, and isolated recovery lifecycle variants, without equivalent replacement. Retained registrations and callbacks are unchanged; headless cells are not pixels, and no fresh runtime, host, SSH, WSL, rendered-app or performance result is claimed. Audit3086 source-only reduction (3079/3080/3081): intentionally retire 93 authored/100 expanded mock-boundary cases, including the complete spawn-errors and SSH reconnect overlay suites, without replacement. Retired alarms include spawn classification/ownership, reply ceilings and renderer-write failure recovery, disconnect/detach fencing branches, authority facts, and overlay retry/visibility wiring. Retained callbacks are unchanged; no fresh execution, real PTY/SSH, rendered UI, or performance result is claimed. Audit2432: The broad unit command loses the isolated BranchPicker open-change notification sample. Its unique true/false callback alarm is intentionally retired; reopening/status-refresh behavior was not exercised. No equivalent replacement or fresh validation of the reduced selector is claimed. Audit2425: The broad unit command loses the workspace-copy-status supplied readError forwarding sample. It did not exercise the branch-switch blocking promised by its title. This alarm is intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2417: The broad unit command loses two ordinary clean Git cherry-pick/revert success samples and two terminal startup foreground-read work-budget samples. These unique alarms are intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2412: The broad unit command loses the whole six-case fake-React-hook scroll-anchor suite, two generic draft-retention samples, a simple new-agent order sample, a real history-publication batching-count sample, an unmounted handoff abort/timer sample and three basic WSL preview/error samples. These alarms are intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2404: The broad unit command loses the standalone share-name lookup 503 propagation suite, initial deletion placement-kind sample, direct changed-mtime stale sample, first-pass rollback existence example and abandoned-preparation directory sample. These exact alarms are intentionally retired without equivalent replacement or fresh validation of the reduced selector. Audit2397 review amendment: The broad unit command loses three standalone suites, two partial-case definitions and two URL table rows across package/download/placement tests. Keep initial wrong-port authorization, modified independent-copy digest refusal and abort-after-begin cancellation dispatch. The remaining alarms are deliberately retired; no equivalent complete replacement or fresh validation of the reduced selector is claimed. Audit2393 current-parent refresh2401: Remove the basic skill-install service alias sample and original public managed-remove receiptless refusal case. Preserve the upstream request ownership suite and transaction interruption coverage. No equivalent complete replacement or fresh validation of the reduced selector is claimed. Audit2389: The broad unit command loses the missing-preview suite, plain-global destination sample and prepublished-receipt recovery example. No equivalent replacement or fresh validation of the reduced selector is claimed. Audit2385: The broad unit command loses four authored walker/staging/protocol examples (six expanded cases); no equivalent replacement or fresh validation is claimed for the reduced suite. Audit2383: The unscoped Vitest command no longer includes the removed Antigravity catalogue suite; its exact provider-root/owner example has no equivalent replacement or fresh validation claim. Audit2377/2381: The unscoped Vitest command includes the reduced bundled-skill and native-discovery suites; the eight removed backup/legacy/catalogue/symlink/SSH-builder examples have no replacement-equivalence or fresh validation claim here. Audit2133 current-parent preparation: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Audit2095 current-parent preparation2126: Current reduced sources/selectors intentionally retire the alarms listed in knownGaps; prior dated runs and replacement claims do not validate the changed set. Historical prior account (preserved verbatim): Require both existing named browser placement scenarios to pass three times with one attempt, zero skips, zero failures, and no report errors.", "commands": [ "gh workflow run packaged-browser-e2e.yml", "pnpm exec playwright test tests/e2e/packaged-mixed-version-browser-placement.spec.ts --config tests/playwright.config.ts --project=electron-headless --workers=1 --repeat-each=3 --retries=0", @@ -14530,7 +14498,14 @@ "Audit2412 removes whole useVirtualizedScrollAnchor.marks-restore-signal.test.ts (six fake-hook geometry/mark/convergence/user-takeover/clamp alarms), use-task-creation-draft-retention original3/4 (immediate discard mock-call count/unopened no-write), use-terminal-bulk-close-actions.placement original6/7 (absent new-agent append and one real history-publication count at consumed3/30 scale), PendingNativeChatCreation original6 (unmounted abort completion/timer cleanup), and wsl-move-dialog-store original1/9/13 (default whole-distro count/reference/reuse and rejected-preview/start refusal recovery). The history counter measures genuine publication work without entries/order/reopen assertions; its unique batching alarm is intentionally retired. Fake React scroll tests ignore dependency scheduling and cleanup; unique simulated arithmetic alarms are deliberately lost. Retained existing-terminal reorder is not new-agent append coverage; mounted handoff timeout is not abort coverage. No equivalent complete replacement or fresh validation is claimed.", "Audit2417 removes commit-actions-real-git original3/4 (ordinary clean cherry-pick and revert content, subject and current-branch samples), and pty-startup-ingress-exited-asker original8/12 (actual foreground-group read budget across100queued queries, and zero foreground reads across100normal chunks plus final emitted/accepted raw cursor equality). Counters measure actual callback work and scale is consumed; these internal-work alarms are deliberately retired. Retained unborn, conflict and sequencer cases are not equivalent ordinary-success coverage. No equivalent complete replacement or fresh validation is claimed.", "Audit2425 removes use-workspace-copy-status original5, which returns a fixture-supplied readError string through statusReadError. No consumer blocker or attempted branch switch established refusal; the unique error-forwarding sample is intentionally retired. Other copy ownership and stale-refresh cases are not equivalent error-path coverage. No equivalent replacement or fresh validation is claimed.", - "Audit2432 removes BranchPicker.test.tsx original3, which only asserted onOpenChange(true/false) with an always-mounted mocked Popover. It never reopened the picker or consumed the callback in the parent status-refresh owner, despite its title. Lose that unique open/close notification alarm. Retained blocker/collision recovery and asynchronous post-unmount outcome cases are not equivalent replacement coverage. No fresh validation of the reduced selector is claimed." + "Audit2432 removes BranchPicker.test.tsx original3, which only asserted onOpenChange(true/false) with an always-mounted mocked Popover. It never reopened the picker or consumed the callback in the parent status-refresh owner, despite its title. Lose that unique open/close notification alarm. Retained blocker/collision recovery and asynchronous post-unmount outcome cases are not equivalent replacement coverage. No fresh validation of the reduced selector is claimed.", + "Audit3086 source-only reduction (3079/3080/3081): intentionally retire 93 authored/100 expanded mock-boundary cases, including the complete spawn-errors and SSH reconnect overlay suites, without replacement. Retired alarms include spawn classification/ownership, reply ceilings and renderer-write failure recovery, disconnect/detach fencing branches, authority facts, and overlay retry/visibility wiring. Retained callbacks are unchanged; no fresh execution, real PTY/SSH, rendered UI, or performance result is claimed.", + "Audit3096 source-only reduction (3087/3088): retire 69 authored/76 expanded cases, including complete recovery-portals and reconnect-banner suites. Intentionally lose dead/default TUI replay, generated alternate-frame style composition, reset/mode/dimension and legacy missing-mode-boundary alarms, UI notice/action/reveal policies, and isolated recovery lifecycle variants, without equivalent replacement. Retained registrations and callbacks are unchanged; headless cells are not pixels, and no fresh runtime, host, SSH, WSL, rendered-app or performance result is claimed.", + "Audit3108 source-only reduction (3094/3095/3100): retire 27 authored/30 expanded cases, including complete markdown view-state, file-preview link-open and CLI agent-server-route suites, without replacement. Deliberate losses include preview visibility/reference lifetime and same-path owners; SSH stat, directory/missing-target, explicit-null owner, nonmarkdown line/column, local URI routing, remote-to-client guard and same-file reuse; document-preview source-owner/folder/reuse routing, stable grant location and outside-workspace POSIX/Windows request projection; and verified remote CLI send/check refusal, suggested --on argv/copy, shell no-probe bypass, unverified-launch admission and older-peer refusal. Retain two markdown source-runtime/qualified-reveal alarms, one stale grant-rejection fence and two remote read/mutation evidence policies, with original callbacks. Final last-wins reveal did not prove transient cancellation; CLI prerequisite did not prove no real send, and envelope tests do not execute server refusal. No fresh runtime, rendered UI, actual file/SSH/paired transport, authorization, mixed-version or performance result is claimed.", + "Audit3114 incorporates source3101: retire three structured Claude child-environment wrapper cases covering inherited stamp isolation/default and explicit entrypoint, Windows SDK-key canonicalization/default/duplicate-key order, and configured lookup/final environment/config-dir conjunction. Keep direct auth/session sanitation and Windows-versus-Linux overlay checks, actual executable lookup from supplied inherited PATH, native Windows child environment delivery, and stale option-mutation fencing. These are not equivalent replacements for the retired wrapper alarms. No fresh test/runtime, real login-shell probe, Claude SDK startup, remote/mixed-version, UI or performance result is claimed.", + "Audit3114 incorporates source3105: retire 17 authored/20 expanded daemon launch-note cases, including the entire two-case SessionLaunchNote suite. Deliberately lose split-marker carry across same-token re-arm, stale-token retirement refusal, foreground tri-state and serial settlement, omitted-note/no-incarnation mixed-version policy, sticky retirement, first-incarnation/replacement launch preservation and held-launch precedence. Keep only the unchanged unresolved foreground-read race against a later finished attach. Supplied notes/foreground and runtime calls do not establish actual daemon/main restart, SSH, native Windows observation, mobile UI, equivalent replacement or fresh runtime/performance validation.", + "Audit3114 incorporates source3109: retire six authored/35 expanded pricing cases: 21 accepted model-name examples, 10 unsupported/future-model fail-closed examples, malformed TTL clamp, unknown-model estimate refusal, flat-model long-context disregard and oversized long-context clamp. Keep mixed five-minute/one-hour write allocation and short/all-long/mixed long-context allocation with original fixed oracles. Real parser/refusal/bounds alarms are knowingly lost without equivalent replacement; retained tests do not validate provider rates, actual billing or request threshold classification. Preserve the archived usage-scan-append source-freeze receipt without transferring historical execution/performance credit. No fresh runtime result is claimed.", + "Audit3128 scoped coverage reduction: Audit3121 removes VoiceDictationLanguageSetting.test.tsx (Spanish-to-es callback and unsupported-model Korean option presence) and voice-speech-model-rows.test.ts (provider/language-order/retired-on-disk list policy); these real narrow alarms are deliberately retired without replacement. VoiceSpeechModelSection per-model concurrent deletion remains unchanged. Audit3122 removes the direct absolute/parent-segment sparse-directory exception-type sample and the five-row IPC sparse-preset error-code/non-save table (two authored/six expanded cases). Those real typed-validation and IPC refusal/error-code alarms are deliberately retired. Keep both real symlink/junction floating-directory trust cases and the minimal RPC structured validation-reason serialization case. The RPC case does not replace IPC save/refusal behavior, typed normalization for both path inputs, actual Git sparse checkout or rendered localization. No equivalent replacement or fresh test/runtime/platform/remote/performance validation is claimed. Audit3124 removes ExperimentalPane.test.tsx (ordinary Pet switch callback), WorkspaceDirectorySetting.test.tsx (padded absolute saved-path normalization/no-write blur), and repository-search.perf.test.ts (navigation/selected-pane builder-call policy, fixture cardinality/nonempty entries and renamed project keyword inclusion). These real narrow alarms are retired without replacement; RepositoryPane.search.test.tsx search-to-sparse-editor request remains unchanged. Audit3129: resolver Windows case-insensitive and POSIX case-sensitive configured-env composition plus mocked-base launch-preflight Windows composition are intentionally no longer checked by the two deleted suites. No equivalent coverage claimed. Keep runtime-structured-agent-host-env.test.ts unchanged: configured Windows Claude-home account identity and host-account precedence remain checked." ], "demotionRule": "Keep experimental if any direction skips or fails; do not extend timeouts or retry to green." }, diff --git a/config/scripts/build/mobile-web-app/build-mobile-web-app-bundle.mjs b/config/scripts/build/mobile-web-app/build-mobile-web-app-bundle.mjs index f5e4c5d7ae7..605d2ac7816 100644 --- a/config/scripts/build/mobile-web-app/build-mobile-web-app-bundle.mjs +++ b/config/scripts/build/mobile-web-app/build-mobile-web-app-bundle.mjs @@ -578,7 +578,9 @@ export function resolveMobileWebPageRoutes(routeKeys, declared = MOBILE_WEB_PAGE export async function buildMobileWebAppBundle({ appDir, outDir = defaultOutDir, - pageRoutes = MOBILE_WEB_PAGE_ROUTES + pageRoutes = MOBILE_WEB_PAGE_ROUTES, + // Only the shipped tree pays for brotli-11; tests and verify's scratch builds read raw assets. + brotli = false } = {}) { const [ desktopVersion, @@ -624,7 +626,8 @@ export async function buildMobileWebAppBundle({ written: [indexAsset, ...written], desktopVersion, protocolWindow, - routes: resolveMobileWebPageRoutes(routeKeys, pageRoutes) + routes: resolveMobileWebPageRoutes(routeKeys, pageRoutes), + brotli }) return { manifest, @@ -649,7 +652,7 @@ if (isDirectInvocation(import.meta.url, process.argv[1])) { } try { const { manifest, outDir, routeKeys, entryStaticBytes, chunkCount } = - await buildMobileWebAppBundle() + await buildMobileWebAppBundle({ brotli: true }) console.log( `[build-mobile-web-app-bundle] OK — ${String(routeKeys.length)} route(s), ` + `${String(chunkCount)} chunk(s), ${String(entryStaticBytes)} bytes before the first route, ` + diff --git a/config/scripts/build/mobile-web-app/mobile-web-bundle-fixture-tree.mjs b/config/scripts/build/mobile-web-app/mobile-web-bundle-fixture-tree.mjs index 18070c95660..71903291664 100644 --- a/config/scripts/build/mobile-web-app/mobile-web-bundle-fixture-tree.mjs +++ b/config/scripts/build/mobile-web-app/mobile-web-bundle-fixture-tree.mjs @@ -15,7 +15,11 @@ const PIXEL_PNG = Buffer.from( 'base64' ) -export async function writeMobileWebBundleFixtureTree({ outDir, body = 'fixture' }) { +export async function writeMobileWebBundleFixtureTree({ + outDir, + body = 'fixture', + brotli = false +}) { const [desktopVersion, protocolWindow] = await Promise.all([ readDesktopVersion(), readProtocolWindow() @@ -49,6 +53,7 @@ export async function writeMobileWebBundleFixtureTree({ outDir, body = 'fixture' image ], desktopVersion, - protocolWindow + protocolWindow, + brotli }) } diff --git a/config/scripts/build/mobile-web-app/mobile-web-bundle-manifest.mjs b/config/scripts/build/mobile-web-app/mobile-web-bundle-manifest.mjs index 60f9d55d091..4504d345fd4 100644 --- a/config/scripts/build/mobile-web-app/mobile-web-bundle-manifest.mjs +++ b/config/scripts/build/mobile-web-app/mobile-web-bundle-manifest.mjs @@ -3,10 +3,13 @@ import { readFile, mkdir, rm, writeFile } from 'node:fs/promises' import { createRequire } from 'node:module' import { join } from 'node:path' import { fileURLToPath } from 'node:url' +import { promisify } from 'node:util' +import { brotliCompress, constants as zlibConstants } from 'node:zlib' const { darkBuildMarkers } = createRequire(import.meta.url)('../../../dark-build-switches.cjs') const projectDir = fileURLToPath(new URL('../../../..', import.meta.url)) +const brotliCompressAsync = promisify(brotliCompress) export const MOBILE_WEB_BUNDLE_SCHEMA_VERSION = 1 export const MOBILE_WEB_BUNDLE_ENTRYPOINT = 'index.html' @@ -101,6 +104,31 @@ export async function readDesktopVersion() { return packageJson.version } +// Mirrors mobileWebBundleBrotliPath in src/shared/mobile-web-bundle/manifest-contract.ts. +export function mobileWebBundleBrotliPath(assetPath) { + return `${assetPath}.br` +} + +// Largest first so the slowest file starts first on zlib's thread pool; keep only copies that shrink. +async function compressWithBrotli(written) { + const largestFirst = [...written].sort((left, right) => right.byteLength - left.byteLength) + const copies = await Promise.all( + largestFirst.map((asset) => + brotliCompressAsync(asset.bytes, { + params: { + [zlibConstants.BROTLI_PARAM_QUALITY]: zlibConstants.BROTLI_MAX_QUALITY, + [zlibConstants.BROTLI_PARAM_SIZE_HINT]: asset.byteLength + } + }) + ) + ) + return new Map( + largestFirst.flatMap((asset, index) => + copies[index].byteLength < asset.byteLength ? [[asset.path, copies[index]]] : [] + ) + ) +} + // Single manifest producer keeps builder/contract/packaging guard shape aligned. export async function writeMobileWebBundleTree({ outDir, @@ -109,10 +137,22 @@ export async function writeMobileWebBundleTree({ protocolWindow, // Empty route list makes shell render every screen natively. routes = [], + // Whole-file brotli copies for phones that inflate natively. + brotli = false, env = process.env }) { + const brotliCopies = brotli ? await compressWithBrotli(written) : new Map() const assets = written - .map(({ path, sha256, byteLength, contentType }) => ({ path, sha256, byteLength, contentType })) + .map(({ path, sha256, byteLength, contentType }) => { + const copy = brotliCopies.get(path) + return { + path, + sha256, + byteLength, + contentType, + ...(copy === undefined ? {} : { brotliByteLength: copy.byteLength }) + } + }) .sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0)) const manifest = { schemaVersion: MOBILE_WEB_BUNDLE_SCHEMA_VERSION, @@ -133,6 +173,9 @@ export async function writeMobileWebBundleTree({ for (const asset of written) { await writeFile(join(outDir, asset.path), asset.bytes) } + for (const [path, copy] of brotliCopies) { + await writeFile(join(outDir, mobileWebBundleBrotliPath(path)), copy) + } await writeFile(join(outDir, 'manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`, 'utf8') // Keep switch metadata outside manifest so compiled page bytes/buildId remain unchanged. for (const marker of darkBuildMarkers(env)) { diff --git a/config/scripts/ci/sharding/ci-shard-timings.json b/config/scripts/ci/sharding/ci-shard-timings.json index 83440c55a25..4fb29f3f0fd 100644 --- a/config/scripts/ci/sharding/ci-shard-timings.json +++ b/config/scripts/ci/sharding/ci-shard-timings.json @@ -4641,9 +4641,9 @@ "src/renderer/src/components/JiraIssueWorkspace.optimistic-edit.test.tsx": 2340, "src/renderer/src/components/JiraIssueWorkspace.status-queue.test.tsx": 1852, "src/renderer/src/components/JiraIssueWorkspace.transition-source.test.tsx": 1843, - "src/renderer/src/components/NativeChatRemoteResume.test.tsx": 8774, - "src/renderer/src/components/NativeChatResumeOnRestartGroups.selection.test.tsx": 2633, - "src/renderer/src/components/NativeChatResumeOnRestartGroups.test.tsx": 3472, + "src/renderer/src/components/native-chat-recovery/integration/NativeChatRemoteResume.test.tsx": 8774, + "src/renderer/src/components/native-chat-recovery/tree/NativeChatResumeOnRestartGroups.selection.test.tsx": 2633, + "src/renderer/src/components/native-chat-recovery/tree/NativeChatResumeOnRestartGroups.test.tsx": 3472, "src/renderer/src/components/NewWorkspaceComposerCard.quick-file-drop.test.tsx": 4557, "src/renderer/src/components/NewWorkspaceComposerCard.set-location.test.tsx": 1301, "src/renderer/src/components/QuickOpen.selection.test.tsx": 1046, @@ -5135,15 +5135,15 @@ "src/renderer/src/components/mobile/WindowsFirewallNotice.test.tsx": 691, "src/renderer/src/components/mobile/paired-mobile-devices.test.ts": 192, "src/renderer/src/components/mobile/use-mobile-page-paired-devices.test.ts": 475, - "src/renderer/src/components/native-chat-launch-resume-decision.test.tsx": 1844, - "src/renderer/src/components/native-chat-resume-bulk-progress.test.tsx": 3261, - "src/renderer/src/components/native-chat-resume-failed-chat-watch.test.ts": 1954, - "src/renderer/src/components/native-chat-resume-failure-guidance.test.ts": 240, - "src/renderer/src/components/native-chat-resume-finished-run.test.tsx": 4173, - "src/renderer/src/components/native-chat-resume-on-restart-grouping.test.ts": 117, - "src/renderer/src/components/native-chat-resume-result-view.test.tsx": 2723, - "src/renderer/src/components/native-chat-resume-run.test.ts": 65, - "src/renderer/src/components/native-chat-resume-skipped-progress.test.tsx": 3009, + "src/renderer/src/components/native-chat-recovery/offers/native-chat-launch-resume-decision.test.tsx": 1844, + "src/renderer/src/components/native-chat-recovery/integration/native-chat-resume-bulk-progress.test.tsx": 3261, + "src/renderer/src/components/native-chat-recovery/offers/native-chat-resume-failed-chat-watch.test.ts": 1954, + "src/renderer/src/components/native-chat-recovery/failures/native-chat-resume-failure-guidance.test.ts": 240, + "src/renderer/src/components/native-chat-recovery/integration/native-chat-resume-finished-run.test.tsx": 4173, + "src/renderer/src/components/native-chat-recovery/tree/native-chat-resume-on-restart-grouping.test.ts": 117, + "src/renderer/src/components/native-chat-recovery/integration/native-chat-resume-result-view.test.tsx": 2723, + "src/renderer/src/components/native-chat-recovery/runs/native-chat-resume-run.test.ts": 65, + "src/renderer/src/components/native-chat-recovery/integration/native-chat-resume-skipped-progress.test.tsx": 3009, "src/renderer/src/components/native-chat/messages/rows/NativeChatAgentMessageCard.test.tsx": 3700, "src/renderer/src/components/native-chat/tools/tasks/NativeChatBackgroundTaskRun.test.tsx": 242, "src/renderer/src/components/native-chat/tools/tasks/NativeChatBackgroundTasksStatus.test.tsx": 782, @@ -5155,7 +5155,7 @@ "src/renderer/src/components/native-chat/attachments/chips/NativeChatImageAttachmentPreview.test.tsx": 2818, "src/renderer/src/components/native-chat/media/visuals/NativeChatInlineVisual.link-placement.test.tsx": 2477, "src/renderer/src/components/native-chat/prompts/NativeChatInteractiveCard.test.tsx": 2881, - "src/renderer/src/components/native-chat/usage/NativeChatInterruptedContinue.test.tsx": 2719, + "src/renderer/src/components/native-chat/viewport/scroll/NativeChatJumpToLatest.test.tsx": 477, "src/renderer/src/components/native-chat/markdown/code/NativeChatMarkdown.code-wrap.test.tsx": 3123, "src/renderer/src/components/native-chat/media/images/NativeChatMarkdown.image-preview.test.tsx": 4346, @@ -5802,9 +5802,6 @@ "src/renderer/src/components/terminal-pane/layout/close/RunningTerminalCloseDialog.test.tsx": 2303, "src/renderer/src/components/terminal-pane/session-fork/TerminalAgentSessionForkDialog.test.tsx": 266, "src/renderer/src/components/terminal-pane/titles/TerminalPaneHeaderOverlay.test.tsx": 1104, - "src/renderer/src/components/terminal-pane/pty-transport/remote-runtime/recovery/TerminalPaneRecoveryPortals.test.tsx": 670, - "src/renderer/src/components/terminal-pane/pty-transport/remote-runtime/recovery/TerminalRemoteRuntimeReconnectBanner.test.tsx": 812, - "src/renderer/src/components/terminal-pane/pty-transport/recovery/ssh/TerminalSshReconnectOverlay.test.tsx": 2354, "src/renderer/src/components/terminal-pane/account-restart/detached/account-restart-queue-progress.test.ts": 1988, "src/renderer/src/components/terminal-pane/agent-completion/agent-completion-coordinator-attention-dispatch.test.ts": 301, "src/renderer/src/components/terminal-pane/agent-completion/agent-completion-coordinator-completion-replay-guard.test.ts": 318, @@ -5897,7 +5894,6 @@ "src/renderer/src/components/terminal-pane/pty-connection/spawn/main-pane-start-wait.test.ts": 76, "src/renderer/src/components/terminal-pane/pty-connection/serialization/pane-serializer-register.test.ts": 123, "src/renderer/src/components/terminal-pane/pty-connection/host-ownership/pty-kitty-file-reader.test.ts": 297, - "src/renderer/src/components/terminal-pane/pty-connection/startup/saved-resume-cancellation.test.ts": 1882, "src/renderer/src/components/terminal-pane/pty-connection/output-restore/snapshot-covered-terminal-queries.test.ts": 150, "src/renderer/src/components/terminal-pane/pty-connection/output/terminal-input-echo-delay.test.ts": 132, "src/renderer/src/components/terminal-pane/pty-connection/host-ownership/unserved-ssh-pane-bridge.test.ts": 70, @@ -5925,7 +5921,6 @@ "src/renderer/src/components/terminal-pane/pty-transport/ipc/pty-transport-pi-coalesce.test.ts": 683, "src/renderer/src/components/terminal-pane/pty-transport/ipc/pty-transport-reattach-admission.test.ts": 1636, "src/renderer/src/components/terminal-pane/pty-transport/ipc/pty-transport-recycled-pty-incarnation.test.ts": 787, - "src/renderer/src/components/terminal-pane/pty-transport/ipc/pty-transport-spawn-errors.test.ts": 1218, "src/renderer/src/components/terminal-pane/input/focus/regular-terminal-focus-ownership.test.ts": 208, "src/renderer/src/components/terminal-pane/pty-connection/output-restore/remote-hidden-output-restore-outcomes.test.ts": 2794, "src/renderer/src/components/terminal-pane/pty-connection/output-restore/remote-hidden-output-restore-unavailable-banner.repro.test.ts": 2355, @@ -5992,7 +5987,6 @@ "src/renderer/src/components/terminal-pane/emulation/terminal-capability-replies.test.ts": 131, "src/renderer/src/components/terminal-pane/clipboard/terminal-clipboard-paste.test.ts": 76, "src/renderer/src/components/terminal-pane/parking/retention/terminal-cold-park-timer-rearm.test.tsx": 701, - "src/renderer/src/components/terminal-pane/pty-transport/delivery/terminal-delivery-watchdog.test.ts": 111, "src/renderer/src/components/terminal-pane/input/file-drop/terminal-drop-handler.test.ts": 638, "src/renderer/src/components/terminal-pane/input/file-drop/terminal-drop-image-path.test.ts": 61, "src/renderer/src/components/terminal-pane/input/file-drop/terminal-drop-internal-handler.test.ts": 800, @@ -6265,8 +6259,8 @@ "src/renderer/src/hooks/useMacTccAttributionSeveredNotice.test.tsx": 2358, "src/renderer/src/hooks/useMetadataListRequest.test.tsx": 266, "src/renderer/src/hooks/useModalReturnFocus.test.tsx": 1938, - "src/renderer/src/hooks/usePrimarySelectionPaste.terminal-native-paste.test.tsx": 388, - "src/renderer/src/hooks/usePrimarySelectionPaste.test.tsx": 411, + "src/renderer/src/hooks/clipboard/primary-selection/usePrimarySelectionPaste.terminal-native-paste.test.tsx": 388, + "src/renderer/src/hooks/clipboard/primary-selection/usePrimarySelectionPaste.test.tsx": 411, "src/renderer/src/hooks/useReadOnScreenAttention.away.test.ts": 2420, "src/renderer/src/hooks/useReadOnScreenAttention.clock-skew.test.ts": 2093, "src/renderer/src/hooks/useReadOnScreenAttention.floating-panel.test.ts": 2157, @@ -6340,9 +6334,9 @@ "src/renderer/src/lib/agent-session/resume/history/ai-vault-session-resume-preparation.test.ts": 408, "src/renderer/src/lib/agent-session/history/title-sync/ai-vault-tab-title-sync-inputs.test.ts": 476, "src/renderer/src/lib/agent-session/history/title-sync/ai-vault-tab-title-sync.test.ts": 925, - "src/renderer/src/lib/app-command-dispatch.test.ts": 64, - "src/renderer/src/lib/app-keyboard-router.test.ts": 187, - "src/renderer/src/lib/app-menu-paste.test.ts": 229, + "src/renderer/src/lib/app-commands/app-command-dispatch.test.ts": 64, + "src/renderer/src/lib/keyboard/app-keyboard-router.test.ts": 187, + "src/renderer/src/lib/clipboard/menu/app-menu-paste.test.ts": 229, "src/renderer/src/lib/automation-session-reuse.test.ts": 70, "src/renderer/src/lib/automation-terminal-ownership.test.ts": 1795, "src/renderer/src/lib/bounded-generation-map.test.ts": 35, @@ -6358,7 +6352,7 @@ "src/renderer/src/lib/source-control/checks/check-run-details-log-normalization.test.ts": 75, "src/renderer/src/lib/claude-account-switch-sessions.test.ts": 782, "src/renderer/src/lib/claude-session-account-move.test.ts": 1816, - "src/renderer/src/lib/clipboard-image-copy.test.ts": 243, + "src/renderer/src/lib/clipboard/images/clipboard-image-copy.test.ts": 243, "src/renderer/src/lib/cmd-j-host-qualified-candidate-ownership.test.ts": 526, "src/renderer/src/lib/cmd-j-linear-issue-intent.test.ts": 42, "src/renderer/src/lib/code-host-error-copy.test.ts": 297, @@ -6367,8 +6361,7 @@ "src/renderer/src/lib/agent-session/accounts/codex/codex-session-restart.test.ts": 2039, "src/renderer/src/lib/codex-stale-pane-account-identity.test.ts": 1823, "src/renderer/src/lib/codex-stale-pane-sweep.test.ts": 1817, - "src/renderer/src/lib/comment-body-line-count.test.ts": 84, - "src/renderer/src/lib/comment-body-submit-state.test.ts": 55, + "src/renderer/src/lib/source-control/review/comments/comment-body-submit-state.test.ts": 55, "src/renderer/src/lib/composer-submit-cancellation.test.ts": 41, "src/renderer/src/lib/workspace/ownership/connection-context.test.ts": 1817, "src/renderer/src/lib/crash-diagnostics.test.ts": 176, @@ -6385,15 +6378,14 @@ "src/renderer/src/runtime/hosts/platform/execution-host-facts.test.ts": 1796, "src/renderer/src/lib/feature-education-telemetry.test.ts": 52, "src/renderer/src/lib/feedback-image-attachments.test.ts": 557, - "src/renderer/src/lib/browser/documents/file-preview-link-open.test.ts": 2311, "src/renderer/src/lib/file-search-selection.test.ts": 51, "src/renderer/src/lib/find-query-bounds.test.ts": 45, "src/renderer/src/lib/finished-agent-resume-resurrection.test.ts": 1872, "src/renderer/src/lib/focus-terminal-tab-surface.split-focus.test.ts": 331, "src/renderer/src/lib/foreground-terminal-tabs.test.ts": 55, - "src/renderer/src/lib/github-links.test.ts": 76, - "src/renderer/src/lib/github-source-runtime-context.test.ts": 62, - "src/renderer/src/lib/gitlab-links.test.ts": 56, + "src/shared/new-workspace/github-links.test.ts": 76, + "src/renderer/src/lib/work-items/github/github-source-runtime-context.test.ts": 62, + "src/shared/new-workspace/gitlab-links.test.ts": 56, "src/renderer/src/lib/hook-command-delayed-delivery.test.ts": 1904, "src/renderer/src/lib/host-chat-program.test.ts": 2419, "src/renderer/src/lib/host-mirror-handle-gap-drain.test.ts": 1706, @@ -6401,11 +6393,10 @@ "src/renderer/src/lib/host-mirrored-pane-resume-replay.test.ts": 1814, "src/renderer/src/lib/browser/navigation/http-link-completion.test.ts": 1760, "src/renderer/src/lib/browser/navigation/http-link-routing-store.test.ts": 2211, - "src/renderer/src/lib/image-blob-png.test.ts": 253, - "src/renderer/src/lib/ime-composition-keyboard-event.test.ts": 274, - "src/renderer/src/lib/ime-text-field.test.tsx": 739, + "src/renderer/src/lib/text-input/ime/ime-composition-keyboard-event.test.ts": 274, + "src/renderer/src/lib/text-input/ime/ime-text-field.test.tsx": 739, "src/renderer/src/lib/keyboard-layout/layout-base-character.test.ts": 48, - "src/renderer/src/lib/large-text-control-paste.test.ts": 220, + "src/renderer/src/lib/text-input/paste/large-text-control-paste.test.ts": 220, "src/renderer/src/lib/agent-session/launch/background/launch-agent-background-session-launch-parity.test.ts": 2792, "src/renderer/src/lib/agent-session/launch/background/launch-agent-background-session.test.ts": 1040, "src/renderer/src/lib/agent-session/launch/new-tab/launch-agent-in-new-tab-main-surface.test.ts": 1821, @@ -6422,11 +6413,10 @@ "src/renderer/src/lib/lazy-with-retry.never-landed-reload.test.ts": 194, "src/renderer/src/lib/lazy-with-retry.preload.test.tsx": 208, "src/renderer/src/lib/lazy-with-retry.test.ts": 282, - "src/renderer/src/lib/linear-board-drag-payload.test.ts": 58, - "src/renderer/src/lib/work-items/linear/linear-issue-context-snapshot.test.ts": 55, + "src/renderer/src/lib/work-items/linear/linear-board-drag-payload.test.ts": 58, "src/renderer/src/lib/work-items/linear/linear-issue-workspace-attachment.test.ts": 99, "src/renderer/src/lib/link-source-local-open-owner.test.ts": 433, - "src/renderer/src/lib/linked-work-item-context.test.ts": 64, + "src/renderer/src/lib/work-items/linked-work-item-context.test.ts": 64, "src/renderer/src/lib/editor/monaco/monaco-editor-gutter-disposal.test.ts": 295, "src/renderer/src/lib/monaco-languages/markdown-heading-outline.test.ts": 70, "src/renderer/src/lib/monaco-languages/monarch-embed-entry-recursion.test.ts": 697, @@ -6510,14 +6500,14 @@ "src/renderer/src/lib/pane-manager/xterm-write-callback-guard.test.ts": 53, "src/renderer/src/lib/pane-spawn-deferral-drain.test.ts": 61, "src/renderer/src/lib/parked-terminal-host-hydration.test.ts": 274, - "src/renderer/src/lib/paste-payload-metadata.test.ts": 53, + "src/renderer/src/lib/text-input/paste/paste-payload-metadata.test.ts": 53, "src/renderer/src/lib/path-head-elision.test.ts": 53, "src/renderer/src/lib/path.test.ts": 55, "src/renderer/src/lib/plugin-command-keybindings.test.ts": 1909, "src/renderer/src/lib/plugin-command-replacements.test.ts": 95, "src/renderer/src/lib/plugin-icons/plugin-icon-svg.test.ts": 231, "src/renderer/src/lib/plugin-monaco-token-rules.test.ts": 51, - "src/renderer/src/lib/pr-bot-author-overrides.test.ts": 349, + "src/renderer/src/lib/source-control/review/pr-bot-author-overrides.test.ts": 349, "src/renderer/src/lib/source-control/review/comments/pr-comment-thread-resolution.test.ts": 48, "src/renderer/src/lib/project-clone-url-prefill.test.ts": 61, "src/renderer/src/lib/project-host-workspace-target.jira-mapping.test.ts": 76, @@ -6579,8 +6569,8 @@ "src/renderer/src/lib/task-provider-error-copy.test.ts": 363, "src/renderer/src/lib/terminal-app-theme.test.ts": 301, "src/renderer/src/lib/terminal-input-activity-coalescing.test.ts": 43, - "src/renderer/src/lib/text-control-paste-ownership.test.ts": 217, - "src/renderer/src/lib/text-control-paste.test.ts": 222, + "src/renderer/src/lib/text-input/paste/text-control-paste-ownership.test.ts": 217, + "src/renderer/src/lib/text-input/paste/text-control-paste.test.ts": 222, "src/renderer/src/lib/text-generation-error-copy.test.ts": 466, "src/renderer/src/lib/typing-latency/echo-instrumentation.test.ts": 145, "src/renderer/src/lib/typing-latency/renderer-long-tasks.test.ts": 75, @@ -6836,7 +6826,6 @@ "src/renderer/src/store/slices/editor/git/editor-git-status-reconciliation.test.ts": 870, "src/renderer/src/store/slices/editor/tabs/editor-layout-owner-records.test.ts": 2006, "src/renderer/src/store/slices/editor/markdown/editor-markdown-link-activation.test.ts": 953, - "src/renderer/src/store/slices/editor/markdown/editor-markdown-view-state.test.ts": 801, "src/renderer/src/store/slices/editor/git/editor-open-diff.test.ts": 1987, "src/renderer/src/store/slices/editor/tabs/editor-open-link-placement.test.ts": 1987, "src/renderer/src/store/slices/editor/tabs/editor-preview-tab-setting.test.ts": 1952, diff --git a/config/scripts/desktop-distribution/runtime-verification/verify-packaged-mobile-web-bundle.cjs b/config/scripts/desktop-distribution/runtime-verification/verify-packaged-mobile-web-bundle.cjs index fb2a9b14615..769e1c90235 100644 --- a/config/scripts/desktop-distribution/runtime-verification/verify-packaged-mobile-web-bundle.cjs +++ b/config/scripts/desktop-distribution/runtime-verification/verify-packaged-mobile-web-bundle.cjs @@ -1,6 +1,7 @@ const { createHash } = require('node:crypto') const { readFileSync, readdirSync, statSync } = require('node:fs') const { join, resolve } = require('node:path') +const { brotliDecompressSync } = require('node:zlib') const { darkBuildMarkerFileNames } = require('../../../dark-build-switches.cjs') @@ -98,6 +99,12 @@ function parseManifest(bundleDir) { if (typeof asset.contentType !== 'string' || asset.contentType.length === 0) { throw failure(`manifest asset ${asset.path} has no contentType`) } + if (asset.brotliByteLength !== undefined) { + assertInteger(asset.brotliByteLength, `assets[${asset.path}].brotliByteLength`) + if (asset.brotliByteLength >= asset.byteLength) { + throw failure(`manifest asset ${asset.path} declares a brotli copy no smaller than itself`) + } + } } if (!manifest.assets.some((asset) => asset.path === manifest.entrypoint)) { throw failure(`manifest entrypoint ${manifest.entrypoint} is not one of its assets`) @@ -132,13 +139,33 @@ function listBundleFiles(directory, prefix = '') { return found } +// Mirrors mobileWebBundleBrotliPath in src/shared/mobile-web-bundle/manifest-contract.ts. +function brotliPath(asset) { + return `${asset.path}.br` +} + +function brotliCopies(manifest) { + return manifest.assets.filter((asset) => asset.brotliByteLength !== undefined) +} + +function statSize(bundleDir, path) { + try { + return statSync(join(bundleDir, path)).size + } catch (error) { + throw failure( + `manifest lists ${path}, which is missing from ${bundleDir} (${error.code ?? error.message}). ${REMEDY}` + ) + } +} + // Reject unlisted on-disk assets: content-addressed stale files survive interrupted builds and would ship unreachable/unverified in asar. function assertNoUnlistedFiles(bundleDir, manifest) { // A dark build's marker is accounted for here; packaging refuses it through assertNotDarkBuild. const listed = new Set([ 'manifest.json', ...darkBuildMarkerFileNames(), - ...manifest.assets.map((asset) => asset.path) + ...manifest.assets.map((asset) => asset.path), + ...brotliCopies(manifest).map((asset) => brotliPath(asset)) ]) const strays = listBundleFiles(bundleDir).filter((path) => !listed.has(path)) if (strays.length > 0) { @@ -155,14 +182,7 @@ function assertMobileWebBundleBuilt(bundleDir = MOBILE_WEB_BUNDLE_DIR) { assertNoUnlistedFiles(bundleDir, manifest) for (const asset of manifest.assets) { const assetPath = join(bundleDir, asset.path) - let size - try { - size = statSync(assetPath).size - } catch (error) { - throw failure( - `manifest lists ${asset.path}, which is missing from ${bundleDir} (${error.code ?? error.message}). ${REMEDY}` - ) - } + const size = statSize(bundleDir, asset.path) if (size !== asset.byteLength) { throw failure( `${asset.path} is ${String(size)} bytes on disk, manifest says ${String(asset.byteLength)}. ${REMEDY}` @@ -175,6 +195,20 @@ function assertMobileWebBundleBuilt(bundleDir = MOBILE_WEB_BUNDLE_DIR) { ) } } + // Phones verify only the inflated bytes, so a copy that inflates to anything else fails every download. + for (const asset of brotliCopies(manifest)) { + const path = brotliPath(asset) + const size = statSize(bundleDir, path) + if (size !== asset.brotliByteLength) { + throw failure( + `${path} is ${String(size)} bytes on disk, manifest says ${String(asset.brotliByteLength)}. ${REMEDY}` + ) + } + const inflated = brotliDecompressSync(readFileSync(join(bundleDir, path))) + if (createHash('sha256').update(inflated).digest('hex') !== asset.sha256) { + throw failure(`${path} does not inflate to ${asset.path}. ${REMEDY}`) + } + } console.log( `[verify-packaged-mobile-web-bundle] OK — buildId ${manifest.buildId}, ` + `${String(manifest.assets.length)} asset(s), ${String(manifest.totalBytes)} bytes` diff --git a/config/scripts/desktop-distribution/runtime-verification/verify-packaged-mobile-web-bundle.test.mjs b/config/scripts/desktop-distribution/runtime-verification/verify-packaged-mobile-web-bundle.test.mjs index b745dff2402..e916dfc216a 100644 --- a/config/scripts/desktop-distribution/runtime-verification/verify-packaged-mobile-web-bundle.test.mjs +++ b/config/scripts/desktop-distribution/runtime-verification/verify-packaged-mobile-web-bundle.test.mjs @@ -2,6 +2,7 @@ import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' import { createRequire } from 'node:module' import { tmpdir } from 'node:os' import { join } from 'node:path' +import { brotliCompressSync } from 'node:zlib' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { writeMobileWebBundleFixtureTree } from '../../build/mobile-web-app/mobile-web-bundle-fixture-tree.mjs' @@ -10,11 +11,11 @@ const { assertMobileWebBundleBuilt } = require('./verify-packaged-mobile-web-bun const electronBuilderConfig = require('../../../electron-builder.config.cjs') const { darkBuildMarkers } = require('../../../dark-build-switches.cjs') -async function withBundle(run) { +async function withBundle(run, { brotli = false } = {}) { const scratch = await mkdtemp(join(tmpdir(), 'orca-mobile-web-guard-')) const bundleDir = join(scratch, 'mobile-web') try { - const { manifest } = await writeMobileWebBundleFixtureTree({ outDir: bundleDir }) + const { manifest } = await writeMobileWebBundleFixtureTree({ outDir: bundleDir, brotli }) await run({ bundleDir, manifest }) } finally { await rm(scratch, { recursive: true, force: true }) @@ -65,6 +66,25 @@ describe('assertMobileWebBundleBuilt', () => { }) }) + it('accepts the brotli copies the manifest lists, and fails one that inflates to other bytes', async () => { + await withBundle( + async ({ bundleDir, manifest }) => { + const asset = manifest.assets.find((entry) => entry.brotliByteLength !== undefined) + expect(() => assertMobileWebBundleBuilt(bundleDir)).not.toThrow() + const forged = brotliCompressSync(Buffer.from('not the asset')) + await writeFile(join(bundleDir, `${asset.path}.br`), forged) + await rewriteManifest(bundleDir, (rewritten) => { + rewritten.assets.find((entry) => entry.path === asset.path).brotliByteLength = + forged.byteLength + }) + expect(() => assertMobileWebBundleBuilt(bundleDir)).toThrow( + `${asset.path}.br does not inflate to ${asset.path}` + ) + }, + { brotli: true } + ) + }) + it('refuses an asset path that escapes the bundle directory', async () => { await withBundle(async ({ bundleDir }) => { await rewriteManifest(bundleDir, (manifest) => { diff --git a/config/scripts/static-analysis/ratchets/react-compiler/check-react-compiler-compat-ratchet.mjs b/config/scripts/static-analysis/ratchets/react-compiler/check-react-compiler-compat-ratchet.mjs index 714acf736a6..e4205589eba 100644 --- a/config/scripts/static-analysis/ratchets/react-compiler/check-react-compiler-compat-ratchet.mjs +++ b/config/scripts/static-analysis/ratchets/react-compiler/check-react-compiler-compat-ratchet.mjs @@ -27,6 +27,10 @@ import { renderLiveReadLines, renderStoreReadLines } from './react-compiler-hazards.mjs' +import { + UNCOMPILED_LIST, + readListedFiles +} from '../../../../build-plugins/react-compiler-uncompiled-files.mjs' // Runs React Compiler (analysis only, nothing is written) over the renderer so code that compiles // today keeps compiling, and code that would change behavior once compiled can't be added. @@ -34,8 +38,6 @@ import { const SCAN_ROOT = 'src/renderer/src' const BAILOUT_BASELINE = 'config/react-compiler-bailout-baseline.txt' const HAZARD_BASELINE = 'config/react-compiler-hazard-baseline.txt' -// The build leaves these files uncompiled, so their hazards cannot run. -const UNCOMPILED_LIST = 'config/react-compiler-uncompiled-files.txt' const COMMAND = 'pnpm check:react-compiler-compat-ratchet' const NOT_SHIPPED = /(?:^|\/)__(?:mocks|fixtures|tests)__\/|\.(?:test|spec|bench)\.[cm]?tsx?$|[.-]test-|[.-]fixtures?\.tsx?$/ @@ -101,18 +103,31 @@ function addTo(map, key, value) { map.set(key, [...(map.get(key) ?? []), value]) } +// The build leaves these files uncompiled, so their hazards cannot run. function readUncompiledFiles(root) { - const file = path.join(root, UNCOMPILED_LIST) - if (!fs.existsSync(file)) { - return new Set() + return fs.existsSync(path.join(root, UNCOMPILED_LIST)) + ? new Set(readListedFiles(UNCOMPILED_LIST, root)) + : new Set() +} + +// Why: the build matches the exact id, so `\`, `../`, a directory or another case would silently compile. +function isCanonicalFile(root, rel) { + try { + const file = path.join(root, rel) + return ( + !rel.includes('\\') && + path.posix.normalize(rel) === rel && + fs.statSync(file).isFile() && + fs.realpathSync.native(file) === path.join(fs.realpathSync.native(root), rel) + ) + } catch { + return false } - return new Set( - fs - .readFileSync(file, 'utf8') - .split('\n') - .map((line) => line.trim()) - .filter((line) => line && !line.startsWith('#')) - ) +} + +/** Listed paths that name no renderer file; a renamed file would silently compile. */ +export function findStaleUncompiledFiles(uncompiled, exists) { + return [...uncompiled].filter((rel) => !rel.startsWith(`${SCAN_ROOT}/`) || !exists(rel)) } /** Virtualizer hooks plus every hook that returns one, followed through wrappers of wrappers. */ @@ -210,12 +225,14 @@ async function scanFile(rel, text, result) { export async function scanRenderer(root = process.cwd()) { const sources = [...readTrackedSources(root, [SCAN_ROOT], isScannedPath)] + const uncompiled = readUncompiledFiles(root) const result = { files: 0, compiled: 0, diagnostics: new Map(), hazards: new Map(), - uncompiled: readUncompiledFiles(root), + uncompiled, + staleUncompiled: findStaleUncompiledFiles(uncompiled, (rel) => isCanonicalFile(root, rel)), virtualizerHooks: collectVirtualizerHooks(sources), liveReaders: collectLiveReaders(sources) } @@ -341,8 +358,15 @@ export async function main(root = process.cwd(), arg = undefined) { ) return 0 } + for (const rel of scan.staleUncompiled) { + console.error( + `::error::${UNCOMPILED_LIST}: ${rel} is not a renderer file. ` + + 'Point it at the renamed file, or remove it once the file is gone.' + ) + } const touched = listTouchedFiles(root) const ok = [ + scan.staleUncompiled.length === 0, check(root, 'bailout(s)', BAILOUT_BASELINE, bailouts, scan.diagnostics, touched), check(root, 'hazard(s)', HAZARD_BASELINE, hazards, scan.hazards, touched) ].every(Boolean) diff --git a/config/scripts/static-analysis/ratchets/react-compiler/check-react-compiler-compat-ratchet.test.mjs b/config/scripts/static-analysis/ratchets/react-compiler/check-react-compiler-compat-ratchet.test.mjs new file mode 100644 index 00000000000..abd806c4003 --- /dev/null +++ b/config/scripts/static-analysis/ratchets/react-compiler/check-react-compiler-compat-ratchet.test.mjs @@ -0,0 +1,39 @@ +import { describe, expect, it } from 'vitest' +import { parseListedFiles } from '../../../../build-plugins/react-compiler-uncompiled-files.mjs' +import { findStaleUncompiledFiles } from './check-react-compiler-compat-ratchet.mjs' + +describe('uncompiled file list', () => { + it('reads paths from list, count and hazard lines, dropping notes', () => { + const text = [ + '# header', + 'src/renderer/src/a.tsx', + 'src/renderer/src/b.tsx # why it stays uncompiled', + '2 src/renderer/src/c.ts', + '1 src/renderer/src/d.ts#useD:store-read-in-memo-block', + '' + ].join('\n') + expect(parseListedFiles(text)).toEqual([ + 'src/renderer/src/a.tsx', + 'src/renderer/src/b.tsx', + 'src/renderer/src/c.ts', + 'src/renderer/src/d.ts' + ]) + }) + + it('reports listed paths that no longer name a renderer file', () => { + const files = new Set([ + 'src/renderer/src/kept.tsx', + 'src/renderer/src/notification-test-delivery.ts' + ]) + expect( + findStaleUncompiledFiles( + new Set([ + 'src/renderer/src/kept.tsx', + 'src/renderer/src/notification-test-delivery.ts', + 'src/renderer/src/renamed.tsx' + ]), + (rel) => files.has(rel) + ) + ).toEqual(['src/renderer/src/renamed.tsx']) + }) +}) diff --git a/config/scripts/test-runtime/sqlite-admission/vitest-node-runtime-files.mjs b/config/scripts/test-runtime/sqlite-admission/vitest-node-runtime-files.mjs index 7f9ba3b8138..0aebc9e3011 100644 --- a/config/scripts/test-runtime/sqlite-admission/vitest-node-runtime-files.mjs +++ b/config/scripts/test-runtime/sqlite-admission/vitest-node-runtime-files.mjs @@ -36,6 +36,7 @@ const NODE_RUNTIME_INCLUDE = [ // Asserts an observer survives a forced collection; conservative retention would hide a lost one. 'config/scripts/test-runtime/dom/happy-dom-mutation-observer-retention.test.ts', // These reach Node SQLite through a shared helper or a dynamic import, which the scan skips. + 'src/main/agent-hooks/server/testing/providers/codex/server-codex-session-start.test.ts', 'src/main/claude/claude-transcript-catch-up.test.ts', 'src/main/claude/claude-transcript-duration.test.ts', 'tests/e2e/native-chat-startup-option-pickers.unit.test.ts', diff --git a/config/vitest.performance.config.ts b/config/vitest.performance.config.ts index f05c41e95f5..ceb2d73b083 100644 --- a/config/vitest.performance.config.ts +++ b/config/vitest.performance.config.ts @@ -6,7 +6,6 @@ import { nodeRuntimePool } from './scripts/test-runtime/node-worker/vitest-node- const contracts = [ 'src/main/runtime/session-tabs/session-tabs-phone-layout-moves.test.ts', - 'src/renderer/src/components/terminal-pane/pty-connection/startup/saved-resume-cancellation.test.ts', 'src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-terminal-writer-acquire.test.ts', 'src/shared/agent-launch/permissions/tui-agent-permission-choice.test.ts', 'src/main/runtime/rpc/methods/projects/repo.test.ts', diff --git a/docs/STYLEGUIDE.md b/docs/STYLEGUIDE.md index 8befb02e39a..ee8a8c1e177 100644 --- a/docs/STYLEGUIDE.md +++ b/docs/STYLEGUIDE.md @@ -44,7 +44,7 @@ Tokens pair a **surface** with a **foreground** that meets contrast on it; alway - Body letter-spacing is set globally on `body`; don't override per component. - Use Tailwind's scale. Common: 14px body/default buttons, 13px sidebar items and dense rows, 12px sub-text and paths, 11px uppercase meta/captions. - Use `text-2xs` (11px) and `text-3xs` (10px), not `text-[11px]` / `text-[10px]`. -- Category labels: 11px, `font-semibold`, `uppercase`, `tracking-wider` (0.05em). +- Category labels: 11px, `font-semibold`, `uppercase`, `tracking-wider` (0.05em). Settings content uses sentence case without tracking: 14px row labels, `text-settings-detail` (12.5px) help, and `text-settings-heading` (15px) semibold section headings. Settings form rows use about 9px vertical padding (44px minimum), selects are 28px, and the centered content column is at most 720px with at least 32px side gutters. These density rules are scoped to `.settings-view-shell`; shared primitives retain their defaults elsewhere. - Native chat has its own lint-enforced scale and colours: see [native-chat.md](./styleguide/native-chat.md). ## Radius and elevation diff --git a/docs/assets/readme-downloads.svg b/docs/assets/readme-downloads.svg index 968f6560164..5d3b3001552 100644 --- a/docs/assets/readme-downloads.svg +++ b/docs/assets/readme-downloads.svg @@ -1,5 +1,5 @@ - - downloads: 162m + + downloads: 163m @@ -15,7 +15,7 @@ downloads downloads - 162m - 162m + 163m + 163m diff --git a/docs/assets/star-history.png b/docs/assets/star-history.png index 27a763163ca..04839af4982 100644 Binary files a/docs/assets/star-history.png and b/docs/assets/star-history.png differ diff --git a/mobile/modules/orca-gzip-utf8-codec/android/build.gradle b/mobile/modules/orca-gzip-utf8-codec/android/build.gradle index 35667d48346..59674abee85 100644 --- a/mobile/modules/orca-gzip-utf8-codec/android/build.gradle +++ b/mobile/modules/orca-gzip-utf8-codec/android/build.gradle @@ -15,6 +15,8 @@ android { } dependencies { + // Android ships no brotli decoder; Google's pure-Java one, as okhttp-brotli uses. + implementation 'org.brotli:dec:0.1.2' testImplementation 'junit:junit:4.13.2' // The android.jar used by JVM unit tests stubs org.json; the shared vectors are JSON. testImplementation 'org.json:json:20240303' diff --git a/mobile/modules/orca-gzip-utf8-codec/android/src/main/java/expo/modules/orcagziputf8codec/BoundedBrotli.kt b/mobile/modules/orca-gzip-utf8-codec/android/src/main/java/expo/modules/orcagziputf8codec/BoundedBrotli.kt new file mode 100644 index 00000000000..ea7ace90661 --- /dev/null +++ b/mobile/modules/orca-gzip-utf8-codec/android/src/main/java/expo/modules/orcagziputf8codec/BoundedBrotli.kt @@ -0,0 +1,31 @@ +package expo.modules.orcagziputf8codec + +import java.io.ByteArrayInputStream +import java.io.IOException +import org.brotli.dec.BrotliInputStream + +class BoundedBrotliException(message: String) : Exception(message) + +/** Inflate one brotli stream into at most outputLimit bytes; callers pass one byte beyond the length they + * accept and check the result's length and hash, so truncated or overlong input fails there. */ +fun brotliDecompressBounded(input: ByteArray, outputLimit: Int): ByteArray { + if (outputLimit <= 0 || input.isEmpty()) { + throw BoundedBrotliException("nothing to inflate") + } + val output = ByteArray(outputLimit) + var produced = 0 + try { + BrotliInputStream(ByteArrayInputStream(input)).use { stream -> + while (produced < outputLimit) { + val count = stream.read(output, produced, outputLimit - produced) + if (count < 0) { + break + } + produced += count + } + } + } catch (error: IOException) { + throw BoundedBrotliException(error.message ?: "invalid brotli data") + } + return if (produced == outputLimit) output else output.copyOf(produced) +} diff --git a/mobile/modules/orca-gzip-utf8-codec/android/src/main/java/expo/modules/orcagziputf8codec/OrcaGzipUtf8CodecModule.kt b/mobile/modules/orca-gzip-utf8-codec/android/src/main/java/expo/modules/orcagziputf8codec/OrcaGzipUtf8CodecModule.kt index f46cd77562e..e9f8edf4a4c 100644 --- a/mobile/modules/orca-gzip-utf8-codec/android/src/main/java/expo/modules/orcagziputf8codec/OrcaGzipUtf8CodecModule.kt +++ b/mobile/modules/orca-gzip-utf8-codec/android/src/main/java/expo/modules/orcagziputf8codec/OrcaGzipUtf8CodecModule.kt @@ -15,5 +15,10 @@ class OrcaGzipUtf8CodecModule : Module() { Function("decodeUtf8") { input: ByteArray -> decodeStrictUtf8(input) } + + // Async: a whole bundle asset can take tens of ms, too long to hold the JS thread. + AsyncFunction("brotliDecompress") { input: ByteArray, outputLimit: Int -> + brotliDecompressBounded(input, outputLimit) + } } } diff --git a/mobile/modules/orca-gzip-utf8-codec/ios/BoundedBrotli.swift b/mobile/modules/orca-gzip-utf8-codec/ios/BoundedBrotli.swift new file mode 100644 index 00000000000..3e7c3c359e9 --- /dev/null +++ b/mobile/modules/orca-gzip-utf8-codec/ios/BoundedBrotli.swift @@ -0,0 +1,28 @@ +import Compression +import Foundation + +struct BoundedBrotliError: Error, CustomStringConvertible { + let description: String +} + +/// Inflate one brotli stream into at most outputLimit bytes; callers pass one byte beyond the length they +/// accept and check the result's length and hash. Compression returns nothing for a stream that is invalid, +/// truncated or longer than the buffer, so all three throw here. +func brotliDecompressBounded(_ input: Data, outputLimit: Int) throws -> Data { + guard outputLimit > 0, !input.isEmpty else { + throw BoundedBrotliError(description: "nothing to inflate") + } + var output = Data(count: outputLimit) + let produced = input.withUnsafeBytes { (inBytes: UnsafeRawBufferPointer) -> Int in + output.withUnsafeMutableBytes { (outBytes: UnsafeMutableRawBufferPointer) -> Int in + compression_decode_buffer( + outBytes.bindMemory(to: UInt8.self).baseAddress!, outputLimit, + inBytes.bindMemory(to: UInt8.self).baseAddress!, inBytes.count, + nil, COMPRESSION_BROTLI) + } + } + guard produced > 0 else { + throw BoundedBrotliError(description: "not a whole brotli stream within the output limit") + } + return produced == outputLimit ? output : output.prefix(produced) +} diff --git a/mobile/modules/orca-gzip-utf8-codec/ios/OrcaGzipUtf8Codec.podspec b/mobile/modules/orca-gzip-utf8-codec/ios/OrcaGzipUtf8Codec.podspec index 747a17b46d5..61ddefce0c5 100644 --- a/mobile/modules/orca-gzip-utf8-codec/ios/OrcaGzipUtf8Codec.podspec +++ b/mobile/modules/orca-gzip-utf8-codec/ios/OrcaGzipUtf8Codec.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = 'OrcaGzipUtf8Codec' s.version = '0.0.1' - s.summary = 'Bounded gunzip and strict UTF-8 decode' + s.summary = 'Bounded gunzip and brotli inflate, and strict UTF-8 decode' s.description = s.summary s.license = { :type => 'MIT' } s.author = 'Orca' @@ -11,6 +11,6 @@ Pod::Spec.new do |s| s.swift_version = '5.9' s.static_framework = true s.dependency 'ExpoModulesCore' - s.libraries = 'z' + s.libraries = 'z', 'compression' s.source_files = '**/*.swift' end diff --git a/mobile/modules/orca-gzip-utf8-codec/ios/OrcaGzipUtf8CodecModule.swift b/mobile/modules/orca-gzip-utf8-codec/ios/OrcaGzipUtf8CodecModule.swift index a3d5c24c168..d13edb8135a 100644 --- a/mobile/modules/orca-gzip-utf8-codec/ios/OrcaGzipUtf8CodecModule.swift +++ b/mobile/modules/orca-gzip-utf8-codec/ios/OrcaGzipUtf8CodecModule.swift @@ -12,5 +12,10 @@ public class OrcaGzipUtf8CodecModule: Module { Function("decodeUtf8") { (input: Data) -> String? in decodeStrictUtf8(input) } + + // Async: a whole bundle asset can take tens of ms, too long to hold the JS thread. + AsyncFunction("brotliDecompress") { (input: Data, outputLimit: Int) throws -> Data in + try brotliDecompressBounded(input, outputLimit: outputLimit) + } } } diff --git a/mobile/modules/orca-gzip-utf8-codec/src/index.ts b/mobile/modules/orca-gzip-utf8-codec/src/index.ts index 7eea6332bbc..e97477f6637 100644 --- a/mobile/modules/orca-gzip-utf8-codec/src/index.ts +++ b/mobile/modules/orca-gzip-utf8-codec/src/index.ts @@ -5,6 +5,8 @@ export type OrcaGzipUtf8CodecModule = { gunzip(input: Uint8Array, outputLimit: number): Uint8Array /** Well-formed UTF-8 with no leading BOM, decoded; null otherwise. */ decodeUtf8(input: Uint8Array): string | null + /** Inflate one brotli stream into at most outputLimit bytes, off the JS thread; throws on bad input. */ + brotliDecompress(input: Uint8Array, outputLimit: number): Promise> } /** Null in a binary built before the module existed. */ diff --git a/mobile/src/components/MobileStateView.tsx b/mobile/src/components/MobileStateView.tsx index ef692756a90..791e43c4789 100644 --- a/mobile/src/components/MobileStateView.tsx +++ b/mobile/src/components/MobileStateView.tsx @@ -22,6 +22,8 @@ type MobileStateViewProps = { loading?: boolean /** Keeps the content centred above a floating bottom bar. */ bottomInset?: number + /** Full-width content under the message, such as a list of choices. */ + children?: ReactNode } /** Every screen's loading, empty, no-match, error and offline state: icon, title, one sentence, one action. */ @@ -33,7 +35,8 @@ export function MobileStateView({ details, action, loading = false, - bottomInset = 0 + bottomInset = 0, + children }: MobileStateViewProps) { useMobileColorScheme() return ( @@ -49,6 +52,7 @@ export function MobileStateView({ {message ? {message} : null} {details ? : null} {action ? : null} + {children ? {children} : null} ) } @@ -187,6 +191,10 @@ export function MobileNoMatchState({ } const styles = themedStyleSheet(() => ({ + children: { + alignSelf: 'stretch', + marginTop: spacing.md + }, state: { flex: 1, alignItems: 'center', diff --git a/mobile/src/components/NewWorktreeModal.test.tsx b/mobile/src/components/NewWorktreeModal.test.tsx index b44507b5157..6da85abe6b3 100644 --- a/mobile/src/components/NewWorktreeModal.test.tsx +++ b/mobile/src/components/NewWorktreeModal.test.tsx @@ -64,6 +64,7 @@ vi.mock('../host-screen/host-list-queries', () => ({ import '../transport/hosts/host-descriptor-store' import { FakeSession } from '../transport/connection/mobile-endpoint-supervisor-test-fakes' +import { markRpcDeliveryUnknown } from '../transport/rpc/rpc-delivery-ambiguity' import '../../../src/shared/execution-host' import type { ExecutionHostId } from '../../../src/shared/execution-host' @@ -194,6 +195,108 @@ describe('NewWorktreeModal project targets', () => { ).toBe('/srv/orca') }) + it("offers only the folder host's copy of a repo when opened from a folder", async () => { + const serverRepo = { ...repos[0]!, path: '/srv/orca' } + const listing = (listed: unknown[]) => + vi.fn((method: string) => + method === 'repo.list' ? replied({ repos: listed }) : new Promise(() => {}) + ) + + await act(async () => { + renderer = create( + createElement(NewWorktreeModal, { + visible: true, + client: requestsOnly(listing(repos)), + serverClients: new Map([ + ['runtime:env-1', requestsOnly(listing([serverRepo]))] + ]), + hostId: 'host-1', + onCreated: () => {}, + onClose: () => {}, + openExternalUrl: () => {}, + folderLaunch: { + repoId: 'repo-1', + executionHostId: 'runtime:env-1', + name: 'payments', + parentWorkspace: 'folder:f-1', + submit: { begin: () => null, presenting: () => true } + } + }) + ) + }) + await flushUpdates() + + // The desktop's same-id repo is not offered: its store doesn't hold this folder. + expect(pickerItems(renderer, 'Run on').map((item) => item.detail)).toEqual(['/srv/orca']) + expect(runTargetPicker(renderer).props.selectedId).toBe('runtime:env-1') + }) + + async function submitFromFolder(createReply: () => Promise) { + const serverRepo = { ...repos[0]!, path: '/srv/orca' } + const server = requestsOnly( + vi.fn((method: string) => + method === 'repo.list' + ? replied({ repos: [serverRepo] }) + : method === 'worktree.create' + ? createReply() + : Promise.reject(new Error(`unexpected ${method}`)) + ) + ) + const settle = vi.fn() + + await act(async () => { + renderer = create( + createElement(NewWorktreeModal, { + visible: true, + client: requestsOnly(() => new Promise(() => {})), + serverClients: new Map([['runtime:env-1', server]]), + hostId: 'host-1', + onCreated: () => {}, + onClose: () => {}, + openExternalUrl: () => {}, + folderLaunch: { + repoId: 'repo-1', + executionHostId: 'runtime:env-1', + name: 'payments', + parentWorkspace: 'folder:f-1', + submit: { begin: () => ({ settle }), presenting: () => true } + } + }) + ) + }) + await flushUpdates() + const form = renderer.root.find((node) => typeof node.props.onCreate === 'function') + await act(async () => form.props.onCreate()) + await flushUpdates() + + const creates = vi + .mocked(server.sendRequest) + .mock.calls.filter(([method]) => method === 'worktree.create') + return { creates: creates.map(([, params]) => params), settle } + } + + it('creates from a folder attached to that folder, on its host', async () => { + const { creates, settle } = await submitFromFolder(() => + replied({ worktree: { id: 'repo-1::/srv/w/payments', displayName: 'payments' } }) + ) + expect(creates).toEqual([ + expect.objectContaining({ + repo: 'id:repo-1', + parentWorkspace: 'folder:f-1', + parentWorkspaceOrigin: 'manual' + }) + ]) + expect(settle).toHaveBeenCalledWith('created') + }) + + it("doesn't try another name when a folder create's reply is lost", async () => { + const { creates, settle } = await submitFromFolder(() => + Promise.reject(markRpcDeliveryUnknown(new Error('connection lost'))) + ) + expect(creates).toHaveLength(1) + expect(settle).toHaveBeenCalledWith('uncertain') + }) + it('ignores a stale repo list after the client changes', async () => { let resolveOldList: ((value: unknown) => void) | undefined const oldList = new Promise((resolve) => { diff --git a/mobile/src/components/NewWorktreeModal.tsx b/mobile/src/components/NewWorktreeModal.tsx index 11bf01b970f..ccae8181be7 100644 --- a/mobile/src/components/NewWorktreeModal.tsx +++ b/mobile/src/components/NewWorktreeModal.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from 'react' +import { useEffect, useMemo, useRef, useState } from 'react' import { Keyboard } from 'react-native' import { getComposerRepoWorktreeBranches } from '../../../src/shared/composer-branch-selection' import { getRepoExecutionHostId, parseExecutionHostId } from '../../../src/shared/execution-host' @@ -71,14 +71,20 @@ function NewWorktreeModalContent(props: NewWorktreeModalProps) { existingWorktrees, openExternalUrl, onCreated, - onClose + onClose, + folderLaunch } = props const { repos, allRepos, selectedRepo, setSelectedRepo, loading, loadError, retryLoad } = useNewWorkspaceRepositories({ client: desktopClient, serverClients, hostId, - visible + visible, + ...(folderLaunch + ? { + initialRepo: { id: folderLaunch.repoId, executionHostId: folderLaunch.executionHostId } + } + : {}) }) // Route picked-repo operations to its host; desktop settings/accounts stay on desktop. const repoServer = selectedRepo ? serverOf(selectedRepo) : undefined @@ -126,6 +132,14 @@ function NewWorktreeModalContent(props: NewWorktreeModalProps) { worktreeBranches: selectedRepoWorktreeBranches, onError: setError }) + // The folder's name, seeded once per opening; the user's edits stand. + const seededFolderName = useRef(false) + useEffect(() => { + if (folderLaunch && !seededFolderName.current) { + seededFolderName.current = true + composer.setName(folderLaunch.name) + } + }, [composer, folderLaunch]) const selectedRepoIsGit = selectedRepo ? selectedRepo.kind !== 'folder' : true useWorktreeCreateBasePrefetch({ client: visible ? client : null, @@ -179,7 +193,19 @@ function NewWorktreeModalContent(props: NewWorktreeModalProps) { setNote((current) => (current === note ? '' : current)) onCreated(worktreeId, name, warning, repoServer) }, - onClose + onClose, + ...(folderLaunch + ? { + folderLaunch: { + ...folderLaunch.submit, + executionHostId: folderLaunch.executionHostId, + parent: { + parentWorkspace: folderLaunch.parentWorkspace, + parentWorkspaceOrigin: 'manual' as const + } + } + } + : {}) }) const sourceAvailability: SmartModeAvailabilityInput = { diff --git a/mobile/src/components/new-workspace-create-copy.ts b/mobile/src/components/new-workspace-create-copy.ts new file mode 100644 index 00000000000..77a9e3cf7e3 --- /dev/null +++ b/mobile/src/components/new-workspace-create-copy.ts @@ -0,0 +1,22 @@ +import { translate } from '../i18n/mobile-i18n' + +/** A create whose reply was lost, or whose resend was refused: it may exist, so another could be a second one. */ +export function uncertainCreateMessage(inFolder: boolean): string { + return inFolder + ? translate( + 'components.newWorkspaceCreateSubmit.uncertainFolderCreate', + 'This worktree may have been created. Check the folder before creating another.' + ) + : translate( + 'components.newWorkspaceCreateSubmit.uncertainCreate', + 'This workspace may have been created. Check your workspaces before creating another.' + ) +} + +/** The folder screen still owns a create for this repository. */ +export function folderCreateBusyMessage(): string { + return translate( + 'components.newWorkspaceCreateSubmit.folderCreateBusy', + 'A worktree in this repository is still being created or checked. It shows in the folder once it lands.' + ) +} diff --git a/mobile/src/components/new-worktree-modal-types.ts b/mobile/src/components/new-worktree-modal-types.ts index 54fa5d0f4a0..02a7a1e4f9d 100644 --- a/mobile/src/components/new-worktree-modal-types.ts +++ b/mobile/src/components/new-worktree-modal-types.ts @@ -18,6 +18,30 @@ export type MobileWorkspaceRepo = Pick > +/** Opened from a folder's empty state: that repo and name, created attached to the folder. */ +export type NewWorktreeFolderLaunch = { + repoId: string + /** The repo's host, so a same-id copy on another host isn't picked. */ + executionHostId: ExecutionHostId + name: string + /** The folder's workspace key, `folder:`. */ + parentWorkspace: string + submit: NewWorktreeFolderSubmit +} + +/** A folder's create, kept from admission until its outcome is known. */ +export type NewWorktreeFolderSubmission = { + settle: (outcome: 'created' | 'uncertain' | 'failed' | 'cancelled') => void +} + +/** How the folder screen owns its composer's creates. */ +export type NewWorktreeFolderSubmit = { + /** Admits a create for the picked repo before anything is sent; null while another is out for it. */ + begin: (repo: MobileWorkspaceRepo) => NewWorktreeFolderSubmission | null + /** False once the folder screen is left: the create runs on, without haptics. */ + presenting: () => boolean +} + export type NewWorktreeModalProps = { visible: boolean client: RpcClient | null @@ -37,6 +61,7 @@ export type NewWorktreeModalProps = { executionHost?: ExecutionHostId ) => void onClose: () => void + folderLaunch?: NewWorktreeFolderLaunch } export type SetupRunPolicy = 'ask' | 'run-by-default' | 'skip-by-default' diff --git a/mobile/src/components/use-new-workspace-create-submit.ts b/mobile/src/components/use-new-workspace-create-submit.ts index 13f5d3cd165..3ee45a87b10 100644 --- a/mobile/src/components/use-new-workspace-create-submit.ts +++ b/mobile/src/components/use-new-workspace-create-submit.ts @@ -3,7 +3,9 @@ import { useRef, useState, type Dispatch, type SetStateAction } from 'react' import type { PersistedTrustedOrcaHooks } from '../../../src/shared/orca-yaml-hook-types' import type { RetiredNameRegistry } from '../../../src/shared/worktree/retired-name-registry' import type { RpcClient } from '../transport/rpc/rpc-client-contract' +import { folderCreateBusyMessage, uncertainCreateMessage } from './new-workspace-create-copy' import { triggerError, triggerSuccess } from '../platform/haptics' +import type { WorktreeCreateParent } from '../tasks/worktree-create-retry' import { createBlankWorkspace } from '../tasks/blank-workspace-create' import { isMobileTuiAgentEnabled } from '../tasks/mobile-tui-agents' import { @@ -24,7 +26,14 @@ import { type NewWorktreeAgentOption, type NewWorktreeRuntimeSettings } from './new-worktree-agent-selection' -import type { MobileWorkspaceRepo, SetupRunPolicy } from './new-worktree-modal-types' +import type { + MobileWorkspaceRepo, + NewWorktreeFolderSubmission, + NewWorktreeFolderSubmit, + SetupRunPolicy +} from './new-worktree-modal-types' +import { getRepoExecutionHostId, type ExecutionHostId } from '../../../src/shared/execution-host' +import { isRpcDeliveryUnknown } from '../transport/rpc/rpc-delivery-ambiguity' import type { OrcaHookTrustPrompt } from './OrcaHookTrustDrawer' import type { NewWorktreeDrawerView } from './use-new-worktree-drawer-navigation' import { getSuggestedCreatureName } from './worktree-name-suggestion' @@ -65,6 +74,11 @@ export function useNewWorkspaceCreateSubmit(args: { setError: Dispatch> onCreated: (worktreeId: string, name: string, warning?: string) => void onClose: () => void + /** Opened from a folder: creates on that folder's host, attached to it, owned by the folder screen. */ + folderLaunch?: NewWorktreeFolderSubmit & { + executionHostId: ExecutionHostId + parent: WorktreeCreateParent + } }): { creating: boolean setupTrustPrompt: OrcaHookTrustPrompt | null @@ -86,7 +100,20 @@ export function useNewWorkspaceCreateSubmit(args: { createInFlightRef.current = true setCreating(true) args.setError('') + const folder = args.folderLaunch + // Admitted for the repo actually picked, before anything is sent; the folder screen owns it until it settles. + const admission = folder + ? getRepoExecutionHostId(selectedRepo) === folder.executionHostId + ? folder.begin(selectedRepo) + : null + : undefined + const presenting = (): boolean => folder?.presenting() ?? true + let outcome: Parameters[0] = 'cancelled' try { + if (admission === null) { + args.setError(folderCreateBusyMessage()) + return + } if (args.sshGate.requiresConnection) { args.setError(`Connect ${selectedRepo.displayName} before creating a workspace.`) return @@ -178,7 +205,8 @@ export function useNewWorkspaceCreateSubmit(args: { agentLaunchSupported: args.getAgentLaunchSupport(), branchIntentSupported: args.getBranchIntentSupport( selection.kind === 'branch' && selection.reuse ? 'open-existing-v2' : 'create-new' - ) + ), + parent: folder?.parent }) : await createBlankWorkspace({ client, @@ -189,20 +217,43 @@ export function useNewWorkspaceCreateSubmit(args: { comment: trimmedNote, setupDecision, worktreeCreateIdempotency: args.getWorktreeCreateCutoverSupport(), - agentLaunchSupported: args.getAgentLaunchSupport() + agentLaunchSupported: args.getAgentLaunchSupport(), + parent: folder?.parent }) if ('error' in result) { - triggerError() + // Refused on a resend after a lost reply: the first send may have created it. + outcome = result.replayed ? 'uncertain' : 'failed' + if (outcome === 'uncertain') { + // Not a failure to retry: creating again could make a second one. + args.setError(uncertainCreateMessage(folder !== undefined)) + return + } + if (presenting()) { + triggerError() + } args.setError(result.error) return } - triggerSuccess() + outcome = 'created' + if (presenting()) { + triggerSuccess() + } args.onClose() args.onCreated(result.worktreeId, result.name, result.warning) } catch (error) { - triggerError() + // A lost reply may still have created it. + outcome = isRpcDeliveryUnknown(error) ? 'uncertain' : 'failed' + if (folder && outcome === 'uncertain') { + // The folder screen refreshes and holds it until its list shows it; sending again could create a second one. + args.setError(uncertainCreateMessage(true)) + return + } + if (presenting()) { + triggerError() + } args.setError(error instanceof Error ? error.message : 'Failed to create workspace') } finally { + admission?.settle(outcome) createInFlightRef.current = false setCreating(false) } diff --git a/mobile/src/components/use-new-workspace-repositories.test.ts b/mobile/src/components/use-new-workspace-repositories.test.ts new file mode 100644 index 00000000000..14196e03e81 --- /dev/null +++ b/mobile/src/components/use-new-workspace-repositories.test.ts @@ -0,0 +1,74 @@ +import { act } from 'react-test-renderer' +import { afterEach, describe, expect, it, vi } from 'vitest' +import type { ExecutionHostId } from '../../../src/shared/execution-host' +import { FakeSession } from '../transport/connection/mobile-endpoint-supervisor-test-fakes' +import { hookMount, settleQueryNotifications } from '../test-support/rpc-recording/hook-mount' +import type { MobileWorkspaceRepo } from './new-worktree-modal-types' +import { useNewWorkspaceRepositories } from './use-new-workspace-repositories' + +vi.mock('react-native', () => ({ AppState: {} })) +const servers = vi.hoisted(() => { + const state: { repos: ReadonlyMap } = { repos: new Map() } + return state +}) +vi.mock('../worktree/use-server-repos', () => ({ useServerRepos: () => servers.repos })) +vi.mock('../worktree/use-last-visited-worktree-repo', () => ({ + useLastVisitedWorktreeRepoId: () => ({ loaded: true, repoId: 'api' }) +})) + +const SERVER: ExecutionHostId = 'runtime:env-1' +const repo = (executionHostId?: ExecutionHostId): MobileWorkspaceRepo => ({ + id: 'api', + displayName: 'api', + path: '/src/api', + kind: 'git', + ...(executionHostId ? { executionHostId } : {}) +}) + +function desktopListing(repos: MobileWorkspaceRepo[]) { + const client = new FakeSession('connected') + client.sendRequest.mockResolvedValue({ + id: '1', + ok: true, + result: { repos }, + _meta: { runtimeId: 'r' } + }) + return client +} + +let mounted: ReturnType | null = null +afterEach(async () => { + await mounted?.unmount() + mounted = null + servers.repos = new Map() +}) + +describe('useNewWorkspaceRepositories initial repo', () => { + it.each([ + { order: 'desktop first', desktopFirst: true }, + { order: 'server first', desktopFirst: false } + ])( + 'picks the named host’s copy of a repo id two hosts share ($order)', + async ({ desktopFirst }) => { + const client = desktopListing(desktopFirst ? [repo()] : []) + let picked: MobileWorkspaceRepo | null = null + mounted = hookMount(() => { + picked = useNewWorkspaceRepositories({ + client, + hostId: 'host-1', + visible: true, + initialRepo: { id: 'api', executionHostId: SERVER } + }).selectedRepo + }) + mounted.mount() + await settleQueryNotifications() + // The desktop's same-id repo must not stand in while the server's listing is on its way. + expect(picked).toBeNull() + + servers.repos = new Map([[SERVER, [repo(SERVER)]]]) + act(() => mounted?.update()) + await settleQueryNotifications() + expect(picked).toMatchObject({ id: 'api', executionHostId: SERVER }) + } + ) +}) diff --git a/mobile/src/components/use-new-workspace-repositories.ts b/mobile/src/components/use-new-workspace-repositories.ts index 4961b186ed2..15a0c14e57e 100644 --- a/mobile/src/components/use-new-workspace-repositories.ts +++ b/mobile/src/components/use-new-workspace-repositories.ts @@ -1,6 +1,6 @@ import { useEffect, useMemo, useState } from 'react' import { useQueryClient } from '@tanstack/react-query' -import type { ExecutionHostId } from '../../../src/shared/execution-host' +import { getRepoExecutionHostId, type ExecutionHostId } from '../../../src/shared/execution-host' import type { RpcClient } from '../transport/rpc/rpc-client-contract' import { cachedHostRepos } from '../host-screen/host-list-queries' import { listHostRepos } from '../worktree/server-repos' @@ -21,6 +21,8 @@ export function useNewWorkspaceRepositories(args: { serverClients?: ReadonlyMap hostId?: string visible: boolean + /** Picked once it is listed, in place of the last visited repo. */ + initialRepo?: { id: string; executionHostId: ExecutionHostId } }): { repos: MobileWorkspaceRepo[] allRepos: MobileWorkspaceRepo[] @@ -31,7 +33,7 @@ export function useNewWorkspaceRepositories(args: { loadError: string | null retryLoad: () => void } { - const { client, serverClients = NO_SERVER_CLIENTS, hostId, visible } = args + const { client, serverClients = NO_SERVER_CLIENTS, hostId, visible, initialRepo } = args const queryClient = useQueryClient() // The host list's repos, so the form opens with them while repo.list answers. const [initialRepos] = useState(() => cachedHostRepos(queryClient, hostId)) @@ -51,7 +53,15 @@ export function useNewWorkspaceRepositories(args: { : [...desktopRepos, ...[...serverRepos.values()].flat()], [desktopRepos, serverRepos] ) - const repos = useMemo(() => getNewWorkspaceDialogEligibleRepos(allRepos), [allRepos]) + const initialRepoId = initialRepo?.id + const initialRepoHost = initialRepo?.executionHostId + // Opened for one host's folder: every picker offers only that host, whose store holds the folder. + const repos = useMemo(() => { + const eligible = getNewWorkspaceDialogEligibleRepos(allRepos) + return initialRepoHost + ? eligible.filter((repo) => getRepoExecutionHostId(repo) === initialRepoHost) + : eligible + }, [allRepos, initialRepoHost]) // Derive the pick so it follows updated listings and disappears when its host stops listing it. const selectedRepo = useMemo( () => refreshMobileNewWorkspaceDialogSelectedRepo(repos, pickedRepo), @@ -59,7 +69,26 @@ export function useNewWorkspaceRepositories(args: { ) useEffect(() => { - if (!visible || !lastVisitedRepo.loaded || selectedRepo || repos.length === 0) { + if (!visible || selectedRepo || !initialRepoId) { + return + } + // Waits for its host's listing: a same-id repo on another host is a different repo. + const initial = repos.find( + (repo) => repo.id === initialRepoId && getRepoExecutionHostId(repo) === initialRepoHost + ) + if (initial) { + setSelectedRepo(initial) + } + }, [initialRepoHost, initialRepoId, repos, selectedRepo, visible]) + + useEffect(() => { + if ( + !visible || + initialRepoId || + !lastVisitedRepo.loaded || + selectedRepo || + repos.length === 0 + ) { return } const preferredRepoId = resolveNewWorkspaceDialogRepoId({ @@ -70,7 +99,7 @@ export function useNewWorkspaceRepositories(args: { if (preferredRepo) { setSelectedRepo(preferredRepo) } - }, [lastVisitedRepo.loaded, lastVisitedRepo.repoId, repos, selectedRepo, visible]) + }, [initialRepoId, lastVisitedRepo.loaded, lastVisitedRepo.repoId, repos, selectedRepo, visible]) useEffect(() => { if (!visible || !client) { diff --git a/mobile/src/i18n/locales/en.json b/mobile/src/i18n/locales/en.json index 2f59b8e3a6d..e1aa56eea01 100644 --- a/mobile/src/i18n/locales/en.json +++ b/mobile/src/i18n/locales/en.json @@ -1493,6 +1493,11 @@ "offlineAsOfTime": { "yesterday": "yesterday, {{time}}" }, + "newWorkspaceCreateSubmit": { + "uncertainFolderCreate": "This worktree may have been created. Check the folder before creating another.", + "uncertainCreate": "This workspace may have been created. Check your workspaces before creating another.", + "folderCreateBusy": "A worktree in this repository is still being created or checked. It shows in the folder once it lands." + }, "smartRailRow": { "title": "Smart rail", "subtitle": "Shows keys for what your agent is doing" @@ -3675,7 +3680,17 @@ "emptyTitle": "No attached worktrees yet", "emptyCopy": "Worktrees created from this workspace will show up here.", "statusUnavailable": "Unavailable", - "backToFolder": "Back to folder" + "backToFolder": "Back to folder", + "createSection": "Create a worktree", + "createIn": "Create a worktree in {{repo}}", + "created": "Created worktree {{name}} in {{repo}}", + "createdNamed": "Created worktree {{name}}", + "createFailed": "Could not create a worktree in {{repo}}.", + "loadReposRetry": "Load repositories", + "setupChoiceTitle": "Retry without setup?", + "setupChoiceMessage": "Orca couldn't confirm the last create in {{repo}}. Retrying it needs a setup choice; it will be created without running the repository's setup.", + "setupChoiceSkip": "Retry without setup", + "checking": "A worktree from {{repo}} may already have been created. Checking the folder…" }, "hostedReviewCopy": { "updatingBranch": "Updating branch...", diff --git a/mobile/src/mobile-web-shell/generation-store.test.ts b/mobile/src/mobile-web-shell/generation-store.test.ts index 2a5c4fc70a4..b14db27039d 100644 --- a/mobile/src/mobile-web-shell/generation-store.test.ts +++ b/mobile/src/mobile-web-shell/generation-store.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import { createGenerationStore, MAX_CACHED_HOSTS } from './generation-store' +import { createGenerationStore } from './generation-store' import { deriveHostCacheKey } from './host-cache-key' import type { MobileWebBundleFetchResult } from '../transport/web-bundle/mobile-web-bundle-fetch' import { computeMobileWebBundleId } from '../../../src/shared/mobile-web-bundle/manifest-contract' @@ -59,343 +59,6 @@ async function activate( await store.commitGeneration(await store.stageGeneration(hostKey, result)) } -function hostKeys(count: number): string[] { - return Array.from({ length: count }, (_, index) => deriveHostCacheKey(`host-${index}`)) -} - -describe('generation store', () => { - it('refuses an asset that is missing or the wrong length, leaving no generation', async () => { - const fs = createFakeFileSystem() - const store = createGenerationStore({ fileSystem: fs }) - const missing = buildResult({ bytes: new Map([['index.html', new Uint8Array(4)]]) }) - const short = buildResult({ - bytes: new Map([ - ['index.html', new Uint8Array(4)], - ['assets/app.js', new Uint8Array(1)] - ]) - }) - - await expect(store.stageGeneration(HOST, missing)).rejects.toThrow('assets/app.js is absent') - await expect(store.stageGeneration(HOST, short)).rejects.toThrow("not the manifest's 2") - expect(fs.paths()).toEqual([]) - expect(await store.readActiveGeneration(HOST)).toBeNull() - }) - - it('drops the staged tree when a write fails', async () => { - const fs = createFakeFileSystem() - const store = createGenerationStore({ fileSystem: fs }) - fs.failWritesAt(`${HOST}/tmp/${BUILD}/assets/app.js`) - - await expect(store.stageGeneration(HOST, buildResult({}))).rejects.toThrow('disk-full') - - expect(fs.paths().some((path) => path.includes(`tmp/${BUILD}`))).toBe(false) - expect(await store.readActiveGeneration(HOST)).toBeNull() - }) - - it('leaves no generation and no tmp for any host when a download is interrupted', async () => { - const fs = createFakeFileSystem() - const store = createGenerationStore({ fileSystem: fs }) - const other = deriveHostCacheKey('host-b') - // The manifest is written last, so a download cut off mid-stage leaves assets without one. - fs.seed(`${HOST}/tmp/${BUILD}/index.html`, { kind: 'file', bytes: new Uint8Array(4) }) - fs.seed(`${other}/tmp/${BUILD}/index.html`, { kind: 'file', bytes: new Uint8Array(4) }) - - expect(await store.readActiveGeneration(HOST)).toBeNull() - expect(await store.readActiveGeneration(other)).toBeNull() - expect(fs.paths().some((path) => path.includes('/tmp'))).toBe(false) - }) - - it('reads two generations as no activation and drops the host tree', async () => { - const fs = createFakeFileSystem() - const store = createGenerationStore({ fileSystem: fs }) - await activate(store, HOST) - fs.seed(`${HOST}/generations/${'c'.repeat(64)}/manifest.json`, { - kind: 'file', - bytes: new TextEncoder().encode('{}') - }) - - expect(await store.readActiveGeneration(HOST)).toBeNull() - expect(fs.paths().some((path) => path.startsWith(HOST))).toBe(false) - }) - - it('reads an unparseable or mismatched manifest as no activation and drops the host tree', async () => { - for (const body of [ - 'not json', - JSON.stringify({ ...buildResult({}).manifest, buildId: 'd'.repeat(64) }) - ]) { - const fs = createFakeFileSystem() - const store = createGenerationStore({ fileSystem: fs }) - await activate(store, HOST) - fs.seed(`${HOST}/generations/${BUILD}/manifest.json`, { - kind: 'file', - bytes: new TextEncoder().encode(body) - }) - - expect(await store.readActiveGeneration(HOST)).toBeNull() - expect(fs.paths().some((path) => path.startsWith(HOST))).toBe(false) - } - }) - - it('evicts the least recently activated host past the ceiling', async () => { - const fs = createFakeFileSystem() - const store = createGenerationStore({ fileSystem: fs }) - const hosts = hostKeys(MAX_CACHED_HOSTS + 1) - - for (const host of hosts) { - await activate(store, host) - } - - expect(await store.readActiveGeneration(hosts[0])).toBeNull() - expect(fs.paths().some((path) => path.startsWith(hosts[0]))).toBe(false) - for (const host of hosts.slice(1)) { - expect((await store.readActiveGeneration(host))?.buildId).toBe(BUILD) - } - expect(fs.text('hosts.json')).toBe(JSON.stringify(hosts.slice(1))) - }) - - it('evicts a host with no index entry before the least recently activated one', async () => { - const fs = createFakeFileSystem() - const store = createGenerationStore({ fileSystem: fs }) - const [oldest, ...rest] = hostKeys(MAX_CACHED_HOSTS - 1) - const orphan = deriveHostCacheKey('orphan') - for (const host of [oldest, ...rest]) { - await activate(store, host) - } - await activate(store, orphan) - const order = [oldest, ...rest] - fs.seed('hosts.json', { kind: 'file', bytes: new TextEncoder().encode(JSON.stringify(order)) }) - - await activate(store, deriveHostCacheKey('one-too-many')) - - expect(fs.paths().some((path) => path.startsWith(orphan))).toBe(false) - expect((await store.readActiveGeneration(oldest))?.buildId).toBe(BUILD) - }) - - it('never counts or evicts a host that is only mid-download', async () => { - const fs = createFakeFileSystem() - const store = createGenerationStore({ fileSystem: fs }) - const hosts = hostKeys(MAX_CACHED_HOSTS) - const oldest = hosts[0] - const downloading = deriveHostCacheKey('downloading') - for (const host of hosts) { - await activate(store, host) - } - const staged = await store.stageGeneration(downloading, buildResult({})) - - await activate(store, deriveHostCacheKey('one-too-many')) - - // Count activations only; eviction must leave staged downloads intact. - expect(fs.paths().some((path) => path.startsWith(oldest))).toBe(false) - expect(fs.text(`${staged.directory.slice(ROOT.length + 1)}/manifest.json`)).not.toBeNull() - await store.commitGeneration(staged) - expect((await store.readActiveGeneration(downloading))?.buildId).toBe(BUILD) - }) - - it('serializes two stage calls for one host and build', async () => { - const fs = createFakeFileSystem() - const store = createGenerationStore({ fileSystem: fs }) - // Give one build id conflicting asset lists to expose interleaved writes into the same staging directory. - const staging = `${HOST}/tmp/${BUILD}` - const earlier = buildResult({ - buildId: BUILD, - assets: [{ path: 'assets/earlier.js', byteLength: 2 }] - }) - const later = buildResult({ - buildId: BUILD, - assets: [{ path: 'assets/later.js', byteLength: 3 }] - }) - - const [first, second] = await Promise.all([ - store.stageGeneration(HOST, earlier), - store.stageGeneration(HOST, later) - ]) - - expect(first.directory).toBe(second.directory) - expect(fs.writes).toEqual([ - `${staging}/assets/earlier.js`, - `${staging}/manifest.json`, - `${staging}/assets/later.js`, - `${staging}/manifest.json` - ]) - expect(fs.paths().filter((path) => path.startsWith(`${staging}/assets/`))).toEqual([ - `${staging}/assets/later.js` - ]) - }) - - it('refuses a path that escapes the staged tree, and a host key that is not one', async () => { - const fs = createFakeFileSystem() - const store = createGenerationStore({ fileSystem: fs }) - const escapes = [ - '../outside.js', - 'assets/../../outside.js', - '/etc/passwd', - 'assets//app.js', - 'manifest.json', - 'Manifest.JSON' - ] - - for (const path of escapes) { - const result = buildResult({ assets: [{ path, byteLength: 1 }] }) - await expect(store.stageGeneration(HOST, result)).rejects.toThrow('refuses to stage') - } - await expect(store.stageGeneration('host-a', buildResult({}))).rejects.toThrow( - 'not a host cache key' - ) - expect(fs.paths()).toEqual([]) - }) - - it('refuses a rename that did not carry the tree, as Android below API 26 can', async () => { - const fs = createFakeFileSystem() - const store = createGenerationStore({ fileSystem: fs }) - fs.loseContentsOnMove() - - const staged = await store.stageGeneration(HOST, buildResult({})) - await expect(store.commitGeneration(staged)).rejects.toThrow('did not carry its manifest') - - expect(await store.readActiveGeneration(HOST)).toBeNull() - expect(fs.paths().some((path) => path.includes('generations/'))).toBe(false) - }) - - it('keeps the host tree when the manifest read fails, and drops it when it is missing', async () => { - const fs = createFakeFileSystem() - const store = createGenerationStore({ fileSystem: fs }) - const manifest = `${HOST}/generations/${BUILD}/manifest.json` - await activate(store, HOST) - const before = fs.paths().filter((path) => !path.endsWith('/tmp')) - - fs.failReadsAt(manifest) - expect(await store.readActiveGeneration(HOST)).toBeNull() - expect(fs.paths()).toEqual(before) - - fs.failReadsAt(null) - await fs.delete(`${ROOT}/${manifest}`) - expect(await store.readActiveGeneration(HOST)).toBeNull() - expect(fs.paths().some((path) => path.startsWith(HOST))).toBe(false) - }) - - it('replaces an entry named for the build id that is not a readable generation', async () => { - const build = BUILD - // Model a crash between rename and the post-rename manifest check. - for (const seeded of [ - { kind: 'directory' }, - { kind: 'file', bytes: new Uint8Array(1) } - ] as const) { - const fs = createFakeFileSystem() - const store = createGenerationStore({ fileSystem: fs }) - fs.seed(`${HOST}/generations/${build}`, seeded) - - await activate(store, HOST) - - expect((await store.readActiveGeneration(HOST))?.buildId).toBe(build) - expect(fs.text(`${HOST}/generations/${build}/index.html`)).not.toBeNull() - } - }) - - it('refuses a handle whose staged tree is gone without touching the activation', async () => { - const fs = createFakeFileSystem() - const store = createGenerationStore({ fileSystem: fs }) - await activate(store, HOST) - - const staged = await store.stageGeneration(HOST, buildResult(SECOND)) - await store.abortStagedGeneration(staged) - - await expect(store.commitGeneration(staged)).rejects.toThrow('no longer on disk') - expect((await store.readActiveGeneration(HOST))?.buildId).toBe(BUILD) - }) - - it('refuses a staged handle it did not issue', async () => { - const fs = createFakeFileSystem() - const store = createGenerationStore({ fileSystem: fs }) - await activate(store, HOST) - const before = fs.paths() - const forged = { - hostKey: HOST, - buildId: SECOND_BUILD, - directory: `${ROOT}/${HOST}/generations/${BUILD}`, - manifest: buildResult({}).manifest - } - - await expect(store.commitGeneration(forged)).rejects.toThrow('did not issue') - await expect(store.abortStagedGeneration(forged)).rejects.toThrow('did not issue') - expect(fs.paths()).toEqual(before) - expect((await store.readActiveGeneration(HOST))?.buildId).toBe(BUILD) - }) - - it('opens builds kept under the old root without a download once the root moves', async () => { - const fs = createFakeFileSystem() - const legacyRootUri = 'file:///caches/mobile-web' - await activate(createGenerationStore({ fileSystem: { ...fs, rootUri: legacyRootUri } }), HOST) - - const store = createGenerationStore({ fileSystem: fs, legacyRootUri }) - - expect((await store.readActiveGeneration(HOST))?.buildId).toBe(BUILD) - expect(await fs.list(legacyRootUri)).toEqual([]) - }) -}) - -describe('reusable assets for the next download', () => { - const NEXT = buildResult({ - assets: [ - { path: 'index.html', byteLength: 8 }, - { path: 'assets/app-2.js', byteLength: 2 } - ] - }) - const NEXT_BUILD = NEXT.manifest.buildId - const [PAGE, SCRIPT] = NEXT.manifest.assets - const DOWNLOADED = new Map([['index.html', new Uint8Array(8).fill(9)]]) - const LEFT_TO_STAGE = { ...NEXT, assets: DOWNLOADED, reused: ['assets/app-2.js'] } - - it.each([HOST, deriveHostCacheKey('second-host')])( - 'matches cached assets by content for %s', - async (opened) => { - const fs = createFakeFileSystem() - const store = createGenerationStore({ fileSystem: fs }) - expect((await store.openReusableAssets(HOST)).has(SCRIPT!)).toBe(false) - await activate(store, HOST) - - const reusable = await store.openReusableAssets(opened) - - expect(reusable.has(SCRIPT!)).toBe(true) - expect(reusable.has(PAGE!)).toBe(false) - expect(reusable.has({ ...SCRIPT!, sha256: 'f'.repeat(64) })).toBe(false) - expect(await reusable.check(SCRIPT!)).toBe(true) - fs.seed(`${HOST}/generations/${BUILD}/assets/app.js`, { - kind: 'file', - bytes: new Uint8Array(1) - }) - expect(await reusable.check(SCRIPT!)).toBe(false) - } - ) - - it('copies the reused assets beside the downloaded ones and commits the whole build', async () => { - const fs = createFakeFileSystem() - const store = createGenerationStore({ fileSystem: fs }) - await activate(store, HOST) - - await activate(store, HOST, LEFT_TO_STAGE) - - const generation = `${HOST}/generations/${NEXT_BUILD}` - expect(fs.bytes(`${generation}/index.html`)).toEqual(new Uint8Array(8).fill(9)) - expect(fs.bytes(`${generation}/assets/app-2.js`)).toEqual(new Uint8Array(2).fill(7)) - expect((await store.readActiveGeneration(HOST))?.buildId).toBe(NEXT_BUILD) - }) - - it('refuses to stage a reused asset the active generation no longer holds', async () => { - const fs = createFakeFileSystem() - const store = createGenerationStore({ fileSystem: fs }) - await activate(store, HOST) - fs.failReadsAt(`${HOST}/generations/${BUILD}/assets/app.js`) - - await expect(store.stageGeneration(HOST, LEFT_TO_STAGE)).rejects.toThrow('unreadable') - await store.deleteHostCache(HOST) - fs.failReadsAt(null) - await expect(store.stageGeneration(HOST, LEFT_TO_STAGE)).rejects.toThrow( - 'no longer holds the reused asset assets/app-2.js' - ) - expect(fs.paths().some((path) => path.includes(`tmp/${NEXT_BUILD}`))).toBe(false) - }) -}) - // Commit deletes the old build first, so it must not run under a page still loading that build's chunks. describe('an update staged behind a live page', () => { it('stays staged while a page reads the old build, and the next read after it commits', async () => { @@ -412,45 +75,4 @@ describe('an update staged behind a live page', () => { expect((await store.readActiveGeneration(HOST))?.buildId).toBe(SECOND_BUILD) expect(await fs.fileExists(`${ROOT}/${HOST}/generations/${BUILD}/index.html`)).toBe(false) }) - - it('lets a page that waited on another page fetch the build itself when that page gives up', async () => { - const store = createGenerationStore({ fileSystem: createFakeFileSystem() }) - const first = await store.claimUpdate(HOST, SECOND_BUILD) - const second = store.claimUpdate(HOST, SECOND_BUILD) - - first?.() - - expect(await second).not.toBeNull() - }) - - it('commits at the next launch when the app ends while the page still reads the old build', async () => { - const fs = createFakeFileSystem() - const first = createGenerationStore({ fileSystem: fs }) - await activate(first, HOST) - first.holdGeneration(HOST) - first.deferCommit(await first.stageGeneration(HOST, buildResult(SECOND))) - - const relaunched = createGenerationStore({ fileSystem: fs }) - - expect((await relaunched.readActiveGeneration(HOST))?.buildId).toBe(SECOND_BUILD) - expect(await fs.fileExists(`${ROOT}/${HOST}/generations/${SECOND_BUILD}/index.html`)).toBe(true) - }) - - it('lets a reader of a host’s build go before a commit or a removal deletes it', async () => { - const fs = createFakeFileSystem() - const onDisk: boolean[] = [] - let reading = BUILD - const store = createGenerationStore({ - fileSystem: fs, - beforeHostFilesDeleted: async (hostKey) => { - onDisk.push(await fs.fileExists(`${ROOT}/${hostKey}/generations/${reading}/manifest.json`)) - } - }) - await activate(store, HOST) - onDisk.length = 0 - await activate(store, HOST, buildResult(SECOND)) - reading = SECOND_BUILD - await store.deleteHostCache(HOST) - expect(onDisk).toEqual([true, true]) - }) }) diff --git a/mobile/src/mobile-web-shell/removed-host-shell-cache.test.ts b/mobile/src/mobile-web-shell/removed-host-shell-cache.test.ts deleted file mode 100644 index 564fd33101f..00000000000 --- a/mobile/src/mobile-web-shell/removed-host-shell-cache.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' - -vi.mock('expo-file-system', () => ({ Directory: class {}, File: class {}, Paths: { cache: '' } })) -vi.mock('./generation-store-file-system', async () => { - const { createFakeGenerationFileSystem } = await import('./generation-file-system-fake') - const fileSystem = createFakeGenerationFileSystem() - return { createExpoGenerationFileSystem: () => fileSystem } -}) - -import { createGenerationStore } from './generation-store' -import { createExpoGenerationFileSystem } from './generation-store-file-system' -import { forgetHostUpdateFailures } from './removed-host-shell-cache' - -function store() { - return createGenerationStore({ fileSystem: createExpoGenerationFileSystem('') }) -} - -async function seed(hostId: string): Promise { - await store().recordUpdateFailure({ - hostId, - at: 1, - reason: 'connection-lost', - hostCode: null, - offeredBuildId: null, - cachedBuildId: null, - outcome: 'opened-cached', - wall: null - }) -} - -describe('forgetHostUpdateFailures', () => { - beforeEach(async () => { - await store().forgetHostUpdateFailures('host-1') - await store().forgetHostUpdateFailures('host-2') - }) - - it("drops the removed host's records and keeps the rest", async () => { - await seed('host-1') - await seed('host-2') - await forgetHostUpdateFailures('host-1') - expect((await store().readUpdateFailures()).map((entry) => entry.hostId)).toEqual(['host-2']) - }) - - it('writes nothing when nothing was recorded', async () => { - const fileSystem = createExpoGenerationFileSystem('') - expect(await fileSystem.readText(`${fileSystem.rootUri}/update-failures.json`)).toBeNull() - const write = vi.spyOn(fileSystem, 'writeText') - try { - await forgetHostUpdateFailures('host-1') - expect(write).not.toHaveBeenCalled() - expect(await fileSystem.readText(`${fileSystem.rootUri}/update-failures.json`)).toBeNull() - } finally { - write.mockRestore() - } - }) -}) diff --git a/mobile/src/source-control/MobileSourceControlRoutePanel.tsx b/mobile/src/source-control/MobileSourceControlRoutePanel.tsx index f82fd2915e9..a6096584cee 100644 --- a/mobile/src/source-control/MobileSourceControlRoutePanel.tsx +++ b/mobile/src/source-control/MobileSourceControlRoutePanel.tsx @@ -10,6 +10,8 @@ export function MobileSourceControlRoutePanel(props: MobileSourceControlPanelPro if (parseWorkspaceKey(props.worktreeId)?.type === 'folder') { return ( void> + onCreate: (repo: Repo, target: FolderRepoTarget, client: RpcClient, name: string) => void + bottomInset: number +} + +/** A folder with no attached worktrees: desktop's copy, then one create card per repository. */ +export function MobileFolderEmptyState(props: Props) { + const { hostId, folder, desktopCapabilities, folderHostCapabilities, observing, refreshers } = + props + const t = useMobileTranslation() + const { client: desktopClient, state } = useHostClient(hostId) + // A relayed server's folder is created on that server, through the desktop. + const routeHost = workspaceRouteExecutionHost(desktopClient, desktopCapabilities, folder.hostId) + const server = routeHost?.startsWith('runtime:') ? routeHost : undefined + // Only exact-name creates land as named; a server's rows also need a desktop new enough to relay its folder lineage. + const canCreate = + state === 'connected' && + routeHost !== null && + folderHostCapabilities.includes(WORKTREE_CREATE_EXACT_NAME_RUNTIME_CAPABILITY) && + (!server || desktopCapabilities.includes(WORKTREE_CREATE_EXACT_NAME_RUNTIME_CAPABILITY)) + const createClient = useMemo( + () => + desktopClient && server + ? scopeRpcClientToExecutionHost(desktopClient, server) + : desktopClient, + [desktopClient, server] + ) + const catalog = useHostQuery( + server ? serverFolderSourceCatalogQuery : folderSourceCatalogQuery, + { hostId, client: canCreate ? createClient : null }, + { exec: server ?? 'local' }, + NO_INPUT, + { enabled: canCreate, subscribed: observing } + ) + const refetchCatalog = catalog.refetch + useEffect(() => { + if (!canCreate) { + return + } + const refresh = () => void refetchCatalog() + refreshers.add(refresh) + return () => { + refreshers.delete(refresh) + } + }, [canCreate, refetchCatalog, refreshers]) + const repos = useMemo( + () => + canCreate + ? selectFolderSourceRepos(catalog.data, projectGroupIdFromRepoId(folder.repoId)) + : [], + [canCreate, catalog.data, folder.repoId] + ) + const name = folder.displayName.trim() + const catalogFailed = canCreate && catalog.data === undefined && catalog.error !== null + + return ( + + void refetchCatalog() + } + : null + } + bottomInset={props.bottomInset} + > + {repos.length > 0 && createClient && name ? ( + + + + {repos.map((repo) => { + const target: FolderRepoTarget = { + hostId, + executionHost: server, + folderWorktreeId: folder.worktreeId, + repoId: repo.id + } + return ( + props.onCreate(repo, target, createClient, name)} + /> + ) + })} + + + ) : null} + + + ) +} + +function FolderRepoCreateCard(props: { + target: FolderRepoTarget + repo: Repo + onPress: () => void +}) { + const t = useMobileTranslation() + const busy = useSyncExternalStore(subscribeFolderRepoCreates, () => + isFolderRepoCreateBusy(props.target) + ) + return ( + + } + accessory={ + busy ? ( + + ) : ( + + ) + } + busy={busy} + onPress={props.onPress} + accessibilityRole="button" + accessibilityLabel={t('sourceControl.folder.createIn', 'Create a worktree in {{repo}}', { + repo: props.repo.displayName + })} + /> + + ) +} + +const styles = themedStyleSheet(() => ({ + scroll: { flexGrow: 1 }, + cards: { gap: spacing.sm } +})) diff --git a/mobile/src/source-control/folder/MobileFolderSourceControlPanel.test.tsx b/mobile/src/source-control/folder/MobileFolderSourceControlPanel.test.tsx new file mode 100644 index 00000000000..102d52bad8b --- /dev/null +++ b/mobile/src/source-control/folder/MobileFolderSourceControlPanel.test.tsx @@ -0,0 +1,126 @@ +import { createElement, type ReactNode } from 'react' +import { act, create, type ReactTestRenderer } from 'react-test-renderer' +import { afterEach, describe, expect, it, vi } from 'vitest' +import type { FolderListRow } from './mobile-folder-attached-worktrees' + +const seen = vi.hoisted(() => { + const state: { covered: boolean; membersObserving: boolean[]; rowsSubscribed: boolean[] } = { + covered: false, + membersObserving: [], + rowsSubscribed: [] + } + return state +}) +vi.mock('react-native', () => ({ ScrollView: 'ScrollView', View: 'View' })) +vi.mock('lucide-react-native', () => ({ GitBranch: 'GitBranch' })) +vi.mock('expo-router', () => ({ useFocusEffect: () => {} })) +vi.mock('react-native-safe-area-context', () => ({ + SafeAreaView: 'SafeAreaView', + useSafeAreaInsets: () => ({ bottom: 0 }) +})) +vi.mock('../../components/MobileStateView', () => ({ + MobileLoadErrorState: 'MobileLoadErrorState', + MobileOfflineState: 'MobileOfflineState', + MobileStateView: 'MobileStateView' +})) +vi.mock('../../transport/connection/connection-retry-action', () => ({ + connectionRetryAction: () => () => {} +})) +vi.mock('../../transport/connection/host-client-hooks', () => ({ + useForceReconnect: () => () => {} +})) +vi.mock('../../components/bottom-drawer/BottomDrawer', () => ({ + OptionPager: ({ main }: { main: ReactNode }) => main, + useOptionPager: () => ({ page: null, open: () => {}, close: () => {} }) +})) +vi.mock('../../components/list/ListGroup', () => ({ ListGroup: 'ListGroup' })) +vi.mock('../../i18n/use-mobile-translation', () => ({ + useMobileTranslation: () => (_key: string, fallback: string) => fallback +})) +vi.mock('../../navigation/use-back-claim', () => ({ useBackClaim: () => {} })) +vi.mock('../../navigation/workspace-route-handoff', () => ({ + useWorkspaceRouteHandoff: () => ({ back: () => {} }) +})) +vi.mock('../../navigation/workspace-route', () => ({ + WorkspaceExecutionScope: 'WorkspaceExecutionScope' +})) +vi.mock('../../theme/mobile-theme', () => ({ colors: {}, spacing: { lg: 16 } })) +vi.mock('../../theme/themed-styles', () => ({ + themedStyleSheet: (styles: () => unknown) => styles() +})) +vi.mock('../../theme/use-mobile-color-scheme', () => ({ useMobileColorScheme: () => {} })) +vi.mock('../MobileSourceControlHeader', () => ({ + MobileSourceControlHeader: 'MobileSourceControlHeader' +})) +vi.mock('../MobileSourceControlPanel', () => ({ MobileSourceControlPanel: 'Panel' })) +vi.mock('../mobile-source-control-styles', () => ({ styles: {} })) +vi.mock('./MobileFolderEmptyState', () => ({ MobileFolderEmptyState: 'EmptyState' })) +vi.mock('../../components/host-protocol-gates-context', () => ({ + useOptionalHostProtocolGates: () => null +})) +vi.mock('./use-folder-create-flow', () => ({ + useFolderCreateFlow: () => ({ + onCreate: async () => {}, + covered: seen.covered, + composerElement: null, + toastElement: null + }) +})) +vi.mock('./MobileFolderWorktreeRow', () => ({ + MobileFolderWorktreeRow: (props: { subscribed: boolean }) => { + seen.rowsSubscribed.push(props.subscribed) + return null + } +})) +vi.mock('./use-mobile-folder-attached-worktrees', () => ({ + useMobileFolderAttachedWorktrees: (_hostId: string, _folderId: string, observing: boolean) => { + seen.membersObserving.push(observing) + return { + folder: { worktreeId: 'folder:f-1', repoId: 'folder-workspace:f-1', displayName: 'payments' }, + worktrees: [ROW], + desktopCapabilities: [], + serverHosts: [], + connState: 'connected', + loadError: null, + refetch: () => {} + } + } +})) + +import { MobileFolderSourceControlPanel } from './MobileFolderSourceControlPanel' + +const ROW: FolderListRow = { + worktreeId: 'repo-1::/w/payments', + repoId: 'repo-1', + repo: 'orca', + displayName: 'payments', + branch: 'payments' +} + +let renderer: ReactTestRenderer | null = null +afterEach(() => { + act(() => renderer?.unmount()) + renderer = null + seen.covered = false + seen.membersObserving = [] + seen.rowsSubscribed = [] +}) + +describe('MobileFolderSourceControlPanel', () => { + it('pauses row status under the create form while the folder members stay live', () => { + seen.covered = true + act(() => { + renderer = create( + createElement(MobileFolderSourceControlPanel, { + hostId: 'host-1', + folderWorktreeId: 'folder:f-1', + name: 'payments', + embedded: true + }) + ) + }) + + expect(seen.rowsSubscribed.at(-1)).toBe(false) + expect(seen.membersObserving.at(-1)).toBe(true) + }) +}) diff --git a/mobile/src/source-control/folder/MobileFolderSourceControlPanel.tsx b/mobile/src/source-control/folder/MobileFolderSourceControlPanel.tsx index f27ba094d7b..e3d21ea0539 100644 --- a/mobile/src/source-control/folder/MobileFolderSourceControlPanel.tsx +++ b/mobile/src/source-control/folder/MobileFolderSourceControlPanel.tsx @@ -24,9 +24,14 @@ import { getWorktreeRowIdentity } from '../../worktree/worktree-host-row-identit import { MobileSourceControlHeader } from '../MobileSourceControlHeader' import { MobileSourceControlPanel } from '../MobileSourceControlPanel' import { styles as sourceControlStyles } from '../mobile-source-control-styles' +import { MobileFolderEmptyState } from './MobileFolderEmptyState' +import { useFolderCreateFlow } from './use-folder-create-flow' import { MobileFolderWorktreeRow } from './MobileFolderWorktreeRow' +import { useOptionalHostProtocolGates } from '../../components/host-protocol-gates-context' import { useMobileFolderAttachedWorktrees } from './use-mobile-folder-attached-worktrees' +const NO_CAPABILITIES: readonly string[] = [] + type Props = { hostId: string folderWorktreeId: string @@ -61,8 +66,22 @@ export function MobileFolderSourceControlPanel({ const observing = focused && !pageOpen const { folder, worktrees, desktopCapabilities, serverHosts, connState, loadError, refetch } = useMobileFolderAttachedWorktrees(hostId, folderWorktreeId, observing) + // In a server's scope these are the server's own; otherwise the desktop's. + const folderHostGates = useOptionalHostProtocolGates() const forceReconnect = useForceReconnect() const [refreshers] = useState(() => new Set<() => void>()) + const createFlow = useFolderCreateFlow({ + hostId, + folderWorktreeId, + executionHost: folder?.hostId?.startsWith('runtime:') ? folder.hostId : undefined, + desktopCapabilities, + members: worktrees, + observing, + refetch, + bottomInset: insets.bottom + }) + // Rows and repositories under a create form wait; the folder's members stay live so creates still reconcile. + const statusObserving = observing && !createFlow.covered const refresh = () => { refetch() for (const refreshNow of refreshers) { @@ -118,13 +137,17 @@ export function MobileFolderSourceControlPanel({ title={t('sourceControl.panel.unavailableTitle', 'Source Control Unavailable')} /> ) : worktrees.length === 0 ? ( - + void createFlow.onCreate(repo, target, client, createName) + } + bottomInset={insets.bottom} /> ) : ( // One card per attached worktree, as desktop shows them. @@ -141,7 +164,7 @@ export function MobileFolderSourceControlPanel({ serverHosts.find((server) => server.hostId === worktree.hostId)?.relay === 'update-needed' } - subscribed={observing} + subscribed={statusObserving} refreshers={refreshers} onOpen={open} /> @@ -160,6 +183,8 @@ export function MobileFolderSourceControlPanel({ )} {/* Every state shares the sheet surface the cards sit on, as the phone's option sheets do. */} {body} + {createFlow.toastElement} + {createFlow.composerElement} ) return ( diff --git a/mobile/src/source-control/folder/folder-repo-worktree-create.test.ts b/mobile/src/source-control/folder/folder-repo-worktree-create.test.ts new file mode 100644 index 00000000000..e89ca544001 --- /dev/null +++ b/mobile/src/source-control/folder/folder-repo-worktree-create.test.ts @@ -0,0 +1,332 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { FakeSession } from '../../transport/connection/mobile-endpoint-supervisor-test-fakes' +import { markRpcDeliveryUnknown } from '../../transport/rpc/rpc-delivery-ambiguity' +import type { RpcResponse } from '../../transport/rpc/rpc-wire-types' +import { + beginFolderComposerSubmission, + createFolderRepoWorktree, + isFolderRepoCreateBusy, + reconcileFolderRepoCreates, + releaseFolderComposerReservation, + type FolderRepoTarget +} from './folder-repo-worktree-create' + +const idempotency = vi.hoisted(() => { + const state: { support: { dedupeTtlMs: number } | false } = { support: false } + return state +}) +vi.mock('../../tasks/worktree-create-capability', () => ({ + readNewWorktreeRuntimeCapabilities: async () => ({ + worktreeCreateIdempotency: idempotency.support + }) +})) + +const FOLDER = 'folder:f-1' +const TAKEN = 'Branch payments already exists\nworktree_create_exact_name_taken' + +afterEach(() => { + vi.useRealTimers() + idempotency.support = false +}) + +let repoCount = 0 +// Each test's own repository: the registry is shared, as it is across screens. +function target(executionHost?: string): FolderRepoTarget { + repoCount += 1 + return { hostId: 'host-1', executionHost, folderWorktreeId: FOLDER, repoId: `repo-${repoCount}` } +} + +function refused(message: string): RpcResponse { + return { id: '1', ok: false, error: { code: 'x', message }, _meta: { runtimeId: 'r' } } +} + +const CREATED: RpcResponse = { + id: '1', + ok: true, + result: { worktree: { id: 'repo::/w/payments', displayName: 'payments' } }, + _meta: { runtimeId: 'r' } +} + +function host(...replies: (RpcResponse | Error)[]): FakeSession { + const client = new FakeSession('connected') + for (const reply of replies) { + if (reply instanceof Error) { + client.sendRequest.mockRejectedValueOnce(reply) + } else { + client.sendRequest.mockResolvedValueOnce(reply) + } + } + return client +} + +const create = (repo: FolderRepoTarget, client: FakeSession) => + createFolderRepoWorktree({ target: repo, client, name: 'payments' }) + +describe('createFolderRepoWorktree', () => { + it('sends one exact-name create attached to the folder', async () => { + const repo = target() + const client = host(CREATED) + + await expect(create(repo, client)).resolves.toEqual({ kind: 'created', name: 'payments' }) + expect(client.sendRequest.mock.calls).toEqual([ + [ + 'worktree.create', + { + repo: `id:${repo.repoId}`, + name: 'payments', + displayName: 'payments', + displayNameKind: 'user', + branchIntent: 'create-exact', + setupDecision: 'inherit', + parentWorkspace: FOLDER, + parentWorkspaceOrigin: 'manual' + }, + expect.anything() + ] + ]) + }) + + it('sends nothing for a second tap while the first create runs', async () => { + const repo = target() + const client = new FakeSession('connected') + let answer: (response: RpcResponse) => void = () => {} + client.sendRequest.mockReturnValue(new Promise((resolve) => (answer = resolve))) + + const first = create(repo, client) + await expect(create(repo, client)).resolves.toEqual({ kind: 'already-running' }) + answer(CREATED) + await first + expect(client.sendRequest).toHaveBeenCalledTimes(1) + }) + + it.each([ + ['the name is taken', TAKEN], + ['the repo asks about setup', 'Setup decision required for this repository'] + ])('keeps the repository for the composer when %s', async (_case, message) => { + const repo = target() + const outcome = await create(repo, host(refused(message))) + + expect(outcome.kind).toBe('needs-composer') + expect(isFolderRepoCreateBusy(repo)).toBe(true) + if (outcome.kind === 'needs-composer') { + releaseFolderComposerReservation(repo, outcome.reservation) + } + expect(isFolderRepoCreateBusy(repo)).toBe(false) + }) + + it('treats a lost reply as possibly created, past the busy backstop, and never offers the composer', async () => { + vi.useFakeTimers({ toFake: ['setTimeout'] }) + const repo = target() + const lost = markRpcDeliveryUnknown(new Error('connection lost')) + + await expect(create(repo, host(lost))).resolves.toEqual({ kind: 'maybe-created' }) + expect(isFolderRepoCreateBusy(repo)).toBe(true) + await expect(create(repo, host(CREATED))).resolves.toEqual({ kind: 'already-running' }) + + // The list never showed it; the card takes taps again but the create is still unresolved. + vi.advanceTimersByTime(60_000) + expect(isFolderRepoCreateBusy(repo)).toBe(false) + const retry = host(refused(TAKEN)) + await expect(create(repo, retry)).resolves.toEqual({ kind: 'maybe-created' }) + expect(retry.sendRequest).toHaveBeenCalledTimes(1) + }) + + it('retries an unconfirmed create as the name it was sent as, even after the folder is renamed', async () => { + vi.useFakeTimers({ toFake: ['setTimeout'] }) + const repo = target() + await create(repo, host(markRpcDeliveryUnknown(new Error('connection lost')))) + vi.advanceTimersByTime(60_000) + + const retry = host(refused(TAKEN)) + await createFolderRepoWorktree({ target: repo, client: retry, name: 'billing' }) + expect(retry.sendRequest.mock.calls[0]?.[1]).toMatchObject({ name: 'payments' }) + }) + + it('asks for setup when an unconfirmed create is refused for it, and resends the same exact name', async () => { + vi.useFakeTimers({ toFake: ['setTimeout'] }) + const repo = target() + await create(repo, host(markRpcDeliveryUnknown(new Error('connection lost')))) + vi.advanceTimersByTime(60_000) + + const asked = await create(repo, host(refused('Setup decision required for this repository'))) + expect(asked.kind).toBe('needs-setup-choice') + expect(isFolderRepoCreateBusy(repo)).toBe(false) + const hold = asked.kind === 'needs-setup-choice' ? asked.hold : -1 + + const resent = host(refused(TAKEN)) + await expect( + createFolderRepoWorktree({ + target: repo, + client: resent, + name: 'billing', + skipSetupFor: hold + }) + ).resolves.toEqual({ kind: 'maybe-created' }) + expect(resent.sendRequest.mock.calls[0]?.[1]).toMatchObject({ + name: 'payments', + branchIntent: 'create-exact', + setupDecision: 'skip' + }) + }) + + it('sends nothing for a setup choice once its create is listed or replaced', async () => { + vi.useFakeTimers({ toFake: ['setTimeout'] }) + const ask = async (repo: FolderRepoTarget) => { + await create(repo, host(markRpcDeliveryUnknown(new Error('connection lost')))) + vi.advanceTimersByTime(60_000) + const asked = await create(repo, host(refused('Setup decision required for this repository'))) + return asked.kind === 'needs-setup-choice' ? asked.hold : -1 + } + const listed = target() + const listedHold = await ask(listed) + const folder = { hostId: 'host-1', executionHost: undefined, folderWorktreeId: FOLDER } + reconcileFolderRepoCreates(folder, [{ repoId: listed.repoId }]) + const replaced = target() + const replacedHold = await ask(replaced) + await ask(replaced) + + const late = host(CREATED) + for (const [repo, hold] of [ + [listed, listedHold], + [replaced, replacedHold] + ] as const) { + await expect( + createFolderRepoWorktree({ + target: repo, + client: late, + name: 'billing', + skipSetupFor: hold + }) + ).resolves.toEqual({ kind: 'checking' }) + } + expect(late.sendRequest).not.toHaveBeenCalled() + }) +}) + +describe('createFolderRepoWorktree after a lost reply on a host that dedupes', () => { + it.each([ + ['answers with the worktree', CREATED, { kind: 'created', name: 'payments' }], + ['refuses the name as taken', refused(TAKEN), { kind: 'maybe-created' }] + ])('resends the same create once reconnected; the host %s', async (_case, replay, expected) => { + idempotency.support = { dedupeTtlMs: 60_000 } + const repo = target() + const client = new FakeSession('connected') + client.sendRequest + .mockImplementationOnce(async () => { + client.publishState('reconnecting') + setTimeout(() => client.publishState('connected'), 0) + throw markRpcDeliveryUnknown(new Error('connection lost')) + }) + .mockResolvedValueOnce(replay) + + await expect(create(repo, client)).resolves.toEqual(expected) + const [first, second] = client.sendRequest.mock.calls.map(([, params]) => params) + expect(second).toEqual(first) + expect(first).toHaveProperty('clientMutationId') + }) +}) + +describe('reconcileFolderRepoCreates', () => { + it('forgets a create once the folder lists its worktree on the same host, not another', async () => { + const repo = target('runtime:env-1') + await create(repo, host(CREATED)) + const folder = { hostId: 'host-1', executionHost: 'runtime:env-1', folderWorktreeId: FOLDER } + + reconcileFolderRepoCreates(folder, [{ repoId: repo.repoId, hostId: 'runtime:env-2' }]) + expect(isFolderRepoCreateBusy(repo)).toBe(true) + reconcileFolderRepoCreates(folder, [{ repoId: repo.repoId, hostId: 'runtime:env-1' }]) + expect(isFolderRepoCreateBusy(repo)).toBe(false) + }) + + it('keeps a create still on its way even if the list already shows the repository', async () => { + const repo = target() + const client = new FakeSession('connected') + let answer: (response: RpcResponse) => void = () => {} + client.sendRequest.mockReturnValue(new Promise((resolve) => (answer = resolve))) + const folder = { hostId: 'host-1', executionHost: undefined, folderWorktreeId: FOLDER } + const members = [{ repoId: repo.repoId }] + + const pending = create(repo, client) + reconcileFolderRepoCreates(folder, members) + expect(isFolderRepoCreateBusy(repo)).toBe(true) + answer(CREATED) + await pending + reconcileFolderRepoCreates(folder, members) + expect(isFolderRepoCreateBusy(repo)).toBe(false) + }) + + it("doesn't let an earlier create's timer end a newer one", async () => { + vi.useFakeTimers({ toFake: ['setTimeout'] }) + const repo = target() + await create(repo, host(markRpcDeliveryUnknown(new Error('lost')))) + vi.advanceTimersByTime(10_000) + const folder = { hostId: 'host-1', executionHost: undefined, folderWorktreeId: FOLDER } + reconcileFolderRepoCreates(folder, [{ repoId: repo.repoId }]) + await create(repo, host(CREATED)) + + vi.advanceTimersByTime(10_000) + expect(isFolderRepoCreateBusy(repo)).toBe(true) + }) +}) + +describe('beginFolderComposerSubmission', () => { + async function composing() { + const repo = target() + const outcome = await create(repo, host(refused(TAKEN))) + if (outcome.kind !== 'needs-composer') { + throw new Error('expected the composer') + } + return { repo, launch: { target: repo, reservation: outcome.reservation, name: 'payments' } } + } + + it('admits one create per repository while it is out, even after the form closes', async () => { + const { repo, launch } = await composing() + let formOpen = true + const submission = beginFolderComposerSubmission(repo, launch, () => formOpen) + + expect(submission).not.toBeNull() + formOpen = false + releaseFolderComposerReservation(repo, launch.reservation) + expect(beginFolderComposerSubmission(repo, launch, () => true)).toBeNull() + expect(await create(repo, host(CREATED))).toEqual({ kind: 'already-running' }) + + submission?.settle('uncertain') + expect(isFolderRepoCreateBusy(repo)).toBe(true) + }) + + it('gives a refused create back to the open form, and a same-host switch its own admission', async () => { + const { repo, launch } = await composing() + const other = { ...repo, repoId: `${repo.repoId}-other` } + + beginFolderComposerSubmission(repo, launch, () => true)?.settle('failed') + expect(isFolderRepoCreateBusy(repo)).toBe(true) + const switched = beginFolderComposerSubmission(other, launch, () => true) + expect(switched).not.toBeNull() + expect(beginFolderComposerSubmission(other, launch, () => true)).toBeNull() + switched?.settle('created') + releaseFolderComposerReservation(repo, launch.reservation) + expect(isFolderRepoCreateBusy(repo)).toBe(false) + expect(isFolderRepoCreateBusy(other)).toBe(true) + }) + it("never sends anything from a card for a form's unconfirmed create, whatever name it used", async () => { + vi.useFakeTimers({ toFake: ['setTimeout'] }) + const { repo, launch } = await composing() + // The form may have sent another name or branch than the card's. + beginFolderComposerSubmission(repo, launch, () => true)?.settle('uncertain') + vi.advanceTimersByTime(60_000) + + expect(beginFolderComposerSubmission(repo, launch, () => true)).toBeNull() + const tap = host(CREATED) + await expect(create(repo, tap)).resolves.toEqual({ kind: 'checking' }) + await expect( + createFolderRepoWorktree({ + target: repo, + client: tap, + name: 'payments', + skipSetupFor: 0 + }) + ).resolves.toEqual({ kind: 'checking' }) + expect(tap.sendRequest).not.toHaveBeenCalled() + expect(isFolderRepoCreateBusy(repo)).toBe(false) + }) +}) diff --git a/mobile/src/source-control/folder/folder-repo-worktree-create.ts b/mobile/src/source-control/folder/folder-repo-worktree-create.ts new file mode 100644 index 00000000000..f4828651a62 --- /dev/null +++ b/mobile/src/source-control/folder/folder-repo-worktree-create.ts @@ -0,0 +1,310 @@ +import { WORKTREE_CREATE_EXACT_NAME_TAKEN_CODE } from '../../../../src/shared/new-workspace/worktree-create-collision' +import { hasRuntimeRpcErrorCode } from '../../../../src/shared/runtime-rpc-error-code' +import type { RpcClient } from '../../transport/rpc/rpc-client-contract' +import { isRpcDeliveryUnknown } from '../../transport/rpc/rpc-delivery-ambiguity' +import { readNewWorktreeRuntimeCapabilities } from '../../tasks/worktree-create-capability' +import { createWorktreeWithNameRetry } from '../../tasks/worktree-create-retry' + +// The host's refusal before creating, when the repo asks to run or skip setup; desktop matches the same text. +const SETUP_DECISION_REQUIRED = 'Setup decision required for this repository' + +// How long a created or unconfirmed card stays busy waiting for the list; the outcome itself is kept until it shows. +const HELD_BUSY_MS = 15_000 + +/** One repository of one folder on one host. */ +export type FolderRepoTarget = { + hostId: string + /** The folder's server; undefined for the paired desktop's own. */ + executionHost: string | undefined + /** `folder:`. */ + folderWorktreeId: string + repoId: string +} + +type Phase = + /** The direct exact-name create is out. */ + | 'creating' + /** Refused before creating; a composer for it is open and nothing has been sent from it. */ + | 'composing' + /** The composer sent its create. */ + | 'submitting' + /** Created, or possibly created: kept until the folder's members show it. */ + | 'held' + +/** + * `name` is the exact name a direct create was sent as, so an unresolved one is retried as itself. + * A composer's create may have used another name or branch, so it is never resent from a card. + */ +type Operation = FolderRepoTarget & { + id: number + phase: Phase + busy: boolean + name: string + origin: 'direct' | 'composer' +} + +// In memory and shared across screens, so a remount or a reopened composer can't send a second create. +const operations = new Map() +const listeners = new Set<() => void>() +let nextId = 1 +let version = 0 + +function notify(): void { + version += 1 + listeners.forEach((listener) => listener()) +} + +export function folderRepoCreateKey(target: FolderRepoTarget): string { + return [target.hostId, target.executionHost ?? '', target.folderWorktreeId, target.repoId].join( + '\0' + ) +} + +export function subscribeFolderRepoCreates(listener: () => void): () => void { + listeners.add(listener) + return () => listeners.delete(listener) +} + +export function getFolderRepoCreatesVersion(): number { + return version +} + +/** The card shows a spinner and takes no taps while its repository's create is busy. */ +export function isFolderRepoCreateBusy(target: FolderRepoTarget): boolean { + return operations.get(folderRepoCreateKey(target))?.busy === true +} + +function start( + target: FolderRepoTarget, + phase: Phase, + name: string, + origin: Operation['origin'] +): Operation { + const operation = { ...target, id: nextId++, phase, busy: true, name, origin } + operations.set(folderRepoCreateKey(target), operation) + notify() + return operation +} + +function isCurrent(operation: Operation): boolean { + return operations.get(folderRepoCreateKey(operation))?.id === operation.id +} + +function release(operation: Operation): void { + if (isCurrent(operation)) { + operations.delete(folderRepoCreateKey(operation)) + notify() + } +} + +function hold(operation: Operation, busy = true): number | undefined { + if (!isCurrent(operation)) { + return undefined + } + // A new identity, so an earlier hold's timer can't end this one. + const held = { ...operation, id: nextId++, phase: 'held' as const, busy } + operations.set(folderRepoCreateKey(held), held) + notify() + if (!busy) { + return held.id + } + setTimeout(() => { + if (isCurrent(held)) { + operations.set(folderRepoCreateKey(held), { ...held, busy: false }) + notify() + } + }, HELD_BUSY_MS) + return held.id +} + +/** Whether `hold` is still this repository's unconfirmed direct create, waiting for a setup choice. */ +export function isFolderRepoHoldPending(target: FolderRepoTarget, hold: number): boolean { + const operation = operations.get(folderRepoCreateKey(target)) + return operation?.id === hold && operation.phase === 'held' && operation.origin === 'direct' +} + +/** Forgets created or unconfirmed creates once the folder lists a worktree from that repository on that host. */ +export function reconcileFolderRepoCreates( + folder: Pick, + members: readonly { repoId: string; hostId?: string }[] +): void { + let changed = false + for (const [key, operation] of operations) { + if ( + operation.phase === 'held' && + operation.hostId === folder.hostId && + operation.executionHost === folder.executionHost && + operation.folderWorktreeId === folder.folderWorktreeId && + members.some( + (member) => + member.repoId === operation.repoId && + (member.hostId?.startsWith('runtime:') ? member.hostId : undefined) === + operation.executionHost + ) + ) { + operations.delete(key) + changed = true + } + } + if (changed) { + notify() + } +} + +export type FolderRepoCreateOutcome = + | { kind: 'created'; name: string; warning?: string } + /** Refused before creating: a branch with that name exists, or the repo asks to run or skip setup. The repository stays reserved for the composer. */ + | { kind: 'needs-composer'; reservation: number } + /** The host may have created it: the reply was lost, or a resend was refused as taken. */ + | { kind: 'maybe-created' } + /** An unconfirmed create's resend needs a setup choice; it is resent as the same exact name with it. */ + | { kind: 'needs-setup-choice'; hold: number } + /** A composer's create is unconfirmed: nothing is sent, the folder is checked until it lists it. */ + | { kind: 'checking' } + | { kind: 'failed'; message: string } + | { kind: 'already-running' } + +/** + * Creates a worktree named after the folder in one of its repositories, attached to the folder, the + * way desktop's empty state does. Only for hosts that advertise exact-name creation. + */ +export async function createFolderRepoWorktree(args: { + target: FolderRepoTarget + /** The client that creates: the desktop's own, or scoped to the folder's server. */ + client: RpcClient + name: string + /** Resends this unconfirmed hold without setup; nothing is sent once the folder shows it or it is replaced. */ + skipSetupFor?: number +}): Promise { + const existing = operations.get(folderRepoCreateKey(args.target)) + const staleResend = + args.skipSetupFor !== undefined && !isFolderRepoHoldPending(args.target, args.skipSetupFor) + if (!staleResend && existing && (existing.phase !== 'held' || existing.busy)) { + return { kind: 'already-running' } + } + if (staleResend || existing?.origin === 'composer') { + return { kind: 'checking' } + } + // An unconfirmed create is retried as the same exact name, even after a rename: a taken name may be its own. + const unresolved = existing !== undefined + const name = existing?.name ?? args.name + const operation = start(args.target, 'creating', name, 'direct') + const settleUnresolved = (outcome: FolderRepoCreateOutcome): FolderRepoCreateOutcome => { + hold(operation) + return outcome + } + try { + const result = await createWorktreeWithNameRetry({ + client: args.client, + baseName: name, + // One exact name: a numbered suffix would be a worktree the user didn't ask for. + maxAttempts: 1, + branchIntentSupported: true, + worktreeCreateIdempotency: readNewWorktreeRuntimeCapabilities(args.client).then( + (capabilities) => capabilities?.worktreeCreateIdempotency ?? false + ), + buildParams: (name) => ({ + repo: `id:${args.target.repoId}`, + name, + displayName: name, + displayNameKind: 'user', + branchIntent: 'create-exact', + setupDecision: args.skipSetupFor === undefined ? 'inherit' : 'skip', + parentWorkspace: args.target.folderWorktreeId, + parentWorkspaceOrigin: 'manual' + }) + }) + if (!('error' in result)) { + hold(operation) + return { + kind: 'created', + name: result.name, + ...(result.warning ? { warning: result.warning } : {}) + } + } + const taken = hasRuntimeRpcErrorCode(result.error, WORKTREE_CREATE_EXACT_NAME_TAKEN_CODE) + if (taken && (result.replayed || unresolved)) { + return settleUnresolved({ kind: 'maybe-created' }) + } + const needsSetup = result.error.includes(SETUP_DECISION_REQUIRED) + if (unresolved && needsSetup) { + // That refusal did no git work, but says nothing about the earlier create; it stays unconfirmed. + const held = hold(operation, false) + return held === undefined + ? { kind: 'maybe-created' } + : { kind: 'needs-setup-choice', hold: held } + } + if (unresolved) { + return settleUnresolved({ kind: 'failed', message: result.error }) + } + if (taken || needsSetup) { + const reserved = { ...operation, phase: 'composing' as const } + operations.set(folderRepoCreateKey(reserved), reserved) + notify() + return { kind: 'needs-composer', reservation: reserved.id } + } + release(operation) + return { kind: 'failed', message: result.error } + } catch (error) { + // Delivery unknown: the host may have created it, and a fresh create would be refused as taken. + if (isRpcDeliveryUnknown(error) || unresolved) { + return settleUnresolved({ kind: 'maybe-created' }) + } + release(operation) + return { kind: 'failed', message: error instanceof Error ? error.message : String(error) } + } +} + +/** A composer closed before sending anything gives its repository back. */ +export function releaseFolderComposerReservation( + target: FolderRepoTarget, + reservation: number +): void { + const operation = operations.get(folderRepoCreateKey(target)) + if (operation?.id === reservation && operation.phase === 'composing') { + release(operation) + } +} + +export type FolderComposerSubmission = { + /** `created` and `uncertain` are kept until the folder shows them; `failed` and `cancelled` give the repository back. */ + settle: (outcome: 'created' | 'uncertain' | 'failed' | 'cancelled') => void +} + +/** + * Admits the composer's create for the repository it actually picked, before anything is sent. + * Null while another create for that repository is out. + */ +export function beginFolderComposerSubmission( + target: FolderRepoTarget, + launch: { target: FolderRepoTarget; reservation: number; name: string }, + /** Whether the composer is still open to retry a refused create. */ + formOpen: () => boolean +): FolderComposerSubmission | null { + const key = folderRepoCreateKey(target) + const existing = operations.get(key) + const ownsReservation = + key === folderRepoCreateKey(launch.target) && + existing?.id === launch.reservation && + existing.phase === 'composing' + if (existing && !ownsReservation) { + return null + } + const operation = start(target, 'submitting', launch.name, 'composer') + return { + settle: (outcome) => { + if (outcome === 'created' || outcome === 'uncertain') { + hold(operation) + return + } + // A refusal leaves the form to try again; the reservation is back with the launch's repository. + if (ownsReservation && formOpen() && isCurrent(operation)) { + const reserved = { ...operation, phase: 'composing' as const, id: launch.reservation } + operations.set(key, reserved) + notify() + return + } + release(operation) + } + } +} diff --git a/mobile/src/source-control/folder/folder-source-repos-query.test.ts b/mobile/src/source-control/folder/folder-source-repos-query.test.ts new file mode 100644 index 00000000000..523143d4bc8 --- /dev/null +++ b/mobile/src/source-control/folder/folder-source-repos-query.test.ts @@ -0,0 +1,124 @@ +import { createElement } from 'react' +import { act, create, type ReactTestRenderer } from 'react-test-renderer' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { stubRpcClient } from '../../transport/rpc/rpc-client-test-stub' +import { useHostQuery } from '../../query/host-query' +import { scopeRpcClientToExecutionHost } from '../../worktree/execution-host-scoped-rpc-client' +import { + documentQueryClient, + settleQueryNotifications +} from '../../test-support/rpc-recording/hook-mount' +import { FakeSession } from '../../transport/connection/mobile-endpoint-supervisor-test-fakes' +import { + folderSourceCatalogQuery, + serverFolderSourceCatalogQuery, + selectFolderSourceRepos +} from './folder-source-repos-query' + +vi.mock('react-native', () => ({ AppState: {} })) + +const GROUP = { + id: 'group-1', + name: 'payments', + parentPath: '/src/payments', + parentGroupId: null, + createdFrom: 'folder', + tabOrder: 0, + isCollapsed: false, + color: null, + createdAt: 1, + updatedAt: 1 +} + +function hostWith(repos: unknown[]): FakeSession { + const client = new FakeSession('connected') + client.sendRequest.mockImplementation(async (method: string) => ({ + id: '1', + ok: true, + result: method === 'repo.list' ? { repos } : { groups: [GROUP] }, + _meta: { runtimeId: 'r' } + })) + return client +} + +const repo = (id: string, path: string) => ({ + id, + path, + displayName: id, + kind: 'git', + badgeColor: 'gray', + addedAt: 1 +}) + +describe('folder source repositories', () => { + it("offers a relayed server's repos in its folder", async () => { + const client = hostWith([repo('api', '/src/payments/api'), repo('elsewhere', '/src/other')]) + const catalog = await serverFolderSourceCatalogQuery.fetch!( + client, + { exec: 'runtime:env-1' }, + {}, + undefined, + { live: false, previousFromStore: false } + ) + + expect(selectFolderSourceRepos(catalog, 'group-1').map((found) => found.id)).toEqual(['api']) + }) +}) + +describe('folder source catalog reads', () => { + let renderer: ReactTestRenderer | null = null + const queries = documentQueryClient() + afterEach(async () => { + act(() => renderer?.unmount()) + renderer = null + await queries.end() + }) + + it.each([true, false])( + "reads a server folder's catalog on the desktop's one event stream (subscribed: %s)", + async (subscribed) => { + let eventStreams = 0 + const desktop = stubRpcClient({ + getLastConnectedAt: () => 1, + sendRequest: async (method) => ({ + id: '1', + ok: true, + result: method === 'repo.list' ? { repos: [] } : { groups: [GROUP] }, + _meta: { runtimeId: 'r' } + }), + subscribe: (method) => { + if (method === 'runtime.clientEvents.subscribe') { + eventStreams += 1 + } + return () => {} + } + }) + const server = scopeRpcClientToExecutionHost(desktop, 'runtime:env-1') + function Harness(): null { + useHostQuery( + folderSourceCatalogQuery, + { hostId: 'host-1', client: desktop }, + { exec: 'local' }, + {}, + { + subscribed: true + } + ) + useHostQuery( + serverFolderSourceCatalogQuery, + { hostId: 'host-1', client: server }, + { exec: 'runtime:env-1' }, + {}, + { subscribed } + ) + return null + } + await act(async () => { + renderer = create(queries.wrap(createElement(Harness))) + }) + await settleQueryNotifications() + + expect(eventStreams).toBe(1) + } + ) +}) diff --git a/mobile/src/source-control/folder/folder-source-repos-query.ts b/mobile/src/source-control/folder/folder-source-repos-query.ts new file mode 100644 index 00000000000..777b3c8fc26 --- /dev/null +++ b/mobile/src/source-control/folder/folder-source-repos-query.ts @@ -0,0 +1,73 @@ +import type { Repo } from '../../../../src/shared/repo-types' +import type { ProjectGroup } from '../../../../src/shared/project-group-types' +import { getFolderSourceRepos } from '../../../../src/shared/folder-workspaces/folder-workspace-source-repos' +import { + LOCAL_EXECUTION_HOST_ID, + getRepoExecutionHostId, + getSshTargetIdForExecutionHost +} from '../../../../src/shared/execution-host' +import { defineHostQuery } from '../../query/host-query' +import type { HostQueryTarget } from '../../query/host-query-key' +import type { RpcClient } from '../../transport/rpc/rpc-client-contract' +import { hostProjectGroupsRead } from '../../worktree/project-sections-operations' +import { listHostRepos, listServerRepos } from '../../worktree/server-repos' + +export type FolderSourceCatalog = { repos: Repo[]; groups: ProjectGroup[] } + +const NO_REPOS: Repo[] = [] + +const fetchFolderSourceCatalog = async ( + client: RpcClient, + target: HostQueryTarget +): Promise => { + const server = target.exec === LOCAL_EXECUTION_HOST_ID ? null : target.exec + const [repos, groupsRead] = await Promise.all([ + server ? listServerRepos(server, client) : listHostRepos(client), + hostProjectGroupsRead.request(client).then((reply) => hostProjectGroupsRead.interpret(reply)) + ]) + if (!repos || !groupsRead.accepted) { + throw new Error('Folder repositories unavailable') + } + // Repos from a server come stamped with it; its groups must match, or the host check drops every repo. + const groups = server + ? groupsRead.value.map((group) => ({ ...group, executionHostId: server })) + : groupsRead.value + return { repos, groups } +} + +/** The paired desktop's own repositories and project groups, re-read when its repos change. */ +export const folderSourceCatalogQuery = defineHostQuery, FolderSourceCatalog>( + { + family: 'folder.sourceCatalog', + fetch: fetchFolderSourceCatalog, + staleTime: 60_000, + refetchOnWindowFocus: true, + signal: { events: ['reposChanged'] } + } +) + +// A relayed server's changes are not signalled, so its reads hold no event stream; focus and refresh read again. +export const serverFolderSourceCatalogQuery = defineHostQuery< + Record, + FolderSourceCatalog +>({ + family: 'folder.sourceCatalog.server', + fetch: fetchFolderSourceCatalog, + staleTime: 60_000, + refetchOnWindowFocus: true +}) + +/** The folder's repositories a worktree can be created in, as desktop's empty state lists them. */ +export function selectFolderSourceRepos( + catalog: FolderSourceCatalog | undefined, + projectGroupId: string | null +): Repo[] { + if (!catalog || !projectGroupId) { + return NO_REPOS + } + const group = catalog.groups.find((candidate) => candidate.id === projectGroupId) ?? null + // Direct SSH hosts can't create worktrees from here (managed hosts route as runtimes). + return getFolderSourceRepos(catalog.repos, catalog.groups, group).filter( + (repo) => !getSshTargetIdForExecutionHost(getRepoExecutionHostId(repo)) + ) +} diff --git a/mobile/src/source-control/folder/mobile-folder-attached-worktrees.ts b/mobile/src/source-control/folder/mobile-folder-attached-worktrees.ts index 5fbae1707b5..6ce2e8040b4 100644 --- a/mobile/src/source-control/folder/mobile-folder-attached-worktrees.ts +++ b/mobile/src/source-control/folder/mobile-folder-attached-worktrees.ts @@ -11,7 +11,7 @@ import { serializeFolderLineage } from './folder-lineage-serialization' /** What a folder list row shows of a host row. */ export type FolderListRow = Pick< Worktree, - 'worktreeId' | 'hostId' | 'repo' | 'displayName' | 'branch' + 'worktreeId' | 'hostId' | 'repoId' | 'repo' | 'displayName' | 'branch' > type FolderLineageRow = MobileLineageRow & Pick @@ -36,6 +36,7 @@ export function toFolderListRow(row: Worktree): FolderListRow { listRow = { worktreeId: row.worktreeId, hostId: row.hostId, + repoId: row.repoId, repo: row.repo, displayName: row.displayName, branch: row.branch diff --git a/mobile/src/source-control/folder/use-folder-create-flow.test.tsx b/mobile/src/source-control/folder/use-folder-create-flow.test.tsx new file mode 100644 index 00000000000..c59d2644858 --- /dev/null +++ b/mobile/src/source-control/folder/use-folder-create-flow.test.tsx @@ -0,0 +1,343 @@ +import { createElement, Fragment } from 'react' +import { act, create, type ReactTestRenderer } from 'react-test-renderer' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { FakeSession } from '../../transport/connection/mobile-endpoint-supervisor-test-fakes' +import { markRpcDeliveryUnknown } from '../../transport/rpc/rpc-delivery-ambiguity' +import type { RpcResponse } from '../../transport/rpc/rpc-wire-types' +import type { NewWorktreeModalProps } from '../../components/new-worktree-modal-types' +import type { FolderListRow } from './mobile-folder-attached-worktrees' +import { isFolderRepoCreateBusy, type FolderRepoTarget } from './folder-repo-worktree-create' +import { useFolderCreateFlow, type FolderCreateRepo } from './use-folder-create-flow' + +const seen = vi.hoisted(() => { + const state: { + composer: NewWorktreeModalProps | null + confirm: { visible: boolean; onConfirm?: () => void; onCancel: () => void } | null + toasts: string[] + haptics: number + } = { composer: null, confirm: null, toasts: [], haptics: 0 } + return state +}) +vi.mock('react-native', () => ({ AppState: {} })) +vi.mock('../../components/NewWorktreeModalController', () => ({ + NewWorktreeModalController: (props: NewWorktreeModalProps) => { + seen.composer = props + return null + } +})) +vi.mock('../../components/ConfirmModal', () => ({ + ConfirmModal: (props: { visible: boolean; onConfirm?: () => void; onCancel: () => void }) => { + seen.confirm = props + return null + } +})) +vi.mock('../../components/TransientToast', () => ({ + TransientToast: () => null, + useTransientToast: () => ({ + message: null, + showToast: (text: string) => seen.toasts.push(text) + }) +})) +vi.mock('../../platform/haptics', () => ({ + triggerSuccess: () => (seen.haptics += 1), + triggerError: () => (seen.haptics += 1) +})) +vi.mock('../../platform/external-link', () => ({ openExternalLink: () => {} })) +vi.mock('../../transport/connection/host-client-hooks', () => ({ + useHostClient: () => ({ client: null, state: 'connected' }) +})) +vi.mock('../../tasks/worktree-create-capability', () => ({ + readNewWorktreeRuntimeCapabilities: async () => ({ worktreeCreateIdempotency: false }) +})) +vi.mock('../../i18n/use-mobile-translation', () => ({ + useMobileTranslation: () => (_key: string, fallback: string) => fallback +})) + +const FOLDER = 'folder:f-1' +const TAKEN = 'Branch payments already exists\nworktree_create_exact_name_taken' +const CREATED: RpcResponse = { + id: '1', + ok: true, + result: { worktree: { id: 'repo::/w/payments', displayName: 'payments' } }, + _meta: { runtimeId: 'r' } +} + +let repoCount = 0 +function repoIn(): { repo: FolderCreateRepo; target: FolderRepoTarget } { + repoCount += 1 + const repoId = `flow-repo-${repoCount}` + return { + repo: { id: repoId, displayName: repoId }, + target: { hostId: 'host-1', executionHost: undefined, folderWorktreeId: FOLDER, repoId } + } +} + +function replying(reply: RpcResponse) { + const client = new FakeSession('connected') + let answer: () => void = () => {} + client.sendRequest.mockReturnValue( + new Promise((resolve) => (answer = () => resolve(reply))) + ) + return { client, answer: () => answer() } +} + +type Flow = ReturnType +let renderer: ReactTestRenderer | null = null + +function mountFlow(refetch: () => void) { + const flow: { current: Flow | null } = { current: null } + let members: FolderListRow[] = [] + function Harness() { + flow.current = useFolderCreateFlow({ + hostId: 'host-1', + folderWorktreeId: FOLDER, + executionHost: undefined, + desktopCapabilities: [], + members, + observing: true, + refetch, + bottomInset: 0 + }) + return createElement(Fragment, null, flow.current.composerElement) + } + act(() => { + renderer = create(createElement(Harness)) + }) + return { + flow, + rerender: (next: FolderListRow[]) => { + members = next + act(() => renderer?.update(createElement(Harness))) + } + } +} + +const composer = (): NewWorktreeModalProps => { + if (!seen.composer) { + throw new Error('the composer never rendered') + } + return seen.composer +} + +afterEach(() => { + vi.useRealTimers() + act(() => renderer?.unmount()) + renderer = null + seen.composer = null + seen.confirm = null + seen.toasts = [] + seen.haptics = 0 +}) + +describe('useFolderCreateFlow', () => { + it("keeps an open form when another repository's create lands", async () => { + const { flow, rerender } = mountFlow(() => {}) + const b = repoIn() + const refusal = replying({ id: '1', ok: false, error: { code: 'x', message: TAKEN } }) + const opening = flow.current!.onCreate(b.repo, b.target, refusal.client, 'payments') + refusal.answer() + await act(() => opening) + expect(composer().folderLaunch?.repoId).toBe(b.target.repoId) + + const a = repoIn() + const creating = replying(CREATED) + const created = flow.current!.onCreate(a.repo, a.target, creating.client, 'payments') + creating.answer() + await act(() => created) + rerender([ + { + worktreeId: 'a-child', + repoId: a.target.repoId, + repo: 'a', + displayName: 'payments', + branch: 'payments' + } + ]) + + expect(composer().visible).toBe(true) + expect(composer().folderLaunch?.repoId).toBe(b.target.repoId) + }) + + it('shows nothing for a create that lands after the screen is left, and still refreshes', async () => { + const refetch = vi.fn() + const { flow } = mountFlow(refetch) + const a = repoIn() + const creating = replying(CREATED) + const created = flow.current!.onCreate(a.repo, a.target, creating.client, 'payments') + + act(() => renderer?.unmount()) + renderer = null + creating.answer() + await created + + expect(refetch).toHaveBeenCalled() + expect(seen.toasts).toEqual([]) + expect(seen.haptics).toBe(0) + }) + + async function openComposer(flow: { current: Flow | null }, repo = repoIn()) { + const refusal = replying({ id: '1', ok: false, error: { code: 'x', message: TAKEN } }) + const opening = flow.current!.onCreate(repo.repo, repo.target, refusal.client, 'payments') + refusal.answer() + await act(() => opening) + return composer() + } + + it("doesn't let an earlier form's close end a newer one", async () => { + const { flow } = mountFlow(() => {}) + const open = () => openComposer(flow) + const first = await open() + act(() => first.onClose()) + const second = await open() + + act(() => first.onClose()) + expect(composer().visible).toBe(true) + expect(composer().folderLaunch?.repoId).toBe(second.folderLaunch?.repoId) + }) + it("shows nothing for an earlier form's create once a newer form is open", async () => { + const { flow } = mountFlow(() => {}) + const first = await openComposer(flow) + act(() => first.onClose()) + const second = await openComposer(flow) + + expect(first.folderLaunch?.submit.presenting()).toBe(false) + act(() => first.onCreated('repo::/w/payments-2', 'payments-2')) + expect(seen.toasts).toEqual([]) + expect(second.folderLaunch?.submit.presenting()).toBe(true) + }) + + it('gives the repository back when the screen is left with an unsent form', async () => { + const { flow } = mountFlow(() => {}) + const repo = repoIn() + await openComposer(flow, repo) + expect(isFolderRepoCreateBusy(repo.target)).toBe(true) + + act(() => renderer?.unmount()) + renderer = null + expect(isFolderRepoCreateBusy(repo.target)).toBe(false) + }) + it('resends an unconfirmed create without setup once the user agrees', async () => { + vi.useFakeTimers({ toFake: ['setTimeout'] }) + const { flow } = mountFlow(() => {}) + const a = repoIn() + const lost = new FakeSession('connected') + lost.sendRequest.mockRejectedValueOnce(markRpcDeliveryUnknown(new Error('connection lost'))) + await act(() => flow.current!.onCreate(a.repo, a.target, lost, 'payments')) + vi.advanceTimersByTime(60_000) + + const retry = new FakeSession('connected') + retry.sendRequest + .mockResolvedValueOnce({ + id: '1', + ok: false, + error: { code: 'x', message: 'Setup decision required for this repository' } + }) + .mockResolvedValueOnce(CREATED) + await act(() => flow.current!.onCreate(a.repo, a.target, retry, 'payments')) + expect(seen.confirm?.visible).toBe(true) + + await act(async () => seen.confirm?.onConfirm?.()) + expect(retry.sendRequest.mock.calls[1]?.[1]).toMatchObject({ + name: 'payments', + branchIntent: 'create-exact', + setupDecision: 'skip' + }) + }) + it("refreshes the folder when a form's create may have landed", async () => { + const refetch = vi.fn() + const { flow } = mountFlow(refetch) + const b = repoIn() + const form = await openComposer(flow, b) + + form.folderLaunch?.submit.begin({ ...b.repo, path: '/w/b' })?.settle('uncertain') + expect(refetch).toHaveBeenCalledTimes(1) + }) + async function setupRefusedRetry(flow: { current: Flow | null }) { + const a = repoIn() + const lost = new FakeSession('connected') + lost.sendRequest.mockRejectedValueOnce(markRpcDeliveryUnknown(new Error('connection lost'))) + await act(() => flow.current!.onCreate(a.repo, a.target, lost, 'payments')) + vi.advanceTimersByTime(60_000) + const retry = replying({ + id: '1', + ok: false, + error: { code: 'x', message: 'Setup decision required for this repository' } + }) + return { a, pending: flow.current!.onCreate(a.repo, a.target, retry.client, 'payments'), retry } + } + + it("doesn't open a setup choice over another form, and the repository can retry after", async () => { + vi.useFakeTimers({ toFake: ['setTimeout'] }) + const { flow } = mountFlow(() => {}) + const { a, pending, retry } = await setupRefusedRetry(flow) + const form = await openComposer(flow) + retry.answer() + await act(() => pending) + + expect(seen.confirm?.visible).toBe(false) + expect(composer().folderLaunch?.repoId).toBe(form.folderLaunch?.repoId) + expect(isFolderRepoCreateBusy(a.target)).toBe(false) + }) + + it("doesn't open a composer over a setup choice, and covers the list while it is open", async () => { + vi.useFakeTimers({ toFake: ['setTimeout'] }) + const { flow } = mountFlow(() => {}) + const { pending, retry } = await setupRefusedRetry(flow) + retry.answer() + await act(() => pending) + expect(seen.confirm?.visible).toBe(true) + expect(flow.current!.covered).toBe(true) + + const b = repoIn() + const refusal = replying({ id: '1', ok: false, error: { code: 'x', message: TAKEN } }) + const opening = flow.current!.onCreate(b.repo, b.target, refusal.client, 'payments') + refusal.answer() + await act(() => opening) + expect(composer().visible).toBe(false) + expect(isFolderRepoCreateBusy(b.target)).toBe(false) + + act(() => seen.confirm?.onCancel()) + expect(flow.current!.covered).toBe(false) + }) + it('closes a setup choice once the folder lists its create, and its stale confirm sends nothing', async () => { + vi.useFakeTimers({ toFake: ['setTimeout'] }) + const { flow, rerender } = mountFlow(() => {}) + const { a, pending, retry } = await setupRefusedRetry(flow) + retry.answer() + await act(() => pending) + const staleConfirm = seen.confirm?.onConfirm + expect(seen.confirm?.visible).toBe(true) + + rerender([ + { + worktreeId: 'a-child', + repoId: a.target.repoId, + repo: 'a', + displayName: 'payments', + branch: 'payments' + } + ]) + expect(seen.confirm?.visible).toBe(false) + await act(async () => staleConfirm?.()) + expect(retry.client.sendRequest).toHaveBeenCalledTimes(1) + expect(seen.composer?.visible).toBe(false) + }) + + it("shows nothing for an older form's create once a setup choice opens, and still refreshes", async () => { + vi.useFakeTimers({ toFake: ['setTimeout'] }) + const refetch = vi.fn() + const { flow } = mountFlow(refetch) + const form = await openComposer(flow) + act(() => form.onClose()) + const { pending, retry } = await setupRefusedRetry(flow) + retry.answer() + await act(() => pending) + expect(seen.confirm?.visible).toBe(true) + refetch.mockClear() + + expect(form.folderLaunch?.submit.presenting()).toBe(false) + act(() => form.onCreated('repo::/w/payments', 'payments')) + expect(seen.toasts).toEqual([]) + expect(refetch).toHaveBeenCalledTimes(1) + }) +}) diff --git a/mobile/src/source-control/folder/use-folder-create-flow.tsx b/mobile/src/source-control/folder/use-folder-create-flow.tsx new file mode 100644 index 00000000000..899a510ea38 --- /dev/null +++ b/mobile/src/source-control/folder/use-folder-create-flow.tsx @@ -0,0 +1,284 @@ +import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from 'react' +import type { Repo } from '../../../../src/shared/repo-types' +import { getRepoExecutionHostId } from '../../../../src/shared/execution-host' +import { ConfirmModal } from '../../components/ConfirmModal' +import { NewWorktreeModalController } from '../../components/NewWorktreeModalController' +import type { MobileWorkspaceRepo } from '../../components/new-worktree-modal-types' +import { TransientToast, useTransientToast } from '../../components/TransientToast' +import { useMobileTranslation } from '../../i18n/use-mobile-translation' +import { openExternalLink } from '../../platform/external-link' +import { triggerError, triggerSuccess } from '../../platform/haptics' +import { useHostClient } from '../../transport/connection/host-client-hooks' +import type { RpcClient } from '../../transport/rpc/rpc-client-contract' +import type { FolderListRow } from './mobile-folder-attached-worktrees' +import { + beginFolderComposerSubmission, + createFolderRepoWorktree, + getFolderRepoCreatesVersion, + isFolderRepoHoldPending, + reconcileFolderRepoCreates, + releaseFolderComposerReservation, + subscribeFolderRepoCreates, + type FolderRepoTarget +} from './folder-repo-worktree-create' + +const NO_LABELS = new Map() + +/** What a create reads of a repository: its id, name and host. */ +export type FolderCreateRepo = Pick + +type SetupChoice = { + /** The unconfirmed create it resends; once that is gone the dialog closes and sends nothing. */ + hold: number + repo: FolderCreateRepo + target: FolderRepoTarget + client: RpcClient + name: string +} + +type ComposerLaunch = { + target: FolderRepoTarget + reservation: number + executionHostId: ReturnType + name: string +} + +/** + * The folder screen's creates: direct exact-name creates, the composer they fall back to, and their + * toasts. Kept above the empty state, so another repository's new card can't close a form being edited. + */ +export function useFolderCreateFlow(args: { + hostId: string + folderWorktreeId: string + /** The folder's server; undefined for the paired desktop's own. */ + executionHost: string | undefined + desktopCapabilities: readonly string[] + members: readonly FolderListRow[] + observing: boolean + refetch: () => void + bottomInset: number +}) { + const { hostId, folderWorktreeId, executionHost, members, observing, refetch } = args + const t = useMobileTranslation() + const { client: desktopClient } = useHostClient(hostId) + const { message, showToast } = useTransientToast() + const [composer, setComposer] = useState(null) + const [setupChoice, setSetupChoice] = useState(null) + // The one form on screen, composer or setup choice. Set as forms open and close, not after render, so + // two answers landing together can't both open one. + const openRef = useRef(null) + // The newest form opened, still set after it closes: an older form's late answer shows nothing over it. + const latestRef = useRef(null) + // Late answers show nothing once this screen is left or covered; the create itself still settles. + const presentingRef = useRef(observing) + useEffect(() => { + presentingRef.current = observing + }, [observing]) + useEffect( + () => () => { + presentingRef.current = false + latestRef.current = null + const open = openRef.current + openRef.current = null + // Left without sending from the form: its repository takes taps again. + if (open && 'reservation' in open) { + releaseFolderComposerReservation(open.target, open.reservation) + } + }, + [] + ) + const presenting = useCallback(() => presentingRef.current, []) + + // A created or unconfirmed create is done once the folder lists a worktree from its repository. + const createsVersion = useSyncExternalStore( + subscribeFolderRepoCreates, + getFolderRepoCreatesVersion + ) + useEffect(() => { + reconcileFolderRepoCreates({ hostId, executionHost, folderWorktreeId }, members) + }, [createsVersion, executionHost, folderWorktreeId, hostId, members]) + + const onCreate = useCallback( + async ( + repo: FolderCreateRepo, + target: FolderRepoTarget, + client: RpcClient, + name: string, + skipSetupFor?: number + ) => { + const outcome = await createFolderRepoWorktree({ target, client, name, skipSetupFor }) + if ( + outcome.kind === 'created' || + outcome.kind === 'maybe-created' || + outcome.kind === 'checking' + ) { + refetch() + } + const opensForm = outcome.kind === 'needs-composer' || outcome.kind === 'needs-setup-choice' + if (opensForm && (!presenting() || openRef.current)) { + // Left, or another form is open: keep that form and leave this repository to try again. + if (outcome.kind === 'needs-composer') { + releaseFolderComposerReservation(target, outcome.reservation) + } + return + } + if (!presenting()) { + return + } + if (outcome.kind === 'created') { + triggerSuccess() + showToast( + outcome.warning ?? + t('sourceControl.folder.created', 'Created worktree {{name}} in {{repo}}', { + name: outcome.name, + repo: repo.displayName + }) + ) + } else if (outcome.kind === 'checking') { + showToast( + t( + 'sourceControl.folder.checking', + 'A worktree from {{repo}} may already have been created. Checking the folder…', + { repo: repo.displayName } + ) + ) + } else if (outcome.kind === 'needs-setup-choice') { + const choice = { hold: outcome.hold, repo, target, client, name } + openRef.current = choice + latestRef.current = choice + setSetupChoice(choice) + } else if (outcome.kind === 'needs-composer') { + const launch = { + target, + reservation: outcome.reservation, + executionHostId: getRepoExecutionHostId(repo), + name + } + openRef.current = launch + latestRef.current = launch + setComposer(launch) + } else if (outcome.kind === 'failed') { + triggerError() + showToast( + outcome.message || + t('sourceControl.folder.createFailed', 'Could not create a worktree in {{repo}}.', { + repo: repo.displayName + }) + ) + } + }, + [presenting, refetch, showToast, t] + ) + + const launch = composer + const presentingLaunch = (opened: ComposerLaunch | null) => + presenting() && opened !== null && latestRef.current === opened + const composerElement = ( + { + const submission = beginFolderComposerSubmission( + { ...launch.target, repoId: repo.id }, + launch, + () => openRef.current === launch + ) + return ( + submission && { + settle: (outcome) => { + submission.settle(outcome) + // A lost reply may have created it: the list shows it, or the form keeps it from being sent twice. + if (outcome === 'uncertain') { + refetch() + } + } + } + ) + }, + presenting: () => presentingLaunch(launch) + } + } + } + : {})} + onCreated={(_worktreeId, createdName, warning) => { + refetch() + if (presentingLaunch(launch)) { + showToast( + warning ?? + t('sourceControl.folder.createdNamed', 'Created worktree {{name}}', { + name: createdName + }) + ) + } + }} + onClose={() => { + // Only this opening's close: an older submission settling can't close a newer form. + if (launch) { + releaseFolderComposerReservation(launch.target, launch.reservation) + if (openRef.current === launch) { + openRef.current = null + } + } + setComposer((current) => (current === launch ? null : current)) + }} + /> + ) + const choice = setupChoice + // Only this dialog's own answer: it closes itself and nothing newer. + const closeChoice = useCallback((closing: SetupChoice | null) => { + if (openRef.current === closing) { + openRef.current = null + } + setSetupChoice((current) => (current === closing ? null : current)) + }, []) + // The folder showed it, or another create replaced it: there is nothing left to retry. + useEffect(() => { + if (choice && !isFolderRepoHoldPending(choice.target, choice.hold)) { + closeChoice(choice) + } + }, [choice, closeChoice, createsVersion]) + const setupChoiceElement = ( + { + if (choice && openRef.current === choice) { + closeChoice(choice) + void onCreate(choice.repo, choice.target, choice.client, choice.name, choice.hold) + } + }} + onCancel={() => closeChoice(choice)} + /> + ) + const toastElement = + return { + onCreate, + /** A form covers the screen: the list under it waits. */ + covered: composer !== null || setupChoice !== null, + composerElement: ( + <> + {composerElement} + {setupChoiceElement} + + ), + toastElement + } +} diff --git a/mobile/src/tasks/blank-workspace-create.ts b/mobile/src/tasks/blank-workspace-create.ts index 5cffd77a7db..05ad977ad21 100644 --- a/mobile/src/tasks/blank-workspace-create.ts +++ b/mobile/src/tasks/blank-workspace-create.ts @@ -1,6 +1,10 @@ import type { TuiAgent } from '../../../src/shared/agent-launch/catalog/tui-agent' import type { RpcClient } from '../transport/rpc/rpc-client-contract' -import { createWorktreeWithNameRetry, type WorktreeCreateResult } from './worktree-create-retry' +import { + createWorktreeWithNameRetry, + type WorktreeCreateParent, + type WorktreeCreateResult +} from './worktree-create-retry' import type { WorktreeCreateAgentLaunch } from './agent-launch-request' import type { WorktreeCreateIdempotencyProbe } from './worktree-create-idempotency-policy' import { @@ -20,6 +24,7 @@ export async function createBlankWorkspace(args: { nameWasGenerated: boolean worktreeCreateIdempotency: WorktreeCreateIdempotencyProbe agentLaunchSupported: WorktreeCreateAgentLaunch['supported'] + parent?: WorktreeCreateParent }): Promise { const agentLaunch: WorktreeCreateAgentLaunch | undefined = args.createdWithAgentId ? { agent: args.createdWithAgentId, supported: args.agentLaunchSupported } @@ -28,6 +33,7 @@ export async function createBlankWorkspace(args: { client: args.client, baseName: args.baseName, nameWasGenerated: args.nameWasGenerated, + parent: args.parent, worktreeCreateIdempotency: args.worktreeCreateIdempotency, ...(agentLaunch ? { agentLaunch } : {}), buildParams: (name) => { diff --git a/mobile/src/tasks/composer-work-item-task.ts b/mobile/src/tasks/composer-work-item-task.ts new file mode 100644 index 00000000000..faaa71adb3b --- /dev/null +++ b/mobile/src/tasks/composer-work-item-task.ts @@ -0,0 +1,55 @@ +import type { MobileLinkedWorkItem } from './mobile-composer-source-types' +import type { WorkspaceCreateTaskItem } from './workspace-create-params' + +/** The composer's linked issue, PR or ticket, as the host's task-workspace source. */ +export function toTaskItem( + item: MobileLinkedWorkItem, + targetRepoId: string +): WorkspaceCreateTaskItem { + if (item.provider === 'github') { + return { + provider: 'github', + source: { + type: item.type === 'pr' ? 'pr' : 'issue', + repoId: item.repoId ?? targetRepoId, + number: item.number, + title: item.title, + url: item.url + } + } + } + if (item.provider === 'gitlab') { + return { + provider: 'gitlab', + source: { + type: item.type === 'mr' ? 'mr' : 'issue', + repoId: item.repoId ?? targetRepoId, + number: item.number, + title: item.title, + url: item.url + } + } + } + if (item.provider === 'jira') { + return { + provider: 'jira', + source: { + key: item.jiraIdentifier ?? '', + title: item.title, + url: item.url + } + } + } + return { + provider: 'linear', + source: { + identifier: item.linearIdentifier ?? '', + title: item.title, + url: item.url, + ...(item.linearWorkspaceId ? { workspaceId: item.linearWorkspaceId } : {}), + ...(item.linearOrganizationUrlKey + ? { organizationUrlKey: item.linearOrganizationUrlKey } + : {}) + } + } +} diff --git a/mobile/src/tasks/source-workspace-create.ts b/mobile/src/tasks/source-workspace-create.ts index d86844b6058..50368bbd580 100644 --- a/mobile/src/tasks/source-workspace-create.ts +++ b/mobile/src/tasks/source-workspace-create.ts @@ -1,20 +1,21 @@ import type { TuiAgent } from '../../../src/shared/agent-launch/catalog/tui-agent' import type { RpcClient } from '../transport/rpc/rpc-client-contract' import { resolveComposerMrBase, resolveComposerPrBase } from './composer-source-base-resolve' -import type { - MobileComposerCreateSelection, - MobileLinkedWorkItem -} from './mobile-composer-source-types' +import type { MobileComposerCreateSelection } from './mobile-composer-source-types' import { resolveMobileWorkspaceCreateName } from './mobile-workspace-name' import type { WorkspaceAgentChoice } from './workspace-agent-selection' +import { toTaskItem } from './composer-work-item-task' import { startupAgentCreateFields, buildTaskWorkspaceCreateParams, type WorkspaceCreateParams, - type WorkspaceCreateSetupDecision, - type WorkspaceCreateTaskItem + type WorkspaceCreateSetupDecision } from './workspace-create-params' -import { createWorktreeWithNameRetry, type WorktreeCreateResult } from './worktree-create-retry' +import { + createWorktreeWithNameRetry, + type WorktreeCreateParent, + type WorktreeCreateResult +} from './worktree-create-retry' import type { WorktreeCreateAgentLaunch } from './agent-launch-request' import type { WorktreeCreateIdempotencyProbe } from './worktree-create-idempotency-policy' import { getClientWorktreeCreateCandidate } from '../../../src/shared/new-workspace/worktree-create-retry-policy' @@ -36,6 +37,7 @@ export type CreateWorkspaceFromComposerArgs = { worktreeCreateIdempotency: WorktreeCreateIdempotencyProbe agentLaunchSupported: WorktreeCreateAgentLaunch['supported'] branchIntentSupported?: boolean | Promise + parent?: WorktreeCreateParent } export async function createWorkspaceFromComposerSource( @@ -57,55 +59,6 @@ function resolveComposerAgentLaunch( return agentId ? { agent: agentId, supported } : undefined } -function toTaskItem(item: MobileLinkedWorkItem, targetRepoId: string): WorkspaceCreateTaskItem { - if (item.provider === 'github') { - return { - provider: 'github', - source: { - type: item.type === 'pr' ? 'pr' : 'issue', - repoId: item.repoId ?? targetRepoId, - number: item.number, - title: item.title, - url: item.url - } - } - } - if (item.provider === 'gitlab') { - return { - provider: 'gitlab', - source: { - type: item.type === 'mr' ? 'mr' : 'issue', - repoId: item.repoId ?? targetRepoId, - number: item.number, - title: item.title, - url: item.url - } - } - } - if (item.provider === 'jira') { - return { - provider: 'jira', - source: { - key: item.jiraIdentifier ?? '', - title: item.title, - url: item.url - } - } - } - return { - provider: 'linear', - source: { - identifier: item.linearIdentifier ?? '', - title: item.title, - url: item.url, - ...(item.linearWorkspaceId ? { workspaceId: item.linearWorkspaceId } : {}), - ...(item.linearOrganizationUrlKey - ? { organizationUrlKey: item.linearOrganizationUrlKey } - : {}) - } - } -} - async function createWorkItemWorkspace(args: { client: RpcClient selection: Extract @@ -116,6 +69,7 @@ async function createWorkItemWorkspace(args: { nameIsAutoManaged?: boolean note: string | undefined worktreeCreateIdempotency: WorktreeCreateIdempotencyProbe + parent?: WorktreeCreateParent }): Promise { const { client, selection, targetRepoId, setupDecision, agent, workspaceName, note } = args const item = selection.item @@ -157,6 +111,7 @@ async function createWorkItemWorkspace(args: { const baseName = String(params.name) // Keep linked work items on terminals: structured sessions cannot hold their unsent URL draft and would submit it as a turn. return createWorktreeWithNameRetry({ + parent: args.parent, client, baseName, worktreeCreateIdempotency: args.worktreeCreateIdempotency, @@ -176,6 +131,7 @@ async function createBranchWorkspace(args: { worktreeCreateIdempotency: WorktreeCreateIdempotencyProbe agentLaunchSupported: WorktreeCreateAgentLaunch['supported'] branchIntentSupported?: boolean | Promise + parent?: WorktreeCreateParent }): Promise { const { client, @@ -206,6 +162,7 @@ async function createBranchWorkspace(args: { fallback: selection.localBranchName }) return createWorktreeWithNameRetry({ + parent: args.parent, client, baseName, worktreeCreateIdempotency: args.worktreeCreateIdempotency, @@ -232,6 +189,7 @@ async function createBranchWorkspace(args: { fallback: selection.branchNameOverride || selection.localBranchName }) return createWorktreeWithNameRetry({ + parent: args.parent, client, baseName, worktreeCreateIdempotency: args.worktreeCreateIdempotency, @@ -270,6 +228,7 @@ async function createNewBranchWorkspace(args: { note: string | undefined worktreeCreateIdempotency: WorktreeCreateIdempotencyProbe agentLaunchSupported: WorktreeCreateAgentLaunch['supported'] + parent?: WorktreeCreateParent }): Promise { const { client, @@ -287,6 +246,7 @@ async function createNewBranchWorkspace(args: { const comment = note?.trim() // Preserve typed git branch names, including slashes; sanitize only the folder, and retry suffixes against the branch name. return createWorktreeWithNameRetry({ + parent: args.parent, client, baseName: selection.branchName, worktreeCreateIdempotency: args.worktreeCreateIdempotency, diff --git a/mobile/src/tasks/worktree-create-retry.test.ts b/mobile/src/tasks/worktree-create-retry.test.ts new file mode 100644 index 00000000000..175cd7c9860 --- /dev/null +++ b/mobile/src/tasks/worktree-create-retry.test.ts @@ -0,0 +1,148 @@ +import { describe, expect, it } from 'vitest' +import { FakeSession } from '../transport/connection/mobile-endpoint-supervisor-test-fakes' +import type { RpcClient } from '../transport/rpc/rpc-client-contract' +import type { RpcResponse } from '../transport/rpc/rpc-wire-types' +import { markRpcDeliveryUnknown } from '../transport/rpc/rpc-delivery-ambiguity' + +import { createWorktreeWithNameRetry } from './worktree-create-retry' + +type Attempt = { method: string; params: unknown } + +function scriptedClient( + outcomes: ({ id: string; displayName?: string; warning?: string } | { errorMessage: string })[], + attempts: Attempt[] +): RpcClient { + let call = 0 + const client = new FakeSession('connected') + client.sendRequest.mockImplementation(async (method: string, params?: unknown) => { + attempts.push({ method, params: params ?? {} }) + const outcome = outcomes[Math.min(call, outcomes.length - 1)]! + call += 1 + if ('errorMessage' in outcome) { + return { + id: '1', + ok: false, + error: { code: 'x', message: outcome.errorMessage }, + _meta: { runtimeId: 'r' } + } + } + return { + id: '1', + ok: true, + result: { + worktree: { + id: outcome.id, + ...(outcome.displayName !== undefined ? { displayName: outcome.displayName } : {}) + }, + ...(outcome.warning !== undefined ? { warning: outcome.warning } : {}) + }, + _meta: { runtimeId: 'r' } + } + }) + return client +} + +describe('createWorktreeWithNameRetry parent', () => { + it('attaches every collision candidate to the same parent', async () => { + const attempts: Attempt[] = [] + const client = scriptedClient( + [{ errorMessage: 'already exists locally' }, { id: 'wt-child' }], + attempts + ) + + await createWorktreeWithNameRetry({ + client, + baseName: 'topic', + buildParams: (name) => ({ repo: 'id:r', name }), + parent: { parentWorkspace: 'folder:f-1', parentWorkspaceOrigin: 'manual' }, + worktreeCreateIdempotency: false + }) + + expect(attempts.map((attempt) => attempt.params)).toEqual([ + { + repo: 'id:r', + name: 'topic', + parentWorkspace: 'folder:f-1', + parentWorkspaceOrigin: 'manual' + }, + { + repo: 'id:r', + name: 'topic-2', + parentWorkspace: 'folder:f-1', + parentWorkspaceOrigin: 'manual' + } + ]) + }) +}) + +describe('createWorktreeWithNameRetry after a lost reply', () => { + it('tries no other name when the resend is refused as taken', async () => { + const client = new FakeSession('connected') + client.sendRequest + .mockImplementationOnce(async () => { + client.publishState('reconnecting') + setTimeout(() => client.publishState('connected'), 0) + throw markRpcDeliveryUnknown(new Error('connection lost')) + }) + .mockResolvedValue({ + id: '1', + ok: false, + error: { code: 'x', message: 'already exists locally' }, + _meta: { runtimeId: 'r' } + }) + + const result = await createWorktreeWithNameRetry({ + client, + baseName: 'topic', + buildParams: (name) => ({ repo: 'id:r', name }), + worktreeCreateIdempotency: { dedupeTtlMs: 60_000 } + }) + + expect(result).toMatchObject({ replayed: true }) + expect(client.sendRequest.mock.calls.map(([, params]) => params)).toEqual([ + expect.objectContaining({ name: 'topic' }), + expect.objectContaining({ name: 'topic' }) + ]) + }) + it('tries no other name when a fallback create after a refused agent launch is resent and refused', async () => { + const client = new FakeSession('connected') + const taken: RpcResponse = { + id: '1', + ok: false, + error: { code: 'x', message: 'already exists locally' }, + _meta: { runtimeId: 'r' } + } + client.sendRequest + .mockResolvedValueOnce({ + id: '1', + ok: false, + error: { code: 'method_not_found', message: 'Unknown method' }, + _meta: { runtimeId: 'r' } + }) + .mockImplementationOnce(async () => { + client.publishState('reconnecting') + setTimeout(() => client.publishState('connected'), 0) + throw markRpcDeliveryUnknown(new Error('connection lost')) + }) + .mockResolvedValue(taken) + + const result = await createWorktreeWithNameRetry({ + client, + baseName: 'topic', + buildParams: (name) => ({ repo: 'id:r', name }), + worktreeCreateIdempotency: { dedupeTtlMs: 60_000 }, + agentLaunch: { agent: 'claude', supported: { replay: false } } + }) + + expect(result).toMatchObject({ replayed: true }) + expect(client.sendRequest.mock.calls.map(([method]) => method)).toEqual([ + 'agent.launch', + 'worktree.create', + 'worktree.create' + ]) + expect(client.sendRequest.mock.calls.slice(1).map(([, params]) => params)).toEqual([ + expect.objectContaining({ name: 'topic' }), + expect.objectContaining({ name: 'topic' }) + ]) + }) +}) diff --git a/mobile/src/tasks/worktree-create-retry.ts b/mobile/src/tasks/worktree-create-retry.ts index 943ff260096..e85c4dcd67b 100644 --- a/mobile/src/tasks/worktree-create-retry.ts +++ b/mobile/src/tasks/worktree-create-retry.ts @@ -35,7 +35,12 @@ import { // Branches and remote PRs outlive worktrees and escape local preflight dedupe; retry host collision errors with numeric suffixes. export type WorktreeCreateResult = | { worktreeId: string; name: string; warning?: string } - | { error: string } + /** `replayed`: the refusal answered a resend after a lost reply, so the first send may have created it. */ + | { error: string; replayed?: true } + +export type WorktreeCreateParent = Required< + Pick +> export type CreateWorktreeWithNameRetryArgs = { client: RpcClient @@ -46,6 +51,8 @@ export type CreateWorktreeWithNameRetryArgs = { branchIntentSupported?: boolean | Promise agentLaunch?: WorktreeCreateAgentLaunch maxAttempts?: number + /** The workspace the new worktree is attached to, on every attempt. */ + parent?: WorktreeCreateParent mintMutationId?: () => string mintLaunchOperationId?: () => string } @@ -62,11 +69,12 @@ export async function createWorktreeWithNameRetry( const mintMutationId = args.mintMutationId ?? defaultWorktreeCreateMutationId const mintLaunchOperationId = args.mintLaunchOperationId ?? structuredSessionOperationId let lastError: string | null = null + let lastReplayed = false for (let attempt = 0; attempt < maxAttempts; attempt += 1) { const candidateName = args.nameWasGenerated ? getGeneratedWorktreeCreateRetryCandidate(baseName, attempt) : getClientWorktreeCreateCandidate(baseName, attempt) - const candidateParams = buildParams(candidateName, attempt) + const candidateParams = { ...buildParams(candidateName, attempt), ...args.parent } if (attempt === 0 && candidateParams.branchIntent) { if ((await args.branchIntentSupported) !== true) { return { @@ -90,6 +98,7 @@ export async function createWorktreeWithNameRetry( worktreeCreateIdempotency ) let response = sent.response + let replayed = sent.replayed if ( !response.ok && !sent.replayed && @@ -100,9 +109,15 @@ export async function createWorktreeWithNameRetry( ) { // Method refusal can mean the client advertisement lagged the probe; downgrade permanently for this create. launch = null - response = ( - await sendWorktreeCreateResilient(client, null, null, params, worktreeCreateIdempotency) - ).response + const fallback = await sendWorktreeCreateResilient( + client, + null, + null, + params, + worktreeCreateIdempotency + ) + response = fallback.response + replayed = fallback.replayed } // Ledger refusal may follow creation; never retry unnamed. Inspect raw refusal messages for legacy collision handling. if (response.ok) { @@ -118,7 +133,10 @@ export async function createWorktreeWithNameRetry( break } lastError = response.error.message + lastReplayed = replayed + // A replayed refusal may follow a create whose reply was lost; another name could be a second worktree. if ( + replayed || candidateParams.branchIntent === 'open-existing-v2' || launch?.replay || !isRetryableWorktreeCreateConflict(lastError ?? '') @@ -128,7 +146,9 @@ export async function createWorktreeWithNameRetry( } return { error: - lastError ?? translate('tasks.worktreeCreateRetry.createFailed', 'Failed to create workspace') + lastError ?? + translate('tasks.worktreeCreateRetry.createFailed', 'Failed to create workspace'), + ...(lastReplayed ? { replayed: true as const } : {}) } } diff --git a/mobile/src/test-support/rpc-recording/adapters/workspace-settings-mounts.ts b/mobile/src/test-support/rpc-recording/adapters/workspace-settings-mounts.ts index 53a17673548..5717b27b063 100644 --- a/mobile/src/test-support/rpc-recording/adapters/workspace-settings-mounts.ts +++ b/mobile/src/test-support/rpc-recording/adapters/workspace-settings-mounts.ts @@ -102,7 +102,9 @@ export function workspaceSettingsMounts( getAgentLaunchSupport: async () => false, transitionDrawer: (view: unknown) => context.effect('drawer', view), onCreated: (id: unknown, name: unknown) => context.effect('created', { id, name }), - onClose: () => context.effect('close', null) + onClose: () => context.effect('close', null), + // Not opened from a folder. + folderLaunch: undefined }) let state: ReturnType const hook = hookMount(() => { diff --git a/mobile/src/transport/encryption/paired-e2ee-native-codec.test.ts b/mobile/src/transport/encryption/paired-e2ee-native-codec.test.ts index 8b5479e7752..41e233f03a6 100644 --- a/mobile/src/transport/encryption/paired-e2ee-native-codec.test.ts +++ b/mobile/src/transport/encryption/paired-e2ee-native-codec.test.ts @@ -1,4 +1,4 @@ -import { gunzipSync } from 'node:zlib' +import { brotliDecompressSync, gunzipSync } from 'node:zlib' import sodium from 'react-native-libsodium' import { beforeAll, describe, expect, it, vi } from 'vitest' @@ -38,6 +38,8 @@ function libsodiumBindings(): PairedE2EENativeBindings { }, codec: { gunzip: (input, outputLimit) => new Uint8Array(gunzipSync(input)).slice(0, outputLimit), + brotliDecompress: async (input, outputLimit) => + new Uint8Array(brotliDecompressSync(input)).slice(0, outputLimit), decodeUtf8: (input) => { if (input[0] === 0xef && input[1] === 0xbb && input[2] === 0xbf) { return null diff --git a/mobile/src/transport/encryption/paired-e2ee-native-codec.ts b/mobile/src/transport/encryption/paired-e2ee-native-codec.ts index 3c6f7c4f328..412a5335327 100644 --- a/mobile/src/transport/encryption/paired-e2ee-native-codec.ts +++ b/mobile/src/transport/encryption/paired-e2ee-native-codec.ts @@ -1,4 +1,4 @@ -// Move frame seal/decode and bundle gunzip off Hermes’s JS thread when native bindings are available. +// Move frame seal/decode and bundle gunzip/brotli off Hermes’s JS thread when native bindings are available. export type PairedE2EENativeCodec = { /** Canonical padded base64 only; null for anything else, including what `atob` forgives. */ decodeBase64(value: string): Uint8Array | null @@ -10,6 +10,8 @@ export type PairedE2EENativeCodec = { sealSecretbox(message: Uint8Array, nonce: Uint8Array, key: Uint8Array): Uint8Array /** Throws on a body that is not one well-formed gzip member. See orca-gzip-utf8-codec. */ gunzip(input: Uint8Array, outputLimit: number): Uint8Array + /** Off the JS thread; Hermes has no brotli, so there is no JS fallback. */ + brotliDecompress(input: Uint8Array, outputLimit: number): Promise> /** Null where only TextDecoder's replacement or BOM rules give the answer, or native failed. */ decodeUtf8(input: Uint8Array): string | null } @@ -25,6 +27,7 @@ export type PairedE2EENativeBindings = { codec: { gunzip(input: Uint8Array, outputLimit: number): Uint8Array decodeUtf8(input: Uint8Array): string | null + brotliDecompress(input: Uint8Array, outputLimit: number): Promise> } sodium: LibsodiumBindings } @@ -96,6 +99,7 @@ function detectPairedE2EENativeCodec(): PairedE2EENativeCodec | null { sodium.secretboxSeal(wholeBuffer(message), wholeBuffer(nonce), wholeBuffer(key)) ), gunzip: (input, outputLimit) => module.gunzip(input, outputLimit), + brotliDecompress: (input, outputLimit) => module.brotliDecompress(input, outputLimit), decodeUtf8: (input) => { // Why: the frame is already open and the counter advanced, so a throw here must not drop it. try { diff --git a/mobile/src/transport/web-bundle/mobile-web-bundle-brotli-fetch.test.ts b/mobile/src/transport/web-bundle/mobile-web-bundle-brotli-fetch.test.ts new file mode 100644 index 00000000000..d88a73b0ad5 --- /dev/null +++ b/mobile/src/transport/web-bundle/mobile-web-bundle-brotli-fetch.test.ts @@ -0,0 +1,116 @@ +import { sha256 } from '@noble/hashes/sha256' +import { brotliCompressSync, brotliDecompressSync } from 'node:zlib' +import { describe, expect, it, vi } from 'vitest' +import { computeMobileWebBundleId } from '../../../../src/shared/mobile-web-bundle/manifest-contract' +import { fetchMobileWebBundle, type MobileWebBundleFetchProgress } from './mobile-web-bundle-fetch' + +import type { RpcClient } from '../rpc/rpc-client-contract' +import type { RpcResponse } from '../rpc/rpc-wire-types' + +vi.mock('expo-crypto', async () => { + const { sha256: hash } = await import('@noble/hashes/sha256') + return { + CryptoDigestAlgorithm: { SHA256: 'SHA-256' }, + digest: async (_algorithm: string, data: Uint8Array) => hash(data).buffer + } +}) + +// The native codec as a phone binary has it; node's brotli stands in for the platform decoder. +vi.mock('../encryption/paired-e2ee-native-codec', () => ({ + pairedE2EENativeCodec: () => ({ + decodeBase64: (value: string) => new Uint8Array(Buffer.from(value, 'base64')), + brotliDecompress: async (input: Uint8Array, outputLimit: number) => + new Uint8Array(brotliDecompressSync(input)).slice(0, outputLimit) + }) +})) + +const toHex = (bytes: Uint8Array): string => + Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('') + +// Small enough that the 46-byte brotli copy spans three windows. +const RANGE_BYTES = 16 +const SCRIPT = 'export const greeting = "hello";\n'.repeat(40) +const MARK = 'a short mark' + +function brotliHost() { + const files = new Map([ + ['assets/script.js', new TextEncoder().encode(SCRIPT)], + ['index.html', new TextEncoder().encode(MARK)] + ]) + const copy = new Uint8Array(brotliCompressSync(files.get('assets/script.js')!)) + const assets = [...files].map(([path, content]) => ({ + path, + sha256: toHex(sha256(content)), + byteLength: content.byteLength, + contentType: 'text/plain', + ...(path === 'assets/script.js' ? { brotliByteLength: copy.byteLength } : {}) + })) + const buildId = computeMobileWebBundleId(assets) + const manifest = { + schemaVersion: 1, + buildId, + desktopVersion: '1.4.300', + minCompatibleRuntimeProtocolVersion: 2, + runtimeProtocolVersion: 2, + entrypoint: 'index.html', + totalBytes: assets.reduce((total, entry) => total + entry.byteLength, 0), + assets + } + const methods: string[] = [] + const answer = (method: string, params: Record): unknown => { + if (method === 'mobileWeb.bundle.manifest') { + return { manifest, chunkBytes: RANGE_BYTES, rangeBytes: RANGE_BYTES } + } + const path = String(params.path) + const offset = Number(params.offset) + const content = files.get(path)! + const stream = method === 'mobileWeb.bundle.brotliRange' ? copy : content + const slice = stream.subarray(offset, offset + RANGE_BYTES) + return { + buildId, + path, + offset, + assetByteLength: content.byteLength, + sha256: toHex(sha256(content)), + ...(method === 'mobileWeb.bundle.range' ? { encoding: 'identity' } : {}), + dataBase64: Buffer.from(slice).toString('base64'), + eof: offset + slice.byteLength >= stream.byteLength + } + } + const client: RpcClient = { + sendRequest: vi.fn(async (method: string, params?: unknown): Promise => { + methods.push(method) + const fields: Record = Object(params) + return { id: 'rpc-1', ok: true, result: answer(method, fields), _meta: { runtimeId: 'r' } } + }), + subscribe: vi.fn(() => () => {}), + updateTerminalSubscriptionViewport: vi.fn(), + getState: () => 'connected', + getReconnectAttempt: () => 0, + getLastConnectedAt: () => 1, + onStateChange: () => () => {}, + notifyForeground: vi.fn(), + close: vi.fn() + } + return { client, methods } +} + +describe('fetchMobileWebBundle from a host that ships brotli copies', () => { + it('reads the copy where the manifest lists one, inflates it, and reports progress in asset bytes', async () => { + const host = brotliHost() + const progress: MobileWebBundleFetchProgress[] = [] + + const fetched = await fetchMobileWebBundle({ + client: host.client, + onProgress: (update) => progress.push(update) + }) + + expect(new TextDecoder().decode(fetched.assets.get('assets/script.js'))).toBe(SCRIPT) + expect(new TextDecoder().decode(fetched.assets.get('index.html'))).toBe(MARK) + expect(host.methods.filter((method) => method === 'mobileWeb.bundle.brotliRange')).toHaveLength( + 3 + ) + expect(host.methods.filter((method) => method === 'mobileWeb.bundle.range')).toHaveLength(1) + expect(progress.at(-1)).toMatchObject({ receivedBytes: 1332, totalBytes: 1332 }) + }) +}) diff --git a/mobile/src/transport/web-bundle/mobile-web-bundle-fetch.ts b/mobile/src/transport/web-bundle/mobile-web-bundle-fetch.ts index 2d30443088d..abd4440ef64 100644 --- a/mobile/src/transport/web-bundle/mobile-web-bundle-fetch.ts +++ b/mobile/src/transport/web-bundle/mobile-web-bundle-fetch.ts @@ -10,11 +10,15 @@ import type { RpcClient } from '../rpc/rpc-client-contract' import { MobileWebBundleFetchError } from './mobile-web-bundle-fetch-refusal' import { markRpcDeliveryUnknown } from '../rpc/rpc-delivery-ambiguity' import { runRpcOperation } from '../rpc/rpc-operation' -import { mobileWebBundleWindowReader } from './mobile-web-bundle-window-reader' +import { + mobileWebBundleWindowReader, + type MobileWebBundleWindowReader +} from './mobile-web-bundle-window-reader' import { assertWindowDescribesAsset, assertWindowFillsItsSlot, - windowSlotBytes + windowSlotBytes, + type MobileWebBundleWindowStream } from './mobile-web-bundle-window-checks' // The host admits four window reads per connection; one reader per asset would serialize large scripts. @@ -56,6 +60,7 @@ export type MobileWebBundleFetchResult = { type AssetReassembly = { readonly entry: MobileWebBundleAssetRead + readonly stream: MobileWebBundleWindowStream readonly whole: Uint8Array outstandingWindows: number } @@ -154,9 +159,9 @@ export async function fetchMobileWebBundle(args: { return } assertWindowDescribesAsset(reply.header, asset.entry, manifest.buildId, offset) - const bytes = reply.bytes(windowSlotBytes(asset.entry, offset, reader.windowBytes)) + const bytes = reply.bytes(windowSlotBytes(asset.stream, offset, reader.windowBytes)) assertWindowFillsItsSlot( - asset.entry, + asset.stream, offset, bytes.byteLength, reply.header.eof, @@ -165,7 +170,7 @@ export async function fetchMobileWebBundle(args: { asset.whole.set(bytes, offset) asset.outstandingWindows -= 1 if (asset.outstandingWindows === 0) { - const verified = await verifyReassembledAsset(asset) + const verified = await verifyReassembledAsset(asset, reader) throwIfCallerAborted(args.signal) // A sibling failed while this hashed; the fetch already rejected with that failure. if (stopped.signal.aborted) { @@ -175,12 +180,12 @@ export async function fetchMobileWebBundle(args: { downloadedAssets += 1 } // Per window, not per asset: the largest asset goes first, so asset completions bunch at the end. - receivedBytes += bytes.byteLength + receivedBytes += assetBytesCredited(asset, offset, bytes.byteLength) reportProgress() } const readWindows = async (entries: readonly MobileWebBundleAssetRead[]): Promise => { - const queue = planWindowReads(entries, reader.windowBytes) + const queue = planWindowReads(entries, reader) if (queue.length === 0) { return } @@ -270,30 +275,61 @@ export async function fetchMobileWebBundle(args: { // Plan all grid offsets largest-asset first so its tail cannot serialize the end; still validate eof replies. function planWindowReads( entries: readonly MobileWebBundleAssetRead[], - windowBytes: number + reader: MobileWebBundleWindowReader ): WindowRead[] { - const largestFirst = [...entries].sort((left, right) => right.byteLength - left.byteLength) - return largestFirst.flatMap((entry) => { - const count = Math.max(1, Math.ceil(entry.byteLength / windowBytes)) + const { windowBytes } = reader + const streams = entries.map((entry) => ({ + entry, + stream: { path: entry.path, byteLength: reader.streamLength(entry) } + })) + const largestFirst = streams.sort( + (left, right) => right.stream.byteLength - left.stream.byteLength + ) + return largestFirst.flatMap(({ entry, stream }) => { + const count = Math.max(1, Math.ceil(stream.byteLength / windowBytes)) const asset: AssetReassembly = { entry, - whole: new Uint8Array(entry.byteLength), + stream, + whole: new Uint8Array(stream.byteLength), outstandingWindows: count } return Array.from({ length: count }, (_, index) => ({ asset, offset: index * windowBytes })) }) } -async function verifyReassembledAsset(asset: AssetReassembly): Promise { +// Progress stays in asset bytes, the unit of the session's total: a window of a brotli copy counts for its +// share of the asset, and an asset's windows add up to exactly its byteLength. +function assetBytesCredited(asset: AssetReassembly, offset: number, byteLength: number): number { + const streamBytes = asset.stream.byteLength + if (streamBytes === asset.entry.byteLength) { + return byteLength + } + const at = (position: number): number => + Math.floor((asset.entry.byteLength * position) / streamBytes) + return at(offset + byteLength) - at(offset) +} + +async function verifyReassembledAsset( + asset: AssetReassembly, + reader: MobileWebBundleWindowReader +): Promise { + const whole = await reader.inflate(asset.entry, asset.whole) + const { path, byteLength } = asset.entry + if (whole.byteLength !== byteLength) { + throw new MobileWebBundleFetchError( + whole.byteLength > byteLength ? 'asset-overlong' : 'asset-short', + `bundle asset ${path} inflated to ${whole.byteLength} of ${byteLength} declared bytes` + ) + } // Native: hashing in JS held the JS thread for seconds, and React's progress renders waited on it. - const hashed = toHex(new Uint8Array(await digest(CryptoDigestAlgorithm.SHA256, asset.whole))) + const hashed = toHex(new Uint8Array(await digest(CryptoDigestAlgorithm.SHA256, whole))) if (hashed !== asset.entry.sha256) { throw new MobileWebBundleFetchError( 'asset-checksum-mismatch', `bundle asset ${asset.entry.path} hashed ${hashed}, not ${asset.entry.sha256}` ) } - return asset.whole + return whole } /** Only the caller's abort surfaces as `fetch-stopped`; an internal stop rejects with the failure diff --git a/mobile/src/transport/web-bundle/mobile-web-bundle-operations.ts b/mobile/src/transport/web-bundle/mobile-web-bundle-operations.ts index 9116b76ae0d..8a717c84255 100644 --- a/mobile/src/transport/web-bundle/mobile-web-bundle-operations.ts +++ b/mobile/src/transport/web-bundle/mobile-web-bundle-operations.ts @@ -1,11 +1,13 @@ import { MobileWebBundleErrorCodeSchema, + MOBILE_WEB_BUNDLE_BROTLI_RANGE_METHOD, MOBILE_WEB_BUNDLE_CHUNK_METHOD, MOBILE_WEB_BUNDLE_MANIFEST_METHOD, MOBILE_WEB_BUNDLE_RANGE_METHOD, type MobileWebBundleErrorCode } from '../../../../src/shared/mobile-web-bundle/bundle-rpc-contract' import { + MobileWebBundleBrotliRangeReplySchema, MobileWebBundleChunkReplySchema, MobileWebBundleManifestReplySchema, MobileWebBundleRangeReplySchema @@ -43,6 +45,15 @@ export const mobileWebBundleRangeRead = defineRpcOperation({ read: rpcResultVariant('mobile-web-bundle-range', MobileWebBundleRangeReplySchema) }) +// Only for assets whose manifest entry names a brotli copy, so only hosts that ship one are asked. +export const mobileWebBundleBrotliRangeRead = defineRpcOperation({ + name: 'mobileWeb.bundle-brotli-range', + method: MOBILE_WEB_BUNDLE_BROTLI_RANGE_METHOD, + acceptance: 'require-result-or-throw', + barrier: 'on-settle', + read: rpcResultVariant('mobile-web-bundle-brotli-range', MobileWebBundleBrotliRangeReplySchema) +}) + /** A code is a bare snake_case token, so only the two positions one can occupy are read. */ const LEADING_CODE_TOKEN = /^[a-z][a-z0-9_]*/ diff --git a/mobile/src/transport/web-bundle/mobile-web-bundle-reply-schemas.ts b/mobile/src/transport/web-bundle/mobile-web-bundle-reply-schemas.ts index b76f7c4f6aa..602d3d5d4ea 100644 --- a/mobile/src/transport/web-bundle/mobile-web-bundle-reply-schemas.ts +++ b/mobile/src/transport/web-bundle/mobile-web-bundle-reply-schemas.ts @@ -38,7 +38,15 @@ const assetSchema = z.looseObject({ path: MobileWebBundleAssetPathSchema, sha256: z.string().regex(SHA256_PATTERN), byteLength: z.number().int().nonnegative().max(MOBILE_WEB_BUNDLE_MAX_ASSET_BYTES), - contentType: z.string().min(1) + contentType: z.string().min(1), + // An unreadable length means no brotli copy, never a refused bundle; the reader also requires it under byteLength. + brotliByteLength: z + .number() + .int() + .nonnegative() + .max(MOBILE_WEB_BUNDLE_MAX_ASSET_BYTES) + .optional() + .catch(undefined) }) // Read unknown schema versions as numbers so compatibility can name the required update. @@ -115,6 +123,12 @@ export const MobileWebBundleRangeReplySchema = z.looseObject({ dataBase64: z.string().max(MOBILE_WEB_BUNDLE_RANGE_MAX_DATA_BASE64_LENGTH) }) +// Windows of the brotli copy are its raw bytes; the header still names the inflated asset. +export const MobileWebBundleBrotliRangeReplySchema = z.looseObject({ + ...windowHeaderFields, + dataBase64: z.string().max(MOBILE_WEB_BUNDLE_RANGE_MAX_DATA_BASE64_LENGTH) +}) + export type MobileWebBundleManifestReply = z.output export type MobileWebBundleManifestRead = MobileWebBundleManifestReply['manifest'] export type MobileWebBundleAssetRead = MobileWebBundleManifestRead['assets'][number] diff --git a/mobile/src/transport/web-bundle/mobile-web-bundle-window-checks.ts b/mobile/src/transport/web-bundle/mobile-web-bundle-window-checks.ts index e59fe72b6fa..bf2aea9dc55 100644 --- a/mobile/src/transport/web-bundle/mobile-web-bundle-window-checks.ts +++ b/mobile/src/transport/web-bundle/mobile-web-bundle-window-checks.ts @@ -2,8 +2,11 @@ import type { MobileWebBundleAssetRead } from './mobile-web-bundle-reply-schemas import { MobileWebBundleFetchError } from './mobile-web-bundle-fetch-refusal' import type { MobileWebBundleWindowHeader } from './mobile-web-bundle-window-reader' +/** The file the windows are cut from: the asset, or its brotli copy. */ +export type MobileWebBundleWindowStream = Pick + export function windowSlotBytes( - entry: MobileWebBundleAssetRead, + entry: MobileWebBundleWindowStream, offset: number, windowBytes: number ): number { @@ -12,7 +15,7 @@ export function windowSlotBytes( /** Offsets are planned, so a reply is accepted only if it fills exactly its slot of the grid. */ export function assertWindowFillsItsSlot( - entry: MobileWebBundleAssetRead, + entry: MobileWebBundleWindowStream, offset: number, byteLength: number, eof: boolean, diff --git a/mobile/src/transport/web-bundle/mobile-web-bundle-window-reader.ts b/mobile/src/transport/web-bundle/mobile-web-bundle-window-reader.ts index 51e439a24a3..7a27848b680 100644 --- a/mobile/src/transport/web-bundle/mobile-web-bundle-window-reader.ts +++ b/mobile/src/transport/web-bundle/mobile-web-bundle-window-reader.ts @@ -1,8 +1,17 @@ +import { errorMessage } from '../../../../src/shared/error-message' import { decodeBase64Bytes } from '../encryption/base64-byte-codec' import { pairedE2EENativeCodec } from '../encryption/paired-e2ee-native-codec' -import { mobileWebBundleChunkRead, mobileWebBundleRangeRead } from './mobile-web-bundle-operations' +import { + mobileWebBundleBrotliRangeRead, + mobileWebBundleChunkRead, + mobileWebBundleRangeRead +} from './mobile-web-bundle-operations' import { decodeMobileWebBundleRange } from './mobile-web-bundle-range-decode' -import type { MobileWebBundleManifestReply } from './mobile-web-bundle-reply-schemas' +import type { + MobileWebBundleAssetRead, + MobileWebBundleManifestReply +} from './mobile-web-bundle-reply-schemas' +import { MobileWebBundleFetchError } from './mobile-web-bundle-fetch-refusal' import type { RpcClient } from '../rpc/rpc-client-contract' import { runRpcOperation } from '../rpc/rpc-operation' import type { SendRequestOptions } from '../rpc/unvalidated-rpc-request-port' @@ -24,6 +33,8 @@ export type MobileWebBundleWindowReply = { export type MobileWebBundleWindowReader = { readonly windowBytes: number + /** What this reader moves for the asset: its brotli copy where it reads one, otherwise the asset itself. */ + readonly streamLength: (asset: MobileWebBundleAssetRead) => number read( window: { buildId: string @@ -32,31 +43,48 @@ export type MobileWebBundleWindowReader = { }, options?: SendRequestOptions ): Promise + /** The asset's bytes from its reassembled stream, before the caller checks their hash. */ + readonly inflate: ( + asset: MobileWebBundleAssetRead, + stream: Uint8Array + ) => Promise> } // Choose ranges only from the manifest’s grid; older hosts still serve chunks. export function mobileWebBundleWindowReader( client: RpcClient, - opened: Pick + opened: Pick ): MobileWebBundleWindowReader { const { rangeBytes } = opened if (rangeBytes === undefined) { return { windowBytes: opened.chunkBytes, - read: async (window, options) => { - const { dataBase64, ...header } = await runRpcOperation( - client, - mobileWebBundleChunkRead, - window, - options - ) - return { header, bytes: () => decodeBase64(dataBase64) } - } + streamLength: (asset) => asset.byteLength, + read: async (window, options) => + rawWindowReply(await runRpcOperation(client, mobileWebBundleChunkRead, window, options)), + inflate: async (_asset, stream) => stream } } + const native = pairedE2EENativeCodec() + // Hermes has no brotli, so without the native codec every asset stays on gzip ranges. + const brotliLengths = new Map( + native === null + ? [] + : opened.manifest.assets.flatMap((asset) => + asset.brotliByteLength !== undefined && asset.brotliByteLength < asset.byteLength + ? [[asset.path, asset.brotliByteLength] as const] + : [] + ) + ) return { windowBytes: rangeBytes, + streamLength: (asset) => brotliLengths.get(asset.path) ?? asset.byteLength, read: async (window, options) => { + if (brotliLengths.has(window.path)) { + return rawWindowReply( + await runRpcOperation(client, mobileWebBundleBrotliRangeRead, window, options) + ) + } const { dataBase64, encoding, ...header } = await runRpcOperation( client, mobileWebBundleRangeRead, @@ -72,7 +100,35 @@ export function mobileWebBundleWindowReader( expected ) } - } + }, + inflate: async (asset, stream) => + native === null || !brotliLengths.has(asset.path) + ? stream + : inflateBrotli(native.brotliDecompress, asset, stream) + } +} + +// Chunks and brotli windows carry their bytes as they are. +function rawWindowReply({ + dataBase64, + ...header +}: MobileWebBundleWindowHeader & { readonly dataBase64: string }): MobileWebBundleWindowReply { + return { header, bytes: () => decodeBase64(dataBase64) } +} + +// One byte past the asset bounds a bomb; the caller rejects any length but the asset's. +async function inflateBrotli( + brotliDecompress: (input: Uint8Array, outputLimit: number) => Promise>, + asset: MobileWebBundleAssetRead, + stream: Uint8Array +): Promise> { + try { + return await brotliDecompress(stream, asset.byteLength + 1) + } catch (error) { + throw new MobileWebBundleFetchError( + 'range-undecodable', + `bundle asset ${asset.path} is not valid brotli: ${errorMessage(error)}` + ) } } diff --git a/package.json b/package.json index 3c157b155c9..f25b0e9565b 100644 --- a/package.json +++ b/package.json @@ -379,7 +379,7 @@ "tailwind-merge": "^3.7.0", "tailwindcss": "^4.3.3", "tar": "7.5.22", - "tsrs": "0.18.0-ts7.1.0-dev.20260929", + "tsrs": "0.22.0-ts7.1.0-beta", "tw-animate-css": "^1.4.0", "typescript": "^7.0.2", "typescript-api": "npm:typescript@6.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 16c159b227e..59e5770f157 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -666,8 +666,8 @@ importers: specifier: 7.5.22 version: 7.5.22 tsrs: - specifier: 0.18.0-ts7.1.0-dev.20260929 - version: 0.18.0-ts7.1.0-dev.20260929 + specifier: 0.22.0-ts7.1.0-beta + version: 0.22.0-ts7.1.0-beta tw-animate-css: specifier: ^1.4.0 version: 1.4.0 @@ -4186,40 +4186,52 @@ packages: cpu: [x64] os: [win32] - '@tsrs/darwin-arm64@0.18.0-ts7.1.0-dev.20260929': - resolution: {integrity: sha512-sAirMpaGA9uRQm2W+t4zC7PjnijNuFa4PCSIfLKKo0JyAUCRfnMeev0UGBQ9F3g+9Nz187tvikUs5Irh5+eajA==} + '@tsrs/darwin-arm64@0.22.0-ts7.1.0-beta': + resolution: {integrity: sha512-mY0dMEtFxig4h5At3nByjZe8E4DTDPqqz73kkxQ6gqW5K9ym+u/VHQhtnpTF6ZOTkYyjP3bTaj5T8HcyoGVddA==} engines: {node: '>=16.20.0'} cpu: [arm64] os: [darwin] - '@tsrs/linux-arm64-gnu@0.18.0-ts7.1.0-dev.20260929': - resolution: {integrity: sha512-zm9/z9yylig68JiZF4Aq0Cm26n09KEU8YoQRlG7tdDWTBNAIViYw8knxnWYsjMWVZ2XiU4mYq1xdrOKuOgABHg==} + '@tsrs/darwin-x64@0.22.0-ts7.1.0-beta': + resolution: {integrity: sha512-dvsSnyU8rpI3OqTIH2xxjt9ThvZC52BsL6/LS69eWg7Ieq2oni1PnLels5gGTFqvHFoRbezi5Y0KY7PQ1OmK5w==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [darwin] + + '@tsrs/linux-arm64-gnu@0.22.0-ts7.1.0-beta': + resolution: {integrity: sha512-igg42LxSN/DHFqafvDqZW2JNBMH8WIwc1JE/IY4QUDOXNhZnUfDgEu9xo1QYwbgryeo5llmhRwM4CI/BQUWF1Q==} engines: {node: '>=16.20.0'} cpu: [arm64] os: [linux] libc: [glibc] - '@tsrs/linux-arm64-musl@0.18.0-ts7.1.0-dev.20260929': - resolution: {integrity: sha512-Lw7VVU/YQ9JcarODICEiwzMLdFqZprqh5RGTgbS5Uc6I95P635/g5xA1pOC8RVs4efBvAR30J54iDG3l5JEYiw==} + '@tsrs/linux-arm64-musl@0.22.0-ts7.1.0-beta': + resolution: {integrity: sha512-f0zCqMkxPof7QVwjdgA8Fb0n6BtX5MOdILH/xfS+He6gRLCoECky0ejbsgLWCpB06GMlsL1gCbDr0/tmc91APg==} engines: {node: '>=16.20.0'} cpu: [arm64] os: [linux] libc: [musl] - '@tsrs/linux-x64-gnu@0.18.0-ts7.1.0-dev.20260929': - resolution: {integrity: sha512-EdBwBgNICGu1r1h23ixdcKwsMo6E9ll7ZsY3mNERNYqE1Pwdtcat99d5E7zB1J+oy/Xk6YqiizaB6nolIjpDTw==} + '@tsrs/linux-x64-gnu@0.22.0-ts7.1.0-beta': + resolution: {integrity: sha512-/e511w7xXmxBf14paz8eMLUbmlIc33Sy1E00y1FqmnUK04FTotWl+80mNvIVX5NrlOOJgrh+B125t1CHWu/8vQ==} engines: {node: '>=16.20.0'} cpu: [x64] os: [linux] libc: [glibc] - '@tsrs/linux-x64-musl@0.18.0-ts7.1.0-dev.20260929': - resolution: {integrity: sha512-LHk+AOAtXuTlhJhOJ2+elriEIX17ycXljojeNxrrzckOipTG1LksoVWuCSas2nu00EnWAzrdKpt9oqPylUKZfQ==} + '@tsrs/linux-x64-musl@0.22.0-ts7.1.0-beta': + resolution: {integrity: sha512-B3aDOdS5+h77HcFep+KtOI+XmLm2G+FQPQC3KPSDjS/IG0tY9GPSc14mYnLwgO++dXluukO/oufk3J4Lo0CFjg==} engines: {node: '>=16.20.0'} cpu: [x64] os: [linux] libc: [musl] + '@tsrs/win32-x64@0.22.0-ts7.1.0-beta': + resolution: {integrity: sha512-cZ69w+wOECbU6YF5UWi15TK73eX1pgoMEHKSmUF4K8PbS7DPIh/nWMsdb3rM8Ee1SUZ9ONCx+S6myh64SRi3Bg==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [win32] + '@types/aria-query@5.0.4': resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} @@ -7940,8 +7952,8 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - tsrs@0.18.0-ts7.1.0-dev.20260929: - resolution: {integrity: sha512-9T0b4Uam/yMFIK16GUx0fUrJqYZnJ/gvSmV9JB8NTyDePvwCI45WFWxyEV/UMV3KfdNRJG/PBsm3fSIB9WmWsA==} + tsrs@0.22.0-ts7.1.0-beta: + resolution: {integrity: sha512-DEnEqQZRM3sHCV7MnQKVeTh61U2SX+0WksVlWp4eVayy0NffQ2oiCOjgH/Eye3sMPYkGwpM3tht3HtVI9NdURg==} engines: {node: '>=16.20.0'} hasBin: true @@ -11174,19 +11186,25 @@ snapshots: '@transcribe-cpp/win32-x64-cpu-vulkan@0.4.1': optional: true - '@tsrs/darwin-arm64@0.18.0-ts7.1.0-dev.20260929': + '@tsrs/darwin-arm64@0.22.0-ts7.1.0-beta': optional: true - '@tsrs/linux-arm64-gnu@0.18.0-ts7.1.0-dev.20260929': + '@tsrs/darwin-x64@0.22.0-ts7.1.0-beta': optional: true - '@tsrs/linux-arm64-musl@0.18.0-ts7.1.0-dev.20260929': + '@tsrs/linux-arm64-gnu@0.22.0-ts7.1.0-beta': optional: true - '@tsrs/linux-x64-gnu@0.18.0-ts7.1.0-dev.20260929': + '@tsrs/linux-arm64-musl@0.22.0-ts7.1.0-beta': optional: true - '@tsrs/linux-x64-musl@0.18.0-ts7.1.0-dev.20260929': + '@tsrs/linux-x64-gnu@0.22.0-ts7.1.0-beta': + optional: true + + '@tsrs/linux-x64-musl@0.22.0-ts7.1.0-beta': + optional: true + + '@tsrs/win32-x64@0.22.0-ts7.1.0-beta': optional: true '@types/aria-query@5.0.4': {} @@ -15511,13 +15529,15 @@ snapshots: tslib@2.8.1: {} - tsrs@0.18.0-ts7.1.0-dev.20260929: + tsrs@0.22.0-ts7.1.0-beta: optionalDependencies: - '@tsrs/darwin-arm64': 0.18.0-ts7.1.0-dev.20260929 - '@tsrs/linux-arm64-gnu': 0.18.0-ts7.1.0-dev.20260929 - '@tsrs/linux-arm64-musl': 0.18.0-ts7.1.0-dev.20260929 - '@tsrs/linux-x64-gnu': 0.18.0-ts7.1.0-dev.20260929 - '@tsrs/linux-x64-musl': 0.18.0-ts7.1.0-dev.20260929 + '@tsrs/darwin-arm64': 0.22.0-ts7.1.0-beta + '@tsrs/darwin-x64': 0.22.0-ts7.1.0-beta + '@tsrs/linux-arm64-gnu': 0.22.0-ts7.1.0-beta + '@tsrs/linux-arm64-musl': 0.22.0-ts7.1.0-beta + '@tsrs/linux-x64-gnu': 0.22.0-ts7.1.0-beta + '@tsrs/linux-x64-musl': 0.22.0-ts7.1.0-beta + '@tsrs/win32-x64': 0.22.0-ts7.1.0-beta tsx@4.23.15: dependencies: diff --git a/src/cli/handlers/orchestration/agent-server-route.test.ts b/src/cli/handlers/orchestration/agent-server-route.test.ts deleted file mode 100644 index b84bae4609c..00000000000 --- a/src/cli/handlers/orchestration/agent-server-route.test.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type * as RuntimeClientModule from '../../runtime-client' -import { refuseAgentServerRoute, requirePeerMailCarrier } from './agent-server-route' - -const { callerShow } = vi.hoisted(() => ({ callerShow: vi.fn() })) -vi.mock('../../runtime-client', async (importOriginal) => { - const actual = await importOriginal() - return { - ...actual, - RuntimeClient: class { - call = callerShow - } - } -}) - -function ctx(command: string[], flags: [string, string | boolean][]) { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a handler context with only the fields this function reads. - return { - flags: new Map(flags), - client: { isRemote: true }, - cwd: '/', - json: false, - rawArgs: [...command, ...flags.flatMap(([name, value]) => [`--${name}`, String(value)])] - } as unknown as Parameters[1] -} - -describe('an agent pointing mail at another server', () => { - beforeEach(() => { - vi.stubEnv('ORCA_AGENT_LAUNCH_TOKEN', 'launch-token') - vi.stubEnv('ORCA_AGENT_SESSION_ID', '') - vi.stubEnv('ORCA_CLI_COMMAND', 'orca') - callerShow.mockResolvedValue({ result: { caller: null, terminal: true } }) - }) - - afterEach(() => { - vi.unstubAllEnvs() - callerShow.mockReset() - }) - - it('is refused with the --on command to run instead', async () => { - await expect( - refuseAgentServerRoute( - 'send', - ctx( - ['orchestration', 'send'], - [ - ['environment', 'devB'], - ['to', 'term_b'], - ['subject', 'hi'] - ] - ) - ) - ).rejects.toMatchObject({ - code: 'agent_server_route_refused', - message: - 'send with --environment devB runs on devB and nothing comes back to you. To message an agent on devB, use: orca orchestration send --on devB --to term_b --subject hi' - }) - }) - - it('refuses a check against another server: the mailbox is local', async () => { - await expect( - refuseAgentServerRoute('check', ctx(['orchestration', 'check'], [['environment', 'devB']])) - ).rejects.toMatchObject({ - code: 'mailbox_is_local', - message: - 'Your mailbox is on this Orca; replies from devB arrive here. Drop --environment devB.' - }) - }) - - it('leaves a person in a shell running the command on that server', async () => { - vi.stubEnv('ORCA_AGENT_LAUNCH_TOKEN', '') - await expect( - refuseAgentServerRoute('send', ctx(['orchestration', 'send'], [['environment', 'devB']])) - ).resolves.toBeUndefined() - expect(callerShow).not.toHaveBeenCalled() - }) - - it('leaves a launch its own Orca does not verify running on that server', async () => { - callerShow.mockResolvedValue({ result: { caller: null, terminal: false } }) - await expect( - refuseAgentServerRoute('send', ctx(['orchestration', 'send'], [['environment', 'devB']])) - ).resolves.toBeUndefined() - }) -}) - -describe('send --on from an older Orca', () => { - it('is refused before anything is sent', async () => { - const call = vi - .fn() - .mockResolvedValue({ result: { capabilities: ['orchestration.contract.v1'] } }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a handler context with only the fields this function reads. - const context = { - flags: new Map([['on', 'devB']]), - client: { call }, - cwd: '/', - json: false - } as unknown as Parameters[0] - - await expect(requirePeerMailCarrier(context)).rejects.toMatchObject({ - code: 'server_mail_unsupported' - }) - expect(call).toHaveBeenCalledTimes(1) - expect(call).toHaveBeenCalledWith('status.get') - }) -}) diff --git a/src/cli/specs/orchestration-worker-specs.ts b/src/cli/specs/orchestration-worker-specs.ts index b4f7dfc12f5..c6426789fd7 100644 --- a/src/cli/specs/orchestration-worker-specs.ts +++ b/src/cli/specs/orchestration-worker-specs.ts @@ -43,7 +43,7 @@ export const ORCHESTRATION_WORKER_COMMAND_SPECS: CommandSpec[] = [ '--agent takes an Orca agent id enabled on the worker server, such as claude, codex, cursor, antigravity, muse, zcode, opencode, or opencode2.', '--model supports Claude, Codex, Cursor, Antigravity, Grok, and Muse opaque provider model ids; --effort requires --model. OMP also takes its model roles and selectors (such as @worker or provider/model:level), with --effort as its thinking level. Neither can combine with --terminal. OpenCode model selection requires an existing worktree, a verified execution-host CLI, and an available model; effort is unsupported. Other agents, including zcode, launch with the model from their own config.', 'Finished workers close on their own unless started with --keep-open; a worker started in a new worktree (new-child or new-top-level) is kept open, so release it when no one will use its chat.', - 'New worktrees use agent-first creation and default --setup to run. Repository start-immediately runs setup beside the agent; wait-for-setup gates agent readiness and task input.', + 'New worktrees use agent-first creation; without --setup, setup runs unless the project turns it off. Repository start-immediately runs setup beside the agent; wait-for-setup gates agent readiness and task input.', 'New worktrees without --name get the name Orca gives an unnamed workspace; the receipt names it. Servers older than this change still require --name.', 'Creation flags (--name, --repo, --base-branch, --display-name, --comment, --setup) are rejected for current/existing worktrees. Use exact --repo on the selected server; project/host convenience routing remains on worktree create.', "How the worker runs follows the user's own setting for new agent tabs; there is no flag for it and no caller needs to ask. A dispatch the setting cannot apply to still starts, so the placement, agent, and launch options passed here are always the ones honoured.", diff --git a/src/main/agent-hooks/server/server-status-application.ts b/src/main/agent-hooks/server/server-status-application.ts index 8c179190ab4..e7a625f10ed 100644 --- a/src/main/agent-hooks/server/server-status-application.ts +++ b/src/main/agent-hooks/server/server-status-application.ts @@ -14,6 +14,7 @@ import type { } from '../../../shared/agent-status/contracts/agent-status-observation' import { AGENT_STATUS_2A_CURRENT_PRODUCER_MODE } from '../../../shared/agent-status/transport/agent-status-legacy-adapter' import { mainAgentState } from '../../../shared/agent-status/lifecycle/main-agent-status' +import { previousCodexTurnContext } from '../../../shared/agent-hook-listener/providers/codex-session-start' import { admitLegacyAgentStatus } from '../../../shared/agent-hook-listener/listener-state' import type { EnrichedAgentHookEventPayload } from './server-types' import { agentTypeToPromptSentAgentKind } from './server-status-identity' @@ -33,7 +34,12 @@ function resolveTurnStartedAt( payload.toolAgentId === undefined && payload.isReplay !== true && payload.providerSessionOnly !== true && - isNewTurnEvent(payload.source, payload.hookEventName) + isNewTurnEvent(payload.source, payload.hookEventName) && + !( + payload.source === 'codex' && + payload.hookEventName === 'SessionStart' && + !payload.hasExplicitPrompt + ) ) { return payload.payload.sessionBoundary === true ? undefined : at } @@ -127,9 +133,17 @@ export abstract class AgentHookServerStatusApplication extends AgentHookServerSt now = Date.now(), observedAt?: number ): EnrichedAgentHookEventPayload { + // Only live submissions open a new turn; cached starts restate historical context. + if ( + !payload.isReplay && + (payload.hookEventName === 'UserPromptSubmit' || payload.hasExplicitPrompt === true) + ) { + this.activeHookTurnCompletedAtByPaneKey.delete(payload.paneKey) + } const previous = this.state.lastStatusByPaneKey.get(payload.paneKey) as | EnrichedAgentHookEventPayload | undefined + const previousTurn = previousCodexTurnContext(previous, payload) const commandCodeNewTurn = previous !== undefined && isCommandCodeNewTurnWhileWorking({ @@ -143,13 +157,13 @@ export abstract class AgentHookServerStatusApplication extends AgentHookServerSt incomingPromptInteractionKey: payload.promptInteractionKey }) const stateStartedAt = - previous && previous.payload.state === payload.payload.state && !commandCodeNewTurn - ? previous.stateStartedAt + previousTurn && previousTurn.payload.state === payload.payload.state && !commandCodeNewTurn + ? previousTurn.stateStartedAt : (observedAt ?? now) - const turnStartedAt = resolveTurnStartedAt(previous, payload, observedAt ?? now) + const turnStartedAt = resolveTurnStartedAt(previousTurn, payload, observedAt ?? now) // Relay rows carry their execution host's turn-end evidence. const { hostTurn, ...turnEnd } = reduceHostTurnEnd( - previous, + previousTurn, payload, observedAt ?? now, payload.connectionId === null @@ -174,9 +188,9 @@ export abstract class AgentHookServerStatusApplication extends AgentHookServerSt ...turnEnd, ...(hostTurn ? { hostTurn } : {}) }, - previous, + previousTurn, leadReplyOf(payload), - !!previous && this.sameTerminalOwner(previous, payload) + !!previousTurn && this.sameTerminalOwner(previousTurn, payload) ) } diff --git a/src/main/agent-hooks/server/server-status-update.ts b/src/main/agent-hooks/server/server-status-update.ts index 4e1a6bb92a3..df37b55c600 100644 --- a/src/main/agent-hooks/server/server-status-update.ts +++ b/src/main/agent-hooks/server/server-status-update.ts @@ -6,10 +6,8 @@ import { needsHookRootEvidence, readHookProcessVerdict } from '../../../shared/agent-hook-listener/presence/agent-hook-owner-presence' -import { - reconcileCodexStatusEvent, - markCodexLeadTurnInterrupted -} from '../../../shared/agent-hook-listener/providers/codex-state' +import { markCodexLeadTurnInterrupted } from '../../../shared/agent-hook-listener/providers/codex-state' +import { reconcileCodexStatusEvent } from '../../../shared/agent-hook-listener/providers/codex-session-start' import { resolveAgentStatusIdentity, shouldSuppressInheritedTerminalStatus @@ -160,10 +158,6 @@ export abstract class AgentHookServerStatusUpdate extends AgentHookServerStatusA this.commitStatusRowMutation(rowBefore, previous) return previous } - // Any submitted prompt (incl. a replayed Codex start) opens a new turn; OSC must not inherit the old stamp. - if (payload.hookEventName === 'UserPromptSubmit' || payload.hasExplicitPrompt === true) { - this.activeHookTurnCompletedAtByPaneKey.delete(payload.paneKey) - } if (terminalOwnedPayload.connectionId) { this.connectionTimestampWatermarkById.set(terminalOwnedPayload.connectionId, now) } @@ -194,9 +188,15 @@ export abstract class AgentHookServerStatusUpdate extends AgentHookServerStatusA const stateReconciledPayload = reconcileCodexStatusEvent( this.state, terminalOwnedPayload, - previous?.payload, + previous, codexRosterAuthority ) + if (!stateReconciledPayload) { + return undefined + } + if (previous && stateReconciledPayload === previous) { + return previous + } // Relay restart loses root context; child hooks must not erase persisted resume/model identity. const rootContextPreservingPayload = retainHookRootContext( stateReconciledPayload, diff --git a/src/main/agent-hooks/server/testing/identity/server-claude-resume-record-continuity.test.ts b/src/main/agent-hooks/server/testing/identity/server-claude-resume-record-continuity.test.ts index 6ba51d5812e..6c356e7b35b 100644 --- a/src/main/agent-hooks/server/testing/identity/server-claude-resume-record-continuity.test.ts +++ b/src/main/agent-hooks/server/testing/identity/server-claude-resume-record-continuity.test.ts @@ -73,3 +73,20 @@ it('records where a resumed process really started over the folder its seed name expect(recordedSession()?.cwd).toBe('/elsewhere') }) + +it('names the pane session from a --fork-session start, which Claude 2.1.214+ reports as fork', async () => { + // Captured from Claude 2.1.321 `claude --resume --fork-session` before any message. + await claudeHook({ + session_id: 'b47ec7fc-07e4-4012-8b28-1d5daa938f52', + transcript_path: + '/Users/u/.claude/projects/-private-tmp-w39942fork/b47ec7fc-07e4-4012-8b28-1d5daa938f52.jsonl', + cwd: '/private/tmp/w39942fork', + hook_event_name: 'SessionStart', + source: 'fork' + }) + + expect(recordedSession()).toMatchObject({ + id: 'b47ec7fc-07e4-4012-8b28-1d5daa938f52', + cwd: '/private/tmp/w39942fork' + }) +}) diff --git a/src/main/agent-hooks/server/testing/providers/codex/server-codex-session-start.test.ts b/src/main/agent-hooks/server/testing/providers/codex/server-codex-session-start.test.ts new file mode 100644 index 00000000000..5d572c7bbb1 --- /dev/null +++ b/src/main/agent-hooks/server/testing/providers/codex/server-codex-session-start.test.ts @@ -0,0 +1,615 @@ +import { mkdtempSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { expect, it, vi } from 'vitest' +import { RelayAgentHookServer } from '../../../../../../wsl-guest/agent-hook-server' +import { buildCachedPaneReplayEnvelope } from '../../../../../../wsl-guest/agent-hook-cached-pane-status' +import type { AgentHookEventPayload } from '../../../../../../shared/agent-hook-listener/listener-event' +import type { AgentHookRelayEnvelope } from '../../../../../../shared/agent-hook-listener/transport/agent-hook-relay' +import { AgentHookServer } from '../../../agent-hook-server' +import { selectFreshExplicitAgentStatus } from '../../../../../runtime/workspaces/process-listing/runtime-hook-agent-row-selection' +import { buildBody, postHookEvent, PANE } from '../../server.test-fixtures' + +function previousRoot(children: number): AgentHookEventPayload { + return { + paneKey: PANE, + connectionId: null, + worktreeId: 'old-owner', + source: 'codex', + hookEventName: 'SubagentStart', + toolAgentId: 'old-0', + providerSession: { key: 'session_id', id: 'old-root' }, + payload: { + agentType: 'codex', + state: 'working', + model: 'old-model', + prompt: 'old task', + mainAgent: { state: 'done', stateStartedAt: 1000 }, + subagents: Array.from({ length: children }, (_, index) => ({ + id: `old-${index}`, + state: 'working', + startedAt: 1000 + })) + } + } +} + +it.each([10, 100])( + 'retires %i foreign-session children at a cached startup boundary', + (children) => { + const desktop = new AgentHookServer() + desktop.beginRemoteRelaySession('wsl:Ubuntu') + desktop.ingestRemote(previousRoot(children), 'wsl:Ubuntu') + expect(desktop.getStatusSnapshot()[0].subagents).toHaveLength(children) + if (children === 100) { + desktop.beginRemoteRelaySession('wsl:Ubuntu') + } + const incoming: AgentHookEventPayload = { + ...previousRoot(0), + toolAgentId: undefined, + hookEventName: 'SessionStart', + providerSession: { key: 'session_id', id: 'new-root' }, + payload: { + agentType: 'codex', + state: 'done', + sessionBoundary: true, + prompt: 'new context', + model: 'new-model', + mainAgent: { state: 'done', outcome: 'cancellation', stateStartedAt: 2000 } + } + } + desktop.ingestRemote( + buildCachedPaneReplayEnvelope({ event: incoming, meta: { source: 'codex' } }), + 'wsl:Ubuntu' + ) + expect(desktop.getStatusSnapshot()[0]).toMatchObject({ + state: 'done', + restoredUnconfirmed: true, + providerSession: { id: 'new-root' }, + prompt: 'new context', + model: 'new-model', + mainAgent: { state: 'done', outcome: 'cancellation', stateStartedAt: 2000 } + }) + expect(desktop.getStatusSnapshot()[0].subagents).toBeUndefined() + expect(desktop._getStateForTests().codexSubagentRosterByPaneKey.get(PANE)?.size).toBe(0) + desktop.ingestRemote({ ...incoming, hookEventName: undefined }, 'wsl:Ubuntu') + expect(desktop.getStatusSnapshot()[0].subagents).toBeUndefined() + } +) + +it.each([false, true])( + 'retains only the incoming owner’s child in a legacy=%s replay', + (legacy) => { + const desktop = new AgentHookServer() + desktop.beginRemoteRelaySession('wsl:Ubuntu') + desktop.ingestRemote(previousRoot(1), 'wsl:Ubuntu') + const incoming: AgentHookEventPayload = { + ...previousRoot(0), + worktreeId: 'new-owner', + toolAgentId: undefined, + hookEventName: 'SessionStart', + payload: { + agentType: 'codex', + state: 'working', + prompt: 'new context', + model: 'new-model', + mainAgent: legacy ? undefined : { state: 'done', stateStartedAt: 2000 }, + subagents: [{ id: 'own-child', state: 'working', startedAt: 2000 }] + } + } + desktop.ingestRemote( + buildCachedPaneReplayEnvelope({ event: incoming, meta: { source: 'codex' } }), + 'wsl:Ubuntu' + ) + expect(desktop.getStatusSnapshot()[0]).toMatchObject({ + state: 'working', + restoredUnconfirmed: true, + worktreeId: 'new-owner', + prompt: 'new context', + model: 'new-model', + mainAgent: { state: 'done' }, + subagents: [{ id: 'own-child', state: 'working' }] + }) + expect(desktop._getStateForTests().codexSubagentRosterByPaneKey.get(PANE)?.size).toBe(1) + desktop.ingestRemote( + { + ...incoming, + hookEventName: undefined, + payload: { + ...incoming.payload, + state: 'done', + mainAgent: { state: 'done', stateStartedAt: 2000 }, + subagents: undefined + } + }, + 'wsl:Ubuntu' + ) + expect(desktop.getStatusSnapshot()[0]).toMatchObject({ state: 'done' }) + expect(desktop.getStatusSnapshot()[0].subagents).toBeUndefined() + expect(desktop._getStateForTests().codexSubagentRosterByPaneKey.get(PANE)?.size).toBe(0) + } +) + +it.each([false, true])( + 'does not lend work to a different legacy live session with main=%s', + (main) => { + const desktop = new AgentHookServer() + const previous = previousRoot(1) + desktop.ingestRemote( + { + ...previous, + payload: { ...previous.payload, mainAgent: { state: 'working', stateStartedAt: 1000 } } + }, + 'wsl:Ubuntu' + ) + expect(desktop.getStatusSnapshot()[0]).toMatchObject({ + mainAgent: { state: 'working', stateStartedAt: 1000 }, + subagents: [{ id: 'old-0', state: 'working' }] + }) + desktop.ingestRemote( + { + ...previous, + toolAgentId: undefined, + hookEventName: 'SessionStart', + providerSession: { key: 'session_id', id: 'new-root' }, + payload: { + agentType: 'codex', + state: 'working', + prompt: 'old cached text', + mainAgent: main ? { state: 'working', stateStartedAt: 1000 } : undefined + } + }, + 'wsl:Ubuntu' + ) + expect(desktop.getStatusSnapshot()[0]).toMatchObject({ + providerSession: { id: 'new-root' }, + state: 'done', + sessionBoundary: true, + mainAgent: { state: 'done' } + }) + expect(desktop.getStatusSnapshot()[0].subagents).toBeUndefined() + expect(desktop.getStatusSnapshot()[0].prompt).toBe('') + expect(desktop.getStatusSnapshot()[0].model).toBeUndefined() + } +) + +it.each(['previous', 'incoming'] as const)( + 'preserves live context when the %s session ID is missing', + (missing) => { + const desktop = new AgentHookServer() + const previous = previousRoot(1) + vi.useFakeTimers({ toFake: ['Date'] }) + vi.setSystemTime(1_000) + try { + desktop.ingestRemote( + { + ...previous, + hookEventName: 'UserPromptSubmit', + toolAgentId: undefined, + hasExplicitPrompt: true, + providerSession: missing === 'previous' ? undefined : previous.providerSession, + payload: { ...previous.payload, mainAgent: { state: 'working', stateStartedAt: 1000 } } + }, + 'wsl:Ubuntu' + ) + expect(desktop.getStatusSnapshot()[0].turnStartedAt).toBe(1_000) + vi.setSystemTime(3_000) + desktop.ingestRemote( + { + ...previous, + toolAgentId: undefined, + hookEventName: 'SessionStart', + providerSession: + missing === 'incoming' ? undefined : { key: 'session_id', id: 'new-root' }, + payload: { agentType: 'codex', state: 'working', prompt: '' } + }, + 'wsl:Ubuntu' + ) + expect(desktop.getStatusSnapshot()[0]).toMatchObject({ + state: 'working', + prompt: 'old task', + model: 'old-model', + mainAgent: { state: 'working', stateStartedAt: 1000 }, + subagents: [{ id: 'old-0', state: 'working' }], + turnStartedAt: 1_000 + }) + } finally { + desktop.stop() + vi.useRealTimers() + } + } +) + +it.each(['live', 'cached', 'old-relay'] as const)( + 'drops a completed foreign root’s host facts on %s startup', + (route) => { + vi.useFakeTimers({ toFake: ['Date'] }) + const desktop = new AgentHookServer() + const root: AgentHookEventPayload = { + paneKey: PANE, + connectionId: null, + source: 'codex', + hookEventName: 'UserPromptSubmit', + hasExplicitPrompt: true, + providerSession: { key: 'session_id', id: 'root-a' }, + payload: { + agentType: 'codex', + state: 'working', + prompt: 'first', + mainAgent: { state: 'working', stateStartedAt: 1_000 } + } + } + try { + vi.setSystemTime(1_000) + desktop.ingestRemote(root, 'ssh-clock') + expect(desktop.getStatusSnapshot()[0].turnStartedAt).toBe(1_000) + vi.setSystemTime(2_000) + desktop.ingestRemote( + { + ...root, + hookEventName: 'Stop', + hasExplicitPrompt: false, + turnEndedAt: 2_000, + turnEndDisposition: 'identified', + payload: { + agentType: 'codex', + state: 'done', + prompt: 'first', + mainAgent: { state: 'done', stateStartedAt: 2_000 } + } + }, + 'ssh-clock' + ) + expect(desktop.getStatusSnapshot()[0]).toMatchObject({ + turnStartedAt: 1_000, + turnEndedAt: 2_000, + stateStartedAt: 2_000 + }) + vi.setSystemTime(3_000) + const startup: AgentHookEventPayload = { + ...root, + source: route === 'old-relay' ? undefined : 'codex', + hookEventName: 'SessionStart', + hasExplicitPrompt: false, + providerSession: { key: 'session_id', id: 'root-b' }, + payload: { + agentType: 'codex', + state: 'done', + prompt: '', + sessionBoundary: true, + mainAgent: { state: 'done', stateStartedAt: 2_000 } + } + } + desktop.ingestRemote( + route === 'cached' + ? buildCachedPaneReplayEnvelope({ event: startup, meta: { source: 'codex' } }) + : startup, + 'ssh-clock' + ) + expect(desktop.getStatusSnapshot()[0]).toMatchObject({ + state: 'done', + providerSession: { id: 'root-b' }, + stateStartedAt: 3_000 + }) + expect(desktop.getStatusSnapshot()[0].turnStartedAt).toBeUndefined() + expect(desktop.getStatusSnapshot()[0].turnEndedAt).toBeUndefined() + expect(desktop.getStatusSnapshot()[0].turnEndDisposition).toBeUndefined() + } finally { + desktop.stop() + vi.useRealTimers() + } + } +) + +it.each(['local', 'relay', 'old-relay', 'old-relay-no-main'] as const)( + 'separates Codex session identity from live turns on %s', + async (route) => { + vi.useFakeTimers({ toFake: ['Date'] }) + vi.setSystemTime(1_000) + const directory = mkdtempSync(join(tmpdir(), 'codex-session-start-')) + const desktop = new AgentHookServer() + let forwarded: AgentHookRelayEnvelope | undefined + const relay = + route === 'local' + ? undefined + : new RelayAgentHookServer({ + endpointDir: directory, + forward: (event) => { + forwarded = event + return desktop.ingestRemote( + route.startsWith('old-') && event.hookEventName === 'SessionStart' + ? { + ...event, + payload: { + ...event.payload, + state: 'working', + sessionBoundary: undefined, + mainAgent: + route === 'old-relay-no-main' + ? undefined + : { state: 'working', stateStartedAt: 900 } + } + } + : event, + 'ssh-connection' + ) + } + }) + await desktop.start({ env: 'production' }) + await relay?.start() + async function post(payload: Record): Promise { + const body = buildBody({ session_id: 'root-session', ...payload }) + const coordinates = relay?.getCoordinates() + const response = coordinates + ? await fetch(`http://127.0.0.1:${coordinates.port}/hook/codex`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Connection: 'close', + 'X-Orca-Agent-Hook-Token': coordinates.token + }, + body: JSON.stringify(body) + }) + : await postHookEvent(desktop, body, '/hook/codex') + expect(response.status).toBe(204) + } + try { + await post({ hook_event_name: 'SessionStart', source: 'startup' }) + expect(desktop.getStatusSnapshot()[0]).toMatchObject({ + state: 'done', + sessionBoundary: true, + mainAgent: { state: 'done' } + }) + expect(desktop.getStatusSnapshot()[0].turnStartedAt).toBeUndefined() + expect(desktop.getStatusSnapshot()[0].turnEndedAt).toBeUndefined() + await post({ hook_event_name: 'UserPromptSubmit', turn_id: 'turn-1', prompt: 'review' }) + expect(desktop.getStatusSnapshot()[0].turnStartedAt).toBe(1_000) + await post({ hook_event_name: 'SubagentStart', agent_id: 'child-session' }) + vi.setSystemTime(2_000) + await post({ hook_event_name: 'SessionStart', source: 'compact' }) + expect(desktop.getStatusSnapshot()[0]).toMatchObject({ + state: 'working', + prompt: 'review', + turnStartedAt: 1_000, + mainAgent: { state: 'working', stateStartedAt: 1_000 }, + subagents: [{ id: 'child-session', state: 'working' }], + providerSession: { id: 'root-session' } + }) + await post({ hook_event_name: 'SessionStart', source: 'future-continuation' }) + expect(desktop.getStatusSnapshot()[0].mainAgent).toMatchObject({ stateStartedAt: 1_000 }) + // A schema-shaped legacy child start must not take over its parent's row. + await post({ + hook_event_name: 'SessionStart', + source: 'resume', + session_id: 'child-session', + transcript_path: join(directory, 'child-rollout.jsonl') + }) + expect(desktop.getStatusSnapshot()[0]).toMatchObject({ + mainAgent: { state: 'working', stateStartedAt: 1_000 }, + providerSession: { id: 'root-session' } + }) + if (route === 'local') { + vi.setSystemTime(2_500) + await post({ hook_event_name: 'UserPromptSubmit', turn_id: 'turn-1', prompt: 'review' }) + expect(desktop.getStatusSnapshot()[0].mainAgent).toMatchObject({ stateStartedAt: 1_000 }) + } + vi.setSystemTime(3_000) + await post({ + hook_event_name: 'PreToolUse', + tool_name: 'request_user_input', + tool_input: { questions: [] } + }) + vi.setSystemTime(4_000) + await post({ hook_event_name: 'SessionStart', source: 'compact' }) + expect(desktop.getStatusSnapshot()[0]).toMatchObject({ + state: 'waiting', + turnStartedAt: route === 'local' ? 2_500 : 1_000, + prompt: 'review', + toolName: 'request_user_input', + mainAgent: { state: 'waiting', stateStartedAt: 3_000 }, + subagents: [{ id: 'child-session', state: 'working' }] + }) + if (route === 'local' || route === 'relay') { + vi.setSystemTime(5_000) + await post({ hook_event_name: 'PostToolUse', tool_name: 'Bash' }) + vi.setSystemTime(6_000) + await post({ hook_event_name: 'UserPromptSubmit', turn_id: 'turn-2', prompt: 'next turn' }) + expect(desktop.getStatusSnapshot()[0].mainAgent).toMatchObject({ stateStartedAt: 6_000 }) + await post({ hook_event_name: 'Stop' }) + await post({ hook_event_name: 'SubagentStart', agent_id: 'child-session' }) + expect(desktop.getStatusSnapshot()[0]).toMatchObject({ + state: 'working', + mainAgent: { state: 'done', stateStartedAt: 6_000 }, + subagents: [{ id: 'child-session', state: 'working' }] + }) + vi.setSystemTime(7_000) + await post({ hook_event_name: 'SessionStart', source: 'compact' }) + expect(desktop.getStatusSnapshot()[0]).toMatchObject({ + state: 'working', + mainAgent: { state: 'done', stateStartedAt: 6_000 }, + subagents: [{ id: 'child-session', state: 'working' }] + }) + await post({ hook_event_name: 'SubagentStop', agent_id: 'child-session' }) + await post({ hook_event_name: 'Interrupt' }) + vi.setSystemTime(8_000) + await post({ hook_event_name: 'SessionStart', source: 'compact' }) + expect(desktop.getStatusSnapshot()[0]).toMatchObject({ + state: 'done', + sessionBoundary: true, + turnStartedAt: 6_000, + mainAgent: { state: 'done', stateStartedAt: 6_000, outcome: 'cancellation' } + }) + await post({ + hook_event_name: 'UserPromptSubmit', + turn_id: 'turn-3', + prompt: 'old session work' + }) + expect(desktop.getStatusSnapshot()[0].turnStartedAt).toBe(8_000) + vi.setSystemTime(9_000) + await post({ hook_event_name: 'Stop', last_assistant_message: 'old reply' }) + expect(desktop.getStatusSnapshot()[0].turnEndedAt).toBe(9_000) + if (route === 'relay') { + expect(forwarded).toMatchObject({ turnEndedAt: 9_000, turnEndDisposition: 'identified' }) + } + if (route === 'local') { + expect(desktop.getAcceptedReply(PANE, 9_000)).toBe('old reply') + } + vi.setSystemTime(route === 'local' ? 9_000 : 10_000) + await post({ + hook_event_name: 'SessionStart', + source: 'startup', + session_id: 'replacement-session', + transcript_path: join(directory, 'replacement.jsonl') + }) + expect(desktop.getStatusSnapshot()[0]).toMatchObject({ + state: 'done', + sessionBoundary: true, + mainAgent: { state: 'done' }, + providerSession: { id: 'replacement-session' } + }) + if (route === 'relay') { + expect(forwarded).toMatchObject({ + hookEventName: 'SessionStart', + providerSession: { id: 'replacement-session' } + }) + expect(forwarded?.turnEndedAt).toBeUndefined() + expect(forwarded?.turnEndDisposition).toBeUndefined() + } + expect(desktop.getStatusSnapshot()[0].turnStartedAt).toBeUndefined() + expect(desktop.getStatusSnapshot()[0].turnEndedAt).toBeUndefined() + expect(desktop.getStatusSnapshot()[0].turnEndDisposition).toBeUndefined() + expect(desktop.getStatusSnapshot()[0].stateStartedAt).toBe( + route === 'local' ? 9_000 : 10_000 + ) + expect(desktop.getAcceptedReply(PANE, 9_000)).toBeUndefined() + vi.setSystemTime(11_000) + await post({ + hook_event_name: 'UserPromptSubmit', + session_id: 'replacement-session', + prompt: 'replacement task' + }) + expect(desktop.getStatusSnapshot()[0]).toMatchObject({ + state: 'working', + prompt: 'replacement task', + turnStartedAt: 11_000 + }) + } + } finally { + relay?.stop() + desktop.stop() + vi.useRealTimers() + rmSync(directory, { recursive: true, force: true }) + } + } +) + +it('does not confirm restored work from an older relay startup or redate a submitted replay', async () => { + const directory = mkdtempSync(join(tmpdir(), 'codex-restored-start-')) + const first = new AgentHookServer() + const desktop = new AgentHookServer() + const event = { + paneKey: PANE, + source: 'codex' as const, + providerSession: { key: 'session_id' as const, id: 'root-session' }, + payload: { + state: 'working' as const, + agentType: 'codex' as const, + prompt: 'old task', + mainAgent: { state: 'working' as const, stateStartedAt: 1_000 } + } + } + try { + await first.start({ env: 'production', userDataPath: directory }) + first.ingestRemote({ ...event, hookEventName: 'UserPromptSubmit' }, 'ssh-connection') + first.flushStatusPersistSync() + first.stop() + await desktop.start({ env: 'production', userDataPath: directory }) + expect(desktop.getStatusSnapshot()[0].restoredUnconfirmed).toBe(true) + desktop.ingestRemote({ ...event, hookEventName: 'SessionStart' }, 'ssh-connection') + expect(desktop.getStatusSnapshot()[0]).toMatchObject({ + state: 'working', + restoredUnconfirmed: true, + providerSession: { id: 'root-session' }, + mainAgent: { state: 'working', stateStartedAt: 1_000 } + }) + desktop.ingestRemote( + { + ...event, + hookEventName: 'SessionStart', + hasExplicitPrompt: true, + isReplay: true + }, + 'ssh-connection' + ) + expect(desktop.getStatusSnapshot()[0].restoredUnconfirmed).toBe(true) + desktop.ingestRemote( + { + ...event, + hookEventName: 'SubagentStart', + toolAgentId: 'child-1', + payload: { + ...event.payload, + subagents: [{ id: 'child-1', state: 'working', startedAt: 1_000 }] + } + }, + 'ssh-connection' + ) + expect(desktop.getStatusSnapshot()[0]).toMatchObject({ + restoredUnconfirmed: true, + subagents: [{ id: 'child-1', state: 'working' }] + }) + desktop.ingestRemote( + { + ...event, + hookEventName: 'UserPromptSubmit', + hasExplicitPrompt: true, + payload: { ...event.payload, mainAgent: { state: 'working', stateStartedAt: 6_000 } } + }, + 'ssh-connection' + ) + expect(desktop.getStatusSnapshot()[0].restoredUnconfirmed).toBeUndefined() + desktop.ingestRemote( + { + ...event, + hookEventName: 'Interrupt', + payload: { + ...event.payload, + state: 'done', + mainAgent: { state: 'done', outcome: 'cancellation', stateStartedAt: 7_000 } + } + }, + 'ssh-connection' + ) + const stopped = desktop.getStatusSnapshot()[0] + expect(stopped.mainAgent).toMatchObject({ state: 'done', outcome: 'cancellation' }) + desktop.ingestRemote( + { ...event, hookEventName: 'SessionStart', hasExplicitPrompt: true, isReplay: true }, + 'ssh-connection' + ) + expect(desktop.getStatusSnapshot()[0]).toEqual(stopped) + expect( + selectFreshExplicitAgentStatus({ + handle: 'fixture', + paneKey: PANE, + hookRows: desktop.getStatusSnapshot() + })?.mainAgentWorkingStartedAt + ).toBeNull() + vi.spyOn(Date, 'now').mockReturnValue(stopped.receivedAt + 31 * 60_000) + desktop.ingestRemote( + { ...event, hookEventName: 'SessionStart', hasExplicitPrompt: true, isReplay: true }, + 'ssh-connection' + ) + expect( + selectFreshExplicitAgentStatus({ + handle: 'fixture', + paneKey: PANE, + hookRows: desktop.getStatusSnapshot() + }) + ).toBeNull() + } finally { + vi.restoreAllMocks() + first.stop() + desktop.stop() + rmSync(directory, { recursive: true, force: true }) + } +}) diff --git a/src/main/agent-hooks/server/testing/providers/codex/server-codex-side-thread.test.ts b/src/main/agent-hooks/server/testing/providers/codex/server-codex-side-thread.test.ts index b2602cf3297..71b9f21e380 100644 --- a/src/main/agent-hooks/server/testing/providers/codex/server-codex-side-thread.test.ts +++ b/src/main/agent-hooks/server/testing/providers/codex/server-codex-side-thread.test.ts @@ -3,6 +3,7 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, expect, it } from 'vitest' import { AgentHookServer } from '../../../agent-hook-server' +import { RelayAgentHookServer } from '../../../../../../wsl-guest/agent-hook-server' import { buildBody, postHookEvent } from '../../server.test-fixtures' // Shapes captured from Codex 0.164: a /side fork is its own session with no rollout. @@ -148,24 +149,79 @@ it.each([ } }) -it('lets a new ephemeral Codex conversation take over a row still working, without its old prompt', async () => { - const dir = mkdtempSync(join(tmpdir(), 'codex-side-thread-')) - dirs.push(dir) - const server = new AgentHookServer() - await server.start({ env: 'production' }) - const post = (payload: Record): Promise => - postHookEvent(server, buildBody(payload), '/hook/codex') - try { - const main = { session_id: MAIN, transcript_path: join(dir, 'rollout-main.jsonl') } - await post({ ...main, hook_event_name: 'UserPromptSubmit', prompt: 'main task' }) - await post({ ...TITLE_THREAD, hook_event_name: 'SessionStart', source: 'startup' }) - await post({ ...TITLE_THREAD, hook_event_name: 'UserPromptSubmit', prompt: 'next task' }) - expect(server.getStatusSnapshot()[0]).toMatchObject({ - state: 'working', - prompt: 'next task', - providerSession: { id: SIDE } - }) - } finally { - server.stop() +it.each(['local', 'relay', 'relay-done'])( + 'lets a submitted Codex conversation take over without its old prompt: %s', + async (route) => { + const dir = mkdtempSync(join(tmpdir(), 'codex-side-thread-')) + dirs.push(dir) + const server = new AgentHookServer() + await server.start({ env: 'production' }) + const relay = + route === 'local' + ? undefined + : new RelayAgentHookServer({ + endpointDir: dir, + forward: (event) => server.ingestRemote(event, 'ssh-connection') + }) + await relay?.start() + const post = (payload: Record): Promise => { + const coordinates = relay?.getCoordinates() + return coordinates + ? fetch(`http://127.0.0.1:${coordinates.port}/hook/codex`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Connection: 'close', + 'X-Orca-Agent-Hook-Token': coordinates.token + }, + body: JSON.stringify(buildBody(payload)) + }) + : postHookEvent(server, buildBody(payload), '/hook/codex') + } + try { + const main = { session_id: MAIN, transcript_path: join(dir, 'rollout-main.jsonl') } + await post({ ...main, hook_event_name: 'UserPromptSubmit', prompt: 'main task' }) + await post({ ...main, hook_event_name: 'SubagentStart', agent_id: 'old-child' }) + if (route === 'relay') { + for (const [session_id, prompt] of [ + ['01a16f91-0000-4000-8000-000000000001', TITLE_PROMPT], + [ + '01a16f91-0000-4000-8000-000000000002', + 'Write a brief catch-up for a user returning to this task.' + ] + ]) { + const hidden = { session_id, transcript_path: null } + await post({ ...hidden, hook_event_name: 'SessionStart', source: 'startup' }) + expect(server.getStatusSnapshot()[0]).toMatchObject({ + prompt: 'main task', + providerSession: { id: MAIN } + }) + await post({ ...hidden, hook_event_name: 'UserPromptSubmit', prompt }) + await post({ ...hidden, hook_event_name: 'Stop' }) + expect(server.getStatusSnapshot()[0]).toMatchObject({ + state: 'working', + prompt: 'main task', + providerSession: { id: MAIN }, + subagents: [{ id: 'old-child', state: 'working' }] + }) + } + } + if (route === 'relay-done') { + await post({ ...main, hook_event_name: 'Stop' }) + } + await post({ ...TITLE_THREAD, hook_event_name: 'SessionStart', source: 'startup' }) + await post({ ...TITLE_THREAD, hook_event_name: 'UserPromptSubmit', prompt: 'next task' }) + expect(server.getStatusSnapshot()[0]).toMatchObject({ + state: 'working', + prompt: 'next task', + providerSession: { id: SIDE } + }) + expect(server.getStatusSnapshot()[0].subagents).toBeUndefined() + await post({ ...TITLE_THREAD, hook_event_name: 'Stop' }) + expect(server.getStatusSnapshot()[0].state).toBe('done') + } finally { + relay?.stop() + server.stop() + } } -}) +) diff --git a/src/main/agent-hooks/server/testing/providers/codex/server-ingest-codex-async-question.test.ts b/src/main/agent-hooks/server/testing/providers/codex/server-ingest-codex-async-question.test.ts index e38f537c8ce..8b32c0af0c4 100644 --- a/src/main/agent-hooks/server/testing/providers/codex/server-ingest-codex-async-question.test.ts +++ b/src/main/agent-hooks/server/testing/providers/codex/server-ingest-codex-async-question.test.ts @@ -120,6 +120,30 @@ describe('Codex async questions', () => { expect(steered?.interactivePrompt).toBeUndefined() }) + it('keeps a compacted question with its session and releases it on replacement', () => { + const pane = codexPane() + pane.report({ hook_event_name: 'UserPromptSubmit', turn_id: 'turn-1', prompt: 'Set up a db' }) + pane.report(ASK) + expect(pane.report(ACCEPTED).main).toMatchObject(waitingOnQuestion) + expect(pane.report({ hook_event_name: 'SessionStart', source: 'compact' }).main).toMatchObject( + waitingOnQuestion + ) + const replacement = pane.report({ + hook_event_name: 'SessionStart', + source: 'startup', + session_id: 'replacement' + }).main + expect(replacement).toMatchObject({ state: 'done', providerSession: { id: 'replacement' } }) + expect(replacement?.interactivePrompt).toBeUndefined() + const working = pane.report({ + hook_event_name: 'PostToolUse', + session_id: 'replacement', + tool_name: 'Bash' + }).main + expect(working).toMatchObject({ state: 'working' }) + expect(working?.interactivePrompt).toBeUndefined() + }) + it('drops an unanswered question when the turn ends', () => { const pane = codexPane() pane.report({ hook_event_name: 'UserPromptSubmit', turn_id: 'turn-1', prompt: 'Set up a db' }) diff --git a/src/main/agent-hooks/server/testing/remote-ingress/server-ingest-remote-codex-transcript.test.ts b/src/main/agent-hooks/server/testing/remote-ingress/server-ingest-remote-codex-transcript.test.ts index 47ee6fd17ca..288031d6990 100644 --- a/src/main/agent-hooks/server/testing/remote-ingress/server-ingest-remote-codex-transcript.test.ts +++ b/src/main/agent-hooks/server/testing/remote-ingress/server-ingest-remote-codex-transcript.test.ts @@ -1,6 +1,16 @@ +import { appendFileSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { normalizeHookPayload } from '../../../../../shared/agent-hook-listener/normalization/normalize-hook-payload' +import { createHookListenerState } from '../../../../../shared/agent-hook-listener/listener-state' +import { pollCodexTranscriptStatus } from '../../../../../shared/agent-hook-listener/providers/codex-transcript-poll' +import { transcriptPollUpdate } from '../../../../../shared/agent-hook-listener/transcript-poll-policy' +import { buildRelayHookEnvelope } from '../../../../../wsl-guest/agent-hook-envelope-build' +import { buildCachedPaneReplayEnvelope } from '../../../../../wsl-guest/agent-hook-cached-pane-status' +import type { AgentHookEventPayload } from '../../../../../shared/agent-hook-listener/listener-event' import { AgentHookServer, _internals } from '../../agent-hook-server' -import { PANE } from '../server.test-fixtures' +import { PANE, buildBody } from '../server.test-fixtures' vi.mock('../../../../telemetry/client', () => ({ track: vi.fn() })) vi.mock('../../../../telemetry/cohort-classifier', () => ({ @@ -8,19 +18,178 @@ vi.mock('../../../../telemetry/cohort-classifier', () => ({ })) const CHILD = '01a16839-7f84-7ed3-aebf-ea51e50330cf' +const SECOND_CHILD = '02a16839-7f84-7ed3-aebf-ea51e50330cf' +const line = (type: string): string => + `${JSON.stringify({ type: 'event_msg', payload: { type } })}\n` + +let directory: string beforeEach(() => { _internals.resetCachesForTests() + directory = mkdtempSync(join(tmpdir(), 'orca-remote-codex-child-')) }) afterEach(() => { + rmSync(directory, { recursive: true, force: true }) vi.restoreAllMocks() }) +describe('remote Codex transcript completion', () => { + it('retires transcript-completed children while keeping surviving child work live', () => { + const parentPath = join(directory, 'rollout-parent.jsonl') + const childPath = join(directory, `rollout-child-${CHILD}.jsonl`) + const secondChildPath = join(directory, `rollout-child-${SECOND_CHILD}.jsonl`) + writeFileSync(parentPath, line('task_complete')) + writeFileSync(childPath, line('task_started')) + writeFileSync(secondChildPath, line('task_started')) + const hostState = createHookListenerState() + const server = new AgentHookServer() + const root = normalizeHookPayload( + hostState, + 'codex', + buildBody({ + hook_event_name: 'Stop', + session_id: 'root', + transcript_path: parentPath + }), + 'production' + ) + if (!root) { + throw new Error('Root hook was rejected') + } + server.ingestRemote(buildRelayHookEnvelope(root, 'codex'), 'wsl:Ubuntu') + + const childBody = buildBody({ + hook_event_name: 'SubagentStart', + agent_id: CHILD, + session_id: CHILD, + transcript_path: childPath + }) + const started = normalizeHookPayload(hostState, 'codex', childBody, 'production') + if (!started) { + throw new Error('Child hook was rejected') + } + server.ingestRemote(buildRelayHookEnvelope(started, 'codex'), 'wsl:Ubuntu') + expect(server.getStatusSnapshot()).toEqual([ + expect.objectContaining({ + paneKey: PANE, + state: 'working', + subagents: [ + { + id: CHILD, + state: 'working', + startedAt: expect.any(Number) + } + ] + }) + ]) + + const secondChildBody = buildBody({ + hook_event_name: 'SubagentStart', + agent_id: SECOND_CHILD, + session_id: SECOND_CHILD, + transcript_path: secondChildPath + }) + const secondStarted = normalizeHookPayload(hostState, 'codex', secondChildBody, 'production') + if (!secondStarted) { + throw new Error('Second child hook was rejected') + } + server.ingestRemote(buildRelayHookEnvelope(secondStarted, 'codex'), 'wsl:Ubuntu') + expect( + server + .getStatusSnapshot()[0] + .subagents?.map((child) => child.id) + .sort() + ).toEqual([CHILD, SECOND_CHILD]) + + const compact = normalizeHookPayload( + hostState, + 'codex', + buildBody({ hook_event_name: 'SessionStart', source: 'compact', session_id: 'root' }), + 'production' + ) + if (!compact) { + throw new Error('Root compact was rejected') + } + server.ingestRemote(buildRelayHookEnvelope(compact, 'codex'), 'wsl:Ubuntu') + const beforeReplay = server.getStatusSnapshot()[0] + expect(beforeReplay).toMatchObject({ + mainAgent: { state: 'done' }, + subagents: [{ id: CHILD }, { id: SECOND_CHILD }] + }) + server.ingestRemote( + buildCachedPaneReplayEnvelope({ event: compact, meta: { source: 'codex' } }), + 'wsl:Ubuntu' + ) + expect(server.getStatusSnapshot()[0]).toEqual(beforeReplay) + + appendFileSync(childPath, line('task_complete')) + const oneRemaining = pollCodexTranscriptStatus( + hostState, + secondStarted, + secondChildBody, + 'production' + ) + if (!oneRemaining) { + throw new Error('No first child completion status') + } + const partialUpdate = transcriptPollUpdate('codex', secondStarted, oneRemaining) + if (!partialUpdate) { + throw new Error('Host did not publish first child completion') + } + expect(partialUpdate).toMatchObject({ + toolAgentId: SECOND_CHILD, + payload: { + state: 'working', + mainAgent: { state: 'done' }, + subagents: [expect.objectContaining({ id: SECOND_CHILD, state: 'working' })] + } + }) + expect(partialUpdate.hookEventName).toBeUndefined() + server.ingestRemote(buildRelayHookEnvelope(partialUpdate, 'codex'), 'wsl:Ubuntu') + expect(server.getStatusSnapshot()[0]).toMatchObject({ + state: 'working', + mainAgent: { state: 'done' }, + subagents: [expect.objectContaining({ id: SECOND_CHILD, state: 'working' })] + }) + + appendFileSync(secondChildPath, line('task_complete')) + const polled = pollCodexTranscriptStatus( + hostState, + partialUpdate, + secondChildBody, + 'production' + ) + if (!polled) { + throw new Error('No completion status') + } + const update = transcriptPollUpdate('codex', partialUpdate, polled) + if (!update) { + throw new Error('Host did not publish transcript completion') + } + expect(update).toMatchObject({ toolAgentId: SECOND_CHILD, payload: { state: 'done' } }) + expect(update.hookEventName).toBeUndefined() + expect(update.payload.subagents).toBeUndefined() + + server.ingestRemote(buildRelayHookEnvelope(update, 'codex'), 'wsl:Ubuntu') + + expect(server.getStatusSnapshot()).toEqual([ + expect.objectContaining({ + paneKey: PANE, + state: 'done', + mainAgent: expect.objectContaining({ state: 'done' }) + }) + ]) + expect(server.getStatusSnapshot()[0].subagents).toBeUndefined() + }) +}) + describe('remote Codex unlabelled roster removals', () => { - const childStart = (id: string, startedAt: number) => ({ + const childStart = (id: string, startedAt: number): AgentHookEventPayload => ({ paneKey: PANE, + connectionId: null, source: 'codex', + providerSession: { key: 'session_id', id: 'root' }, hookEventName: 'SubagentStart', toolAgentId: id, payload: { @@ -34,6 +203,7 @@ describe('remote Codex unlabelled roster removals', () => { const emptyPoll = (mainState: 'working' | 'done', toolAgentId?: string) => ({ paneKey: PANE, source: 'codex', + providerSession: { key: 'session_id', id: 'root' }, toolAgentId, payload: { agentType: 'codex', @@ -48,15 +218,65 @@ describe('remote Codex unlabelled roster removals', () => { server.beginRemoteRelaySession('wsl:Ubuntu') server.ingestRemote(childStart(CHILD, 1), 'wsl:Ubuntu') server.beginRemoteRelaySession('wsl:Ubuntu') + const replay = buildCachedPaneReplayEnvelope({ + event: { ...childStart(CHILD, 1), hookEventName: 'SessionStart', toolAgentId: undefined }, + meta: { source: 'codex' } + }) + server.ingestRemote(replay, 'wsl:Ubuntu') server.ingestRemote(emptyPoll('done'), 'wsl:Ubuntu') expect(server.getStatusSnapshotForPane(PANE)[0]).toMatchObject({ state: 'working', subagents: [expect.objectContaining({ id: CHILD })] }) + server._getStateForTests().codexSubagentRosterByPaneKey.delete(PANE) + server.ingestRemote(replay, 'wsl:Ubuntu') + server.ingestRemote(emptyPoll('done'), 'wsl:Ubuntu') + expect(server.getStatusSnapshotForPane(PANE)[0]?.subagents?.map((child) => child.id)).toEqual([ + CHILD + ]) server.ingestRemote(childStart(CHILD, 1), 'wsl:Ubuntu') server.ingestRemote(emptyPoll('done'), 'wsl:Ubuntu') expect(server.getStatusSnapshotForPane(PANE)[0]).toMatchObject({ state: 'done' }) expect(server.getStatusSnapshotForPane(PANE)[0]?.subagents).toBeUndefined() }) + + it.each([100, 1000])( + 'releases %i completed child lifetimes across cached starts', + (lifetimes) => { + const server = new AgentHookServer() + const state = server._getStateForTests() + server.beginRemoteRelaySession('wsl:Ubuntu') + server.ingestRemote(childStart(CHILD, 1), 'wsl:Ubuntu') + for (let index = 0; index < lifetimes; index++) { + const next = childStart(`completed-${index}`, index + 2) + server.ingestRemote(next, 'wsl:Ubuntu') + server.ingestRemote( + buildCachedPaneReplayEnvelope({ + event: { ...next, hookEventName: 'SessionStart', toolAgentId: undefined }, + meta: { source: 'codex' } + }), + 'wsl:Ubuntu' + ) + server.ingestRemote( + { + ...emptyPoll('done'), + payload: { + ...emptyPoll('done').payload, + subagents: [{ id: CHILD, state: 'working', startedAt: 1 }] + } + }, + 'wsl:Ubuntu' + ) + expect( + server.getStatusSnapshotForPane(PANE)[0]?.subagents?.map((child) => child.id) + ).toEqual([CHILD]) + expect(state.codexSubagentRosterByPaneKey.get(PANE)?.size).toBe(1) + } + server.ingestRemote(emptyPoll('done'), 'wsl:Ubuntu') + expect(server.getStatusSnapshotForPane(PANE)[0]).toMatchObject({ state: 'done' }) + expect(server.getStatusSnapshotForPane(PANE)[0]?.subagents).toBeUndefined() + expect(state.codexSubagentRosterByPaneKey.get(PANE)?.size).toBe(0) + } + ) }) diff --git a/src/main/claude-usage/claude-model-pricing.test.ts b/src/main/claude-usage/claude-model-pricing.test.ts index 5eea907a6cd..08a90fae976 100644 --- a/src/main/claude-usage/claude-model-pricing.test.ts +++ b/src/main/claude-usage/claude-model-pricing.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import type { ClaudeUsageTokenTotals } from './types' -import { estimateCostUsd, normalizeModelForPricing } from './claude-model-pricing' +import { estimateCostUsd } from './claude-model-pricing' const MTOK = 1_000_000 @@ -18,61 +18,10 @@ function estimate(model: string, ...counts: number[]): number | null { return estimateCostUsd(model, tokens(...counts)) } -describe('model pricing name matching', () => { - it.each([ - [' ANTHROPIC/claude-opus-4.1-thinking ', 'claude-opus-4-1'], - ['claude-opus-4-20250514', 'claude-opus-4'], - ['claude-opus-4.20250514', 'claude-opus-4'], - ['claude-opus-4@20250514', 'claude-opus-4'], - ['claude-opus-4-5@20251101', 'claude-opus-4-5'], - ['us.anthropic.claude-sonnet-4-5-20250929-v1:0', 'claude-sonnet-4-5'], - ['claude-sonnet-5-thinking', 'claude-sonnet-5'], - ['claude-sonnet-5-5', 'claude-sonnet-5-5'], - ['claude-sonnet-5.5[1m]', 'claude-sonnet-5-5'], - ['claude-opus-5.5[1m]', 'claude-opus-5-5'], - ['claude-opus-5-5-thinking', 'claude-opus-5-5'], - ['claude-fable-5-1[1m]', 'claude-fable-5-1'], - ['claude-fable-5-1m', 'claude-fable-5'], - ['claude-opus-4.8-thinking', 'claude-opus-4-8'], - ['claude-3-5-sonnet-20241022', 'claude-sonnet-3-5'], - ['claude-3.5-sonnet', 'claude-sonnet-3-5'], - ['claude-3-7-sonnet-20250219', 'claude-sonnet-3-7'], - ['claude-3-5-haiku-20241022', 'claude-haiku-3-5'], - ['claude-3-haiku-20240307', 'claude-haiku-3'], - ['claude-4.5-sonnet-thinking', 'claude-sonnet-4-5'], - ['model_placeholder_m26', 'claude-opus-4-6'] - ])('reads the family and version out of %s', (model, key) => { - expect(normalizeModelForPricing(model)).toBe(key) - }) - - it.each([ - 'claude-opus-5-6', - 'claude-sonnet-5-6', - 'claude-fable-5-3', - 'claude-opus-6', - 'claude-opus-4.9', - 'claude-opus-5-50', - 'claude-sonnet-50', - 'claude-3-opus-20240229', - 'sonnet', - 'gpt-5' - ])('leaves %s unpriced', (model) => { - expect(normalizeModelForPricing(model)).toBeNull() - }) -}) - describe('estimateCostUsd cache-write TTL rates', () => { it('splits a mixed write bucket without double-billing the 1-hour share', () => { expect(estimate('claude-opus-5', 0, 0, 0, 1_000_000, 400_000)).toBeCloseTo(7.75) }) - - it('clamps a 1-hour count that exceeds the reported write total', () => { - expect(estimate('claude-opus-5', 0, 0, 0, 1_000, 5_000)).toBeCloseTo(0.01) - }) - - it('still returns null for unknown models', () => { - expect(estimate('gpt-5', 0, 0, 0, 1_000_000, 1_000_000)).toBeNull() - }) }) describe('estimateCostUsd long-context requests', () => { @@ -89,17 +38,6 @@ describe('estimateCostUsd long-context requests', () => { }) ).toBeCloseTo(0.1 + 0.5) }) - - it('ignores a long-context subset for a model with flat rates', () => { - const long = tokens(MTOK) - expect(estimateCostUsd('claude-sonnet-4-6', { ...long, longContext: long })).toBeCloseTo(3) - }) - - it('clamps a long-context subset larger than its totals', () => { - expect( - estimateCostUsd('claude-haiku-5-5', { ...tokens(MTOK), longContext: tokens(3 * MTOK) }) - ).toBeCloseTo(0.5) - }) }) // Rate source: platform.claude.com/docs/en/about-claude/pricing. diff --git a/src/main/claude/structured/claude-structured-execution-observations.ts b/src/main/claude/structured/claude-structured-execution-observations.ts index 5ea6bba7408..bd27887fdfd 100644 --- a/src/main/claude/structured/claude-structured-execution-observations.ts +++ b/src/main/claude/structured/claude-structured-execution-observations.ts @@ -28,6 +28,7 @@ export function beginClaudeOptionMutation(session: ClaudeSession, key: string): if (key === 'model') { delete session.appliedOptions delete session.reportedOptions.reportedModel + session.modelMutationDispatch = session.dispatchSequence } else { if (session.reportedModelMutation === previous) { session.reportedModelMutation = sequence @@ -76,7 +77,12 @@ function observeClaudeResponseModel( frame: Record ): boolean { const response = readClaudeResponseModel(frame) - if (!response) { + // Why: a turn running at a model pick keeps answering on the old model; it must not unseat the pick. + if ( + !response || + (session.modelMutationDispatch === session.dispatchSequence && + session.reportedModelMutation !== session.optionMutationSequence) + ) { return false } const model = diff --git a/src/main/claude/structured/claude-structured-response-model.test.ts b/src/main/claude/structured/claude-structured-response-model.test.ts index 8d14e17c4bb..e37af00c246 100644 --- a/src/main/claude/structured/claude-structured-response-model.test.ts +++ b/src/main/claude/structured/claude-structured-response-model.test.ts @@ -1,5 +1,10 @@ import { expect, it } from 'vitest' -import { acquired, claudeFrame, fakeClaude } from './claude-structured-session-test-support' +import { + acquired, + claudeFrame, + fakeClaude, + USER_MESSAGE +} from './claude-structured-session-test-support' import type { ClaudeStructuredSessionEvent } from './claude-structured-session-state' it('publishes the main response model after fallback and restoration, ignoring child, foreign and synthetic frames', async () => { @@ -74,6 +79,12 @@ it('retains the init context suffix when the response reports the same bare mode const adapter = await acquired(claude) const connection = claude.connections[0]! try { + await adapter.dispatch({ + sessionId: 'session-1', + clientMessageId: 'first-response', + body: USER_MESSAGE, + fence: 7 + }) claudeFrame(connection, { type: 'system', subtype: 'init', model: 'claude-opus-5-5[1m]' }) claudeFrame(connection, { type: 'assistant', @@ -84,6 +95,12 @@ it('retains the init context suffix when the response reports the same bare mode (await adapter.readOptions({ sessionId: 'session-1', fence: 7 })).current.reportedModel ).toBe('claude-opus-5-5[1m]') await adapter.setOption({ sessionId: 'session-1', fence: 7, key: 'model', value: 'opus' }) + await adapter.dispatch({ + sessionId: 'session-1', + clientMessageId: 'after-context-pick', + body: USER_MESSAGE, + fence: 7 + }) claudeFrame(connection, { type: 'assistant', uuid: 'after-context-pick', @@ -96,3 +113,42 @@ it('retains the init context suffix when the response reports the same bare mode await adapter.closeAll() } }) + +it('keeps a model picked during a turn over that turn’s later replies until a turn runs it', async () => { + const claude = fakeClaude({ initModel: 'claude-sonnet-5-5' }) + const adapter = await acquired(claude) + const connection = claude.connections[0]! + const read = async () => (await adapter.readOptions({ sessionId: 'session-1', fence: 7 })).current + const reply = (uuid: string, model: string) => + claudeFrame(connection, { + type: 'assistant', + uuid, + message: { role: 'assistant', model, content: [] } + }) + try { + await adapter.dispatch({ + sessionId: 'session-1', + clientMessageId: 'running', + body: USER_MESSAGE, + fence: 7 + }) + claudeFrame(connection, { type: 'system', subtype: 'init', model: 'claude-sonnet-5-5' }) + reply('before-pick', 'claude-sonnet-5-5') + await adapter.setOption({ sessionId: 'session-1', fence: 7, key: 'model', value: 'haiku' }) + reply('same-turn', 'claude-sonnet-5-5') + expect(await read()).toMatchObject({ model: 'haiku' }) + expect((await read()).reportedModel).toBeUndefined() + expect(connection.calls).toContainEqual({ subtype: 'set_model', params: { model: 'haiku' } }) + + await adapter.dispatch({ + sessionId: 'session-1', + clientMessageId: 'next', + body: USER_MESSAGE, + fence: 7 + }) + reply('next-turn', 'claude-haiku-5-5') + expect(await read()).toMatchObject({ model: 'haiku', reportedModel: 'claude-haiku-5-5' }) + } finally { + await adapter.closeAll() + } +}) diff --git a/src/main/claude/structured/claude-structured-session-adapter.ts b/src/main/claude/structured/claude-structured-session-adapter.ts index 01c999c9dee..53d4d7a7df8 100644 --- a/src/main/claude/structured/claude-structured-session-adapter.ts +++ b/src/main/claude/structured/claude-structured-session-adapter.ts @@ -30,6 +30,7 @@ import { claudeStoppedRequestEndWait } from '../claude-request-end-wait' import { finishClaudeCloseAfterExit, drainClaudeObservedExits, + liveClaudeSession, observeClaudeSessionExit, settleClaudeUnexpectedExit, type ClaudeExitLifecycle @@ -296,15 +297,6 @@ export class ClaudeStructuredSessionAdapter implements StructuredAgentSessionAda } private session(sessionId: string): ClaudeSession { - const session = this.sessions.get(sessionId) - if (!session) { - // A child that just exited is named by its own diagnostic, not by its absence. - throw ( - this.exits.get(sessionId)?.error ?? - this.settledExitErrors.get(sessionId) ?? - new Error(`no live claude stream-json session for ${sessionId}`) - ) - } - return session + return liveClaudeSession(this.exitLifecycle, sessionId) } } diff --git a/src/main/claude/structured/claude-structured-session-exit-lifecycle.ts b/src/main/claude/structured/claude-structured-session-exit-lifecycle.ts index f8ca501b502..a2e09a80453 100644 --- a/src/main/claude/structured/claude-structured-session-exit-lifecycle.ts +++ b/src/main/claude/structured/claude-structured-session-exit-lifecycle.ts @@ -255,3 +255,19 @@ export function finishClaudeCloseAfterExit(input: { }) ) } + +export function liveClaudeSession( + lifecycle: ClaudeExitLifecycle, + sessionId: string +): ClaudeSession { + const session = lifecycle.sessions.get(sessionId) + if (!session) { + // A child that just exited is named by its own diagnostic, not by its absence. + throw ( + lifecycle.exits.get(sessionId)?.error ?? + lifecycle.settledExitErrors.get(sessionId) ?? + new Error(`no live claude stream-json session for ${sessionId}`) + ) + } + return session +} diff --git a/src/main/claude/structured/claude-structured-session-state.ts b/src/main/claude/structured/claude-structured-session-state.ts index c0fb4d1f620..55959e4900f 100644 --- a/src/main/claude/structured/claude-structured-session-state.ts +++ b/src/main/claude/structured/claude-structured-session-state.ts @@ -135,6 +135,8 @@ export type ClaudeSession = { fastModePerSessionOptIn?: boolean /** Mutation sequence of the last model report; newer writes outrank it. */ reportedModelMutation: number + /** `dispatchSequence` at the last model write; that turn's replies predate the pick. */ + modelMutationDispatch?: number /** Options whose recorded value the provider reported, not merely accepted. */ confirmedOptions: Set /** Inputs carried by launch or accepted controls, independent of provider reports. */ diff --git a/src/main/claude/structured/launch/claude-structured-child-env.test.ts b/src/main/claude/structured/launch/claude-structured-child-env.test.ts index 3eda5aa1cfb..b9c63b68c70 100644 --- a/src/main/claude/structured/launch/claude-structured-child-env.test.ts +++ b/src/main/claude/structured/launch/claude-structured-child-env.test.ts @@ -3,11 +3,7 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, expect, it, vi } from 'vitest' import { runProcess } from '@orca/process-host' -import { - claudeChildEnv, - resolveClaudeChildEnvSources, - withOrcaClaudeEntrypoint -} from './claude-structured-child-env' +import { claudeChildEnv, resolveClaudeChildEnvSources } from './claude-structured-child-env' import { resolveStructuredAgentCommand } from '../../../native-chat/structured-agent-command-resolution' afterEach(() => { @@ -36,92 +32,6 @@ it('resolves a custom Claude command against the login shell PATH', async () => } }) -it('isolates inherited Claude stamps before applying launch settings', () => { - const sources = { - command: 'claude', - prefixArgs: [], - overlay: undefined, - inheritedEnv: { - CLAUDE_CODE_ENTRYPOINT: 'cli', - CLAUDECODE: '1', - CLAUDE_CODE_MESSAGING_SOCKET: 'parent-socket', - CLAUDE_AGENT_SDK_VERSION: 'parent-sdk', - CLAUDE_EFFORT: 'parent-effort', - CLAUDE_CODE_HTTP_PROXY: 'http://user-proxy' - } - } - const options = { runtimeOnPath: false } - expect(claudeChildEnv(sources, undefined, options)).toEqual({ - CLAUDE_CODE_ENTRYPOINT: 'orca', - CLAUDE_CODE_HTTP_PROXY: 'http://user-proxy' - }) - const overlay = { CLAUDE_CODE_ENTRYPOINT: 'custom', CLAUDE_EFFORT: 'configured-effort' } - expect(claudeChildEnv({ ...sources, overlay }, undefined, options)).toEqual({ - ...overlay, - CLAUDE_CODE_HTTP_PROXY: 'http://user-proxy' - }) -}) - -it('keeps a differently cased Windows launch value over the Orca default', () => { - const inherited = { - Claude_Code_Entrypoint: 'cli', - CLAUDE_CODE_MESSAGING_TOKEN: 'parent-token', - Claude_Agent_Sdk_Version: 'parent-sdk', - PATH: 'C:\\bin' - } - expect( - withOrcaClaudeEntrypoint( - inherited, - { claude_code_entrypoint: 'company-client', claude_agent_sdk_version: 'configured-sdk' }, - 'win32' - ) - ).toEqual({ - PATH: 'C:\\bin', - CLAUDE_CODE_ENTRYPOINT: 'company-client', - CLAUDE_AGENT_SDK_VERSION: 'configured-sdk' - }) - expect(withOrcaClaudeEntrypoint(inherited, undefined, 'win32')).toEqual({ - PATH: 'C:\\bin', - CLAUDE_CODE_ENTRYPOINT: 'orca' - }) - expect( - withOrcaClaudeEntrypoint( - inherited, - { - CLAUDE_CODE_ENTRYPOINT: 'first', - claude_code_entrypoint: 'last' - }, - 'win32' - ) - ).toEqual({ PATH: 'C:\\bin', CLAUDE_CODE_ENTRYPOINT: 'last' }) -}) - -it('uses configured Windows values for both Claude lookup and its final child', async () => { - vi.stubGlobal('process', { ...process, platform: 'win32' }) - const sources = await resolveClaudeChildEnvSources({ - resolveInheritedEnv: async () => ({ - PATH: '/inherited/bin', - TOKEN: 'inherited', - CLAUDE_CONFIG_DIR: '/ambient' - }), - resolveEnv: () => ({ - path: '/configured/bin', - tOKEN: 'configured', - claude_config_dir: '/configured/home' - }), - resolveCommand: (options) => { - expect(options?.pathEnv).toBe('/configured/bin') - return { command: '/configured/bin/claude.exe', prefixArgs: [] } - } - }) - expect(claudeChildEnv(sources, undefined, { runtimeOnPath: false })).toEqual({ - PATH: '/configured/bin', - TOKEN: 'configured', - CLAUDE_CONFIG_DIR: '/configured/home', - CLAUDE_CODE_ENTRYPOINT: 'orca' - }) -}) - it.skipIf(process.platform !== 'win32')( 'passes configured values through a real Windows child despite inherited casing', async () => { diff --git a/src/main/daemon/protocol/daemon-create-or-attach-result.ts b/src/main/daemon/protocol/daemon-create-or-attach-result.ts index f6403d313f1..3a98790f081 100644 --- a/src/main/daemon/protocol/daemon-create-or-attach-result.ts +++ b/src/main/daemon/protocol/daemon-create-or-attach-result.ts @@ -1,6 +1,7 @@ import type { TuiAgent } from '../../../shared/agent-launch/catalog/tui-agent' import type { ShellReadyState } from './daemon-rpc-contract' import type { TerminalSnapshot } from './terminal-snapshot' +import type { DaemonLaunchNote } from './daemon-launch-note-protocol' import type { AgentSessionClaimedSpawnResult } from '../../../shared/agent-session/agent-session-host-authority' import type { PtyIncarnationId } from '../../../shared/terminal/pty/lifecycle/pty-incarnation' @@ -11,6 +12,8 @@ export type DaemonCreateOrAttachResult = { shellState: ShellReadyState historySeeded?: boolean launchAgent?: TuiAgent + /** The launch main armed and has not retired, null for none; older daemons omit it. */ + launchNote?: DaemonLaunchNote | null /** Undefined only when talking to a daemon predating WSL session context. */ wslDistro?: string | null agentSessionEnsure?: AgentSessionClaimedSpawnResult @@ -23,15 +26,18 @@ export type DaemonCreateOrAttachResult = { export function getDaemonSessionResultMetadata(session: { launchAgent: TuiAgent | null + launchNote: { current: DaemonLaunchNote | null } historySeeded: boolean | undefined wslDistro: string | null }): { launchAgent?: TuiAgent + launchNote: DaemonLaunchNote | null historySeeded?: boolean wslDistro: string | null } { return { ...(session.launchAgent ? { launchAgent: session.launchAgent } : {}), + launchNote: session.launchNote.current, ...(session.historySeeded !== undefined ? { historySeeded: session.historySeeded } : {}), // Null identifies native sessions; only older daemons omit the field. wslDistro: session.wslDistro diff --git a/src/main/daemon/protocol/daemon-launch-note-protocol.ts b/src/main/daemon/protocol/daemon-launch-note-protocol.ts new file mode 100644 index 00000000000..d00fbb1970d --- /dev/null +++ b/src/main/daemon/protocol/daemon-launch-note-protocol.ts @@ -0,0 +1,62 @@ +import type { SleepingAgentLaunchConfig } from '../../../shared/agent-session/lifecycle/agent-session-resume' + +/** The Orca launch running in a session, kept by the daemon so it outlives a main restart. */ +export type DaemonLaunchNote = { launchToken: string; launchConfig: SleepingAgentLaunchConfig } + +/** Main arms the launch it admits and retires it by token, so a stale retire cannot clear a newer launch. */ +export type DaemonLaunchNoteChange = { arm: DaemonLaunchNote } | { retire: string } + +export type SetLaunchNoteRequest = { + id: string + type: 'setLaunchNote' + payload: { sessionId: string; change: DaemonLaunchNoteChange } +} + +const isString = (value: unknown): value is string => typeof value === 'string' +const isRecord = (value: unknown): value is Record => + typeof value === 'object' && value !== null +const isStringRecord = (value: unknown): value is Record => + isRecord(value) && Object.values(value).every(isString) + +export function parseDaemonLaunchNoteChange(raw: unknown): DaemonLaunchNoteChange | null { + if (!isRecord(raw)) { + return null + } + const { arm, retire } = raw + if (isString(retire)) { + return { retire } + } + const note = parseDaemonLaunchNote(arm) + return note ? { arm: note } : null +} + +function parseDaemonLaunchNote(raw: unknown): DaemonLaunchNote | null { + if (!isRecord(raw)) { + return null + } + const { launchToken, launchConfig: config } = raw + if (!isString(launchToken) || launchToken.length === 0 || launchToken.length > 128) { + return null + } + if (!isRecord(config)) { + return null + } + const { agentArgs, agentEnv, agentCommand, ompResumeFilePath } = config + if ( + !isString(agentArgs) || + !isStringRecord(agentEnv) || + (agentCommand !== undefined && !isString(agentCommand)) || + (ompResumeFilePath !== undefined && !isString(ompResumeFilePath)) + ) { + return null + } + return { + launchToken, + launchConfig: { + agentArgs, + agentEnv: { ...agentEnv }, + ...(agentCommand ? { agentCommand } : {}), + ...(ompResumeFilePath ? { ompResumeFilePath } : {}) + } + } +} diff --git a/src/main/daemon/protocol/daemon-rpc-contract.ts b/src/main/daemon/protocol/daemon-rpc-contract.ts index 3515886b2e6..16d9f117b96 100644 --- a/src/main/daemon/protocol/daemon-rpc-contract.ts +++ b/src/main/daemon/protocol/daemon-rpc-contract.ts @@ -25,6 +25,7 @@ import type { import type * as HistorySeedProtocol from '../history/seed-transfer/terminal-history-seed-transfer-protocol' import type * as PtyOwnerQueryProtocol from './daemon-pty-owner-query-protocol' import type { TerminalSnapshot } from './terminal-snapshot' +import type { SetLaunchNoteRequest } from './daemon-launch-note-protocol' export type SessionState = 'created' | 'spawning' | 'running' | 'exiting' | 'exited' @@ -270,6 +271,7 @@ export type DaemonRequest = | TakePendingOutputRequest | PtyOwnerQueryProtocol.CloseStartupQueryAuthorityRequest | PtyOwnerQueryProtocol.SetColorQueryReplyColorsRequest + | SetLaunchNoteRequest export type RpcResponseOk = { id: string diff --git a/src/main/daemon/providers/adapter/daemon-pty-process-inspection.ts b/src/main/daemon/providers/adapter/daemon-pty-process-inspection.ts index ea457dab4a8..fa6bce3f717 100644 --- a/src/main/daemon/providers/adapter/daemon-pty-process-inspection.ts +++ b/src/main/daemon/providers/adapter/daemon-pty-process-inspection.ts @@ -1,3 +1,4 @@ +import type { DaemonLaunchNoteChange } from '../../protocol/daemon-launch-note-protocol' import { PTY_FOREGROUND_OBSERVATION_PROTOCOL_VERSION, COMPLETION_PROCESS_INSPECTION_PROTOCOL_VERSION, @@ -90,6 +91,11 @@ export abstract class DaemonPtyProcessInspection extends DaemonPtyBufferSnapshot } } + setLaunchNote(id: string, change: DaemonLaunchNoteChange): void { + // Why fire and forget: an older daemon rejects the request and keeps no note, so restore declines. + void this.client.request('setLaunchNote', { sessionId: id, change }).catch(() => undefined) + } + async confirmShellForeground(id: string): Promise { try { const result = await this.client.request<{ confirmed: boolean }>('confirmShellForeground', { diff --git a/src/main/daemon/providers/adapter/daemon-spawn-result-identity.ts b/src/main/daemon/providers/adapter/daemon-spawn-result-identity.ts index 406706d0da9..2a8b1377504 100644 --- a/src/main/daemon/providers/adapter/daemon-spawn-result-identity.ts +++ b/src/main/daemon/providers/adapter/daemon-spawn-result-identity.ts @@ -3,10 +3,11 @@ import type { DaemonCreateOrAttachResult as CreateOrAttachResult } from '../../p export function daemonSpawnResultIdentity( result: CreateOrAttachResult -): Pick { +): Pick { return { ...(result.incarnationId ? { incarnationId: result.incarnationId } : {}), ...(result.agentSessionEnsure ? { agentSessionEnsure: result.agentSessionEnsure } : {}), - ...(result.launchAgent ? { launchAgent: result.launchAgent } : {}) + ...(result.launchAgent ? { launchAgent: result.launchAgent } : {}), + ...(result.launchNote !== undefined ? { launchNote: result.launchNote } : {}) } } diff --git a/src/main/daemon/server/daemon-request-router.ts b/src/main/daemon/server/daemon-request-router.ts index 3955908162e..137bcd04de0 100644 --- a/src/main/daemon/server/daemon-request-router.ts +++ b/src/main/daemon/server/daemon-request-router.ts @@ -1,3 +1,4 @@ +import { parseDaemonLaunchNoteChange } from '../protocol/daemon-launch-note-protocol' import { handleDaemonTerminalKill } from './daemon-terminal-kill' import { handleDaemonFencedTermination } from './daemon-fenced-termination' import { performance } from 'node:perf_hooks' @@ -146,6 +147,13 @@ export class DaemonRequestRouter { return { confirmed: await this.options.host.confirmShellForeground(request.payload.sessionId) } + case 'setLaunchNote': { + const change = parseDaemonLaunchNoteChange(request.payload.change) + if (change) { + this.options.host.setLaunchNote(request.payload.sessionId, change) + } + return {} + } case 'clearScrollback': this.options.host.clearScrollback(request.payload.sessionId) return {} diff --git a/src/main/daemon/server/daemon-terminal-admission.ts b/src/main/daemon/server/daemon-terminal-admission.ts index d304e29ea9c..39812c791a1 100644 --- a/src/main/daemon/server/daemon-terminal-admission.ts +++ b/src/main/daemon/server/daemon-terminal-admission.ts @@ -183,6 +183,7 @@ export class DaemonTerminalAdmission { shellState: result.shellState, incarnationId: result.incarnationId, ...(result.launchAgent ? { launchAgent: result.launchAgent } : {}), + ...(result.launchNote !== undefined ? { launchNote: result.launchNote } : {}), wslDistro: result.wslDistro, ...(result.historySeeded !== undefined ? { historySeeded: result.historySeeded } : {}), ...(result.agentSessionEnsure ? { agentSessionEnsure: result.agentSessionEnsure } : {}), diff --git a/src/main/daemon/sessions/session-launch-note.ts b/src/main/daemon/sessions/session-launch-note.ts new file mode 100644 index 00000000000..c8be96ad8df --- /dev/null +++ b/src/main/daemon/sessions/session-launch-note.ts @@ -0,0 +1,37 @@ +import { + createOsc133CommandFinishedScanner, + type Osc133CommandFinishedScanner +} from '../../../shared/terminal/output/terminal-osc133-command-finished' +import type { + DaemonLaunchNote, + DaemonLaunchNoteChange +} from '../protocol/daemon-launch-note-protocol' + +/** Main's live launch here; retired on command-finished too, since it can finish while main is down. */ +export class SessionLaunchNote { + current: DaemonLaunchNote | null = null + private retirement: Osc133CommandFinishedScanner | null = null + + scan(data: string): void { + this.retirement?.scan(data) + } + + change(change: DaemonLaunchNoteChange): void { + const token = this.current?.launchToken + if ('retire' in change) { + if (change.retire === token) { + this.current = null + this.retirement = null + } + return + } + // Re-arming the same launch keeps the scanner, whose carry may hold a split marker. + const note = change.arm + if (note.launchToken !== token) { + this.current = note + this.retirement = createOsc133CommandFinishedScanner(() => + this.change({ retire: note.launchToken }) + ) + } + } +} diff --git a/src/main/daemon/sessions/session-process-inspection.ts b/src/main/daemon/sessions/session-process-inspection.ts index 513e282b496..8460a1540cf 100644 --- a/src/main/daemon/sessions/session-process-inspection.ts +++ b/src/main/daemon/sessions/session-process-inspection.ts @@ -1,8 +1,18 @@ import type { ChildProcessVerdictRead, SubprocessHandle } from './session-subprocess-handle' import type { TerminalForegroundSample } from '../../../shared/terminal/pty/lifecycle/terminal-foreground-observation' +import { SessionLaunchNote } from './session-launch-note' export class SessionProcessInspection { - constructor(protected readonly subprocess: SubprocessHandle) {} + /** Main's launch note for this process; Session feeds it the output that can retire it. */ + readonly launchNote = new SessionLaunchNote() + + readonly processNameIsSpawnFile: boolean + readonly shellIsRootChild: boolean + + constructor(protected readonly subprocess: SubprocessHandle) { + this.processNameIsSpawnFile = subprocess.processNameIsSpawnFile === true + this.shellIsRootChild = subprocess.shellIsRootChild === true + } inspectChildProcesses(fresh?: boolean, ignoreDetached?: boolean): ChildProcessVerdictRead { return this.subprocess.inspectChildProcesses?.(fresh, ignoreDetached) ?? 'unverifiable' diff --git a/src/main/daemon/sessions/session.ts b/src/main/daemon/sessions/session.ts index 0463d39dcca..38c89f0a845 100644 --- a/src/main/daemon/sessions/session.ts +++ b/src/main/daemon/sessions/session.ts @@ -28,8 +28,6 @@ export class Session extends SessionProcessInspection { readonly terminalHandle: string | null readonly launchAgent: TuiAgent | null readonly wslDistro: string | null - readonly processNameIsSpawnFile: boolean - readonly shellIsRootChild: boolean private _state: SessionState = 'running' private _exitCode: number | null = null exitCause: TerminalExitCause | undefined @@ -51,8 +49,6 @@ export class Session extends SessionProcessInspection { this.terminalHandle = opts.terminalHandle ?? null this.launchAgent = opts.launchAgent ?? null this.wslDistro = opts.wslDistro ?? null - this.processNameIsSpawnFile = opts.subprocess.processNameIsSpawnFile === true - this.shellIsRootChild = opts.subprocess.shellIsRootChild === true this.onSessionExit = opts.onExit const isAlive = () => !this._disposed && this._state !== 'exited' const pipeline = createSessionOutputPipeline(opts, isAlive) @@ -80,6 +76,7 @@ export class Session extends SessionProcessInspection { try { this.subprocess.onData((data) => { if (!this._disposed) { + this.launchNote.scan(data) this.shellReady.ingestSubprocessData(data) } }) diff --git a/src/main/daemon/terminal-host/terminal-host-create-contract.ts b/src/main/daemon/terminal-host/terminal-host-create-contract.ts index 55bb45c6697..c90d5109dc4 100644 --- a/src/main/daemon/terminal-host/terminal-host-create-contract.ts +++ b/src/main/daemon/terminal-host/terminal-host-create-contract.ts @@ -1,3 +1,4 @@ +import type { DaemonLaunchNote } from '../protocol/daemon-launch-note-protocol' import type { StartupCommandDelivery } from '../../../shared/agent-launch/startup/codex-startup-delivery' import type { TuiAgent } from '../../../shared/agent-launch/catalog/tui-agent' import type { ShellReadyState } from '../protocol/daemon-rpc-contract' @@ -52,6 +53,7 @@ export type CreateOrAttachResult = { shellState: ShellReadyState historySeeded?: boolean launchAgent?: TuiAgent + launchNote?: DaemonLaunchNote | null wslDistro: string | null attachToken: symbol incarnationId: PtyIncarnationId diff --git a/src/main/daemon/terminal-host/terminal-host.ts b/src/main/daemon/terminal-host/terminal-host.ts index 6e4934f514f..970d51c97fc 100644 --- a/src/main/daemon/terminal-host/terminal-host.ts +++ b/src/main/daemon/terminal-host/terminal-host.ts @@ -1,3 +1,4 @@ +import type { DaemonLaunchNoteChange } from '../protocol/daemon-launch-note-protocol' import { terminalSessionExitCode } from './terminal-host-returned-process' import { runTerminalCreation, type PendingTerminalCreation } from './terminal-creation-work' import { beginTerminalHostFencedTermination } from './terminal-host-fenced-termination' @@ -261,6 +262,10 @@ export class TerminalHost { ) } + setLaunchNote(sessionId: string, change: DaemonLaunchNoteChange): void { + this.sessions.get(sessionId)?.launchNote.change(change) + } + clearScrollback(sessionId: string): void { getAliveTerminalHostSession(this.sessions, sessionId).clearScrollback() } diff --git a/src/main/ipc/agent-status-ipc-boundary.ts b/src/main/ipc/agent-status-ipc-boundary.ts index ed1528e6aff..6c314baf534 100644 --- a/src/main/ipc/agent-status-ipc-boundary.ts +++ b/src/main/ipc/agent-status-ipc-boundary.ts @@ -12,7 +12,8 @@ export type AgentStatusRuntimeEnrichment = Pick< | 'getAgentStatusTerminalHandleForPaneKey' | 'getAgentStatusOrchestrationContextForPaneKey' | 'getTerminalProcessIncarnation' -> +> & + Partial> const MAX_AGENT_STATUS_DROP_TAB_ID_LENGTH = 160 @@ -94,9 +95,11 @@ export function enrichAgentStatusIpcPayload( } const terminalHandle = runtime.getAgentStatusTerminalHandleForPaneKey(data.paneKey) const orchestration = runtime.getAgentStatusOrchestrationContextForPaneKey(data.paneKey) + const launchConfig = runtime.getLaunchStatementForPaneKey?.(data.paneKey, data.agentType) return { ...data, ...(terminalHandle ? { terminalHandle } : {}), + ...(launchConfig !== undefined ? { launchConfig } : {}), ...(orchestration ? { orchestration } : {}) } } diff --git a/src/main/ipc/pty/ipc/spawn-commit.ts b/src/main/ipc/pty/ipc/spawn-commit.ts index 0c9885cc9b7..c6452b61af3 100644 --- a/src/main/ipc/pty/ipc/spawn-commit.ts +++ b/src/main/ipc/pty/ipc/spawn-commit.ts @@ -42,7 +42,8 @@ export async function commitPtyIpcSpawn(ctx: PtyIpcSpawnState): Promise inspectProcessFromRuntimeController(ptyId, options), confirmForegroundProcess: (ptyId) => confirmForegroundProcessFromRuntimeController(ptyId), confirmShellForeground: (ptyId) => confirmShellForegroundFromRuntimeController(ptyId), + // Why local only: the daemon that outlives main is this host's. + setLaunchNote: (ptyId, change) => { + if (hasPtyProviderForInspection(ptyId)) { + getProviderForPty(ptyId).setLaunchNote?.(ptyId, change) + } + }, getCwd: (ptyId) => getCwdFromRuntimeController(ptyId), hasChildProcesses: (ptyId) => hasChildProcessesFromRuntimeController(ptyId), clearBuffer: (ptyId) => clearBufferFromRuntimeController(deps, ptyId), diff --git a/src/main/ipc/pty/runtime/spawn-commit.ts b/src/main/ipc/pty/runtime/spawn-commit.ts index 77166e31909..0af230216e9 100644 --- a/src/main/ipc/pty/runtime/spawn-commit.ts +++ b/src/main/ipc/pty/runtime/spawn-commit.ts @@ -32,7 +32,9 @@ export async function commitRuntimePtySpawn(ctx: RuntimePtySpawnState) { worktreeId: args.worktreeId, shellOverride: ctx.daemonShellOverride, rendererLaunchAuthority: null, - launchedAgent: null + launchedAgent: null, + // The runtime's own create records its launch once the spawn commits. + freshSpawn: false }, preAllocatedHandle: args.preAllocatedHandle, // Runtime spawns bypass renderer IPC; publish reverse pane lookup for hooks and cleanup. diff --git a/src/main/ipc/pty/spawn-commit-assembly.ts b/src/main/ipc/pty/spawn-commit-assembly.ts index d2048c4b8ab..09df9060ebe 100644 --- a/src/main/ipc/pty/spawn-commit-assembly.ts +++ b/src/main/ipc/pty/spawn-commit-assembly.ts @@ -19,7 +19,11 @@ import { } from './host-env/codex-home' import { commitAttachedPtySize } from './delivery/attached-pty-size' import { seedTerminalRestoreRecordsFromSpawnResult } from './pane/agent-session-owners' -import { admitProviderReattachLaunchIdentity } from './pane/launch-authority' +import { + admitProviderReattachLaunchIdentity, + reattachLaunchBinding, + type admitRendererAgentLaunchAuthority +} from './pane/launch-authority' import type { ReplacedPaneOwner } from './pane/replaced-pane-owner' import { admitPtySpawnOwnership, registerPersistedPtySpawn } from './pane/spawn-registration' import type { PtySpawnOutcome } from './spawn-outcome' @@ -90,9 +94,11 @@ export type PtySpawnCommitInput = { store: Store | undefined worktreeId: string | null | undefined shellOverride: string | undefined - rendererLaunchAuthority: { launchToken: string; launchAgent: TuiAgent } | null + rendererLaunchAuthority: ReturnType /** The agent a fresh spawn's launch command starts; null for reattaches, adoptions and shells. */ launchedAgent: TuiAgent | null + /** Neither a reattach nor an adoption of a live pane's process. */ + freshSpawn: boolean } preAllocatedHandle: string | null | undefined spawnTiming?: PtySpawnTiming @@ -162,7 +168,9 @@ export async function commitPtySpawn(input: PtySpawnCommitInput): Promise ({ - handlers: new Map unknown>() -})) -vi.mock('electron', () => ({ - ipcMain: { - handle: (channel: string, handler: (event: unknown, args: unknown) => unknown) => - handlers.set(channel, handler) - } -})) -vi.mock('../../persistence/moving-wsl-projects/wsl-move-scope-guard', () => ({ - assertWslMoveProjectWritable: vi.fn() -})) describe('sparse preset validation transport', () => { - it('identifies both absolute and parent-segment directory failures by type', () => { - expect(() => normalizeSparseDirectories(['/outside'])).toThrow(InvalidSparseDirectoryError) - expect(() => normalizeSparseDirectories(['../outside'])).toThrow(InvalidSparseDirectoryError) - }) - - it.each([ - { name: '', directories: ['src'], code: 'name_required' }, - { name: 'a'.repeat(81), directories: ['src'], code: 'name_too_long' }, - { name: 'Preset', directories: [], code: 'directories_required' }, - { name: 'Preset', directories: ['../outside'], code: 'directories_invalid' }, - { name: 'Preset', directories: ['/outside'], code: 'directories_invalid' } - ])( - 'returns IPC code $code instead of saving an invalid preset', - ({ name, directories, code }) => { - const saveSparsePreset = vi.fn() - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: validation only reads these store methods. - const store = { getRepo: () => ({}), saveSparsePreset } as unknown as Store - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: invalid input returns before the window is accessed. - registerSparsePresetHandlers({} as BrowserWindow, store) - const result = handlers.get('sparsePresets:save')?.(undefined, { - repoId: 'repo', - name, - directories - }) - expect(result).toMatchObject({ errorCode: code }) - expect(saveSparsePreset).not.toHaveBeenCalled() - } - ) - it('keeps the validation reason in the RPC envelope', () => { let failure: unknown try { diff --git a/src/main/native-chat/agent-model-catalog/agent-model-catalog-lister.ts b/src/main/native-chat/agent-model-catalog/agent-model-catalog-lister.ts index c6f1e3196cd..6aebf3e875e 100644 --- a/src/main/native-chat/agent-model-catalog/agent-model-catalog-lister.ts +++ b/src/main/native-chat/agent-model-catalog/agent-model-catalog-lister.ts @@ -31,17 +31,28 @@ export type AgentModelCatalogListModels = ( owner: StructuredAgentResolutionOperation ) => Promise -/** The refresh that starts a listing gives it its budget; joining never extends or shortens it. */ +/** Covers the gap between one waiting read and the next, so a following reader keeps its place. */ +const AGENT_MODEL_CATALOG_RELEASE_GRACE_MS = 5_000 + +/** Queued listings need a holder; admitted probes run to their own timeout. */ export type AgentModelCatalogListingBudget = { /** Expiry withdraws it from admission, stops its process and drops its late answer. */ deadline?: number + /** This caller's interest; a queued listing is withdrawn a grace after no holder is left. */ signal?: AbortSignal + /** Service shutdown retires the listing immediately, without the holder release grace. */ + lifetime?: AbortSignal + admission?: PrioritySemaphore + /** Execution time after admission. */ + timeoutMs?: number } export type InFlightListing = { agent: string targets: Set current: () => boolean + /** Adds a queued holder until its signal aborts; admitted probes need none. */ + hold: (signal: AbortSignal | undefined) => void promise: Promise finish: (entry: AgentModelCatalogEntry | null) => void cleanup: () => void @@ -93,47 +104,91 @@ export function refreshAgentModelCatalogListing(input: { ) => AgentModelCatalogEntry | null }): Promise { const { fingerprint, agent, listModels } = input - const { deadline, signal } = input.budget ?? {} + const { signal, lifetime, admission, timeoutMs, deadline: budgetDeadline } = input.budget ?? {} + let deadline = budgetDeadline const existing = input.refreshes.get(fingerprint) if (existing) { + existing.hold(signal) demandAgentModelEfforts(existing, input.selectedModel, input.maxTargets) return existing.promise } const result = Promise.withResolvers() - // Ending the flight (its budget, the account or CLI retired, shutdown) stops its process too. const stopped = new AbortController() - const expiry = - deadline === undefined - ? undefined - : setTimeout(() => retire(), Math.max(0, deadline - Date.now())) + let expiry: ReturnType | undefined + const holders = new Map void>() + // A listing started without a signal is held until it settles. + let held = signal === undefined + const setDeadline = (next: number | undefined): void => { + deadline = next + clearTimeout(expiry) + if (deadline !== undefined) { + expiry = setTimeout(retire, Math.max(0, deadline - Date.now())) + } + } const flight: InFlightListing = { agent, targets: new Set(input.selectedModel ? [input.selectedModel] : []), - current: () => !signal?.aborted && (deadline === undefined || Date.now() < deadline), + current: () => !lifetime?.aborted && (deadline === undefined || Date.now() < deadline), + hold: (holder) => { + if (!held && holder && !holder.aborted && !holders.has(holder)) { + setDeadline(budgetDeadline) + const abandon = (): void => { + holders.delete(holder) + if (!held && holders.size === 0) { + setDeadline( + Math.min( + budgetDeadline ?? Infinity, + Date.now() + AGENT_MODEL_CATALOG_RELEASE_GRACE_MS + ) + ) + } + } + holders.set(holder, abandon) + holder.addEventListener('abort', abandon, { once: true }) + } + }, promise: result.promise, finish: result.resolve, cleanup: () => { clearTimeout(expiry) - signal?.removeEventListener('abort', retire) + lifetime?.removeEventListener('abort', retire) + for (const [holder, abandon] of holders) { + holder.removeEventListener('abort', abandon) + } stopped.abort() } } const retire = (): void => retireAgentModelCatalogListing(input.refreshes, input.listingWaiters, fingerprint, flight) const current = (): boolean => input.refreshes.get(fingerprint) === flight && flight.current() - const owner: StructuredAgentResolutionOperation = { current, signal: stopped.signal, deadline } input.refreshes.set(fingerprint, flight) - signal?.addEventListener('abort', retire, { once: true }) - if (!flight.current()) { + setDeadline(budgetDeadline) + flight.hold(signal) + lifetime?.addEventListener('abort', retire, { once: true }) + if (signal?.aborted || !flight.current()) { retire() } const span = startSpan('agentModelCatalog.discovery', { attributes: { agent, lister: 'probe' } }) void Promise.resolve().then(async () => { + let release: (() => void) | undefined try { + release = await admission?.acquire(0, stopped.signal) if (!current()) { span.end() return } + held = true + // Admission replaces the abandonment deadline with the probe's own execution budget. + setDeadline( + timeoutMs === undefined + ? budgetDeadline + : Math.min(budgetDeadline ?? Infinity, Date.now() + timeoutMs) + ) + const owner: StructuredAgentResolutionOperation = { + current, + signal: stopped.signal, + deadline + } let targets = input.selectedModel ? [input.selectedModel] : undefined let entry: AgentModelCatalogEntry | null = null do { @@ -165,6 +220,7 @@ export function refreshAgentModelCatalogListing(input: { } } finally { retire() + release?.() } }) return flight.promise @@ -172,20 +228,17 @@ export function refreshAgentModelCatalogListing(input: { export function scopedAgentModelCatalogLister(input: { legacy?: boolean - admission: PrioritySemaphore signal: AbortSignal resolved: AgentModelCatalogResolvedProbe | undefined probe: AgentModelCatalogProbe | undefined probeHome: AgentSessionAccountHome | null workspacePath: string | undefined }): AgentModelCatalogListModels { - const { legacy, admission, resolved, probe, probeHome, workspacePath } = input + const { legacy, resolved, probe, probeHome, workspacePath } = input return async (selectedModels, operation) => { const signal = operation.signal ? AbortSignal.any([input.signal, operation.signal]) : input.signal - // Waiting for a slot spends the listing's own budget; expiry withdraws it from the queue. - const release = legacy ? () => {} : await admission.acquire(0, signal) const deadline = new AbortController() // Share the retry budget and await bounded provider teardown before settling. const timer = legacy ? setTimeout(() => deadline.abort(), 30_000) : undefined @@ -211,8 +264,6 @@ export function scopedAgentModelCatalogLister(input: { throw signal.aborted ? new AgentModelCatalogListingStoppedError(String(error)) : error } finally { clearTimeout(timer) - // The slot is held until the provider's teardown finishes. - release() } } } diff --git a/src/main/native-chat/agent-model-catalog/agent-model-catalog-operation.ts b/src/main/native-chat/agent-model-catalog/agent-model-catalog-operation.ts index 0e8d5646f5b..897d9427738 100644 --- a/src/main/native-chat/agent-model-catalog/agent-model-catalog-operation.ts +++ b/src/main/native-chat/agent-model-catalog/agent-model-catalog-operation.ts @@ -12,7 +12,7 @@ export type AgentModelCatalogReadOperation = { deadline?: number retirement: number signal: AbortSignal - current: () => boolean + current: (options?: { ignoreDeadline: true }) => boolean controller: AbortController } @@ -35,9 +35,9 @@ export function captureAgentModelCatalogRead( retirement, signal, controller, - current: () => + current: (options) => !signal.aborted && - (deadline === undefined || Date.now() < deadline) && + (options?.ignoreDeadline || deadline === undefined || Date.now() < deadline) && deps.store.retirementRevision(params.agent) === retirement && agentModelCatalogRequestKey(deps, original) === identity } diff --git a/src/main/native-chat/agent-model-catalog/agent-model-catalog-refresh.ts b/src/main/native-chat/agent-model-catalog/agent-model-catalog-refresh.ts index 4f8d39458ad..202174b0d1b 100644 --- a/src/main/native-chat/agent-model-catalog/agent-model-catalog-refresh.ts +++ b/src/main/native-chat/agent-model-catalog/agent-model-catalog-refresh.ts @@ -16,6 +16,7 @@ import { import { resolveAgentModelCatalogScope } from './agent-model-catalog-scope' import { scopedAgentModelCatalogLister } from './agent-model-catalog-lister' import { seedAccountList } from './agent-model-catalog-account-list' +import type { AgentModelCatalogEntry } from './agent-model-catalog-entry' import { agentModelCatalogReleasedOwner, bindAgentModelCatalogScope, @@ -23,7 +24,7 @@ import { } from './agent-model-catalog-snapshot' import { AGENT_MODEL_CATALOG_PICKER_WAIT_MS } from './agent-model-catalog-listing-waiters' -/** One budget for a whole refresh: setup, the folder's settings, admission and the listing. */ +/** Bounds scope resolution and the folder's settings; listings own their execution budget. */ const AGENT_MODEL_CATALOG_REFRESH_MS = AGENT_MODEL_CATALOG_PICKER_WAIT_MS /** What a refresh resolves, captured when it starts; no reader's session, deadline or callback. */ @@ -54,7 +55,7 @@ export type AgentModelCatalogRefresh = { type Held = AgentModelCatalogRefresh & { workspaceId?: string; end: () => void } /** Equivalent requests share one refresh that belongs to no reader. It ends when it finishes, at - * its fixed budget, on its agent's or workspace's retirement, or with the service; readers only + * its setup budget, on its agent's or workspace's retirement, or with the service; readers only * wait on it, each until its own deadline or cancellation, and never extend or end it. */ export function createAgentModelCatalogRefreshes(context: { deps: AgentModelCatalogServiceDeps @@ -155,6 +156,7 @@ export function createAgentModelCatalogRefreshes(context: { Object.assign(bound, { fingerprint, exact, cwd }) bindAgentModelCatalogScope(bindings, launch.bindingKey, bound, requestKey) const probe = deps.probes?.[agent] + let completion: Promise | undefined if ( !launch.passive && (exact || !agentModelCatalogReleasedOwner(scoped)) && @@ -163,7 +165,6 @@ export function createAgentModelCatalogRefreshes(context: { ) { const lister = scopedAgentModelCatalogLister({ legacy: launch.legacy, - admission, signal: lifetime, resolved, probe, @@ -177,8 +178,8 @@ export function createAgentModelCatalogRefreshes(context: { deps.store.efforts.due(fingerprint, model) || deps.store.isListing(fingerprint)) ) { - // A listing another refresh started keeps that refresh's budget. - void deps.store.refresh( + // This refresh holds the listing while it runs; once admitted, the listing runs on. + completion = deps.store.refresh( fingerprint, agent, (selected, owner) => { @@ -187,7 +188,13 @@ export function createAgentModelCatalogRefreshes(context: { void task.finally(() => tasks.delete(task)).catch(() => {}) return task }, - { deadline, signal: lifetime }, + { + signal, + lifetime, + ...(launch.legacy + ? { deadline } + : { admission, timeoutMs: AGENT_MODEL_CATALOG_REFRESH_MS }) + }, model ) } @@ -195,16 +202,18 @@ export function createAgentModelCatalogRefreshes(context: { for (const model of targets.size > 0 ? [...targets] : [undefined]) { list(model) } - listing.resolve() - await bounded( - deps.store.pendingListing(fingerprint, true, true, signal) ?? Promise.resolve(null) - ) } listing.resolve() if (launch.passive) { await bound.projecting - } else if (current()) { - seedAccountList(deps, scope, agent) + } else { + // Discovery may finish after this refresh's setup budget ends. + const seeded = Promise.resolve(completion).then((entry) => { + if (entry !== null && bound.current()) { + seedAccountList(deps, scope, agent) + } + }) + await bounded(seeded) } } const end = (): void => ended.abort() diff --git a/src/main/native-chat/agent-model-catalog/agent-model-catalog-service.ts b/src/main/native-chat/agent-model-catalog/agent-model-catalog-service.ts index 35b005e03a7..71d60ff9a0d 100644 --- a/src/main/native-chat/agent-model-catalog/agent-model-catalog-service.ts +++ b/src/main/native-chat/agent-model-catalog/agent-model-catalog-service.ts @@ -112,13 +112,26 @@ export function createAgentModelCatalogService( binding: () => bindings.get(requestKey(params)) }) } - const result = snapshot(params) - // A waiting read that used its whole budget is final; work still running doesn't reopen it. - const { listingInProgress: _, ...settled } = result - const answer = waits && Date.now() >= deadline ? settled : result - return lifetime.signal.aborted - ? answer - : readAccountList(deps, params, answer, () => memoryBinding({ agent: params.agent })?.base) + const expired = waits && Date.now() >= deadline + // Expiry ends the wait, but a still-current scope can describe the listing left behind. + const answerParams = + expired && params.operation?.current({ ignoreDeadline: true }) + ? { ...params, operation: undefined } + : params + const result = snapshot(answerParams) + const answer = lifetime.signal.aborted + ? result + : readAccountList( + deps, + answerParams, + result, + () => memoryBinding({ agent: params.agent })?.base + ) + if (!expired || !answer.listingInProgress) { + return answer + } + // Expiring a reader cannot settle the listing, including for clients predating waitExpired. + return { ...answer, waitExpired: true } }, recordLiveListing: live.recordLiveListing, async prewarm() { diff --git a/src/main/native-chat/agent-model-catalog/agent-model-catalog-shared-listing.test.ts b/src/main/native-chat/agent-model-catalog/agent-model-catalog-shared-listing.test.ts index a0f0e749d2c..4b72c349a15 100644 --- a/src/main/native-chat/agent-model-catalog/agent-model-catalog-shared-listing.test.ts +++ b/src/main/native-chat/agent-model-catalog/agent-model-catalog-shared-listing.test.ts @@ -6,10 +6,7 @@ import { } from '../structured-agent-command-resolution' import { AgentModelCatalogStore, type AgentModelCatalogSuccess } from './agent-model-catalog-store' import type { AgentSessionAccountHome } from '../../../shared/agent-session/lifecycle/agent-session-account-home' -import { - joinHostModelListingWait, - isHostModelListingWaitInFlight -} from '../../../shared/host-model-listing-waits' +import { agentModelCatalogFingerprint } from './agent-model-catalog-fingerprint' function listing(id: string): AgentModelCatalogSuccess { return { @@ -98,7 +95,6 @@ it('finishes a shared listing for a folder that joined before the first reader g vi.useRealTimers() }) const h = sharedListingHarness() - const started = Date.now() void h.service.demand({ agent: 'codex', workspacePath: '/a', deadline: Date.now() + 1_000 }) await vi.waitFor(() => expect(h.run).toHaveBeenCalledOnce()) const b = h.service.read({ @@ -110,8 +106,6 @@ it('finishes a shared listing for a folder that joined before the first reader g await vi.advanceTimersByTimeAsync(2_000) // The first reader is gone, but the listing it started is still running for the folder that joined. expect(h.runs[0]?.signal?.aborted).toBe(false) - // Its requests carry the refresh's own budget, not the first reader's deadline. - expect(h.runs[0]?.deadline).toBe(started + 30_000) h.discovery.resolve({ ...listing('shared'), complete: true, configured: {} }) await vi.advanceTimersByTimeAsync(0) expect(await b).toMatchObject({ models: [{ id: 'shared' }] }) @@ -138,51 +132,79 @@ it.each(['retired', 'stopped'] as const)( } ) -it('never starts a queued shared listing once its budget has run out, nor a replacement', async () => { +it('keeps a queued listing only while a refresh or reader still waits on it', async () => { vi.useFakeTimers() onTestFinished(() => { vi.useRealTimers() }) const h = sharedListingHarness() - // Two other CLIs hold both discovery slots past every budget below. + // Two other CLIs hold both discovery slots. for (const busy of ['/busy-1', '/busy-2']) { void h.service.demand({ agent: 'codex', workspacePath: busy }) } await vi.waitFor(() => expect(h.run).toHaveBeenCalledTimes(2)) - void h.service.demand({ agent: 'codex', workspacePath: '/a', deadline: Date.now() + 1_000 }) - void h.service.demand({ agent: 'codex', workspacePath: '/b', deadline: Date.now() + 2_000 }) - await vi.advanceTimersByTimeAsync(31_000) + void h.service.demand({ agent: 'codex', workspacePath: '/own-dropped' }) + void h.service.demand({ agent: 'codex', workspacePath: '/own-waited' }) + await vi.advanceTimersByTimeAsync(25_000) + void h.service.read({ agent: 'codex', workspacePath: '/own-waited', waitForListing: true }) + await vi.advanceTimersByTimeAsync(15_000) h.listingFor('/busy-1').resolve(listing('busy')) h.listingFor('/busy-2').resolve(listing('busy')) await vi.advanceTimersByTimeAsync(60_000) + expect(h.runs.map((entry) => entry.identity)).toEqual(['/busy-1', '/busy-2', '/own-waited']) +}) + +it('retires queued discovery before shutdown releases an admission slot', async () => { + vi.useFakeTimers() + onTestFinished(() => { + vi.useRealTimers() + }) + const h = sharedListingHarness() + for (const workspacePath of ['/busy-1', '/busy-2', '/own-queued']) { + void h.service.demand({ agent: 'codex', workspacePath }) + } + await vi.advanceTimersByTimeAsync(0) + expect(h.service.snapshot({ agent: 'codex', workspacePath: '/own-queued' })).toMatchObject({ + listingInProgress: true + }) + + const stopped = h.service.stop() + h.listingFor('/busy-1').resolve(listing('busy')) + h.listingFor('/busy-2').resolve(listing('busy')) + await vi.advanceTimersByTimeAsync(0) + await stopped + expect(h.runs.map((entry) => entry.identity)).toEqual(['/busy-1', '/busy-2']) }) -it.each([5, 50])( - 'runs two of %i folders with their own CLIs at a time, starts none after its budget, and leaves nothing behind', - async (folders) => { - vi.useFakeTimers() - onTestFinished(() => { - vi.useRealTimers() - }) - const h = sharedListingHarness() - for (let index = 0; index < folders; index++) { - void h.service.demand({ agent: 'codex', workspacePath: `/own-${index}` }) - } - await vi.advanceTimersByTimeAsync(0) - expect(h.run).toHaveBeenCalledTimes(2) - await vi.advanceTimersByTimeAsync(30_000) - expect(h.runs.every((entry) => entry.signal?.aborted)).toBe(true) - // No reader asked again, so nothing replaces the expired work. - await vi.advanceTimersByTimeAsync(60_000) - expect(h.run).toHaveBeenCalledTimes(2) - for (let index = 0; index < folders; index++) { - expect( - h.service.snapshot({ agent: 'codex', workspacePath: `/own-${index}` }) - ).not.toHaveProperty('listingInProgress') - } +it('reports an expired captured wait and lets the next reader finish its queued listing', async () => { + vi.useFakeTimers() + onTestFinished(() => { + vi.useRealTimers() + }) + const h = sharedListingHarness() + for (const workspacePath of ['/busy-1', '/busy-2']) { + void h.service.demand({ agent: 'codex', workspacePath }) } -) + await vi.advanceTimersByTimeAsync(0) + const params = { agent: 'codex', workspacePath: '/own-waited', waitForListing: true } + const operation = h.service.captureRead(params) + const request = { ...params, operation, deadline: operation.deadline } + const first = h.service.read(request) + await vi.advanceTimersByTimeAsync(30_000) + expect(await first).toEqual({ origin: 'unknown', listingInProgress: true, waitExpired: true }) + + operation.controller.abort() + expect(await h.service.read(request)).toEqual({ origin: 'unknown' }) + + const next = h.service.read(params) + await vi.advanceTimersByTimeAsync(0) + h.listingFor('/busy-1').resolve(listing('busy')) + h.listingFor('/own-waited').resolve(listing('fresh')) + await vi.advanceTimersByTimeAsync(0) + expect(await next).toMatchObject({ origin: 'probe', models: [{ id: 'fresh' }] }) + expect(await next).not.toHaveProperty('waitExpired') +}) it("never answers a newly picked folder with the previous folder's late listing", async () => { const h = sharedListingHarness() @@ -326,98 +348,88 @@ it('settles each reader at its own deadline or cancellation, and stops the listi expect(h.service.snapshot({ agent: 'codex', workspacePath: '/a' }).origin).toBe('unknown') }) -it.each(['cold', 'warm'] as const)( - 'ends queued %s discovery within its original deadline and preserves existing observations', - async (mode) => { - vi.useFakeTimers() - const store = new AgentModelCatalogStore() - let failing = false - const run = vi.fn( - ( - _home: AgentSessionAccountHome, - options?: { signal?: AbortSignal; workspacePath?: string } - ) => - new Promise((resolve, reject) => { - const timer = setTimeout( - () => - failing - ? reject(new Error('offline')) - : resolve({ - ...listing(options?.workspacePath ?? 'listed'), - complete: true, - configured: {} - }), - 12_000 - ) - options?.signal?.addEventListener( - 'abort', - () => { - clearTimeout(timer) - reject(new Error('stopped')) - }, - { once: true } - ) - }) - ) - const service = createAgentModelCatalogService({ - store, - getConversation: () => undefined, - getRecord: () => undefined, - drivesRecord: () => true, - resolveAccountHome: async () => ({ variable: 'CODEX_HOME', path: '/account' }), - // Each folder resolves its own CLI (a project-local install), so each lists separately. - probes: { - codex: Object.assign(run, { - resolve: async ( - home: AgentSessionAccountHome, - _cwd: string, - _lookup?: unknown, - _operation?: unknown, - launchDirectory?: string - ) => ({ - identity: launchDirectory, - run: (signal?: AbortSignal) => run(home, { signal, workspacePath: launchDirectory }) - }) - }) - } - }) - const read = (path: string, waitForListing = false) => - waitForListing - ? service.read({ agent: 'codex', workspacePath: path, waitForListing }) - : discover(service, { agent: 'codex', workspacePath: path }) - const replies: unknown[] = [] - let leave = () => {} - try { - if (mode === 'warm') { - const initial = read('/5', true) - await vi.advanceTimersByTimeAsync(12_000) - await initial - await vi.advanceTimersByTimeAsync(120_001) - } - failing = false - await Promise.all(['/1', '/2', '/3', '/4', '/5'].map((path) => read(path))) - const follow = vi.fn(() => read('/5', true)) - const owner = { close() {} } - leave = joinHostModelListingWait('queued', follow, (reply) => replies.push(reply), owner) - const second = joinHostModelListingWait('queued', follow, () => {}, owner) - await vi.advanceTimersByTimeAsync(30_000) - expect(replies[0]).not.toHaveProperty('listingInProgress') - expect(follow).toHaveBeenCalledTimes(1) - - await vi.advanceTimersByTimeAsync(7_000) - expect(replies.at(-1)).not.toHaveProperty('listingInProgress') - expect(isHostModelListingWaitInFlight('queued', owner)).toBe(false) - if (mode === 'warm') { - expect(replies.at(-1)).toMatchObject({ models: [{ id: '/5' }] }) - } else { - expect(replies.at(-1)).toEqual({ origin: 'unknown' }) - } - expect(run).toHaveBeenCalledTimes(mode === 'warm' ? 6 : 5) - second() - } finally { - leave() - service.stop() - vi.useRealTimers() +it('publishes cached catalogs immediately and completes queued discoveries after admission', async () => { + vi.useFakeTimers() + const store = new AgentModelCatalogStore() + let warming = true + const probe = + (agent: string) => (_home: AgentSessionAccountHome, options?: { signal?: AbortSignal }) => + new Promise((resolve, reject) => { + // ast-grep-ignore: test-no-real-time-wait -- Fake timers simulate probe execution after queued admission. + const timer = setTimeout( + () => + resolve({ + ...listing(warming ? 'saved' : `${agent}-fresh`), + complete: true, + configured: {} + }), + agent === 'pi' || agent === 'gemini' ? 20_000 : 15_000 + ) + options?.signal?.addEventListener( + 'abort', + () => { + clearTimeout(timer) + reject(new Error('stopped')) + }, + { once: true } + ) + }) + const service = createAgentModelCatalogService({ + store, + getConversation: () => undefined, + getRecord: () => undefined, + drivesRecord: () => true, + resolveAccountHome: async () => ({ variable: 'CODEX_HOME', path: '/account' }), + probes: { + pi: probe('pi'), + gemini: probe('gemini'), + claude: probe('claude'), + codex: probe('codex') } + }) + try { + const saved = service.read({ agent: 'claude', waitForListing: true }) + await vi.advanceTimersByTimeAsync(15_000) + await saved + warming = false + await vi.advanceTimersByTimeAsync(120_001) + + for (const agent of ['pi', 'gemini', 'claude', 'codex']) { + await discover(service, { agent }) + } + expect(await service.read({ agent: 'claude' })).toMatchObject({ + models: [{ id: 'saved' }], + listingInProgress: true + }) + + const expired = service.read({ + agent: 'claude', + waitForListing: true, + deadline: Date.now() + 5_000 + }) + await vi.advanceTimersByTimeAsync(5_000) + expect(await expired).toMatchObject({ + models: [{ id: 'saved' }], + listingInProgress: true, + waitExpired: true + }) + + await vi.advanceTimersByTimeAsync(15_000) + const warm = service.read({ agent: 'claude', waitForListing: true }) + const cold = service.read({ agent: 'codex', waitForListing: true }) + await vi.advanceTimersByTimeAsync(15_000) + expect(await warm).toMatchObject({ models: [{ id: 'claude-fresh' }] }) + expect(await cold).toMatchObject({ models: [{ id: 'codex-fresh' }] }) + const account = agentModelCatalogFingerprint({ + agent: 'claude', + accountHome: { variable: 'CODEX_HOME', path: '/account' }, + wslDistro: null + }) + expect(store.observation(account)).toMatchObject({ + models: [{ id: 'claude-fresh', isDefault: false }] + }) + } finally { + await service.stop() + vi.useRealTimers() } -) +}) diff --git a/src/main/native-chat/agent-model-catalog/agent-model-catalog-store.ts b/src/main/native-chat/agent-model-catalog/agent-model-catalog-store.ts index 753084a7cf9..781ced64ed5 100644 --- a/src/main/native-chat/agent-model-catalog/agent-model-catalog-store.ts +++ b/src/main/native-chat/agent-model-catalog/agent-model-catalog-store.ts @@ -310,6 +310,8 @@ export class AgentModelCatalogStore { pendingListing( ...wait: Parameters ): ReturnType { + // Waiting on a queued listing keeps it queued. + this.refreshes.get(wait[0])?.hold(wait[3]) return this.listingWaiters.wait(...wait) } diff --git a/src/main/providers/pty-provider-contract.ts b/src/main/providers/pty-provider-contract.ts index df69443221f..3119863ad4e 100644 --- a/src/main/providers/pty-provider-contract.ts +++ b/src/main/providers/pty-provider-contract.ts @@ -1,3 +1,4 @@ +import type { DaemonLaunchNoteChange } from '../daemon/protocol/daemon-launch-note-protocol' import type { PtyFencedTerminationRequest, PtyFencedTerminationOutcome @@ -207,6 +208,8 @@ export type IPtyProvider = { confirmForegroundProcess?: (id: string) => Promise /** Fresh execution-host proof that the spawned shell owns the PTY foreground. */ confirmShellForeground?: (id: string) => Promise + /** Persist main's live launch where the PTY outlives main; only the daemon keeps one. */ + setLaunchNote?: (id: string, change: DaemonLaunchNoteChange) => void serialize(ids: string[]): Promise revive(state: string): Promise listProcesses(opts?: { diff --git a/src/main/providers/pty-spawn-result.ts b/src/main/providers/pty-spawn-result.ts index 9a949cd87f6..ced9ec130a2 100644 --- a/src/main/providers/pty-spawn-result.ts +++ b/src/main/providers/pty-spawn-result.ts @@ -1,3 +1,4 @@ +import type { DaemonLaunchNote } from '../daemon/protocol/daemon-launch-note-protocol' import type { TerminalExitCause } from '../../shared/terminal/pty/lifecycle/terminal-exit-cause' import type { TerminalLaunchResult } from '../../shared/agent-session/lifecycle/agent-session-resume' import type { TerminalOscLinkRange } from '../../shared/terminal/links/terminal-osc-link-ranges' @@ -25,6 +26,8 @@ export type PtySpawnResult = TerminalLaunchResult & { pid?: number | null /** Minimal allowlisted launch ownership returned by daemon reattach. */ launchAgent?: TuiAgent + /** The unretired launch main armed before a restart, returned by daemon reattach. */ + launchNote?: DaemonLaunchNote | null /** Local WSL context: null is native; undefined is unavailable/legacy. */ wslDistro?: string | null /** Daemon reattach ANSI screen restore for xterm. */ diff --git a/src/main/runtime/agent-session/environment/structured-agent-environment-overrides.test.ts b/src/main/runtime/agent-session/environment/structured-agent-environment-overrides.test.ts deleted file mode 100644 index 1a31295d11e..00000000000 --- a/src/main/runtime/agent-session/environment/structured-agent-environment-overrides.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { afterEach, expect, it, vi } from 'vitest' -import { createStructuredAgentEnvironmentResolvers } from './structured-agent-shell-environment' - -afterEach(() => vi.unstubAllGlobals()) - -it.each([ - ['win32', { TOKEN: 'configured', PATH: '/configured/bin', KEEP: 'keep' }], - [ - 'linux', - { - TOKEN: 'inherited', - PATH: '/inherited/bin', - tOKEN: 'configured', - path: '/configured/bin', - KEEP: 'keep' - } - ] -] as const)('composes the effective agent environment on %s', async (platform, expected) => { - vi.stubGlobal('process', { ...process, platform }) - const environment = createStructuredAgentEnvironmentResolvers({ - resolveEnvironment: async () => ({ TOKEN: 'inherited', PATH: '/inherited/bin', KEEP: 'keep' }), - resolveAgentLaunchEnv: () => ({ tOKEN: 'configured', path: '/configured/bin' }) - }) - expect(await environment.resolveAgentEnvironment('claude')).toEqual(expected) - expect(await environment.resolveAgentEnvironment('codex')).toEqual(expected) -}) diff --git a/src/main/runtime/agent-session/launch/structured-agent-launch-env-preflight.test.ts b/src/main/runtime/agent-session/launch/structured-agent-launch-env-preflight.test.ts deleted file mode 100644 index 41906268caa..00000000000 --- a/src/main/runtime/agent-session/launch/structured-agent-launch-env-preflight.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { afterEach, expect, it, vi } from 'vitest' -import { resolveHostStructuredAgentLaunchEnvironment } from './structured-agent-launch-support' -import type * as Environment from '../environment/structured-agent-shell-environment' - -vi.mock('../environment/structured-agent-shell-environment', async (importOriginal) => ({ - ...(await importOriginal()), - resolveHostAgentBaseEnvironment: async () => ({ - TOKEN: 'inherited', - PATH: '/inherited/bin', - KEEP: 'keep' - }) -})) -afterEach(() => vi.unstubAllGlobals()) - -it('uses the configured Windows environment for launch preflight', async () => { - vi.stubGlobal('process', { ...process, platform: 'win32' }) - const env = await resolveHostStructuredAgentLaunchEnvironment('claude', { - agentDefaultEnv: { claude: { tOKEN: 'configured', path: '/configured/bin' } } - }) - expect(env).toEqual({ TOKEN: 'configured', PATH: '/configured/bin', KEEP: 'keep' }) -}) diff --git a/src/main/runtime/daemon-launch-note-reattach.test.ts b/src/main/runtime/daemon-launch-note-reattach.test.ts new file mode 100644 index 00000000000..bd97df9a6ca --- /dev/null +++ b/src/main/runtime/daemon-launch-note-reattach.test.ts @@ -0,0 +1,57 @@ +import { describe, expect, it, vi } from 'vitest' +import { OrcaRuntimeService } from './orca-runtime' +import { makePaneKey } from '../../shared/workspace-layout/stable-pane-id' + +const PTY_ID = 'pty-reattached' +const TAB_ID = '11111111-1111-4111-8111-111111111111' +const LEAF_ID = '22222222-2222-4222-8222-222222222222' +const INCARNATION = 'reattach-incarnation' +const LAUNCH_CONFIG = { + agentArgs: '--dangerously-skip-permissions --verbose', + agentEnv: { K: 'v' } +} + +const NOTE = { launchToken: 'launch-1', launchConfig: LAUNCH_CONFIG } + +const PANE_KEY = makePaneKey(TAB_ID, LEAF_ID) + +function attach( + runtime: OrcaRuntimeService, + launchNote: typeof NOTE | null | undefined, + incarnationId = INCARNATION +) { + runtime.registerPty(PTY_ID, 'worktree-1', null, { + tabId: TAB_ID, + leafId: LEAF_ID, + incarnationId, + providerReattachLaunchIdentity: { incarnationId, launchAgent: 'claude' }, + reattach: launchNote === undefined ? {} : { launchNote } + }) +} + +function makeRuntime() { + const runtime = new OrcaRuntimeService(null) + const setLaunchNote = vi.fn() + runtime.setPtyController({ + write: vi.fn(() => true), + kill: vi.fn(), + getForegroundProcess: async () => null, + setLaunchNote + }) + return { runtime, setLaunchNote } +} + +describe('a daemon reattach after a main restart', () => { + it('does not let an earlier foreground read revive a launch a later attach saw finish', async () => { + const { runtime } = makeRuntime() + let answer: (foreground: 'agent') => void = () => {} + vi.spyOn(runtime, 'readLaunchedAgentForeground').mockReturnValue( + new Promise((resolve) => (answer = resolve)) + ) + attach(runtime, NOTE) + attach(runtime, null) + answer('agent') + await new Promise((resolve) => setTimeout(resolve, 0)) + expect(runtime.getLaunchStatementForPaneKey(PANE_KEY, 'claude')).toBeNull() + }) +}) diff --git a/src/main/runtime/orca-runtime-saved-restore-launch.test.ts b/src/main/runtime/orca-runtime-saved-restore-launch.test.ts new file mode 100644 index 00000000000..fd03c943bd2 --- /dev/null +++ b/src/main/runtime/orca-runtime-saved-restore-launch.test.ts @@ -0,0 +1,102 @@ +import { describe, expect, it, vi } from 'vitest' +import { EnsureAgentSessionParams } from '../../shared/rpc-contract/agent-session-params' +import type { AgentStatusIpcPayload } from '../../shared/agent-status/contracts/agent-status-ipc-payload' +import { OrcaRuntimeService } from './orca-runtime' +import { partialRuntimeStore } from './orca-runtime-tests/runtime-store-partial-fixture' + +const TAB = '11111111-1111-4111-8111-111111111111' +const SOURCE_PANE = `${TAB}:22222222-2222-4222-8222-222222222222` +const NEW_TAB = '33333333-3333-4333-8333-333333333333' +const NEW_LEAF = '44444444-4444-4444-8444-444444444444' +const SESSION = { key: 'session_id', id: 'conversation-1' } as const + +function restoreOnHost(args: { + recorded?: { agentArgs: string; agentEnv: Record } + hookMode?: string +}) { + const runtime = new OrcaRuntimeService( + partialRuntimeStore({ + getSettings: () => ({ + disabledTuiAgents: [], + agentCmdOverrides: {}, + agentDefaultArgs: { claude: '--model current' }, + agentDefaultEnv: { claude: { CURRENT: '1' } } + }) + }) + ) + const record = { + paneKey: SOURCE_PANE, + worktreeId: 'worktree-1', + agent: 'claude' as const, + providerSession: SESSION, + prompt: '', + state: 'done' as const, + capturedAt: 1, + updatedAt: 1, + ...(args.recorded ? { launchConfig: args.recorded } : {}) + } + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the host's restore reads only the row's pane, agent and provider session. + const hookRow = { + paneKey: SOURCE_PANE, + agentType: 'claude', + providerSession: { ...SESSION, permissionMode: args.hookMode } + } as AgentStatusIpcPayload + Object.assign(runtime, { + resolveTerminalWorkspaceLaunchScope: vi.fn(async () => ({ + id: 'worktree-1', + path: '/tmp/worktree-1', + connectionId: null + })), + getWorkspaceSessionForWorktree: () => ({ + sleepingAgentSessionsByPaneKey: { [SOURCE_PANE]: record } + }), + getAgentProviderSessionSnapshotFn: () => [hookRow] + }) + const createTerminal = vi.spyOn(runtime, 'createTerminal').mockResolvedValue({ + handle: 'term_restore', + tabId: NEW_TAB, + paneKey: `${NEW_TAB}:${NEW_LEAF}`, + ptyId: 'pty-restore', + worktreeId: 'worktree-1', + title: null, + surface: 'background' + }) + return { + restore: () => + runtime.ensureAgentSession( + EnsureAgentSessionParams.parse({ + kind: 'explicit', + resumeOrigin: 'saved', + restoreSourcePaneKey: SOURCE_PANE, + worktree: 'id:worktree-1', + agent: 'claude', + providerSession: SESSION, + agentArgs: '--model client', + placement: { tabId: NEW_TAB, leafId: NEW_LEAF } + }) + ), + launched: () => createTerminal.mock.calls[0]?.[1] + } +} + +// A Wake opens a new tab: the host must replay the pane that ran, not the new pane or its own settings. +describe('a saved restore into a new pane on its host', () => { + it("replays the source pane's note", async () => { + const host = restoreOnHost({ + recorded: { agentArgs: '--verbose', agentEnv: { RECORDED: '1' } } + }) + await host.restore() + expect(host.launched()?.command).toContain('--verbose') + expect(host.launched()?.command).not.toContain('--model') + expect(host.launched()?.env).toMatchObject({ RECORDED: '1' }) + expect(host.launched()?.env).not.toHaveProperty('CURRENT') + }) + + it("without a note, keeps only the bypass the host's hooks saw, never current settings", async () => { + const host = restoreOnHost({ hookMode: 'bypassPermissions' }) + await host.restore() + expect(host.launched()?.command).toContain('--dangerously-skip-permissions') + expect(host.launched()?.command).not.toContain('--model') + expect(host.launched()?.env).not.toHaveProperty('CURRENT') + }) +}) diff --git a/src/main/runtime/orchestration/db/dispatch-context/worker-follow-up-lineage.ts b/src/main/runtime/orchestration/db/dispatch-context/worker-follow-up-lineage.ts index 58e7314c0a6..2e753dad16e 100644 --- a/src/main/runtime/orchestration/db/dispatch-context/worker-follow-up-lineage.ts +++ b/src/main/runtime/orchestration/db/dispatch-context/worker-follow-up-lineage.ts @@ -1,8 +1,10 @@ +import { sleepingAgentLaunchConfigSchema } from '../../../../../shared/workspace-session/workspace-session-sleeping-agents' import { resolveWorkerLineageFacts, FOLLOW_UP_MESSAGE_TYPES } from './worker-follow-up-plan' import { normalizeAgentProviderSession, - type AgentProviderSessionMetadata + type AgentProviderSessionMetadata, + type SleepingAgentLaunchConfig } from '../../../../../shared/agent-session/lifecycle/agent-session-resume' import { getConnectionExecutionHostId, @@ -23,6 +25,8 @@ export type WorkerResumeSession = { agent: TuiAgent providerSession: AgentProviderSessionMetadata captureHostId: ExecutionHostId + /** The recipe the worker launched with; its pane's own record is gone once it closes (#37693). */ + launchConfig?: SleepingAgentLaunchConfig } export function getFollowUpDispatch( @@ -84,10 +88,18 @@ export function readWorkerResumeSession( const providerSession = normalizeAgentProviderSession( 'providerSession' in parsed ? parsed.providerSession : undefined ) + const launchConfig = sleepingAgentLaunchConfigSchema.parse( + 'launchConfig' in parsed ? parsed.launchConfig : undefined + ) return isTuiAgent(agent) && providerSession && (connectionId === null || typeof connectionId === 'string') - ? { agent, providerSession, captureHostId: getConnectionExecutionHostId(connectionId) } + ? { + agent, + providerSession, + captureHostId: getConnectionExecutionHostId(connectionId), + ...(launchConfig ? { launchConfig } : {}) + } : null } catch { return null @@ -107,10 +119,12 @@ export function recordWorkerResumeSession( const connectionId = isLocalHookConnectionId(session.connectionId) ? null : (session.connectionId ?? null) + const { launchConfig } = session const value = JSON.stringify({ agent: session.agent, providerSession: session.providerSession, - connectionId + connectionId, + ...(launchConfig ? { launchConfig } : {}) }) // A repeat of the stored conversation writes nothing. db.db diff --git a/src/main/runtime/rpc/methods/mobile-delivery/mobile-web-bundle-asset-reader.ts b/src/main/runtime/rpc/methods/mobile-delivery/mobile-web-bundle-asset-reader.ts index 7a9e56e679a..e429c63e881 100644 --- a/src/main/runtime/rpc/methods/mobile-delivery/mobile-web-bundle-asset-reader.ts +++ b/src/main/runtime/rpc/methods/mobile-delivery/mobile-web-bundle-asset-reader.ts @@ -48,7 +48,7 @@ async function hashAsset(root: string, asset: MobileWebBundleAsset): Promise, offset: number, length: number ): Promise { diff --git a/src/main/runtime/rpc/methods/mobile-delivery/mobile-web-bundle-range.test.ts b/src/main/runtime/rpc/methods/mobile-delivery/mobile-web-bundle-range.test.ts index 8ce5846116c..5553b263030 100644 --- a/src/main/runtime/rpc/methods/mobile-delivery/mobile-web-bundle-range.test.ts +++ b/src/main/runtime/rpc/methods/mobile-delivery/mobile-web-bundle-range.test.ts @@ -2,11 +2,13 @@ import { randomBytes } from 'node:crypto' import { mkdtempSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' -import { gunzipSync } from 'node:zlib' +import { brotliDecompressSync, gunzipSync } from 'node:zlib' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { + MOBILE_WEB_BUNDLE_BROTLI_RANGE_METHOD, MOBILE_WEB_BUNDLE_CHUNK_BYTES as RANGE_BYTES, MOBILE_WEB_BUNDLE_RANGE_METHOD, + MobileWebBundleBrotliRangeResultSchema, MobileWebBundleRangeResultSchema, type MobileWebBundleRangeResult } from '../../../../../shared/mobile-web-bundle/bundle-rpc-contract' @@ -140,3 +142,68 @@ describe('mobileWeb.bundle.range on an install that carries a bundle', () => { ).toBe(true) }) }) + +describe('mobileWeb.bundle.brotliRange', () => { + // Base64 text shrinks only to about 3/4, so its copy spans several windows. + const SCRIPT: SyntheticAsset = (() => { + const bytes = Buffer.from(randomBytes(RANGE_BYTES * 2).toString('base64')) + return { + path: `assets/${sha256Hex(bytes)}.js`, + bytes, + contentType: 'text/javascript', + brotli: true + } + })() + let bundle: SyntheticMobileWebBundle + + beforeEach(() => { + bundle = writeSyntheticMobileWebBundle(join(scratch, 'out', 'mobile-web'), 1, [ + SCRIPT, + INCOMPRESSIBLE + ]) + }) + + it('pages the brotli copy on the range grid, and the pages inflate to the asset', async () => { + const entry = bundle.assets.find((asset) => asset.path === SCRIPT.path) + const copyLength = entry?.brotliByteLength ?? 0 + expect(copyLength).toBeGreaterThan(0) + const pieces: Buffer[] = [] + for (let offset = 0; ; offset += RANGE_BYTES) { + const response = await call(MOBILE_WEB_BUNDLE_BROTLI_RANGE_METHOD, { + buildId: bundle.buildId, + path: SCRIPT.path, + offset + }) + if (!response.ok) { + throw new Error(response.error.message) + } + const result = MobileWebBundleBrotliRangeResultSchema.parse(response.result) + expect(result).toMatchObject({ + path: SCRIPT.path, + offset, + assetByteLength: SCRIPT.bytes.byteLength, + sha256: sha256Hex(SCRIPT.bytes), + eof: offset + RANGE_BYTES >= copyLength + }) + pieces.push(Buffer.from(result.dataBase64, 'base64')) + if (result.eof) { + break + } + } + expect(pieces.length).toBe(Math.ceil(copyLength / RANGE_BYTES)) + expect(pieces.length).toBeGreaterThan(1) + expect(Buffer.concat(pieces).byteLength).toBe(copyLength) + expect(brotliDecompressSync(Buffer.concat(pieces)).equals(SCRIPT.bytes)).toBe(true) + }) + + it('refuses an asset the manifest gives no brotli copy', async () => { + const response = await call(MOBILE_WEB_BUNDLE_BROTLI_RANGE_METHOD, { + buildId: bundle.buildId, + path: INCOMPRESSIBLE.path, + offset: 0 + }) + + expect(response.ok).toBe(false) + expect(response.ok ? null : response.error.message).toBe('mobile_web_bundle_asset_unknown') + }) +}) diff --git a/src/main/runtime/rpc/methods/mobile-delivery/mobile-web-bundle.test-fixture.ts b/src/main/runtime/rpc/methods/mobile-delivery/mobile-web-bundle.test-fixture.ts index 1782d55d158..ff015a62f71 100644 --- a/src/main/runtime/rpc/methods/mobile-delivery/mobile-web-bundle.test-fixture.ts +++ b/src/main/runtime/rpc/methods/mobile-delivery/mobile-web-bundle.test-fixture.ts @@ -1,6 +1,7 @@ import { createHash } from 'node:crypto' import { mkdirSync, writeFileSync } from 'node:fs' import { join } from 'node:path' +import { brotliCompressSync } from 'node:zlib' import { installFakeAppEnvironment } from '../../../../../../config/scripts/test-runtime/environment/vitest-host-ports-setup' import type { OrcaRuntimeService } from '../../../orca-runtime' import { RpcDispatcher } from '../../dispatcher' @@ -8,6 +9,7 @@ import { MOBILE_WEB_BUNDLE_METHODS } from './mobile-web-bundle' import { MOBILE_WEB_BUNDLE_CHUNK_BYTES } from '../../../../../shared/mobile-web-bundle/bundle-rpc-contract' import { computeMobileWebBundleId, + mobileWebBundleBrotliPath, type MobileWebBundleAsset } from '../../../../../shared/mobile-web-bundle/manifest-contract' @@ -22,7 +24,8 @@ export function mobileWebBundleFiller(byteLength: number, seed: number): Buffer return bytes } -export type SyntheticAsset = { path: string; bytes: Buffer; contentType: string } +/** `brotli` writes the asset's brotli copy and lists its length, as the shipped build does. */ +export type SyntheticAsset = { path: string; bytes: Buffer; contentType: string; brotli?: boolean } /** Use synthetic multi-chunk assets because the current builder emits smaller files and CI unit jobs do not build the bundle. */ function syntheticAssets(seed: number): SyntheticAsset[] { @@ -58,15 +61,22 @@ export function writeSyntheticMobileWebBundle( ): SyntheticMobileWebBundle { mkdirSync(join(root, 'assets'), { recursive: true }) const written = [...syntheticAssets(seed), ...extraAssets] + const copies = new Map() for (const asset of written) { writeFileSync(join(root, asset.path), asset.bytes) + if (asset.brotli === true) { + const copy = brotliCompressSync(asset.bytes) + writeFileSync(join(root, mobileWebBundleBrotliPath(asset.path)), copy) + copies.set(asset.path, copy) + } } const assets = written .map((asset) => ({ path: asset.path, sha256: sha256Hex(asset.bytes), byteLength: asset.bytes.byteLength, - contentType: asset.contentType + contentType: asset.contentType, + ...(copies.has(asset.path) ? { brotliByteLength: copies.get(asset.path)?.byteLength } : {}) })) .sort((left, right) => (left.path < right.path ? -1 : 1)) const buildId = computeMobileWebBundleId(assets) diff --git a/src/main/runtime/rpc/methods/mobile-delivery/mobile-web-bundle.ts b/src/main/runtime/rpc/methods/mobile-delivery/mobile-web-bundle.ts index ddcea5db11c..1d700f41cf2 100644 --- a/src/main/runtime/rpc/methods/mobile-delivery/mobile-web-bundle.ts +++ b/src/main/runtime/rpc/methods/mobile-delivery/mobile-web-bundle.ts @@ -2,19 +2,24 @@ * Never proxy to SSH/relay execution hosts: the phone paired with this desktop. * Verify/read failures map to asset-changed without exposing host paths. */ import { + MOBILE_WEB_BUNDLE_BROTLI_RANGE_METHOD, MOBILE_WEB_BUNDLE_CHUNK_BYTES, MOBILE_WEB_BUNDLE_CHUNK_METHOD, MOBILE_WEB_BUNDLE_MANIFEST_METHOD, MOBILE_WEB_BUNDLE_RANGE_METHOD, MobileWebBundleChunkParamsSchema, MobileWebBundleRangeParamsSchema, + type MobileWebBundleBrotliRangeResult, type MobileWebBundleChunkParams, type MobileWebBundleChunkResult, type MobileWebBundleErrorCode, type MobileWebBundleManifestResult, type MobileWebBundleRangeResult } from '../../../../../shared/mobile-web-bundle/bundle-rpc-contract' -import type { MobileWebBundleAsset } from '../../../../../shared/mobile-web-bundle/manifest-contract' +import { + mobileWebBundleBrotliPath, + type MobileWebBundleAsset +} from '../../../../../shared/mobile-web-bundle/manifest-contract' import { loadBundledMobileWebBundle, type BundledMobileWebBundle @@ -72,16 +77,27 @@ function findAsset(bundle: BundledMobileWebBundle, path: string): MobileWebBundl function assertOffsetAddressesAWindow( offset: number, windowBytes: number, - asset: MobileWebBundleAsset + file: WindowSource ): void { if (offset % windowBytes !== 0) { throw bundleError('mobile_web_bundle_offset_invalid') } - if (offset > 0 && offset >= asset.byteLength) { + if (offset > 0 && offset >= file.byteLength) { throw bundleError('mobile_web_bundle_offset_invalid') } } +/** The file a window is cut from: the asset itself, or its brotli copy. */ +type WindowSource = Pick + +/** Only assets the manifest gives a copy have one; asking for another is asking for an unlisted file. */ +function brotliCopyOf(asset: MobileWebBundleAsset): WindowSource { + if (asset.brotliByteLength === undefined) { + throw bundleError('mobile_web_bundle_asset_unknown') + } + return { path: mobileWebBundleBrotliPath(asset.path), byteLength: asset.brotliByteLength } +} + /** The self-description every chunk or range reply carries, plus the raw bytes of its window. */ type VerifiedWindow = { header: Omit @@ -93,7 +109,8 @@ async function readVerifiedWindow( ctx: RpcContext, params: MobileWebBundleChunkParams, windowBytes: number, - encode: (window: VerifiedWindow) => Promise + encode: (window: VerifiedWindow) => Promise, + sourceOf: (asset: MobileWebBundleAsset) => WindowSource = (asset) => asset ): Promise { const bundle = requireBundle() // Check build before path: auto-update means restart from the manifest, not missing-asset. @@ -101,7 +118,8 @@ async function readVerifiedWindow( throw bundleError('mobile_web_bundle_build_changed') } const asset = findAsset(bundle, params.path) - assertOffsetAddressesAWindow(params.offset, windowBytes, asset) + const source = sourceOf(asset) + assertOffsetAddressesAWindow(params.offset, windowBytes, source) const release = acquireMobileWebBundleReadSlot(mobileWebBundleReadBucket(ctx)) if (!release) { @@ -115,7 +133,7 @@ async function readVerifiedWindow( abortIfDisconnected(ctx) const data = await readMobileWebBundleAssetWindow( bundle.root, - asset, + source, params.offset, windowBytes ) @@ -127,7 +145,7 @@ async function readVerifiedWindow( offset: params.offset, assetByteLength: asset.byteLength, sha256: asset.sha256, - eof: params.offset + data.byteLength >= asset.byteLength + eof: params.offset + data.byteLength >= source.byteLength }, data }) @@ -138,6 +156,11 @@ async function readVerifiedWindow( } } +/** Chunks and brotli windows go out as read: chunks predate encodings, and brotli output does not shrink again. */ +async function rawWindowReply(window: VerifiedWindow): Promise { + return { ...window.header, dataBase64: window.data.toString('base64') } +} + export const MOBILE_WEB_BUNDLE_METHODS = [ defineMethod({ name: MOBILE_WEB_BUNDLE_MANIFEST_METHOD, @@ -155,10 +178,7 @@ export const MOBILE_WEB_BUNDLE_METHODS = [ permission: 'workspace', params: MobileWebBundleChunkParamsSchema, handler: (params, ctx): Promise => - readVerifiedWindow(ctx, params, MOBILE_WEB_BUNDLE_CHUNK_BYTES, async (window) => ({ - ...window.header, - dataBase64: window.data.toString('base64') - })) + readVerifiedWindow(ctx, params, MOBILE_WEB_BUNDLE_CHUNK_BYTES, rawWindowReply) }), defineMethod({ name: MOBILE_WEB_BUNDLE_RANGE_METHOD, @@ -173,5 +193,13 @@ export const MOBILE_WEB_BUNDLE_METHODS = [ dataBase64: encoded.bytes.toString('base64') } }) + }), + defineMethod({ + name: MOBILE_WEB_BUNDLE_BROTLI_RANGE_METHOD, + permission: 'workspace', + params: MobileWebBundleRangeParamsSchema, + handler: (params, ctx): Promise => + // The raw asset's hash still gates every window of its copy. + readVerifiedWindow(ctx, params, MOBILE_WEB_BUNDLE_CHUNK_BYTES, rawWindowReply, brotliCopyOf) }) ] diff --git a/src/main/runtime/rpc/methods/orchestration/federation/federated-worker-agent-launch.ts b/src/main/runtime/rpc/methods/orchestration/federation/federated-worker-agent-launch.ts index 0a3b9d07f9f..060cb204d08 100644 --- a/src/main/runtime/rpc/methods/orchestration/federation/federated-worker-agent-launch.ts +++ b/src/main/runtime/rpc/methods/orchestration/federation/federated-worker-agent-launch.ts @@ -6,6 +6,7 @@ import type { TuiAgent } from '../../../../../../shared/agent-launch/catalog/tui import { executeAgentLaunch } from '../../../../../agent-launch/agent-launch-executor' import type { OrcaRuntimeService } from '../../../../orca-runtime' import type { OrchestrationDb } from '../../../../orchestration/db/orchestration-db' +import { resolveUnattendedSetupDecision } from '../../../../../workspace-hooks/effective-hook-config' import { WORKER_START_VOCABULARY } from '../structured-workers/orchestration-worker-start-mode' import type { WorkerSetupReceipt } from '../worker/worker-topology' import { resolveWorkerWorktreeName } from '../worker/worker-worktree-creation' @@ -78,7 +79,10 @@ export async function launchFederatedWorkerAgent(args: { dispatchId: params.dispatchId, stage: 'worktree_creating' }) - const setupDecision = params.setup ?? 'run' + const setupDecision = resolveUnattendedSetupDecision( + await runtime.showRepo(repo), + params.setup + ) const result = await runtime.createManagedWorktree({ onStartupTerminalCreated: (terminal) => { terminalCreation = terminal @@ -119,7 +123,7 @@ export async function launchFederatedWorkerAgent(args: { name: result.worktree.displayName }) const setup: WorkerSetupReceipt = { - requested: setupDecision, + requested: params.setup ?? 'inherit', effective: setupDecision, source: args.setupSource, hookFound: result.setupReceipt?.hookFound ?? false, diff --git a/src/main/runtime/rpc/methods/orchestration/federation/federated-worker-start.ts b/src/main/runtime/rpc/methods/orchestration/federation/federated-worker-start.ts index 1cb692c1d60..2d97909ae01 100644 --- a/src/main/runtime/rpc/methods/orchestration/federation/federated-worker-start.ts +++ b/src/main/runtime/rpc/methods/orchestration/federation/federated-worker-start.ts @@ -122,7 +122,7 @@ export async function startFederatedWorker(args: FederatedWorkerStartRequest): P ? ORCHESTRATION_FEDERATION_CONTROL_MAIL_PROTOCOL_VERSION : 1 - const setupDecision = createsWorktree ? (params.setup ?? 'run') : 'not_applicable' + const setupDecision = createsWorktree ? (params.setup ?? 'inherit') : 'not_applicable' const runId = args.bindRunId() const started = db.createStartingWorkerDispatch({ creator: resolveDispatchCreator(runtime, params.from, args.callerSession), @@ -195,7 +195,7 @@ export async function startFederatedWorker(args: FederatedWorkerStartRequest): P displayName: params.displayName, ...(params.displayName !== undefined ? { displayNameKind: 'user' as const } : {}), comment: params.comment, - setup: createsWorktree ? (params.setup ?? 'run') : undefined, + setup: createsWorktree ? params.setup : undefined, setupSource: createsWorktree ? params.setup ? 'explicit_request' diff --git a/src/main/runtime/rpc/methods/orchestration/worker/worker-follow-up-placement.ts b/src/main/runtime/rpc/methods/orchestration/worker/worker-follow-up-placement.ts index f21da18e24b..606dabfc297 100644 --- a/src/main/runtime/rpc/methods/orchestration/worker/worker-follow-up-placement.ts +++ b/src/main/runtime/rpc/methods/orchestration/worker/worker-follow-up-placement.ts @@ -1,3 +1,4 @@ +import { resolveResumeLaunchRecipe } from '../../../../../../shared/agent-launch/choices/resume-launch-recipe' import type { OrcaRuntimeService } from '../../../../orca-runtime' import type { OrchestrationDb } from '../../../../orchestration/db/orchestration-db' import { OrchestrationError } from '../../../../orchestration/orchestration-error' @@ -50,6 +51,12 @@ export async function reopenWorkerTerminal( ): Promise { const resumed = await runtime.resumeAgentSessionInWorkspace(worktreeId, { ...session, + // Why always a recipe: without its own note the worker resumes unrecorded, never with another pane's. + launchConfig: resolveResumeLaunchRecipe( + session.launchConfig, + session.agent, + session.providerSession + ), // A retry of this follow-up reconciles the pane it may have started. mutationId: dispatchId }) diff --git a/src/main/runtime/rpc/methods/orchestration/worker/worker-worktree-creation.ts b/src/main/runtime/rpc/methods/orchestration/worker/worker-worktree-creation.ts index 225e552e7d5..8f74380878e 100644 --- a/src/main/runtime/rpc/methods/orchestration/worker/worker-worktree-creation.ts +++ b/src/main/runtime/rpc/methods/orchestration/worker/worker-worktree-creation.ts @@ -6,6 +6,7 @@ import type { TuiAgent } from '../../../../../../shared/agent-launch/catalog/tui import { suggestCreatureNameForPaths } from '../../../../../../shared/worktree-name-suggestion' import type { OrcaRuntimeService } from '../../../../orca-runtime' import type { OrchestrationDb } from '../../../../orchestration/db/orchestration-db' +import { resolveUnattendedSetupDecision } from '../../../../../workspace-hooks/effective-hook-config' import { OrchestrationError } from '../../../../orchestration/orchestration-error' import { parseWorkspaceKey } from '../../../../../../shared/workspace-scope' import type { WorkerEffect, WorkerSetupReceipt } from './worker-topology' @@ -121,7 +122,10 @@ export async function createWorkerWorktree(args: { setupReceipt: WorkerSetupReceipt }> { const { runtime, db, dispatchId, requestedWorktree, target, params, effects } = args - const setupDecision = params.setup ?? 'run' + const setupDecision = resolveUnattendedSetupDecision( + await runtime.showRepo(target.repoSelector), + params.setup + ) let terminalCreation: RuntimeTerminalCreate | undefined db.recordWorkerStage({ dispatchId, stage: 'worktree_creating', effects }) const created = await runtime.createManagedWorktree({ @@ -173,7 +177,7 @@ export async function createWorkerWorktree(args: { residualResources: effects }) const setupReceipt = { - requested: setupDecision, + requested: params.setup ?? 'inherit', effective: setupDecision, source: params.setup ? 'explicit_request' : 'orchestration_default', hookFound: created.setupReceipt?.hookFound ?? false, diff --git a/src/main/runtime/rpc/methods/orchestration/worker/workers-new-worktree.test.ts b/src/main/runtime/rpc/methods/orchestration/worker/workers-new-worktree.test.ts index 3dc1ec37a2b..8ecf24d3652 100644 --- a/src/main/runtime/rpc/methods/orchestration/worker/workers-new-worktree.test.ts +++ b/src/main/runtime/rpc/methods/orchestration/worker/workers-new-worktree.test.ts @@ -137,6 +137,29 @@ describe('orchestration new-worktree workers', () => { } } + it("skips setup when the project's setup is off and the coordinator did not ask for it", async () => { + vi.mocked(runtime.showRepo).mockResolvedValue({ + id: 'repo', + path: '/repo', + displayName: 'repo', + badgeColor: 'gray', + addedAt: 1, + kind: 'git', + hookSettings: { + mode: 'auto', + setupRunPolicy: 'skip-by-default', + scripts: { setup: '', archive: '' } + } + }) + mockCreatedWorktree({ state: 'skipped' }) + + await startWorker() + + expect(runtime.createManagedWorktree).toHaveBeenCalledWith( + expect.objectContaining({ setupDecision: 'skip' }) + ) + }) + // Composer and idle readiness paths must both record setup evidence; the composer path previously dropped it. it('does not inject task input when the gated setup terminal fails to start', async () => { diff --git a/src/main/runtime/rpc/server/runtime-rpc-mobile-paired-desktop-methods.ts b/src/main/runtime/rpc/server/runtime-rpc-mobile-paired-desktop-methods.ts index 142ec54d6bf..f60694792ba 100644 --- a/src/main/runtime/rpc/server/runtime-rpc-mobile-paired-desktop-methods.ts +++ b/src/main/runtime/rpc/server/runtime-rpc-mobile-paired-desktop-methods.ts @@ -3,6 +3,7 @@ import { ORCA_SIDEBAR_INPUTS_IS_MAIN } from '../../../../shared/sidebar-inputs-s /** Phone methods the relaying Mac always answers itself, even when the call names a server. */ export const PAIRED_DESKTOP_METHODS = [ // Device and pairing concerns of the phone's own pairing. + 'mobileWeb.bundle.brotliRange', 'mobileWeb.bundle.chunk', 'mobileWeb.bundle.manifest', 'mobileWeb.bundle.range', diff --git a/src/main/runtime/rpc/terminal-agent-prompt-socket.test.ts b/src/main/runtime/rpc/terminal-agent-prompt-socket.test.ts index 629c3535517..ed807aa74fc 100644 --- a/src/main/runtime/rpc/terminal-agent-prompt-socket.test.ts +++ b/src/main/runtime/rpc/terminal-agent-prompt-socket.test.ts @@ -25,6 +25,9 @@ import { bindPaneFirst } from '../orca-runtime-tests/bind-first-pane.test-fixtur import * as inputAdmission from '../terminal/input/terminal-input-admission' import { writePtyFromRuntimeController } from '../../ipc/pty/runtime/operations' import { getLocalPtyProvider } from '../../ipc/pty/provider/registry' +import { createHookListenerState } from '../../../shared/agent-hook-listener/listener-state' +import { normalizeHookPayload } from '../../../shared/agent-hook-listener/normalization/normalize-hook-payload' +import { AgentHookServer } from '../../agent-hooks/server/agent-hook-server' vi.mock('../../git/worktrees/catalog/worktree-scan-cache', async (importOriginal) => { const listings = getWorktreeListingMocks() @@ -59,11 +62,29 @@ const { getWorktreeListingMocks } = vi.hoisted(() => { return { getWorktreeListingMocks: () => (listings ??= createWorktreeListingMocks()) } }) -it.each(['before', 'after', 'paired', 'orchestration'] as const)( +it.each([ + 'before', + 'after', + 'paired', + 'orchestration', + 'codex-startup', + 'codex-replay', + 'codex-cold-replay' +] as const)( 'real CLI phone admission and delivery: %s', async (boundary) => { const disconnect = boundary === 'before' || boundary === 'after' const paired = boundary === 'paired' + const codex = boundary.startsWith('codex-') + const replay = boundary === 'codex-replay' || boundary === 'codex-cold-replay' + const hookServer = replay ? new AgentHookServer() : undefined + await hookServer?.start({ env: 'production' }) + const text = codex ? 'x'.repeat(1_000) : 'first' + const hookState = createHookListenerState() + let hook: ReturnType = null + let promptHandle = '' + let held = true + let enters = 0 const latch = () => Promise.withResolvers() const [pasted, release, closed, received] = [latch(), latch(), latch(), latch()] const root = mkdtempSync(join(tmpdir(), 'orca-prompt-socket-')) @@ -71,14 +92,88 @@ it.each(['before', 'after', 'paired', 'orchestration'] as const)( const endpoint = kind === 'named-pipe' ? `\\\\.\\pipe\\${root.split(/[/\\]/).at(-1)}` : join(root, 'rpc') const transport = new UnixSocketTransport({ endpoint, kind }) - const providerWrite = vi.spyOn(getLocalPtyProvider(), 'write').mockReturnValue(true) + vi.spyOn(getLocalPtyProvider(), 'write').mockReturnValue(true) const promptAdmission = vi.spyOn(inputAdmission, 'assertTerminalInputAdmitted') const rawAdmission = vi.spyOn(inputAdmission, 'createTerminalInputReservation') const { runtime, handle, writes } = await createAgentPromptSubmissionRuntime( - () => pasted.resolve(), - 'claude', - (host, id, data) => writePtyFromRuntimeController({ runtime: host }, id, data, 'driving') + (_host, data) => { + pasted.resolve() + if (codex && data === '\r') { + enters += 1 + held = enters === 1 + hook = normalizeHookPayload( + hookState, + 'codex', + { + paneKey: 'tab-prompt:22222222-2222-4222-8222-222222222222', + payload: held + ? { + hook_event_name: 'SessionStart', + source: 'startup', + session_id: '11111111-1111-4111-8111-111111111111' + } + : { + hook_event_name: 'UserPromptSubmit', + session_id: '11111111-1111-4111-8111-111111111111', + turn_id: 'turn-1', + prompt: text + } + }, + 'production' + ) + if (hook && hookServer) { + if (!held || boundary !== 'codex-cold-replay') { + hookServer.ingestRemote(hook, 'fixture-connection') + } + if (held) { + hookServer.ingestRemote( + { + ...hook, + hasExplicitPrompt: true, + isReplay: true, + payload: { + ...hook.payload, + state: 'working', + prompt: 'old task', + sessionBoundary: undefined, + mainAgent: { state: 'working', stateStartedAt: 1_000 } + } + }, + 'fixture-connection' + ) + } + } + } + }, + codex ? 'codex' : 'claude', + (host, id, data) => writePtyFromRuntimeController({ runtime: host }, id, data, 'driving'), + { + getAgentStatusSnapshot: () => + hookServer + ? hookServer + .getStatusSnapshot() + .map((row) => ({ ...row, terminalHandle: promptHandle })) + : hook + ? [ + { + ...hook.payload, + paneKey: hook.paneKey, + terminalHandle: promptHandle, + receivedAt: hook.payload.mainAgent?.stateStartedAt ?? 1_000, + stateStartedAt: hook.payload.mainAgent?.stateStartedAt ?? 1_000, + connectionId: null + } + ] + : [] + } ) + promptHandle = handle + if (codex) { + Object.defineProperty(runtime, 'getPtyWriteHostPlatform', { value: () => 'win32' }) + Object.defineProperty(runtime, 'readCurrentTerminalWaitScreen', { + value: async () => ({ lines: [], draft: held ? '[Pasted Content 1000 chars]' : undefined }) + }) + } const db = new OrchestrationDb(':memory:') runtime.setOrchestrationDb(db) const methods = [...STATUS_METHODS, ...TERMINAL_METHODS, ...ORCHESTRATION_METHODS] @@ -134,7 +229,10 @@ it.each(['before', 'after', 'paired', 'orchestration'] as const)( PATH: process.env.PATH, SystemRoot: process.env.SystemRoot } - let args = ['terminal', 'send', '--terminal', handle, '--text', 'first', '--enter', '--json'] + let args = ['terminal', 'send', '--terminal', handle, '--text', text, '--enter', '--json'] + if (codex) { + args.push('--wait-submit', '5') + } if (paired) { args = args.filter((arg) => arg !== '--enter') } @@ -207,13 +305,22 @@ it.each(['before', 'after', 'paired', 'orchestration'] as const)( expect(JSON.parse(output).result).toMatchObject( boundary === 'orchestration' ? { injected: true } : { send: { accepted: true } } ) - expect(writes).toEqual(paired ? ['first'] : [expect.stringContaining('first'), '\r']) + expect(writes).toEqual( + paired + ? ['first'] + : codex + ? [expect.stringContaining(text), '\r', '\r'] + : [expect.stringContaining('first'), '\r'] + ) + if (codex) { + expect(held).toBe(false) + expect(JSON.parse(output).result.send.prompt.stages).toContain('turn_started') + } expect(paired ? rawAdmission : promptAdmission).toHaveBeenCalledWith( runtime, paired ? 'programmatic' : 'pty-prompt', - undefined + codex ? 'programmatic' : undefined ) - expect(providerWrite).toHaveBeenCalledTimes(writes.length) expect(runtime.getDriver('pty-prompt')).toEqual({ kind: 'mobile', clientId: 'phone-A' }) return } @@ -251,6 +358,7 @@ it.each(['before', 'after', 'paired', 'orchestration'] as const)( } await exited await server?.stop() + hookServer?.stop() await transport.stop() await Promise.allSettled(done) db.close() diff --git a/src/main/runtime/session-construction/agent-session/get-agent-session-execution-namespace.ts b/src/main/runtime/session-construction/agent-session/get-agent-session-execution-namespace.ts index cbf30467955..f8d82f45512 100644 --- a/src/main/runtime/session-construction/agent-session/get-agent-session-execution-namespace.ts +++ b/src/main/runtime/session-construction/agent-session/get-agent-session-execution-namespace.ts @@ -18,6 +18,10 @@ import { agentLaunchQuotingShell } from '../../../../shared/agent-launch/startup import { OrchestrationError } from '../../orchestration/orchestration-error' import { withAgentSessionAccountEnv } from '../../agent-session/agent-session-account-env' import { resolveWslHostAgentResumeInputs } from '../../terminal/launch/wsl-host-agent-terminal-launch' +import type { SleepingAgentLaunchConfig } from '../../../../shared/agent-session/lifecycle/agent-session-resume' +import { isTerminalLeafId, makePaneKey } from '../../../../shared/workspace-layout/stable-pane-id' +import { resolveSavedRestoreLaunchRecipe } from './saved-restore-launch-recipe' +import { worktreeIdsEqual } from '../../../../shared/worktree/id' import { resolveTuiAgentLaunchRawArgs, resolveTuiAgentLaunchEnv @@ -86,6 +90,34 @@ export abstract class OrcaRuntimeWithGetAgentSessionExecutionNamespace extends O return Object.keys(options).length > 0 ? options : undefined } + /** A saved restore replays the source pane as this host ran it; History and other user resumes keep current settings. */ + private savedRestoreRecipe( + worktreeId: string, + request: Extract + ): SleepingAgentLaunchConfig { + // Why the source pane: a Wake opens a new tab, so `placement` names a pane that never ran it. + const { tabId, leafId } = request.placement ?? {} + const sourcePaneKey = + request.restoreSourcePaneKey ?? + // A legacy non-UUID leaf names no saved record, so it gets the no-note rule instead of throwing. + (tabId && !tabId.includes(':') && leafId && isTerminalLeafId(leafId) + ? makePaneKey(tabId, leafId) + : null) + const record = sourcePaneKey + ? this.getWorkspaceSessionForWorktree(worktreeId)?.sleepingAgentSessionsByPaneKey?.[ + sourcePaneKey + ] + : undefined + return resolveSavedRestoreLaunchRecipe({ + agent: request.agent, + providerSession: request.providerSession, + record: record && worktreeIdsEqual(record.worktreeId, worktreeId) ? record : undefined, + liveRow: sourcePaneKey + ? this.getAgentProviderSessionSnapshotFn?.().find((row) => row.paneKey === sourcePaneKey) + : undefined + }) + } + async ensureAgentSession( request: RuntimeEnsureAgentSessionRequest, _caller: RuntimeAgentSessionRpcCaller = {} @@ -134,17 +166,21 @@ export abstract class OrcaRuntimeWithGetAgentSessionExecutionNamespace extends O const shell = agentLaunchQuotingShell(launchRuntime) // Use the resolved PTY route, not display repo metadata that may belong to another host. const isRemote = Boolean(workspace.connectionId) + const restored = + request.resumeOrigin === 'saved' ? this.savedRestoreRecipe(workspace.id, request) : undefined const startup = buildAgentResumeStartupPlan( resolveWslHostAgentResumeInputs({ agent: request.agent, providerSession: identity.providerSession, cmdOverrides: settings.agentCmdOverrides ?? {}, - agentArgs: - request.agentArgs !== undefined - ? request.agentArgs - : resolveTuiAgentLaunchRawArgs(request.agent, settings, undefined, shell), - agentEnv: resolveTuiAgentLaunchEnv(request.agent, settings), - ompResumeFilePath: request.ompResumeFilePath, + ...(restored ?? { + agentArgs: + request.agentArgs !== undefined + ? request.agentArgs + : resolveTuiAgentLaunchRawArgs(request.agent, settings, undefined, shell), + agentEnv: resolveTuiAgentLaunchEnv(request.agent, settings) + }), + ompResumeFilePath: restored?.ompResumeFilePath ?? request.ompResumeFilePath, sessionOptions: this.toAgentSessionOptions(request.launchPreferences), sessionOptionsOverrideAgentArgs: Boolean(request.launchPreferences), platform, diff --git a/src/main/runtime/session-construction/agent-session/saved-restore-launch-recipe.ts b/src/main/runtime/session-construction/agent-session/saved-restore-launch-recipe.ts new file mode 100644 index 00000000000..098541f2132 --- /dev/null +++ b/src/main/runtime/session-construction/agent-session/saved-restore-launch-recipe.ts @@ -0,0 +1,35 @@ +import type { TuiAgent } from '../../../../shared/agent-launch/catalog/tui-agent' +import { resolveResumeLaunchRecipe } from '../../../../shared/agent-launch/choices/resume-launch-recipe' +import { + agentProviderSessionsEqual, + normalizeAgentProviderSession, + type AgentProviderSessionMetadata, + type SleepingAgentLaunchConfig, + type SleepingAgentSessionRecord +} from '../../../../shared/agent-session/lifecycle/agent-session-resume' +import type { AgentStatusIpcPayload } from '../../../../shared/agent-status/contracts/agent-status-ipc-payload' + +/** The source pane's note on this host, else the unrecorded rule; never current settings or client Arguments. */ +export function resolveSavedRestoreLaunchRecipe(args: { + agent: TuiAgent + providerSession: AgentProviderSessionMetadata + /** The source pane's record and latest status on this host. */ + record: SleepingAgentSessionRecord | undefined + liveRow: AgentStatusIpcPayload | undefined +}): SleepingAgentLaunchConfig { + const { agent } = args + const sameSession = (session: AgentProviderSessionMetadata | null | undefined) => + session && agentProviderSessionsEqual(agent, session, args.providerSession) ? session : null + const recorded = + args.record?.agent === agent && sameSession(args.record.providerSession) + ? args.record + : undefined + if (recorded?.launchConfig) { + return resolveResumeLaunchRecipe(recorded.launchConfig, agent, recorded.providerSession) + } + const live = + args.liveRow?.agentType === agent + ? sameSession(normalizeAgentProviderSession(args.liveRow.providerSession)) + : null + return resolveResumeLaunchRecipe(undefined, agent, live ?? recorded?.providerSession) +} diff --git a/src/main/runtime/session-lifecycle/projection/frame-agent-status.ts b/src/main/runtime/session-lifecycle/projection/frame-agent-status.ts index afcb22ef8e3..a3288f132ae 100644 --- a/src/main/runtime/session-lifecycle/projection/frame-agent-status.ts +++ b/src/main/runtime/session-lifecycle/projection/frame-agent-status.ts @@ -1,18 +1,22 @@ import type { AgentStatusEntry } from '../../../../shared/agent-status/contracts/agent-status-types' +import { launchStatement, type PtyLaunchFacts } from '../../terminal/launch/launch-evidence' /** A pane's projected agent status as a session-tabs frame publishes it. The frame names its * worktree on every agent: a restored row without a live PTY names none, and clients decide row - * ownership (and retraction) by worktree. The turn stamp travels on the tab, not the status. */ + * ownership (and retraction) by worktree. The turn stamp and owner's launch fact travel on the tab. */ export function frameAgentStatus( projected: (AgentStatusEntry & { turnCompletedAt?: number }) | undefined, - worktreeId: string -): { agentStatus?: AgentStatusEntry; projectedTurnCompletedAt?: number } { + worktreeId: string, + pty?: PtyLaunchFacts | null +): { agentStatus?: AgentStatusEntry; projectedTurnCompletedAt?: number; launchAttested?: boolean } { if (!projected) { return {} } const { turnCompletedAt, ...fields } = projected + const statement = pty ? launchStatement(pty, fields.agentType) : undefined return { agentStatus: { ...fields, worktreeId }, + ...(statement !== undefined ? { launchAttested: statement !== null } : {}), ...(turnCompletedAt !== undefined ? { projectedTurnCompletedAt: turnCompletedAt } : {}) } } diff --git a/src/main/runtime/session-lifecycle/projection/resume-agent-session-in-workspace.ts b/src/main/runtime/session-lifecycle/projection/resume-agent-session-in-workspace.ts index 8d513434fe8..d7e55a61796 100644 --- a/src/main/runtime/session-lifecycle/projection/resume-agent-session-in-workspace.ts +++ b/src/main/runtime/session-lifecycle/projection/resume-agent-session-in-workspace.ts @@ -3,7 +3,8 @@ import { makePaneKey } from '../../../../shared/workspace-layout/stable-pane-id' import { agentProviderSessionsEqual, normalizeAgentProviderSession, - type AgentProviderSessionMetadata + type AgentProviderSessionMetadata, + type SleepingAgentLaunchConfig } from '../../../../shared/agent-session/lifecycle/agent-session-resume' import type { TuiAgent } from '../../../../shared/agent-launch/catalog/tui-agent' import { @@ -46,6 +47,8 @@ export abstract class OrcaRuntimeWithResumeAgentSessionInWorkspace extends OrcaR captureHostId: ExecutionHostId /** Names the new pane, so a retry of one request reconciles the pane it may have started. */ mutationId: string + /** The caller's complete recipe; without one, the conversation's own pane record supplies it. */ + launchConfig?: SleepingAgentLaunchConfig signal?: AbortSignal } ): Promise { @@ -91,7 +94,12 @@ export abstract class OrcaRuntimeWithResumeAgentSessionInWorkspace extends OrcaR private async resumeClaimedAgentSession( worktreeId: string, session: ResumedAgentSession, - request: { captureHostId: ExecutionHostId; mutationId: string; signal?: AbortSignal }, + request: { + captureHostId: ExecutionHostId + mutationId: string + launchConfig?: SleepingAgentLaunchConfig + signal?: AbortSignal + }, create: { identity: { tabId: string; leafId: string } onPtySpawnDispatched: () => void @@ -145,7 +153,7 @@ export abstract class OrcaRuntimeWithResumeAgentSessionInWorkspace extends OrcaR try { resume = this.resolveMobileSessionResumeCommand(workspace, { ...session, - launchConfig: record?.launchConfig + launchConfig: request.launchConfig ?? record?.launchConfig }) } catch (error) { if (error instanceof WslHostWindowsOnlyAgentError) { diff --git a/src/main/runtime/session-lifecycle/projection/runtime-mobile-session-projection.ts b/src/main/runtime/session-lifecycle/projection/runtime-mobile-session-projection.ts index 50d798486ab..b03a60a4f42 100644 --- a/src/main/runtime/session-lifecycle/projection/runtime-mobile-session-projection.ts +++ b/src/main/runtime/session-lifecycle/projection/runtime-mobile-session-projection.ts @@ -270,7 +270,8 @@ export function projectRuntimeMobileSessionTabs( ) const { projectedTurnCompletedAt, ...clientAgentStatus } = frameAgentStatus( projectedAgentStatus.agentStatus, - snapshot.worktree + snapshot.worktree, + terminalPty ) const rawTurnCompletedAt = hookAgentStatus?.live?.payload.turnCompletedAt ?? diff --git a/src/main/runtime/terminal/identity/terminal-identity-source.ts b/src/main/runtime/terminal/identity/terminal-identity-source.ts index 657319a8f16..dfcd5dcaf90 100644 --- a/src/main/runtime/terminal/identity/terminal-identity-source.ts +++ b/src/main/runtime/terminal/identity/terminal-identity-source.ts @@ -6,6 +6,7 @@ import type { import type { TerminalHandleRecord, PtyIncarnationHandleRecord } from './terminal-handle-records' import { TerminalIncarnationCandidates } from './terminal-incarnation-candidates' +import { retireReplacedLaunch } from '../launch/launch-evidence' type Mutable = { -readonly [K in keyof T]: T[K] } /** Canonical terminal facts; semantic operations retain selection, callbacks and failure policy. */ @@ -82,6 +83,7 @@ export class TerminalIdentitySource { } setPtyIncarnationId(record: RuntimePtyWorktreeRecord, value: string | null): void { + retireReplacedLaunch(record, value) this.setPtyField(record, 'incarnationId', value) } diff --git a/src/main/runtime/terminal/launch/launch-evidence.test.ts b/src/main/runtime/terminal/launch/launch-evidence.test.ts new file mode 100644 index 00000000000..b9f60dce680 --- /dev/null +++ b/src/main/runtime/terminal/launch/launch-evidence.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from 'vitest' +import { heldLaunchConfig, type PtyLaunchFacts } from './launch-evidence' + +const config = { agentArgs: '--dangerously-skip-permissions', agentEnv: { FOO: '1' } } + +function heldAs(agent: 'claude' | 'claude-agent-teams'): PtyLaunchFacts { + return { + incarnationId: 'p1', + launchEvidence: { kind: 'held', incarnationId: 'p1', agent, token: 'token', config } + } +} + +describe('heldLaunchConfig', () => { + it('keeps a Claude agent teams launch note when its hook reports claude', () => { + expect(heldLaunchConfig(heldAs('claude-agent-teams'), 'claude')).toEqual(config) + }) + + it('gives a different agent typed into the pane no note', () => { + expect(heldLaunchConfig(heldAs('claude'), 'codex')).toBeUndefined() + }) +}) diff --git a/src/main/runtime/terminal/launch/launch-evidence.ts b/src/main/runtime/terminal/launch/launch-evidence.ts new file mode 100644 index 00000000000..0a0667d9788 --- /dev/null +++ b/src/main/runtime/terminal/launch/launch-evidence.ts @@ -0,0 +1,118 @@ +import type { TuiAgent } from '../../../../shared/agent-launch/catalog/tui-agent' +import type { SleepingAgentLaunchConfig } from '../../../../shared/agent-session/lifecycle/agent-session-resume' +import { sameAgentIdentity } from '../../../../shared/agent-status/title/agent-title-owner' +import type { PtyIncarnationId } from '../../../../shared/terminal/pty/lifecycle/pty-incarnation' +import type { DaemonLaunchNote } from '../../../daemon/protocol/daemon-launch-note-protocol' + +/** Main's word on whether a PTY's process still runs an Orca launch. Always replaced whole, so its + * identity fences an async check; never persisted, so a main restart starts unknown. */ +export type LaunchEvidence = + | { readonly kind: 'unknown' } + | { readonly kind: 'gone'; readonly incarnationId: PtyIncarnationId | null } + | { + readonly kind: 'held' + readonly incarnationId: PtyIncarnationId | null + readonly agent: TuiAgent + readonly token: string + readonly config: SleepingAgentLaunchConfig + } + +export type PtyLaunchFacts = { + incarnationId: PtyIncarnationId | null + launchEvidence?: LaunchEvidence + launchConfig?: SleepingAgentLaunchConfig | null +} + +type HeldLaunch = { agent: TuiAgent; token: string; config: SleepingAgentLaunchConfig } + +/** The recipe of the Orca launch running in this PTY's process as `agent`; a typed agent gets none. + * A wrapper launch (Claude agent teams) reports its wrapped agent, which is still that launch. */ +export function heldLaunchConfig( + pty: PtyLaunchFacts | null | undefined, + agent: string | null | undefined +): SleepingAgentLaunchConfig | undefined { + const evidence = pty?.launchEvidence + return evidence?.kind === 'held' && + // A launch recorded before its process id was known belongs to the process until a replacement. + (evidence.incarnationId === null || evidence.incarnationId === pty?.incarnationId) && + agent && + sameAgentIdentity(evidence.agent, agent) + ? evidence.config + : undefined +} + +/** Main's word on this PTY's launch as `agent`: its recipe, null when none runs, undefined when main + * can't tell, so readers keep their earlier rule. */ +export function launchStatement( + pty: PtyLaunchFacts | null | undefined, + agent: string | null | undefined +): SleepingAgentLaunchConfig | null | undefined { + const kind = pty?.launchEvidence?.kind ?? 'unknown' + return kind === 'unknown' ? undefined : (heldLaunchConfig(pty, agent) ?? null) +} + +export function holdLaunch( + pty: PtyLaunchFacts, + incarnationId: PtyIncarnationId | null, + launch: HeldLaunch +): void { + pty.launchEvidence = { kind: 'held', incarnationId, ...launch } +} + +/** A freshly started process: its own launch is held, and any other process runs none. */ +export function recordFreshLaunch(pty: PtyLaunchFacts, launch: Partial = {}): void { + const { agent, token, config } = launch + pty.launchEvidence = + agent && token && config + ? { kind: 'held', incarnationId: pty.incarnationId, agent, token, config } + : { kind: 'gone', incarnationId: pty.incarnationId } +} + +/** A finished command or confirmed exit; sticky for this process until a fresh launch. */ +export function retireLaunch( + pty: PtyLaunchFacts, + incarnationId: PtyIncarnationId | null = pty.incarnationId +): void { + pty.launchEvidence = { kind: 'gone', incarnationId } + pty.launchConfig = null +} + +/** Learning the first id of the same process binds its evidence; a new process can't still run its + * predecessor's launch, and the newcomer establishes its own. */ +export function retireReplacedLaunch(pty: PtyLaunchFacts, next: PtyIncarnationId | null): void { + const previous = pty.incarnationId + const evidence = pty.launchEvidence + if (!evidence || !next || previous === next) { + return + } + if (!previous) { + if (evidence.kind !== 'unknown' && evidence.incarnationId === null) { + pty.launchEvidence = { ...evidence, incarnationId: next } + } + return + } + if (evidence.kind === 'unknown' || evidence.incarnationId !== next) { + retireLaunch(pty, previous) + } +} + +/** A reattach to process `incarnationId` can't revive or replace what main proved about it. Else a + * retired note proves the launch gone; anything else is unknown, returned when a foreground check + * of the note may settle it. */ +export function recordReattachedLaunch( + pty: PtyLaunchFacts, + incarnationId: PtyIncarnationId | undefined, + note: DaemonLaunchNote | null | undefined +): LaunchEvidence | null { + const current = pty.launchEvidence + if (current && current.kind !== 'unknown' && current.incarnationId === (incarnationId ?? null)) { + return null + } + if (note === null && incarnationId) { + retireLaunch(pty, incarnationId) + return null + } + const pending: LaunchEvidence = { kind: 'unknown' } + pty.launchEvidence = pending + return note && incarnationId ? pending : null +} diff --git a/src/main/runtime/terminal/launch/mobile-terminal-owned-create.ts b/src/main/runtime/terminal/launch/mobile-terminal-owned-create.ts index 137fdc576ee..5f2c779aee6 100644 --- a/src/main/runtime/terminal/launch/mobile-terminal-owned-create.ts +++ b/src/main/runtime/terminal/launch/mobile-terminal-owned-create.ts @@ -181,7 +181,12 @@ export abstract class OrcaRuntimeWithCreateRuntimeOwnedMobileSessionTerminal ext } if (recipe) { livePty.pty.launchAgent ??= recipe.launchAgent ?? null - if (!livePty.pty.launchConfig && recipe.launchConfig) { + // A finished launch's recipe must not return to the shell left behind. + if ( + !livePty.pty.launchConfig && + recipe.launchConfig && + livePty.pty.launchEvidence?.kind !== 'gone' + ) { livePty.pty.launchConfig = copySleepingAgentLaunchConfig(recipe.launchConfig) } } diff --git a/src/main/runtime/terminal/launch/mobile-terminal-startup.ts b/src/main/runtime/terminal/launch/mobile-terminal-startup.ts index c22235ecf6a..b9d77875ad8 100644 --- a/src/main/runtime/terminal/launch/mobile-terminal-startup.ts +++ b/src/main/runtime/terminal/launch/mobile-terminal-startup.ts @@ -11,7 +11,7 @@ import { import { isTuiAgentEnabled } from '../../../../shared/agent-launch/catalog/tui-agent-selection' import { buildAgentResumeStartupPlan } from '../../../../shared/agent-launch/startup/tui-agent-resume-startup' import { buildAgentStartupPlan } from '../../../../shared/agent-launch/startup/tui-agent-startup' -import { resolveResumeLaunchRecipe } from '../../../../shared/agent-launch/choices/tui-agent-launch-defaults' +import { resolveResumeLaunchRecipe } from '../../../../shared/agent-launch/choices/resume-launch-recipe' import { resolveAgentStartupPlanInputs } from '../../../../shared/agent-launch/startup/agent-startup-plan-inputs' import { resolveWslHostAgentLaunchInputs, @@ -91,7 +91,7 @@ export abstract class OrcaRuntimeWithResolveMobileSessionTerminalCommand extends agent: record.agent, providerSession, cmdOverrides: settings.agentCmdOverrides ?? {}, - ...resolveResumeLaunchRecipe(launchConfig), + ...resolveResumeLaunchRecipe(launchConfig, record.agent, providerSession), platform: launchRuntime.platform, shell: agentLaunchQuotingShell(launchRuntime), isRemote: Boolean(workspace.connectionId), diff --git a/src/main/runtime/terminal/launch/terminal-create.ts b/src/main/runtime/terminal/launch/terminal-create.ts index 30e41793000..ecb46eb52ee 100644 --- a/src/main/runtime/terminal/launch/terminal-create.ts +++ b/src/main/runtime/terminal/launch/terminal-create.ts @@ -243,8 +243,8 @@ export abstract class OrcaRuntimeWithCreateTerminal extends OrcaRuntimeWithTermi this.setPtyManagementTitleFromObservedTitle(pty, title, at) ) } - this.watchLaunchComposer( - result.id, + this.watchLaunchedAgent( + pty, recordTerminalLaunchMetadata(pty, result, launch, launchOpts.command) ) if (!launch.adoptedStablePane) { diff --git a/src/main/runtime/terminal/launch/terminal-launch-result.ts b/src/main/runtime/terminal/launch/terminal-launch-result.ts index f58ffddb42b..d62b1cd7ac2 100644 --- a/src/main/runtime/terminal/launch/terminal-launch-result.ts +++ b/src/main/runtime/terminal/launch/terminal-launch-result.ts @@ -4,6 +4,7 @@ import type { RuntimePtyController } from '../lifecycle/runtime-pty-controller-c import type { TerminalCreateOptions } from './terminal-create-options' import type { RuntimePtyWorktreeRecord } from '../records/runtime-terminal-state-records' import type { TuiAgent } from '../../../../shared/agent-launch/catalog/tui-agent' +import { recordFreshLaunch } from './launch-evidence' import { copySleepingAgentLaunchConfig, resolveLaunchCommandPaneAgent, @@ -54,8 +55,8 @@ export function recordTerminalLaunchMetadata( command: string | undefined ): TuiAgent | null { if (launch.adoptedStablePane) { - // A lower owner can supply facts before this runtime has a launch record. - if (!pty.launchConfig && result.launchConfig) { + // A lower owner can supply facts before this runtime has a launch record, unless its launch ended. + if (!pty.launchConfig && result.launchConfig && pty.launchEvidence?.kind !== 'gone') { pty.launchConfig = copySleepingAgentLaunchConfig(result.launchConfig) } pty.launchToken ??= result.launchToken ?? null @@ -69,6 +70,11 @@ export function recordTerminalLaunchMetadata( pty.launchToken = launch.launchToken ?? null pty.launchIncarnationId = launch.launchToken ? pty.incarnationId : null pty.launchAgent = launch.launchAgent ?? null + recordFreshLaunch(pty, { + agent: pty.launchAgent ?? undefined, + token: pty.launchToken ?? undefined, + config: pty.launchConfig ?? undefined + }) if (result.agentResumeRefused) { pty.launchCommandAgent = null return null diff --git a/src/main/runtime/terminal/launch/terminal-split-in-main.ts b/src/main/runtime/terminal/launch/terminal-split-in-main.ts index dc6d2908246..c4bb3129b04 100644 --- a/src/main/runtime/terminal/launch/terminal-split-in-main.ts +++ b/src/main/runtime/terminal/launch/terminal-split-in-main.ts @@ -14,6 +14,7 @@ import { isTerminalLeafId, makePaneKey } from '../../../../shared/workspace-layo import { randomUUID } from 'node:crypto' import { paneStartFailure } from '../surface/pane-start-statuses' import { collectLeafIds } from '../../../../shared/workspace-layout/terminal-pane-layout-tree' +import { recordFreshLaunch } from './launch-evidence' export type SplitPaneSource = { worktreeId: string; tabId: string; leafId: string } @@ -138,6 +139,9 @@ export abstract class OrcaRuntimeWithSplitPaneInMain extends OrcaRuntimeWithSpli if (!createdPty) { throw new Error('terminal_exited') } + if (!result.isReattach) { + recordFreshLaunch(createdPty) + } recordPtySurface( this.terminalIdentity, createdPty, diff --git a/src/main/runtime/terminal/launch/terminal-split-surface.ts b/src/main/runtime/terminal/launch/terminal-split-surface.ts index b5abf1ed4a8..04ff13386d7 100644 --- a/src/main/runtime/terminal/launch/terminal-split-surface.ts +++ b/src/main/runtime/terminal/launch/terminal-split-surface.ts @@ -14,6 +14,7 @@ import type { Worktree } from '../../../../shared/worktree/types' import { spawnInAdmittedPane, withdrawRuntimePane } from './runtime-pane-admission' import { UNMEASURED_PTY_SIZE } from '../../../ipc/pty/pty-first-size' import { resolveLaunchCommandPaneAgent } from './runtime-agent-launch-resolution' +import { recordFreshLaunch } from './launch-evidence' // Rejected splits need only a quick teardown verdict before fallback kill; unreachable hosts must not stall rejection. const REJECTED_SPLIT_PTY_STOP_TIMEOUT_MS = 2_000 @@ -129,6 +130,9 @@ export abstract class OrcaRuntimeWithSplitPtyBackedTerminal extends OrcaRuntimeW ) createdPty.runtimeSessionOwned = pty.runtimeSessionOwned createdPty.launchCommandAgent = resolveLaunchCommandPaneAgent(opts.command) + if (!result.isReattach) { + recordFreshLaunch(createdPty) + } this.watchLaunchComposer(result.id, createdPty.launchCommandAgent) this.setPairedRendererSessionOwnership( createdPty.ptyId, diff --git a/src/main/runtime/terminal/lifecycle/daemon-launch-note.ts b/src/main/runtime/terminal/lifecycle/daemon-launch-note.ts new file mode 100644 index 00000000000..59d596a4379 --- /dev/null +++ b/src/main/runtime/terminal/lifecycle/daemon-launch-note.ts @@ -0,0 +1,90 @@ +import type { TuiAgent } from '../../../../shared/agent-launch/catalog/tui-agent' +import type { PtyIncarnationId } from '../../../../shared/terminal/pty/lifecycle/pty-incarnation' +import type { DaemonLaunchNote } from '../../../daemon/protocol/daemon-launch-note-protocol' +import type { LaunchedAgentForeground } from '../wait/launched-agent-foreground' +import type { RuntimePtyWorktreeRecord } from '../records/runtime-terminal-state-records' +import { + holdLaunch, + recordReattachedLaunch, + retireLaunch, + type LaunchEvidence +} from '../launch/launch-evidence' +import { OrcaRuntimeWithInvalidateAllHandlesForPty } from './pty-handle-invalidation' + +/** Main's admitted launch, kept in the daemon so a reattach after a main restart can re-arm it. */ +export abstract class OrcaRuntimeWithDaemonLaunchNote extends OrcaRuntimeWithInvalidateAllHandlesForPty { + abstract readLaunchedAgentForeground( + ptyId: string, + agent: TuiAgent + ): Promise + + protected abstract watchLaunchComposer(ptyId: string, agent: TuiAgent | null): void + + /** Watch a launched agent's composer and keep its launch in the daemon. */ + protected watchLaunchedAgent(pty: RuntimePtyWorktreeRecord, agent: TuiAgent | null): void { + this.watchLaunchComposer(pty.ptyId, agent) + this.armDaemonLaunchNote(pty) + } + + /** Keep main's held launch in the daemon, which outlives main. */ + protected armDaemonLaunchNote(pty: RuntimePtyWorktreeRecord): void { + const evidence = pty.launchEvidence + if (evidence?.kind === 'held') { + this.ptyController?.setLaunchNote?.(pty.ptyId, { + arm: { launchToken: evidence.token, launchConfig: evidence.config } + }) + } + } + + /** A finished command or confirmed exit; the daemon outlives main, so a reattach must not re-arm it. */ + protected retireHeldLaunch(ptyId: string): RuntimePtyWorktreeRecord | undefined { + const pty = this.ptysById.get(ptyId) + if (pty?.launchEvidence?.kind === 'held') { + this.ptyController?.setLaunchNote?.(ptyId, { retire: pty.launchEvidence.token }) + } + if (pty) { + retireLaunch(pty) + } + return pty + } + + /** An unretired note is held only while its agent still holds the foreground. Main can't tell when + * the host keeps no notes (an older daemon) or the foreground is unreadable (Windows). */ + protected recordReattachedLaunch( + pty: RuntimePtyWorktreeRecord, + incarnationId: PtyIncarnationId | undefined, + note: DaemonLaunchNote | null | undefined, + agent: TuiAgent | undefined + ): void { + const pending = recordReattachedLaunch(pty, incarnationId, note) + if (pending && note && incarnationId && agent) { + void this.checkReattachedLaunch(pty, pending, incarnationId, note, agent) + } + } + + private async checkReattachedLaunch( + pty: RuntimePtyWorktreeRecord, + pending: LaunchEvidence, + incarnationId: PtyIncarnationId, + note: DaemonLaunchNote, + agent: TuiAgent + ): Promise { + const foreground = await this.readLaunchedAgentForeground(pty.ptyId, agent).catch( + () => 'unknown' as const + ) + // Newer evidence (a later attach, a finished command, a fresh launch) outranks this read. + if ( + pty.launchEvidence !== pending || + this.ptysById.get(pty.ptyId) !== pty || + pty.incarnationId !== incarnationId + ) { + return + } + if (foreground === 'shell') { + this.ptyController?.setLaunchNote?.(pty.ptyId, { retire: note.launchToken }) + retireLaunch(pty) + } else if (foreground === 'agent' && pty.launchAgent === agent) { + holdLaunch(pty, incarnationId, { agent, token: note.launchToken, config: note.launchConfig }) + } + } +} diff --git a/src/main/runtime/terminal/lifecycle/pty-registration.ts b/src/main/runtime/terminal/lifecycle/pty-registration.ts index 95240176d04..7c2238096f5 100644 --- a/src/main/runtime/terminal/lifecycle/pty-registration.ts +++ b/src/main/runtime/terminal/lifecycle/pty-registration.ts @@ -3,9 +3,12 @@ import { terminalExitError } from '../../../../shared/terminal/pty/lifecycle/terminal-start-failure' import type { PtySpawnResult } from '../../../providers/pty-spawn-result' -import { OrcaRuntimeWithInvalidateAllHandlesForPty } from './pty-handle-invalidation' import type { PtyIncarnationId } from '../../../../shared/terminal/pty/lifecycle/pty-incarnation' import type { TuiAgent } from '../../../../shared/agent-launch/catalog/tui-agent' +import type { SleepingAgentLaunchConfig } from '../../../../shared/agent-session/lifecycle/agent-session-resume' +import { recordFreshLaunch, retireLaunch } from '../launch/launch-evidence' +import type { DaemonLaunchNote } from '../../../daemon/protocol/daemon-launch-note-protocol' +import { OrcaRuntimeWithDaemonLaunchNote } from './daemon-launch-note' import { isValidTerminalTabId } from '../../../../shared/workspace-layout/terminal-tab-id' import { isTerminalLeafId, makePaneKey } from '../../../../shared/workspace-layout/stable-pane-id' import { isTuiAgent } from '../../../../shared/agent-launch/catalog/tui-agent-config' @@ -20,7 +23,7 @@ import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../../../shared/layout-owner-switc import { mainTerminalRegistered } from '../surface/main-terminal-surfaces' import { assertPaneBoundAtRegistration, isRuntimeHostedWorkspace } from './bind-first-registration' -export abstract class OrcaRuntimeWithRegisterPty extends OrcaRuntimeWithInvalidateAllHandlesForPty { +export abstract class OrcaRuntimeWithRegisterPty extends OrcaRuntimeWithDaemonLaunchNote { protected abstract ensurePtyBackedMobileSurfaceForRendererTab( worktreeId: string, tabId: string @@ -42,13 +45,21 @@ export abstract class OrcaRuntimeWithRegisterPty extends OrcaRuntimeWithInvalida incarnationId?: PtyIncarnationId /** Handle allocated for the replacement incarnation, when one is known. */ terminalHandle?: string - agentLaunchAuthority?: { launchToken: string; launchAgent: TuiAgent } + agentLaunchAuthority?: { + launchToken: string + launchAgent: TuiAgent + launchConfig?: SleepingAgentLaunchConfig + } /** The agent a fresh spawn's launch command starts, whose submits wait for its composer. */ launchedAgent?: TuiAgent providerReattachLaunchIdentity?: { incarnationId: PtyIncarnationId launchAgent: TuiAgent } + /** A newly started process, which runs no launch but an admitted one. */ + freshSpawn?: true + /** A reattach, with its host's launch note; none when the host keeps no notes (an older daemon, SSH). */ + reattach?: { launchNote?: DaemonLaunchNote | null } }, isWsl?: boolean ): void { @@ -153,9 +164,18 @@ export abstract class OrcaRuntimeWithRegisterPty extends OrcaRuntimeWithInvalida pty.launchToken = agentLaunchAuthority.launchToken pty.launchIncarnationId = binding.incarnationId pty.launchAgent = agentLaunchAuthority.launchAgent + recordFreshLaunch(pty, { + agent: agentLaunchAuthority.launchAgent, + token: agentLaunchAuthority.launchToken, + config: agentLaunchAuthority.launchConfig + }) + this.armDaemonLaunchNote(pty) + } else if (binding?.freshSpawn && pty.incarnationId === (binding.incarnationId ?? null)) { + retireLaunch(pty) } this.watchLaunchComposer(ptyId, binding?.launchedAgent ?? null) const providerReattachLaunchIdentity = binding?.providerReattachLaunchIdentity + let reattachedAgent: TuiAgent | undefined if ( providerReattachLaunchIdentity && paneKey && @@ -165,7 +185,12 @@ export abstract class OrcaRuntimeWithRegisterPty extends OrcaRuntimeWithInvalida isTuiAgent(providerReattachLaunchIdentity.launchAgent) ) { // Why: daemon metadata owns the surviving process; its incarnation fence restores identity without minting renderer launch authority. - pty.launchAgent = providerReattachLaunchIdentity.launchAgent + pty.launchAgent = reattachedAgent = providerReattachLaunchIdentity.launchAgent + } + // An older relay reports no incarnation; that still can't prove the launch gone. + if (binding?.reattach && (pty.incarnationId ?? undefined) === binding.incarnationId) { + const { launchNote } = binding.reattach + this.recordReattachedLaunch(pty, binding.incarnationId, launchNote, reattachedAgent) } const pendingIncarnation = this.pendingPtyRegistrationIncarnations.get(ptyId)?.incarnationId if ( diff --git a/src/main/runtime/terminal/lifecycle/runtime-pty-controller-contract.ts b/src/main/runtime/terminal/lifecycle/runtime-pty-controller-contract.ts index 6e9f11b90ac..f05674811c6 100644 --- a/src/main/runtime/terminal/lifecycle/runtime-pty-controller-contract.ts +++ b/src/main/runtime/terminal/lifecycle/runtime-pty-controller-contract.ts @@ -1,3 +1,4 @@ +import type { DaemonLaunchNoteChange } from '../../../daemon/protocol/daemon-launch-note-protocol' import type { TerminalLimitStatus, TerminalLimitUpdate @@ -172,6 +173,7 @@ export type RuntimePtyController = { supportsForegroundObservation?(ptyId: string): boolean confirmForegroundProcess?(ptyId: string): Promise confirmShellForeground?(ptyId: string): Promise + setLaunchNote?(ptyId: string, change: DaemonLaunchNoteChange): void hasChildProcesses?(ptyId: string): Promise clearBuffer?(ptyId: string): Promise resetInputModes?(ptyId: string): Promise diff --git a/src/main/runtime/terminal/output/terminal-orchestration-authority.ts b/src/main/runtime/terminal/output/terminal-orchestration-authority.ts index a399c440b34..7c350e70396 100644 --- a/src/main/runtime/terminal/output/terminal-orchestration-authority.ts +++ b/src/main/runtime/terminal/output/terminal-orchestration-authority.ts @@ -183,7 +183,7 @@ export abstract class OrcaRuntimeWithGetOrchestrationDispatchAuthority extends O } protected retirePtyAgentLaunchAuthority(ptyId: string): void { - const pty = this.ptysById.get(ptyId) + const pty = this.retireHeldLaunch(ptyId) if (!pty) { return } diff --git a/src/main/runtime/terminal/records/runtime-terminal-state-records.ts b/src/main/runtime/terminal/records/runtime-terminal-state-records.ts index c16c1868f3b..0fdfbcbff13 100644 --- a/src/main/runtime/terminal/records/runtime-terminal-state-records.ts +++ b/src/main/runtime/terminal/records/runtime-terminal-state-records.ts @@ -18,6 +18,7 @@ import type { RetainedTailRedrawCursor } from '../output/tail/terminal-tail-redr import type { TerminalTailWaitState } from '../wait/terminal-wait-tail-state' import type { TerminalCommandPaint } from '../input/terminal-command-paint' import type { PtyShellOwnershipMirror } from '../query/pty-shell-ownership-mirror' +import type { LaunchEvidence } from '../launch/launch-evidence' import type { TerminalExitCause } from '../../../../shared/terminal/pty/lifecycle/terminal-exit-cause' import type { AgentSessionOwnerBinding } from '../../../../shared/agent-session/agent-session-host-authority' @@ -92,6 +93,8 @@ export type RuntimePtyWorktreeRecord = RuntimeTerminalTailState & { launchToken: string | null launchIncarnationId: PtyIncarnationId | null launchAgent: TuiAgent | null + /** Whether the process still runs an Orca launch; missing when main can't tell. */ + launchEvidence?: LaunchEvidence /** Identify flagged unmanaged --command panes before foreground reads; null for adopted/restored panes. */ launchCommandAgent: TuiAgent | null agentSessionOwners: AgentSessionOwnerBinding[] diff --git a/src/main/runtime/terminal/stop/hook-status-resume-records.ts b/src/main/runtime/terminal/stop/hook-status-resume-records.ts index 41c2635557c..e218127c9d1 100644 --- a/src/main/runtime/terminal/stop/hook-status-resume-records.ts +++ b/src/main/runtime/terminal/stop/hook-status-resume-records.ts @@ -9,6 +9,7 @@ import { type ResumeRecordTab } from '../../../../shared/agent-session/lifecycle/agent-resume-record-from-status' import { + isPaneSleepRecord, normalizeAgentProviderSession, type SleepingAgentLaunchConfig, type SleepingAgentSessionRecord @@ -22,11 +23,38 @@ import type { WorkspaceSessionState } from '../../../../shared/workspace-session /** The part of a pane's previous status the next one may inherit its provider session from. */ export type PreviousHookStatus = Pick -/** A pane's agent launch recipe, when its terminal was launched as `agent`. */ +/** Main's word on a pane's launch as `agent`: its note, null for none, undefined when main can't tell. */ export type PaneLaunchConfigOf = ( paneKey: string, agent: ResumeRecordStatus['agentType'] -) => SleepingAgentLaunchConfig | undefined +) => SleepingAgentLaunchConfig | null | undefined + +/** Main's word on the launch, else the pane's earlier record of this session when main can't tell. */ +function statusLaunchConfig( + statement: SleepingAgentLaunchConfig | null | undefined, + existing: SleepingAgentSessionRecord | undefined, + status: ResumeRecordStatus, + keepsEarlier = true +): SleepingAgentLaunchConfig | undefined { + return statement === undefined && keepsEarlier + ? recordLaunchConfigForStatus(existing, status) + : (statement ?? undefined) +} + +/** A sleep's snapshot of this session waits for its wake; a status from no Orca launch neither + * replaces it nor inherits its recipe. */ +function keepsSleepSnapshot( + statement: SleepingAgentLaunchConfig | null | undefined, + existing: SleepingAgentSessionRecord | undefined, + status: ResumeRecordStatus +): boolean { + return ( + statement === null && + existing !== undefined && + isPaneSleepRecord(existing) && + recordLaunchConfigForStatus(existing, status) !== undefined + ) +} /** A hook row as the window's status entry reads it; null for rows that are not a pane's status. */ export function resumeStatusFromHookRow( @@ -80,17 +108,22 @@ export function hookStatusResumeCommand(args: { } const existing = session.sleepingAgentSessionsByPaneKey?.[status.paneKey] const retainsResumableIdentity = statusRetainsResumableIdentity(status, status.providerSession) + const statement = args.launchConfigOf(status.paneKey, status.agentType) + if (keepsSleepSnapshot(statement, existing, status)) { + return null + } const live = liveResumeRecord({ entry: status, retainsResumableIdentity, worktreeIdOf: () => worktreeId, tabOf: () => tab, capturedAt: status.updatedAt, - launchConfig: - args.launchConfigOf(status.paneKey, status.agentType) ?? - (status.state !== 'done' || retainsResumableIdentity - ? recordLaunchConfigForStatus(existing, status) - : undefined) + launchConfig: statusLaunchConfig( + statement, + existing, + status, + status.state !== 'done' || retainsResumableIdentity + ) }) const change = liveResumeRecordChange(existing, live) if (!change) { @@ -112,6 +145,10 @@ export function quitResumeRecords(args: { }): SleepingAgentSessionRecord[] { return args.statuses.flatMap((status) => { const existing = args.session.sleepingAgentSessionsByPaneKey?.[status.paneKey] + const statement = args.launchConfigOf(status.paneKey, status.agentType) + if (keepsSleepSnapshot(statement, existing, status)) { + return [] + } const tabFor = tabOf(args.session, status.paneKey) const record = capturedResumeRecord({ entry: status, @@ -120,9 +157,7 @@ export function quitResumeRecords(args: { capturedAt: args.capturedAt, worktreeIdOf: () => status.worktreeId ?? args.worktreeIdOf(status.paneKey), tabOf: tabFor, - launchConfigOf: () => - args.launchConfigOf(status.paneKey, status.agentType) ?? - recordLaunchConfigForStatus(existing, status) + launchConfigOf: () => statusLaunchConfig(statement, existing, status) }) // Only a pane this partition holds keeps a record here. return record && tabFor(record.worktreeId) ? [record] : [] diff --git a/src/main/runtime/terminal/stop/launch-evidence-resume-records.test.ts b/src/main/runtime/terminal/stop/launch-evidence-resume-records.test.ts new file mode 100644 index 00000000000..a6783edbad3 --- /dev/null +++ b/src/main/runtime/terminal/stop/launch-evidence-resume-records.test.ts @@ -0,0 +1,115 @@ +// A pane's saved recipe follows main's word on its launch, not the session the agent names. +import { describe, expect, it, vi } from 'vitest' +import type { AgentStatusIpcPayload } from '../../../../shared/agent-status/contracts/agent-status-ipc-payload' +import type { SleepingAgentSessionRecord } from '../../../../shared/agent-session/lifecycle/agent-session-resume' +import type { ExecutionHostId } from '../../../../shared/execution-host' +import type { LayoutApplyOutcome } from '../../../../shared/workspace-layout/workspace-layout-command-types' +import { applyCommandToSession } from '../../../persistence/terminal-topology/terminal-pane-admission' + +vi.mock('../../../../shared/layout-owner-switch', () => ({ ORCA_LAYOUT_OWNER_IS_MAIN: true })) + +// Preserve side-effect order: mocks, lifecycle, fixtures. +await import('../../orca-runtime-tests/runtime-test-module-registration.spec') +const { OrcaRuntimeService } = await import('../../orca-runtime') +await import('../../orca-runtime-tests/orca-runtime-test-lifecycle.spec') +const { + HEADLESS_LEAF_ID, + TEST_WORKTREE_ID, + makeRuntimeStoreWithWorkspaceSession, + makeWorkspaceSessionWithHeadlessTerminal +} = await import('../../orca-runtime-tests/orca-runtime-test-fixtures.spec') + +const PANE_KEY = `host-tab:${HEADLESS_LEAF_ID}` +const PTY_ID = 'persisted-pty' +const SESSION = { key: 'session_id' as const, id: 'claude-session-1' } +const RECIPE = { agentArgs: '--dangerously-skip-permissions', agentEnv: {} } + +function savedRecord(origin: SleepingAgentSessionRecord['origin']): SleepingAgentSessionRecord { + return { + paneKey: PANE_KEY, + tabId: 'host-tab', + worktreeId: TEST_WORKTREE_ID, + agent: 'claude', + providerSession: SESSION, + prompt: '', + state: 'done', + capturedAt: 5, + updatedAt: 5, + origin, + connectionId: null, + launchConfig: RECIPE + } +} + +function claudeRow(): AgentStatusIpcPayload { + return { + paneKey: PANE_KEY, + worktreeId: TEST_WORKTREE_ID, + agentType: 'claude', + providerSession: SESSION, + state: 'working', + prompt: 'keep going', + receivedAt: 10, + stateStartedAt: 10, + connectionId: null + } +} + +function makeRuntime(saved: SleepingAgentSessionRecord) { + const { runtimeStore, getSession, setSession } = makeRuntimeStoreWithWorkspaceSession( + makeWorkspaceSessionWithHeadlessTerminal({ + sleepingAgentSessionsByPaneKey: { [PANE_KEY]: saved } + }) + ) + const applyLayoutCommand = vi.fn( + async ( + command: Parameters[2], + hostIds: ExecutionHostId[] + ): Promise => { + for (const hostId of hostIds) { + const change = applyCommandToSession(hostId, getSession(), command) + if (change.session) { + setSession(change.session) + } + } + return { ok: true, result: {} } + } + ) + const runtime = new OrcaRuntimeService(Object.assign(runtimeStore, { applyLayoutCommand })) + runtime.syncWindowGraph(0, { tabs: [], leaves: [] }) + const binding = { tabId: 'host-tab', leafId: HEADLESS_LEAF_ID, incarnationId: 'p1' } + return { + runtime, + binding, + getSession, + savedAfterStatus: async () => { + runtime.recordHookStatusResume(claudeRow()) + await runtime['hookStatusResumeWrites'] + return getSession().sleepingAgentSessionsByPaneKey?.[PANE_KEY] + } + } +} + +describe('a pane saved recipe and the launch running in it', () => { + it('gives an agent typed into a fresh shell none of the session earlier recipe', async () => { + const { runtime, binding, savedAfterStatus } = makeRuntime(savedRecord('live')) + runtime.registerPty(PTY_ID, TEST_WORKTREE_ID, null, { ...binding, freshSpawn: true }) + const saved = await savedAfterStatus() + expect(saved?.state).toBe('working') + expect(saved?.launchConfig).toBeUndefined() + }) + + it('keeps a sleep snapshot for its wake without lending its recipe to a typed resume', async () => { + const { runtime, binding, getSession, savedAfterStatus } = makeRuntime( + savedRecord('worktree-sleep') + ) + runtime.registerPty(PTY_ID, TEST_WORKTREE_ID, null, { + ...binding, + agentLaunchAuthority: { launchToken: 'launch-1', launchAgent: 'claude', launchConfig: RECIPE } + }) + runtime.emitDaemonPtyTransientFact(PTY_ID, { kind: 'command-finished', exitCode: 0 }) + expect(await savedAfterStatus()).toEqual(savedRecord('worktree-sleep')) + Object.assign(runtime, { getAgentProviderSessionSnapshotFn: () => [claudeRow()] }) + expect(runtime.quitResumeRecordsFor(getSession())).toEqual([]) + }) +}) diff --git a/src/main/runtime/terminal/stop/worktree-sleep-resume-recording.ts b/src/main/runtime/terminal/stop/worktree-sleep-resume-recording.ts index 37ec1fbce3a..9b7d7761ff4 100644 --- a/src/main/runtime/terminal/stop/worktree-sleep-resume-recording.ts +++ b/src/main/runtime/terminal/stop/worktree-sleep-resume-recording.ts @@ -1,5 +1,7 @@ import { sleepingRecordsEquivalentIgnoringCaptureTime } from '../../../../shared/agent-session/lifecycle/agent-resume-record-equivalence' import { OrcaRuntimeWithStopTerminalsForWorktree } from './worktree-terminal-stop' +import { launchStatement } from '../launch/launch-evidence' +import type { RuntimePtyWorktreeRecord } from '../records/runtime-terminal-state-records' import { applyWorktreeSleepResumeRecords, captureWorktreeSleepResumeRecords, @@ -96,12 +98,7 @@ export abstract class OrcaRuntimeWithWorktreeSleepResumeRecording extends OrcaRu paneKeysByPtyId: new Map( [...ptyIds].map((ptyId) => [ptyId, this.collectAgentStatusPaneKeysForPty(ptyId)]) ), - launchByPtyId: new Map( - [...ptyIds].map((ptyId) => { - const pty = this.ptysById.get(ptyId) - return [ptyId, { agent: pty?.launchAgent ?? null, config: pty?.launchConfig ?? null }] - }) - ), + launchByPtyId: new Map([...ptyIds].map((ptyId) => [ptyId, this.ptysById.get(ptyId)])), readRows: () => this.getAgentProviderSessionSnapshotFn?.() ?? [], checkAgentPresence: this.checkHookAgentPresenceFn, capturedAt: Date.now() @@ -129,18 +126,11 @@ export abstract class OrcaRuntimeWithWorktreeSleepResumeRecording extends OrcaRu >() private hookStatusResumeWrites: Promise = Promise.resolve() - private readonly paneLaunchConfigOf: PaneLaunchConfigOf = (paneKey, agent) => { - for (const pty of this.ptysById.values()) { - if ( - pty.launchConfig && - pty.launchAgent === agent && - this.paneOfPty(pty).paneKey === paneKey - ) { - return pty.launchConfig - } - } - return undefined - } + protected abstract getPtyRecordForPaneKey(paneKey: string): RuntimePtyWorktreeRecord | null + + /** Main's word on the Orca launch running in this pane now; the one check every saved note passes. */ + getLaunchStatementForPaneKey: PaneLaunchConfigOf = (paneKey, agent) => + launchStatement(this.getPtyRecordForPaneKey(paneKey), agent) // Main records accepted status for its panes even without windows, preserving window resume rules; serialize writes so statuses read prior records. recordHookStatusResume(row: AgentStatusIpcPayload): void { @@ -207,7 +197,7 @@ export abstract class OrcaRuntimeWithWorktreeSleepResumeRecording extends OrcaRu status, session: this.store.getWorkspaceSession(hostId), worktreeId, - launchConfigOf: this.paneLaunchConfigOf + launchConfigOf: this.getLaunchStatementForPaneKey }) if (command) { // Not this.applyLayoutCommand: this runs inside the resume-record queue, which that would await. @@ -225,7 +215,7 @@ export abstract class OrcaRuntimeWithWorktreeSleepResumeRecording extends OrcaRu statuses, session, worktreeIdOf: (paneKey) => this.getTerminalWorktreeIdForPaneKey(paneKey), - launchConfigOf: this.paneLaunchConfigOf, + launchConfigOf: this.getLaunchStatementForPaneKey, capturedAt: Date.now() }) } diff --git a/src/main/runtime/terminal/stop/worktree-sleep-resume-records.ts b/src/main/runtime/terminal/stop/worktree-sleep-resume-records.ts index 3102e1b32a7..4b2841543d0 100644 --- a/src/main/runtime/terminal/stop/worktree-sleep-resume-records.ts +++ b/src/main/runtime/terminal/stop/worktree-sleep-resume-records.ts @@ -2,7 +2,6 @@ import { agentProviderSessionsEqual, isResumableTuiAgent, normalizeAgentProviderSession, - type SleepingAgentLaunchConfig, type SleepingAgentSessionRecord } from '../../../../shared/agent-session/lifecycle/agent-session-resume' import type { AgentStatusIpcPayload } from '../../../../shared/agent-status/contracts/agent-status-ipc-payload' @@ -11,11 +10,17 @@ import { parsePaneKey } from '../../../../shared/workspace-layout/stable-pane-id import { isLocalHookConnectionId } from '../../../../shared/wsl-hook-relay-contract' import type { WorkspaceSessionState } from '../../../../shared/workspace-session/workspace-session-state-types' import { isWorkspaceSleepRecord } from '../../../../shared/workspace-sleep' +import { launchStatement, type PtyLaunchFacts } from '../launch/launch-evidence' /** Per slept PTY: the resume records captured, and the panes the hook store proves moved on. */ export type WorktreeSleepResumeCapture = Map< string, - { retiredPaneKeys: string[]; records: SleepingAgentSessionRecord[] } + { + retiredPaneKeys: string[] + records: SleepingAgentSessionRecord[] + /** Main can't tell this PTY's launch, so a record keeps an earlier capture's same-session note. */ + launchUnknown?: boolean + } > /** Captures before a host-driven sleep stops its PTYs and persists for the ones proven stopped. */ @@ -32,11 +37,8 @@ export type WorktreeSleepResumeRecorder = { export async function captureWorktreeSleepResumeRecords(args: { worktreeId: string paneKeysByPtyId: ReadonlyMap> - /** The agent each PTY was launched as, with its recipe; resume must reuse a custom binary/args. */ - launchByPtyId: ReadonlyMap< - string, - { agent: string | null; config: SleepingAgentLaunchConfig | null } - > + /** Each PTY's launch facts; only a launch main still holds supplies its recipe. */ + launchByPtyId: ReadonlyMap /** Read after each presence probe, so a conversation started during it is the one captured. */ readRows: () => readonly AgentStatusIpcPayload[] checkAgentPresence: @@ -53,6 +55,7 @@ export async function captureWorktreeSleepResumeRecords(args: { const records: SleepingAgentSessionRecord[] = [] const retiredPaneKeys: string[] = [] const launch = args.launchByPtyId.get(ptyId) + let launchUnknown = false for (const { paneKey, tabId } of panes) { // Why: the one holder rule — an agent the hook store proved exited left a plain shell. if ((await args.checkAgentPresence?.(paneKey, 0).catch(() => null)) === 'exited') { @@ -70,6 +73,9 @@ export async function captureWorktreeSleepResumeRecords(args: { retiredPaneKeys.push(paneKey) continue } + const statement = launchStatement(launch, agent) + launchUnknown ||= statement === undefined + const launchConfig = statement ?? undefined records.push({ paneKey, tabId, @@ -85,10 +91,14 @@ export async function captureWorktreeSleepResumeRecords(args: { restoreOnTabOpenOnly: true, // Why: the SSH host boundary keeps the id off another machine; a WSL relay tag is local transport. connectionId: isLocalHookConnectionId(row.connectionId) ? null : row.connectionId, - ...(launch?.config && launch.agent === agent ? { launchConfig: launch.config } : {}) + ...(launchConfig ? { launchConfig } : {}) }) } - capture.set(ptyId, { retiredPaneKeys, records }) + capture.set(ptyId, { + retiredPaneKeys, + records, + ...(launchUnknown ? { launchUnknown } : {}) + }) } return capture } @@ -111,9 +121,10 @@ export function applyWorktreeSleepResumeRecords( } } for (const record of entry?.records ?? []) { + // Only a terminal main can't tell about keeps an earlier capture's recipe for the same session. const previous = next[record.paneKey] - // Reattach can lose launch recipes; retain an earlier capture's config for the same session. const keptLaunchConfig = + entry?.launchUnknown && !record.launchConfig && previous?.launchConfig && previous.agent === record.agent && diff --git a/src/main/runtime/terminal/wait/terminal-interactive-wait.ts b/src/main/runtime/terminal/wait/terminal-interactive-wait.ts index 175354115b7..3938aa9e98e 100644 --- a/src/main/runtime/terminal/wait/terminal-interactive-wait.ts +++ b/src/main/runtime/terminal/wait/terminal-interactive-wait.ts @@ -1,3 +1,4 @@ +import { heldLaunchConfig } from '../launch/launch-evidence' import type { TerminalIdentityReadContext } from '../query/terminal-identity-read-context' import { getRepoRoutingConnectionId } from '../../../../shared/execution-host' import { OrcaRuntimeWithAdoptTerminalOrphansFromInventory } from '../adoption/adopt-terminal-orphans-from-inventory' @@ -198,9 +199,10 @@ export abstract class OrcaRuntimeWithGetTerminalInteractiveWait extends OrcaRunt let connectionId: string | null | undefined let launchToken: string | null | undefined let wslDistro: string | undefined + let pty: RuntimePtyWorktreeRecord | undefined try { const ptyId = this.getTerminalAgentStatusPtyId(handle) - const pty = this.ptysById.get(ptyId) + pty = this.ptysById.get(ptyId) connectionId = pty?.connectionId ?? null launchToken = pty?.launchToken ?? null // A WSL pane's PTY is local, so its hook events only match once the distro is supplied. @@ -210,7 +212,7 @@ export abstract class OrcaRuntimeWithGetTerminalInteractiveWait extends OrcaRunt } catch { // Exact worker validation rejects this in production; test/legacy providers may not expose PTY metadata. } - return selectExactWorkerProviderSession({ + const session = selectExactWorkerProviderSession({ paneKey, processIncarnation, connectionId, @@ -219,6 +221,8 @@ export abstract class OrcaRuntimeWithGetTerminalInteractiveWait extends OrcaRunt observedAfter, statuses: this.getAgentStatusSnapshotFn?.() ?? [] }) + const launchConfig = session ? heldLaunchConfig(pty, session.agent) : undefined + return session && launchConfig ? { ...session, launchConfig } : session } resolveOrchestrationAgentLauncher( diff --git a/src/main/startup/main-window-agent-status.ts b/src/main/startup/main-window-agent-status.ts index 23409edbe1f..e4e0b994d4e 100644 --- a/src/main/startup/main-window-agent-status.ts +++ b/src/main/startup/main-window-agent-status.ts @@ -45,12 +45,14 @@ export function installMainWindowAgentStatusListeners(options: MainWindowAgentSt if (structuredHost) { return } + const launchConfig = state.runtime?.getLaunchStatementForPaneKey(paneKey, payload.agentType) if (providerSessionOnly) { // Idle Pi session_start only refreshes resume identity; skip titles, telemetry and status UI. state.mainWindow?.webContents.send('agentStatus:set', { ...payload, paneKey, ...(launchToken ? { launchToken } : {}), + ...(launchConfig !== undefined ? { launchConfig } : {}), tabId, worktreeId, connectionId, @@ -71,6 +73,7 @@ export function installMainWindowAgentStatusListeners(options: MainWindowAgentSt ...payload, paneKey, ...(launchToken ? { launchToken } : {}), + ...(launchConfig !== undefined ? { launchConfig } : {}), ...(terminalHandle ? { terminalHandle } : {}), tabId, worktreeId, diff --git a/src/main/workspace-hooks/effective-hook-config.ts b/src/main/workspace-hooks/effective-hook-config.ts index f5b00df705e..5391d59ee80 100644 --- a/src/main/workspace-hooks/effective-hook-config.ts +++ b/src/main/workspace-hooks/effective-hook-config.ts @@ -77,6 +77,17 @@ export function shouldRunSetupForCreate(repo: Repo, decision: SetupDecision = 'i return policy === 'run-by-default' } +/** Unattended creates (orchestration workers) cannot ask, so only an explicit project skip turns setup off. */ +export function resolveUnattendedSetupDecision( + repo: Repo, + requested?: SetupDecision +): Extract { + if (requested === 'run' || requested === 'skip') { + return requested + } + return getEffectiveSetupRunPolicy(repo) === 'skip-by-default' ? 'skip' : 'run' +} + export function getDefaultTabsLaunch( hooks: OrcaHooks | null, repo: Repo, diff --git a/src/renderer/src/app-shell/AppRootSurfaces.tsx b/src/renderer/src/app-shell/AppRootSurfaces.tsx index e97fe473f52..47951f375c3 100644 --- a/src/renderer/src/app-shell/AppRootSurfaces.tsx +++ b/src/renderer/src/app-shell/AppRootSurfaces.tsx @@ -13,7 +13,7 @@ import { MarkdownTemplatePicker } from '../components/editor/markdown/documents/ import RecentTabSwitcher from '../components/tab-bar/RecentTabSwitcher' import { SkillFreshnessUpdateDialog } from '../components/skills/SkillFreshnessUpdateDialog' import { StarNagAgentValueMomentObserver } from '../components/star-nag/StarNagAgentValueMomentObserver' -import { NativeChatResumeOnRestartModal } from '../components/NativeChatResumeRuntimeModal' +import { NativeChatResumeOnRestartModal } from '../components/native-chat-recovery/NativeChatResumeRuntimeModal' import { StarNagToastHost } from '../components/star-nag/StarNagToastHost' import { TelemetryFirstLaunchSurface } from '../components/TelemetryFirstLaunchSurface' import { ZoomOverlay } from '../components/ZoomOverlay' diff --git a/src/renderer/src/app-shell/plugin-settings-shortcut-handler.ts b/src/renderer/src/app-shell/plugin-settings-shortcut-handler.ts index cc45a4aace2..6892d4d237a 100644 --- a/src/renderer/src/app-shell/plugin-settings-shortcut-handler.ts +++ b/src/renderer/src/app-shell/plugin-settings-shortcut-handler.ts @@ -1,5 +1,5 @@ import { isPairedWebClientWindow } from '@/lib/desktop-window-chrome' -import { isEditableTarget, isNativeChatPromptTarget } from '@/lib/editable-target' +import { isEditableTarget, isNativeChatPromptTarget } from '@/lib/text-input/focus/editable-target' export function createPluginsSettingsShortcutHandler( target: EventTarget | null | undefined, diff --git a/src/renderer/src/app-shell/use-app-shell-services.ts b/src/renderer/src/app-shell/use-app-shell-services.ts index 0d984d7fc06..93465e308d8 100644 --- a/src/renderer/src/app-shell/use-app-shell-services.ts +++ b/src/renderer/src/app-shell/use-app-shell-services.ts @@ -2,13 +2,13 @@ import { useAppStore } from '../store' import { useClosedEditorTabCleanup } from '../components/editor/model-lifetime/retention/useClosedEditorTabCleanup' import { useIpcEvents } from '../hooks/useIpcEvents' import { useReadOnScreenAttention } from '../hooks/useReadOnScreenAttention' -import { useAppMenuPaste } from '../hooks/useAppMenuPaste' -import { useAppMenuSelectionActions } from '../hooks/useAppMenuSelectionActions' -import { useLargeTextControlPaste } from '../hooks/useLargeTextControlPaste' +import { useAppMenuPaste } from '../hooks/clipboard/menu/useAppMenuPaste' +import { useAppMenuSelectionActions } from '../hooks/app-commands/useAppMenuSelectionActions' +import { useLargeTextControlPaste } from '../hooks/text-input/paste/useLargeTextControlPaste' import { resolvePrimarySelectionMiddleClickPaste, usePrimarySelectionPaste -} from '../hooks/usePrimarySelectionPaste' +} from '../hooks/clipboard/primary-selection/usePrimarySelectionPaste' import { useRadixBodyPointerEventsRecovery } from '../hooks/useRadixBodyPointerEventsRecovery' import { useOsc52ClipboardDefaultOnNotice } from '../components/terminal-pane/clipboard/osc52-clipboard-default-on-notice' import { useWindowsTerminalFontWeightNotice } from '../components/terminal-pane/appearance/windows-terminal-font-weight-notice' diff --git a/src/renderer/src/app-shell/use-global-keybindings.ts b/src/renderer/src/app-shell/use-global-keybindings.ts index 5c6479de315..ec2c0578ece 100644 --- a/src/renderer/src/app-shell/use-global-keybindings.ts +++ b/src/renderer/src/app-shell/use-global-keybindings.ts @@ -3,7 +3,7 @@ import { toast } from 'sonner' import { translate } from '@/i18n/i18n' import { canShowRightSidebarForView } from '@/lib/right-sidebar-visibility' import { getSelectedTextForFileSearch } from '../lib/file-search-selection' -import { registerAppCommandDispatcher } from '@/lib/app-command-dispatch' +import { registerAppCommandDispatcher } from '@/lib/app-commands/app-command-dispatch' import { executePluginCommand } from '@/lib/plugin-command-execution' import { runBuiltInOrReplacement } from '@/lib/plugin-command-replacements' import { @@ -34,7 +34,7 @@ import { } from './app-command-handlers' import type { AppChromeLayout } from './use-app-chrome-layout' import type { FloatingWorkspacePanelState } from './use-floating-workspace-panel' -import { addAppKeyListener, removeAppKeyListener } from '@/lib/app-keyboard-router' +import { addAppKeyListener, removeAppKeyListener } from '@/lib/keyboard/app-keyboard-router' import { textInputOwnsComposition } from '../../../shared/text-input-shortcut-policy' import { isAgentTerminalPreviewTarget } from '../components/dashboard-popout/agent-terminal-preview-target' diff --git a/src/renderer/src/assets/main.css b/src/renderer/src/assets/main.css index 4b589daad92..782d1d11619 100644 --- a/src/renderer/src/assets/main.css +++ b/src/renderer/src/assets/main.css @@ -49,6 +49,10 @@ --text-2xs--line-height: calc(1 / 0.6875); --text-3xs: 0.625rem; --text-3xs--line-height: calc(0.875 / 0.625); + --text-settings-detail: 0.78125rem; + --text-settings-detail--line-height: 1.35; + --text-settings-heading: 0.9375rem; + --text-settings-heading--line-height: 1.4; --text-chat-code: 0.75rem; --text-chat-code--line-height: calc(1 / 0.75); } @@ -1086,7 +1090,9 @@ body[data-modal-overlay-open] [data-slot='command-dialog-content'], [data-slot='sheet-content'], [data-onboarding-overlay] - ):has(~ [data-slot='dialog-overlay'], ~ [data-slot='sheet-overlay']) { + ):has( + ~ :is([data-slot='dialog-overlay'], [data-slot='sheet-overlay']):not([data-state='closed']) + ) { filter: blur(2px); } @@ -2176,6 +2182,43 @@ html.native-shell .app-layout { animation: settings-shell-enter 180ms ease-out; } +/* Shared row primitives also render outside Settings; their density stays local to this page. */ +.settings-view-shell > :not(aside) [data-slot='label'], +.settings-view-shell > :not(aside) [data-source-title] { + font-size: var(--text-sm); + line-height: 1.4; +} + +.settings-view-shell > :not(aside) p.text-xs, +.settings-view-shell > :not(aside) p.text-2xs, +.settings-view-shell > :not(aside) p[class~='text-[10px]'], +.settings-view-shell > :not(aside) p[class~='text-[11px]'], +.settings-view-shell > :not(aside) p[class~='text-[12px]'], +.settings-view-shell > :not(aside) p[class~='text-[13px]'], +.settings-view-shell > :not(aside) [data-source-path] { + font-size: var(--text-settings-detail); + line-height: 1.35; +} + +.settings-view-shell > :not(aside) [data-slot='select-trigger'], +.settings-view-shell > :not(aside) button[role='combobox'] { + min-height: 28px; + height: 28px; +} + +.settings-view-shell > :not(aside) [data-settings-row]:not(:has([data-settings-row])) { + padding-block: 9px; + min-height: 44px; +} + +.settings-view-shell > :not(aside) [data-settings-group-heading] { + font-size: var(--text-settings-heading); +} + +.settings-view-shell > :not(aside) [data-settings-group-description] { + font-size: var(--text-settings-detail); +} + .repo-icon-emoji-picker .EmojiPickerReact { --epr-bg-color: var(--card); --epr-category-label-bg-color: var(--card); diff --git a/src/renderer/src/components/ClaudeAccountSwitchSessionTree.tsx b/src/renderer/src/components/ClaudeAccountSwitchSessionTree.tsx index 8b712f06a6a..25ad85b58f6 100644 --- a/src/renderer/src/components/ClaudeAccountSwitchSessionTree.tsx +++ b/src/renderer/src/components/ClaudeAccountSwitchSessionTree.tsx @@ -16,12 +16,12 @@ import { } from '@/lib/worktree-default-display-name' import { makePaneKey } from '../../../shared/workspace-layout/stable-pane-id' import { useAppStore } from '../store' -import { ResumeTreeRow } from './NativeChatResumeTreeRow' +import { ResumeTreeRow } from './native-chat-recovery/tree/NativeChatResumeTreeRow' import { resumeSelectionState, toggleResumeSelection -} from './native-chat-resume-on-restart-grouping' -import { moveInTree } from './native-chat-resume-tree-keys' +} from './native-chat-recovery/tree/native-chat-resume-on-restart-grouping' +import { moveInTree } from './native-chat-recovery/tree/native-chat-resume-tree-keys' type SessionRowData = ClaudeSessionMoveCandidate & { reason?: string } diff --git a/src/renderer/src/components/JiraIssueWorkspace.tsx b/src/renderer/src/components/JiraIssueWorkspace.tsx index 6740918308b..45cc2029fd4 100644 --- a/src/renderer/src/components/JiraIssueWorkspace.tsx +++ b/src/renderer/src/components/JiraIssueWorkspace.tsx @@ -2,7 +2,7 @@ import React, { useCallback, useMemo } from 'react' import { toast } from 'sonner' import * as VisuallyHidden from 'radix-ui/visually-hidden' import { Sheet, SheetContent, SheetDescription, SheetTitle } from '@/components/ui/sheet' -import { hasBoundedCommentBodyText } from '@/lib/comment-body-submit-state' +import { hasBoundedCommentBodyText } from '@/lib/source-control/review/comments/comment-body-submit-state' import { useAppStore } from '@/store' import { jiraGetIssueForTarget, @@ -20,7 +20,7 @@ import { JiraIssueCommentComposer, JiraIssueWorkspaceContent } from './jira-issu import { useJiraIssueWorkspaceActions } from './jira-issue-workspace-actions' import { getJiraReadIdentity } from '@/store/slices/jira/jira-read-coordination' import { defaultScopeHost } from '@/runtime/hosts/selection/default-creation-host' -import { taskSourceRuntimeTarget } from '@/lib/task-source-runtime-target' +import { taskSourceRuntimeTarget } from '@/lib/work-items/task-source-runtime-target' import { getEnvironmentRuntimeContextKey } from '@/runtime/hosts/observations/provider-runtime-context' import { runtimeTargetEnvironmentId } from '@/runtime/rpc/targets/runtime-client-target' import { editedJiraIssueFields } from './jira-issue-workspace-hydration' diff --git a/src/renderer/src/components/LinearIssueTextEditor.tsx b/src/renderer/src/components/LinearIssueTextEditor.tsx index 595a2123c0c..110eb89794b 100644 --- a/src/renderer/src/components/LinearIssueTextEditor.tsx +++ b/src/renderer/src/components/LinearIssueTextEditor.tsx @@ -1,6 +1,6 @@ import { writeLinearIssue } from '@/lib/work-items/linear/linear-issue-write' -import { taskProviderErrorMessage } from '@/lib/task-provider-error-copy' -import { ImeTextarea } from '@/lib/ime-text-field' +import { taskProviderErrorMessage } from '@/lib/work-items/task-provider-error-copy' +import { ImeTextarea } from '@/lib/text-input/ime/ime-text-field' import React, { useCallback, useRef, useState } from 'react' import { LoaderCircle } from 'lucide-react' import { toast } from 'sonner' diff --git a/src/renderer/src/components/NewWorkspaceComposerModal.tsx b/src/renderer/src/components/NewWorkspaceComposerModal.tsx index fb79a4a863f..22daa3182dd 100644 --- a/src/renderer/src/components/NewWorkspaceComposerModal.tsx +++ b/src/renderer/src/components/NewWorkspaceComposerModal.tsx @@ -44,7 +44,7 @@ import { getWorkspaceComposerInitialFocusTarget } from '@/lib/workspace-composer import '@/components/sidebar/workspace-creation/folder/folder-workspace-source-repos' import { getFolderWorkspacePrimaryActionLabel } from '@/components/sidebar/workspace-creation/folder/folder-workspace-source-selection' import { useAgentMenuObservation } from '@/components/tab-bar/use-new-tab-agent-menu-rows' -import { addAppKeyListener, removeAppKeyListener } from '@/lib/app-keyboard-router' +import { addAppKeyListener, removeAppKeyListener } from '@/lib/keyboard/app-keyboard-router' // Keep Add Project’s clone/SSH machinery outside the composer entry render, matching App loading. const HostedAddRepoDialog = lazyWithRetry( diff --git a/src/renderer/src/components/SelectedTextCopyMenu.tsx b/src/renderer/src/components/SelectedTextCopyMenu.tsx index 4fe4f173a14..6049aeca2e4 100644 --- a/src/renderer/src/components/SelectedTextCopyMenu.tsx +++ b/src/renderer/src/components/SelectedTextCopyMenu.tsx @@ -3,7 +3,7 @@ import { createPortal } from 'react-dom' import { Copy } from 'lucide-react' import { useTranslate } from '@/i18n/use-translate' import { addViewportSizeChangeListener } from '@/hooks/viewport-size-change-listener' -import { addAppKeyListener, removeAppKeyListener } from '@/lib/app-keyboard-router' +import { addAppKeyListener, removeAppKeyListener } from '@/lib/keyboard/app-keyboard-router' type SelectedTextCopyMenuProps = { children: React.ReactNode diff --git a/src/renderer/src/components/TerminalSearch.tsx b/src/renderer/src/components/TerminalSearch.tsx index b22321e66d5..7d0cb2b2254 100644 --- a/src/renderer/src/components/TerminalSearch.tsx +++ b/src/renderer/src/components/TerminalSearch.tsx @@ -1,4 +1,4 @@ -import { ImeInput } from '@/lib/ime-text-field' +import { ImeInput } from '@/lib/text-input/ime/ime-text-field' import { useEffect, useState, useCallback } from 'react' import { ChevronUp, ChevronDown, X, CaseSensitive, Regex } from 'lucide-react' import type { SearchAddon } from '@xterm/addon-search' diff --git a/src/renderer/src/components/activity/ActivityPrototypePage.tsx b/src/renderer/src/components/activity/ActivityPrototypePage.tsx index f822f763156..d3cd16377ec 100644 --- a/src/renderer/src/components/activity/ActivityPrototypePage.tsx +++ b/src/renderer/src/components/activity/ActivityPrototypePage.tsx @@ -21,7 +21,7 @@ import { useActivityTerminalPortalStatus } from './activity-terminal-portal-status' import type { ActivityTerminalPortalSlotId } from './activity-thread-types' -import { addAppKeyListener, removeAppKeyListener } from '@/lib/app-keyboard-router' +import { addAppKeyListener, removeAppKeyListener } from '@/lib/keyboard/app-keyboard-router' import { selectDetectedWorktreesByRepo, selectFolderWorkspaces, diff --git a/src/renderer/src/components/activity/activity-event-build-cache.ts b/src/renderer/src/components/activity/activity-event-build-cache.ts index 9193f3b4ade..0b8fbf54313 100644 --- a/src/renderer/src/components/activity/activity-event-build-cache.ts +++ b/src/renderer/src/components/activity/activity-event-build-cache.ts @@ -13,6 +13,23 @@ import type { ActivityLiveAgentState } from './activity-thread-types' import { buildPaneActivityEvents } from './activity-pane-events' +import { + buildActivityTabContext, + buildActivityTabHostIndex, + type ActivityTabContext, + type ActivityTabHostIndex +} from './activity-event-builder-context' +import type { BuildActivityEventsArgs } from './activity-event-builder' + +type ActivityTabInputs = Pick< + BuildActivityEventsArgs, + 'tabsByWorktree' | 'unifiedTabsByWorktree' | 'chatNames' +> + +type ActivityTabIndexes = ActivityTabInputs & { + context: Map + hosts: ActivityTabHostIndex +} type PaneActivityCacheEntry = { source: unknown @@ -29,12 +46,46 @@ type PaneActivityCacheEntry = { export type ActivityEventBuildCache = { panes: Map + tabs?: ActivityTabIndexes } export function createActivityEventBuildCache(): ActivityEventBuildCache { return { panes: new Map() } } +export function resolveActivityTabIndexes( + args: ActivityTabInputs, + cache: ActivityEventBuildCache | undefined +): ActivityTabIndexes { + const previous = cache?.tabs + if ( + previous && + previous.tabsByWorktree === args.tabsByWorktree && + previous.unifiedTabsByWorktree === args.unifiedTabsByWorktree && + previous.chatNames === args.chatNames + ) { + return previous + } + const next: ActivityTabIndexes = { + tabsByWorktree: args.tabsByWorktree, + unifiedTabsByWorktree: args.unifiedTabsByWorktree, + chatNames: args.chatNames, + context: buildActivityTabContext( + args.tabsByWorktree, + args.unifiedTabsByWorktree, + args.chatNames + ), + hosts: + previous && previous.unifiedTabsByWorktree === args.unifiedTabsByWorktree + ? previous.hosts + : buildActivityTabHostIndex(args.unifiedTabsByWorktree) + } + if (cache) { + cache.tabs = next + } + return next +} + export type PaneBuildRequest = { cacheKey: string source: unknown diff --git a/src/renderer/src/components/activity/activity-event-builder.ts b/src/renderer/src/components/activity/activity-event-builder.ts index b1375164901..57b65b1fc25 100644 --- a/src/renderer/src/components/activity/activity-event-builder.ts +++ b/src/renderer/src/components/activity/activity-event-builder.ts @@ -18,14 +18,13 @@ import { capActivityEvents } from './activity-event-cap' import { newestActivityHistoryEntries } from './activity-pane-events' import { createActivityEventBuildCache, + resolveActivityTabIndexes, resolvePaneBuild, type ActivityEventBuildCache } from './activity-event-build-cache' import { appendUnsupportedAndRetainedEvents } from './activity-event-builder-sources' import { attributedActivityTabContext, - buildActivityTabContext, - buildActivityTabHostIndex, resolveActivityEventOwner, type ActivityEventOwner } from './activity-event-builder-context' @@ -63,12 +62,7 @@ export function buildActivityEvents( const events: ActivityEvent[] = [] const paneEntryByPaneKey: Record = {} const seenEventIds = new Set() - const tabContext = buildActivityTabContext( - args.tabsByWorktree, - args.unifiedTabsByWorktree, - args.chatNames - ) - const tabHostIndex = buildActivityTabHostIndex(args.unifiedTabsByWorktree) + const { context: tabContext, hosts: tabHostIndex } = resolveActivityTabIndexes(args, cache) const ownerCache = new Map() const liveAgentByPaneKey: Record = {} const seenCacheKeys = cache ? new Set() : null diff --git a/src/renderer/src/components/activity/activity-thread-context-menu.tsx b/src/renderer/src/components/activity/activity-thread-context-menu.tsx index a91d6436a18..725fceb20b7 100644 --- a/src/renderer/src/components/activity/activity-thread-context-menu.tsx +++ b/src/renderer/src/components/activity/activity-thread-context-menu.tsx @@ -11,7 +11,7 @@ import { import { translate } from '@/i18n/i18n' import { useTranslate, type Translate } from '@/i18n/use-translate' import { CLOSE_ALL_CONTEXT_MENUS_EVENT } from '@/lib/close-all-context-menus' -import { formatClipboardFilePath } from '@/lib/clipboard-file-path' +import { formatClipboardFilePath } from '@/lib/clipboard/clipboard-file-path' import { getWorktreeRowLocalPathOpenOwner } from '@/lib/workspace/ownership/worktree-row-open-in-owner' import { useAppStore } from '@/store' import { diff --git a/src/renderer/src/components/activity/activity-thread-hover-card-summary.tsx b/src/renderer/src/components/activity/activity-thread-hover-card-summary.tsx index d128804f98c..5fc59eb9962 100644 --- a/src/renderer/src/components/activity/activity-thread-hover-card-summary.tsx +++ b/src/renderer/src/components/activity/activity-thread-hover-card-summary.tsx @@ -9,7 +9,7 @@ import { getWorktreeGitIdentityDisplay } from '../../../../shared/worktree/git-i import { jumpToWorktreeFromSidebar } from '@/lib/workspace/activation/worktree-jump-navigation' import { translate } from '@/i18n/i18n' import { useTranslate } from '@/i18n/use-translate' -import { formatClipboardFilePath } from '@/lib/clipboard-file-path' +import { formatClipboardFilePath } from '@/lib/clipboard/clipboard-file-path' import { getWorktreeRowLocalPathOpenOwner } from '@/lib/workspace/ownership/worktree-row-open-in-owner' import { cn } from '@/lib/utils' import { diff --git a/src/renderer/src/components/activity/use-agent-pane-threads.ts b/src/renderer/src/components/activity/use-agent-pane-threads.ts index 66406c57320..5f0f444d29d 100644 --- a/src/renderer/src/components/activity/use-agent-pane-threads.ts +++ b/src/renderer/src/components/activity/use-agent-pane-threads.ts @@ -67,6 +67,7 @@ export function useAllAgentPaneThreads({ badgeOnly = false }: { badgeOnly?: bool ) ) { eventBuildCacheRef.current.panes.clear() + eventBuildCacheRef.current.tabs = undefined return EMPTY_ACTIVITY_BUILD } // Global history caps affect live-thread unread and search state. diff --git a/src/renderer/src/components/agent/AgentLaunchSetupFooter.tsx b/src/renderer/src/components/agent/AgentLaunchSetupFooter.tsx index a8d9c48a463..82edb8b3792 100644 --- a/src/renderer/src/components/agent/AgentLaunchSetupFooter.tsx +++ b/src/renderer/src/components/agent/AgentLaunchSetupFooter.tsx @@ -6,7 +6,7 @@ import { cn } from '@/lib/utils' import { ShortcutKeyCombo } from '@/components/ShortcutKeyCombo' import { translate } from '@/i18n/i18n' import { useTranslate, type Translate } from '@/i18n/use-translate' -import { getShortcutPlatform } from '@/lib/shortcut-platform' +import { getShortcutPlatform } from '@/lib/keyboard/shortcut-platform' import type { NativeChatComposerHandle } from '../native-chat/composer/native-chat-composer-types' import { AgentLaunchPromptComposer } from './AgentLaunchPromptComposer' import { diff --git a/src/renderer/src/components/agent/AgentLaunchSetupPanel.tsx b/src/renderer/src/components/agent/AgentLaunchSetupPanel.tsx index 3e4850dfffa..e3bcdbd25d0 100644 --- a/src/renderer/src/components/agent/AgentLaunchSetupPanel.tsx +++ b/src/renderer/src/components/agent/AgentLaunchSetupPanel.tsx @@ -22,7 +22,7 @@ import { import { useTranslate } from '@/i18n/use-translate' import { agentModelChoiceChips } from './launch-settings/agent-model-choice-chips' import { isScreenSubmitShortcut } from '@/lib/screen-submit-shortcut' -import { isImeOwnedKeyboardEvent } from '@/lib/ime-composition-keyboard-event' +import { isImeOwnedKeyboardEvent } from '@/lib/text-input/ime/ime-composition-keyboard-event' import { AgentDefaultArgsInput } from './launch-settings/AgentLaunchDefaultsEditor' import { NativeChatSessionOptionRows } from '../native-chat/session/options/controls/NativeChatSessionOptionRows' import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '@/components/ui/collapsible' @@ -40,7 +40,10 @@ import { applyAgentPickerEdit, type AgentPickerEdit } from '../../../../shared/agent-launch/picker/agent-launch-picker-edits' -import { agentPickerArgumentsKnown } from '../../../../shared/agent-launch/picker/agent-launch-picker-authority' +import { + agentPickerArgumentsKnown, + agentPickerCatalogAuthoritative +} from '../../../../shared/agent-launch/picker/agent-launch-picker-authority' import { agentPickerEffectiveChoices } from '../../../../shared/agent-launch/picker/agent-launch-picker' import type { AgentPickerSurface } from '../../../../shared/agent-launch/picker/agent-launch-picker' import { @@ -231,13 +234,7 @@ export function AgentLaunchSetupPanel({ {catalogMessage} ) : null} - {hasModels && - modelQuery && - entry.catalog.origin !== 'unknown' && - !entry.catalog.listingInProgress && - entry.catalogRead !== 'pending' && - entry.catalogRead !== 'failed' && - entry.catalogRead !== 'unsupported' ? ( + {hasModels && modelQuery && agentPickerCatalogAuthoritative(entry) ? ( {t('agentPicker.noModels', 'No models match your search.')} ) : null} diff --git a/src/renderer/src/components/agent/agent-launch-picker-acquisition.test.ts b/src/renderer/src/components/agent/agent-launch-picker-acquisition.test.ts index 5c2b348bd2f..61dc3cb3aca 100644 --- a/src/renderer/src/components/agent/agent-launch-picker-acquisition.test.ts +++ b/src/renderer/src/components/agent/agent-launch-picker-acquisition.test.ts @@ -1,4 +1,4 @@ -import { expect, it } from 'vitest' +import { expect, it, onTestFinished, vi } from 'vitest' import { currentAgentPickerAcquisition, observeAgentPickerAcquisition @@ -86,8 +86,16 @@ it('settles closed catalog reads and resumes their refresh when the picker reope models: [model('saved')] }) await expect - .poll(() => currentAgentPickerAcquisition('closed-catalog')?.map((entry) => entry.catalogRead)) - .toEqual(['settled', 'settled']) + .poll(() => currentAgentPickerAcquisition('closed-catalog')?.map((entry) => entry.catalog)) + .toEqual([ + { origin: 'unknown', listingInProgress: true }, + { + origin: 'probe', + fetchedAt: 1, + listingInProgress: true, + models: [{ id: 'saved', label: 'saved', isDefault: false, efforts: [] }] + } + ]) const stopReopened = observeAgentPickerAcquisition('closed-catalog', entries, reads, observer) expect(published.map((entry) => entry.catalogRead)).toEqual(['pending', 'pending']) @@ -102,3 +110,99 @@ it('settles closed catalog reads and resumes their refresh when the picker reope stopReopened() expect(currentAgentPickerAcquisition('closed-catalog')).toBeUndefined() }) + +it('waits again only when a wait outlasted its budget, then stops once the listing has ended', async () => { + vi.useFakeTimers() + onTestFinished(() => { + vi.useRealTimers() + }) + const saved = { origin: 'probe' as const, fetchedAt: 1, models: [model('saved')] } + // A floating folder's quick read always reports a listing; only waiting reads know it ended. + const waits = [{ ...saved, waitExpired: true as const }, saved] + const reads: boolean[] = [] + let published: AgentPickerEntry[] = [] + const stop = observeAgentPickerAcquisition( + 'outlasting-listing', + [codex], + { + catalog: async (_entry, waitForListing) => { + reads.push(waitForListing) + return waitForListing ? (waits.shift() ?? saved) : { ...saved, listingInProgress: true } + }, + support: async () => undefined + }, + { selected: () => undefined, publish: (entries) => (published = entries) } + ) + try { + await vi.advanceTimersByTimeAsync(60_000) + expect(reads).toEqual([false, true, true]) + expect(published[0]).toMatchObject({ catalogRead: 'settled', catalog: saved }) + } finally { + stop() + } +}) + +it('publishes every saved catalog while earlier refreshes are still pending', async () => { + const fresh = Promise.withResolvers() + const entries: AgentPickerEntry[] = [ + codex, + { ...codex, agent: 'claude', label: 'Claude', rows: [{ agent: 'claude', kind: 'cli' }] }, + { ...codex, agent: 'gemini', label: 'Gemini', rows: [{ agent: 'gemini', kind: 'cli' }] } + ] + let published: AgentPickerEntry[] = [] + const stop = observeAgentPickerAcquisition( + 'independent-saved-catalogs', + entries, + { + catalog: async (entry, waitForListing) => + waitForListing + ? fresh.promise + : { + origin: 'probe', + fetchedAt: 1, + listingInProgress: true, + models: [model(entry.agent)] + }, + support: async () => undefined + }, + { selected: () => 'codex', publish: (next) => (published = next) } + ) + try { + await expect + .poll(() => published.map((entry) => entry.catalog)) + .toMatchObject([ + { models: [{ id: 'codex' }] }, + { models: [{ id: 'claude' }] }, + { models: [{ id: 'gemini' }] } + ]) + } finally { + stop() + fresh.resolve({ origin: 'probe', fetchedAt: 2, models: [model('fresh')] }) + } +}) + +it('keeps an unflagged saved catalog provisional when its read fails', async () => { + const fresh = Promise.withResolvers() + let published: AgentPickerEntry[] = [] + const stop = observeAgentPickerAcquisition( + 'failed-unflagged-catalog', + [{ ...codex, catalog: { origin: 'probe', fetchedAt: 1, models: [model('saved')] } }], + { catalog: () => fresh.promise, support: async () => undefined }, + { selected: () => undefined, publish: (next) => (published = next) } + ) + try { + expect(published[0]).toMatchObject({ + catalogRead: 'pending', + catalog: { models: [{ id: 'saved' }] } + }) + fresh.reject(new Error('refresh failed')) + await expect + .poll(() => published[0]) + .toMatchObject({ + catalogRead: 'failed', + catalog: { models: [{ id: 'saved' }] } + }) + } finally { + stop() + } +}) diff --git a/src/renderer/src/components/agent/agent-launch-picker-acquisition.ts b/src/renderer/src/components/agent/agent-launch-picker-acquisition.ts index bd03dcd6ac7..01314c130a2 100644 --- a/src/renderer/src/components/agent/agent-launch-picker-acquisition.ts +++ b/src/renderer/src/components/agent/agent-launch-picker-acquisition.ts @@ -17,6 +17,7 @@ type Reads = { ) => Promise support: (entry: AgentPickerEntry) => Promise } +type ReadKind = 'catalog' | 'refresh' | 'support' type SupportRead = { state: 'queued' | 'pending' | 'settled' value?: AgentPickerEntry['structured'] @@ -26,9 +27,8 @@ class AgentPickerAcquisition { readonly observers = new Set() entries: AgentPickerEntry[] settingsPending = false - private outstanding = { catalog: 0, support: 0 } - private queued: Record<'catalog' | 'support', Set> - private deferredCatalogs = new Set() + private outstanding = { catalog: 0, refresh: 0, support: 0 } + private queued: Record> private supports = new Map() private defaults: AgentPickerEntry[] @@ -52,6 +52,7 @@ class AgentPickerAcquisition { catalog: new Set( this.entries.flatMap((entry, i) => (entry.catalogRead === 'pending' ? [i] : [])) ), + refresh: new Set(), support: new Set(this.supports.keys()) } } @@ -65,8 +66,7 @@ class AgentPickerAcquisition { this.initial[i].catalogRead !== 'unsupported' && entry.surfaces.length && entry.catalogRead !== 'pending' && - (this.deferredCatalogs.has(i) || - entry.catalog.origin === 'unknown' || + (entry.catalog.origin === 'unknown' || entry.catalogRead === 'failed' || entry.catalogRead === 'unsupported' || entry.catalog.unavailable !== undefined || @@ -90,6 +90,7 @@ class AgentPickerAcquisition { } observer.publish(this.entries) this.admit('catalog') + this.admit('refresh') this.admit('support') return () => { this.observers.delete(observer) @@ -147,24 +148,27 @@ class AgentPickerAcquisition { }) } - private async readCatalog(entry: AgentPickerEntry, index: number) { - this.deferredCatalogs.delete(index) - const saved = await this.reads.catalog(entry, false) - if (!saved?.listingInProgress) { - return saved + private async readCatalog(entry: AgentPickerEntry, index: number, waitForListing: boolean) { + const catalog = await this.reads.catalog(entry, waitForListing) + // An open picker waits on a running listing, and again when a wait used its whole budget. + const refresh = + this.observers.size > 0 && + (waitForListing ? catalog?.waitExpired : catalog?.listingInProgress) === true + if (refresh) { + this.queued.refresh.add(index) } - if (saved.origin !== 'unknown') { - this.entries = this.entries.map((e, i) => (i === index ? { ...e, catalog: saved } : e)) - this.publish() - } - if (!this.observers.size) { - this.deferredCatalogs.add(index) - return saved - } - return this.reads.catalog(entry, true) + this.entries = this.entries.map((e, i) => + i === index + ? { + ...e, + ...(catalog ? { catalog } : {}), + catalogRead: !catalog ? 'failed' : refresh ? 'pending' : 'settled' + } + : e + ) } - private admit(kind: 'catalog' | 'support') { + private admit(kind: ReadKind) { while (this.observers.size && this.outstanding[kind] < 2 && this.queued[kind].size) { const priorities = [...this.observers].map((o) => o.selected()) const preferred = this.entries.findIndex((e) => priorities.includes(e.agent)) @@ -181,40 +185,24 @@ class AgentPickerAcquisition { this.supports.set(index, { state: 'pending' }) } const read = - kind === 'catalog' - ? this.readCatalog(entry, index) - .then((catalog) => { - const resume = this.deferredCatalogs.has(index) && this.observers.size > 0 - if (resume) { - this.queued.catalog.add(index) - } - this.entries = this.entries.map((e, i) => - i === index - ? { - ...e, - ...(catalog ? { catalog } : {}), - catalogRead: resume ? 'pending' : 'settled' - } - : e - ) - }) - .catch((error: unknown) => { - const unsupported = - typeof error === 'object' && - error !== null && - 'code' in error && - (error.code === 'method_not_found' || - error.code === 'structured_agent_session_unsupported' || - error.code === 'forbidden') - this.entries = this.entries.map((e, i) => - i === index - ? { - ...e, - catalogRead: unsupported ? 'unsupported' : 'failed' - } - : e - ) - }) + kind !== 'support' + ? this.readCatalog(entry, index, kind === 'refresh').catch((error: unknown) => { + const unsupported = + typeof error === 'object' && + error !== null && + 'code' in error && + (error.code === 'method_not_found' || + error.code === 'structured_agent_session_unsupported' || + error.code === 'forbidden') + this.entries = this.entries.map((e, i) => + i === index + ? { + ...e, + catalogRead: unsupported ? 'unsupported' : 'failed' + } + : e + ) + }) : this.reads .support(entry) .catch(() => undefined) @@ -228,6 +216,9 @@ class AgentPickerAcquisition { this.outstanding[kind]-- this.publish() this.admit(kind) + if (kind === 'catalog') { + this.admit('refresh') + } this.retire() }) } @@ -238,6 +229,7 @@ class AgentPickerAcquisition { !this.observers.size && !this.settingsPending && !this.outstanding.catalog && + !this.outstanding.refresh && !this.outstanding.support ) { pending.delete(this.key) diff --git a/src/renderer/src/components/agent/agent-launch-picker-retirement.test.ts b/src/renderer/src/components/agent/agent-launch-picker-retirement.test.ts new file mode 100644 index 00000000000..94a270f9890 --- /dev/null +++ b/src/renderer/src/components/agent/agent-launch-picker-retirement.test.ts @@ -0,0 +1,95 @@ +import { expect, it } from 'vitest' +import type { AgentPickerRetainedSetup } from '../../../../shared/agent-launch/picker/agent-launch-picker-authority' +import type { AgentPickerEntry } from '../../../../shared/agent-launch/picker/agent-launch-picker' +import { retireAgentPickerSetups } from './agent-launch-picker-retirement' + +const entry: AgentPickerEntry = { + agent: 'codex', + label: 'Codex', + rows: [{ agent: 'codex', kind: 'cli' }], + surfaces: ['terminal'], + defaultSurface: 'terminal', + terminal: {}, + structured: {}, + catalog: { + origin: 'probe', + fetchedAt: 1, + models: [{ id: 'saved', label: 'Saved', isDefault: false, efforts: [] }] + } +} + +it('preserves edited presets until a completed catalog confirms their model or effort was removed', () => { + const setups = new Map([ + [ + 'missing-model', + { + target: undefined, + agent: 'codex', + preset: { model: 'fresh' }, + source: undefined, + draft: { + agent: 'codex', + surface: 'terminal', + choices: { model: 'saved' }, + arguments: '--add-dir model-edits' + }, + active: false + } + ], + [ + 'missing-effort', + { + target: undefined, + agent: 'codex', + preset: { model: 'saved', effort: 'high' }, + source: undefined, + draft: { + agent: 'codex', + surface: 'terminal', + choices: { model: 'saved', options: { effort: 'low' } }, + arguments: '--add-dir effort-edits' + }, + active: false + } + ] + ]) + for (const catalogRead of ['pending', 'failed', 'unsupported'] as const) { + retireAgentPickerSetups([{ ...entry, catalogRead }], setups, undefined, 'select') + expect([...setups.values()].map(({ draft }) => draft.arguments)).toEqual([ + '--add-dir model-edits', + '--add-dir effort-edits' + ]) + } + + retireAgentPickerSetups( + [ + { + ...entry, + catalogRead: 'settled', + catalog: { + origin: 'probe', + fetchedAt: 2, + models: [ + { id: 'fresh', label: 'Fresh', isDefault: false, efforts: [] }, + { + id: 'saved', + label: 'Saved', + isDefault: false, + efforts: [{ value: 'high', label: 'High' }] + } + ] + } + } + ], + setups, + undefined, + 'select' + ) + expect([...setups.values()].map(({ draft }) => draft.arguments)).toEqual([ + '--add-dir model-edits', + '--add-dir effort-edits' + ]) + + retireAgentPickerSetups([{ ...entry, catalogRead: 'settled' }], setups, undefined, 'select') + expect([...setups.keys()]).toEqual([]) +}) diff --git a/src/renderer/src/components/agent/agent-launch-picker-retirement.ts b/src/renderer/src/components/agent/agent-launch-picker-retirement.ts index 12cdd401e1d..d1020b1bd80 100644 --- a/src/renderer/src/components/agent/agent-launch-picker-retirement.ts +++ b/src/renderer/src/components/agent/agent-launch-picker-retirement.ts @@ -1,4 +1,7 @@ -import type { AgentPickerRetainedSetup } from '../../../../shared/agent-launch/picker/agent-launch-picker-authority' +import { + agentPickerCatalogAuthoritative, + type AgentPickerRetainedSetup +} from '../../../../shared/agent-launch/picker/agent-launch-picker-authority' import type { AgentPickerEntry } from '../../../../shared/agent-launch/picker/agent-launch-picker' import { launchDraftSources } from '../native-chat/drafts/native-chat-launch-draft-source' @@ -19,9 +22,8 @@ export function retireAgentPickerSetups( const membership = new Map( entries.map((entry) => [ entryKey(entry), - entry.catalog.origin === 'unknown' || entry.catalog.listingInProgress - ? undefined - : new Map( + agentPickerCatalogAuthoritative(entry) + ? new Map( entry.catalog.models.map((model) => [ model.id, model.effortsKnown === false @@ -29,6 +31,7 @@ export function retireAgentPickerSetups( : new Set(model.efforts.map((effort) => effort.value)) ]) ) + : undefined ]) ) const retire = ( diff --git a/src/renderer/src/components/automations/AutomationEditorPromptSection.tsx b/src/renderer/src/components/automations/AutomationEditorPromptSection.tsx index 9e010cff1ee..b1a57ce5f17 100644 --- a/src/renderer/src/components/automations/AutomationEditorPromptSection.tsx +++ b/src/renderer/src/components/automations/AutomationEditorPromptSection.tsx @@ -1,4 +1,4 @@ -import { ImeTextarea } from '@/lib/ime-text-field' +import { ImeTextarea } from '@/lib/text-input/ime/ime-text-field' import React from 'react' import { Pencil } from 'lucide-react' import { Button } from '@/components/ui/button' diff --git a/src/renderer/src/components/automations/AutomationTimeField.tsx b/src/renderer/src/components/automations/AutomationTimeField.tsx index 30bf12207a9..68e4a13cadd 100644 --- a/src/renderer/src/components/automations/AutomationTimeField.tsx +++ b/src/renderer/src/components/automations/AutomationTimeField.tsx @@ -1,4 +1,4 @@ -import { ImeInput } from '@/lib/ime-text-field' +import { ImeInput } from '@/lib/text-input/ime/ime-text-field' import React from 'react' import { Clock } from 'lucide-react' import { cn } from '@/lib/utils' diff --git a/src/renderer/src/components/automations/automation-run-history-keyboard-navigation.ts b/src/renderer/src/components/automations/automation-run-history-keyboard-navigation.ts index b9f4d04a853..5b61eab7ff0 100644 --- a/src/renderer/src/components/automations/automation-run-history-keyboard-navigation.ts +++ b/src/renderer/src/components/automations/automation-run-history-keyboard-navigation.ts @@ -1,5 +1,5 @@ import type { AutomationRun } from '../../../../shared/automations/history/automation-run' -import { isImeOwnedKeyboardEvent } from '@/lib/ime-composition-keyboard-event' +import { isImeOwnedKeyboardEvent } from '@/lib/text-input/ime/ime-composition-keyboard-event' export type AutomationRunHistoryArrowKey = 'ArrowUp' | 'ArrowDown' diff --git a/src/renderer/src/components/automations/use-automations-page-escape.ts b/src/renderer/src/components/automations/use-automations-page-escape.ts index c501399c876..36656ad6fe7 100644 --- a/src/renderer/src/components/automations/use-automations-page-escape.ts +++ b/src/renderer/src/components/automations/use-automations-page-escape.ts @@ -2,7 +2,7 @@ import { useEffect } from 'react' import { hasVisibleOverlay } from '@/lib/visible-overlay' import type { AutomationsPageLocalState } from './use-automations-page-local-state' import type { AutomationsPageStoreState } from './use-automations-page-store-state' -import { addAppKeyListener, removeAppKeyListener } from '@/lib/app-keyboard-router' +import { addAppKeyListener, removeAppKeyListener } from '@/lib/keyboard/app-keyboard-router' export function useAutomationsPageEscape({ store, diff --git a/src/renderer/src/components/browser-pane/annotate/MarkupOverlay.tsx b/src/renderer/src/components/browser-pane/annotate/MarkupOverlay.tsx index e7f7a1aa083..73c73aac099 100644 --- a/src/renderer/src/components/browser-pane/annotate/MarkupOverlay.tsx +++ b/src/renderer/src/components/browser-pane/annotate/MarkupOverlay.tsx @@ -1,4 +1,4 @@ -import { ImeInput } from '@/lib/ime-text-field' +import { ImeInput } from '@/lib/text-input/ime/ime-text-field' import React, { useCallback, useRef, useState } from 'react' import { createPortal } from 'react-dom' import { Check, X } from 'lucide-react' diff --git a/src/renderer/src/components/browser-pane/annotate/browser-page-annotation-tray.tsx b/src/renderer/src/components/browser-pane/annotate/browser-page-annotation-tray.tsx index 5c2e09ec6b4..46c1fbb77b4 100644 --- a/src/renderer/src/components/browser-pane/annotate/browser-page-annotation-tray.tsx +++ b/src/renderer/src/components/browser-pane/annotate/browser-page-annotation-tray.tsx @@ -1,4 +1,4 @@ -import { ImeTextarea } from '@/lib/ime-text-field' +import { ImeTextarea } from '@/lib/text-input/ime/ime-text-field' import { useState } from 'react' import { ChevronDown, diff --git a/src/renderer/src/components/browser-pane/annotate/browser-page-grab-toast.tsx b/src/renderer/src/components/browser-pane/annotate/browser-page-grab-toast.tsx index 455ce434517..ace3bcf7bfd 100644 --- a/src/renderer/src/components/browser-pane/annotate/browser-page-grab-toast.tsx +++ b/src/renderer/src/components/browser-pane/annotate/browser-page-grab-toast.tsx @@ -8,7 +8,7 @@ import { DropdownMenuTrigger } from '@/components/ui/dropdown-menu' import { useTranslate } from '@/i18n/use-translate' -import { imageCopyFailedMessage } from '@/lib/clipboard-image-copy' +import { imageCopyFailedMessage } from '@/lib/clipboard/images/clipboard-image-copy' import type { BrowserPageGrabAnnotations, BrowserPageGrabToastState diff --git a/src/renderer/src/components/browser-pane/annotate/pending-browser-annotation-card.tsx b/src/renderer/src/components/browser-pane/annotate/pending-browser-annotation-card.tsx index 71f7627a02c..7da77dff10c 100644 --- a/src/renderer/src/components/browser-pane/annotate/pending-browser-annotation-card.tsx +++ b/src/renderer/src/components/browser-pane/annotate/pending-browser-annotation-card.tsx @@ -1,4 +1,4 @@ -import { ImeTextarea } from '@/lib/ime-text-field' +import { ImeTextarea } from '@/lib/text-input/ime/ime-text-field' import { useEffect, useState } from 'react' import { CornerDownLeft, MessageSquarePlus } from 'lucide-react' import { Button } from '@/components/ui/button' diff --git a/src/renderer/src/components/browser-pane/annotate/use-browser-page-grab-annotations.ts b/src/renderer/src/components/browser-pane/annotate/use-browser-page-grab-annotations.ts index a782d563b57..2802b62f08c 100644 --- a/src/renderer/src/components/browser-pane/annotate/use-browser-page-grab-annotations.ts +++ b/src/renderer/src/components/browser-pane/annotate/use-browser-page-grab-annotations.ts @@ -2,7 +2,7 @@ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react import { translate } from '@/i18n/i18n' import { useMountedRef } from '@/hooks/useMountedRef' import { useAppStore } from '@/store' -import { imageCopyFailedMessage } from '@/lib/clipboard-image-copy' +import { imageCopyFailedMessage } from '@/lib/clipboard/images/clipboard-image-copy' import { isBrowserAnnotationPageFull, type BrowserAnnotationIntent, diff --git a/src/renderer/src/components/browser-pane/annotate/useGrabMode.ts b/src/renderer/src/components/browser-pane/annotate/useGrabMode.ts index b390a9cbf39..955e04d099c 100644 --- a/src/renderer/src/components/browser-pane/annotate/useGrabMode.ts +++ b/src/renderer/src/components/browser-pane/annotate/useGrabMode.ts @@ -7,7 +7,7 @@ import type { import { useMountedRef } from '@/hooks/useMountedRef' import { translate } from '@/i18n/i18n' import { isEditableKeyboardTarget } from '../host-guest/browser-keyboard' -import { addAppKeyListener, removeAppKeyListener } from '@/lib/app-keyboard-router' +import { addAppKeyListener, removeAppKeyListener } from '@/lib/keyboard/app-keyboard-router' import { useGuestEscape } from '../host-guest/use-guest-escape' export type GrabModeState = 'idle' | 'armed' | 'awaiting' | 'confirming' | 'error' diff --git a/src/renderer/src/components/browser-pane/assemble-chrome/BrowserFind.tsx b/src/renderer/src/components/browser-pane/assemble-chrome/BrowserFind.tsx index 050014a426d..30ab022f4ad 100644 --- a/src/renderer/src/components/browser-pane/assemble-chrome/BrowserFind.tsx +++ b/src/renderer/src/components/browser-pane/assemble-chrome/BrowserFind.tsx @@ -1,4 +1,4 @@ -import { ImeInput } from '@/lib/ime-text-field' +import { ImeInput } from '@/lib/text-input/ime/ime-text-field' import { useCallback, useEffect, useRef, useState } from 'react' import { ChevronUp, ChevronDown, X } from 'lucide-react' import { Button } from '@/components/ui/button' diff --git a/src/renderer/src/components/browser-pane/assemble-chrome/browser-page-context-menu.tsx b/src/renderer/src/components/browser-pane/assemble-chrome/browser-page-context-menu.tsx index cca92a6846e..076f6934cb1 100644 --- a/src/renderer/src/components/browser-pane/assemble-chrome/browser-page-context-menu.tsx +++ b/src/renderer/src/components/browser-pane/assemble-chrome/browser-page-context-menu.tsx @@ -9,7 +9,7 @@ import { } from 'react' import { createPortal } from 'react-dom' import type { BrowserPageContextMenuState } from '../describe-page/browser-page-types' -import { addAppKeyListener, removeAppKeyListener } from '@/lib/app-keyboard-router' +import { addAppKeyListener, removeAppKeyListener } from '@/lib/keyboard/app-keyboard-router' import { BrowserPageContextMenuItems } from './browser-page-context-menu-items' export function BrowserPageContextMenu({ diff --git a/src/renderer/src/components/browser-pane/assemble-chrome/use-browser-address-bar-escape.ts b/src/renderer/src/components/browser-pane/assemble-chrome/use-browser-address-bar-escape.ts index 7fb16815969..686b9fcb672 100644 --- a/src/renderer/src/components/browser-pane/assemble-chrome/use-browser-address-bar-escape.ts +++ b/src/renderer/src/components/browser-pane/assemble-chrome/use-browser-address-bar-escape.ts @@ -1,5 +1,5 @@ import { useCallback, useLayoutEffect, useRef, type RefObject } from 'react' -import { isImeOwnedKeyboardEvent } from '@/lib/ime-composition-keyboard-event' +import { isImeOwnedKeyboardEvent } from '@/lib/text-input/ime/ime-composition-keyboard-event' import { requestBrowserFocus } from '../host-guest/browser-focus' /** Escape in the bar, in the order of Chromium's OmniboxEditModel::OnEscapeKeyPressed. */ diff --git a/src/renderer/src/components/browser-pane/assemble-chrome/use-browser-page-chrome-focus.ts b/src/renderer/src/components/browser-pane/assemble-chrome/use-browser-page-chrome-focus.ts index 1a7537329ca..d4b2684c694 100644 --- a/src/renderer/src/components/browser-pane/assemble-chrome/use-browser-page-chrome-focus.ts +++ b/src/renderer/src/components/browser-pane/assemble-chrome/use-browser-page-chrome-focus.ts @@ -20,7 +20,7 @@ import { browserChromeShortcutOwnsEvent } from '../describe-page/browser-overlay import type { BrowserChromeShortcutScope } from '../describe-page/browser-page-types' import type { BrowserAddressBarSelection } from './browser-address-bar-edit-session' import type { BrowserPageGuestFocus } from './browser-page-guest-focus' -import { addAppKeyListener, removeAppKeyListener } from '@/lib/app-keyboard-router' +import { addAppKeyListener, removeAppKeyListener } from '@/lib/keyboard/app-keyboard-router' import { useBrowserHistoryShortcut } from '../history/use-browser-history-shortcut' import { useWebviewDragPassthroughActive } from '../host-guest/use-webview-drag-passthrough-active' diff --git a/src/renderer/src/components/browser-pane/assemble-chrome/use-browser-page-find-shortcuts.ts b/src/renderer/src/components/browser-pane/assemble-chrome/use-browser-page-find-shortcuts.ts index fc9178d22b2..114dd272f6a 100644 --- a/src/renderer/src/components/browser-pane/assemble-chrome/use-browser-page-find-shortcuts.ts +++ b/src/renderer/src/components/browser-pane/assemble-chrome/use-browser-page-find-shortcuts.ts @@ -4,7 +4,7 @@ import { useAppStore } from '@/store' import { keybindingMatchesAction } from '../../../../../shared/keybindings' import { browserChromeShortcutOwnsEvent } from '../describe-page/browser-overlay-shortcut-target' import type { BrowserChromeShortcutScope } from '../describe-page/browser-page-types' -import { addAppKeyListener, removeAppKeyListener } from '@/lib/app-keyboard-router' +import { addAppKeyListener, removeAppKeyListener } from '@/lib/keyboard/app-keyboard-router' export function useBrowserPageFindShortcuts({ browserTabId, diff --git a/src/renderer/src/components/browser-pane/history/browser-history-row-removal.ts b/src/renderer/src/components/browser-pane/history/browser-history-row-removal.ts index cb8266f14a1..d610616c990 100644 --- a/src/renderer/src/components/browser-pane/history/browser-history-row-removal.ts +++ b/src/renderer/src/components/browser-pane/history/browser-history-row-removal.ts @@ -1,5 +1,5 @@ import { useCallback } from 'react' -import { isImeOwnedKeyboardEvent } from '@/lib/ime-composition-keyboard-event' +import { isImeOwnedKeyboardEvent } from '@/lib/text-input/ime/ime-composition-keyboard-event' import { useAppStore } from '@/store' import type { BrowserAddressBarSuggestion } from '../assemble-chrome/browser-address-bar-suggestions' diff --git a/src/renderer/src/components/browser-pane/history/use-browser-history-shortcut.ts b/src/renderer/src/components/browser-pane/history/use-browser-history-shortcut.ts index e0802f8acc4..0997bc032ff 100644 --- a/src/renderer/src/components/browser-pane/history/use-browser-history-shortcut.ts +++ b/src/renderer/src/components/browser-pane/history/use-browser-history-shortcut.ts @@ -1,7 +1,7 @@ import { useEffect } from 'react' import { useAppStore } from '@/store' import { getShortcutPlatform } from '@/hooks/useShortcutLabel' -import { addAppKeyListener, removeAppKeyListener } from '@/lib/app-keyboard-router' +import { addAppKeyListener, removeAppKeyListener } from '@/lib/keyboard/app-keyboard-router' import { keybindingMatchesAction } from '../../../../../shared/keybindings' import { browserChromeShortcutOwnsEvent } from '../describe-page/browser-overlay-shortcut-target' import type { BrowserChromeShortcutScope } from '../describe-page/browser-page-types' diff --git a/src/renderer/src/components/browser-pane/host-guest/use-browser-page-keyboard-shortcuts.ts b/src/renderer/src/components/browser-pane/host-guest/use-browser-page-keyboard-shortcuts.ts index fe79b0c7444..5d77b5de61f 100644 --- a/src/renderer/src/components/browser-pane/host-guest/use-browser-page-keyboard-shortcuts.ts +++ b/src/renderer/src/components/browser-pane/host-guest/use-browser-page-keyboard-shortcuts.ts @@ -7,7 +7,7 @@ import type { BrowserChromeShortcutScope } from '../describe-page/browser-page-t import type { GrabIntent } from '../../../../../shared/browser/element-grab/browser-grab-types' import { isEditableKeyboardTarget } from './browser-keyboard' import { useBrowserPageWebviewShortcuts } from './use-browser-page-webview-shortcuts' -import { addAppKeyListener, removeAppKeyListener } from '@/lib/app-keyboard-router' +import { addAppKeyListener, removeAppKeyListener } from '@/lib/keyboard/app-keyboard-router' export function useBrowserPageKeyboardShortcuts({ browserTabId, diff --git a/src/renderer/src/components/browser-pane/host-guest/use-browser-page-webview-shortcuts.ts b/src/renderer/src/components/browser-pane/host-guest/use-browser-page-webview-shortcuts.ts index 7e669c902b7..1ef17342481 100644 --- a/src/renderer/src/components/browser-pane/host-guest/use-browser-page-webview-shortcuts.ts +++ b/src/renderer/src/components/browser-pane/host-guest/use-browser-page-webview-shortcuts.ts @@ -10,7 +10,7 @@ import { publishBrowserPageSiteDisplay, type BrowserPageZoomCommand } from './browser-page-zoom' -import { addAppKeyListener, removeAppKeyListener } from '@/lib/app-keyboard-router' +import { addAppKeyListener, removeAppKeyListener } from '@/lib/keyboard/app-keyboard-router' /** Handle chrome chords in the renderer and guest chords through main’s IPC; focused guests own a separate WebContents. */ export function useBrowserPageWebviewShortcuts({ diff --git a/src/renderer/src/components/browser-pane/stream-remote/remote-browser-page-context-menu.tsx b/src/renderer/src/components/browser-pane/stream-remote/remote-browser-page-context-menu.tsx index 9ff0d9d6f05..0c35b2407ea 100644 --- a/src/renderer/src/components/browser-pane/stream-remote/remote-browser-page-context-menu.tsx +++ b/src/renderer/src/components/browser-pane/stream-remote/remote-browser-page-context-menu.tsx @@ -16,7 +16,7 @@ import { type RemoteBrowserPaneNotice, type RemoteBrowserRuntimeTarget } from './remote-browser-page-input-model' -import { addAppKeyListener, removeAppKeyListener } from '@/lib/app-keyboard-router' +import { addAppKeyListener, removeAppKeyListener } from '@/lib/keyboard/app-keyboard-router' export function useRemoteBrowserPageContextMenu({ busy, diff --git a/src/renderer/src/components/browser-pane/stream-remote/use-remote-browser-page-chrome-chords.ts b/src/renderer/src/components/browser-pane/stream-remote/use-remote-browser-page-chrome-chords.ts index e21ddca989a..54d55532b8d 100644 --- a/src/renderer/src/components/browser-pane/stream-remote/use-remote-browser-page-chrome-chords.ts +++ b/src/renderer/src/components/browser-pane/stream-remote/use-remote-browser-page-chrome-chords.ts @@ -10,7 +10,7 @@ import { REMOTE_BROWSER_FIND_UNAVAILABLE_NOTICE_ID, type RemoteBrowserPaneNotice } from './remote-browser-page-input-model' -import { addAppKeyListener, removeAppKeyListener } from '@/lib/app-keyboard-router' +import { addAppKeyListener, removeAppKeyListener } from '@/lib/keyboard/app-keyboard-router' const FIND_UNAVAILABLE_NOTICE_MS = 4000 diff --git a/src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.tsx b/src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.tsx index c56fe70029f..8f3d5dceeb5 100644 --- a/src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.tsx +++ b/src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.tsx @@ -34,7 +34,7 @@ import { useDocPreviewWebviewHistory } from './doc-preview-webview-history' import { useDocPreviewGuestFocus } from './use-doc-preview-guest-focus' import { useDocPreviewGuestTools } from './use-doc-preview-guest-tools' import { useConfirmationDialog } from '@/components/confirmation-dialog-context' -import { formatClipboardFilePath } from '@/lib/clipboard-file-path' +import { formatClipboardFilePath } from '@/lib/clipboard/clipboard-file-path' import { getOpenFileRevealOwner } from '@/lib/reveal-in-file-manager' import { subscribeDocPreviewExternalLinkConfirmation } from './doc-preview-external-link-confirmation' diff --git a/src/renderer/src/components/browser-pane/workspace-doc/doc-preview-document-chip.tsx b/src/renderer/src/components/browser-pane/workspace-doc/doc-preview-document-chip.tsx index 36c64b7a1e4..c0b6acb1699 100644 --- a/src/renderer/src/components/browser-pane/workspace-doc/doc-preview-document-chip.tsx +++ b/src/renderer/src/components/browser-pane/workspace-doc/doc-preview-document-chip.tsx @@ -1,7 +1,7 @@ import { Check, FileCode2 } from 'lucide-react' import { Badge } from '@/components/ui/badge' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' -import { useClipboardTextCopyFeedback } from '@/hooks/use-clipboard-text-copy-feedback' +import { useClipboardTextCopyFeedback } from '@/hooks/clipboard/use-clipboard-text-copy-feedback' import { useTranslate } from '@/i18n/use-translate' import type { DocPreviewDocumentIdentity } from './doc-preview-document-identity' diff --git a/src/renderer/src/components/cmd-j/PaletteFilterFieldOptions.tsx b/src/renderer/src/components/cmd-j/PaletteFilterFieldOptions.tsx index 5fa06bbd497..f3af842801a 100644 --- a/src/renderer/src/components/cmd-j/PaletteFilterFieldOptions.tsx +++ b/src/renderer/src/components/cmd-j/PaletteFilterFieldOptions.tsx @@ -1,4 +1,4 @@ -import { ImeInput } from '@/lib/ime-text-field' +import { ImeInput } from '@/lib/text-input/ime/ime-text-field' import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useVirtualizer } from '@tanstack/react-virtual' import { Check, ChevronLeft, SearchIcon } from 'lucide-react' diff --git a/src/renderer/src/components/cmd-j/quick-actions.ts b/src/renderer/src/components/cmd-j/quick-actions.ts index 2925bb5c6cb..f101601efeb 100644 --- a/src/renderer/src/components/cmd-j/quick-actions.ts +++ b/src/renderer/src/components/cmd-j/quick-actions.ts @@ -1,4 +1,4 @@ -import { gitGraphSupport, openGitGraph } from '@/lib/git-graph-tab' +import { gitGraphSupport, openGitGraph } from '@/lib/source-control/graph/git-graph-tab' import { GitGraph, FileText, diff --git a/src/renderer/src/components/cmd-j/task-search-activation.ts b/src/renderer/src/components/cmd-j/task-search-activation.ts index 19ab530753b..2b7989e3d56 100644 --- a/src/renderer/src/components/cmd-j/task-search-activation.ts +++ b/src/renderer/src/components/cmd-j/task-search-activation.ts @@ -11,7 +11,7 @@ import { getLinkedWorkItemSuggestedName, type LinkedWorkItemSummary } from '@/lib/new-workspace' -import { buildLinearIssueLinkedWorkItem } from '@/lib/linear-linked-work-item' +import { buildLinearIssueLinkedWorkItem } from '@/lib/work-items/linear/linear-linked-work-item' import type { Worktree } from '../../../../shared/worktree/types' import { haveConflictingTaskSourceAccounts, diff --git a/src/renderer/src/components/cmd-j/task-search-requests.ts b/src/renderer/src/components/cmd-j/task-search-requests.ts index e0530de53bc..46145b59e35 100644 --- a/src/renderer/src/components/cmd-j/task-search-requests.ts +++ b/src/renderer/src/components/cmd-j/task-search-requests.ts @@ -8,7 +8,7 @@ import { jiraStatusForTarget } from '@/runtime/jira/runtime-jira-project-client' import { ipcOrRuntimeTargetForHostId } from '@/runtime/rpc/targets/runtime-client-target' import { gitLabApiFor } from '@/runtime/repository-gitlab/gitlab-owner-api' import { resolveJiraSourceHostId } from '../../../../shared/jira/jira-source-host' -import { getJiraAccountLabel } from '@/lib/jira-account-label' +import { getJiraAccountLabel } from '@/lib/work-items/jira/jira-account-label' import { getRepoExecutionHostId, parseExecutionHostId, @@ -28,7 +28,7 @@ import type { GlobalSettings } from '../../../../shared/global-settings-types' import type { PaletteFilterState } from './palette-filter' import { getTaskEligibleRepos } from '../task-page/task-page-default-repo-selection' import { getRepoBackedProviderAvailability } from '../task-source-provider-availability' -import { getTaskPageRepoSourceContext } from '../../lib/task-repo-source-context' +import { getTaskPageRepoSourceContext } from '../../lib/work-items/task-repo-source-context' import { searchJiraIssuesByKeyOrText } from '../jira-key-or-text-search' import { bindTaskPageJiraItemSourceContext } from '../task-page/jira/task-page-jira-item-source-context' import { makeCmdJTaskSearchResult, type CmdJTaskSearchResult } from './task-search-results' diff --git a/src/renderer/src/components/contextual-tours/ContextualTourOverlaySurface.tsx b/src/renderer/src/components/contextual-tours/ContextualTourOverlaySurface.tsx index 059e2c1a276..e4184bb60b3 100644 --- a/src/renderer/src/components/contextual-tours/ContextualTourOverlaySurface.tsx +++ b/src/renderer/src/components/contextual-tours/ContextualTourOverlaySurface.tsx @@ -28,7 +28,7 @@ import { type ContextualTourFloatingPosition } from './contextual-tour-floating-position' import { useTranslate } from '@/i18n/use-translate' -import { addAppKeyListener, removeAppKeyListener } from '@/lib/app-keyboard-router' +import { addAppKeyListener, removeAppKeyListener } from '@/lib/keyboard/app-keyboard-router' import { hasVisibleOverlay } from '@/lib/visible-overlay' import { getMeasurableContextualTourTarget } from './contextual-tour-gate' diff --git a/src/renderer/src/components/contextual-tours/contextual-tour-step-copy.ts b/src/renderer/src/components/contextual-tours/contextual-tour-step-copy.ts index fdf872d9272..d7b780715b6 100644 --- a/src/renderer/src/components/contextual-tours/contextual-tour-step-copy.ts +++ b/src/renderer/src/components/contextual-tours/contextual-tour-step-copy.ts @@ -1,5 +1,5 @@ import { translate } from '@/i18n/i18n' -import { getShortcutPlatform } from '@/lib/shortcut-platform' +import { getShortcutPlatform } from '@/lib/keyboard/shortcut-platform' import type { ContextualTourStepId } from '../../../../shared/contextual-tours' export type LocalizedContextualTourStepCopy = { diff --git a/src/renderer/src/components/dashboard-popout/AgentTerminalPreview.clipboard-routes.test.tsx b/src/renderer/src/components/dashboard-popout/AgentTerminalPreview.clipboard-routes.test.tsx index 7b1d8bec869..e8c49b42fec 100644 --- a/src/renderer/src/components/dashboard-popout/AgentTerminalPreview.clipboard-routes.test.tsx +++ b/src/renderer/src/components/dashboard-popout/AgentTerminalPreview.clipboard-routes.test.tsx @@ -4,16 +4,10 @@ import '@testing-library/jest-dom/vitest' import type { ITerminalOptions } from '@xterm/xterm' import { act, cleanup, render, waitFor } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { - TERMINAL_PASTE_CHUNK_MAX_BYTES, - TERMINAL_PASTE_DIRECT_MAX_BYTES -} from '@/components/terminal-pane/input/paste/terminal-paste-limits' + import '@/components/terminal-pane/input/paste/terminal-bracketed-paste' -import { - BRACKETED_PASTE_END, - BRACKETED_PASTE_START -} from '../../../../shared/terminal/input/terminal-bracketed-paste-text' -import { dispatchAppMenuPasteEvent } from '@/lib/app-menu-paste' + +import { dispatchAppMenuPasteEvent } from '@/lib/clipboard/menu/app-menu-paste' const terminalHarness = vi.hoisted(() => ({ instances: [] as { @@ -117,7 +111,7 @@ vi.mock('@/components/terminal-pane/input/terminal-user-input-signal', () => ({ vi.mock('@/hooks/theme/use-system-prefers-dark', () => ({ useSystemPrefersDark: () => false })) -vi.mock('@/lib/shortcut-platform', () => ({ +vi.mock('@/lib/keyboard/shortcut-platform', () => ({ getShortcutPlatform: () => platformState.value })) vi.mock('@/components/terminal-pane/input/ime/terminal-ime-native-text-forwarder', () => ({ @@ -238,58 +232,4 @@ describe('AgentTerminalPreview clipboard routes', () => { expect(terminal.paste).not.toHaveBeenCalled() expect(input).not.toHaveBeenCalled() }) - - it('streams large pastes as bounded IPC payloads instead of one renderer-blocking write', async () => { - const encoder = new TextEncoder() - const multibytePrefix = '😀'.repeat(TERMINAL_PASTE_DIRECT_MAX_BYTES / 4 + 1) - const largePaste = `${multibytePrefix}\r\nnext\n` - const expectedPaste = `${multibytePrefix}\rnext\r` - readClipboardText.mockResolvedValueOnce(largePaste) - const view = render() - await waitFor(() => expect(terminalHarness.instances).toHaveLength(1)) - const terminal = terminalHarness.instances[0]! - focusInsidePreview(view.container) - - act(() => { - dispatchAppMenuPasteEvent() - }) - const expectedChunks = Math.ceil( - encoder.encode(expectedPaste).byteLength / TERMINAL_PASTE_CHUNK_MAX_BYTES - ) - await waitFor(() => expect(input).toHaveBeenCalledTimes(expectedChunks)) - - const payloads = input.mock.calls.map(([, data]) => data as string) - expect(terminal.paste).not.toHaveBeenCalled() - expect(payloads.join('')).toBe(expectedPaste) - expect( - payloads.every( - (payload) => encoder.encode(payload).byteLength <= TERMINAL_PASTE_CHUNK_MAX_BYTES - ) - ).toBe(true) - }) - - it('closes a bracketed large paste when focus changes between chunks', async () => { - readClipboardText.mockResolvedValueOnce('x'.repeat(TERMINAL_PASTE_DIRECT_MAX_BYTES + 1)) - const view = render() - await waitFor(() => expect(terminalHarness.instances).toHaveLength(1)) - const terminal = terminalHarness.instances[0]! - terminal.modes.bracketedPasteMode = true - focusInsidePreview(view.container) - const outsideInput = document.createElement('input') - view.container.appendChild(outsideInput) - input.mockImplementationOnce(async () => { - outsideInput.focus() - return true - }) - - act(() => { - dispatchAppMenuPasteEvent() - }) - await waitFor(() => expect(input).toHaveBeenCalledTimes(2)) - - expect(input.mock.calls.map(([, data]) => data)).toEqual([ - BRACKETED_PASTE_START, - BRACKETED_PASTE_END - ]) - }) }) diff --git a/src/renderer/src/components/dashboard-popout/AgentTerminalPreview.test.tsx b/src/renderer/src/components/dashboard-popout/AgentTerminalPreview.test.tsx index 108b72f1764..8a78d7a3b65 100644 --- a/src/renderer/src/components/dashboard-popout/AgentTerminalPreview.test.tsx +++ b/src/renderer/src/components/dashboard-popout/AgentTerminalPreview.test.tsx @@ -109,7 +109,7 @@ vi.mock('@/components/terminal-pane/input/terminal-user-input-signal', () => ({ vi.mock('@/hooks/theme/use-system-prefers-dark', () => ({ useSystemPrefersDark: () => false })) -vi.mock('@/lib/shortcut-platform', () => ({ +vi.mock('@/lib/keyboard/shortcut-platform', () => ({ getShortcutPlatform: () => platformState.value })) vi.mock('@/components/terminal-pane/input/ime/terminal-ime-native-text-forwarder', () => ({ @@ -384,28 +384,4 @@ describe('AgentTerminalPreview', () => { expect(terminalHarness.userInputDispose).toHaveBeenCalledTimes(1) expect(unsubscribe).toHaveBeenCalledWith('pty-1') }) - - it('delays repeated capture after an overflow and cancels the retry on unmount', async () => { - vi.useFakeTimers() - connect.mockResolvedValue({ - snapshot: { data: 'screen', cols: 80, rows: 24, seq: 1 }, - replay: [], - resyncRequired: true - }) - const view = render() - await vi.waitFor(() => expect(terminalHarness.instances).toHaveLength(1)) - const terminal = terminalHarness.instances[0]! - expect(connect).toHaveBeenCalledTimes(1) - - act(() => terminal.writeCallbacks.splice(0).forEach((callback) => callback())) - await vi.advanceTimersByTimeAsync(149) - expect(connect).toHaveBeenCalledTimes(1) - await vi.advanceTimersByTimeAsync(1) - expect(connect).toHaveBeenCalledTimes(2) - - act(() => terminal.writeCallbacks.splice(0).forEach((callback) => callback())) - view.unmount() - await vi.advanceTimersByTimeAsync(150) - expect(connect).toHaveBeenCalledTimes(2) - }) }) diff --git a/src/renderer/src/components/dashboard-popout/DashboardPopoutRoot.tsx b/src/renderer/src/components/dashboard-popout/DashboardPopoutRoot.tsx index 4c10712d8fa..21826639e31 100644 --- a/src/renderer/src/components/dashboard-popout/DashboardPopoutRoot.tsx +++ b/src/renderer/src/components/dashboard-popout/DashboardPopoutRoot.tsx @@ -1,5 +1,5 @@ -import { useAppMenuPaste } from '@/hooks/useAppMenuPaste' -import { useAppMenuSelectionActions } from '@/hooks/useAppMenuSelectionActions' +import { useAppMenuPaste } from '@/hooks/clipboard/menu/useAppMenuPaste' +import { useAppMenuSelectionActions } from '@/hooks/app-commands/useAppMenuSelectionActions' import { AgentKanbanBoard } from './AgentKanbanBoard' import { useDashboardSnapshot } from './useDashboardSnapshot' import { useAgentDashboardTelemetry } from './use-agent-dashboard-telemetry' diff --git a/src/renderer/src/components/dashboard-popout/dashboard-preview-dom-test-fixture.ts b/src/renderer/src/components/dashboard-popout/dashboard-preview-dom-test-fixture.ts index 90dce83bcd2..563b9c0589a 100644 --- a/src/renderer/src/components/dashboard-popout/dashboard-preview-dom-test-fixture.ts +++ b/src/renderer/src/components/dashboard-popout/dashboard-preview-dom-test-fixture.ts @@ -54,86 +54,6 @@ export function installPreviewDomFixture(): () => void { return () => style.remove() } -export function observePreviewListeners(elements: HTMLElement[]) { - type Registration = { - target: EventTarget - type: string - listener: EventListenerOrEventListenerObject - capture: boolean - wrapped: EventListener - } - const active = new Set() - let callbacks = 0 - const spies = [document, window, ...elements].flatMap((target) => { - const nativeAdd: EventTarget['addEventListener'] = target.addEventListener - const nativeRemove: EventTarget['removeEventListener'] = target.removeEventListener - const add = vi.spyOn(target, 'addEventListener').mockImplementation(function ( - this: EventTarget, - type: string, - listener: EventListenerOrEventListenerObject | null, - options?: boolean | AddEventListenerOptions - ) { - if (!listener) { - return - } - const capture = typeof options === 'boolean' ? options : Boolean(options?.capture) - if ( - Array.from(active).some( - (r) => - r.target === this && r.type === type && r.listener === listener && r.capture === capture - ) - ) { - return - } - const wrapped: EventListener = function (this: EventTarget, event) { - if (this === document || this === window) { - callbacks++ - } - if (typeof listener === 'function') { - listener.call(this, event) - } else { - listener.handleEvent(event) - } - } - active.add({ target: this, type, listener, capture, wrapped }) - nativeAdd.call(this, type, wrapped, options) - }) - const remove = vi.spyOn(target, 'removeEventListener').mockImplementation(function ( - this: EventTarget, - type: string, - listener: EventListenerOrEventListenerObject | null, - options?: boolean | EventListenerOptions - ) { - const capture = typeof options === 'boolean' ? options : Boolean(options?.capture) - const registration = Array.from(active).find( - (r) => - r.target === this && r.type === type && r.listener === listener && r.capture === capture - ) - if (registration) { - nativeRemove.call(this, type, registration.wrapped, options) - active.delete(registration) - } else { - nativeRemove.call(this, type, listener, options) - } - }) - return [add, remove] - }) - return { - active, - get callbacks() { - return callbacks - }, - resetCallbacks: () => { - callbacks = 0 - }, - restore: () => { - for (const spy of spies) { - spy.mockRestore() - } - } - } -} - export function mountPreviewLinkInteraction( source: DashboardPreviewLinkSource, readSettings: () => GlobalSettings | null, diff --git a/src/renderer/src/components/dashboard-popout/dashboard-preview-link-composition.test.tsx b/src/renderer/src/components/dashboard-popout/dashboard-preview-link-composition.test.tsx index 4994fbe105d..bf688a8de9d 100644 --- a/src/renderer/src/components/dashboard-popout/dashboard-preview-link-composition.test.tsx +++ b/src/renderer/src/components/dashboard-popout/dashboard-preview-link-composition.test.tsx @@ -8,17 +8,14 @@ import { } from '@testing-library/react' import { afterEach, beforeEach, expect, it, vi } from 'vitest' import type { RemoteRuntimeMultiplexedTerminalCallbacks } from '@/runtime/hosted-terminals/streams/remote-terminal-stream-contracts' -import { TooltipProvider } from '@/components/ui/tooltip' + import { parseRemoteRuntimePtyId } from '../../../../shared/remote-runtime/remote-runtime-pty-id' import { resetRemoteRuntimeTerminalPreviewSessionsForTests } from './remote-runtime-terminal-preview-api' import { installPreviewDomFixture, mountPreviewLinkInteraction } from './dashboard-preview-dom-test-fixture' -import { verifyPreviewLinkPopulation } from './dashboard-preview-link-population-test-fixture' -import { toast } from 'sonner' -import { runtimeStatuses } from '@/store/slices/browser/browser-slice-test-harness' import type { openStandaloneTerminal } from '../standalone-terminal/open-standalone-terminal' import { getDefaultSettings } from '../../../../shared/constants' @@ -33,14 +30,12 @@ import { registerWorkspaceHttpLinkBrowserOpener } from '@/lib/browser/navigation/http-link-routing' import { openWorkspaceBrowserTabForLink } from '@/lib/browser/workspace/workspace-browser-tab-open' -import { AgentTerminalPreview } from './AgentTerminalPreview' + import { terminalPreviewApiFor } from './terminal-preview-api' import { AgentKanbanBoard } from './AgentKanbanBoard' import type { DashboardCard } from '../../../../shared/dashboard-snapshot' import type { GlobalSettings } from '../../../../shared/global-settings-types' -import type { ExecutionHostId } from '../../../../shared/execution-host' -import type { DashboardPreviewLinkSource } from './dashboard-preview-http-actions' const mocks = vi.hoisted(() => ({ getState: vi.fn(), @@ -76,9 +71,7 @@ vi.mock( }) }) ) -function render(ui: React.ReactElement) { - return renderWithoutApp(ui, { wrapper: TooltipProvider }) -} + vi.mock('@/store', () => ({ useAppStore: Object.assign( (selector: (state: ReturnType) => unknown) => selector(mocks.getState()), @@ -88,7 +81,7 @@ vi.mock('@/store', () => ({ vi.mock('@/hooks/theme/use-system-prefers-dark', () => ({ useSystemPrefersDark: () => false })) -vi.mock('@/lib/shortcut-platform', () => ({ getShortcutPlatform: () => 'linux' })) +vi.mock('@/lib/keyboard/shortcut-platform', () => ({ getShortcutPlatform: () => 'linux' })) vi.mock('sonner', () => ({ toast: { error: vi.fn(), success: vi.fn(), info: vi.fn(), warning: vi.fn() } })) @@ -215,104 +208,6 @@ async function output(data: string, ptyId = 'pty-1') { } const osc = `\x1b]8;;${URL_TO_OPEN}\x07hidden label\x1b]8;;\x07` -it.each(['actions', 'open'] as const)( - 'retains the provider-owned hidden OSC target across unrelated live row output for %s and context', - async (behavior) => { - store.setState((state) => ({ - settings: { ...state.settings!, terminalLinkClickBehavior: behavior } - })) - const view = render() - await waitFor(() => screenFor(view.container)) - await output(`\x1b[?1000h\x1b[?1006h${osc}\x1b[3;1Hprogress 1`) - const screen = screenFor(view.container) - const terminal = view.container.querySelector('.xterm')! - pointer(screen, 'mousemove', { buttons: 0 }) - await waitFor(() => expect(terminal.getAttribute('title')).toBe(URL_TO_OPEN)) - await output('\x1b[3;1Hprogress 2') - await waitFor(() => expect(view.container.textContent).toContain('progress 2')) - expect(terminal.getAttribute('title')).toBe(URL_TO_OPEN) - // Keep the pointer stationary: the retained provider does not emit another hover. - pointer(screen, 'mousedown') - pointer(screen, 'mouseup') - if (behavior === 'actions') { - const system = await view.findByRole('button', { name: /System Browser/ }) - expect(document.querySelector('[data-terminal-link-destination]')?.textContent).toBe( - URL_TO_OPEN - ) - fireEvent.click(system) - } - await waitFor(() => expect(openUrl).toHaveBeenCalledExactlyOnceWith(URL_TO_OPEN)) - expect(input.mock.calls.map(([, data]) => data)).toEqual([]) - await output('\x1b[3;1Hprogress 3') - const context = pointer(screen, 'contextmenu', { button: 2, buttons: 0 }) - expect(context.defaultPrevented).toBe(true) - const copy = await view.findByRole('button', { name: 'Copy link' }) - expect(document.querySelector('[data-terminal-link-destination]')?.textContent).toBe( - URL_TO_OPEN - ) - fireEvent.click(copy) - await waitFor(() => expect(window.api.ui.writeClipboardText).toHaveBeenCalledWith(URL_TO_OPEN)) - pointer(screen, 'mouseleave', { buttons: 0 }) - expect(terminal.hasAttribute('title')).toBe(false) - } -) - -function seedRemoteWorkspace( - hostId: ExecutionHostId, - environmentId: string, - streaming: boolean -): void { - store.setState((state) => ({ - activeWorkspaceExecutionHostId: hostId, - worktreesByRepo: { - repo1: state.worktreesByRepo.repo1!.map((row) => ({ - ...row, - hostId, - runtimeOwnerEnvironmentId: environmentId - })) - }, - unifiedTabsByWorktree: { - [WORKSPACE]: state.unifiedTabsByWorktree[WORKSPACE]!.map((tab) => ({ - ...tab, - executionHostId: hostId - })) - }, - runtimeStatusByEnvironmentId: new Map([ - [environmentId, runtimeStatuses(streaming ? ['browser.screencast.v1'] : []).get('env-1')!] - ]), - settings: { ...state.settings!, browserClientHostedRemoteEnabled: false, openLinksInApp: true } - })) -} - -it('gives one useful refusal when a real saved chooser action loses runtime browser eligibility', async () => { - const hostId = 'runtime:e' - const ptyId = 'remote:e@@handle-1' - const linkSource = { - ...source, - executionHostId: hostId, - ptyId - } satisfies DashboardPreviewLinkSource - const foreground = vi.fn() - seedRemoteWorkspace(hostId, 'e', true) - const props = { ptyId, linkSource, onBrowserForeground: foreground } - const view = render() - await waitFor(() => screenFor(view.container)) - await output(osc, ptyId) - const screen = screenFor(view.container) - pointer(screen, 'mousedown') - pointer(screen, 'mouseup') - const savedAction = await view.findByRole('button', { name: /Orca Browser/ }) - // Actual failed-status refresh shape, after the real request has captured its action. - store.setState({ runtimeStatusByEnvironmentId: new Map([['e', { status: null, checkedAt: 2 }]]) }) - fireEvent.click(savedAction) - await waitFor(() => expect(toast.error).toHaveBeenCalledExactlyOnceWith('Unable to open URL.')) - expect(document.querySelector('[data-link-action-popover]')).toBeNull() - expect(store.getState().browserPagesByWorkspace).toEqual({}) - expect(openUrl).not.toHaveBeenCalled() - expect(foreground).not.toHaveBeenCalled() - expect(input.mock.calls.map(([, data]) => data)).toEqual([]) -}) - function mountedInteraction() { return mountPreviewLinkInteraction( source, @@ -321,22 +216,6 @@ function mountedInteraction() { ) } -it.each([10, 100])( - 'bounds actual registration, document callbacks and target input/output at P=%s; reset/dispose leave zero adapter residue', - async (population) => { - await verifyPreviewLinkPopulation( - population, - source, - () => store.getState().settings, - (instance) => instances.push(instance), - osc, - URL_TO_OPEN, - pointer, - screenFor - ) - } -) - it('retains actual boolean/Promise forwarding results and flushes unclaimed reports in order, with finite overflow and cleanup', async () => { const preview = mountedInteraction() const interaction = preview.install() diff --git a/src/renderer/src/components/dashboard-popout/dashboard-preview-link-population-test-fixture.ts b/src/renderer/src/components/dashboard-popout/dashboard-preview-link-population-test-fixture.ts deleted file mode 100644 index fd534a58981..00000000000 --- a/src/renderer/src/components/dashboard-popout/dashboard-preview-link-population-test-fixture.ts +++ /dev/null @@ -1,267 +0,0 @@ -import { expect, vi } from 'vitest' -import { waitFor } from '@testing-library/react' -import { Terminal } from '@xterm/xterm' -import type { GlobalSettings } from '../../../../shared/global-settings-types' -import type { openStandaloneTerminal } from '../standalone-terminal/open-standalone-terminal' -import type { DashboardPreviewLinkSource } from './dashboard-preview-http-actions' -import { - mountPreviewLinkInteraction, - observePreviewListeners -} from './dashboard-preview-dom-test-fixture' - -function observePreviewTimers() { - type NativeId = ReturnType - type Callback = Parameters[0] - type Record = { - callback: Callback - callbackId: number - name: string - delay: number | undefined - stack: string | undefined - id?: NativeId - executed: number - canceled: number - } - const records: Record[] = [] - const callbackIds = new Map() - const clears: Parameters[0][] = [] - let peakFallbacks = 0 - const ownerOf = (record: Record) => { - if ( - record.name === 'finishDeferredInput' && - record.stack?.includes('terminal-link-pty-mouse-suppression.ts') - ) { - return 'adapter-fallback' - } - if ( - record.delay === 300000 && - record.stack?.includes('/@xterm/xterm/') && - record.stack.includes('_resetIdleTimer') - ) { - return 'xterm-cursor-idle' - } - return 'unattributed' - } - const microtasks: { name: string; executed: boolean }[] = [] - const nativeSet = window.setTimeout - const nativeClear = window.clearTimeout - const nativeQueue = globalThis.queueMicrotask - const set = vi.spyOn(window, 'setTimeout').mockImplementation(function ( - this: Window, - callback, - delay, - ...args - ) { - let callbackId = callbackIds.get(callback) - if (callbackId === undefined) { - callbackId = callbackIds.size + 1 - callbackIds.set(callback, callbackId) - } - const record: Record = { - callback, - callbackId, - name: typeof callback === 'function' ? callback.name : 'string', - delay, - stack: new Error('Preview timer owner registration').stack, - executed: 0, - canceled: 0 - } - records.push(record) - const wrapped = - typeof callback === 'function' - ? function (this: unknown, ...values: Parameters) { - try { - return callback.apply(this, values) - } finally { - record.executed++ - } - } - : callback - record.id = nativeSet.call(this, wrapped, delay, ...args) - peakFallbacks = Math.max( - peakFallbacks, - records.filter( - (r) => ownerOf(r) === 'adapter-fallback' && r.canceled === 0 && r.executed === 0 - ).length - ) - return record.id - }) - const clear = vi.spyOn(window, 'clearTimeout').mockImplementation(function (this: Window, id) { - clears.push(id) - for (const record of records) { - if (record.id === id) { - record.canceled++ - } - } - return nativeClear.call(this, id) - }) - const queue = vi.spyOn(globalThis, 'queueMicrotask').mockImplementation(function ( - this: typeof globalThis, - callback - ) { - const record = { name: callback.name, executed: false } - microtasks.push(record) - return nativeQueue.call(this, function (this: unknown) { - try { - return callback.call(this) - } finally { - record.executed = true - } - }) - }) - const summary = () => ({ - timers: records.map((record, index) => ({ - id: index + 1, - callbackId: record.callbackId, - name: record.name, - delay: record.delay, - stack: record.stack, - owner: ownerOf(record), - canceled: record.canceled, - executed: record.executed, - matchingClearCalls: clears.filter((id) => id === record.id).length - })), - microtasks - }) - return { - records, - microtasks, - ownerOf, - get peakFallbacks() { - return peakFallbacks - }, - summary, - restore: () => { - set.mockRestore() - clear.mockRestore() - queue.mockRestore() - } - } -} - -export async function verifyPreviewLinkPopulation( - population: number, - source: DashboardPreviewLinkSource, - readSettings: () => GlobalSettings | null, - trackInstance: (instance: ReturnType) => void, - osc: string, - uri: string, - pointer: (screen: HTMLElement, type: string, options?: MouseEventInit) => MouseEvent, - screenFor: (container: HTMLElement) => HTMLElement -) { - const baseline = Array.from({ length: population }, () => - mountPreviewLinkInteraction(source, readSettings, trackInstance, false) - ) - const defaultFirst = baseline[0]! - await new Promise((resolve) => - defaultFirst.instance.write(osc, { live: true, onDone: resolve }) - ) - const defaultScreen = screenFor(defaultFirst.container) - pointer(defaultScreen, 'mousemove', { buttons: 0 }) - await waitFor(() => - expect(defaultFirst.instance.terminal.element?.getAttribute('title')).toBe(uri) - ) - const defaults = observePreviewTimers() - pointer(defaultScreen, 'mousedown') - pointer(defaultScreen, 'mouseup') - for (const preview of baseline) { - preview.instance.dispose() - } - await Promise.resolve() - const defaultSummary = defaults.summary() - expect(defaults.records.every((record) => defaults.ownerOf(record) === 'xterm-cursor-idle')).toBe( - true - ) - expect(defaults.records.every((record) => record.canceled > 0 || record.executed > 0)).toBe(true) - defaults.restore() - const provider = vi.spyOn(Terminal.prototype, 'registerLinkProvider') - const previews = Array.from({ length: population }, () => - mountPreviewLinkInteraction(source, readSettings, trackInstance) - ) - expect(provider).toHaveBeenCalledTimes(population) - const observation = observePreviewListeners(previews.map((p) => p.instance.terminal.element!)) - const { active } = observation - const interactions = previews.map((preview) => preview.install()) - expect(provider).toHaveBeenCalledTimes(population) - expect(active.size).toBe(9 * population) - document.dispatchEvent(new MouseEvent('mousemove', { bubbles: true })) - expect(observation.callbacks).toBe(population) - observation.resetCallbacks() - const first = previews[0]! - await new Promise((resolve) => first.instance.write(osc, { live: true, onDone: resolve })) - const gettersBefore = previews.map((p) => [ - p.getSource.mock.calls.length, - p.getSettings.mock.calls.length - ]) - for (let i = 0; i < 10; i++) { - first.instance.terminal.input('typed') - } - expect(first.sink).toHaveBeenCalledTimes(10) - expect(previews.slice(1).every((p) => p.sink.mock.calls.length === 0)).toBe(true) - await new Promise((resolve) => - first.instance.write(' output', { live: true, onDone: resolve }) - ) - expect( - previews.map((p) => [p.getSource.mock.calls.length, p.getSettings.mock.calls.length]) - ).toEqual(gettersBefore) - expect(first.instance.terminal.buffer.active.getLine(0)?.translateToString(true)).toContain( - 'output' - ) - const screen = screenFor(first.container) - const timers = observePreviewTimers() - try { - pointer(screen, 'mousedown') - expect(active.size).toBeLessThanOrEqual(9 * population + 16) - pointer(screen, 'mouseup') - expect(first.request).toHaveBeenCalledOnce() - const adapterTimers = timers.records.filter( - (record) => timers.ownerOf(record) === 'adapter-fallback' - ) - const nativeTimers = timers.records.filter( - (record) => timers.ownerOf(record) === 'xterm-cursor-idle' - ) - expect(timers.records.some((record) => timers.ownerOf(record) === 'unattributed')).toBe(false) - expect(adapterTimers.length).toBeGreaterThan(0) - expect(adapterTimers.length).toBeLessThanOrEqual(3) - expect(timers.peakFallbacks).toBeLessThanOrEqual(1) - expect(nativeTimers).toHaveLength(defaultSummary.timers.length) - for (const interaction of interactions) { - interaction.dispose() - } - expect(adapterTimers.every((record) => record.canceled > 0)).toBe(true) - expect(active.size).toBe(0) - const reset = previews.map((p) => p.install()) - expect(provider).toHaveBeenCalledTimes(population) - expect(active.size).toBe(9 * population) - for (const interaction of reset) { - interaction.dispose() - } - expect(active.size).toBe(0) - for (const preview of previews) { - preview.instance.dispose() - } - await Promise.resolve() - expect(nativeTimers.every((record) => record.canceled > 0 || record.executed > 0)).toBe(true) - const gestureClears = timers.microtasks.filter((task) => task.name === 'clear') - expect(gestureClears).toHaveLength(population) - expect(gestureClears.every((task) => task.executed)).toBe(true) - console.info( - 'TIMER-OWNER-IDENTITIES', - JSON.stringify({ - population, - default: defaultSummary, - adapter: timers.summary(), - peakFallbacks: timers.peakFallbacks - }) - ) - } finally { - for (const interaction of interactions) { - interaction.dispose() - } - for (const preview of previews) { - preview.instance.dispose() - } - timers.restore() - observation.restore() - } -} diff --git a/src/renderer/src/components/dashboard-popout/preview-terminal-app-menu-clipboard.ts b/src/renderer/src/components/dashboard-popout/preview-terminal-app-menu-clipboard.ts index a3891cb0fbd..9b154afdbfb 100644 --- a/src/renderer/src/components/dashboard-popout/preview-terminal-app-menu-clipboard.ts +++ b/src/renderer/src/components/dashboard-popout/preview-terminal-app-menu-clipboard.ts @@ -1,10 +1,10 @@ import type { Terminal } from '@xterm/xterm' -import { isEditableTarget } from '@/lib/editable-target' -import { APP_MENU_PASTE_EVENT } from '@/lib/app-menu-paste' +import { isEditableTarget } from '@/lib/text-input/focus/editable-target' +import { APP_MENU_PASTE_EVENT } from '@/lib/clipboard/menu/app-menu-paste' import { APP_MENU_SELECTION_ACTION_EVENT, type AppMenuSelectionAction -} from '@/lib/app-menu-selection-actions' +} from '@/lib/app-commands/app-menu-selection-actions' import { copyTerminalSelection } from '@/components/terminal-pane/clipboard/terminal-selection-copy' import type { PreviewTerminalPasteSource } from './preview-terminal-paste' diff --git a/src/renderer/src/components/dashboard-popout/preview-terminal-ime-bridge-kitty-bytes.test.ts b/src/renderer/src/components/dashboard-popout/preview-terminal-ime-bridge-kitty-bytes.test.ts index 77b897f3742..99642873d34 100644 --- a/src/renderer/src/components/dashboard-popout/preview-terminal-ime-bridge-kitty-bytes.test.ts +++ b/src/renderer/src/components/dashboard-popout/preview-terminal-ime-bridge-kitty-bytes.test.ts @@ -3,7 +3,7 @@ import { Terminal } from '@xterm/xterm' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { installPreviewImeBridge } from './preview-terminal-ime-bridge' -vi.mock('@/lib/shortcut-platform', () => ({ +vi.mock('@/lib/keyboard/shortcut-platform', () => ({ getShortcutPlatform: () => 'darwin' })) @@ -11,13 +11,10 @@ const COMMA = { key: ',', code: 'Comma', keyCode: 188 } type Session = { emitted: string[] - flagReads: number setFlags: (flags: number) => void keydown: (init?: { shiftKey?: boolean; repeat?: boolean; key?: string; code?: string }) => void keyup: (init?: { shiftKey?: boolean; key?: string; code?: string }) => void commit: (text: string) => void - /** An input event the text system produced without committing text. */ - swallow: () => void dispose: () => void } @@ -28,12 +25,8 @@ function open(initialFlags: number): Session { terminal.open(container) const textarea = terminal.textarea! let flags = initialFlags - let flagReads = 0 const bridge = installPreviewImeBridge(terminal, { - getKittyKeyboardFlags: () => { - flagReads += 1 - return flags - } + getKittyKeyboardFlags: () => flags })! terminal.attachCustomKeyEventHandler((event) => !bridge.claimKeyEvent(event)) const emitted: string[] = [] @@ -66,16 +59,12 @@ function open(initialFlags: number): Session { return { emitted, - get flagReads() { - return flagReads - }, setFlags: (next) => { flags = next }, keydown: (init = {}) => key('keydown', init), keyup: (init = {}) => key('keyup', init), commit: (text) => input(text), - swallow: () => input(null), dispose: () => { bridge.dispose() terminal.dispose() @@ -94,70 +83,6 @@ describe('preview IME bridge outbound bytes', () => { document.body.replaceChildren() }) - describe.each([ - ['keydown -> insertText -> keyup', false], - // Some macOS input sources commit after keyup; preserve press ownership until then. - ['keydown -> keyup -> insertText', true] - ])('in %s order', (_name, keyupFirst) => { - const type = (session: Session): void => { - session.keydown() - if (keyupFirst) { - session.keyup() - session.commit(',') - } else { - session.commit(',') - session.keyup() - } - } - - it.each([ - ['no flags', 0, ','], - ['disambiguate only', 1, ','], - ['event types', 2, ',\x1b[44;1:3u'], - ['all keys as escape codes', 8, '\x1b[44u'], - ['all keys + event types', 10, '\x1b[44u\x1b[44;1:3u'] - ])('emits the %s byte shape', (_flagName, flags, expected) => { - const session = open(flags) - type(session) - expect(session.emitted.join('')).toBe(expected) - session.dispose() - }) - - it('reads the mutable getter exactly once, at commit time', () => { - const session = open(0) - session.keydown() - expect(session.flagReads).toBe(0) - session.setFlags(8) - if (keyupFirst) { - session.keyup() - expect(session.flagReads).toBe(0) - session.commit(',') - } else { - session.commit(',') - session.keyup() - } - expect(session.flagReads).toBe(1) - // The commit-time read is what both the press AND its release encode from. - expect(session.emitted.join('')).toBe('\x1b[44u') - session.dispose() - }) - - it('emits nothing when the input source swallowed the press', () => { - const session = open(10) - session.keydown() - if (keyupFirst) { - session.keyup() - session.swallow() - } else { - session.swallow() - session.keyup() - } - // A release describes a press the app received; none did. - expect(session.emitted.join('')).toBe('') - session.dispose() - }) - }) - it('encodes the release from the keyup state when Shift is released first', () => { const session = open(10) // `!` on Digit1: the press reports the base key with the Shift modifier. diff --git a/src/renderer/src/components/dashboard-popout/preview-terminal-ime-bridge.ts b/src/renderer/src/components/dashboard-popout/preview-terminal-ime-bridge.ts index b1d6b05fcfe..9dfee2124b5 100644 --- a/src/renderer/src/components/dashboard-popout/preview-terminal-ime-bridge.ts +++ b/src/renderer/src/components/dashboard-popout/preview-terminal-ime-bridge.ts @@ -1,5 +1,5 @@ import type { Terminal } from '@xterm/xterm' -import { getShortcutPlatform } from '@/lib/shortcut-platform' +import { getShortcutPlatform } from '@/lib/keyboard/shortcut-platform' import { installTerminalImeCompositionTracker } from '@/components/terminal-pane/input/ime/terminal-ime-composition-tracker' import { installTerminalImeNativeTextForwarder } from '@/components/terminal-pane/input/ime/terminal-ime-native-text-forwarder' diff --git a/src/renderer/src/components/dashboard-popout/preview-terminal-key-handler.ts b/src/renderer/src/components/dashboard-popout/preview-terminal-key-handler.ts index a7846026550..11ce1df7f38 100644 --- a/src/renderer/src/components/dashboard-popout/preview-terminal-key-handler.ts +++ b/src/renderer/src/components/dashboard-popout/preview-terminal-key-handler.ts @@ -1,5 +1,5 @@ import type { Terminal } from '@xterm/xterm' -import { getShortcutPlatform } from '@/lib/shortcut-platform' +import { getShortcutPlatform } from '@/lib/keyboard/shortcut-platform' import { keybindingMatchesAction } from '../../../../shared/keybindings' import { useAppStore } from '@/store' import { diff --git a/src/renderer/src/components/dashboard-popout/preview-terminal-paste.ts b/src/renderer/src/components/dashboard-popout/preview-terminal-paste.ts index 10187880d5e..aa757c946b3 100644 --- a/src/renderer/src/components/dashboard-popout/preview-terminal-paste.ts +++ b/src/renderer/src/components/dashboard-popout/preview-terminal-paste.ts @@ -1,5 +1,5 @@ import type { Terminal } from '@xterm/xterm' -import { getShortcutPlatform } from '@/lib/shortcut-platform' +import { getShortcutPlatform } from '@/lib/keyboard/shortcut-platform' import { executeTerminalPastePlan } from '@/components/terminal-pane/input/paste/terminal-paste-executor' import { planTerminalPasteWithYield } from '@/components/terminal-pane/input/paste/terminal-paste-coordinator' import { resolveTerminalPasteRuntime } from '@/components/terminal-pane/input/paste/terminal-paste-runtime' diff --git a/src/renderer/src/components/dashboard/AgentDashboardExperimentalSetting.tsx b/src/renderer/src/components/dashboard/AgentDashboardExperimentalSetting.tsx index a2e48e51396..4f5f74314ae 100644 --- a/src/renderer/src/components/dashboard/AgentDashboardExperimentalSetting.tsx +++ b/src/renderer/src/components/dashboard/AgentDashboardExperimentalSetting.tsx @@ -27,7 +27,7 @@ export function AgentDashboardExperimentalSetting({ 'auto.components.settings.ExperimentalPane.agentDashboard.description', 'Kanban board for monitoring agents across worktrees, in-window or as a pop-out.' )} - keywords={getExperimentalSearchEntry(t).agentDashboard.keywords} + keywords={getExperimentalSearchEntry().agentDashboard.keywords} className="space-y-3 py-3" id="experimental-agent-dashboard" > diff --git a/src/renderer/src/components/dashboard/dashboard-card-context.ts b/src/renderer/src/components/dashboard/dashboard-card-context.ts index 61a7d836f30..7895505d95c 100644 --- a/src/renderer/src/components/dashboard/dashboard-card-context.ts +++ b/src/renderer/src/components/dashboard/dashboard-card-context.ts @@ -1,4 +1,4 @@ -import { branchName } from '@/lib/git-utils' +import { branchName } from '@/lib/source-control/branches/branch-name' import { getHostedReviewCacheKey } from '@/store/slices/hosted-review/hosted-review-cache-identity' import type { AppState } from '@/store/types' import type { DashboardCardReview } from '../../../../shared/dashboard-snapshot' diff --git a/src/renderer/src/components/dictation/dictation-insertion-target.test.ts b/src/renderer/src/components/dictation/dictation-insertion-target.test.ts index 1ec6d27e188..703cfe5e473 100644 --- a/src/renderer/src/components/dictation/dictation-insertion-target.test.ts +++ b/src/renderer/src/components/dictation/dictation-insertion-target.test.ts @@ -5,7 +5,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import { TEXT_CONTROL_PASTE_DIRECT_MAX_BYTES, TEXT_CONTROL_PASTE_MAX_BYTES -} from '@/lib/text-control-paste' +} from '@/lib/text-input/paste/text-control-paste-model' import { captureInsertionTarget, insertText } from './dictation-insertion-target' type DocumentWithExecCommand = Document & { diff --git a/src/renderer/src/components/dictation/dictation-insertion-target.ts b/src/renderer/src/components/dictation/dictation-insertion-target.ts index 4756c950859..35e2f39ce8b 100644 --- a/src/renderer/src/components/dictation/dictation-insertion-target.ts +++ b/src/renderer/src/components/dictation/dictation-insertion-target.ts @@ -3,11 +3,13 @@ import { getUtf8ChunkEndIndex } from '../../../../shared/utf8-byte-limits' import { TEXT_CONTROL_PASTE_CHUNK_MAX_BYTES, TEXT_CONTROL_PASTE_DIRECT_MAX_BYTES, - TEXT_CONTROL_PASTE_MAX_BYTES, + TEXT_CONTROL_PASTE_MAX_BYTES +} from '@/lib/text-input/paste/text-control-paste-model' +import { measureTextControlPasteByteLength, measureTextControlPasteByteLengthWithYield, pasteTextIntoTextControl -} from '@/lib/text-control-paste' +} from '@/lib/text-input/paste/text-control-paste' import { DICTATION_TEXT_BLOCK_SELECTOR, dictationSeparators diff --git a/src/renderer/src/components/dictation/dictation-transcript-delivery.ts b/src/renderer/src/components/dictation/dictation-transcript-delivery.ts index 0b890d3bf31..3cfeadbae95 100644 --- a/src/renderer/src/components/dictation/dictation-transcript-delivery.ts +++ b/src/renderer/src/components/dictation/dictation-transcript-delivery.ts @@ -1,6 +1,6 @@ import { toast } from 'sonner' import { translate } from '@/i18n/i18n' -import { describeClipboardWriteFailure } from '@/lib/clipboard-write-failure' +import { describeClipboardWriteFailure } from '@/lib/clipboard/clipboard-write-failure' import { appendDictationText } from '../../../../shared/dictation-transcript-spacing' import { insertText, type DictationInsertionTarget } from './dictation-insertion-target' diff --git a/src/renderer/src/components/dictation/use-dictation-controls.ts b/src/renderer/src/components/dictation/use-dictation-controls.ts index 654c00bd82e..799f9aced5c 100644 --- a/src/renderer/src/components/dictation/use-dictation-controls.ts +++ b/src/renderer/src/components/dictation/use-dictation-controls.ts @@ -1,6 +1,6 @@ import { useEffect, useEffectEvent } from 'react' -import { addAppKeyListener, removeAppKeyListener } from '@/lib/app-keyboard-router' -import { isImeOwnedKeyboardEvent } from '@/lib/ime-composition-keyboard-event' +import { addAppKeyListener, removeAppKeyListener } from '@/lib/keyboard/app-keyboard-router' +import { isImeOwnedKeyboardEvent } from '@/lib/text-input/ime/ime-composition-keyboard-event' import { isPlainEscapeKeyEvent } from '../terminal-pane/input/keyboard/agent-interrupt-key-event' import { DICTATION_CONTROL_EVENT, type DictationControlAction } from './dictation-control-events' import type { DictationState } from '../../../../shared/speech-types' diff --git a/src/renderer/src/components/dictation/use-hold-dictation-gesture.ts b/src/renderer/src/components/dictation/use-hold-dictation-gesture.ts index 42d29175ea1..5186b7ba380 100644 --- a/src/renderer/src/components/dictation/use-hold-dictation-gesture.ts +++ b/src/renderer/src/components/dictation/use-hold-dictation-gesture.ts @@ -1,5 +1,5 @@ import { useEffect, useEffectEvent, useRef } from 'react' -import { getShortcutPlatform } from '@/lib/shortcut-platform' +import { getShortcutPlatform } from '@/lib/keyboard/shortcut-platform' import { keybindingMatchesAction, normalizeTerminalShortcutPolicy, @@ -8,7 +8,7 @@ import { import { textInputOwnsComposition } from '../../../../shared/text-input-shortcut-policy' import type { GlobalSettings } from '../../../../shared/global-settings-types' import { dictationShortcutMode } from '../../../../shared/dictation-shortcut-mode' -import { addAppKeyListener, removeAppKeyListener } from '@/lib/app-keyboard-router' +import { addAppKeyListener, removeAppKeyListener } from '@/lib/keyboard/app-keyboard-router' import { showTerminalShortcutCaptureNotification } from '@/lib/terminal-shortcut-capture-notification' import { getKeybindingContext } from '../terminal-workspace-model' diff --git a/src/renderer/src/components/diff-comments/DiffCommentCard.tsx b/src/renderer/src/components/diff-comments/DiffCommentCard.tsx index 6e13895d1a3..e475fe2568a 100644 --- a/src/renderer/src/components/diff-comments/DiffCommentCard.tsx +++ b/src/renderer/src/components/diff-comments/DiffCommentCard.tsx @@ -1,4 +1,4 @@ -import { ImeTextarea } from '@/lib/ime-text-field' +import { ImeTextarea } from '@/lib/text-input/ime/ime-text-field' import { CornerDownLeft, Pencil, Trash } from 'lucide-react' import { useLayoutEffect, useRef, useState, type ReactNode } from 'react' import { Button } from '@/components/ui/button' diff --git a/src/renderer/src/components/diff-comments/DiffCommentDraftCard.tsx b/src/renderer/src/components/diff-comments/DiffCommentDraftCard.tsx index 2f91ffcf48d..6db2470e6b6 100644 --- a/src/renderer/src/components/diff-comments/DiffCommentDraftCard.tsx +++ b/src/renderer/src/components/diff-comments/DiffCommentDraftCard.tsx @@ -1,7 +1,7 @@ import { useGitHubTextareaImages } from '@/components/github/use-github-textarea-images' import { useGitHubImagePending } from '@/components/github/use-github-image-uploads' import type { GitHubImageIntake } from '@/components/github/use-github-image-uploads' -import { ImeTextarea } from '@/lib/ime-text-field' +import { ImeTextarea } from '@/lib/text-input/ime/ime-text-field' import { useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from 'react' import { CornerDownLeft } from 'lucide-react' import { Button } from '@/components/ui/button' @@ -10,7 +10,7 @@ import { useTranslate } from '@/i18n/use-translate' import { getCommentBodySubmitState, hasBoundedCommentBodyText -} from '@/lib/comment-body-submit-state' +} from '@/lib/source-control/review/comments/comment-body-submit-state' import { toast } from 'sonner' import { installOpenDraftAddReviewNoteGuard } from '../editor/editor-shortcuts' diff --git a/src/renderer/src/components/diff-comments/DiffCommentPopover.tsx b/src/renderer/src/components/diff-comments/DiffCommentPopover.tsx index bbf20089622..179641303d5 100644 --- a/src/renderer/src/components/diff-comments/DiffCommentPopover.tsx +++ b/src/renderer/src/components/diff-comments/DiffCommentPopover.tsx @@ -1,4 +1,4 @@ -import { ImeTextarea } from '@/lib/ime-text-field' +import { ImeTextarea } from '@/lib/text-input/ime/ime-text-field' import { useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from 'react' import { CornerDownLeft } from 'lucide-react' import { toast } from 'sonner' @@ -7,7 +7,7 @@ import { useMountedRef } from '@/hooks/useMountedRef' import { getCommentBodySubmitState, hasBoundedCommentBodyText -} from '@/lib/comment-body-submit-state' +} from '@/lib/source-control/review/comments/comment-body-submit-state' import { useTranslate } from '@/i18n/use-translate' import { installOpenDraftAddReviewNoteGuard } from '../editor/editor-shortcuts' import { resolveDiffCommentPopoverTop } from './diff-comment-popover-position' diff --git a/src/renderer/src/components/diff-comments/diff-comment-popover-outside-click.test.tsx b/src/renderer/src/components/diff-comments/diff-comment-popover-outside-click.test.tsx index 9de8cabc908..1e07143007d 100644 --- a/src/renderer/src/components/diff-comments/diff-comment-popover-outside-click.test.tsx +++ b/src/renderer/src/components/diff-comments/diff-comment-popover-outside-click.test.tsx @@ -3,7 +3,7 @@ import { act } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { COMMENT_BODY_NONBLANK_SCAN_MAX_BYTES } from '@/lib/comment-body-submit-state' +import { COMMENT_BODY_NONBLANK_SCAN_MAX_BYTES } from '@/lib/source-control/review/comments/comment-body-submit-state' import { DiffCommentPopover } from './DiffCommentPopover' const roots: Root[] = [] diff --git a/src/renderer/src/components/diff-comments/useDiffCommentDecorator.tsx b/src/renderer/src/components/diff-comments/useDiffCommentDecorator.tsx index 0a85789676d..e61ebac7a0c 100644 --- a/src/renderer/src/components/diff-comments/useDiffCommentDecorator.tsx +++ b/src/renderer/src/components/diff-comments/useDiffCommentDecorator.tsx @@ -6,7 +6,7 @@ import { import { useCallback, useEffect, useMemo, useRef } from 'react' import type { editor as monacoEditor } from 'monaco-editor' import { createRoot, type Root } from 'react-dom/client' -import { getCommentBodyLayoutLineCount } from '@/lib/comment-body-line-count' +import { getCommentBodyLayoutLineCount } from '@/lib/source-control/review/comments/comment-body-line-count' import { translate } from '@/i18n/i18n' import { useAppStore } from '@/store' import { diff --git a/src/renderer/src/components/editor/EditorPanel.tsx b/src/renderer/src/components/editor/EditorPanel.tsx index 252570c45c0..a1c18c97924 100644 --- a/src/renderer/src/components/editor/EditorPanel.tsx +++ b/src/renderer/src/components/editor/EditorPanel.tsx @@ -1,7 +1,7 @@ import React, { useCallback, useMemo, useRef, useState } from 'react' import { useAppStore } from '@/store' import { getConnectionId } from '@/lib/workspace/ownership/connection-context' -import { formatClipboardFilePath } from '@/lib/clipboard-file-path' +import { formatClipboardFilePath } from '@/lib/clipboard/clipboard-file-path' import { getOpenFileRevealOwner } from '@/lib/reveal-in-file-manager' import { canShowWorkspaceFileBrowserAction } from '@/lib/browser/documents/file-preview' import { openEditorHtmlPreview } from './open-editor-html-preview' diff --git a/src/renderer/src/components/editor/EditorPanelHeaderPath.tsx b/src/renderer/src/components/editor/EditorPanelHeaderPath.tsx index c1a023e792a..9db39932b01 100644 --- a/src/renderer/src/components/editor/EditorPanelHeaderPath.tsx +++ b/src/renderer/src/components/editor/EditorPanelHeaderPath.tsx @@ -1,4 +1,4 @@ -import { ImeInput } from '@/lib/ime-text-field' +import { ImeInput } from '@/lib/text-input/ime/ime-text-field' import { useEffect, useRef, useState } from 'react' import { Eye, Pencil } from 'lucide-react' import { @@ -10,7 +10,7 @@ import { DropdownMenuTrigger } from '@/components/ui/dropdown-menu' import { useShortcutLabel } from '@/hooks/useShortcutLabel' -import { isImeCompositionKeyDown } from '@/lib/ime-composition-keyboard-event' +import { isImeCompositionKeyDown } from '@/lib/text-input/ime/ime-composition-keyboard-event' import { useTranslate } from '@/i18n/use-translate' import type { OpenFile } from '@/store/slices/editor/types/open-file' import { isVirtualEditorFile } from '@/store/slices/editor/tabs/editor-tab-content-type' diff --git a/src/renderer/src/components/editor/MonacoGutterContextMenu.tsx b/src/renderer/src/components/editor/MonacoGutterContextMenu.tsx index 869a71c7f24..e8f5725801c 100644 --- a/src/renderer/src/components/editor/MonacoGutterContextMenu.tsx +++ b/src/renderer/src/components/editor/MonacoGutterContextMenu.tsx @@ -13,7 +13,7 @@ import { getConnectionId } from '@/lib/workspace/ownership/connection-context' import { findFirstIndexedWorktree } from '@/lib/workspace/ownership/worktree-runtime-owner-index' import { getRuntimeGitRemoteFileUrl } from '@/runtime/source-control/git/runtime-git-working-tree-client' import { formatPathLineReference } from './line-copy-path' -import { formatClipboardFilePath } from '@/lib/clipboard-file-path' +import { formatClipboardFilePath } from '@/lib/clipboard/clipboard-file-path' import { getOpenFileRevealOwner } from '@/lib/reveal-in-file-manager' import { useTranslate } from '@/i18n/use-translate' import { selectWorktreesByRepo } from '@/store/workspaces/catalog/workspace-record-fields' diff --git a/src/renderer/src/components/editor/PanZoomImageDialog.tsx b/src/renderer/src/components/editor/PanZoomImageDialog.tsx index 61e08bfec27..3159fc4c776 100644 --- a/src/renderer/src/components/editor/PanZoomImageDialog.tsx +++ b/src/renderer/src/components/editor/PanZoomImageDialog.tsx @@ -10,7 +10,7 @@ import { useAppStore } from '@/store' import { clipboardImageCopyErrorDescription, imageCopyFailedMessage -} from '@/lib/clipboard-image-copy' +} from '@/lib/clipboard/images/clipboard-image-copy' import { IMAGE_VIEWER_ZOOM_STEP } from './image-viewer-zoom' import { imageClipboardPngDataUrl } from './image-png-export' import type { PanZoomSize } from '../../../../shared/pan-zoom-view' diff --git a/src/renderer/src/components/editor/PdfFind.tsx b/src/renderer/src/components/editor/PdfFind.tsx index db6f0df30fc..ff1ab22bd53 100644 --- a/src/renderer/src/components/editor/PdfFind.tsx +++ b/src/renderer/src/components/editor/PdfFind.tsx @@ -1,4 +1,4 @@ -import { ImeInput } from '@/lib/ime-text-field' +import { ImeInput } from '@/lib/text-input/ime/ime-text-field' import { useCallback, useEffect, useState } from 'react' import { ChevronUp, ChevronDown, X } from 'lucide-react' import { Button } from '@/components/ui/button' diff --git a/src/renderer/src/components/editor/check-details/actions/check-run-details-fix-with-ai.ts b/src/renderer/src/components/editor/check-details/actions/check-run-details-fix-with-ai.ts index 0d48367b4ae..bb6ecb75dc3 100644 --- a/src/renderer/src/components/editor/check-details/actions/check-run-details-fix-with-ai.ts +++ b/src/renderer/src/components/editor/check-details/actions/check-run-details-fix-with-ai.ts @@ -10,7 +10,7 @@ import { } from './check-run-details-fix-context' import { openSourceControlAiSettingsTarget } from '@/components/right-sidebar/source-control/panel/source-control-ai-settings-navigation' import { getConnectionId } from '@/lib/workspace/ownership/connection-context' -import { startFixChecksAgent } from '@/lib/fix-checks-agent-launch' +import { startFixChecksAgent } from '@/lib/source-control/checks/fix-checks-agent-launch' import { readSourceControlLaunchRecipeAgentId } from '../../../../../../shared/source-control/ai/source-control-launch-agent-selection' import { resolveRepoWorktreeLaunchPlatform } from '@/runtime/hosts/platform/execution-host-facts' import { useAppStore } from '@/store/index' diff --git a/src/renderer/src/components/editor/check-details/panel/CheckRunCopyButton.tsx b/src/renderer/src/components/editor/check-details/panel/CheckRunCopyButton.tsx index 9d4554b7758..88711bbd39f 100644 --- a/src/renderer/src/components/editor/check-details/panel/CheckRunCopyButton.tsx +++ b/src/renderer/src/components/editor/check-details/panel/CheckRunCopyButton.tsx @@ -1,6 +1,6 @@ import React from 'react' import { Check, Copy } from 'lucide-react' -import { useClipboardTextCopyFeedback } from '@/hooks/use-clipboard-text-copy-feedback' +import { useClipboardTextCopyFeedback } from '@/hooks/clipboard/use-clipboard-text-copy-feedback' import { useTranslate } from '@/i18n/use-translate' import { cn } from '@/lib/utils' import { Button } from '@/components/ui/button' diff --git a/src/renderer/src/components/editor/combined-diff/browse-files/combined-diff-file-tree-row.tsx b/src/renderer/src/components/editor/combined-diff/browse-files/combined-diff-file-tree-row.tsx index 2f42ab264e8..71909b98da0 100644 --- a/src/renderer/src/components/editor/combined-diff/browse-files/combined-diff-file-tree-row.tsx +++ b/src/renderer/src/components/editor/combined-diff/browse-files/combined-diff-file-tree-row.tsx @@ -1,6 +1,6 @@ import { createElement, memo } from 'react' import { ChevronDown } from 'lucide-react' -import { STATUS_COLORS } from '@/lib/git-path-decoration' +import { STATUS_COLORS } from '@/lib/source-control/paths/git-path-decoration' import { STATUS_LABELS } from '../../../../../../shared/git/status/git-status-labels' import type { SourceControlTreeNode } from '../../../../../../shared/source-control/listing/source-control-tree' import { FileTypeIcon, FolderIcon } from '@/components/FileTypeIcon' diff --git a/src/renderer/src/components/editor/combined-diff/review-controls/combined-diff-notes-popover.tsx b/src/renderer/src/components/editor/combined-diff/review-controls/combined-diff-notes-popover.tsx index cbf6f80755d..04fee3e0759 100644 --- a/src/renderer/src/components/editor/combined-diff/review-controls/combined-diff-notes-popover.tsx +++ b/src/renderer/src/components/editor/combined-diff/review-controls/combined-diff-notes-popover.tsx @@ -9,7 +9,7 @@ import { DialogHeader, DialogTitle } from '@/components/ui/dialog' -import { getDiffCommentLineLabel } from '@/lib/diff-comment-compat' +import { getDiffCommentLineLabel } from '@/lib/source-control/review/comments/diff-comment-compat' import { useTranslate } from '@/i18n/use-translate' import type { DiffComment } from '../../../../../../shared/diff-comment-types' diff --git a/src/renderer/src/components/editor/combined-diff/review-controls/use-combined-diff-notes-actions.ts b/src/renderer/src/components/editor/combined-diff/review-controls/use-combined-diff-notes-actions.ts index 32715254740..a07398b270b 100644 --- a/src/renderer/src/components/editor/combined-diff/review-controls/use-combined-diff-notes-actions.ts +++ b/src/renderer/src/components/editor/combined-diff/review-controls/use-combined-diff-notes-actions.ts @@ -4,7 +4,7 @@ import { useOwnedDiffComments } from '@/components/diff-comments/use-diff-commen import { withoutMarkdownReviewNotes } from '@/lib/editor/markdown/review-notes/markdown-review-notes-setting' import React, { useCallback, useEffect, useRef, useState } from 'react' import { toast } from 'sonner' -import { formatDiffComments } from '@/lib/diff-comments-format' +import { formatDiffComments } from '../../../../../../shared/diff-comments-format' import { translate } from '@/i18n/i18n' import type { DiffComment } from '../../../../../../shared/diff-comment-types' diff --git a/src/renderer/src/components/editor/csv/CsvViewer.editing.test.tsx b/src/renderer/src/components/editor/csv/CsvViewer.editing.test.tsx index 5363d38d6ee..07011de9507 100644 --- a/src/renderer/src/components/editor/csv/CsvViewer.editing.test.tsx +++ b/src/renderer/src/components/editor/csv/CsvViewer.editing.test.tsx @@ -9,7 +9,7 @@ import { flushPendingEditorBuffer, hasPendingEditorBufferChange } from '../save/flush/editor-pending-flush' -import { APP_MENU_PASTE_EVENT } from '@/lib/app-menu-paste' +import { APP_MENU_PASTE_EVENT } from '@/lib/clipboard/menu/app-menu-paste' import { ORCA_EDITOR_FILE_SAVED_EVENT } from '../autosave/editor-autosave' import { EMPTY_EDITOR_BUFFER_VIEW_INDEX as NO_SHARING } from '@/store/slices/editor/buffers/editor-buffer-views' @@ -104,29 +104,6 @@ describe('editable CSV table', () => { expect(screen.getByTestId('dirty').textContent).toBe('true') }) - it('saves the current cell value and reports a failed write without dropping the draft', async () => { - const { save } = setup() - save.mockResolvedValue(false) - fireEvent.keyDown(edit('B', 'saved value'), { key: 's', ctrlKey: true, metaKey: true }) - expect(save).toHaveBeenCalledWith('\ufeffname,value\r\nsaved value,2\r\nA,1') - expect((await screen.findByRole('alert')).textContent).toContain( - 'Your edits remain in the draft' - ) - expect(screen.getByTestId('source').textContent).toContain('saved value') - }) - - it('pastes atomically into selected cells and does not partially apply an oversized rectangle', () => { - const { changes } = setup() - const cell = screen.getByRole('gridcell', { name: 'B' }) - fireEvent.pointerDown(cell, { button: 0 }) - const grid = screen.getByRole('grid') - fireEvent.paste(grid, { clipboardData: { getData: () => 'new\t3\nother\t4' } }) - expect(screen.getByTestId('source').textContent).toBe('\ufeffname,value\r\nnew,3\r\nother,4') - fireEvent.paste(grid, { clipboardData: { getData: () => 'one\ttwo\tthree' } }) - expect(changes).toHaveBeenCalledTimes(1) - expect(screen.getByRole('alert').textContent).toContain('Paste exceeds the table') - }) - it('edits a filtered row in its original source position', () => { setup() fireEvent.change(screen.getByRole('textbox', { name: 'Filter rows' }), { @@ -136,17 +113,6 @@ describe('editable CSV table', () => { expect(screen.getByTestId('source').textContent).toBe('\ufeffname,value\r\nB,2\r\nA,updated') }) - it('keeps the chosen table delimiter when an edit would change the Auto guess', () => { - setup('first;second\nvalue;two') - fireEvent.doubleClick(screen.getByRole('columnheader', { name: 'first' })) - const input = screen.getByRole('textbox', { name: /Edit row/ }) - fireEvent.change(input, { target: { value: 'tabs\there\ttoo' } }) - fireEvent.keyDown(input, { key: 'Enter' }) - expect(screen.getByRole('grid').getAttribute('aria-colcount')).toBe('3') - expect(screen.getByRole('columnheader', { name: 'tabs\there\ttoo' })).toBeTruthy() - expect(screen.getByRole('combobox', { name: 'Delimiter' }).textContent).toBe('Semicolon (;)') - }) - it('cancels only the new cell after an earlier save completes', () => { setup() fireEvent.keyDown(edit('B', 'saved base'), { key: 'Enter' }) @@ -199,29 +165,6 @@ describe('editable CSV table', () => { expect(result.current.interaction?.editing).toBeNull() }) - it('rejects a delayed paste if typing starts before the clipboard read completes', async () => { - let resolveRead: (value: string) => void = () => {} - const pending = new Promise((resolve) => { - resolveRead = resolve - }) - Object.defineProperty(window, 'api', { - configurable: true, - value: { ui: { readClipboardText: () => pending } } - }) - const { changes } = setup() - fireEvent.pointerDown(screen.getByRole('gridcell', { name: 'B' }), { button: 0 }) - fireEvent.click(screen.getByRole('button', { name: 'Paste' })) - edit('B', 'new pending input') - await act(async () => { - resolveRead('old clipboard') - await pending - }) - expect(changes).not.toHaveBeenCalled() - expect(screen.getByRole('alert').textContent).toContain('CSV changed before paste completed') - const retained = screen.getByRole('textbox', { name: /Edit row/ }) - expect(retained instanceof HTMLTextAreaElement && retained.value).toBe('new pending input') - }) - it('routes menu paste only to the focused grid when two file panes are mounted', async () => { const first = vi.fn() const second = vi.fn() diff --git a/src/renderer/src/components/editor/csv/useCsvClipboard.ts b/src/renderer/src/components/editor/csv/useCsvClipboard.ts index caccc649b9b..f87ddc8951e 100644 --- a/src/renderer/src/components/editor/csv/useCsvClipboard.ts +++ b/src/renderer/src/components/editor/csv/useCsvClipboard.ts @@ -1,5 +1,5 @@ import { useEffect, useLayoutEffect, useRef, type ClipboardEvent } from 'react' -import { APP_MENU_PASTE_EVENT } from '@/lib/app-menu-paste' +import { APP_MENU_PASTE_EVENT } from '@/lib/clipboard/menu/app-menu-paste' import { copyCsvSelection, pasteCsvSelection } from './csv-cell-clipboard' import type { CsvCellSelection } from './csv-cell-selection' import type { CsvCellEditSession } from './csv-grid-interaction' diff --git a/src/renderer/src/components/editor/diff/monaco-view/DiffViewer.tsx b/src/renderer/src/components/editor/diff/monaco-view/DiffViewer.tsx index e02d1627e37..9fb9bf3ab77 100644 --- a/src/renderer/src/components/editor/diff/monaco-view/DiffViewer.tsx +++ b/src/renderer/src/components/editor/diff/monaco-view/DiffViewer.tsx @@ -13,7 +13,7 @@ import { toast } from 'sonner' import { translate } from '@/i18n/i18n' import { useTranslate } from '@/i18n/use-translate' import { applyDiffEditorLineNumberOptions } from './diff-editor-line-number-options' -import { isDiffComment } from '@/lib/diff-comment-compat' +import { isDiffComment } from '@/lib/source-control/review/comments/diff-comment-compat' import { installEditorSaveShortcut, installMonacoEditorFindShortcut } from '../../editor-shortcuts' import { diffEditorScrollbarOptions } from './diff-editor-scrollbar-options' import { LargeDiffFallback } from '../large-files/LargeDiffFallback' diff --git a/src/renderer/src/components/editor/diff/sections/DiffSectionItem.tsx b/src/renderer/src/components/editor/diff/sections/DiffSectionItem.tsx index 4c980ab1d6d..ab232a7d5ff 100644 --- a/src/renderer/src/components/editor/diff/sections/DiffSectionItem.tsx +++ b/src/renderer/src/components/editor/diff/sections/DiffSectionItem.tsx @@ -9,7 +9,7 @@ import { useEditorTypography } from '@/hooks/use-editor-typography' import { useDiffCommentDecorator } from '../../../diff-comments/useDiffCommentDecorator' import { applyDiffEditorLineNumberOptions } from '../monaco-view/diff-editor-line-number-options' import { DiffSectionHeader } from './DiffSectionHeader' -import { isDiffComment } from '@/lib/diff-comment-compat' +import { isDiffComment } from '@/lib/source-control/review/comments/diff-comment-compat' import { installEditorSaveShortcut, installMonacoEditorFindShortcut } from '../../editor-shortcuts' import { DiffSectionBody } from './DiffSectionBody' import { useDiffSectionLayoutMetrics } from './useDiffSectionLayoutMetrics' diff --git a/src/renderer/src/components/editor/editor-shortcuts.ts b/src/renderer/src/components/editor/editor-shortcuts.ts index 35909e6ee71..7832aa141a6 100644 --- a/src/renderer/src/components/editor/editor-shortcuts.ts +++ b/src/renderer/src/components/editor/editor-shortcuts.ts @@ -1,5 +1,5 @@ import type { KeyboardEvent as ReactKeyboardEvent } from 'react' -import { getShortcutPlatform } from '@/lib/shortcut-platform' +import { getShortcutPlatform } from '@/lib/keyboard/shortcut-platform' import { useAppStore } from '@/store' import { keybindingMatchesAction, type KeybindingActionId } from '../../../../shared/keybindings' diff --git a/src/renderer/src/components/editor/git-graph/git-graph-commit-actions-controller.ts b/src/renderer/src/components/editor/git-graph/git-graph-commit-actions-controller.ts index ac69ad7ee1f..cd4bca04574 100644 --- a/src/renderer/src/components/editor/git-graph/git-graph-commit-actions-controller.ts +++ b/src/renderer/src/components/editor/git-graph/git-graph-commit-actions-controller.ts @@ -4,7 +4,7 @@ import type { BranchSwitchTarget } from '../../../../../shared/git/branch-switch import { createStore } from 'zustand/vanilla' import { toast } from 'sonner' import { useAppStore } from '@/store' -import { gitGraphScope } from '@/lib/git-graph-tab' +import { gitGraphScope } from '@/lib/source-control/graph/git-graph-tab' import { gitGraphOwnerKey } from './git-graph-workspace-navigation' import { gitCommitActionsSupported } from '@/runtime/source-control/git/git-commit-actions-capability' import { diff --git a/src/renderer/src/components/editor/git-graph/git-graph-filter.tsx b/src/renderer/src/components/editor/git-graph/git-graph-filter.tsx index 4e514ed205f..afe2589dbd9 100644 --- a/src/renderer/src/components/editor/git-graph/git-graph-filter.tsx +++ b/src/renderer/src/components/editor/git-graph/git-graph-filter.tsx @@ -6,7 +6,7 @@ import { Checkbox } from '@/components/ui/checkbox' import { getRuntimeGitRefs } from '@/runtime/source-control/git/runtime-git-status-client' import { runtimeEnvironmentSupportsCapability } from '@/runtime/rpc/compatibility/runtime-capability-proof' import { GIT_HISTORY_REFS_CAPABILITY } from '../../../../../shared/source-control/runtime/git-graph-capabilities' -import { gitGraphScope, useGitGraphScope } from '@/lib/git-graph-tab' +import { gitGraphScope, useGitGraphScope } from '@/lib/source-control/graph/git-graph-tab' import { useTranslate } from '@/i18n/use-translate' import type { GitHistoryItemRef, diff --git a/src/renderer/src/components/editor/git-graph/git-graph-history-controller.ts b/src/renderer/src/components/editor/git-graph/git-graph-history-controller.ts index 3465685eb14..cd5a139ea05 100644 --- a/src/renderer/src/components/editor/git-graph/git-graph-history-controller.ts +++ b/src/renderer/src/components/editor/git-graph/git-graph-history-controller.ts @@ -4,7 +4,8 @@ import { gitGraphOwnerKey, switchGitGraphWorkspace } from './git-graph-workspace import { createStore } from 'zustand/vanilla' import { shallow } from 'zustand/shallow' import { useAppStore } from '@/store' -import { gitGraphScope, gitGraphScopeInputs, GIT_GRAPH_REFRESH_EVENT } from '@/lib/git-graph-tab' +import { gitGraphScope, gitGraphScopeInputs } from '@/lib/source-control/graph/git-graph-tab' +import { GIT_GRAPH_REFRESH_EVENT } from '@/lib/source-control/graph/git-graph-refresh-event' import { getRuntimeGitHistory } from '@/runtime/source-control/git/runtime-git-status-client' import { translate } from '@/i18n/i18n' import type { diff --git a/src/renderer/src/components/editor/git-graph/git-graph-ref-menu.tsx b/src/renderer/src/components/editor/git-graph/git-graph-ref-menu.tsx index 0b968d353da..428bec78c9e 100644 --- a/src/renderer/src/components/editor/git-graph/git-graph-ref-menu.tsx +++ b/src/renderer/src/components/editor/git-graph/git-graph-ref-menu.tsx @@ -13,7 +13,7 @@ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { useAppStore } from '@/store' import { selectWorktreesByRepo } from '@/store/workspaces/catalog/workspace-record-fields' -import type { gitGraphScope } from '@/lib/git-graph-tab' +import type { gitGraphScope } from '@/lib/source-control/graph/git-graph-tab' import { gitGraphWorkspaceCatalog } from './git-graph-workspace-navigation' import { useTranslate } from '@/i18n/use-translate' import type { GitHistoryItemRef } from '../../../../../shared/git/history/git-history-types' diff --git a/src/renderer/src/components/editor/git-graph/git-graph-rendering.test.tsx b/src/renderer/src/components/editor/git-graph/git-graph-rendering.test.tsx index ba96587e12e..61a4f918161 100644 --- a/src/renderer/src/components/editor/git-graph/git-graph-rendering.test.tsx +++ b/src/renderer/src/components/editor/git-graph/git-graph-rendering.test.tsx @@ -2,10 +2,7 @@ import { workspaceId, local, repo } from './git-graph-workspace.test-fixtures' // @vitest-environment happy-dom import { useAppStore } from '@/store' import { gitGraphHistoryForTab, disposeGitGraphHistory } from './git-graph-tab-controllers' -import { attachClosedEditorTabCleanup } from '../model-lifetime/retention/closed-editor-tab-controller' -import { gitGraphScope } from '@/lib/git-graph-tab' -import { captureWorktreeOperationGenerationSnapshot } from '@/lib/workspace/ownership/worktree-operation-generation' -import type { OpenFile } from '@/store/slices/editor/types/open-file' + import { act } from 'react' import { createRoot } from 'react-dom/client' import { afterEach, beforeEach, expect, it, vi } from 'vitest' @@ -327,115 +324,6 @@ it('keeps a newer selection and its details during refresh and after a partial f expect(container.querySelector('aside h2')?.textContent).toBe('Commit 120') }) -it.each(['local', 'ssh:A'] as const)( - 'retains %s twin history across workspace switches, then follows its tab owner move', - async (hostId) => { - useAppStore.setState({ - repos: [ - ...useAppStore.getState().repos, - { ...repo, executionHostId: 'ssh:A', connectionId: 'A' } - ], - worktreesByRepo: { r: [local, { ...local, hostId: 'ssh:A' }] }, - activeWorkspaceExecutionHostId: hostId - }) - const cleanup = attachClosedEditorTabCleanup(useAppStore) - const original = useAppStore.getState().openFiles - const file: OpenFile = { - id: `${workspaceId}::git-graph`, - worktreeId: workspaceId, - mode: 'git-graph', - relativePath: 'Git Graph', - language: 'plaintext', - isDirty: false, - filePath: `${workspaceId}::git-graph`, - operationProvenance: { - ownershipProjection: 'explicit', - generation: captureWorktreeOperationGenerationSnapshot({ - executionHostId: hostId, - runtimeEnvironmentId: null - }) - } - } - useAppStore.setState({ openFiles: [...original, file] }) - const render = () => ( - - - - ) - const page = { - ...result, - items: [...result.items, { id: 'b', subject: 'second', message: 'second', parentIds: [] }], - refs: 'all' as const - } - getHistory.mockReset().mockResolvedValueOnce(page) - try { - await act(async () => root.render(render())) - expect(getHistory.mock.calls[0]?.[0].connectionId).toBe(hostId === 'ssh:A' ? 'A' : undefined) - await act(async () => { - const commit = Array.from(container.querySelectorAll('button')).find( - (button) => button.textContent === 'second' - )! - commit.click() - const scroll = container.querySelector('div.overflow-y-auto')! - scroll.scrollTop = 144 - scroll.dispatchEvent(new Event('scroll')) - }) - await act(async () => root.render(null)) - await act(async () => - useAppStore.setState({ activeWorktreeId: 'other', activeWorkspaceExecutionHostId: 'local' }) - ) - expect(gitGraphScope(workspaceId)).toBeNull() - expect(gitGraphHistoryForTab(workspaceId).view.getState().selectedId).toBe('b') - await act(async () => - useAppStore.setState({ - activeWorktreeId: workspaceId, - activeWorkspaceExecutionHostId: hostId - }) - ) - await act(async () => root.render(render())) - expect(container.querySelector('[data-current="true"]')?.textContent).toContain('second') - expect(container.querySelector('div.overflow-y-auto')?.scrollTop).toBe(144) - expect(getHistory).toHaveBeenCalledTimes(1) - await act(async () => root.render(null)) - useAppStore.setState({ - worktreesByRepo: { r: [{ ...local, hostId: hostId === 'local' ? 'ssh:A' : 'local' }] } - }) - expect(gitGraphHistoryForTab(workspaceId).view.getState().result).toBeUndefined() - expect(gitGraphHistoryForTab(workspaceId).scrollOffset).toBe(0) - await act(async () => root.render(render())) - const movedHost = hostId === 'local' ? 'ssh:A' : 'local' - getHistory.mockResolvedValueOnce({ ...result, refs: 'all' }) - await act(async () => - useAppStore.setState({ - openFiles: [ - ...original, - { - ...file, - operationProvenance: { - ownershipProjection: 'explicit', - generation: captureWorktreeOperationGenerationSnapshot({ - executionHostId: movedHost, - runtimeEnvironmentId: null - }) - } - } - ] - }) - ) - expect(getHistory.mock.calls.at(-1)?.[0].connectionId).toBe( - movedHost === 'ssh:A' ? 'A' : undefined - ) - expect(gitGraphHistoryForTab(workspaceId).view.getState().selectedId).toBe('a') - await act(async () => root.render(null)) - useAppStore.setState({ openFiles: original }) - expect(gitGraphHistoryForTab(workspaceId).view.getState().result).toBeUndefined() - } finally { - cleanup() - useAppStore.setState({ openFiles: original }) - } - } -) - it('waits for an in-flight page before retrieving a distant parent in one operation', async () => { let history: ReturnType function History() { @@ -632,23 +520,3 @@ it('keeps the confirm strip across tab switches, discarding it on navigation, fi await act(async () => root.render(render())) expect(container.textContent).not.toContain('Revert a?') }) - -it('canonicalizes selected refs before requesting history', async () => { - let history: ReturnType - function History() { - history = useGitGraphHistory(workspaceId) - return null - } - getHistory.mockReset().mockResolvedValue({ ...result, refs: 'all' }) - await act(async () => root.render()) - getHistory.mockClear().mockResolvedValue({ - ...result, - refs: ['refs/heads/main', 'refs/heads/topic'], - resolvedRefs: ['refs/heads/main', 'refs/heads/topic'] - }) - await act(async () => { - history!.changeFilter(['refs/heads/topic', 'refs/heads/main', 'refs/heads/topic']) - await history!.controller.settledResult() - }) - expect(getHistory.mock.calls[0]?.[1].refs).toEqual(['refs/heads/main', 'refs/heads/topic']) -}) diff --git a/src/renderer/src/components/editor/git-graph/git-graph-workspace-navigation.ts b/src/renderer/src/components/editor/git-graph/git-graph-workspace-navigation.ts index dd059828ebe..6867b8a83f0 100644 --- a/src/renderer/src/components/editor/git-graph/git-graph-workspace-navigation.ts +++ b/src/renderer/src/components/editor/git-graph/git-graph-workspace-navigation.ts @@ -5,7 +5,7 @@ import { getWorktreeExecutionHostId } from '../../../../../shared/execution-host import { findIndexedRepoOwnerForHost } from '@/lib/workspace/ownership/repo-owner-index' import { selectWorktreesByRepo } from '@/store/workspaces/catalog/workspace-record-fields' import { useAppStore } from '@/store' -import { gitGraphScope } from '@/lib/git-graph-tab' +import { gitGraphScope } from '@/lib/source-control/graph/git-graph-tab' import { getRuntimeGitRefs } from '@/runtime/source-control/git/runtime-git-status-client' export const gitGraphOwnerKey = (scope: ReturnType) => diff --git a/src/renderer/src/components/editor/git-graph/use-git-graph-history.ts b/src/renderer/src/components/editor/git-graph/use-git-graph-history.ts index 8e87e789707..58cc1cbcd54 100644 --- a/src/renderer/src/components/editor/git-graph/use-git-graph-history.ts +++ b/src/renderer/src/components/editor/git-graph/use-git-graph-history.ts @@ -1,7 +1,7 @@ import { useEffect } from 'react' import { useStore } from 'zustand' import { useAppStore } from '@/store' -import { useGitGraphScope } from '@/lib/git-graph-tab' +import { useGitGraphScope } from '@/lib/source-control/graph/git-graph-tab' import { gitGraphHistoryForTab } from './git-graph-tab-controllers' import type { GitHistoryRefs, diff --git a/src/renderer/src/components/editor/image-png-export.ts b/src/renderer/src/components/editor/image-png-export.ts index c9b8ee933e0..ab8efae13af 100644 --- a/src/renderer/src/components/editor/image-png-export.ts +++ b/src/renderer/src/components/editor/image-png-export.ts @@ -5,7 +5,7 @@ import { } from '../../../../shared/clipboard-image' import type { PanZoomSize } from '../../../../shared/pan-zoom-view' import { svgMarkupDataUri } from '../../../../shared/file-preview/svg-document' -import { blobToBase64, convertImageBlobToPng } from '../../lib/image-blob-png' +import { blobToBase64, convertImageBlobToPng } from '../../lib/clipboard/images/image-blob-png' const PREFERRED_PIXEL_RATIO = 2 // Chromium returns a blank canvas when a side exceeds this. diff --git a/src/renderer/src/components/editor/markdown-preview/review/MarkdownPreviewAnnotationComposer.tsx b/src/renderer/src/components/editor/markdown-preview/review/MarkdownPreviewAnnotationComposer.tsx index 18b945c93a9..bdb1f6f73c8 100644 --- a/src/renderer/src/components/editor/markdown-preview/review/MarkdownPreviewAnnotationComposer.tsx +++ b/src/renderer/src/components/editor/markdown-preview/review/MarkdownPreviewAnnotationComposer.tsx @@ -1,5 +1,5 @@ import type { DiffCommentOwner } from '@/store/slices/diff-comments/diff-comment-owner' -import { ImeTextarea } from '@/lib/ime-text-field' +import { ImeTextarea } from '@/lib/text-input/ime/ime-text-field' import { useCallback, useEffect, useRef, useState } from 'react' import { CornerDownLeft } from 'lucide-react' import { Button } from '@/components/ui/button' diff --git a/src/renderer/src/components/editor/markdown-preview/surface/use-markdown-preview-source-foundation.ts b/src/renderer/src/components/editor/markdown-preview/surface/use-markdown-preview-source-foundation.ts index 7da3e2aa8b2..a098069e424 100644 --- a/src/renderer/src/components/editor/markdown-preview/surface/use-markdown-preview-source-foundation.ts +++ b/src/renderer/src/components/editor/markdown-preview/surface/use-markdown-preview-source-foundation.ts @@ -3,7 +3,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { createConnectionIdForFileSelector } from '@/lib/workspace/ownership/connection-owner-resolution' import { computeEditorFontSize } from '@/lib/editor/appearance/editor-font-zoom' import type { HttpLinkSourceOwner } from '@/lib/browser/navigation/http-link-routing' -import { isMarkdownComment } from '@/lib/diff-comment-compat' +import { isMarkdownComment } from '@/lib/source-control/review/comments/diff-comment-compat' import { useFileOwnerRuntimeTarget } from '@/runtime/rpc/targets/use-worktree-runtime-target' import { useAppStore } from '@/store/index' import { exceedsMarkdownRichModeSizeLimit } from '../../markdown/render-mode/markdown-rich-size-limit' diff --git a/src/renderer/src/components/editor/markdown-preview/viewport/markdown-preview-scroll-input.ts b/src/renderer/src/components/editor/markdown-preview/viewport/markdown-preview-scroll-input.ts index 52a49641104..3b48a9e6340 100644 --- a/src/renderer/src/components/editor/markdown-preview/viewport/markdown-preview-scroll-input.ts +++ b/src/renderer/src/components/editor/markdown-preview/viewport/markdown-preview-scroll-input.ts @@ -1,4 +1,4 @@ -import { isEditableTarget } from '@/lib/editable-target' +import { isEditableTarget } from '@/lib/text-input/focus/editable-target' export function listenMarkdownPreviewScrollInput( container: HTMLDivElement, diff --git a/src/renderer/src/components/editor/markdown-preview/viewport/use-markdown-preview-viewport.ts b/src/renderer/src/components/editor/markdown-preview/viewport/use-markdown-preview-viewport.ts index 5aff2b1f4c6..45940e0a8ba 100644 --- a/src/renderer/src/components/editor/markdown-preview/viewport/use-markdown-preview-viewport.ts +++ b/src/renderer/src/components/editor/markdown-preview/viewport/use-markdown-preview-viewport.ts @@ -2,7 +2,7 @@ import { revealMarkdownNavigationDetails } from '../../markdown/navigation/markd import type { MarkdownPreviewDocument } from '../document-worker/markdown-preview-document-types' import type { VirtualMarkdownPreviewNavigation } from './use-markdown-preview-navigation' import { useCallback, useEffect, useLayoutEffect, type MutableRefObject } from 'react' -import { getShortcutPlatform } from '@/lib/shortcut-platform' +import { getShortcutPlatform } from '@/lib/keyboard/shortcut-platform' import { resolveMarkdownPreviewAddReviewNoteKey } from '../review/markdown-preview-annotation-shortcut' import { decodeMarkdownPreviewAnchor, @@ -14,7 +14,7 @@ import { isMarkdownPreviewFindShortcut } from '../search/markdown-preview-search import { useMarkdownPreviewDomSearch } from '../search/use-markdown-preview-dom-search' import type { MarkdownPreviewFoundation } from '../surface/use-markdown-preview-foundation' import { useMarkdownPreviewScrollViewport } from './use-markdown-preview-scroll-viewport' -import { addAppKeyListener, removeAppKeyListener } from '@/lib/app-keyboard-router' +import { addAppKeyListener, removeAppKeyListener } from '@/lib/keyboard/app-keyboard-router' export function useMarkdownPreviewViewport({ foundation, diff --git a/src/renderer/src/components/editor/markdown-source-tools/monaco/use-monaco-markdown-annotations.ts b/src/renderer/src/components/editor/markdown-source-tools/monaco/use-monaco-markdown-annotations.ts index 1aaf08c391b..5ce92cf87e7 100644 --- a/src/renderer/src/components/editor/markdown-source-tools/monaco/use-monaco-markdown-annotations.ts +++ b/src/renderer/src/components/editor/markdown-source-tools/monaco/use-monaco-markdown-annotations.ts @@ -18,7 +18,7 @@ import type { WorkspaceResource } from '@/lib/workspace/ownership/workspace-copy import { toast } from 'sonner' import { useTranslate } from '@/i18n/use-translate' import { useAppStore } from '@/store/index' -import { isMarkdownComment } from '@/lib/diff-comment-compat' +import { isMarkdownComment } from '@/lib/source-control/review/comments/diff-comment-compat' import { formatMarkdownReviewNotes, type MarkdownReviewNote diff --git a/src/renderer/src/components/editor/monaco-context-menu-paste.ts b/src/renderer/src/components/editor/monaco-context-menu-paste.ts index 97741aa32b9..a3cb5c457a2 100644 --- a/src/renderer/src/components/editor/monaco-context-menu-paste.ts +++ b/src/renderer/src/components/editor/monaco-context-menu-paste.ts @@ -1,6 +1,6 @@ import type { editor } from 'monaco-editor' import type { ReadClipboardTextOptions } from '../../../../shared/clipboard-text' -import { measureTextControlPasteByteLength } from '@/lib/text-control-paste' +import { measureTextControlPasteByteLength } from '@/lib/text-input/paste/text-control-paste' import { MONACO_PASTE_DIRECT_MAX_BYTES, MONACO_PASTE_MAX_BYTES, diff --git a/src/renderer/src/components/editor/monaco-large-text-paste.ts b/src/renderer/src/components/editor/monaco-large-text-paste.ts index 88ed77eb5da..8916e996579 100644 --- a/src/renderer/src/components/editor/monaco-large-text-paste.ts +++ b/src/renderer/src/components/editor/monaco-large-text-paste.ts @@ -4,7 +4,7 @@ import { getUtf8ChunkEndIndex } from '../../../../shared/utf8-byte-limits' import { measureTextControlPasteByteLength, measureTextControlPasteByteLengthWithYield -} from '@/lib/text-control-paste' +} from '@/lib/text-input/paste/text-control-paste' export const MONACO_PASTE_DIRECT_MAX_BYTES = 64 * 1024 export const MONACO_PASTE_CHUNK_MAX_BYTES = 16 * 1024 diff --git a/src/renderer/src/components/editor/notebook/document/IpynbViewer.tsx b/src/renderer/src/components/editor/notebook/document/IpynbViewer.tsx index 5f538ade877..138d584813b 100644 --- a/src/renderer/src/components/editor/notebook/document/IpynbViewer.tsx +++ b/src/renderer/src/components/editor/notebook/document/IpynbViewer.tsx @@ -1,4 +1,4 @@ -import { isImeOwnedKeyboardEvent } from '@/lib/ime-composition-keyboard-event' +import { isImeOwnedKeyboardEvent } from '@/lib/text-input/ime/ime-composition-keyboard-event' import { useCallback, useMemo, useState } from 'react' import { AlertCircle, Save } from 'lucide-react' import { computeEditorFontSize } from '@/lib/editor/appearance/editor-font-zoom' @@ -15,7 +15,7 @@ import { import { useShortcutKeyDetails } from '@/hooks/useShortcutLabel' import { useTranslate } from '@/i18n/use-translate' import { editorShortcutMatches } from '../../editor-shortcuts' -import { getShortcutPlatform } from '@/lib/shortcut-platform' +import { getShortcutPlatform } from '@/lib/keyboard/shortcut-platform' import { findFirstIndexedWorktree } from '@/lib/workspace/ownership/worktree-runtime-owner-index' import { IpynbCellToolbar, IpynbToolbarButton } from '../cells/IpynbCellToolbar' import { IpynbCellSource } from '../cells/IpynbCellEditor' diff --git a/src/renderer/src/components/editor/rich-markdown/clipboard/rich-markdown-large-text-paste.ts b/src/renderer/src/components/editor/rich-markdown/clipboard/rich-markdown-large-text-paste.ts index 0543fee9517..d0d490c7407 100644 --- a/src/renderer/src/components/editor/rich-markdown/clipboard/rich-markdown-large-text-paste.ts +++ b/src/renderer/src/components/editor/rich-markdown/clipboard/rich-markdown-large-text-paste.ts @@ -10,7 +10,7 @@ import { import { measureTextControlPasteByteLength, measureTextControlPasteByteLengthWithYield -} from '@/lib/text-control-paste' +} from '@/lib/text-input/paste/text-control-paste' import { translate } from '@/i18n/i18n' import { getRichMarkdownImageResolverContextVersion } from '../images/rich-markdown-image-context' import { trackRichMarkdownLargePasteHistory } from './rich-markdown-large-paste-history' diff --git a/src/renderer/src/components/editor/rich-markdown/commands/rich-markdown-key-handler.test.ts b/src/renderer/src/components/editor/rich-markdown/commands/rich-markdown-key-handler.test.ts index 9293ae57213..32c3655c071 100644 --- a/src/renderer/src/components/editor/rich-markdown/commands/rich-markdown-key-handler.test.ts +++ b/src/renderer/src/components/editor/rich-markdown/commands/rich-markdown-key-handler.test.ts @@ -12,7 +12,7 @@ import { encodeRawMarkdownHtmlForRichEditor } from '../raw-source/raw-markdown-h import { commitRichMarkdownSerialization } from '../document/rich-markdown-serialization-commit' // Pin the shortcut platform because navigator.userAgent varies across test environments. -vi.mock('@/lib/shortcut-platform', () => ({ +vi.mock('@/lib/keyboard/shortcut-platform', () => ({ getShortcutPlatform: () => 'darwin' as NodeJS.Platform })) diff --git a/src/renderer/src/components/editor/rich-markdown/commands/rich-markdown-key-handler.ts b/src/renderer/src/components/editor/rich-markdown/commands/rich-markdown-key-handler.ts index f047c3e3e08..8eea6f91099 100644 --- a/src/renderer/src/components/editor/rich-markdown/commands/rich-markdown-key-handler.ts +++ b/src/renderer/src/components/editor/rich-markdown/commands/rich-markdown-key-handler.ts @@ -1,8 +1,8 @@ import type { KeyHandlerContext } from './rich-markdown-key-handler-context' -import { isImeOwnedKeyboardEvent } from '@/lib/ime-composition-keyboard-event' +import { isImeOwnedKeyboardEvent } from '@/lib/text-input/ime/ime-composition-keyboard-event' import type { Editor } from '@tiptap/react' -import { getShortcutPlatform } from '@/lib/shortcut-platform' +import { getShortcutPlatform } from '@/lib/keyboard/shortcut-platform' import { useAppStore } from '@/store/index' import { isMarkdownPreviewFindShortcut } from '../../markdown-preview/search/markdown-preview-search' import { handleRichMarkdownAddReviewNoteShortcut } from '../review/rich-markdown-annotation-shortcut' diff --git a/src/renderer/src/components/editor/rich-markdown/images/rich-markdown-blob-image-paste.test.ts b/src/renderer/src/components/editor/rich-markdown/images/rich-markdown-blob-image-paste.test.ts index 2d800ba69db..a70b54dbd8a 100644 --- a/src/renderer/src/components/editor/rich-markdown/images/rich-markdown-blob-image-paste.test.ts +++ b/src/renderer/src/components/editor/rich-markdown/images/rich-markdown-blob-image-paste.test.ts @@ -4,7 +4,7 @@ import { useAppStore } from '@/store/index' import type { OpenFile } from '@/store/slices/editor/types/open-file' import { makeWorktree } from '@/store/slices/worktrees/worktree.test-fixture' import { TEST_REPO } from '@/store/repos/repo.test-fixture' -import type * as ImageBlobPng from '@/lib/image-blob-png' +import type * as ImageBlobPng from '@/lib/clipboard/images/image-blob-png' import { Editor } from '@tiptap/react' import StarterKit from '@tiptap/starter-kit' import Image from '@tiptap/extension-image' @@ -23,7 +23,7 @@ vi.mock('@/lib/workspace/ownership/connection-context', () => ({ getConnectionId: () => null, getConnectionIdForFile: () => null })) -vi.mock('@/lib/image-blob-png', async (original) => { +vi.mock('@/lib/clipboard/images/image-blob-png', async (original) => { const actual = await original() return { ...actual, convertImageBlobToPng: async (blob: Blob) => blob } }) diff --git a/src/renderer/src/components/editor/rich-markdown/images/rich-markdown-blob-image-paste.ts b/src/renderer/src/components/editor/rich-markdown/images/rich-markdown-blob-image-paste.ts index 466694658dd..077a55f7c54 100644 --- a/src/renderer/src/components/editor/rich-markdown/images/rich-markdown-blob-image-paste.ts +++ b/src/renderer/src/components/editor/rich-markdown/images/rich-markdown-blob-image-paste.ts @@ -2,7 +2,7 @@ import type { Editor } from '@tiptap/react' import type { Slice } from '@tiptap/pm/model' import { toast } from 'sonner' import { translate } from '@/i18n/i18n' -import { blobToBase64, convertImageBlobToPng } from '@/lib/image-blob-png' +import { blobToBase64, convertImageBlobToPng } from '@/lib/clipboard/images/image-blob-png' import { importExternalPathsToRuntime } from '@/runtime/files/imports/runtime-file-import-client' import { callRuntimeRpc } from '@/runtime/rpc/calls/runtime-rpc-call' import { assertClipboardImageBase64LengthWithinLimit } from '../../../../../../shared/clipboard-image' diff --git a/src/renderer/src/components/editor/rich-markdown/links/RichMarkdownLinkBubble.tsx b/src/renderer/src/components/editor/rich-markdown/links/RichMarkdownLinkBubble.tsx index c370307825c..a40a3bb1448 100644 --- a/src/renderer/src/components/editor/rich-markdown/links/RichMarkdownLinkBubble.tsx +++ b/src/renderer/src/components/editor/rich-markdown/links/RichMarkdownLinkBubble.tsx @@ -1,4 +1,4 @@ -import { ImeInput } from '@/lib/ime-text-field' +import { ImeInput } from '@/lib/text-input/ime/ime-text-field' import React, { useCallback, useEffect, useRef, useState } from 'react' import { createPortal } from 'react-dom' import type { Editor } from '@tiptap/react' diff --git a/src/renderer/src/components/editor/rich-markdown/review/rich-markdown-review-note-layout.ts b/src/renderer/src/components/editor/rich-markdown/review/rich-markdown-review-note-layout.ts index ba366de1c30..5b9b8f79613 100644 --- a/src/renderer/src/components/editor/rich-markdown/review/rich-markdown-review-note-layout.ts +++ b/src/renderer/src/components/editor/rich-markdown/review/rich-markdown-review-note-layout.ts @@ -1,5 +1,5 @@ import type { DiffComment } from '../../../../../../shared/diff-comment-types' -import { getCommentBodyLayoutLineCount } from '@/lib/comment-body-line-count' +import { getCommentBodyLayoutLineCount } from '@/lib/source-control/review/comments/comment-body-line-count' export type RichMarkdownReviewNotePosition = { comment: DiffComment diff --git a/src/renderer/src/components/editor/rich-markdown/review/useRichMarkdownReviewData.ts b/src/renderer/src/components/editor/rich-markdown/review/useRichMarkdownReviewData.ts index 5c1f52edfd5..1227b03d9c5 100644 --- a/src/renderer/src/components/editor/rich-markdown/review/useRichMarkdownReviewData.ts +++ b/src/renderer/src/components/editor/rich-markdown/review/useRichMarkdownReviewData.ts @@ -1,7 +1,7 @@ import { useMemo } from 'react' import { getRelativePathInsideRoot } from '@/lib/path' import { normalizeRelativePath } from '../../../../../../shared/cross-platform-path' -import { isMarkdownComment } from '@/lib/diff-comment-compat' +import { isMarkdownComment } from '@/lib/source-control/review/comments/diff-comment-compat' import { formatMarkdownReviewNotes, sortMarkdownReviewNotes, diff --git a/src/renderer/src/components/editor/rich-markdown/search/RichMarkdownSearchBar.tsx b/src/renderer/src/components/editor/rich-markdown/search/RichMarkdownSearchBar.tsx index bbcb6d57623..8f79f8e0309 100644 --- a/src/renderer/src/components/editor/rich-markdown/search/RichMarkdownSearchBar.tsx +++ b/src/renderer/src/components/editor/rich-markdown/search/RichMarkdownSearchBar.tsx @@ -12,7 +12,7 @@ import { import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { useTranslate } from '@/i18n/use-translate' -import { isImeCompositionKeyDown } from '@/lib/ime-composition-keyboard-event' +import { isImeCompositionKeyDown } from '@/lib/text-input/ime/ime-composition-keyboard-event' import { useOptionalShortcutLabel } from '@/hooks/useShortcutLabel' type RichMarkdownSearchBarProps = { diff --git a/src/renderer/src/components/editor/rich-markdown/search/useRichMarkdownSearch.ts b/src/renderer/src/components/editor/rich-markdown/search/useRichMarkdownSearch.ts index cd11cb41d50..818df4aa70c 100644 --- a/src/renderer/src/components/editor/rich-markdown/search/useRichMarkdownSearch.ts +++ b/src/renderer/src/components/editor/rich-markdown/search/useRichMarkdownSearch.ts @@ -1,6 +1,6 @@ import { useCallback, useEffect, useMemo, useRef, useState, type RefObject } from 'react' import type { Editor } from '@tiptap/react' -import { getShortcutPlatform } from '@/lib/shortcut-platform' +import { getShortcutPlatform } from '@/lib/keyboard/shortcut-platform' import { useAppStore } from '@/store/index' import { isMarkdownPreviewFindShortcut, @@ -14,8 +14,8 @@ import { } from './rich-markdown-search' import { createRichMarkdownSearchMatchesCache } from './rich-markdown-search-matches-cache' import { useRichMarkdownSearchHighlights } from './useRichMarkdownSearchHighlights' -import { addAppKeyListener, removeAppKeyListener } from '@/lib/app-keyboard-router' -import { isImeOwnedKeyboardEvent } from '@/lib/ime-composition-keyboard-event' +import { addAppKeyListener, removeAppKeyListener } from '@/lib/keyboard/app-keyboard-router' +import { isImeOwnedKeyboardEvent } from '@/lib/text-input/ime/ime-composition-keyboard-event' export function useRichMarkdownSearch({ editor, diff --git a/src/renderer/src/components/editor/setup-contextual-copy.ts b/src/renderer/src/components/editor/setup-contextual-copy.ts index 4b10cf8e6bf..9ffc872641d 100644 --- a/src/renderer/src/components/editor/setup-contextual-copy.ts +++ b/src/renderer/src/components/editor/setup-contextual-copy.ts @@ -12,7 +12,7 @@ import { PRIMARY_SELECTION_MAX_LENGTH, isPrimarySelectionEnabled, setPrimarySelectionText -} from '@/lib/primary-selection' +} from '@/lib/clipboard/primary-selection/primary-selection' import { translate } from '@/i18n/i18n' export function setupContextualCopy({ diff --git a/src/renderer/src/components/editor/use-pdf-viewer-find-shortcut.ts b/src/renderer/src/components/editor/use-pdf-viewer-find-shortcut.ts index a63fc482a6c..f55af428a3b 100644 --- a/src/renderer/src/components/editor/use-pdf-viewer-find-shortcut.ts +++ b/src/renderer/src/components/editor/use-pdf-viewer-find-shortcut.ts @@ -1,9 +1,9 @@ import { useEffect, type RefObject } from 'react' -import { getShortcutPlatform } from '@/lib/shortcut-platform' -import { isEditableTarget } from '@/lib/editable-target' +import { getShortcutPlatform } from '@/lib/keyboard/shortcut-platform' +import { isEditableTarget } from '@/lib/text-input/focus/editable-target' import { isEventTargetInsideFloatingWorkspacePanel } from '@/lib/floating-workspace-terminal-actions' import { keybindingMatchesAction, type KeybindingOverrides } from '../../../../shared/keybindings' -import { addAppKeyListener, removeAppKeyListener } from '@/lib/app-keyboard-router' +import { addAppKeyListener, removeAppKeyListener } from '@/lib/keyboard/app-keyboard-router' /** Claim Find inside this PDF or through its focused group, excluding other text fields and split/window surfaces. */ export function pdfViewerOwnsFind( diff --git a/src/renderer/src/components/editor/view-mode/useMarkdownPreviewShortcut.ts b/src/renderer/src/components/editor/view-mode/useMarkdownPreviewShortcut.ts index 05c867e633f..7439ee90d6b 100644 --- a/src/renderer/src/components/editor/view-mode/useMarkdownPreviewShortcut.ts +++ b/src/renderer/src/components/editor/view-mode/useMarkdownPreviewShortcut.ts @@ -1,11 +1,11 @@ import { editorViewPlacement } from '@/store/slices/editor/tabs/editor-view-placement' import { useEffect, type RefObject } from 'react' import { detectLanguage } from '../../../../../shared/editor-language/editor-language-detect' -import { getShortcutPlatform } from '@/lib/shortcut-platform' +import { getShortcutPlatform } from '@/lib/keyboard/shortcut-platform' import { useAppStore } from '@/store/index' import type { OpenFile } from '@/store/slices/editor/types/open-file' import { canOpenMarkdownPreview, isMarkdownPreviewShortcut } from './markdown-preview-controls' -import { addAppKeyListener, removeAppKeyListener } from '@/lib/app-keyboard-router' +import { addAppKeyListener, removeAppKeyListener } from '@/lib/keyboard/app-keyboard-router' type UseMarkdownPreviewShortcutParams = { activeViewStateId?: string | null diff --git a/src/renderer/src/components/feature-wall/use-feature-wall-tour-keyboard-shortcut.ts b/src/renderer/src/components/feature-wall/use-feature-wall-tour-keyboard-shortcut.ts index 0bbcb7481b8..ae8af9f8cd2 100644 --- a/src/renderer/src/components/feature-wall/use-feature-wall-tour-keyboard-shortcut.ts +++ b/src/renderer/src/components/feature-wall/use-feature-wall-tour-keyboard-shortcut.ts @@ -1,6 +1,6 @@ import { useEffect } from 'react' import { isScreenSubmitShortcut } from '@/lib/screen-submit-shortcut' -import { addAppKeyListener, removeAppKeyListener } from '@/lib/app-keyboard-router' +import { addAppKeyListener, removeAppKeyListener } from '@/lib/keyboard/app-keyboard-router' export function useFeatureWallTourKeyboardShortcut({ isOpen, diff --git a/src/renderer/src/components/floating-terminal/use-floating-terminal-global-shortcut-listeners.ts b/src/renderer/src/components/floating-terminal/use-floating-terminal-global-shortcut-listeners.ts index e81b152eaee..25317fe22d4 100644 --- a/src/renderer/src/components/floating-terminal/use-floating-terminal-global-shortcut-listeners.ts +++ b/src/renderer/src/components/floating-terminal/use-floating-terminal-global-shortcut-listeners.ts @@ -4,7 +4,7 @@ import { isFloatingWorkspaceTerminalInputTarget, switchFloatingWorkspaceTab } from '@/lib/floating-workspace-terminal-actions' -import { getShortcutPlatform } from '@/lib/shortcut-platform' +import { getShortcutPlatform } from '@/lib/keyboard/shortcut-platform' import { useAppStore } from '@/store' import { keybindingMatchesAction, @@ -14,7 +14,7 @@ import { import { toModifierDoubleTapEvent } from '../../../../shared/modifier-double-tap-detector' import type { FloatingTerminalPanelLocalState } from './use-floating-terminal-panel-local-state' import type { FloatingTerminalPanelShortcuts } from './use-floating-terminal-panel-shortcuts' -import { addAppKeyListener, removeAppKeyListener } from '@/lib/app-keyboard-router' +import { addAppKeyListener, removeAppKeyListener } from '@/lib/keyboard/app-keyboard-router' const FLOATING_TAB_SWITCH_ACTIONS: readonly KeybindingActionId[] = [ 'tab.nextSameType', diff --git a/src/renderer/src/components/floating-terminal/use-floating-terminal-panel-shortcuts.ts b/src/renderer/src/components/floating-terminal/use-floating-terminal-panel-shortcuts.ts index 766a2e8a017..96a4685fc45 100644 --- a/src/renderer/src/components/floating-terminal/use-floating-terminal-panel-shortcuts.ts +++ b/src/renderer/src/components/floating-terminal/use-floating-terminal-panel-shortcuts.ts @@ -6,7 +6,7 @@ import { matchFloatingWorkspacePanelShortcut } from '@/lib/floating-workspace-shortcut-policy' import { isFloatingWorkspaceTerminalInputTarget } from '@/lib/floating-workspace-terminal-actions' -import { getShortcutPlatform } from '@/lib/shortcut-platform' +import { getShortcutPlatform } from '@/lib/keyboard/shortcut-platform' import { requestTerminalTabRename } from '@/components/tab-bar/terminal-tab-rename-request' import { resolveTerminalAgentTabShortcut } from '@/components/terminal-agent-tab-shortcut' import { launchFloatingWorkspaceAgentShortcut } from '@/components/agent-tab-shortcut-launch' diff --git a/src/renderer/src/components/github-item-dialog/discuss-item/comment-reply-form.tsx b/src/renderer/src/components/github-item-dialog/discuss-item/comment-reply-form.tsx index 2e61e0b98b9..ccd49525724 100644 --- a/src/renderer/src/components/github-item-dialog/discuss-item/comment-reply-form.tsx +++ b/src/renderer/src/components/github-item-dialog/discuss-item/comment-reply-form.tsx @@ -8,7 +8,7 @@ import type { GitHubImageUploadAddress } from '@/components/github/github-image- import { getCommentBodySubmitState, hasBoundedCommentBodyText -} from '@/lib/comment-body-submit-state' +} from '@/lib/source-control/review/comments/comment-body-submit-state' import { translate } from '@/i18n/i18n' import { useTranslate } from '@/i18n/use-translate' diff --git a/src/renderer/src/components/github-item-dialog/discuss-item/conversation-tab.tsx b/src/renderer/src/components/github-item-dialog/discuss-item/conversation-tab.tsx index 970ec2d78e4..6d1183f74f7 100644 --- a/src/renderer/src/components/github-item-dialog/discuss-item/conversation-tab.tsx +++ b/src/renderer/src/components/github-item-dialog/discuss-item/conversation-tab.tsx @@ -6,7 +6,7 @@ import { getPRCommentAudienceCounts, type PRCommentAudienceFilter } from '../../../../../shared/source-control/review-comments/pr-comment-audience' -import { usePRBotAuthorOverrides } from '@/lib/pr-bot-author-overrides' +import { usePRBotAuthorOverrides } from '@/lib/source-control/review/pr-bot-author-overrides' import { groupPRComments } from '../../../../../shared/source-control/review-comments/pr-comment-groups' import { getCommentReplyTargetCandidates, @@ -18,9 +18,9 @@ import { } from '@/components/right-sidebar/checks-panel/comments/pr-comments-ai-launch-ack' import { buildPRCommentConversationReplyBody } from '@/components/right-sidebar/checks-panel/comments/pr-comment-fixing-reply-body' import { useAppStore } from '@/store' -import { canUseGitHubRepoContext } from '@/lib/github-source-runtime-context' +import { canUseGitHubRepoContext } from '@/lib/work-items/github/github-source-runtime-context' import { getGitHubMutationTarget } from '@/components/github/github-work-item-mutation-target' -import { taskSourceRuntimeTarget } from '@/lib/task-source-runtime-target' +import { taskSourceRuntimeTarget } from '@/lib/work-items/task-source-runtime-target' import { githubCommentImageUploadAddress } from '@/components/github/github-comment-image-upload-address' import { resolveGitHubBodyDraft, @@ -57,7 +57,7 @@ import { GHCommentComposer } from './gh-comment-composer' import { ConversationTabDescription } from './conversation-tab-description' import { ConversationTabActivity } from './conversation-tab-activity' import { ConversationTabPRSidebar } from './conversation-tab-pr-sidebar' -import { codeHostErrorText } from '@/lib/code-host-error-copy' +import { codeHostErrorText } from '@/lib/source-control/remote/code-host-error-copy' export function ConversationTab({ item, diff --git a/src/renderer/src/components/github-item-dialog/discuss-item/gh-comment-composer.tsx b/src/renderer/src/components/github-item-dialog/discuss-item/gh-comment-composer.tsx index 8cc593a2043..26eb86b4117 100644 --- a/src/renderer/src/components/github-item-dialog/discuss-item/gh-comment-composer.tsx +++ b/src/renderer/src/components/github-item-dialog/discuss-item/gh-comment-composer.tsx @@ -10,14 +10,14 @@ import { githubCommentImageUploadAddress } from '@/components/github/github-comm import { getCommentBodySubmitState, hasBoundedCommentBodyText -} from '@/lib/comment-body-submit-state' +} from '@/lib/source-control/review/comments/comment-body-submit-state' import { addIssueCommentForRepo } from '@/components/github/github-work-item-comment-mutations' import type { PRComment } from '../../../../../shared/github/comment-types' import type { GitHubOwnerRepo } from '../../../../../shared/github/pull-request-types' import type { TaskSourceContext } from '../../../../../shared/work-items/task-source-context' import { translate } from '@/i18n/i18n' import { useTranslate } from '@/i18n/use-translate' -import { codeHostErrorText } from '@/lib/code-host-error-copy' +import { codeHostErrorText } from '@/lib/source-control/remote/code-host-error-copy' export function GHCommentComposer({ className, diff --git a/src/renderer/src/components/github-item-dialog/edit-item-fields/gh-edit-section.tsx b/src/renderer/src/components/github-item-dialog/edit-item-fields/gh-edit-section.tsx index 47de49ec951..33995bb59e0 100644 --- a/src/renderer/src/components/github-item-dialog/edit-item-fields/gh-edit-section.tsx +++ b/src/renderer/src/components/github-item-dialog/edit-item-fields/gh-edit-section.tsx @@ -5,7 +5,7 @@ import { useRepoLabels, useRepoAssignees } from '@/hooks/useGitHubRepoMetadata' import { useImmediateMutation } from '@/hooks/useIssueMetadata' import { useRepoLabelsBySlug, useRepoAssigneesBySlug } from '@/hooks/useGitHubSlugMetadata' import { githubRepoIdentityKey } from '../../../../../shared/github/repository-identity-key' -import { getGitHubRepoRoutingTarget } from '@/lib/github-source-runtime-context' +import { getGitHubRepoRoutingTarget } from '@/lib/work-items/github/github-source-runtime-context' import { getTaskPageGitHubDuplicateCandidates, getTaskPageGitHubDuplicateTargetErrorMessage, diff --git a/src/renderer/src/components/github-item-dialog/edit-item-fields/gh-edit-task-record.ts b/src/renderer/src/components/github-item-dialog/edit-item-fields/gh-edit-task-record.ts index 1c9f635ab46..b2a4d780105 100644 --- a/src/renderer/src/components/github-item-dialog/edit-item-fields/gh-edit-task-record.ts +++ b/src/renderer/src/components/github-item-dialog/edit-item-fields/gh-edit-task-record.ts @@ -1,5 +1,5 @@ import { useAppStore } from '@/store' -import { getWorkItemRepo } from '@/lib/work-item-repo-row' +import { getWorkItemRepo } from '@/lib/work-items/work-item-repo-row' import { untrackedTaskOperation, type TaskChange } from '@/store/github/task-records/task-change' import type { GitHubWorkItem } from '../../../../../shared/github/work-item-types' import type { TaskSourceContext } from '../../../../../shared/work-items/task-source-context' diff --git a/src/renderer/src/components/github-item-dialog/inspect-pull-request/checks-tab-actions.ts b/src/renderer/src/components/github-item-dialog/inspect-pull-request/checks-tab-actions.ts index ecb9b2d7104..b1813f82765 100644 --- a/src/renderer/src/components/github-item-dialog/inspect-pull-request/checks-tab-actions.ts +++ b/src/renderer/src/components/github-item-dialog/inspect-pull-request/checks-tab-actions.ts @@ -1,4 +1,4 @@ -import { readPRCheckRefreshOwner } from '@/lib/pr-check-refresh-owner' +import { readPRCheckRefreshOwner } from '@/lib/source-control/checks/pr-check-refresh-owner' import type React from 'react' import { toast } from 'sonner' import { callRuntimeRpc } from '@/runtime/rpc/calls/runtime-rpc-call' @@ -8,15 +8,18 @@ import { type GitHubChecksContextOwner, type GitHubChecksTabState } from '@/components/github-checks-tab-state' -import { getGitHubRuntimeRepoId, type GitHubRuntimeHost } from '@/lib/github-source-runtime-context' -import { startFixChecksAgent } from '@/lib/fix-checks-agent-launch' +import { + getGitHubRuntimeRepoId, + type GitHubRuntimeHost +} from '@/lib/work-items/github/github-source-runtime-context' +import { startFixChecksAgent } from '@/lib/source-control/checks/fix-checks-agent-launch' import { buildFixBrokenChecksPrompt } from '../../../../../shared/source-control/checks/pr-checks-fix-prompt' import type { PRCheckDetail } from '../../../../../shared/github/check-types' import type { GitHubOwnerRepo } from '../../../../../shared/github/pull-request-types' import type { GitHubWorkItem } from '../../../../../shared/github/work-item-types' import type { TaskSourceContext } from '../../../../../shared/work-items/task-source-context' import { translate } from '@/i18n/i18n' -import { codeHostErrorText } from '@/lib/code-host-error-copy' +import { codeHostErrorText } from '@/lib/source-control/remote/code-host-error-copy' type RuntimeHost = GitHubRuntimeHost | null diff --git a/src/renderer/src/components/github-item-dialog/inspect-pull-request/checks-tab-request-details.ts b/src/renderer/src/components/github-item-dialog/inspect-pull-request/checks-tab-request-details.ts index cc4fad7ba1d..c9877e753da 100644 --- a/src/renderer/src/components/github-item-dialog/inspect-pull-request/checks-tab-request-details.ts +++ b/src/renderer/src/components/github-item-dialog/inspect-pull-request/checks-tab-request-details.ts @@ -11,7 +11,7 @@ import { settleGitHubChecksTabDetails, type CheckDetailsLoadState } from '@/components/github-checks-tab-state' -import { getGitHubRuntimeRepoId } from '@/lib/github-source-runtime-context' +import { getGitHubRuntimeRepoId } from '@/lib/work-items/github/github-source-runtime-context' import type { PRCheckDetail } from '../../../../../shared/github/check-types' import { translate } from '@/i18n/i18n' import type { ChecksTabActionContext } from './checks-tab-actions' diff --git a/src/renderer/src/components/github-item-dialog/inspect-pull-request/checks-tab.tsx b/src/renderer/src/components/github-item-dialog/inspect-pull-request/checks-tab.tsx index 1c688842f40..807ef270d75 100644 --- a/src/renderer/src/components/github-item-dialog/inspect-pull-request/checks-tab.tsx +++ b/src/renderer/src/components/github-item-dialog/inspect-pull-request/checks-tab.tsx @@ -14,7 +14,7 @@ import { import { canUseGitHubRepoContext, getGitHubSourceRuntimeHost -} from '@/lib/github-source-runtime-context' +} from '@/lib/work-items/github/github-source-runtime-context' import { getBrokenChecks } from '../../../../../shared/source-control/checks/pr-checks-fix-prompt' import { githubRepoIdentityKey } from '../../../../../shared/github/repository-identity-key' import type { PRCheckDetail } from '../../../../../shared/github/check-types' diff --git a/src/renderer/src/components/github-item-dialog/inspect-pull-request/pr-files-combined-diff-load.ts b/src/renderer/src/components/github-item-dialog/inspect-pull-request/pr-files-combined-diff-load.ts index 918761d60ff..24c0e89f9c3 100644 --- a/src/renderer/src/components/github-item-dialog/inspect-pull-request/pr-files-combined-diff-load.ts +++ b/src/renderer/src/components/github-item-dialog/inspect-pull-request/pr-files-combined-diff-load.ts @@ -21,7 +21,7 @@ import type { import type { TaskSourceContext } from '../../../../../shared/work-items/task-source-context' import { translate } from '@/i18n/i18n' import { loadPRFileContents } from '../load-item-details/pr-file-content-cache' -import { codeHostErrorText } from '@/lib/code-host-error-copy' +import { codeHostErrorText } from '@/lib/source-control/remote/code-host-error-copy' type LoadSectionArgs = { index: number diff --git a/src/renderer/src/components/github-item-dialog/land-pull-request/pr-actions-panel.tsx b/src/renderer/src/components/github-item-dialog/land-pull-request/pr-actions-panel.tsx index 09e1be54910..287b84a6294 100644 --- a/src/renderer/src/components/github-item-dialog/land-pull-request/pr-actions-panel.tsx +++ b/src/renderer/src/components/github-item-dialog/land-pull-request/pr-actions-panel.tsx @@ -10,7 +10,7 @@ import { callRuntimeRpc } from '@/runtime/rpc/calls/runtime-rpc-call' import { getGitHubRepoRoutingTarget, getGitHubRuntimeRepoId -} from '@/lib/github-source-runtime-context' +} from '@/lib/work-items/github/github-source-runtime-context' import { presentGitHubPRMergeState } from '@/components/github-pr-merge-state' import { resolveGitHubPRMergeMethods } from '../../../../../shared/github/pull-request-merge-methods' import type { GitHubPRMergeMethod } from '../../../../../shared/github/pull-request-types' @@ -29,7 +29,7 @@ import { runPullRequestStateUpdate } from '@/components/github/github-work-item- import type { GitHubItemDialogProjectOrigin } from '../load-item-details/github-item-dialog-types' import { PRActionsMergeMenu } from './pr-actions-merge-menu' import { WorkItemStateBadge } from '../load-item-details/work-item-state-badge' -import { codeHostErrorText } from '@/lib/code-host-error-copy' +import { codeHostErrorText } from '@/lib/source-control/remote/code-host-error-copy' export function PRActionsPanel({ item, diff --git a/src/renderer/src/components/github-item-dialog/land-pull-request/pr-reviewers-panel.tsx b/src/renderer/src/components/github-item-dialog/land-pull-request/pr-reviewers-panel.tsx index d93837961de..b2c0d13782e 100644 --- a/src/renderer/src/components/github-item-dialog/land-pull-request/pr-reviewers-panel.tsx +++ b/src/renderer/src/components/github-item-dialog/land-pull-request/pr-reviewers-panel.tsx @@ -15,7 +15,7 @@ import type { GitHubAssignableUser } from '../../../../../shared/github/pull-req import type { GitHubWorkItem } from '../../../../../shared/github/work-item-types' import type { TaskSourceContext } from '../../../../../shared/work-items/task-source-context' import { useTranslate } from '@/i18n/use-translate' -import { getGitHubRepoRoutingTarget } from '@/lib/github-source-runtime-context' +import { getGitHubRepoRoutingTarget } from '@/lib/work-items/github/github-source-runtime-context' import { resolvePullRequestRepo } from '@/components/github/github-work-item-identity' import { mergeReviewerSuggestions } from '@/components/github/work-item-state-presentation' import type { GitHubItemDialogProjectOrigin } from '../load-item-details/github-item-dialog-types' diff --git a/src/renderer/src/components/github-item-dialog/land-pull-request/pr-reviewers-request-actions.ts b/src/renderer/src/components/github-item-dialog/land-pull-request/pr-reviewers-request-actions.ts index ec2724fc104..f5a64a990cc 100644 --- a/src/renderer/src/components/github-item-dialog/land-pull-request/pr-reviewers-request-actions.ts +++ b/src/renderer/src/components/github-item-dialog/land-pull-request/pr-reviewers-request-actions.ts @@ -1,7 +1,7 @@ import { toast } from 'sonner' import { useAppStore } from '@/store' import { callRuntimeRpc } from '@/runtime/rpc/calls/runtime-rpc-call' -import { getGitHubRuntimeRepoId } from '@/lib/github-source-runtime-context' +import { getGitHubRuntimeRepoId } from '@/lib/work-items/github/github-source-runtime-context' import { normalizeGitHubReviewerLogins, parseGitHubReviewerInputLogins @@ -16,7 +16,7 @@ import type { GitHubWorkItem } from '../../../../../shared/github/work-item-type import type { TaskSourceContext } from '../../../../../shared/work-items/task-source-context' import type { RuntimeClientTarget } from '../../../../../shared/runtime-client-target' import { translate } from '@/i18n/i18n' -import { codeHostErrorText } from '@/lib/code-host-error-copy' +import { codeHostErrorText } from '@/lib/source-control/remote/code-host-error-copy' type ReviewerRequestArgs = { submitting: boolean diff --git a/src/renderer/src/components/github-item-dialog/load-item-details/github-work-item-target-context.ts b/src/renderer/src/components/github-item-dialog/load-item-details/github-work-item-target-context.ts index 6f4b98db0fe..e23c482ae78 100644 --- a/src/renderer/src/components/github-item-dialog/load-item-details/github-work-item-target-context.ts +++ b/src/renderer/src/components/github-item-dialog/load-item-details/github-work-item-target-context.ts @@ -1,5 +1,5 @@ import { useGitHubOperationOwner } from '@/hooks/useGitHubOperationScope' -import { canUseGitHubRepoContext } from '@/lib/github-source-runtime-context' +import { canUseGitHubRepoContext } from '@/lib/work-items/github/github-source-runtime-context' import { resolvePullRequestRepo, parseOwnerRepoFromItemUrl diff --git a/src/renderer/src/components/github-item-dialog/load-item-details/pr-file-content-cache.ts b/src/renderer/src/components/github-item-dialog/load-item-details/pr-file-content-cache.ts index 8bf5548abc8..b5a88077926 100644 --- a/src/renderer/src/components/github-item-dialog/load-item-details/pr-file-content-cache.ts +++ b/src/renderer/src/components/github-item-dialog/load-item-details/pr-file-content-cache.ts @@ -2,7 +2,7 @@ import { callRuntimeRpc } from '@/runtime/rpc/calls/runtime-rpc-call' import { getGitHubRuntimeRepoId, getGitHubSourceRuntimeHost -} from '@/lib/github-source-runtime-context' +} from '@/lib/work-items/github/github-source-runtime-context' import { PR_FILE_CONTENT_CACHE_MAX_BYTES, getRetainedPRFileContentsByteCount diff --git a/src/renderer/src/components/github-item-dialog/load-item-details/use-github-task-refresh.ts b/src/renderer/src/components/github-item-dialog/load-item-details/use-github-task-refresh.ts index 3ed44582f86..ea74f808637 100644 --- a/src/renderer/src/components/github-item-dialog/load-item-details/use-github-task-refresh.ts +++ b/src/renderer/src/components/github-item-dialog/load-item-details/use-github-task-refresh.ts @@ -2,9 +2,12 @@ import { useGitHubItemTargetIdentity } from './github-work-item-target-context' import { toast } from 'sonner' import { translate } from '@/i18n/i18n' import { runtimeEnvironmentSupportsCapability } from '@/runtime/rpc/compatibility/runtime-capability-proof' -import { getGitHubSourceRuntimeHost } from '@/lib/github-source-runtime-context' +import { getGitHubSourceRuntimeHost } from '@/lib/work-items/github/github-source-runtime-context' import { GITHUB_CANONICAL_DETAILS_RUNTIME_CAPABILITY } from '../../../../../shared/work-items/task-source-runtime-capabilities' -import { githubTaskExecutionIdentity, captureGitHubTaskTarget } from '@/lib/github-task-target' +import { + githubTaskExecutionIdentity, + captureGitHubTaskTarget +} from '@/lib/work-items/github/github-task-target' import { useCallback, useLayoutEffect, @@ -25,7 +28,7 @@ import { startWorkItemDetailsFetch, loadCanonicalGitHubTaskDetails } from './work-item-details-fetch-settle' -import { getWorkItemRepo } from '@/lib/work-item-repo-row' +import { getWorkItemRepo } from '@/lib/work-items/work-item-repo-row' import { useMountedRef } from '@/hooks/useMountedRef' export function useGitHubTaskRefresh({ diff --git a/src/renderer/src/components/github-item-dialog/load-item-details/use-github-work-item-details.ts b/src/renderer/src/components/github-item-dialog/load-item-details/use-github-work-item-details.ts index 89abe0ab33c..9a170dc3946 100644 --- a/src/renderer/src/components/github-item-dialog/load-item-details/use-github-work-item-details.ts +++ b/src/renderer/src/components/github-item-dialog/load-item-details/use-github-work-item-details.ts @@ -8,7 +8,7 @@ import { import { useCallback, useEffect, useMemo, useState, useSyncExternalStore } from 'react' import { useGitHubTaskItems } from '@/hooks/useGitHubTaskItems' import { useGitHubTaskRefresh } from './use-github-task-refresh' -import { addressedGitHubIssueItem } from '@/lib/github-task-target' +import { addressedGitHubIssueItem } from '@/lib/work-items/github/github-task-target' import { normalizeItemDialogTab, type ItemDialogTab diff --git a/src/renderer/src/components/github-item-dialog/load-item-details/work-item-details-fetch-settle.ts b/src/renderer/src/components/github-item-dialog/load-item-details/work-item-details-fetch-settle.ts index 52a9b019549..731bef192ed 100644 --- a/src/renderer/src/components/github-item-dialog/load-item-details/work-item-details-fetch-settle.ts +++ b/src/renderer/src/components/github-item-dialog/load-item-details/work-item-details-fetch-settle.ts @@ -12,7 +12,10 @@ import type { } from '../../../../../shared/github/work-item-types' import type { GitHubTaskRecordCache } from '@/store/github/task-records/task-record-cache' import type { TaskBinding } from '@/store/github/task-records/task-record-model' -import { githubTaskRpcOptions, type GitHubTaskTarget } from '@/lib/github-task-target' +import { + githubTaskRpcOptions, + type GitHubTaskTarget +} from '@/lib/work-items/github/github-task-target' import { useAppStore } from '@/store' import { lookupGitHubWorkItemDetailsForSource } from '@/lib/work-items/github/github-work-item-source-lookup' import { diff --git a/src/renderer/src/components/github-item-dialog/open-dialog/github-item-dialog-pr-tabs.tsx b/src/renderer/src/components/github-item-dialog/open-dialog/github-item-dialog-pr-tabs.tsx index d8fb55f4e85..e5aef3265ae 100644 --- a/src/renderer/src/components/github-item-dialog/open-dialog/github-item-dialog-pr-tabs.tsx +++ b/src/renderer/src/components/github-item-dialog/open-dialog/github-item-dialog-pr-tabs.tsx @@ -6,7 +6,7 @@ import { resolvePullRequestRepo, type ItemDialogTab } from '@/components/github/github-work-item-identity' -import { canUseGitHubRepoContext } from '@/lib/github-source-runtime-context' +import { canUseGitHubRepoContext } from '@/lib/work-items/github/github-source-runtime-context' import { useTranslate } from '@/i18n/use-translate' import type { PRComment } from '../../../../../shared/github/comment-types' import type { GitHubAssignableUser } from '../../../../../shared/github/pull-request-types' diff --git a/src/renderer/src/components/github-project/ProjectCellValueEditors.tsx b/src/renderer/src/components/github-project/ProjectCellValueEditors.tsx index a8601e7c780..953861adb2e 100644 --- a/src/renderer/src/components/github-project/ProjectCellValueEditors.tsx +++ b/src/renderer/src/components/github-project/ProjectCellValueEditors.tsx @@ -1,4 +1,4 @@ -import { ImeInput } from '@/lib/ime-text-field' +import { ImeInput } from '@/lib/text-input/ime/ime-text-field' import React, { useState } from 'react' import { Plus } from 'lucide-react' import { Input } from '@/components/ui/input' diff --git a/src/renderer/src/components/github-project/ProjectViewToolbar.tsx b/src/renderer/src/components/github-project/ProjectViewToolbar.tsx index 73563a511ce..7fc50b8eb72 100644 --- a/src/renderer/src/components/github-project/ProjectViewToolbar.tsx +++ b/src/renderer/src/components/github-project/ProjectViewToolbar.tsx @@ -7,7 +7,7 @@ import { useTranslate } from '@/i18n/use-translate' import ProjectPicker from './ProjectPicker' import type { ProjectViewTableState } from './useProjectViewTable' import { githubProjectHost } from '../../../../shared/github/project-identity' -import { addAppKeyListener, removeAppKeyListener } from '@/lib/app-keyboard-router' +import { addAppKeyListener, removeAppKeyListener } from '@/lib/keyboard/app-keyboard-router' import { openAppHttpLink } from '@/lib/browser/navigation/app-http-link-open' export function ProjectViewToolbar({ tableState }: { tableState: ProjectViewTableState }) { diff --git a/src/renderer/src/components/github-project/ProjectViewWrapper.tsx b/src/renderer/src/components/github-project/ProjectViewWrapper.tsx index c19fe2e6746..31bfc095261 100644 --- a/src/renderer/src/components/github-project/ProjectViewWrapper.tsx +++ b/src/renderer/src/components/github-project/ProjectViewWrapper.tsx @@ -22,7 +22,7 @@ import type { GitHubProjectRow } from '../../../../shared/github/project-types' import type { ExecutionHostId } from '../../../../shared/execution-host' -import { taskSourceRuntimeTarget } from '@/lib/task-source-runtime-target' +import { taskSourceRuntimeTarget } from '@/lib/work-items/task-source-runtime-target' import { openAppHttpLink } from '@/lib/browser/navigation/app-http-link-open' type Props = { diff --git a/src/renderer/src/components/github-project/project-dialog-state.ts b/src/renderer/src/components/github-project/project-dialog-state.ts index 5e0f68a40c3..cbd76e63136 100644 --- a/src/renderer/src/components/github-project/project-dialog-state.ts +++ b/src/renderer/src/components/github-project/project-dialog-state.ts @@ -1,5 +1,5 @@ import type { Repo } from '../../../../shared/repo-types' -import { getWorkItemRepo, type WorkItemRepoRef } from '@/lib/work-item-repo-row' +import { getWorkItemRepo, type WorkItemRepoRef } from '@/lib/work-items/work-item-repo-row' type RepoBackedProjectDialogState = { repoId: string diff --git a/src/renderer/src/components/github-project/slug-dialog/Comments.tsx b/src/renderer/src/components/github-project/slug-dialog/Comments.tsx index eb793aaf212..34898a7015e 100644 --- a/src/renderer/src/components/github-project/slug-dialog/Comments.tsx +++ b/src/renderer/src/components/github-project/slug-dialog/Comments.tsx @@ -8,7 +8,7 @@ import { gitHubSlugApiForTarget } from '@/runtime/repository-github/github-slug- import { getCommentBodySubmitState, hasBoundedCommentBodyText -} from '@/lib/comment-body-submit-state' +} from '@/lib/source-control/review/comments/comment-body-submit-state' import type { PRComment } from '../../../../../shared/github/comment-types' import { useTranslate } from '@/i18n/use-translate' import { projectViewErrorMessage } from '../project-view-error-copy' diff --git a/src/renderer/src/components/github/GitHubImageTextarea.tsx b/src/renderer/src/components/github/GitHubImageTextarea.tsx index 76b9b732819..ef54656ade2 100644 --- a/src/renderer/src/components/github/GitHubImageTextarea.tsx +++ b/src/renderer/src/components/github/GitHubImageTextarea.tsx @@ -1,5 +1,5 @@ import { useCallback, useRef, type ComponentProps, type Ref } from 'react' -import { ImeTextarea } from '@/lib/ime-text-field' +import { ImeTextarea } from '@/lib/text-input/ime/ime-text-field' import { useGitHubTextareaImages } from './use-github-textarea-images' import type { GitHubImageIntake } from './use-github-image-uploads' diff --git a/src/renderer/src/components/github/PRAssigneesPanel.tsx b/src/renderer/src/components/github/PRAssigneesPanel.tsx index 375b3496739..10835e82c6f 100644 --- a/src/renderer/src/components/github/PRAssigneesPanel.tsx +++ b/src/renderer/src/components/github/PRAssigneesPanel.tsx @@ -9,7 +9,7 @@ import { useAppStore } from '@/store' import { useImmediateMutation } from '@/hooks/useIssueMetadata' import { useRepoAssignees } from '@/hooks/useGitHubRepoMetadata' import { useRepoAssigneesBySlug } from '@/hooks/useGitHubSlugMetadata' -import { getGitHubRepoRoutingTarget } from '@/lib/github-source-runtime-context' +import { getGitHubRepoRoutingTarget } from '@/lib/work-items/github/github-source-runtime-context' import { parseOwnerRepoFromItemUrl, resolvePullRequestRepo, diff --git a/src/renderer/src/components/github/github-comment-image-upload-address.ts b/src/renderer/src/components/github/github-comment-image-upload-address.ts index 5ef4f06ce8b..d732709ac49 100644 --- a/src/renderer/src/components/github/github-comment-image-upload-address.ts +++ b/src/renderer/src/components/github/github-comment-image-upload-address.ts @@ -4,7 +4,7 @@ import type { GitHubOwnerRepo } from '../../../../shared/github/pull-request-typ import { getGitHubRuntimeRepoId, getGitHubSourceRuntimeHost -} from '@/lib/github-source-runtime-context' +} from '@/lib/work-items/github/github-source-runtime-context' export function githubCommentImageUploadAddress(args: { repoId?: string | null diff --git a/src/renderer/src/components/github/github-image-upload.ts b/src/renderer/src/components/github/github-image-upload.ts index ef7234836ad..dafef68f422 100644 --- a/src/renderer/src/components/github/github-image-upload.ts +++ b/src/renderer/src/components/github/github-image-upload.ts @@ -10,7 +10,7 @@ import { callRuntimeRpc } from '@/runtime/rpc/calls/runtime-rpc-call' import { getRuntimeEnvironmentRevision } from '@/runtime/rpc/environment-revisions/runtime-environment-revision' import { isWebClientLocation } from '@/lib/web-client-location' import { readStoredWebRuntimeEnvironment } from '@/web/web-runtime-environment' -import { blobToBase64 } from '@/lib/image-blob-png' +import { blobToBase64 } from '@/lib/clipboard/images/image-blob-png' export type GitHubImageUploadAddress = { runtime: RuntimeClientTarget diff --git a/src/renderer/src/components/github/github-work-item-comment-mutations.ts b/src/renderer/src/components/github/github-work-item-comment-mutations.ts index 539a46f97bb..0fc458701cf 100644 --- a/src/renderer/src/components/github/github-work-item-comment-mutations.ts +++ b/src/renderer/src/components/github/github-work-item-comment-mutations.ts @@ -9,7 +9,7 @@ import { emitGitHubWorkItemDetailsCacheMutation } from '@/lib/work-items/github/ import { getGitHubRuntimeRepoId, getGitHubSourceRuntimeHost -} from '@/lib/github-source-runtime-context' +} from '@/lib/work-items/github/github-source-runtime-context' import type { TaskSourceContext } from '../../../../shared/work-items/task-source-context' import type { GitHubOwnerRepo } from '../../../../shared/github/pull-request-types' diff --git a/src/renderer/src/components/github/github-work-item-edit-mutations.ts b/src/renderer/src/components/github/github-work-item-edit-mutations.ts index 7fd728fad03..91ddcd150b4 100644 --- a/src/renderer/src/components/github/github-work-item-edit-mutations.ts +++ b/src/renderer/src/components/github/github-work-item-edit-mutations.ts @@ -8,8 +8,8 @@ import { getGitHubRepoRoutingTarget, getGitHubRuntimeRepoId, getGitHubSourceRuntimeHost -} from '@/lib/github-source-runtime-context' -import { taskSourceRuntimeTarget } from '@/lib/task-source-runtime-target' +} from '@/lib/work-items/github/github-source-runtime-context' +import { taskSourceRuntimeTarget } from '@/lib/work-items/task-source-runtime-target' import { resolvePullRequestRepo, type GitHubWorkItemProjectOrigin @@ -19,7 +19,7 @@ import { githubProjectHost } from '../../../../shared/github/project-identity' import type { TaskSourceContext } from '../../../../shared/work-items/task-source-context' import type { GitHubOwnerRepo } from '../../../../shared/github/pull-request-types' import type { GitHubWorkItem } from '../../../../shared/github/work-item-types' -import { codeHostErrorText } from '@/lib/code-host-error-copy' +import { codeHostErrorText } from '@/lib/source-control/remote/code-host-error-copy' export async function runIssueUpdate(args: { repoPath: string | null diff --git a/src/renderer/src/components/gitlab-item-dialog/gitlab-files-tab.tsx b/src/renderer/src/components/gitlab-item-dialog/gitlab-files-tab.tsx index 4e6a073a202..4bf55161799 100644 --- a/src/renderer/src/components/gitlab-item-dialog/gitlab-files-tab.tsx +++ b/src/renderer/src/components/gitlab-item-dialog/gitlab-files-tab.tsx @@ -2,7 +2,7 @@ import { LoaderCircle, Send } from 'lucide-react' import { Button } from '@/components/ui/button' import { TabsContent } from '@/components/ui/tabs' import { useTranslate } from '@/i18n/use-translate' -import { hasBoundedCommentBodyText } from '@/lib/comment-body-submit-state' +import { hasBoundedCommentBodyText } from '@/lib/source-control/review/comments/comment-body-submit-state' import type { GitLabWorkItem } from '../../../../shared/gitlab-types' import type { GitLabItemDialogState } from './use-gitlab-item-dialog-state' import type { GitLabReviewActions } from './use-gitlab-review-actions' diff --git a/src/renderer/src/components/gitlab-item-dialog/gitlab-item-dialog-view.tsx b/src/renderer/src/components/gitlab-item-dialog/gitlab-item-dialog-view.tsx index 5e697a39dbf..979650d08be 100644 --- a/src/renderer/src/components/gitlab-item-dialog/gitlab-item-dialog-view.tsx +++ b/src/renderer/src/components/gitlab-item-dialog/gitlab-item-dialog-view.tsx @@ -1,4 +1,4 @@ -import { ImeTextarea } from '@/lib/ime-text-field' +import { ImeTextarea } from '@/lib/text-input/ime/ime-text-field' import { CircleDot, ExternalLink, GitMerge, LoaderCircle, RefreshCw, Send, X } from 'lucide-react' import * as VisuallyHidden from 'radix-ui/visually-hidden' import { Button } from '@/components/ui/button' @@ -11,7 +11,7 @@ import { } from '@/components/ui/sheet' import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs' import { useTranslate } from '@/i18n/use-translate' -import { hasBoundedCommentBodyText } from '@/lib/comment-body-submit-state' +import { hasBoundedCommentBodyText } from '@/lib/source-control/review/comments/comment-body-submit-state' import { isScreenSubmitShortcut } from '@/lib/screen-submit-shortcut' import type { GitLabWorkItem } from '../../../../shared/gitlab-types' import { normalizeGitLabLabels, StateBadge } from '../gitlab-item-dialog-parts' diff --git a/src/renderer/src/components/gitlab-item-dialog/use-gitlab-details-editing.ts b/src/renderer/src/components/gitlab-item-dialog/use-gitlab-details-editing.ts index bad709e7959..1c0c77dfa85 100644 --- a/src/renderer/src/components/gitlab-item-dialog/use-gitlab-details-editing.ts +++ b/src/renderer/src/components/gitlab-item-dialog/use-gitlab-details-editing.ts @@ -12,7 +12,7 @@ import { } from '../gitlab-item-dialog-parts' import type { GitLabDialogRepoSelector } from './gitlab-item-dialog-types' import type { GitLabItemDialogState } from './use-gitlab-item-dialog-state' -import { codeHostErrorText } from '@/lib/code-host-error-copy' +import { codeHostErrorText } from '@/lib/source-control/remote/code-host-error-copy' export function useGitLabDetailsEditing( item: GitLabWorkItem | null, diff --git a/src/renderer/src/components/gitlab-item-dialog/use-gitlab-pipeline-actions.ts b/src/renderer/src/components/gitlab-item-dialog/use-gitlab-pipeline-actions.ts index d191255dd23..74399d9933f 100644 --- a/src/renderer/src/components/gitlab-item-dialog/use-gitlab-pipeline-actions.ts +++ b/src/renderer/src/components/gitlab-item-dialog/use-gitlab-pipeline-actions.ts @@ -6,7 +6,7 @@ import type { GitLabPipelineJob, GitLabWorkItem } from '../../../../shared/gitla import { showGitLabMutationError } from '../gitlab-item-dialog-parts' import type { GitLabDialogRepoSelector } from './gitlab-item-dialog-types' import type { GitLabItemDialogState } from './use-gitlab-item-dialog-state' -import { codeHostErrorText } from '@/lib/code-host-error-copy' +import { codeHostErrorText } from '@/lib/source-control/remote/code-host-error-copy' export function useGitLabPipelineActions( item: GitLabWorkItem | null, diff --git a/src/renderer/src/components/gitlab-item-dialog/use-gitlab-primary-actions.ts b/src/renderer/src/components/gitlab-item-dialog/use-gitlab-primary-actions.ts index 12f7951d25c..b59f1288ddc 100644 --- a/src/renderer/src/components/gitlab-item-dialog/use-gitlab-primary-actions.ts +++ b/src/renderer/src/components/gitlab-item-dialog/use-gitlab-primary-actions.ts @@ -1,14 +1,14 @@ import { gitLabApiFor } from '@/runtime/repository-gitlab/gitlab-owner-api' import { useCallback } from 'react' import { toast } from 'sonner' -import { getCommentBodySubmitState } from '@/lib/comment-body-submit-state' +import { getCommentBodySubmitState } from '@/lib/source-control/review/comments/comment-body-submit-state' import { translate } from '@/i18n/i18n' import { useAppStore } from '@/store' import type { GitLabWorkItem } from '../../../../shared/gitlab-types' import { showGitLabMutationError } from '../gitlab-item-dialog-parts' import type { GitLabDialogRepoSelector } from './gitlab-item-dialog-types' import type { GitLabItemDialogState } from './use-gitlab-item-dialog-state' -import { codeHostErrorText } from '@/lib/code-host-error-copy' +import { codeHostErrorText } from '@/lib/source-control/remote/code-host-error-copy' export function useGitLabPrimaryActions( item: GitLabWorkItem | null, diff --git a/src/renderer/src/components/gitlab-item-dialog/use-gitlab-review-actions.ts b/src/renderer/src/components/gitlab-item-dialog/use-gitlab-review-actions.ts index 74a6b6fde3b..3cf5276d92b 100644 --- a/src/renderer/src/components/gitlab-item-dialog/use-gitlab-review-actions.ts +++ b/src/renderer/src/components/gitlab-item-dialog/use-gitlab-review-actions.ts @@ -1,14 +1,14 @@ import { gitLabApiFor } from '@/runtime/repository-gitlab/gitlab-owner-api' import { useCallback } from 'react' import { toast } from 'sonner' -import { getCommentBodySubmitState } from '@/lib/comment-body-submit-state' +import { getCommentBodySubmitState } from '@/lib/source-control/review/comments/comment-body-submit-state' import { translate } from '@/i18n/i18n' import { useAppStore } from '@/store' import type { GitLabAssignableUser, GitLabWorkItem } from '../../../../shared/gitlab-types' import { dedupeGitLabUsers, showGitLabMutationError } from '../gitlab-item-dialog-parts' import type { GitLabDialogRepoSelector } from './gitlab-item-dialog-types' import type { GitLabItemDialogState } from './use-gitlab-item-dialog-state' -import { codeHostErrorText } from '@/lib/code-host-error-copy' +import { codeHostErrorText } from '@/lib/source-control/remote/code-host-error-copy' export function useGitLabReviewActions( item: GitLabWorkItem | null, diff --git a/src/renderer/src/components/integrations/source-control/bitbucket-credentials-dialog.tsx b/src/renderer/src/components/integrations/source-control/bitbucket-credentials-dialog.tsx index 83b5cee741f..504d987419b 100644 --- a/src/renderer/src/components/integrations/source-control/bitbucket-credentials-dialog.tsx +++ b/src/renderer/src/components/integrations/source-control/bitbucket-credentials-dialog.tsx @@ -18,7 +18,7 @@ import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group' import { cn } from '@/lib/utils' import { hasRemoteProviderRuntime } from '@/runtime/hosts/observations/provider-runtime-context' import { preventOutsideDismissWhenDirty } from '@/lib/outside-dismiss-guard' -import { bitbucketConnectErrorText } from '@/lib/bitbucket-connect-error-copy' +import { bitbucketConnectErrorText } from '@/lib/source-control/remote/bitbucket-connect-error-copy' import { useTranslate } from '@/i18n/use-translate' import { BitbucketApiTokenScopes } from './bitbucket-api-token-scopes' diff --git a/src/renderer/src/components/integrations/task-trackers/jira-integration-card.tsx b/src/renderer/src/components/integrations/task-trackers/jira-integration-card.tsx index 30658c5c339..d67934da8c6 100644 --- a/src/renderer/src/components/integrations/task-trackers/jira-integration-card.tsx +++ b/src/renderer/src/components/integrations/task-trackers/jira-integration-card.tsx @@ -1,4 +1,4 @@ -import { taskProviderErrorMessage } from '@/lib/task-provider-error-copy' +import { taskProviderErrorMessage } from '@/lib/work-items/task-provider-error-copy' import { UnsealedCredentialNotice } from '../../provider-accounts/UnsealedCredentialNotice' import { useState } from 'react' import { AlertCircle, CheckCircle2, LoaderCircle, Unlink } from 'lucide-react' diff --git a/src/renderer/src/components/integrations/task-trackers/linear-integration-card.tsx b/src/renderer/src/components/integrations/task-trackers/linear-integration-card.tsx index 41c301b5f4f..62e0a092090 100644 --- a/src/renderer/src/components/integrations/task-trackers/linear-integration-card.tsx +++ b/src/renderer/src/components/integrations/task-trackers/linear-integration-card.tsx @@ -1,4 +1,4 @@ -import { taskProviderErrorMessage } from '@/lib/task-provider-error-copy' +import { taskProviderErrorMessage } from '@/lib/work-items/task-provider-error-copy' import { UnsealedCredentialNotice } from '../../provider-accounts/UnsealedCredentialNotice' import { useState } from 'react' import { AlertCircle, CheckCircle2, LoaderCircle, Unlink } from 'lucide-react' diff --git a/src/renderer/src/components/jira-connect-dialog.tsx b/src/renderer/src/components/jira-connect-dialog.tsx index 808fd39c519..3ae33349c72 100644 --- a/src/renderer/src/components/jira-connect-dialog.tsx +++ b/src/renderer/src/components/jira-connect-dialog.tsx @@ -1,5 +1,8 @@ import { jiraCredentialStorageCopy } from './jira-credential-storage-copy' -import { taskProviderErrorMessage, taskProviderErrorText } from '@/lib/task-provider-error-copy' +import { + taskProviderErrorMessage, + taskProviderErrorText +} from '@/lib/work-items/task-provider-error-copy' import { useId, useState } from 'react' import { LoaderCircle, Lock } from 'lucide-react' import { useAppStore } from '@/store' diff --git a/src/renderer/src/components/jira-issue-mutation-queue.ts b/src/renderer/src/components/jira-issue-mutation-queue.ts index a2da7cf0264..de77986a4b9 100644 --- a/src/renderer/src/components/jira-issue-mutation-queue.ts +++ b/src/renderer/src/components/jira-issue-mutation-queue.ts @@ -1,4 +1,4 @@ -import { taskProviderErrorMessage } from '@/lib/task-provider-error-copy' +import { taskProviderErrorMessage } from '@/lib/work-items/task-provider-error-copy' import type { JiraIssue, JiraMutationResult } from '../../../shared/jira/jira-types' import type { GlobalSettings } from '../../../shared/global-settings-types' import type { TaskSourceContext } from '../../../shared/work-items/task-source-context' diff --git a/src/renderer/src/components/jira-issue-transition-control.tsx b/src/renderer/src/components/jira-issue-transition-control.tsx index c43bfcfb515..517a138891e 100644 --- a/src/renderer/src/components/jira-issue-transition-control.tsx +++ b/src/renderer/src/components/jira-issue-transition-control.tsx @@ -1,4 +1,7 @@ -import { taskProviderErrorMessage, taskProviderErrorText } from '@/lib/task-provider-error-copy' +import { + taskProviderErrorMessage, + taskProviderErrorText +} from '@/lib/work-items/task-provider-error-copy' import { useState } from 'react' import { ChevronDown, CircleAlert, ExternalLink, Loader2 } from 'lucide-react' import { Button } from '@/components/ui/button' diff --git a/src/renderer/src/components/jira-issue-workspace-actions.ts b/src/renderer/src/components/jira-issue-workspace-actions.ts index dcf574e9139..58187c693ce 100644 --- a/src/renderer/src/components/jira-issue-workspace-actions.ts +++ b/src/renderer/src/components/jira-issue-workspace-actions.ts @@ -10,7 +10,7 @@ import type { } from '../../../shared/jira/jira-types' import { applyJiraIssueKeyCasing } from '../../../shared/workspace-name' import type { WorkspaceSeedNameOptions } from '../../../shared/workspace-name' -import { getJiraIssueKeyCasingSeedOptions } from '@/lib/jira-issue-key-casing' +import { getJiraIssueKeyCasingSeedOptions } from '@/lib/work-items/jira/jira-issue-key-casing' import { buildJiraIssuePrompt } from './jira-prompt-templates' import { issueCopiedMessage, diff --git a/src/renderer/src/components/jira-transition-fields-form.tsx b/src/renderer/src/components/jira-transition-fields-form.tsx index 1cbef07b564..d3b733df0bd 100644 --- a/src/renderer/src/components/jira-transition-fields-form.tsx +++ b/src/renderer/src/components/jira-transition-fields-form.tsx @@ -10,7 +10,7 @@ import { } from '@/components/ui/select' import { Textarea } from '@/components/ui/textarea' import { useTranslate } from '@/i18n/use-translate' -import { getCommentBodySubmitState } from '@/lib/comment-body-submit-state' +import { getCommentBodySubmitState } from '@/lib/source-control/review/comments/comment-body-submit-state' import { buildResolutionFieldValue, classifyTransitionRequirements, diff --git a/src/renderer/src/components/linear-api-key-dialog.tsx b/src/renderer/src/components/linear-api-key-dialog.tsx index ef8c99daa83..655243e224b 100644 --- a/src/renderer/src/components/linear-api-key-dialog.tsx +++ b/src/renderer/src/components/linear-api-key-dialog.tsx @@ -1,4 +1,7 @@ -import { taskProviderErrorMessage, taskProviderErrorText } from '@/lib/task-provider-error-copy' +import { + taskProviderErrorMessage, + taskProviderErrorText +} from '@/lib/work-items/task-provider-error-copy' import { useId, useMemo, useState } from 'react' import { ExternalLink, LoaderCircle, Lock } from 'lucide-react' import type { LinearWorkspace } from '../../../shared/linear/workspace-types' diff --git a/src/renderer/src/components/linear-issue-project-selector.tsx b/src/renderer/src/components/linear-issue-project-selector.tsx index e84cad7264c..5067124df84 100644 --- a/src/renderer/src/components/linear-issue-project-selector.tsx +++ b/src/renderer/src/components/linear-issue-project-selector.tsx @@ -1,6 +1,9 @@ import { getLinearSourceScope } from '@/store/slices/linear/linear-slice-scope' import { confirmLinearIssueWrite } from '@/store/slices/linear/linear-issue-publication' -import { taskProviderErrorMessage, taskProviderErrorText } from '@/lib/task-provider-error-copy' +import { + taskProviderErrorMessage, + taskProviderErrorText +} from '@/lib/work-items/task-provider-error-copy' import React, { useCallback, useEffect, useRef, useState } from 'react' import { Check, ChevronDown, FolderKanban, LoaderCircle } from 'lucide-react' import { toast } from 'sonner' diff --git a/src/renderer/src/components/linear-issue-sub-issues.tsx b/src/renderer/src/components/linear-issue-sub-issues.tsx index 66b421c8d86..94c6fefe105 100644 --- a/src/renderer/src/components/linear-issue-sub-issues.tsx +++ b/src/renderer/src/components/linear-issue-sub-issues.tsx @@ -1,6 +1,9 @@ import { observeLinearIssueRead } from '@/store/slices/linear/linear-issue-publication' -import { taskProviderErrorMessage, taskProviderErrorText } from '@/lib/task-provider-error-copy' -import { ImeInput } from '@/lib/ime-text-field' +import { + taskProviderErrorMessage, + taskProviderErrorText +} from '@/lib/work-items/task-provider-error-copy' +import { ImeInput } from '@/lib/text-input/ime/ime-text-field' import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { ArrowRight, LoaderCircle, Plus } from 'lucide-react' import { toast } from 'sonner' diff --git a/src/renderer/src/components/linear-issue-workspace-detail-state.ts b/src/renderer/src/components/linear-issue-workspace-detail-state.ts index da7cf56d45e..b63e4d3335d 100644 --- a/src/renderer/src/components/linear-issue-workspace-detail-state.ts +++ b/src/renderer/src/components/linear-issue-workspace-detail-state.ts @@ -3,7 +3,7 @@ import { getLinearSourceScope } from '@/store/slices/linear/linear-slice-scope' import { observeLinearIssueRead } from '@/store/slices/linear/linear-issue-publication' import { useLinearIssuePublication } from '@/hooks/useLinearIssuePublication' import { translate } from '@/i18n/i18n' -import { taskProviderErrorText } from '@/lib/task-provider-error-copy' +import { taskProviderErrorText } from '@/lib/work-items/task-provider-error-copy' import { useCallback, useEffect, useRef, useState } from 'react' import { createNonSecureContextUuid as createBrowserUuid } from '../../../shared/non-secure-context-uuid' diff --git a/src/renderer/src/components/linear-issue-workspace-sidebar.tsx b/src/renderer/src/components/linear-issue-workspace-sidebar.tsx index 5eda5753cb9..3e4131b889d 100644 --- a/src/renderer/src/components/linear-issue-workspace-sidebar.tsx +++ b/src/renderer/src/components/linear-issue-workspace-sidebar.tsx @@ -4,7 +4,7 @@ import { ChevronDown, Clipboard, FolderOpen, GitBranch } from 'lucide-react' import { LinearIssueEditSection, type LinearEditState } from '@/components/LinearItemDrawer' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { buildLinearIssueContextSnapshot } from '@/lib/work-items/linear/linear-issue-context-snapshot' -import { buildContainedLinkedContextBlock } from '@/lib/linked-work-item-context' +import { buildContainedLinkedContextBlock } from '@/lib/work-items/linked-work-item-context' import { useTranslate } from '@/i18n/use-translate' import { buildLinearIssueBranchName } from '@/components/linear-issue-workspace-text' import type { LinearComment, LinearIssue } from '../../../shared/linear/issue-types' diff --git a/src/renderer/src/components/linear-item-drawer-comment-footer.tsx b/src/renderer/src/components/linear-item-drawer-comment-footer.tsx index 17c2bb85662..df0c7ab31e2 100644 --- a/src/renderer/src/components/linear-item-drawer-comment-footer.tsx +++ b/src/renderer/src/components/linear-item-drawer-comment-footer.tsx @@ -1,5 +1,5 @@ -import { taskProviderErrorText } from '@/lib/task-provider-error-copy' -import { ImeTextarea } from '@/lib/ime-text-field' +import { taskProviderErrorText } from '@/lib/work-items/task-provider-error-copy' +import { ImeTextarea } from '@/lib/text-input/ime/ime-text-field' import React, { useCallback, useRef, useState } from 'react' import { LoaderCircle, Send } from 'lucide-react' import { toast } from 'sonner' @@ -8,7 +8,7 @@ import { Button } from '@/components/ui/button' import { getCommentBodySubmitState, hasBoundedCommentBodyText -} from '@/lib/comment-body-submit-state' +} from '@/lib/source-control/review/comments/comment-body-submit-state' import { getScreenSubmitShortcutLabel, isScreenSubmitShortcut } from '@/lib/screen-submit-shortcut' import { createNonSecureContextUuid as createBrowserUuid } from '../../../shared/non-secure-context-uuid' import { useAppStore } from '@/store' diff --git a/src/renderer/src/components/linear-item-drawer-edit-controller.tsx b/src/renderer/src/components/linear-item-drawer-edit-controller.tsx index 2b748d5d479..73aa076673d 100644 --- a/src/renderer/src/components/linear-item-drawer-edit-controller.tsx +++ b/src/renderer/src/components/linear-item-drawer-edit-controller.tsx @@ -1,6 +1,9 @@ import { getLinearSourceScope } from '@/store/slices/linear/linear-slice-scope' import { writeLinearIssue } from '@/lib/work-items/linear/linear-issue-write' -import { taskProviderErrorMessage, taskProviderErrorText } from '@/lib/task-provider-error-copy' +import { + taskProviderErrorMessage, + taskProviderErrorText +} from '@/lib/work-items/task-provider-error-copy' import { useCallback, useState } from 'react' import { toast } from 'sonner' diff --git a/src/renderer/src/components/linear-project-view-surfaces.tsx b/src/renderer/src/components/linear-project-view-surfaces.tsx index b437e0d8562..00ecc5477bb 100644 --- a/src/renderer/src/components/linear-project-view-surfaces.tsx +++ b/src/renderer/src/components/linear-project-view-surfaces.tsx @@ -1,5 +1,5 @@ -import { linearIssueReadError } from '@/lib/linear-read-error' -import { taskProviderErrorText } from '@/lib/task-provider-error-copy' +import { linearIssueReadError } from '@/lib/work-items/linear/linear-read-error' +import { taskProviderErrorText } from '@/lib/work-items/task-provider-error-copy' import React from 'react' import { ArrowLeft, ArrowRight, ExternalLink, Layers3, LoaderCircle, RefreshCw } from 'lucide-react' diff --git a/src/renderer/src/components/link-actions/LinkActionPopover.tsx b/src/renderer/src/components/link-actions/LinkActionPopover.tsx index 5c07fc87eea..1df465ac3fd 100644 --- a/src/renderer/src/components/link-actions/LinkActionPopover.tsx +++ b/src/renderer/src/components/link-actions/LinkActionPopover.tsx @@ -5,7 +5,7 @@ import { ShortcutKeyCombo } from '@/components/ShortcutKeyCombo' import { Button } from '@/components/ui/button' import { Popover, PopoverAnchor, PopoverContent } from '@/components/ui/popover' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' -import { useClipboardTextCopyFeedback } from '@/hooks/use-clipboard-text-copy-feedback' +import { useClipboardTextCopyFeedback } from '@/hooks/clipboard/use-clipboard-text-copy-feedback' import { useTranslate } from '@/i18n/use-translate' import { BROWSER_TERMINAL_LINK_ACTIONS_SETTINGS_TARGET_ID } from '@/lib/settings-navigation-types' import { useAppStore } from '@/store' diff --git a/src/renderer/src/components/NativeChatResumeOnRestartModal.tsx b/src/renderer/src/components/native-chat-recovery/NativeChatResumeOnRestartModal.tsx similarity index 93% rename from src/renderer/src/components/NativeChatResumeOnRestartModal.tsx rename to src/renderer/src/components/native-chat-recovery/NativeChatResumeOnRestartModal.tsx index 2ee9c4ee7cd..569fbb2d98e 100644 --- a/src/renderer/src/components/NativeChatResumeOnRestartModal.tsx +++ b/src/renderer/src/components/native-chat-recovery/NativeChatResumeOnRestartModal.tsx @@ -1,35 +1,35 @@ -import { NativeChatResumeDialogHeader } from './NativeChatResumeDialogHeader' +import { NativeChatResumeDialogHeader } from './dialogs/NativeChatResumeDialogHeader' import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react' -import { Button } from './ui/button' -import { Checkbox } from './ui/checkbox' -import { Dialog, DialogContent, DialogFooter } from './ui/dialog' -import { useAppStore } from '../store' +import { Button } from '../ui/button' +import { Checkbox } from '../ui/checkbox' +import { Dialog, DialogContent, DialogFooter } from '../ui/dialog' +import { useAppStore } from '../../store/index' import { useTranslate, type Translate } from '@/i18n/use-translate' import { activateAiVaultStructuredSession } from '@/lib/activate-ai-vault-structured-session' -import { ResumeOnRestartGroups } from './NativeChatResumeOnRestartGroups' -import { ResumeTreeRow } from './NativeChatResumeTreeRow' +import { ResumeOnRestartGroups } from './tree/NativeChatResumeOnRestartGroups' +import { ResumeTreeRow } from './tree/NativeChatResumeTreeRow' import { resumeFailureGuidance, resumeFailureSelectable, type ResumeFailureAction -} from './native-chat-resume-failure-guidance' +} from './failures/native-chat-resume-failure-guidance' import { resumeSelectionState, toggleResumeSelection, type ResumeCandidate, type ResumeFailure -} from './native-chat-resume-on-restart-grouping' +} from './tree/native-chat-resume-on-restart-grouping' import { useNativeChatRestartOffer, useNativeChatRestartResuming, - useNativeChatRestartRun, - type NativeChatRestartStore -} from './native-chat-resume-on-restart-store' -import { useResumeRunPanel } from './NativeChatResumeRunPanel' -import { resumeRunFailures } from './native-chat-resume-run-view' -import { NativeChatResumeAutoNote } from './NativeChatResumeAutoNote' -import { NativeChatRemoteResumeActions } from './NativeChatRemoteResumeActions' -import { moveInTree } from './native-chat-resume-tree-keys' + useNativeChatRestartRun +} from './offers/native-chat-resume-on-restart-store' +import type { NativeChatRestartStore } from './offers/native-chat-restart-runtime-store' +import { useResumeRunPanel } from './runs/NativeChatResumeRunPanel' +import { resumeRunFailures } from './runs/native-chat-resume-run-view' +import { NativeChatResumeAutoNote } from './dialogs/NativeChatResumeAutoNote' +import { NativeChatRemoteResumeActions } from './dialogs/NativeChatRemoteResumeActions' +import { moveInTree } from './tree/native-chat-resume-tree-keys' /** Resume reattaches and sends a continuation, never the original user prompt; disclose this before sending. Automatic preference skips the prompt but uses the same host safety checks. * The store owns runs across dialog closes. Closing snoozes; Dismiss all deletes durable offers. Row Retry/Dismiss keep the dialog open, and settled runs cannot close a user-reopened dialog. Unretryable failures stay outside Resume. */ diff --git a/src/renderer/src/components/NativeChatResumeRuntimeModal.tsx b/src/renderer/src/components/native-chat-recovery/NativeChatResumeRuntimeModal.tsx similarity index 68% rename from src/renderer/src/components/NativeChatResumeRuntimeModal.tsx rename to src/renderer/src/components/native-chat-recovery/NativeChatResumeRuntimeModal.tsx index 6353ba2687f..de21e6fe269 100644 --- a/src/renderer/src/components/NativeChatResumeRuntimeModal.tsx +++ b/src/renderer/src/components/native-chat-recovery/NativeChatResumeRuntimeModal.tsx @@ -1,15 +1,13 @@ import { useSyncExternalStore } from 'react' -import { - localNativeChatRestartStore, - type NativeChatRestartStore -} from './native-chat-resume-on-restart-store' +import { localNativeChatRestartStore } from './offers/native-chat-resume-on-restart-store' +import type { NativeChatRestartStore } from './offers/native-chat-restart-runtime-store' import { NativeChatResumeOccurrenceModal } from './NativeChatResumeOnRestartModal' -import { useNativeChatRestartOfferEnabled } from './native-chat-restart-offer-gate' -import { useNativeChatRestartRuntimes } from './native-chat-restart-runtimes' -import { runtimeEnvironmentDisplayName } from '../../../shared/remote-runtime/host-catalog/orca-cloud-environment-name' +import { useLocalStructuredChatsInUse } from '../../runtime/native-chat/local-sessions/local-structured-chats' +import { useNativeChatRestartRuntimes } from './offers/native-chat-restart-runtimes' +import { runtimeEnvironmentDisplayName } from '../../../../shared/remote-runtime/host-catalog/orca-cloud-environment-name' export function NativeChatResumeOnRestartModal(): React.JSX.Element { - const enabled = useNativeChatRestartOfferEnabled() + const enabled = useLocalStructuredChatsInUse() const runtimes = useNativeChatRestartRuntimes() return ( <> diff --git a/src/renderer/src/components/NativeChatRemoteResumeActions.tsx b/src/renderer/src/components/native-chat-recovery/dialogs/NativeChatRemoteResumeActions.tsx similarity index 92% rename from src/renderer/src/components/NativeChatRemoteResumeActions.tsx rename to src/renderer/src/components/native-chat-recovery/dialogs/NativeChatRemoteResumeActions.tsx index f5b4b6b1963..6562bc8e3b9 100644 --- a/src/renderer/src/components/NativeChatRemoteResumeActions.tsx +++ b/src/renderer/src/components/native-chat-recovery/dialogs/NativeChatRemoteResumeActions.tsx @@ -1,6 +1,6 @@ -import { Button } from './ui/button' +import { Button } from '../../ui/button' import { useTranslate } from '@/i18n/use-translate' -import type { NativeChatRestartStore } from './native-chat-resume-on-restart-store' +import type { NativeChatRestartStore } from '../offers/native-chat-restart-runtime-store' export function NativeChatRemoteResumeActions({ store, diff --git a/src/renderer/src/components/NativeChatResumeAutoNote.tsx b/src/renderer/src/components/native-chat-recovery/dialogs/NativeChatResumeAutoNote.tsx similarity index 88% rename from src/renderer/src/components/NativeChatResumeAutoNote.tsx rename to src/renderer/src/components/native-chat-recovery/dialogs/NativeChatResumeAutoNote.tsx index ccbe1f20d24..3d0e6e77d32 100644 --- a/src/renderer/src/components/NativeChatResumeAutoNote.tsx +++ b/src/renderer/src/components/native-chat-recovery/dialogs/NativeChatResumeAutoNote.tsx @@ -1,7 +1,7 @@ import { useTranslate } from '@/i18n/use-translate' -import { useAppStore } from '../store' -import { Button } from './ui/button' -import { NATIVE_CHAT_RESUME_SETTING_SECTION_ID } from './settings/chat/native-chat-resume-setting-target' +import { useAppStore } from '../../../store/index' +import { Button } from '../../ui/button' +import { NATIVE_CHAT_RESUME_SETTING_SECTION_ID } from '../../settings/chat/native-chat-resume-setting-target' /** Explain automatic resume only for runs started by this launch’s saved preference, with its settings link. */ export function NativeChatResumeAutoNote({ diff --git a/src/renderer/src/components/NativeChatResumeDialogHeader.tsx b/src/renderer/src/components/native-chat-recovery/dialogs/NativeChatResumeDialogHeader.tsx similarity index 94% rename from src/renderer/src/components/NativeChatResumeDialogHeader.tsx rename to src/renderer/src/components/native-chat-recovery/dialogs/NativeChatResumeDialogHeader.tsx index 2e7045fb15b..8dabbb04f46 100644 --- a/src/renderer/src/components/NativeChatResumeDialogHeader.tsx +++ b/src/renderer/src/components/native-chat-recovery/dialogs/NativeChatResumeDialogHeader.tsx @@ -1,7 +1,7 @@ import { RotateCcw } from 'lucide-react' import { useTranslate } from '@/i18n/use-translate' -import { DialogHeader, DialogTitle, DialogDescription } from './ui/dialog' -import type { ResumeRunPanel } from './NativeChatResumeRunPanel' +import { DialogHeader, DialogTitle, DialogDescription } from '../../ui/dialog' +import type { ResumeRunPanel } from '../runs/NativeChatResumeRunPanel' export function NativeChatResumeDialogHeader({ runPanel, diff --git a/src/renderer/src/components/native-chat-resume-on-restart-dialog.ts b/src/renderer/src/components/native-chat-recovery/dialogs/native-chat-resume-on-restart-dialog.ts similarity index 100% rename from src/renderer/src/components/native-chat-resume-on-restart-dialog.ts rename to src/renderer/src/components/native-chat-recovery/dialogs/native-chat-resume-on-restart-dialog.ts diff --git a/src/renderer/src/components/NativeChatResumeFailureDetails.tsx b/src/renderer/src/components/native-chat-recovery/failures/NativeChatResumeFailureDetails.tsx similarity index 95% rename from src/renderer/src/components/NativeChatResumeFailureDetails.tsx rename to src/renderer/src/components/native-chat-recovery/failures/NativeChatResumeFailureDetails.tsx index 2aa5e92575a..3a53d0ce3c8 100644 --- a/src/renderer/src/components/NativeChatResumeFailureDetails.tsx +++ b/src/renderer/src/components/native-chat-recovery/failures/NativeChatResumeFailureDetails.tsx @@ -1,13 +1,13 @@ import { AlertCircle, Clock, X } from 'lucide-react' -import { Button } from './ui/button' -import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip' +import { Button } from '../../ui/button' +import { Tooltip, TooltipContent, TooltipTrigger } from '../../ui/tooltip' import { translate } from '@/i18n/i18n' import { useTranslate, type Translate } from '@/i18n/use-translate' import { resumeFailureGuidance, type ResumeFailureAction } from './native-chat-resume-failure-guidance' -import type { ResumeFailure } from './native-chat-resume-on-restart-grouping' +import type { ResumeFailure } from '../tree/native-chat-resume-on-restart-grouping' /** Keep unconfirmed outcomes distinct from refusals; offer Retry only where it can succeed. */ diff --git a/src/renderer/src/components/native-chat-resume-failure-guidance.test.ts b/src/renderer/src/components/native-chat-recovery/failures/native-chat-resume-failure-guidance.test.ts similarity index 50% rename from src/renderer/src/components/native-chat-resume-failure-guidance.test.ts rename to src/renderer/src/components/native-chat-recovery/failures/native-chat-resume-failure-guidance.test.ts index ebb9b42ce82..091dc578299 100644 --- a/src/renderer/src/components/native-chat-resume-failure-guidance.test.ts +++ b/src/renderer/src/components/native-chat-recovery/failures/native-chat-resume-failure-guidance.test.ts @@ -3,17 +3,6 @@ import { resumeFailureGuidance } from './native-chat-resume-failure-guidance' describe('resumeFailureGuidance', () => { // Provider refusals already live in the chat; respect the host’s non-retryable verdict. - it.each(['codex said no', 'agent_session_conflict'])( - 'offers no retry for %s once the host says a retry would not run', - (reason) => { - expect(resumeFailureGuidance({ outcome: 'refused', reason, retryable: false })).toMatchObject( - { primary: 'open', secondary: 'dismiss' } - ) - expect(resumeFailureGuidance({ outcome: 'refused', reason, retryable: true })).toMatchObject( - resumeFailureGuidance({ outcome: 'refused', reason }) - ) - } - ) it('never offers a retry for a delivery nobody could confirm', () => { expect( diff --git a/src/renderer/src/components/native-chat-resume-failure-guidance.ts b/src/renderer/src/components/native-chat-recovery/failures/native-chat-resume-failure-guidance.ts similarity index 97% rename from src/renderer/src/components/native-chat-resume-failure-guidance.ts rename to src/renderer/src/components/native-chat-recovery/failures/native-chat-resume-failure-guidance.ts index a51552c1ea9..b79d1360561 100644 --- a/src/renderer/src/components/native-chat-resume-failure-guidance.ts +++ b/src/renderer/src/components/native-chat-recovery/failures/native-chat-resume-failure-guidance.ts @@ -1,6 +1,6 @@ import { translate } from '@/i18n/i18n' import type { Translate } from '@/i18n/use-translate' -import type { ResumeFailure } from './native-chat-resume-on-restart-grouping' +import type { ResumeFailure } from '../tree/native-chat-resume-on-restart-grouping' /** Offer retry only for recoverable refusals; newer chat work or a standing provider refusal requires continuing in the chat. */ diff --git a/src/renderer/src/components/NativeChatRemoteResume.test.tsx b/src/renderer/src/components/native-chat-recovery/integration/NativeChatRemoteResume.test.tsx similarity index 57% rename from src/renderer/src/components/NativeChatRemoteResume.test.tsx rename to src/renderer/src/components/native-chat-recovery/integration/NativeChatRemoteResume.test.tsx index c189ce7e8a7..98a27c91f03 100644 --- a/src/renderer/src/components/NativeChatRemoteResume.test.tsx +++ b/src/renderer/src/components/native-chat-recovery/integration/NativeChatRemoteResume.test.tsx @@ -12,16 +12,16 @@ import { import { createNativeChatRestartStore, type NativeChatRestartStore -} from './native-chat-resume-on-restart-store' -import type { RuntimeEnvironmentStatus } from '../../../shared/remote-runtime/host-status/runtime-host-status' +} from '../offers/native-chat-restart-runtime-store' +import type { RuntimeEnvironmentStatus } from '../../../../../shared/remote-runtime/host-status/runtime-host-status' const rpc = vi.hoisted(() => vi.fn()) const stream: ResumeStatusStream = vi.hoisted(() => ({ emit: () => {}, snapshot: new Map() })) -vi.mock('../runtime/native-chat/requests/structured-agent-session-call', () => ({ +vi.mock('../../../runtime/native-chat/requests/structured-agent-session-call', () => ({ callStructuredAgentSession: rpc })) -vi.mock('../runtime/native-chat/requests/structured-agent-session-subscriptions', () => ({ +vi.mock('../../../runtime/native-chat/requests/structured-agent-session-subscriptions', () => ({ subscribeStructuredAgentSessionStatus: async ( _target: unknown, emit: ResumeStatusStream['emit'] @@ -80,42 +80,3 @@ it('snoozes server failures locally through reconnect and reopens for a new runt await act(async () => remote.observeRuntimeStatus(ready('second'))) expect(remote.getNativeChatResumeOnRestartDialogRequest()).toBe(true) }) - -it.each(['failure', 'success', 'history'])( - 'supersedes an older ordinary read through all awaits (%s)', - async (mode) => { - const remote = store() - rpc.mockResolvedValue({ sessions: offered }) - await act(async () => remote.observeRuntimeStatus(ready('r1'))) - const old = Promise.withResolvers() - const history = Promise.withResolvers() - if (mode === 'history') { - vi.stubGlobal('api', { - runtimeEnvironments: { desktopCreatedChatIds: () => history.promise } - }) - rpc.mockResolvedValueOnce({ sessions: [] }) - await act(async () => remote.refreshNativeChatRestartOffer()) - rpc.mockResolvedValue({ sessions: offered }) - } else { - rpc.mockReturnValueOnce(old.promise) - } - let read: Promise - await act(async () => { - read = remote.refreshNativeChatRestartOffer() - }) - rpc.mockResolvedValue({ sessions: mode === 'failure' ? offered : [] }) - remote.observeRuntimeStatus(undefined) - await act(async () => remote.observeRuntimeStatus(ready('r1'))) - await act(async () => { - if (mode === 'failure') { - old.reject(new Error('obsolete failure')) - } else { - old.resolve({ sessions: offered }) - } - history.resolve(['a']) - await read! - }) - expect(remote.getNativeChatRestartOffer().candidates).toHaveLength(mode === 'failure' ? 2 : 0) - expect(remote.getNativeChatResumeOnRestartDialogRequest()).toBe(mode === 'failure') - } -) diff --git a/src/renderer/src/components/native-chat-resume-bulk-progress.test.tsx b/src/renderer/src/components/native-chat-recovery/integration/native-chat-resume-bulk-progress.test.tsx similarity index 93% rename from src/renderer/src/components/native-chat-resume-bulk-progress.test.tsx rename to src/renderer/src/components/native-chat-recovery/integration/native-chat-resume-bulk-progress.test.tsx index a0dd4c3961f..aede3f88195 100644 --- a/src/renderer/src/components/native-chat-resume-bulk-progress.test.tsx +++ b/src/renderer/src/components/native-chat-recovery/integration/native-chat-resume-bulk-progress.test.tsx @@ -8,28 +8,28 @@ import { } from './native-chat-resume-modal.test-support' import { act } from 'react' import { expect, it, vi, type Mock } from 'vitest' -import { NativeChatResumeOnRestartModal } from './NativeChatResumeRuntimeModal' +import { NativeChatResumeOnRestartModal } from '../NativeChatResumeRuntimeModal' -import { NativeChatResumeStatusSegment } from './status-bar/NativeChatResumeStatusSegment' -import type { ResumeCandidate, ResumeFailure } from './native-chat-resume-on-restart-grouping' -import type { AgentSessionStatusSummary } from '../../../shared/agent-session/agent-session-wire' -import { requestNativeChatResumeOnRestartDialog } from './native-chat-resume-on-restart-dialog' +import { NativeChatResumeStatusSegment } from '../../status-bar/NativeChatResumeStatusSegment' +import type { ResumeCandidate, ResumeFailure } from '../tree/native-chat-resume-on-restart-grouping' +import type { AgentSessionStatusSummary } from '../../../../../shared/agent-session/agent-session-wire' +import { requestNativeChatResumeOnRestartDialog } from '../dialogs/native-chat-resume-on-restart-dialog' import { continueNativeChatRestartOffer, getNativeChatRestartOffer, refreshNativeChatRestartOffer -} from './native-chat-resume-on-restart-store' +} from '../offers/native-chat-resume-on-restart-store' const rpc: Mock = vi.hoisted(() => vi.fn()) const statusStream: ResumeStatusStream = vi.hoisted(() => ({ emit: (_event: Parameters[0]) => {}, snapshot: new Map() })) -vi.mock('../runtime/native-chat/requests/structured-agent-session-call', () => ({ +vi.mock('../../../runtime/native-chat/requests/structured-agent-session-call', () => ({ callStructuredAgentSession: rpc })) -vi.mock('../runtime/native-chat/requests/structured-agent-session-subscriptions', () => ({ +vi.mock('../../../runtime/native-chat/requests/structured-agent-session-subscriptions', () => ({ subscribeStructuredAgentSessionStatus: async ( _target: unknown, emit: (event: Parameters[0]) => void diff --git a/src/renderer/src/components/native-chat-resume-finished-run.test.tsx b/src/renderer/src/components/native-chat-recovery/integration/native-chat-resume-finished-run.test.tsx similarity index 70% rename from src/renderer/src/components/native-chat-resume-finished-run.test.tsx rename to src/renderer/src/components/native-chat-recovery/integration/native-chat-resume-finished-run.test.tsx index 7edb9efda76..8e66c16fecb 100644 --- a/src/renderer/src/components/native-chat-resume-finished-run.test.tsx +++ b/src/renderer/src/components/native-chat-recovery/integration/native-chat-resume-finished-run.test.tsx @@ -1,8 +1,8 @@ // @vitest-environment happy-dom import { act } from 'react' import { expect, it, vi, type Mock } from 'vitest' -import { NativeChatResumeOnRestartModal } from './NativeChatResumeRuntimeModal' -import { NativeChatResumeStatusSegment } from './status-bar/NativeChatResumeStatusSegment' +import { NativeChatResumeOnRestartModal } from '../NativeChatResumeRuntimeModal' +import { NativeChatResumeStatusSegment } from '../../status-bar/NativeChatResumeStatusSegment' import { createResumeModalFixture, failure, @@ -15,11 +15,11 @@ import { getNativeChatRestartRun, refreshNativeChatRestartOffer, releaseFinishedNativeChatRestartRun -} from './native-chat-resume-on-restart-store' +} from '../offers/native-chat-resume-on-restart-store' import { consumeNativeChatResumeOnRestartDialogRequest, requestNativeChatResumeOnRestartDialog -} from './native-chat-resume-on-restart-dialog' +} from '../dialogs/native-chat-resume-on-restart-dialog' import { chatBox, namedBox } from './native-chat-resume-on-restart-modal.test-support' @@ -28,11 +28,11 @@ const statusStream: ResumeStatusStream = vi.hoisted(() => ({ emit: (_event: Parameters[0]) => {}, snapshot: new Map() })) -vi.mock('../runtime/native-chat/requests/structured-agent-session-call', () => ({ +vi.mock('../../../runtime/native-chat/requests/structured-agent-session-call', () => ({ callStructuredAgentSession: rpc })) -vi.mock('../runtime/native-chat/requests/structured-agent-session-subscriptions', () => ({ +vi.mock('../../../runtime/native-chat/requests/structured-agent-session-subscriptions', () => ({ subscribeStructuredAgentSessionStatus: async ( _target: unknown, emit: ResumeStatusStream['emit'] @@ -46,7 +46,7 @@ vi.mock('sonner', async () => (await import('./native-chat-resume-sonner-mock.test-support')).resumeSonnerMock() ) -const { mount, button, fakeHost, runStatus, toasts } = createResumeModalFixture(rpc, statusStream) +const { mount, button, fakeHost, runStatus } = createResumeModalFixture(rpc, statusStream) const dialog = () => document.querySelector('[role="dialog"]')?.textContent async function mountSurfaces(): Promise { @@ -58,46 +58,6 @@ async function mountSurfaces(): Promise { ) } -it.each(['status entry', 'toast Show'] as const)( - 'opens every finished result from %s after Resume closed the dialog', - async (source) => { - fakeHost({}, (sessionId) => (sessionId === 'a' ? 'continued' : 'refused'), 'unknown') - await mountSurfaces() - await act(async () => button('Resume 2 chats').click()) - expect(dialog()).toBeUndefined() - await act(async () => - source === 'status entry' ? button('1 chat failed to resume').click() : lastToastShow()?.() - ) - expect(dialog()).toContain('Resumed 1 of 2 chats') - expect(dialog()).toContain('All2') - expect(dialog()).toContain('Need you1') - expect(runStatus('Prompt a')).toBe('Prompt a: Resumed') - expect(runStatus('Prompt b')).toBe('Prompt b: Couldn’t resume') - expect(button('Retry')).toBeTruthy() - expect(button('Open chat')).toBeTruthy() - expect(button('Dismiss "Prompt b" in workspace')).toBeTruthy() - expect(toasts()).toEqual([['1 chat couldn’t be resumed', 'Resumed 1 chat']]) - } -) - -it.each(['status entry', 'toast Show'] as const)( - 'returns to the host failure offer from %s after the finished view is closed', - async (source) => { - fakeHost({}, (sessionId) => (sessionId === 'a' ? 'continued' : 'refused')) - await mountSurfaces() - await act(async () => button('Resume 2 chats').click()) - await act(async () => button('1 chat failed to resume').click()) - expect(dialog()).toContain('Resumed 1 of 2 chats') - await act(async () => button('Close').click()) - await act(async () => - source === 'status entry' ? button('1 chat failed to resume').click() : lastToastShow()?.() - ) - expect(dialog()).toContain('Resume interrupted chats?') - expect(dialog()).toContain('Prompt b') - expect(dialog()).not.toContain('Prompt a') - } -) - it('keeps the unseen summary when a view was opened and closed only during the run', async () => { const host = fakeHost({}, (sessionId) => (sessionId === 'a' ? 'continued' : 'unknown')) host.hold('a', 'b') @@ -113,27 +73,6 @@ it('keeps the unseen summary when a view was opened and closed only during the r expect(runStatus('Prompt b')).toBe('Prompt b: Couldn’t confirm the chat was resumed') }) -it.each(['status entry', 'toast Show'] as const)( - 'keeps a resumed entry until the all-success summary is seen from %s', - async (source) => { - fakeHost() - await mountSurfaces() - await act(async () => button('Resume 2 chats').click()) - expect(dialog()).toBeUndefined() - expect(button('2 chats resumed. Click to see them.').textContent).toBe('2 chats resumed') - await act(async () => - source === 'status entry' ? button('2 chats resumed').click() : lastToastShow()?.() - ) - expect(dialog()).toContain('Resumed 2 of 2 chats') - expect(runStatus('Prompt a')).toBe('Prompt a: Resumed') - expect(runStatus('Prompt b')).toBe('Prompt b: Resumed') - await act(async () => button('Done').click()) - expect(document.body.textContent).not.toContain('chat') - await act(async () => lastToastShow()?.()) - expect(dialog()).toBeUndefined() - } -) - it('adds no resumed entry for a run watched to the end in the dialog', async () => { const host = fakeHost() host.hold('a', 'b') diff --git a/src/renderer/src/components/native-chat-resume-modal.test-support.tsx b/src/renderer/src/components/native-chat-recovery/integration/native-chat-resume-modal.test-support.tsx similarity index 93% rename from src/renderer/src/components/native-chat-resume-modal.test-support.tsx rename to src/renderer/src/components/native-chat-recovery/integration/native-chat-resume-modal.test-support.tsx index 017d2793a2d..583cc5cd4d9 100644 --- a/src/renderer/src/components/native-chat-resume-modal.test-support.tsx +++ b/src/renderer/src/components/native-chat-recovery/integration/native-chat-resume-modal.test-support.tsx @@ -1,22 +1,22 @@ -import { EVERY_AGENT_CHAT } from '../../../shared/native-chat/every-agent-chat.test-fixture' +import { EVERY_AGENT_CHAT } from '../../../../../shared/native-chat/every-agent-chat.test-fixture' import { act } from 'react' import { toast } from 'sonner' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, vi, type Mock } from 'vitest' -import { useAppStore } from '../store' -import { getDefaultSettings } from '../../../shared/constants' +import { useAppStore } from '../../../store/index' +import { getDefaultSettings } from '../../../../../shared/constants' import type { AgentSessionStatusEvent, AgentSessionStatusSummary -} from '../../../shared/agent-session/agent-session-wire' +} from '../../../../../shared/agent-session/agent-session-wire' import { resetStructuredAgentSessionStatusFeedsForTests } from '@/runtime/native-chat/status/structured-agent-session-status-feed' -import { TooltipProvider } from './ui/tooltip' -import type { ResumeCandidate, ResumeFailure } from './native-chat-resume-on-restart-grouping' -import { consumeNativeChatResumeOnRestartDialogRequest } from './native-chat-resume-on-restart-dialog' +import { TooltipProvider } from '../../ui/tooltip' +import type { ResumeCandidate, ResumeFailure } from '../tree/native-chat-resume-on-restart-grouping' +import { consumeNativeChatResumeOnRestartDialogRequest } from '../dialogs/native-chat-resume-on-restart-dialog' import { _resetNativeChatRestartOffer, getNativeChatRestartOffer -} from './native-chat-resume-on-restart-store' +} from '../offers/native-chat-resume-on-restart-store' export const offered: ResumeCandidate[] = ['a', 'b'].map((sessionId) => ({ sessionId, diff --git a/src/renderer/src/components/native-chat-resume-on-restart-modal.test-support.ts b/src/renderer/src/components/native-chat-recovery/integration/native-chat-resume-on-restart-modal.test-support.ts similarity index 95% rename from src/renderer/src/components/native-chat-resume-on-restart-modal.test-support.ts rename to src/renderer/src/components/native-chat-recovery/integration/native-chat-resume-on-restart-modal.test-support.ts index b95ce31631a..ceda69d133b 100644 --- a/src/renderer/src/components/native-chat-resume-on-restart-modal.test-support.ts +++ b/src/renderer/src/components/native-chat-recovery/integration/native-chat-resume-on-restart-modal.test-support.ts @@ -1,4 +1,4 @@ -import type { ResumeCandidate } from './native-chat-resume-on-restart-grouping' +import type { ResumeCandidate } from '../tree/native-chat-resume-on-restart-grouping' export const offered: ResumeCandidate[] = ['a', 'b'].map((sessionId) => ({ sessionId, diff --git a/src/renderer/src/components/native-chat-resume-result-view.test.tsx b/src/renderer/src/components/native-chat-recovery/integration/native-chat-resume-result-view.test.tsx similarity index 91% rename from src/renderer/src/components/native-chat-resume-result-view.test.tsx rename to src/renderer/src/components/native-chat-recovery/integration/native-chat-resume-result-view.test.tsx index b2d2773611b..80bd49d2041 100644 --- a/src/renderer/src/components/native-chat-resume-result-view.test.tsx +++ b/src/renderer/src/components/native-chat-recovery/integration/native-chat-resume-result-view.test.tsx @@ -1,8 +1,8 @@ // @vitest-environment happy-dom import { act } from 'react' import { expect, it, vi, type Mock } from 'vitest' -import { NativeChatResumeOnRestartModal } from './NativeChatResumeRuntimeModal' -import { NativeChatResumeStatusSegment } from './status-bar/NativeChatResumeStatusSegment' +import { NativeChatResumeOnRestartModal } from '../NativeChatResumeRuntimeModal' +import { NativeChatResumeStatusSegment } from '../../status-bar/NativeChatResumeStatusSegment' import { createResumeModalFixture, type RestartRpc, @@ -11,18 +11,18 @@ import { import { lastToastShow } from './native-chat-resume-toast.test-support' import { chatBox, dontAskAgain } from './native-chat-resume-on-restart-modal.test-support' -import { useAppStore } from '../store' +import { useAppStore } from '../../../store/index' const rpc: Mock = vi.hoisted(() => vi.fn()) const statusStream: ResumeStatusStream = vi.hoisted(() => ({ emit: (_event: Parameters[0]) => {}, snapshot: new Map() })) -vi.mock('../runtime/native-chat/requests/structured-agent-session-call', () => ({ +vi.mock('../../../runtime/native-chat/requests/structured-agent-session-call', () => ({ callStructuredAgentSession: rpc })) -vi.mock('../runtime/native-chat/requests/structured-agent-session-subscriptions', () => ({ +vi.mock('../../../runtime/native-chat/requests/structured-agent-session-subscriptions', () => ({ subscribeStructuredAgentSessionStatus: async ( _target: unknown, emit: ResumeStatusStream['emit'] diff --git a/src/renderer/src/components/native-chat-resume-skipped-progress.test.tsx b/src/renderer/src/components/native-chat-recovery/integration/native-chat-resume-skipped-progress.test.tsx similarity index 78% rename from src/renderer/src/components/native-chat-resume-skipped-progress.test.tsx rename to src/renderer/src/components/native-chat-recovery/integration/native-chat-resume-skipped-progress.test.tsx index 0207dd7c5f8..c88ee1ae544 100644 --- a/src/renderer/src/components/native-chat-resume-skipped-progress.test.tsx +++ b/src/renderer/src/components/native-chat-recovery/integration/native-chat-resume-skipped-progress.test.tsx @@ -7,21 +7,21 @@ import { type RestartRpc, type ResumeStatusStream } from './native-chat-resume-modal.test-support' -import { NativeChatResumeOnRestartModal } from './NativeChatResumeRuntimeModal' -import { NativeChatResumeStatusSegment } from './status-bar/NativeChatResumeStatusSegment' -import { requestNativeChatResumeOnRestartDialog } from './native-chat-resume-on-restart-dialog' -import { getNativeChatRestartRun } from './native-chat-resume-on-restart-store' +import { NativeChatResumeOnRestartModal } from '../NativeChatResumeRuntimeModal' +import { NativeChatResumeStatusSegment } from '../../status-bar/NativeChatResumeStatusSegment' +import { requestNativeChatResumeOnRestartDialog } from '../dialogs/native-chat-resume-on-restart-dialog' +import { getNativeChatRestartRun } from '../offers/native-chat-resume-on-restart-store' const rpc: Mock = vi.hoisted(() => vi.fn()) const statusStream: ResumeStatusStream = vi.hoisted(() => ({ emit: (_event: Parameters[0]) => {}, snapshot: new Map() })) -vi.mock('../runtime/native-chat/requests/structured-agent-session-call', () => ({ +vi.mock('../../../runtime/native-chat/requests/structured-agent-session-call', () => ({ callStructuredAgentSession: rpc })) -vi.mock('../runtime/native-chat/requests/structured-agent-session-subscriptions', () => ({ +vi.mock('../../../runtime/native-chat/requests/structured-agent-session-subscriptions', () => ({ subscribeStructuredAgentSessionStatus: async ( _target: unknown, emit: ResumeStatusStream['emit'] diff --git a/src/renderer/src/components/native-chat-resume-sonner-mock.test-support.ts b/src/renderer/src/components/native-chat-recovery/integration/native-chat-resume-sonner-mock.test-support.ts similarity index 100% rename from src/renderer/src/components/native-chat-resume-sonner-mock.test-support.ts rename to src/renderer/src/components/native-chat-recovery/integration/native-chat-resume-sonner-mock.test-support.ts diff --git a/src/renderer/src/components/native-chat-resume-toast.test-support.ts b/src/renderer/src/components/native-chat-recovery/integration/native-chat-resume-toast.test-support.ts similarity index 100% rename from src/renderer/src/components/native-chat-resume-toast.test-support.ts rename to src/renderer/src/components/native-chat-recovery/integration/native-chat-resume-toast.test-support.ts diff --git a/src/renderer/src/components/native-chat-launch-resume-decision.test.tsx b/src/renderer/src/components/native-chat-recovery/offers/native-chat-launch-resume-decision.test.tsx similarity index 85% rename from src/renderer/src/components/native-chat-launch-resume-decision.test.tsx rename to src/renderer/src/components/native-chat-recovery/offers/native-chat-launch-resume-decision.test.tsx index 5946eb95798..c852d63d1fe 100644 --- a/src/renderer/src/components/native-chat-launch-resume-decision.test.tsx +++ b/src/renderer/src/components/native-chat-recovery/offers/native-chat-launch-resume-decision.test.tsx @@ -4,9 +4,9 @@ import { act } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, expect, it, vi } from 'vitest' -import { useAppStore } from '../store' -import { getDefaultSettings } from '../../../shared/constants' -import type { ResumeCandidate } from './native-chat-resume-on-restart-grouping' +import { useAppStore } from '../../../store/index' +import { getDefaultSettings } from '../../../../../shared/constants' +import type { ResumeCandidate } from '../tree/native-chat-resume-on-restart-grouping' import { _resetNativeChatRestartOffer, useNativeChatRestartOffer, @@ -18,15 +18,15 @@ import { } from './native-chat-launch-resume-decision' const rpc = vi.hoisted(() => vi.fn()) -vi.mock('../runtime/native-chat/requests/structured-agent-session-call', () => ({ +vi.mock('../../../runtime/native-chat/requests/structured-agent-session-call', () => ({ callStructuredAgentSession: rpc })) -vi.mock('../runtime/native-chat/requests/structured-agent-session-subscriptions', () => ({ +vi.mock('../../../runtime/native-chat/requests/structured-agent-session-subscriptions', () => ({ subscribeStructuredAgentSessionStatus: () => new Promise(() => {}) })) vi.mock('sonner', async () => - (await import('./native-chat-resume-sonner-mock.test-support')).resumeSonnerMock() + (await import('../integration/native-chat-resume-sonner-mock.test-support')).resumeSonnerMock() ) globalThis.IS_REACT_ACT_ENVIRONMENT = true diff --git a/src/renderer/src/components/native-chat-launch-resume-decision.ts b/src/renderer/src/components/native-chat-recovery/offers/native-chat-launch-resume-decision.ts similarity index 95% rename from src/renderer/src/components/native-chat-launch-resume-decision.ts rename to src/renderer/src/components/native-chat-recovery/offers/native-chat-launch-resume-decision.ts index e3676dce4e2..44013231996 100644 --- a/src/renderer/src/components/native-chat-launch-resume-decision.ts +++ b/src/renderer/src/components/native-chat-recovery/offers/native-chat-launch-resume-decision.ts @@ -1,7 +1,7 @@ // Suppress competing continuation offers until the launch makes its resume decision and publishes resuming ids. import { useSyncExternalStore } from 'react' -import { useAppStore } from '../store' +import { useAppStore } from '../../../store/index' let decided = false const listeners = new Set<() => void>() diff --git a/src/renderer/src/components/native-chat-remote-restart-offer.ts b/src/renderer/src/components/native-chat-recovery/offers/native-chat-remote-restart-offer.ts similarity index 92% rename from src/renderer/src/components/native-chat-remote-restart-offer.ts rename to src/renderer/src/components/native-chat-recovery/offers/native-chat-remote-restart-offer.ts index 63cf2e1179e..e66c769e09c 100644 --- a/src/renderer/src/components/native-chat-remote-restart-offer.ts +++ b/src/renderer/src/components/native-chat-recovery/offers/native-chat-remote-restart-offer.ts @@ -1,5 +1,5 @@ -import type { RuntimeEnvironmentStatus } from '../../../shared/remote-runtime/host-status/runtime-host-status' -import type { RuntimeClientTarget } from '../../../shared/runtime-client-target' +import type { RuntimeEnvironmentStatus } from '../../../../../shared/remote-runtime/host-status/runtime-host-status' +import type { RuntimeClientTarget } from '../../../../../shared/runtime-client-target' import type { HostOfferRead } from './native-chat-restart-host-offer' export const RESTART_READ_RETRY_DELAYS_MS = [100, 250, 500] as const diff --git a/src/renderer/src/components/native-chat-restart-host-offer.ts b/src/renderer/src/components/native-chat-recovery/offers/native-chat-restart-host-offer.ts similarity index 94% rename from src/renderer/src/components/native-chat-restart-host-offer.ts rename to src/renderer/src/components/native-chat-recovery/offers/native-chat-restart-host-offer.ts index 2bea9f74f46..134278b8693 100644 --- a/src/renderer/src/components/native-chat-restart-host-offer.ts +++ b/src/renderer/src/components/native-chat-recovery/offers/native-chat-restart-host-offer.ts @@ -1,8 +1,8 @@ import { callStructuredAgentSession } from '@/runtime/native-chat/requests/structured-agent-session-call' import { RuntimeRpcCallError } from '@/runtime/rpc/calls/runtime-rpc-result' -import type { RuntimeClientTarget } from '../../../shared/runtime-client-target' +import type { RuntimeClientTarget } from '../../../../../shared/runtime-client-target' import { executionHostIdForStructuredTarget } from '@/runtime/native-chat/session-ownership/structured-agent-session-owner' -import type { ResumeCandidate, ResumeFailure } from './native-chat-resume-on-restart-grouping' +import type { ResumeCandidate, ResumeFailure } from '../tree/native-chat-resume-on-restart-grouping' export type HostOfferRead = { candidates: readonly ResumeCandidate[] failed: readonly ResumeFailure[] diff --git a/src/renderer/src/components/native-chat-restart-occurrence.ts b/src/renderer/src/components/native-chat-recovery/offers/native-chat-restart-occurrence.ts similarity index 96% rename from src/renderer/src/components/native-chat-restart-occurrence.ts rename to src/renderer/src/components/native-chat-recovery/offers/native-chat-restart-occurrence.ts index 0c79ce5007d..dda74a426f0 100644 --- a/src/renderer/src/components/native-chat-restart-occurrence.ts +++ b/src/renderer/src/components/native-chat-recovery/offers/native-chat-restart-occurrence.ts @@ -6,19 +6,19 @@ import { type NativeChatRestartOffer, type HostOfferRead } from './native-chat-restart-host-offer' -import type { RuntimeClientTarget } from '../../../shared/runtime-client-target' +import type { RuntimeClientTarget } from '../../../../../shared/runtime-client-target' import { getStructuredAgentSessionStatusFeed } from '@/runtime/native-chat/status/structured-agent-session-status-feed' import { markNativeChatLaunchResumeDecided } from './native-chat-launch-resume-decision' -import { announceRestartResults } from './native-chat-restart-action-notifications' +import { announceRestartResults } from '../runs/native-chat-restart-action-notifications' import { beginResumeRun, resumeRunInFlight, resumeRunPendingIds, type ResumeRun, type ResumeRunStarter -} from './native-chat-resume-run' +} from '../runs/native-chat-resume-run' import { createUnsentResumes } from './native-chat-resume-unsent-requests' -import * as dialog from './native-chat-resume-on-restart-dialog' +import * as dialog from '../dialogs/native-chat-resume-on-restart-dialog' import { createOfferedChatWatch } from './native-chat-resume-offered-chat-watch' import { createRemoteRestartOfferLifecycle, @@ -27,8 +27,8 @@ import { import { continueRestartChunks, type RestartContinuationBatchResult -} from './native-chat-restart-continuation-chunks' -import { watchResumeRunStatus } from './native-chat-resume-run-status-watch' +} from '../runs/native-chat-restart-continuation-chunks' +import { watchResumeRunStatus } from '../runs/native-chat-resume-run-status-watch' let nextOccurrence = 0 diff --git a/src/renderer/src/components/native-chat-restart-runtime-store.ts b/src/renderer/src/components/native-chat-recovery/offers/native-chat-restart-runtime-store.ts similarity index 89% rename from src/renderer/src/components/native-chat-restart-runtime-store.ts rename to src/renderer/src/components/native-chat-recovery/offers/native-chat-restart-runtime-store.ts index bac2a11ae96..a92727cf52e 100644 --- a/src/renderer/src/components/native-chat-restart-runtime-store.ts +++ b/src/renderer/src/components/native-chat-recovery/offers/native-chat-restart-runtime-store.ts @@ -1,5 +1,5 @@ -import type { RuntimeClientTarget } from '../../../shared/runtime-client-target' -import type { RuntimeEnvironmentStatus } from '../../../shared/remote-runtime/host-status/runtime-host-status' +import type { RuntimeClientTarget } from '../../../../../shared/runtime-client-target' +import type { RuntimeEnvironmentStatus } from '../../../../../shared/remote-runtime/host-status/runtime-host-status' import { remoteRestartRuntimeId } from './native-chat-remote-restart-offer' import { createNativeChatRestartOccurrence } from './native-chat-restart-occurrence' import { EMPTY } from './native-chat-restart-host-offer' @@ -7,8 +7,8 @@ import { resumeRunPendingIds, type ResumeRun, type ResumeRunStarter -} from './native-chat-resume-run' -import * as dialog from './native-chat-resume-on-restart-dialog' +} from '../runs/native-chat-resume-run' +import * as dialog from '../dialogs/native-chat-resume-on-restart-dialog' const NO_RESUMING = resumeRunPendingIds(null) @@ -84,3 +84,5 @@ export function createNativeChatRestartStore(target: RuntimeClientTarget) { } } } + +export type NativeChatRestartStore = ReturnType diff --git a/src/renderer/src/components/native-chat-restart-runtimes.ts b/src/renderer/src/components/native-chat-recovery/offers/native-chat-restart-runtimes.ts similarity index 88% rename from src/renderer/src/components/native-chat-restart-runtimes.ts rename to src/renderer/src/components/native-chat-recovery/offers/native-chat-restart-runtimes.ts index acd6b38f44c..cecb9cd55a9 100644 --- a/src/renderer/src/components/native-chat-restart-runtimes.ts +++ b/src/renderer/src/components/native-chat-recovery/offers/native-chat-restart-runtimes.ts @@ -1,11 +1,11 @@ import { useEffect, useSyncExternalStore } from 'react' -import { useAppStore } from '../store' -import type { RuntimeClientTarget } from '../../../shared/runtime-client-target' +import { useAppStore } from '../../../store/index' +import type { RuntimeClientTarget } from '../../../../../shared/runtime-client-target' import { createNativeChatRestartStore, - localNativeChatRestartStore, type NativeChatRestartStore -} from './native-chat-resume-on-restart-store' +} from './native-chat-restart-runtime-store' +import { localNativeChatRestartStore } from './native-chat-resume-on-restart-store' const stores = new Map() const listeners = new Set<() => void>() diff --git a/src/renderer/src/components/native-chat-resume-failed-chat-watch.test.ts b/src/renderer/src/components/native-chat-recovery/offers/native-chat-resume-failed-chat-watch.test.ts similarity index 76% rename from src/renderer/src/components/native-chat-resume-failed-chat-watch.test.ts rename to src/renderer/src/components/native-chat-recovery/offers/native-chat-resume-failed-chat-watch.test.ts index 699a08de4be..28ce94136b3 100644 --- a/src/renderer/src/components/native-chat-resume-failed-chat-watch.test.ts +++ b/src/renderer/src/components/native-chat-recovery/offers/native-chat-resume-failed-chat-watch.test.ts @@ -4,11 +4,10 @@ import { afterEach, beforeEach, expect, it, vi } from 'vitest' import type { AgentSessionStatusEvent, AgentSessionStatusSummary -} from '../../../shared/agent-session/agent-session-wire' +} from '../../../../../shared/agent-session/agent-session-wire' import { resetStructuredAgentSessionStatusFeedsForTests } from '@/runtime/native-chat/status/structured-agent-session-status-feed' import { _resetNativeChatRestartOffer, - continueNativeChatRestartOffer, dismissNativeChatRestartOffer, getNativeChatRestartOffer, refreshNativeChatRestartOffer @@ -19,11 +18,11 @@ const mocks = vi.hoisted(() => ({ subscribeStatus: vi.fn(), unsubscribe: vi.fn() })) -vi.mock('../runtime/native-chat/requests/structured-agent-session-call', () => ({ +vi.mock('../../../runtime/native-chat/requests/structured-agent-session-call', () => ({ callStructuredAgentSession: mocks.rpc })) -vi.mock('../runtime/native-chat/requests/structured-agent-session-subscriptions', () => ({ +vi.mock('../../../runtime/native-chat/requests/structured-agent-session-subscriptions', () => ({ subscribeStructuredAgentSessionStatus: mocks.subscribeStatus })) @@ -118,28 +117,3 @@ it('never lets a re-read that was already in flight bring back a dismissed failu expect(getNativeChatRestartOffer().failed).toEqual([]) }) - -it('waits for a resume in flight instead of re-reading under it', async () => { - await listFailure({ failed: [failure] }) - const acting = Promise.withResolvers() - let failed: unknown[] = [failure] - mocks.rpc.mockImplementation((_target: unknown, method: string) => - method === 'agentSession.restartContinue' - ? acting.promise - : Promise.resolve({ sessions: [], failed }) - ) - const retry = continueNativeChatRestartOffer(['a']) - hostEmit()({ type: 'status', session: summary('working', 'Carry on please', Date.now() + 1) }) - await vi.advanceTimersByTimeAsync(500) - expect(offerReads()).toBe(1) - - failed = [] - acting.resolve({ - sessions: [], - failed: [], - continued: [{ sessionId: 'a', outcome: 'continued' }] - }) - await retry - expect(offerReads()).toBe(1) - expect(getNativeChatRestartOffer().failed).toEqual([]) -}) diff --git a/src/renderer/src/components/native-chat-resume-offered-chat-watch.ts b/src/renderer/src/components/native-chat-recovery/offers/native-chat-resume-offered-chat-watch.ts similarity index 96% rename from src/renderer/src/components/native-chat-resume-offered-chat-watch.ts rename to src/renderer/src/components/native-chat-recovery/offers/native-chat-resume-offered-chat-watch.ts index e0d3cf11a37..c482b1cc046 100644 --- a/src/renderer/src/components/native-chat-resume-offered-chat-watch.ts +++ b/src/renderer/src/components/native-chat-recovery/offers/native-chat-resume-offered-chat-watch.ts @@ -1,6 +1,6 @@ import type { StructuredAgentSessionStatusChange } from '@/runtime/native-chat/status/structured-agent-session-status-dispatch' import type { StructuredAgentSessionStatusFeedOwner } from '@/runtime/native-chat/status/structured-agent-session-status-feed' -import type { AgentSessionStatusSummary } from '../../../shared/agent-session/agent-session-wire' +import type { AgentSessionStatusSummary } from '../../../../../shared/agent-session/agent-session-wire' /** Watch offered/failed chats only; debounce status/prompt changes into host rereads without one request per streamed summary. */ diff --git a/src/renderer/src/components/native-chat-resume-on-restart-store.ts b/src/renderer/src/components/native-chat-recovery/offers/native-chat-resume-on-restart-store.ts similarity index 86% rename from src/renderer/src/components/native-chat-resume-on-restart-store.ts rename to src/renderer/src/components/native-chat-recovery/offers/native-chat-resume-on-restart-store.ts index e7b8081c747..acff6a88abf 100644 --- a/src/renderer/src/components/native-chat-resume-on-restart-store.ts +++ b/src/renderer/src/components/native-chat-recovery/offers/native-chat-resume-on-restart-store.ts @@ -1,13 +1,12 @@ +import type { NativeChatRestartStore } from './native-chat-restart-runtime-store' import { RESTART_READ_RETRY_DELAYS_MS } from './native-chat-remote-restart-offer' -import { requestNativeChatResumeOnRestartDialog } from './native-chat-resume-on-restart-dialog' -import { useAppStore } from '../store' +import { requestNativeChatResumeOnRestartDialog } from '../dialogs/native-chat-resume-on-restart-dialog' +import { useAppStore } from '../../../store/index' import { markNativeChatLaunchResumeDecided } from './native-chat-launch-resume-decision' -import { allResumeSessionIds } from './native-chat-resume-on-restart-grouping' +import { allResumeSessionIds } from '../tree/native-chat-resume-on-restart-grouping' import { useEffect, useSyncExternalStore } from 'react' import { createNativeChatRestartStore } from './native-chat-restart-runtime-store' -export type { NativeChatRestartOffer } from './native-chat-restart-host-offer' -export { createNativeChatRestartStore } from './native-chat-restart-runtime-store' -export type NativeChatRestartStore = ReturnType + export const localNativeChatRestartStore = createNativeChatRestartStore({ kind: 'local' }) export const { getNativeChatRestartOffer, diff --git a/src/renderer/src/components/native-chat-resume-unsent-requests.ts b/src/renderer/src/components/native-chat-recovery/offers/native-chat-resume-unsent-requests.ts similarity index 95% rename from src/renderer/src/components/native-chat-resume-unsent-requests.ts rename to src/renderer/src/components/native-chat-recovery/offers/native-chat-resume-unsent-requests.ts index b5be6b0ff24..fb03ac2b99e 100644 --- a/src/renderer/src/components/native-chat-resume-unsent-requests.ts +++ b/src/renderer/src/components/native-chat-recovery/offers/native-chat-resume-unsent-requests.ts @@ -1,4 +1,4 @@ -import type { ResumeCandidate, ResumeFailure } from './native-chat-resume-on-restart-grouping' +import type { ResumeCandidate, ResumeFailure } from '../tree/native-chat-resume-on-restart-grouping' // A lost reply is not a delivery receipt; retain uncertainty until the host supplies current facts. diff --git a/src/renderer/src/components/NativeChatResumeRunPanel.tsx b/src/renderer/src/components/native-chat-recovery/runs/NativeChatResumeRunPanel.tsx similarity index 98% rename from src/renderer/src/components/NativeChatResumeRunPanel.tsx rename to src/renderer/src/components/native-chat-recovery/runs/NativeChatResumeRunPanel.tsx index 6af112638e5..f7e29653a5c 100644 --- a/src/renderer/src/components/NativeChatResumeRunPanel.tsx +++ b/src/renderer/src/components/native-chat-recovery/runs/NativeChatResumeRunPanel.tsx @@ -3,7 +3,7 @@ import { Loader2 } from 'lucide-react' import { translate } from '@/i18n/i18n' import { useTranslate, type Translate } from '@/i18n/use-translate' import { useNow } from '@/hooks/use-now' -import type { ResumeCandidate, ResumeFailure } from './native-chat-resume-on-restart-grouping' +import type { ResumeCandidate, ResumeFailure } from '../tree/native-chat-resume-on-restart-grouping' import { resumeRunInFlight, type ResumeRun, type ResumeRunStarter } from './native-chat-resume-run' import { resumeRunView, type ResumeRunFilter } from './native-chat-resume-run-view' import { ResumeRunStatusIcon } from './NativeChatResumeRunStatusIcon' diff --git a/src/renderer/src/components/NativeChatResumeRunStatusIcon.tsx b/src/renderer/src/components/native-chat-recovery/runs/NativeChatResumeRunStatusIcon.tsx similarity index 94% rename from src/renderer/src/components/NativeChatResumeRunStatusIcon.tsx rename to src/renderer/src/components/native-chat-recovery/runs/NativeChatResumeRunStatusIcon.tsx index 7a3ae94902b..6439650f307 100644 --- a/src/renderer/src/components/NativeChatResumeRunStatusIcon.tsx +++ b/src/renderer/src/components/native-chat-recovery/runs/NativeChatResumeRunStatusIcon.tsx @@ -1,8 +1,8 @@ import { AlertCircle, Check, Clock, Loader2 } from 'lucide-react' -import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip' +import { Tooltip, TooltipContent, TooltipTrigger } from '../../ui/tooltip' import { translate } from '@/i18n/i18n' import { useTranslate, type Translate } from '@/i18n/use-translate' -import { formatNativeChatDuration } from '../../../shared/native-chat/transcript/native-chat-turn-status' +import { formatNativeChatDuration } from '../../../../../shared/native-chat/transcript/native-chat-turn-status' import type { ResumeRunRowStatus } from './native-chat-resume-run-view' export type ResumeRunStartPhase = 'starting' | 'ready' | null diff --git a/src/renderer/src/components/NativeChatResumeRunSummary.tsx b/src/renderer/src/components/native-chat-recovery/runs/NativeChatResumeRunSummary.tsx similarity index 95% rename from src/renderer/src/components/NativeChatResumeRunSummary.tsx rename to src/renderer/src/components/native-chat-recovery/runs/NativeChatResumeRunSummary.tsx index 46b4ec1defa..4732c8e3cd4 100644 --- a/src/renderer/src/components/NativeChatResumeRunSummary.tsx +++ b/src/renderer/src/components/native-chat-recovery/runs/NativeChatResumeRunSummary.tsx @@ -1,7 +1,7 @@ -import { ToggleGroup, ToggleGroupItem } from './ui/toggle-group' +import { ToggleGroup, ToggleGroupItem } from '../../ui/toggle-group' import { translate } from '@/i18n/i18n' import { useTranslate, type Translate } from '@/i18n/use-translate' -import { formatNativeChatDuration } from '../../../shared/native-chat/transcript/native-chat-turn-status' +import { formatNativeChatDuration } from '../../../../../shared/native-chat/transcript/native-chat-turn-status' import type { ResumeRunFilter, ResumeRunView } from './native-chat-resume-run-view' import type { ResumeRunStartPhase } from './NativeChatResumeRunStatusIcon' diff --git a/src/renderer/src/components/native-chat-restart-action-notifications.ts b/src/renderer/src/components/native-chat-recovery/runs/native-chat-restart-action-notifications.ts similarity index 97% rename from src/renderer/src/components/native-chat-restart-action-notifications.ts rename to src/renderer/src/components/native-chat-recovery/runs/native-chat-restart-action-notifications.ts index 5172fb204f5..a3768dd72b4 100644 --- a/src/renderer/src/components/native-chat-restart-action-notifications.ts +++ b/src/renderer/src/components/native-chat-recovery/runs/native-chat-restart-action-notifications.ts @@ -1,8 +1,8 @@ import { createElement, Fragment, type ReactNode } from 'react' import { toast } from 'sonner' import { translate } from '@/i18n/i18n' -import type { ResumeFailure } from './native-chat-resume-on-restart-grouping' -import { INLINE_ACTION_CLASS_NAMES } from './ui/sonner' +import type { ResumeFailure } from '../tree/native-chat-resume-on-restart-grouping' +import { INLINE_ACTION_CLASS_NAMES } from '../../ui/sonner' export type RestartContinuationOutcome = { sessionId: string diff --git a/src/renderer/src/components/native-chat-restart-continuation-chunks.ts b/src/renderer/src/components/native-chat-recovery/runs/native-chat-restart-continuation-chunks.ts similarity index 93% rename from src/renderer/src/components/native-chat-restart-continuation-chunks.ts rename to src/renderer/src/components/native-chat-recovery/runs/native-chat-restart-continuation-chunks.ts index 0b6d1a89468..42ad4d2741f 100644 --- a/src/renderer/src/components/native-chat-restart-continuation-chunks.ts +++ b/src/renderer/src/components/native-chat-recovery/runs/native-chat-restart-continuation-chunks.ts @@ -1,5 +1,5 @@ -import type { ResumeFailure } from './native-chat-resume-on-restart-grouping' -import type { RuntimeClientTarget } from '../../../shared/runtime-client-target' +import type { ResumeFailure } from '../tree/native-chat-resume-on-restart-grouping' +import type { RuntimeClientTarget } from '../../../../../shared/runtime-client-target' import { callStructuredAgentSession } from '@/runtime/native-chat/requests/structured-agent-session-call' import { createRuntimeRpcAbortError } from '@/runtime/rpc/calls/abortable-runtime-environment-call' import { @@ -7,7 +7,7 @@ import { type HostOfferRead, type HostOfferPayload, type NativeChatRestartOffer -} from './native-chat-restart-host-offer' +} from '../offers/native-chat-restart-host-offer' import { restartContinuationHistory, type RestartContinuationOutcome diff --git a/src/renderer/src/components/native-chat-resume-run-status-watch.ts b/src/renderer/src/components/native-chat-recovery/runs/native-chat-resume-run-status-watch.ts similarity index 95% rename from src/renderer/src/components/native-chat-resume-run-status-watch.ts rename to src/renderer/src/components/native-chat-recovery/runs/native-chat-resume-run-status-watch.ts index 879956e64ca..a3ee47c0b69 100644 --- a/src/renderer/src/components/native-chat-resume-run-status-watch.ts +++ b/src/renderer/src/components/native-chat-recovery/runs/native-chat-resume-run-status-watch.ts @@ -1,4 +1,4 @@ -import type { RuntimeClientTarget } from '../../../shared/runtime-client-target' +import type { RuntimeClientTarget } from '../../../../../shared/runtime-client-target' import { getStructuredAgentSessionStatusFeed } from '@/runtime/native-chat/status/structured-agent-session-status-feed' import type { StructuredAgentSessionStatusChange } from '@/runtime/native-chat/status/structured-agent-session-status-dispatch' import { observeResumeRun, type ResumeRun } from './native-chat-resume-run' diff --git a/src/renderer/src/components/native-chat-resume-run-view.ts b/src/renderer/src/components/native-chat-recovery/runs/native-chat-resume-run-view.ts similarity index 98% rename from src/renderer/src/components/native-chat-resume-run-view.ts rename to src/renderer/src/components/native-chat-recovery/runs/native-chat-resume-run-view.ts index 4bfb6a5e0f6..fa3f7056f2e 100644 --- a/src/renderer/src/components/native-chat-resume-run-view.ts +++ b/src/renderer/src/components/native-chat-recovery/runs/native-chat-resume-run-view.ts @@ -1,4 +1,4 @@ -import type { ResumeCandidate, ResumeFailure } from './native-chat-resume-on-restart-grouping' +import type { ResumeCandidate, ResumeFailure } from '../tree/native-chat-resume-on-restart-grouping' import type { ResumeRun, ResumeRunHostStatus } from './native-chat-resume-run' export type ResumeRunRowStatus = diff --git a/src/renderer/src/components/native-chat-resume-run.test.ts b/src/renderer/src/components/native-chat-recovery/runs/native-chat-resume-run.test.ts similarity index 76% rename from src/renderer/src/components/native-chat-resume-run.test.ts rename to src/renderer/src/components/native-chat-recovery/runs/native-chat-resume-run.test.ts index b30612f11c1..9497eec9c57 100644 --- a/src/renderer/src/components/native-chat-resume-run.test.ts +++ b/src/renderer/src/components/native-chat-recovery/runs/native-chat-resume-run.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { ResumeCandidate } from './native-chat-resume-on-restart-grouping' +import type { ResumeCandidate } from '../tree/native-chat-resume-on-restart-grouping' import { beginResumeRun, observeResumeRun, @@ -21,24 +21,6 @@ const failure = { ...row('a'), failedAt: 2, outcome: 'unconfirmed' as const, rea const run = beginResumeRun([row('a'), row('b')], 10, 'user') describe('the dialog over a bulk action', () => { - it('never regresses a seen phase or verdict when live progress clears', () => { - let observed = run - for (const phase of ['queued', 'starting', 'continued'] as const) { - observed = observeResumeRun(observed, () => ({ restartResume: { phase } })) - const before = resumeRunView(observed, [], () => undefined, 'all') - expect(observeResumeRun(observed, () => undefined)).toBe(observed) - expect(observeResumeRun(observed, () => ({ restartResume: { phase: 'queued' } }))).toBe( - observed - ) - expect(resumeRunView(observed, [], () => undefined, 'all')).toEqual(before) - } - expect(resumeRunView(observed, [], () => undefined, 'all').counts.done).toBe(2) - expect(observeResumeRun(observed, () => ({ restartResume: { phase: 'refused' } }))).toBe( - observed - ) - expect(observeResumeRun({ ...observed, inFlight: false }, () => undefined).inFlight).toBe(false) - }) - it('remembers readiness and excludes skipped chats from rows and the denominator', () => { const observed = observeResumeRun(run, (id) => ({ restartResume: { phase: id === 'a' ? 'skipped' : 'starting' }, @@ -145,24 +127,6 @@ describe('the dialog over a bulk action', () => { expect(refused.counts.attention).toBe(0) }) - it('retains successful history but always shows current host failures', () => { - const finished = { - ...run, - inFlight: false, - continued: [{ sessionId: 'a', outcome: 'continued' as const }] - } - expect(resumeRunView(finished, [], () => undefined, 'resumed').rows).toEqual([row('a')]) - const failed = resumeRunView( - finished, - [failure], - (id) => (id === 'a' ? failure : undefined), - 'attention' - ) - expect(failed.rows).toEqual([failure]) - expect(failed.statusBySession.has('a')).toBe(false) - expect(resumeRunPendingIds(finished)).toBe(resumeRunPendingIds(null)) - }) - it('shows final per-chat failures from the feed before the batch returns', () => { for (const phase of ['refused', 'unconfirmed'] as const) { const view = resumeRunView( diff --git a/src/renderer/src/components/native-chat-resume-run.ts b/src/renderer/src/components/native-chat-recovery/runs/native-chat-resume-run.ts similarity index 93% rename from src/renderer/src/components/native-chat-resume-run.ts rename to src/renderer/src/components/native-chat-recovery/runs/native-chat-resume-run.ts index d7ba6975573..e15748ad3cd 100644 --- a/src/renderer/src/components/native-chat-resume-run.ts +++ b/src/renderer/src/components/native-chat-recovery/runs/native-chat-resume-run.ts @@ -1,6 +1,6 @@ -import type { ResumeCandidate } from './native-chat-resume-on-restart-grouping' +import type { ResumeCandidate } from '../tree/native-chat-resume-on-restart-grouping' import type { RestartContinuationOutcome } from './native-chat-restart-action-notifications' -import type { AgentSessionStatusSummary } from '../../../shared/agent-session/agent-session-wire' +import type { AgentSessionStatusSummary } from '../../../../../shared/agent-session/agent-session-wire' export type ResumeRunHostStatus = Pick< AgentSessionStatusSummary, diff --git a/src/renderer/src/components/NativeChatResumeGroupNode.tsx b/src/renderer/src/components/native-chat-recovery/tree/NativeChatResumeGroupNode.tsx similarity index 100% rename from src/renderer/src/components/NativeChatResumeGroupNode.tsx rename to src/renderer/src/components/native-chat-recovery/tree/NativeChatResumeGroupNode.tsx diff --git a/src/renderer/src/components/NativeChatResumeOnRestartAgentRow.tsx b/src/renderer/src/components/native-chat-recovery/tree/NativeChatResumeOnRestartAgentRow.tsx similarity index 91% rename from src/renderer/src/components/NativeChatResumeOnRestartAgentRow.tsx rename to src/renderer/src/components/native-chat-recovery/tree/NativeChatResumeOnRestartAgentRow.tsx index b1ebf752d87..f7ca3dd3493 100644 --- a/src/renderer/src/components/NativeChatResumeOnRestartAgentRow.tsx +++ b/src/renderer/src/components/native-chat-recovery/tree/NativeChatResumeOnRestartAgentRow.tsx @@ -1,15 +1,18 @@ import { useContext } from 'react' import { AgentIcon } from '@/components/agent/icons/AgentIcon' -import { agentTypeToIconAgent } from '../../../shared/agent-status/presentation/agent-type-icon-agent' -import { formatAgentTypeLabel } from '../../../shared/agent-status/presentation/agent-type-label' +import { agentTypeToIconAgent } from '../../../../../shared/agent-status/presentation/agent-type-icon-agent' +import { formatAgentTypeLabel } from '../../../../../shared/agent-status/presentation/agent-type-label' import { formatShortTimeAgo } from '@/lib/short-time-ago' import { useTranslate } from '@/i18n/use-translate' import type { ResumeCandidate, ResumeFailure } from './native-chat-resume-on-restart-grouping' import { resumeFailureSelectable, type ResumeFailureAction -} from './native-chat-resume-failure-guidance' -import { ResumeFailureGuidanceLine, ResumeFailureStatus } from './NativeChatResumeFailureDetails' +} from '../failures/native-chat-resume-failure-guidance' +import { + ResumeFailureGuidanceLine, + ResumeFailureStatus +} from '../failures/NativeChatResumeFailureDetails' import { resumeActivityLabel } from './native-chat-resume-activity-label' import { ResumeTreeRow } from './NativeChatResumeTreeRow' import { ResumeTreeDepthContext } from './native-chat-resume-tree-state' diff --git a/src/renderer/src/components/NativeChatResumeOnRestartGroups.selection.test.tsx b/src/renderer/src/components/native-chat-recovery/tree/NativeChatResumeOnRestartGroups.selection.test.tsx similarity index 95% rename from src/renderer/src/components/NativeChatResumeOnRestartGroups.selection.test.tsx rename to src/renderer/src/components/native-chat-recovery/tree/NativeChatResumeOnRestartGroups.selection.test.tsx index f6c3909762a..0219a1a7548 100644 --- a/src/renderer/src/components/NativeChatResumeOnRestartGroups.selection.test.tsx +++ b/src/renderer/src/components/native-chat-recovery/tree/NativeChatResumeOnRestartGroups.selection.test.tsx @@ -3,9 +3,9 @@ import { act } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, expect, it } from 'vitest' -import { useAppStore } from '../store' -import { getHostContextLabel } from '../../../shared/worktree/host-context-labels' -import type { ExecutionHostId } from '../../../shared/execution-host' +import { useAppStore } from '../../../store/index' +import { getHostContextLabel } from '../../../../../shared/worktree/host-context-labels' +import type { ExecutionHostId } from '../../../../../shared/execution-host' import { candidate, chatBox, diff --git a/src/renderer/src/components/NativeChatResumeOnRestartGroups.test.tsx b/src/renderer/src/components/native-chat-recovery/tree/NativeChatResumeOnRestartGroups.test.tsx similarity index 92% rename from src/renderer/src/components/NativeChatResumeOnRestartGroups.test.tsx rename to src/renderer/src/components/native-chat-recovery/tree/NativeChatResumeOnRestartGroups.test.tsx index 1b45bfcd55d..fcb1c80c443 100644 --- a/src/renderer/src/components/NativeChatResumeOnRestartGroups.test.tsx +++ b/src/renderer/src/components/native-chat-recovery/tree/NativeChatResumeOnRestartGroups.test.tsx @@ -3,9 +3,9 @@ import { act } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, expect, it, vi } from 'vitest' -import { useAppStore } from '../store' -import { getDefaultSettings } from '../../../shared/constants' -import type { ExecutionHostId } from '../../../shared/execution-host' +import { useAppStore } from '../../../store/index' +import { getDefaultSettings } from '../../../../../shared/constants' +import type { ExecutionHostId } from '../../../../../shared/execution-host' import { candidate, chatBox, diff --git a/src/renderer/src/components/NativeChatResumeOnRestartGroups.tsx b/src/renderer/src/components/native-chat-recovery/tree/NativeChatResumeOnRestartGroups.tsx similarity index 97% rename from src/renderer/src/components/NativeChatResumeOnRestartGroups.tsx rename to src/renderer/src/components/native-chat-recovery/tree/NativeChatResumeOnRestartGroups.tsx index 7f8857632b5..a83ecedd4ed 100644 --- a/src/renderer/src/components/NativeChatResumeOnRestartGroups.tsx +++ b/src/renderer/src/components/native-chat-recovery/tree/NativeChatResumeOnRestartGroups.tsx @@ -6,13 +6,13 @@ import { WorktreeHostContextBadge } from '@/components/sidebar/workspace-card/ho import { useSidebarHostScopeOptions } from '@/components/sidebar/worktree-list/controls/use-sidebar-host-scope-options' import { useTranslate } from '@/i18n/use-translate' import { resolveWorktreeBranchLabel } from '@/lib/worktree-default-display-name' -import { getHostContextLabel } from '../../../shared/worktree/host-context-labels' +import { getHostContextLabel } from '../../../../../shared/worktree/host-context-labels' import { LOCAL_EXECUTION_HOST_ID, parseExecutionHostId, type ExecutionHostId -} from '../../../shared/execution-host' -import { useAppStore } from '../store' +} from '../../../../../shared/execution-host' +import { useAppStore } from '../../../store/index' import * as workspaceDisplay from './native-chat-resume-workspace-display' import { useLineageAncestors, @@ -43,8 +43,6 @@ import { type ResumeWorkspaceNode } from './native-chat-resume-on-restart-grouping' -export type { ResumeCandidate } from './native-chat-resume-on-restart-grouping' - /** Group by machine/project/workspace/chat, nesting child workspaces after parent chats. Show machine nodes for remote/multiple hosts; name unknown workspaces by their encoded folder. */ function WorkspaceNode({ diff --git a/src/renderer/src/components/NativeChatResumeTreeHarness.test-support.tsx b/src/renderer/src/components/native-chat-recovery/tree/NativeChatResumeTreeHarness.test-support.tsx similarity index 96% rename from src/renderer/src/components/NativeChatResumeTreeHarness.test-support.tsx rename to src/renderer/src/components/native-chat-recovery/tree/NativeChatResumeTreeHarness.test-support.tsx index 02f482ba820..49ade1443c8 100644 --- a/src/renderer/src/components/NativeChatResumeTreeHarness.test-support.tsx +++ b/src/renderer/src/components/native-chat-recovery/tree/NativeChatResumeTreeHarness.test-support.tsx @@ -2,7 +2,7 @@ import { useState } from 'react' import { ResumeOnRestartGroups } from './NativeChatResumeOnRestartGroups' -import { TooltipProvider } from './ui/tooltip' +import { TooltipProvider } from '../../ui/tooltip' import type { ResumeCandidate, ResumeFailure } from './native-chat-resume-on-restart-grouping' import { onToggleSpy } from './native-chat-resume-tree.test-support' diff --git a/src/renderer/src/components/NativeChatResumeTreeRow.tsx b/src/renderer/src/components/native-chat-recovery/tree/NativeChatResumeTreeRow.tsx similarity index 99% rename from src/renderer/src/components/NativeChatResumeTreeRow.tsx rename to src/renderer/src/components/native-chat-recovery/tree/NativeChatResumeTreeRow.tsx index 036e99dc88b..67b126c3bd9 100644 --- a/src/renderer/src/components/NativeChatResumeTreeRow.tsx +++ b/src/renderer/src/components/native-chat-recovery/tree/NativeChatResumeTreeRow.tsx @@ -1,7 +1,7 @@ import { useContext } from 'react' import { ChevronRight } from 'lucide-react' import { useTranslate } from '@/i18n/use-translate' -import { Checkbox } from './ui/checkbox' +import { Checkbox } from '../../ui/checkbox' import { ResumeTreeSelectingContext } from './native-chat-resume-tree-state' /** Indent per tree level, right of the shared checkbox column. */ diff --git a/src/renderer/src/components/native-chat-resume-activity-label.ts b/src/renderer/src/components/native-chat-recovery/tree/native-chat-resume-activity-label.ts similarity index 96% rename from src/renderer/src/components/native-chat-resume-activity-label.ts rename to src/renderer/src/components/native-chat-recovery/tree/native-chat-resume-activity-label.ts index b6d60a5a408..4a2d3a8582d 100644 --- a/src/renderer/src/components/native-chat-resume-activity-label.ts +++ b/src/renderer/src/components/native-chat-recovery/tree/native-chat-resume-activity-label.ts @@ -1,6 +1,6 @@ import { translate } from '@/i18n/i18n' import type { Translate } from '@/i18n/use-translate' -import type { AgentSessionRestartActivity } from '../../../shared/agent-session/lifecycle/agent-session-restart-activity' +import type { AgentSessionRestartActivity } from '../../../../../shared/agent-session/lifecycle/agent-session-restart-activity' export type ResumeActivityLabel = { /** One line naming what the chat was doing, in the sidebar's own vocabulary. */ diff --git a/src/renderer/src/components/native-chat-resume-on-restart-grouping.test.ts b/src/renderer/src/components/native-chat-recovery/tree/native-chat-resume-on-restart-grouping.test.ts similarity index 100% rename from src/renderer/src/components/native-chat-resume-on-restart-grouping.test.ts rename to src/renderer/src/components/native-chat-recovery/tree/native-chat-resume-on-restart-grouping.test.ts diff --git a/src/renderer/src/components/native-chat-resume-on-restart-grouping.ts b/src/renderer/src/components/native-chat-recovery/tree/native-chat-resume-on-restart-grouping.ts similarity index 91% rename from src/renderer/src/components/native-chat-resume-on-restart-grouping.ts rename to src/renderer/src/components/native-chat-recovery/tree/native-chat-resume-on-restart-grouping.ts index c89cf443bc4..3464918edad 100644 --- a/src/renderer/src/components/native-chat-resume-on-restart-grouping.ts +++ b/src/renderer/src/components/native-chat-recovery/tree/native-chat-resume-on-restart-grouping.ts @@ -1,10 +1,10 @@ -import { parseWorkspaceKey } from '../../../shared/workspace-scope' -import type { AgentSessionWorkspaceKind } from '../../../shared/agent-session/lifecycle/agent-session-record' -import { LOCAL_EXECUTION_HOST_ID, type ExecutionHostId } from '../../../shared/execution-host' -import { projectGroupIdFromRepoId } from '../../../shared/folder-workspaces/folder-workspace-worktree' -import type { RepoIcon } from '../../../shared/repo-icon' -import type { AgentSessionRestartActivity } from '../../../shared/agent-session/lifecycle/agent-session-restart-activity' -import type { StructuredAgentId } from '../../../shared/agent-session/lifecycle/agent-session-provider-handle' +import { parseWorkspaceKey } from '../../../../../shared/workspace-scope' +import type { AgentSessionWorkspaceKind } from '../../../../../shared/agent-session/lifecycle/agent-session-record' +import { LOCAL_EXECUTION_HOST_ID, type ExecutionHostId } from '../../../../../shared/execution-host' +import { projectGroupIdFromRepoId } from '../../../../../shared/folder-workspaces/folder-workspace-worktree' +import type { RepoIcon } from '../../../../../shared/repo-icon' +import type { AgentSessionRestartActivity } from '../../../../../shared/agent-session/lifecycle/agent-session-restart-activity' +import type { StructuredAgentId } from '../../../../../shared/agent-session/lifecycle/agent-session-provider-handle' export type ResumeCandidate = { sessionId: string diff --git a/src/renderer/src/components/native-chat-resume-tree-keys.ts b/src/renderer/src/components/native-chat-recovery/tree/native-chat-resume-tree-keys.ts similarity index 100% rename from src/renderer/src/components/native-chat-resume-tree-keys.ts rename to src/renderer/src/components/native-chat-recovery/tree/native-chat-resume-tree-keys.ts diff --git a/src/renderer/src/components/native-chat-resume-tree-state.ts b/src/renderer/src/components/native-chat-recovery/tree/native-chat-resume-tree-state.ts similarity index 98% rename from src/renderer/src/components/native-chat-resume-tree-state.ts rename to src/renderer/src/components/native-chat-recovery/tree/native-chat-resume-tree-state.ts index a748c9f05c8..ac106c831d2 100644 --- a/src/renderer/src/components/native-chat-resume-tree-state.ts +++ b/src/renderer/src/components/native-chat-recovery/tree/native-chat-resume-tree-state.ts @@ -7,7 +7,7 @@ import type { import { resumeFailureSelectable, type ResumeFailureAction -} from './native-chat-resume-failure-guidance' +} from '../failures/native-chat-resume-failure-guidance' /** Start expanded per mount; closing/unmounting resets the tree rather than persisting expansion. */ export function useResumeTreeExpansion(): { diff --git a/src/renderer/src/components/native-chat-resume-tree.test-support.ts b/src/renderer/src/components/native-chat-recovery/tree/native-chat-resume-tree.test-support.ts similarity index 93% rename from src/renderer/src/components/native-chat-resume-tree.test-support.ts rename to src/renderer/src/components/native-chat-recovery/tree/native-chat-resume-tree.test-support.ts index 70f030f2570..16a0a8917da 100644 --- a/src/renderer/src/components/native-chat-resume-tree.test-support.ts +++ b/src/renderer/src/components/native-chat-recovery/tree/native-chat-resume-tree.test-support.ts @@ -1,9 +1,9 @@ import { vi, type Mock } from 'vitest' -import { useAppStore } from '../store' -import { getDefaultSettings } from '../../../shared/constants' -import type { ExecutionHostId } from '../../../shared/execution-host' -import type { WorktreeLineage } from '../../../shared/worktree/lineage-types' -import type { Worktree } from '../../../shared/worktree/types' +import { useAppStore } from '../../../store/index' +import { getDefaultSettings } from '../../../../../shared/constants' +import type { ExecutionHostId } from '../../../../../shared/execution-host' +import type { WorktreeLineage } from '../../../../../shared/worktree/lineage-types' +import type { Worktree } from '../../../../../shared/worktree/types' import type { ResumeCandidate, ResumeFailure } from './native-chat-resume-on-restart-grouping' export function worktree(name: string, overrides: Partial = {}): Worktree { diff --git a/src/renderer/src/components/native-chat-resume-workspace-display.ts b/src/renderer/src/components/native-chat-recovery/tree/native-chat-resume-workspace-display.ts similarity index 97% rename from src/renderer/src/components/native-chat-resume-workspace-display.ts rename to src/renderer/src/components/native-chat-recovery/tree/native-chat-resume-workspace-display.ts index ec3b1f776dd..961053d856e 100644 --- a/src/renderer/src/components/native-chat-resume-workspace-display.ts +++ b/src/renderer/src/components/native-chat-recovery/tree/native-chat-resume-workspace-display.ts @@ -3,7 +3,7 @@ import { resolveWorktreeDisplayName } from '@/lib/worktree-default-display-name' import { getWorktreePathBasenameFromId, splitWorktreeIdForFilesystem -} from '../../../shared/worktree/id' +} from '../../../../../shared/worktree/id' import type { ResumeRepoGroup, ResumeWorkspaceGroup diff --git a/src/renderer/src/components/native-chat-resume-workspace-lookups.ts b/src/renderer/src/components/native-chat-recovery/tree/native-chat-resume-workspace-lookups.ts similarity index 92% rename from src/renderer/src/components/native-chat-resume-workspace-lookups.ts rename to src/renderer/src/components/native-chat-recovery/tree/native-chat-resume-workspace-lookups.ts index 7e18c1f61ec..9de3a406497 100644 --- a/src/renderer/src/components/native-chat-resume-workspace-lookups.ts +++ b/src/renderer/src/components/native-chat-recovery/tree/native-chat-resume-workspace-lookups.ts @@ -2,7 +2,7 @@ import { useMemo } from 'react' import { getCyclicProjectedWorktreeLineageIds, getSidebarLineageAncestors -} from '../../../shared/sidebar-list/worktree-lineage-projection' +} from '../../../../../shared/sidebar-list/worktree-lineage-projection' import { getIndexedAllWorktrees, getIndexedWorktreeMap @@ -11,9 +11,9 @@ import { getWorktreeOnHostFromState } from '@/store/workspaces/selection/worktre import { composeWorktreeHostIdentity, getWorktreeHostIdentity -} from '../../../shared/worktree/host-qualified-identity' -import type { ExecutionHostId } from '../../../shared/execution-host' -import { useAppStore } from '../store' +} from '../../../../../shared/worktree/host-qualified-identity' +import type { ExecutionHostId } from '../../../../../shared/execution-host' +import { useAppStore } from '../../../store/index' import type { ResumeWorkspaceGroup } from './native-chat-resume-on-restart-grouping' import { selectWorktreesByRepo } from '@/store/workspaces/catalog/workspace-record-fields' diff --git a/src/renderer/src/components/native-chat-restart-offer-gate.ts b/src/renderer/src/components/native-chat-restart-offer-gate.ts deleted file mode 100644 index 3c890d7ce65..00000000000 --- a/src/renderer/src/components/native-chat-restart-offer-gate.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { useLocalStructuredChatsInUse } from '@/runtime/native-chat/local-sessions/local-structured-chats' - -/** Ask only when local structured chats are enabled or present; unused installations must not start a host for an empty offer. */ -export function useNativeChatRestartOfferEnabled(): boolean { - return useLocalStructuredChatsInUse() -} diff --git a/src/renderer/src/components/native-chat/appearance/use-native-chat-font-size.ts b/src/renderer/src/components/native-chat/appearance/use-native-chat-font-size.ts index 2cd2d46aa39..d18ff409dea 100644 --- a/src/renderer/src/components/native-chat/appearance/use-native-chat-font-size.ts +++ b/src/renderer/src/components/native-chat/appearance/use-native-chat-font-size.ts @@ -2,9 +2,9 @@ import { useEffect, type RefObject } from 'react' import { chatFontSizeActionForEvent } from './native-chat-font-size' import { writeNativeChatFontSize } from './native-chat-font-size-write' import { isWebClientLocation } from '@/lib/web-client-location' -import { getShortcutPlatform } from '@/lib/shortcut-platform' +import { getShortcutPlatform } from '@/lib/keyboard/shortcut-platform' import { useAppStore } from '../../../store/index' -import { addAppKeyListener, removeAppKeyListener } from '@/lib/app-keyboard-router' +import { addAppKeyListener, removeAppKeyListener } from '@/lib/keyboard/app-keyboard-router' export function useNativeChatFontSize( enabled: boolean, diff --git a/src/renderer/src/components/native-chat/attachments/chips/native-chat-pending-attachment-cache.ts b/src/renderer/src/components/native-chat/attachments/chips/native-chat-pending-attachment-cache.ts index 19969a5efc5..4be8893b156 100644 --- a/src/renderer/src/components/native-chat/attachments/chips/native-chat-pending-attachment-cache.ts +++ b/src/renderer/src/components/native-chat/attachments/chips/native-chat-pending-attachment-cache.ts @@ -22,12 +22,9 @@ import { import { resolveNativeChatComposerDraftOwner } from '../../drafts/native-chat-composer-draft-memory' import { chipScopes, - hasNativeChatPendingAttachment, nativeChatPendingAttachmentScope } from './native-chat-pending-attachment-scopes' -export { hasNativeChatPendingAttachment, nativeChatPendingAttachmentScope } - const EMPTY: readonly NativeChatComposerImageAttachment[] = Object.freeze([]) const pendingCache = new Map() const pendingOwners = new Map() @@ -116,7 +113,7 @@ export function takeNativeChatPendingAttachment( scopeKey: string, id: string ): NativeChatComposerImageAttachment | undefined { - scopeKey = pendingDestination(scopeKey, id) + scopeKey = nativeChatPendingAttachmentScope(scopeKey, id) const current = nativeChatPendingAttachmentSnapshot(scopeKey) const taken = current.find((attachment) => attachment.id === id && attachment.pending) if (taken) { @@ -187,10 +184,6 @@ export function moveLaunchPendingAttachmentListeners(from: string, to: string): listeners.set(to, target) } -function pendingDestination(scope: string, id: string): string { - return currentDraftScope(chipScopes.get(id) ?? scope) -} - /** Persist completed images in attachment order; return false if already removed. */ export function settleNativeChatPendingAttachment( scopeKey: string, @@ -198,7 +191,7 @@ export function settleNativeChatPendingAttachment( path: string, connectionId?: string | null ): boolean { - scopeKey = pendingDestination(scopeKey, id) + scopeKey = nativeChatPendingAttachmentScope(scopeKey, id) const current = nativeChatPendingAttachmentSnapshot(scopeKey) const index = current.findIndex((chip) => chip.id === id && chip.pending) if (index === -1) { @@ -238,7 +231,9 @@ export function settleNativeChatPendingAttachmentReferences( insertAtCaret?: (paths: string[]) => void ): boolean { const originalScope = currentDraftScope(scopeKey) - scopeKey = references.length ? pendingDestination(scopeKey, references[0].id) : scopeKey + scopeKey = references.length + ? nativeChatPendingAttachmentScope(scopeKey, references[0].id) + : scopeKey const owner = pendingOwners.get(scopeKey) const paths = references.flatMap(({ id, path }) => takeNativeChatPendingAttachment(scopeKey, id) ? [path] : [] diff --git a/src/renderer/src/components/native-chat/attachments/chips/use-native-chat-composer-attachments.ts b/src/renderer/src/components/native-chat/attachments/chips/use-native-chat-composer-attachments.ts index ead5779b555..ac55d01b4e9 100644 --- a/src/renderer/src/components/native-chat/attachments/chips/use-native-chat-composer-attachments.ts +++ b/src/renderer/src/components/native-chat/attachments/chips/use-native-chat-composer-attachments.ts @@ -20,9 +20,9 @@ import { clearNativeChatPendingAttachments, settleNativeChatPendingAttachment, settleNativeChatPendingAttachmentReferences, - nativeChatPendingAttachmentSnapshot, - nativeChatPendingAttachmentScope + nativeChatPendingAttachmentSnapshot } from './native-chat-pending-attachment-cache' +import { nativeChatPendingAttachmentScope } from './native-chat-pending-attachment-scopes' import { useNativeChatComposerImageChips } from './use-native-chat-composer-image-chips' import { createNonSecureContextUuid as createUuidV4 } from '../../../../../../shared/non-secure-context-uuid' import { diff --git a/src/renderer/src/components/native-chat/attachments/intake/native-chat-composer-workspace-file-drop.test.tsx b/src/renderer/src/components/native-chat/attachments/intake/native-chat-composer-workspace-file-drop.test.tsx index 82ae4618f88..7a31994beb7 100644 --- a/src/renderer/src/components/native-chat/attachments/intake/native-chat-composer-workspace-file-drop.test.tsx +++ b/src/renderer/src/components/native-chat/attachments/intake/native-chat-composer-workspace-file-drop.test.tsx @@ -16,7 +16,7 @@ import type { NativeChatComposerInput } from '../../composer/editor/native-chat- import { NativeChatComposerField } from '../../composer/editor/NativeChatComposerField' import { useNativeChatComposerAttachments } from '../chips/use-native-chat-composer-attachments' import { useNativeChatWorkspaceFileDrop } from './use-native-chat-workspace-file-drop' -import { useImeEnterGestureOwnership } from '@/lib/ime-composition-keyboard-event' +import { useImeEnterGestureOwnership } from '@/lib/text-input/ime/ime-composition-keyboard-event' import type * as RemoteRuntimePtyIdModule from '../../../../../../shared/remote-runtime/remote-runtime-pty-id' const testState: { @@ -282,28 +282,6 @@ describe('native chat workspace file drops', () => { }) // Explorer paths from the same runtime host are readable by its agent. - it('accepts a same-host drop on a remote runtime target and refuses a foreign one', () => { - testState.executionHostId = 'runtime:env-1' - testState.ownerKind = 'remote' - testState.targetIsRemoteRuntime = true - const view = render() - dispatchDragEvent( - 'drop', - editor(), - internalTransfer(['/env/owned.ts'], { executionHostId: 'runtime:env-1' }) - ) - expect(screen.getByTestId('draft').textContent).toBe('@/env/owned.ts ') - view.unmount() - - render() - dispatchDragEvent( - 'drop', - editor(), - internalTransfer(['/elsewhere/foreign.ts'], { executionHostId: 'runtime:env-2' }) - ) - expect(screen.getByTestId('draft').textContent).toBe('') - expect(screen.getByText('Files can only be attached to their source workspace.')).toBeTruthy() - }) it('queues an internal reference until composition settles without stealing focus', () => { render() @@ -319,30 +297,5 @@ describe('native chat workspace file drops', () => { expect(document.activeElement).toBe(input) }) - it('rejects an IME-queued path when its execution host changes before composition settles', () => { - render() - const input = editor() - fireEvent.compositionStart(input) - dispatchDragEvent('drop', input, internalTransfer(['/repo/a.ts'])) - - testState.executionHostId = 'ssh:replacement' - fireEvent.compositionEnd(input, { data: '' }) - - expect(screen.getByTestId('draft').textContent).toBe('preedit') - expect(screen.getByText('Files can only be attached to their source workspace.')).toBeTruthy() - }) - // Recheck current workspace after IME; comparing only captured identities would miss a pane retarget. - it('rejects an IME-queued path when the pane changes workspace before settling', () => { - const view = render() - const input = editor() - fireEvent.compositionStart(input) - dispatchDragEvent('drop', input, internalTransfer(['/repo/a.ts'])) - - view.rerender() - fireEvent.compositionEnd(input, { data: '' }) - - expect(screen.getByTestId('draft').textContent).toBe('preedit') - expect(screen.getByText('Files can only be attached to their source workspace.')).toBeTruthy() - }) }) diff --git a/src/renderer/src/components/native-chat/attachments/intake/native-chat-local-attachment-drop.ts b/src/renderer/src/components/native-chat/attachments/intake/native-chat-local-attachment-drop.ts index afd7dd0780c..0944765bb72 100644 --- a/src/renderer/src/components/native-chat/attachments/intake/native-chat-local-attachment-drop.ts +++ b/src/renderer/src/components/native-chat/attachments/intake/native-chat-local-attachment-drop.ts @@ -10,7 +10,7 @@ import { import type { NativeChatAttachmentOwner } from '../uploads/native-chat-attachment-owner' import { stageNativeChatDraftFile } from '../../drafts/native-chat-draft-attachment-staging' import { isImageDropPath } from '../../../../../../shared/image-path-paste' -import { nativeChatPendingAttachmentScope } from '../chips/native-chat-pending-attachment-cache' +import { nativeChatPendingAttachmentScope } from '../chips/native-chat-pending-attachment-scopes' import type { NativeChatAttachmentFocusOptions, NativeChatResolvedPathOptions diff --git a/src/renderer/src/components/native-chat/composer/NativeChatComposerPane.tsx b/src/renderer/src/components/native-chat/composer/NativeChatComposerPane.tsx index 631d6dc531e..442257a4319 100644 --- a/src/renderer/src/components/native-chat/composer/NativeChatComposerPane.tsx +++ b/src/renderer/src/components/native-chat/composer/NativeChatComposerPane.tsx @@ -22,7 +22,7 @@ import { useNativeChatComposerSessionOptions } from './controls/use-native-chat- import { useNativeChatDictation } from './editor/use-native-chat-dictation' import { useNativeChatPtyComposerSend } from './submit/use-native-chat-pty-composer-send' import { useNativeChatStructuredComposerSend } from './submit/use-native-chat-structured-composer-send' -import { useImeEnterGestureOwnership } from '@/lib/ime-composition-keyboard-event' +import { useImeEnterGestureOwnership } from '@/lib/text-input/ime/ime-composition-keyboard-event' import { useNativeChatComposerAppMenuSelection } from './focus/use-native-chat-composer-app-menu-selection' import { useNativeChatComposerSubmit } from './submit/use-native-chat-composer-submit' import { useNativeChatComposerCommandPicker } from './pickers/use-native-chat-composer-command-picker' diff --git a/src/renderer/src/components/native-chat/composer/editor/NativeChatComposerField.tsx b/src/renderer/src/components/native-chat/composer/editor/NativeChatComposerField.tsx index d7e68a3ec87..de8e86c21cb 100644 --- a/src/renderer/src/components/native-chat/composer/editor/NativeChatComposerField.tsx +++ b/src/renderer/src/components/native-chat/composer/editor/NativeChatComposerField.tsx @@ -3,7 +3,7 @@ import { notifyNativeChatComposerState } from '../focus/native-chat-composer-mou import type { NativeChatComposerInput } from './native-chat-composer-input' import { useCallback, useLayoutEffect, useMemo, useRef } from 'react' import { flushSync } from 'react-dom' -import type { useImeEnterGestureOwnership } from '@/lib/ime-composition-keyboard-event' +import type { useImeEnterGestureOwnership } from '@/lib/text-input/ime/ime-composition-keyboard-event' import { cn } from '@/lib/utils' import type { ComposerAutocomplete } from '../../../../../../shared/native-chat/input/native-chat-composer-autocomplete' import type { NativeChatPickerItem } from '../../../../../../shared/native-chat/input/native-chat-picker-items' diff --git a/src/renderer/src/components/native-chat/composer/editor/native-chat-composer-composition.test.tsx b/src/renderer/src/components/native-chat/composer/editor/native-chat-composer-composition.test.tsx index 8b4e1ccc070..edfacb95d12 100644 --- a/src/renderer/src/components/native-chat/composer/editor/native-chat-composer-composition.test.tsx +++ b/src/renderer/src/components/native-chat/composer/editor/native-chat-composer-composition.test.tsx @@ -1,4 +1,4 @@ -import { endComposition } from '@/lib/ime-composition-event.test-support' +import { endComposition } from '@/lib/text-input/ime/ime-composition-event.test-support' import { changePrompt, promptValue } from './native-chat-prompt-editor.test-support' // @vitest-environment happy-dom @@ -22,7 +22,7 @@ import { NativeChatComposerField, type NativeChatComposerFieldProps } from './NativeChatComposerField' -import { useImeEnterGestureOwnership } from '@/lib/ime-composition-keyboard-event' +import { useImeEnterGestureOwnership } from '@/lib/text-input/ime/ime-composition-keyboard-event' import { TooltipProvider } from '@/components/ui/tooltip' import { useNativeChatDraft } from '../../drafts/use-native-chat-draft' import { useNativeChatDraftEvents } from './use-native-chat-draft-events' diff --git a/src/renderer/src/components/native-chat/composer/focus/native-chat-root-key-routing.ts b/src/renderer/src/components/native-chat/composer/focus/native-chat-root-key-routing.ts index 2ee860e95ff..83acde598b8 100644 --- a/src/renderer/src/components/native-chat/composer/focus/native-chat-root-key-routing.ts +++ b/src/renderer/src/components/native-chat/composer/focus/native-chat-root-key-routing.ts @@ -1,4 +1,4 @@ -import { getShortcutPlatform } from '@/lib/shortcut-platform' +import { getShortcutPlatform } from '@/lib/keyboard/shortcut-platform' import { findNativeChatPasteField } from '@/components/native-chat/paste/native-chat-paste-request' import type { NativeChatComposerHandle } from '../native-chat-composer-types' import { diff --git a/src/renderer/src/components/native-chat/composer/focus/use-native-chat-composer-app-menu-selection.test.tsx b/src/renderer/src/components/native-chat/composer/focus/use-native-chat-composer-app-menu-selection.test.tsx index 12b1d9e67be..73d21954f4c 100644 --- a/src/renderer/src/components/native-chat/composer/focus/use-native-chat-composer-app-menu-selection.test.tsx +++ b/src/renderer/src/components/native-chat/composer/focus/use-native-chat-composer-app-menu-selection.test.tsx @@ -2,8 +2,8 @@ import { act, cleanup, fireEvent, render } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { AppMenuSelectionAction } from '@/lib/app-menu-selection-actions' -import { useAppMenuSelectionActions } from '@/hooks/useAppMenuSelectionActions' +import type { AppMenuSelectionAction } from '@/lib/app-commands/app-menu-selection-actions' +import { useAppMenuSelectionActions } from '@/hooks/app-commands/useAppMenuSelectionActions' import { useNativeChatComposerAppMenuSelection } from './use-native-chat-composer-app-menu-selection' let appMenuListener: ((action: AppMenuSelectionAction) => void) | null = null diff --git a/src/renderer/src/components/native-chat/composer/focus/use-native-chat-composer-app-menu-selection.ts b/src/renderer/src/components/native-chat/composer/focus/use-native-chat-composer-app-menu-selection.ts index 34d06b7a17a..bf89ce5dd87 100644 --- a/src/renderer/src/components/native-chat/composer/focus/use-native-chat-composer-app-menu-selection.ts +++ b/src/renderer/src/components/native-chat/composer/focus/use-native-chat-composer-app-menu-selection.ts @@ -3,7 +3,7 @@ import { useCallback, useEffect, useRef } from 'react' import { APP_MENU_SELECTION_ACTION_EVENT, type AppMenuSelectionAction -} from '@/lib/app-menu-selection-actions' +} from '@/lib/app-commands/app-menu-selection-actions' export function useNativeChatComposerAppMenuSelection(isComposingOverride?: () => boolean) { const textareaRef = useRef(null) diff --git a/src/renderer/src/components/native-chat/composer/focus/use-native-chat-composer-reveal-focus.ts b/src/renderer/src/components/native-chat/composer/focus/use-native-chat-composer-reveal-focus.ts index 29ac591e558..fea1ce359f4 100644 --- a/src/renderer/src/components/native-chat/composer/focus/use-native-chat-composer-reveal-focus.ts +++ b/src/renderer/src/components/native-chat/composer/focus/use-native-chat-composer-reveal-focus.ts @@ -1,6 +1,6 @@ import { useEffect, useLayoutEffect, useRef, useState, type RefObject } from 'react' -import { shouldPreserveEditableFocus } from '@/lib/editable-focus-preservation' +import { shouldPreserveEditableFocus } from '@/lib/text-input/focus/editable-focus-preservation' import { scheduleNextFrame } from '@/components/terminal-pane/input/ime/terminal-ime-input-context-refresh' import { getOverlayFocusOwners } from '@/lib/overlay-focus-ownership' import type { NativeChatComposerHandle } from '../native-chat-composer-types' diff --git a/src/renderer/src/components/native-chat/composer/submit/native-chat-structured-send-composition-clear.test.tsx b/src/renderer/src/components/native-chat/composer/submit/native-chat-structured-send-composition-clear.test.tsx index 77fab9a8bf6..c799652cd52 100644 --- a/src/renderer/src/components/native-chat/composer/submit/native-chat-structured-send-composition-clear.test.tsx +++ b/src/renderer/src/components/native-chat/composer/submit/native-chat-structured-send-composition-clear.test.tsx @@ -1,4 +1,4 @@ -import { endComposition } from '@/lib/ime-composition-event.test-support' +import { endComposition } from '@/lib/text-input/ime/ime-composition-event.test-support' import { changePrompt, promptValue } from '../editor/native-chat-prompt-editor.test-support' // @vitest-environment happy-dom diff --git a/src/renderer/src/components/native-chat/media/images/NativeChatResolvedView.context-menu.test.tsx b/src/renderer/src/components/native-chat/media/images/NativeChatResolvedView.context-menu.test.tsx index 52b089900f6..eacc080f271 100644 --- a/src/renderer/src/components/native-chat/media/images/NativeChatResolvedView.context-menu.test.tsx +++ b/src/renderer/src/components/native-chat/media/images/NativeChatResolvedView.context-menu.test.tsx @@ -5,7 +5,7 @@ import { isValidElement, type ComponentProps } from 'react' import type { NativeChatCrewFrame } from '../../../orchestration-crew/NativeChatCrewPanel' import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type * as ImageBlobPng from '@/lib/image-blob-png' +import type * as ImageBlobPng from '@/lib/clipboard/images/image-blob-png' import { NativeChatImageAttachmentPreview } from '../../attachments/chips/NativeChatImageAttachmentPreview' import type { NativeChatLiveSession } from '../../session/terminal/read/native-chat-live-session-contract' import type { NativeChatMessage } from '../../../../../../shared/native-chat/native-chat-types' @@ -66,7 +66,7 @@ vi.mock('../../composer/NativeChatComposer', () => ({ vi.mock('@/components/editor/document-images/useLocalImageSrc', () => ({ useLocalImageSrc: () => 'blob:original' })) -vi.mock('@/lib/image-blob-png', async (importOriginal) => ({ +vi.mock('@/lib/clipboard/images/image-blob-png', async (importOriginal) => ({ ...(await importOriginal()), convertImageBlobToPng: mocks.convertImageBlobToPng })) diff --git a/src/renderer/src/components/native-chat/media/images/native-chat-image-copy.ts b/src/renderer/src/components/native-chat/media/images/native-chat-image-copy.ts index 026d3918a10..3d6a84457b0 100644 --- a/src/renderer/src/components/native-chat/media/images/native-chat-image-copy.ts +++ b/src/renderer/src/components/native-chat/media/images/native-chat-image-copy.ts @@ -3,7 +3,7 @@ import { translate } from '@/i18n/i18n' import { clipboardImageCopyErrorDescription, copyImageToClipboard -} from '@/lib/clipboard-image-copy' +} from '@/lib/clipboard/images/clipboard-image-copy' import { isWebClientLocation } from '@/lib/web-client-location' /** The chat context menu offers "Copy image" for elements carrying this attribute. */ diff --git a/src/renderer/src/components/native-chat/messages/notices/NativeChatNoticeRow.tsx b/src/renderer/src/components/native-chat/messages/notices/NativeChatNoticeRow.tsx index b42560e9ed7..d868e7e2662 100644 --- a/src/renderer/src/components/native-chat/messages/notices/NativeChatNoticeRow.tsx +++ b/src/renderer/src/components/native-chat/messages/notices/NativeChatNoticeRow.tsx @@ -9,7 +9,7 @@ import { AlertCircle, AlertTriangle, Info } from 'lucide-react' import { useTranslation } from 'react-i18next' import { useContext, useSyncExternalStore } from 'react' import { NativeChatForkedFromContext } from '../actions/native-chat-fork-context' -import { NativeChatPullRequestEventLine } from '../../NativeChatPullRequestEventLine' +import { NativeChatPullRequestEventLine } from './NativeChatPullRequestEventLine' import { PULL_REQUEST_EVENT_PRESENTATION } from '../../../../../../shared/native-chat/native-chat-pull-request-event' import { NativeChatMarkdown } from '../../markdown/NativeChatMarkdown' import { NativeChatCodeBlock } from '../../markdown/code/NativeChatCodeBlock' diff --git a/src/renderer/src/components/native-chat/NativeChatPullRequestEventLine.fixture.tsx b/src/renderer/src/components/native-chat/messages/notices/NativeChatPullRequestEventLine.fixture.tsx similarity index 91% rename from src/renderer/src/components/native-chat/NativeChatPullRequestEventLine.fixture.tsx rename to src/renderer/src/components/native-chat/messages/notices/NativeChatPullRequestEventLine.fixture.tsx index 493dd2828ad..d02435ad9b6 100644 --- a/src/renderer/src/components/native-chat/NativeChatPullRequestEventLine.fixture.tsx +++ b/src/renderer/src/components/native-chat/messages/notices/NativeChatPullRequestEventLine.fixture.tsx @@ -1,6 +1,6 @@ -import { defineFixture } from '../../component-fixtures/component-fixture' +import { defineFixture } from '../../../../component-fixtures/component-fixture' import { NativeChatPullRequestEventLine } from './NativeChatPullRequestEventLine' -import { NativeChatOpenedPullRequestCard } from './tools/pull-requests/NativeChatOpenedPullRequestCard' +import { NativeChatOpenedPullRequestCard } from '../../tools/pull-requests/NativeChatOpenedPullRequestCard' // Journal keys percent-encode the line's client message id, as the host writes them. const MESSAGE_ID = 'orca:pr-event%3Agithub.com%2Fstablyai%2Forca%233%3A8d5f74b3c0e1d2f3%3A1' diff --git a/src/renderer/src/components/native-chat/NativeChatPullRequestEventLine.tsx b/src/renderer/src/components/native-chat/messages/notices/NativeChatPullRequestEventLine.tsx similarity index 89% rename from src/renderer/src/components/native-chat/NativeChatPullRequestEventLine.tsx rename to src/renderer/src/components/native-chat/messages/notices/NativeChatPullRequestEventLine.tsx index 3df1185b724..0a93bbea851 100644 --- a/src/renderer/src/components/native-chat/NativeChatPullRequestEventLine.tsx +++ b/src/renderer/src/components/native-chat/messages/notices/NativeChatPullRequestEventLine.tsx @@ -1,19 +1,19 @@ import { CircleCheck, CircleX } from 'lucide-react' import { useTranslate } from '@/i18n/use-translate' import { cn } from '@/lib/utils' -import type { NativeChatTextBlock } from '../../../../shared/native-chat/native-chat-types' +import type { NativeChatTextBlock } from '../../../../../../shared/native-chat/native-chat-types' import { parsePullRequestEventItemId, pullRequestEventCommitUrl, pullRequestEventPullRequestUrl, pullRequestEventShortSha -} from '../../../../shared/native-chat/native-chat-pull-request-event' -import { NATIVE_CHAT_CI_RESULT_LINES_SETTING_SECTION_ID } from '../settings/chat/native-chat-ci-result-lines-setting-target' +} from '../../../../../../shared/native-chat/native-chat-pull-request-event' +import { NATIVE_CHAT_CI_RESULT_LINES_SETTING_SECTION_ID } from '../../../settings/chat/native-chat-ci-result-lines-setting-target' import { NativeChatCardHideButton, NATIVE_CHAT_HIDEABLE_CARD_CLASS -} from './messages/disclosure/NativeChatCardHideButton' -import { useNativeChatLinkClick } from './links/actions/native-chat-link-click-context' +} from '../disclosure/NativeChatCardHideButton' +import { useNativeChatLinkClick } from '../../links/actions/native-chat-link-click-context' function LineLink({ href, diff --git a/src/renderer/src/components/native-chat/pane/actions/use-native-chat-context-menu.tsx b/src/renderer/src/components/native-chat/pane/actions/use-native-chat-context-menu.tsx index daa01c30206..b8571437e80 100644 --- a/src/renderer/src/components/native-chat/pane/actions/use-native-chat-context-menu.tsx +++ b/src/renderer/src/components/native-chat/pane/actions/use-native-chat-context-menu.tsx @@ -26,8 +26,8 @@ import { import { isMacPlatform } from '../routing/native-chat-shortcut' import { formatOptionalPrimaryShortcutLabel } from '@/hooks/useShortcutLabel' import { useAppStore } from '@/store' -import { isEditableTarget } from '@/lib/editable-target' -import { findEditablePrimarySelectionPasteTarget } from '@/lib/primary-selection-paste' +import { isEditableTarget } from '@/lib/text-input/focus/editable-target' +import { findEditablePrimarySelectionPasteTarget } from '@/lib/clipboard/primary-selection/primary-selection-paste' import { NativeChatCopyOrcaSessionIdMenuItem } from './NativeChatCopyOrcaSessionIdMenuItem' import { NativeChatSelectionQuote } from '../../selection-quotes/NativeChatSelectionQuote' import { diff --git a/src/renderer/src/components/native-chat/pane/actions/use-structured-native-chat-pane-commands.tsx b/src/renderer/src/components/native-chat/pane/actions/use-structured-native-chat-pane-commands.tsx index aea76ac4e44..f65490cd27c 100644 --- a/src/renderer/src/components/native-chat/pane/actions/use-structured-native-chat-pane-commands.tsx +++ b/src/renderer/src/components/native-chat/pane/actions/use-structured-native-chat-pane-commands.tsx @@ -1,7 +1,7 @@ import { useCallback, useMemo, type KeyboardEventHandler, type RefObject } from 'react' import { useAppStore } from '@/store' import { formatShortcutLabel } from '@/hooks/useShortcutLabel' -import { getShortcutPlatform } from '@/lib/shortcut-platform' +import { getShortcutPlatform } from '@/lib/keyboard/shortcut-platform' import { isOrcaSessionId } from '../../../../../../shared/orca-session-address' import { resolveStructuredSessionOrcaSessionId } from '../../../../runtime/native-chat/orchestration/structured-session-orca-session-id' import type { NativeChatStructuredViewProps } from '../../native-chat-view-types' diff --git a/src/renderer/src/components/native-chat/pane/routing/native-chat-pane-shortcut.ts b/src/renderer/src/components/native-chat/pane/routing/native-chat-pane-shortcut.ts index 734be6b7023..ce817e7ad28 100644 --- a/src/renderer/src/components/native-chat/pane/routing/native-chat-pane-shortcut.ts +++ b/src/renderer/src/components/native-chat/pane/routing/native-chat-pane-shortcut.ts @@ -1,5 +1,5 @@ import type { KeyboardEvent } from 'react' -import { getShortcutPlatform } from '@/lib/shortcut-platform' +import { getShortcutPlatform } from '@/lib/keyboard/shortcut-platform' import { keybindingMatchesAction } from '../../../../../../shared/keybindings/matching' import type { KeybindingInput, diff --git a/src/renderer/src/components/native-chat/pane/routing/use-native-chat-toggle-shortcut.ts b/src/renderer/src/components/native-chat/pane/routing/use-native-chat-toggle-shortcut.ts index 03082c34030..9d71d93bf52 100644 --- a/src/renderer/src/components/native-chat/pane/routing/use-native-chat-toggle-shortcut.ts +++ b/src/renderer/src/components/native-chat/pane/routing/use-native-chat-toggle-shortcut.ts @@ -17,14 +17,14 @@ import { resolveNativeChatTabAgentEvidence } from '../../../tab-bar/native-chat- import { canToggleNativeChat } from '../../../../../../shared/native-chat/native-chat-availability' import { isNativeChatTranscriptLocalReadable } from '../../../../../../shared/native-chat/transcript/native-chat-transcript-readability' import { matchesNativeChatToggleShortcut } from './native-chat-shortcut' -import { getShortcutPlatform } from '@/lib/shortcut-platform' +import { getShortcutPlatform } from '@/lib/keyboard/shortcut-platform' import { getConnectionIdFromState } from '@/lib/workspace/ownership/connection-owner-resolution' import { resolveKeyboardWorkspaceId } from '@/lib/floating-workspace-terminal-actions' import { isNativeChatTabWideFallbackSafe, resolveNativeChatActiveLayoutLeafId } from './native-chat-leaf-routing' -import { addAppKeyListener, removeAppKeyListener } from '@/lib/app-keyboard-router' +import { addAppKeyListener, removeAppKeyListener } from '@/lib/keyboard/app-keyboard-router' export function resolveNativeChatToggleShortcutDetectedAgent({ terminalTabId, diff --git a/src/renderer/src/components/native-chat/paste/use-native-chat-paste-bridge.ts b/src/renderer/src/components/native-chat/paste/use-native-chat-paste-bridge.ts index feaed342d9f..d24ffa83781 100644 --- a/src/renderer/src/components/native-chat/paste/use-native-chat-paste-bridge.ts +++ b/src/renderer/src/components/native-chat/paste/use-native-chat-paste-bridge.ts @@ -2,17 +2,17 @@ import { nativeChatComposerDestination } from '../composer/focus/native-chat-com import { useCallback, useEffect, type RefObject } from 'react' import { toast } from 'sonner' -import { isEditableTarget } from '@/lib/editable-target' +import { isEditableTarget } from '@/lib/text-input/focus/editable-target' import { NativeChatPasteRequest, findNativeChatPasteField, nativeChatPasteUnavailableNotice, NATIVE_CHAT_PASTE_REQUEST_EVENT } from '@/components/native-chat/paste/native-chat-paste-request' -import { APP_MENU_PASTE_EVENT } from '@/lib/app-menu-paste' +import { APP_MENU_PASTE_EVENT } from '@/lib/clipboard/menu/app-menu-paste' import { isWebClientLocation } from '@/lib/web-client-location' -import { pasteTextIntoTextControl } from '@/lib/text-control-paste' -import { TEXT_CONTROL_PASTE_MAX_BYTES } from '@/lib/text-control-paste-model' +import { pasteTextIntoTextControl } from '@/lib/text-input/paste/text-control-paste' +import { TEXT_CONTROL_PASTE_MAX_BYTES } from '@/lib/text-input/paste/text-control-paste-model' import type { NativeChatComposerHandle } from '../composer/native-chat-composer-types' type NativeChatPasteBridgeRefs = { diff --git a/src/renderer/src/components/native-chat/paste/use-native-chat-paste-lifetime.ts b/src/renderer/src/components/native-chat/paste/use-native-chat-paste-lifetime.ts index 57dd412fab2..0fbe46af218 100644 --- a/src/renderer/src/components/native-chat/paste/use-native-chat-paste-lifetime.ts +++ b/src/renderer/src/components/native-chat/paste/use-native-chat-paste-lifetime.ts @@ -2,7 +2,7 @@ import { useCallback, useLayoutEffect, useMemo, useRef } from 'react' import { basename } from '@/lib/path' import type { NativeChatAttachmentOwner } from '../attachments/uploads/native-chat-attachment-owner' import type { NativeChatReservedAttachment } from '../attachments/intake/native-chat-session-attachment-drop' -import { hasNativeChatPendingAttachment } from '../attachments/chips/native-chat-pending-attachment-cache' +import { hasNativeChatPendingAttachment } from '../attachments/chips/native-chat-pending-attachment-scopes' /** Composer owns preview URLs; the draft attachment cache owns pending uploads. */ export function useNativeChatPasteLifetime(args: { diff --git a/src/renderer/src/components/native-chat/prompts/NativeChatQuestionCard.tsx b/src/renderer/src/components/native-chat/prompts/NativeChatQuestionCard.tsx index 0f2e27541f1..36ce3845a18 100644 --- a/src/renderer/src/components/native-chat/prompts/NativeChatQuestionCard.tsx +++ b/src/renderer/src/components/native-chat/prompts/NativeChatQuestionCard.tsx @@ -1,4 +1,4 @@ -import { ImeTextarea } from '@/lib/ime-text-field' +import { ImeTextarea } from '@/lib/text-input/ime/ime-text-field' import { Textarea } from '@/components/ui/textarea' import { useRef, useState, type RefObject } from 'react' import { Check, Pencil, X } from 'lucide-react' @@ -13,7 +13,7 @@ import { NativeChatPromptCollapseToggle } from './NativeChatPromptCollapse' import { useNativeChatPromptCardFocus } from './use-native-chat-prompt-card-focus' import { useNativeChatQuestionAutoAdvance } from './use-native-chat-question-auto-advance' import { useNativeChatQuestionKeys } from './use-native-chat-question-keys' -import { isEditableTarget } from '@/lib/editable-target' +import { isEditableTarget } from '@/lib/text-input/focus/editable-target' import type { AgentJournalFreeTextInput } from '../../../../../shared/agent-session/journal/agent-session-journal-types' import { askAcceptsEmptyAnswer, diff --git a/src/renderer/src/components/native-chat/prompts/use-native-chat-question-keys.ts b/src/renderer/src/components/native-chat/prompts/use-native-chat-question-keys.ts index 39f41cdb290..41799448529 100644 --- a/src/renderer/src/components/native-chat/prompts/use-native-chat-question-keys.ts +++ b/src/renderer/src/components/native-chat/prompts/use-native-chat-question-keys.ts @@ -1,5 +1,5 @@ import { useEffect, useEffectEvent, type RefObject } from 'react' -import { isEditableTarget } from '@/lib/editable-target' +import { isEditableTarget } from '@/lib/text-input/focus/editable-target' import { isInNativeChatPaneOf } from './use-native-chat-prompt-card-focus' /** Keep question shortcuts scoped to its pane, including when a replaced row drops focus. */ diff --git a/src/renderer/src/components/native-chat/usage/NativeChatInterruptedContinue.test.tsx b/src/renderer/src/components/native-chat/usage/NativeChatInterruptedContinue.test.tsx deleted file mode 100644 index a12db361c21..00000000000 --- a/src/renderer/src/components/native-chat/usage/NativeChatInterruptedContinue.test.tsx +++ /dev/null @@ -1,371 +0,0 @@ -// @vitest-environment happy-dom -import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react' -import { useRef, useState } from 'react' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { NativeChatStructuredComposerTransport } from '@/components/native-chat/composer/submit/native-chat-structured-composer-transport' -import { useStructuredAgentSessionSends } from '@/components/native-chat/session/structured/delivery/use-structured-agent-session-sends' -import { - getStructuredAgentSessionSendNotice, - setStructuredAgentSessionSendNotice -} from '@/components/native-chat/session/structured/delivery/structured-agent-session-pending-sends' -import { resetStructuredAgentSessionSendsForTests } from '@/components/native-chat/session/structured/delivery/structured-agent-session-message-sender' -import { clearNativeChatComposerDraftsForTests } from '@/components/native-chat/drafts/native-chat-draft-reset' -import { - readNativeChatComposerDraft, - updateNativeChatComposerDraft -} from '@/components/native-chat/drafts/native-chat-composer-draft-store' -import { structuredAgentSessionDraftScopeKey } from '@/components/native-chat/drafts/native-chat-draft-scope' -import { emitNativeChatMessageSent } from '@/components/native-chat/telemetry/native-chat-telemetry' -import { TooltipProvider } from '@/components/ui/tooltip' -import { agentSessionFailureWords } from '../../../../../shared/agent-session/failures/agent-session-failure-words' -import { agentJournalItemKey } from '../../../../../shared/agent-session/journal/agent-session-journal-item-key' -import type { - AgentJournalRenderItem, - AgentJournalSubmission -} from '../../../../../shared/agent-session/journal/agent-session-journal-types' -import type { RuntimeClientTarget } from '../../../../../shared/runtime-client-target' -import { - NativeChatInterruptedContinueButton, - useNativeChatInterruptedContinuation -} from '@/components/native-chat/usage/NativeChatInterruptedContinue' -import '@testing-library/jest-dom/vitest' - -const mocks = vi.hoisted(() => { - const hostLabel = (): string | null => 'studio-mac' - const capability = (): 'unknown' | 'supported' | 'unsupported' => 'supported' - const resuming: readonly string[] = [] - return { - call: vi.fn(), - capability: capability(), - hostLabel: hostLabel(), - resuming, - launchPending: false - } -}) - -vi.mock('@/components/native-chat/telemetry/native-chat-telemetry', () => ({ - emitNativeChatMessageSent: vi.fn() -})) - -vi.mock('@/i18n/i18n', () => ({ translate: (_key: string, fallback: string) => fallback })) -vi.mock('@/runtime/rpc/compatibility/runtime-compatibility-probe', () => ({ - ensureRuntimeEnvironmentCompatible: async () => {} -})) -vi.mock('@/lib/agent-session/launch/structured-agent-session-launch-label', () => ({ - structuredAgentLabel: () => 'Codex' -})) -vi.mock('../../../runtime/native-chat/requests/structured-agent-session-call', () => ({ - callStructuredAgentSession: mocks.call -})) -vi.mock('@/runtime/native-chat/host-capabilities/structured-agent-session-host-capability', () => ({ - useStructuredAgentSessionHostCapabilityState: () => mocks.capability -})) -vi.mock('../../native-chat-restart-runtimes', () => ({ - useNativeChatTargetRestartResuming: () => mocks.resuming -})) -vi.mock('../../native-chat-launch-resume-decision', () => ({ - useNativeChatLaunchResumePending: () => mocks.launchPending -})) -vi.mock('@/components/native-chat/pane/ownership/use-structured-agent-session-host-label', () => ({ - useStructuredAgentSessionHostLabel: () => mocks.hostLabel -})) - -const TURN = agentJournalItemKey({ provider: 'codex', threadId: 't', turnId: 'cut', ordinal: 1 }) -const PAIRED: RuntimeClientTarget = { kind: 'environment', environmentId: 'studio-mac' } - -const cutChat: AgentJournalRenderItem[] = [ - { - itemId: TURN, - revision: 2, - sequence: 1, - observedAt: 1, - body: { kind: 'turn', turnId: 'cut', state: 'interrupted', startedAt: 1, completedAt: 5 } - }, - { - itemId: agentJournalItemKey({ provider: 'orca', clientMessageId: 'stale-session:s:death-1-5' }), - revision: 1, - sequence: 2, - observedAt: 6, - turnScope: { kind: 'turn', turnItemId: TURN }, - body: { kind: 'status', text: 'Codex stopped.', tone: 'error', orcaStop: { cause: 'crash' } } - } -] - -const optionsSurface: NativeChatStructuredComposerTransport['optionsSurface'] = { - getSnapshot: () => [], - setOption: async () => ({ snapshot: [] }), - invokeAction: async () => ({ snapshot: [] }), - subscribe: () => () => {} -} - -type Props = { - journalItems?: readonly AgentJournalRenderItem[] - submissions?: readonly Pick[] - isWorking?: boolean - target?: RuntimeClientTarget - transport?: NativeChatStructuredComposerTransport - pending?: Parameters[0]['pending'] -} - -function Harness(props: Props): React.JSX.Element { - const composerRef = useRef(null) - // The chat's own composer error, as NativeChatStructuredSession holds it. - const [composerError, setComposerError] = useState(null) - const continuation = useNativeChatInterruptedContinuation({ - reportComposerError: setComposerError, - focusComposer: () => { - composerRef.current?.focus() - }, - target: props.target ?? PAIRED, - agent: 'codex', - sessionId: 'session-1', - journalItems: props.journalItems ?? cutChat, - submissions: props.submissions ?? [], - isWorking: props.isWorking ?? false, - pending: props.pending ?? [], - transport: props.transport ?? { - capturePresentation: () => () => true, - send: () => true, - dispatchCommand: async () => ({ handled: false, accepted: false, error: null }), - optionsSurface, - optionSnapshot: [], - onError: () => {}, - runtime: 'remote', - sessionId: 'session-1', - runtimeEnvironmentId: 'studio-mac' - } - }) - return ( - - - {continuation.view.offeredTurnItemId ?? 'none'} - {String(continuation.view.continueAvailable)} - {String(continuation.view.remoteHost)} - {composerError ?? continuation.continueError?.text ?? 'none'} - - {continuation.view.usageLimitTurnItemId === null ? null : ( - - )} - {continuation.view.offeredTurnItemId === null ? null : ( - - )} - - ) -} - -const continueButton = () => screen.queryByRole('button', { name: 'Continue' }) -const FAILED = "Couldn't continue this chat. Try again, or send a message." -const ATTACHMENTS = 'Remove attachments before using a chat-session command.' - -beforeEach(() => { - mocks.capability = 'supported' - mocks.resuming = [] - mocks.launchPending = false - mocks.hostLabel = 'studio-mac' - mocks.call.mockReset() -}) - -afterEach(() => { - cleanup() - resetStructuredAgentSessionSendsForTests() - clearNativeChatComposerDraftsForTests() -}) - -describe('Continue on a reply an Orca stop cut off', () => { - it('replaces an older composer error with its own failure, and clears it when it goes through', async () => { - mocks.call.mockResolvedValueOnce({ outcome: 'refused', reason: 'agent_session_conflict' }) - mocks.call.mockResolvedValueOnce({ outcome: 'pending' }) - render() - fireEvent.click(screen.getByRole('button', { name: 'compose' })) - expect(screen.getByTestId('error')).toHaveTextContent(ATTACHMENTS) - - fireEvent.click(continueButton()!) - await waitFor(() => expect(continueButton()).toBeInTheDocument()) - expect(screen.getByTestId('error')).toHaveTextContent(FAILED) - - fireEvent.click(screen.getByRole('button', { name: 'compose' })) - expect(screen.getByTestId('error')).toHaveTextContent(ATTACHMENTS) - fireEvent.click(continueButton()!) - await waitFor(() => expect(mocks.call).toHaveBeenCalledTimes(2)) - expect(screen.getByTestId('error')).toHaveTextContent('none') - }) - - it('drops the line once the chat was continued, here or by another client', async () => { - // The answer was lost after the host accepted it. - mocks.call.mockRejectedValue(new Error('timed out')) - const { rerender } = render() - fireEvent.click(continueButton()!) - await waitFor(() => expect(screen.getByTestId('error')).toHaveTextContent(FAILED)) - - rerender() - - expect(screen.getByTestId('error')).toHaveTextContent('none') - }) - - it('leaves no line when a retry finds the chat already continued', async () => { - mocks.call.mockRejectedValueOnce(new Error('timed out')) - mocks.call.mockResolvedValueOnce({ outcome: 'superseded' }) - render() - fireEvent.click(continueButton()!) - await waitFor(() => expect(screen.getByTestId('error')).toHaveTextContent(FAILED)) - - fireEvent.click(continueButton()!) - - await waitFor(() => expect(mocks.call).toHaveBeenCalledTimes(2)) - expect(screen.getByTestId('error')).toHaveTextContent('none') - }) -}) - -it('keeps the draft through Continue admission, refusal, uncertainty and recorded-before-journal', async () => { - const items: AgentJournalRenderItem[] = [ - { - ...cutChat[0]!, - body: { - kind: 'turn', - turnId: 'cut', - state: 'completed', - outcome: 'failure', - startedAt: 1, - completedAt: 5 - } - }, - { - ...cutChat[1]!, - body: { - kind: 'status', - ...agentSessionFailureWords({ kind: 'usageLimitReached' }, { surface: 'row' }) - } - } - ] - const scope = structuredAgentSessionDraftScopeKey('session-1') - const draft = { - text: 'My unfinished request', - images: [{ id: 'attached', path: '/tmp/photo.png' }] - } - updateNativeChatComposerDraft(scope, draft, 'immediate') - const options = { model: 'gpt-current', effort: 'high' } - const uploading = { id: 'upload', path: '', pending: true, pendingName: 'next.png' } - const reveal = vi.fn() - function LiveHarness() { - const sends = useStructuredAgentSessionSends({ - sessionId: 'session-1', - target: PAIRED, - fence: 1, - submissions: [], - queuedMessageIds: [], - queue: { - capability: 'unsupported', - followUpsQueue: false, - isWorking: false, - stopping: false, - session: {}, - prompts: [] - }, - historyLoaded: false, - sendOptions: () => options - }) - const transport: NativeChatStructuredComposerTransport = { - capturePresentation: () => () => true, - send: (text, images, request) => - sends.send( - text, - images.map((image) => ({ path: image.path, previewUri: image.path })), - request - ), - onSubmitted: reveal, - onError: vi.fn(), - dispatchCommand: async () => ({ handled: false, accepted: false, error: null }), - optionsSurface, - optionSnapshot: [], - runtime: 'remote', - sessionId: 'session-1', - runtimeEnvironmentId: 'studio-mac' - } - return ( - <> - {uploading.pendingName} - - - ) - } - let answer = Promise.withResolvers() - mocks.call.mockImplementation(() => answer.promise) - const { rerender } = render() - mocks.capability = 'unknown' - rerender() - expect(continueButton()).toBeDisabled() - mocks.capability = 'supported' - rerender() - fireEvent.click(screen.getByRole('button', { name: 'compose' })) - act(() => setStructuredAgentSessionSendNotice('session-1', 'Previous send failed')) - continueButton()!.focus() - fireEvent.click(continueButton()!) - expect(screen.getByRole('textbox', { name: 'Composer' })).toHaveFocus() - expect(getStructuredAgentSessionSendNotice('session-1')).toBeNull() - expect(screen.getByTestId('error')).toHaveTextContent('none') - expect(continueButton()).toBeNull() - await waitFor(() => expect(mocks.call).toHaveBeenCalledOnce()) - expect(mocks.call.mock.calls[0].slice(0, 3)).toMatchObject([ - PAIRED, - 'agentSession.send', - { - body: { - kind: 'message', - role: 'user', - blocks: [{ type: 'text', text: 'Continue where you left off.' }] - }, - options, - usageLimitContinuationOfTurnItemId: TURN - } - ]) - expect(reveal).toHaveBeenCalledOnce() - expect(emitNativeChatMessageSent).toHaveBeenCalledWith({ agent: 'codex', runtime: 'remote' }) - await act(async () => - answer.resolve({ - ok: false, - refusal: { - code: 'agent_session_operation_invalid', - details: { reason: 'providerRejected' }, - message: 'Refused' - } - }) - ) - await waitFor(() => expect(continueButton()).toBeInTheDocument()) - expect(screen.getByTestId('error')).toHaveTextContent(FAILED) - expect(readNativeChatComposerDraft(scope)).toMatchObject(draft) - // An old host gets the exact plain-send control shape, including after an uncertain attempt. - mocks.capability = 'unsupported' - answer = Promise.withResolvers() - rerender() - fireEvent.click(continueButton()!) - await waitFor(() => expect(mocks.call).toHaveBeenCalledTimes(2)) - expect(mocks.call.mock.calls[1][2]).not.toHaveProperty('usageLimitContinuationOfTurnItemId') - await act(async () => answer.reject(new Error('connection lost'))) - await waitFor(() => expect(screen.getByTestId('error')).toHaveTextContent('none')) - expect(screen.getByTestId('error')).not.toHaveTextContent('was not sent') - expect(readNativeChatComposerDraft(scope)).toMatchObject(draft) - mocks.capability = 'supported' - answer = Promise.withResolvers() - rerender() - fireEvent.click(continueButton()!) - await waitFor(() => expect(mocks.call).toHaveBeenCalledTimes(3)) - const id = mocks.call.mock.calls[2][2].envelope.clientOperationId - await act(async () => - answer.resolve({ - ok: true, - fence: 1, - cursor: { epoch: 'e', sequence: 5 }, - value: { clientMessageId: id, submission: { clientMessageId: id, dispatchState: 'accepted' } } - }) - ) - // Completed content is not a live operation; the next action uses actual host state. - expect(continueButton()).toBeInTheDocument() - expect(readNativeChatComposerDraft(scope)).toMatchObject(draft) - expect(screen.getByText('next.png')).toBeInTheDocument() - expect(options).toEqual({ model: 'gpt-current', effort: 'high' }) -}) diff --git a/src/renderer/src/components/native-chat/usage/NativeChatInterruptedContinue.tsx b/src/renderer/src/components/native-chat/usage/NativeChatInterruptedContinue.tsx index 3d3a8a0e865..403a8dd907f 100644 --- a/src/renderer/src/components/native-chat/usage/NativeChatInterruptedContinue.tsx +++ b/src/renderer/src/components/native-chat/usage/NativeChatInterruptedContinue.tsx @@ -36,8 +36,8 @@ import { } from '../../../../../shared/native-chat/native-chat-interrupted-continue' import type { NativeChatOrcaStopView } from '@/components/native-chat/messages/notices/native-chat-orca-stop-context' import { useStructuredAgentSessionHostLabel } from '@/components/native-chat/pane/ownership/use-structured-agent-session-host-label' -import { useNativeChatTargetRestartResuming } from '@/components/native-chat-restart-runtimes' -import { useNativeChatLaunchResumePending } from '@/components/native-chat-launch-resume-decision' +import { useNativeChatTargetRestartResuming } from '@/components/native-chat-recovery/offers/native-chat-restart-runtimes' +import { useNativeChatLaunchResumePending } from '@/components/native-chat-recovery/offers/native-chat-launch-resume-decision' import type { NativeChatComposerNoticeContent } from '@/components/native-chat/composer/controls/native-chat-composer-notice' import type { NativeChatStructuredComposerTransport } from '@/components/native-chat/composer/submit/native-chat-structured-composer-transport' diff --git a/src/renderer/src/components/native-chat/viewport/find/NativeChatFindBar.tsx b/src/renderer/src/components/native-chat/viewport/find/NativeChatFindBar.tsx index 31cc0267b19..17bc8d376b1 100644 --- a/src/renderer/src/components/native-chat/viewport/find/NativeChatFindBar.tsx +++ b/src/renderer/src/components/native-chat/viewport/find/NativeChatFindBar.tsx @@ -2,7 +2,7 @@ import { useEffect } from 'react' import './native-chat-find.css' import { ChevronDown, ChevronUp, X } from 'lucide-react' import { Button } from '@/components/ui/button' -import { ImeInput } from '@/lib/ime-text-field' +import { ImeInput } from '@/lib/text-input/ime/ime-text-field' import { useTranslate } from '@/i18n/use-translate' import type { NativeChatFind } from './use-native-chat-find' import { useNativeChatFindMatches } from './use-native-chat-find-matches' diff --git a/src/renderer/src/components/native-chat/viewport/find/use-native-chat-find.ts b/src/renderer/src/components/native-chat/viewport/find/use-native-chat-find.ts index 73ad174250e..12eb70007a1 100644 --- a/src/renderer/src/components/native-chat/viewport/find/use-native-chat-find.ts +++ b/src/renderer/src/components/native-chat/viewport/find/use-native-chat-find.ts @@ -7,14 +7,14 @@ import { type RefObject } from 'react' import { keybindingMatchesAction } from '../../../../../../shared/keybindings/matching' -import { getShortcutPlatform } from '@/lib/shortcut-platform' -import { isEditableTarget } from '@/lib/editable-target' +import { getShortcutPlatform } from '@/lib/keyboard/shortcut-platform' +import { isEditableTarget } from '@/lib/text-input/focus/editable-target' import { isWebClientLocation } from '@/lib/web-client-location' import { useAppStore } from '../../../../store/index' import type { NativeChatComposerHandle } from '../../composer/native-chat-composer-types' import type { NativeChatMessageListHandle } from '../../messages/actions/use-native-chat-reveal-latest' import { focusNativeChatPromptCard } from '../../prompts/use-native-chat-prompt-card-focus' -import { addAppKeyListener, removeAppKeyListener } from '@/lib/app-keyboard-router' +import { addAppKeyListener, removeAppKeyListener } from '@/lib/keyboard/app-keyboard-router' // Keep open state/query with the retained chat so reopen preserves its last search. export type NativeChatFind = { diff --git a/src/renderer/src/components/new-workspace/NewWorkspaceComposerAdvancedSection.tsx b/src/renderer/src/components/new-workspace/NewWorkspaceComposerAdvancedSection.tsx index d71d3d057e4..434bf3dd4cc 100644 --- a/src/renderer/src/components/new-workspace/NewWorkspaceComposerAdvancedSection.tsx +++ b/src/renderer/src/components/new-workspace/NewWorkspaceComposerAdvancedSection.tsx @@ -8,12 +8,12 @@ import { import { toast } from 'sonner' import SparseCheckoutPresetSelect from '@/components/sparse/SparseCheckoutPresetSelect' import { cn } from '@/lib/utils' +import { TEXT_CONTROL_PASTE_DIRECT_MAX_BYTES } from '@/lib/text-input/paste/text-control-paste-model' import { - TEXT_CONTROL_PASTE_DIRECT_MAX_BYTES, measureTextControlPasteByteLength, pasteTextIntoTextControl, shouldHandleTextControlPaste -} from '@/lib/text-control-paste' +} from '@/lib/text-input/paste/text-control-paste' import { translate } from '@/i18n/i18n' import { useTranslate } from '@/i18n/use-translate' import { ComposerParentWorktreePicker } from './ComposerParentWorktreePicker' diff --git a/src/renderer/src/components/new-workspace/ProjectCombobox.tsx b/src/renderer/src/components/new-workspace/ProjectCombobox.tsx index c480f417a7a..9ef8358d215 100644 --- a/src/renderer/src/components/new-workspace/ProjectCombobox.tsx +++ b/src/renderer/src/components/new-workspace/ProjectCombobox.tsx @@ -1,4 +1,4 @@ -import { ImeInput } from '@/lib/ime-text-field' +import { ImeInput } from '@/lib/text-input/ime/ime-text-field' import React, { useCallback, useMemo, useRef, useState } from 'react' import { ChevronDown, FolderPlus } from 'lucide-react' import { Popover, PopoverAnchor, PopoverContent } from '@/components/ui/popover' diff --git a/src/renderer/src/components/new-workspace/RunTargetField.tsx b/src/renderer/src/components/new-workspace/RunTargetField.tsx index 6135d3c063e..4aae2529a7f 100644 --- a/src/renderer/src/components/new-workspace/RunTargetField.tsx +++ b/src/renderer/src/components/new-workspace/RunTargetField.tsx @@ -1,4 +1,4 @@ -import { ImeInput } from '@/lib/ime-text-field' +import { ImeInput } from '@/lib/text-input/ime/ime-text-field' import React, { useState } from 'react' import { Box, ChevronDown, Cloud } from 'lucide-react' import { PopoverAnchor } from '@/components/ui/popover' diff --git a/src/renderer/src/components/new-workspace/smart-workspace-github-direct-link.test.ts b/src/renderer/src/components/new-workspace/smart-workspace-github-direct-link.test.ts index 3c8d24df21a..5fc53698028 100644 --- a/src/renderer/src/components/new-workspace/smart-workspace-github-direct-link.test.ts +++ b/src/renderer/src/components/new-workspace/smart-workspace-github-direct-link.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest' -import { parseGitHubIssueOrPRLink } from '@/lib/github-links' +import { parseGitHubIssueOrPRLink } from '../../../../shared/github/links' import type { RepoOption } from './smart-workspace-name-field-model' import { resolveSmartWorkspaceGithubDirectLink } from './smart-workspace-github-direct-link' diff --git a/src/renderer/src/components/new-workspace/smart-workspace-github-direct-link.ts b/src/renderer/src/components/new-workspace/smart-workspace-github-direct-link.ts index 1ed36f311ed..de7a2bc4916 100644 --- a/src/renderer/src/components/new-workspace/smart-workspace-github-direct-link.ts +++ b/src/renderer/src/components/new-workspace/smart-workspace-github-direct-link.ts @@ -1,10 +1,10 @@ import type React from 'react' -import type { parseGitHubIssueOrPRLink } from '@/lib/github-links' +import type { parseGitHubIssueOrPRLink } from '../../../../shared/github/links' import { lookupGitHubWorkItemByOwnerRepoForSource, lookupGitHubWorkItemForSource } from '@/lib/work-items/github/github-work-item-source-lookup' -import { lookupSmartGitHubSubmitItem } from '@/lib/smart-github-submit' +import { lookupSmartGitHubSubmitItem } from '@/lib/work-items/github/smart-github-submit' import type { GitHubWorkItem } from '../../../../shared/github/work-item-types' import { buildTaskSourceContextFromRepo, diff --git a/src/renderer/src/components/new-workspace/smart-workspace-name-field-model.ts b/src/renderer/src/components/new-workspace/smart-workspace-name-field-model.ts index 910d868bd6c..79bee1ddd88 100644 --- a/src/renderer/src/components/new-workspace/smart-workspace-name-field-model.ts +++ b/src/renderer/src/components/new-workspace/smart-workspace-name-field-model.ts @@ -1,6 +1,6 @@ import type React from 'react' import type { useAppStore } from '@/store' -import type { parseGitHubIssueOrPRLink, RepoSlug } from '@/lib/github-links' +import type { parseGitHubIssueOrPRLink, RepoSlug } from '../../../../shared/github/links' import type { GitHubWorkItem } from '../../../../shared/github/work-item-types' import type { GitLabWorkItem } from '../../../../shared/gitlab-types' import type { JiraIssue, JiraSite } from '../../../../shared/jira/jira-types' diff --git a/src/renderer/src/components/new-workspace/smart-workspace-name-input-surface.tsx b/src/renderer/src/components/new-workspace/smart-workspace-name-input-surface.tsx index 8515a05f04a..eb379820850 100644 --- a/src/renderer/src/components/new-workspace/smart-workspace-name-input-surface.tsx +++ b/src/renderer/src/components/new-workspace/smart-workspace-name-input-surface.tsx @@ -4,7 +4,7 @@ import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { WorkspaceEmojiPickerButton } from '@/components/workspace-emoji/WorkspaceEmojiPickerButton' -import { isImeCompositionKeyDown } from '@/lib/ime-composition-keyboard-event' +import { isImeCompositionKeyDown } from '@/lib/text-input/ime/ime-composition-keyboard-event' import { isBlockingJiraUrlIntent } from '../../../../shared/new-workspace/smart-workspace-source-results' import { isBlockingLinearUrlIntent } from '../../../../shared/new-workspace/smart-workspace-linear-intent' import { isComposerFieldToFieldFocus } from './smart-workspace-source-popover-focus' diff --git a/src/renderer/src/components/new-workspace/smart-workspace-repo-slug.ts b/src/renderer/src/components/new-workspace/smart-workspace-repo-slug.ts index 09ee5346092..8dfe4a7a9ae 100644 --- a/src/renderer/src/components/new-workspace/smart-workspace-repo-slug.ts +++ b/src/renderer/src/components/new-workspace/smart-workspace-repo-slug.ts @@ -1,9 +1,9 @@ -import type { RepoSlug } from '@/lib/github-links' +import type { RepoSlug } from '../../../../shared/github/links' import { callRuntimeRpc } from '@/runtime/rpc/calls/runtime-rpc-call' import { getGitHubRuntimeRepoId, getGitHubSourceRuntimeTarget -} from '@/lib/github-source-runtime-context' +} from '@/lib/work-items/github/github-source-runtime-context' import { githubRepoIdentityKey } from '../../../../shared/github/repository-identity-key' import { getTaskSourceCacheScope, diff --git a/src/renderer/src/components/new-workspace/use-jira-url-source.ts b/src/renderer/src/components/new-workspace/use-jira-url-source.ts index 48dd7ad1ad5..9baf170761a 100644 --- a/src/renderer/src/components/new-workspace/use-jira-url-source.ts +++ b/src/renderer/src/components/new-workspace/use-jira-url-source.ts @@ -1,6 +1,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useAppStore } from '@/store' -import { getJiraAccountLabel } from '@/lib/jira-account-label' +import { getJiraAccountLabel } from '@/lib/work-items/jira/jira-account-label' import { assertRuntimeEnvironmentCapability } from '@/runtime/rpc/compatibility/runtime-capability-proof' import { getMatchingJiraSites, diff --git a/src/renderer/src/components/new-workspace/use-smart-workspace-github-search.ts b/src/renderer/src/components/new-workspace/use-smart-workspace-github-search.ts index 0168d7ee78b..c7b10dcdbee 100644 --- a/src/renderer/src/components/new-workspace/use-smart-workspace-github-search.ts +++ b/src/renderer/src/components/new-workspace/use-smart-workspace-github-search.ts @@ -1,11 +1,12 @@ import { useEffect, useMemo } from 'react' import { useAppStore } from '@/store' -import { normalizeGitHubLinkQuery, parseGitHubIssueOrPRLink } from '@/lib/github-links' +import { normalizeGitHubLinkQuery } from '../../../../shared/new-workspace/github-links' +import { parseGitHubIssueOrPRLink } from '../../../../shared/github/links' import { lookupGitHubWorkItemByOwnerRepoForSource, lookupGitHubWorkItemForSource } from '@/lib/work-items/github/github-work-item-source-lookup' -import { lookupSmartGitHubSubmitItem } from '@/lib/smart-github-submit' +import { lookupSmartGitHubSubmitItem } from '@/lib/work-items/github/smart-github-submit' import type { GitHubWorkItem } from '../../../../shared/github/work-item-types' import { RESULT_LIMIT } from './smart-workspace-name-field-model' import { resolveSmartWorkspaceGithubDirectLink } from './smart-workspace-github-direct-link' diff --git a/src/renderer/src/components/new-workspace/use-smart-workspace-gitlab-search.ts b/src/renderer/src/components/new-workspace/use-smart-workspace-gitlab-search.ts index 9eafa23feeb..62278de3bf5 100644 --- a/src/renderer/src/components/new-workspace/use-smart-workspace-gitlab-search.ts +++ b/src/renderer/src/components/new-workspace/use-smart-workspace-gitlab-search.ts @@ -2,8 +2,8 @@ import { useEffect, useMemo } from 'react' import { listGitLabWorkItemsForSource, lookupGitLabWorkItemByPathForSource -} from '@/lib/gitlab-work-item-source-lookup' -import { parseGitLabIssueOrMRLink } from '@/lib/gitlab-links' +} from '@/lib/work-items/gitlab/gitlab-work-item-source-lookup' +import { parseGitLabIssueOrMRLink } from '../../../../shared/new-workspace/gitlab-links' import type { GitLabWorkItem } from '../../../../shared/gitlab-types' import { RESULT_LIMIT } from './smart-workspace-name-field-model' import type { useWorkItemSourceFoundation } from './use-work-item-source-foundation' diff --git a/src/renderer/src/components/new-workspace/use-smart-workspace-name-field-foundation.source.test.tsx b/src/renderer/src/components/new-workspace/use-smart-workspace-name-field-foundation.source.test.tsx index 64dd0f532b4..0c11b0d2d66 100644 --- a/src/renderer/src/components/new-workspace/use-smart-workspace-name-field-foundation.source.test.tsx +++ b/src/renderer/src/components/new-workspace/use-smart-workspace-name-field-foundation.source.test.tsx @@ -34,7 +34,7 @@ const mocks = vi.hoisted(() => ({ } })) vi.mock('@/runtime/linear/runtime-linear-client', () => ({ linearStatus: mocks.readStatus })) -vi.mock('@/lib/gitlab-work-item-source-lookup', () => ({ +vi.mock('@/lib/work-items/gitlab/gitlab-work-item-source-lookup', () => ({ listGitLabWorkItemsForSource: mocks.listGitlabWorkItems, lookupGitLabWorkItemByPathForSource: vi.fn() })) diff --git a/src/renderer/src/components/new-workspace/use-smart-workspace-secondary-searches.ts b/src/renderer/src/components/new-workspace/use-smart-workspace-secondary-searches.ts index 1cbbd4b7c4d..b15ea773cc6 100644 --- a/src/renderer/src/components/new-workspace/use-smart-workspace-secondary-searches.ts +++ b/src/renderer/src/components/new-workspace/use-smart-workspace-secondary-searches.ts @@ -6,7 +6,7 @@ import { } from '@/store/slices/linear/linear-issue-publication' import type { LinearIssue } from '../../../../shared/linear/issue-types' import type { LinearCollectionResult } from '../../../../shared/linear/workspace-types' -import { linearIssueViewError } from '@/lib/linear-read-error' +import { linearIssueViewError } from '@/lib/work-items/linear/linear-read-error' import { getRepoExecutionHostId } from '../../../../shared/execution-host' import { useEffect, useMemo, useRef } from 'react' import { searchRuntimeRepoBaseRefDetails } from '@/runtime/source-control/references/runtime-repo-client' diff --git a/src/renderer/src/components/new-workspace/use-work-item-source-presentation.ts b/src/renderer/src/components/new-workspace/use-work-item-source-presentation.ts index 133fd2ebe29..71f6d1b6d03 100644 --- a/src/renderer/src/components/new-workspace/use-work-item-source-presentation.ts +++ b/src/renderer/src/components/new-workspace/use-work-item-source-presentation.ts @@ -1,7 +1,7 @@ import { useEffect, useMemo } from 'react' import { CaseSensitive, LoaderCircle, Search } from 'lucide-react' -import { parseGitHubIssueOrPRLink } from '@/lib/github-links' -import { parseGitLabIssueOrMRLink } from '@/lib/gitlab-links' +import { parseGitHubIssueOrPRLink } from '../../../../shared/github/links' +import { parseGitLabIssueOrMRLink } from '../../../../shared/new-workspace/gitlab-links' import { isSmartWorkspaceLinearIssueIntentMatch, parseBoundedSmartWorkspaceLinearIssueInput, diff --git a/src/renderer/src/components/new-workspace/use-work-item-source-state.ts b/src/renderer/src/components/new-workspace/use-work-item-source-state.ts index 91d721ae321..ca66a8e8461 100644 --- a/src/renderer/src/components/new-workspace/use-work-item-source-state.ts +++ b/src/renderer/src/components/new-workspace/use-work-item-source-state.ts @@ -1,6 +1,6 @@ import { useGitHubTaskItems } from '@/hooks/useGitHubTaskItems' import { useRef, useState } from 'react' -import type { RepoSlug } from '@/lib/github-links' +import type { RepoSlug } from '../../../../shared/github/links' import type { GitHubWorkItem } from '../../../../shared/github/work-item-types' import type { GitLabWorkItem } from '../../../../shared/gitlab-types' import type { JiraIssue } from '../../../../shared/jira/jira-types' diff --git a/src/renderer/src/components/onboarding/OnboardingFlow.tsx b/src/renderer/src/components/onboarding/OnboardingFlow.tsx index f94baec6f02..624585c53f1 100644 --- a/src/renderer/src/components/onboarding/OnboardingFlow.tsx +++ b/src/renderer/src/components/onboarding/OnboardingFlow.tsx @@ -1,7 +1,7 @@ import { OnboardingLanguageSwitcher } from './OnboardingLanguageSwitcher' import { useCallback, useEffect, useRef, useState } from 'react' import { cn } from '@/lib/utils' -import { isEditableTarget } from '@/lib/editable-target' +import { isEditableTarget } from '@/lib/text-input/focus/editable-target' import { getScreenSubmitModifierLabel, isScreenSubmitShortcut } from '@/lib/screen-submit-shortcut' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' import type { OnboardingState } from '../../../../shared/onboarding-state-types' @@ -16,7 +16,7 @@ import { OnboardingFooter } from './OnboardingFooter' import { shouldRequestOnboardingSkipConfirmation } from './onboarding-dismiss-target' import logo from '../../../../../resources/logo.svg' import { useTranslate } from '@/i18n/use-translate' -import { addAppKeyListener, removeAppKeyListener } from '@/lib/app-keyboard-router' +import { addAppKeyListener, removeAppKeyListener } from '@/lib/keyboard/app-keyboard-router' import { useScrollEdgeFades } from '@/lib/use-scroll-edge-fades' import { AgentSurfaceStep } from './AgentSurfaceStep' import { stepCopy, stepTooltipLabels } from './onboarding-step-copy' diff --git a/src/renderer/src/components/orchestration-crew/OrchestrationCrewPanel.tsx b/src/renderer/src/components/orchestration-crew/OrchestrationCrewPanel.tsx index ee0609f4947..974cb20fec5 100644 --- a/src/renderer/src/components/orchestration-crew/OrchestrationCrewPanel.tsx +++ b/src/renderer/src/components/orchestration-crew/OrchestrationCrewPanel.tsx @@ -11,11 +11,11 @@ import type { OrchestrationCrewSnapshot } from '../../../../shared/orchestration/orchestration-crew' import type { RuntimeClientTarget } from '../../../../shared/runtime-client-target' -import { useOrchestrationCrew } from '../native-chat/worker-lifetime/use-orchestration-crew' +import { useOrchestrationCrew } from './use-orchestration-crew' import { translate } from '@/i18n/i18n' import { useTranslate, type Translate } from '@/i18n/use-translate' import { hasVisibleOverlay } from '@/lib/visible-overlay' -import { isImeOwnedKeyboardEvent } from '@/lib/ime-composition-keyboard-event' +import { isImeOwnedKeyboardEvent } from '@/lib/text-input/ime/ime-composition-keyboard-event' import { CrewRowView, IconButton } from './CrewRowView' import { CREW_STATUS_GROUPS, diff --git a/src/renderer/src/components/orchestration-crew/TerminalCrewPortals.test.tsx b/src/renderer/src/components/orchestration-crew/TerminalCrewPortals.test.tsx deleted file mode 100644 index ae24479a968..00000000000 --- a/src/renderer/src/components/orchestration-crew/TerminalCrewPortals.test.tsx +++ /dev/null @@ -1,223 +0,0 @@ -// @vitest-environment happy-dom -import '@testing-library/jest-dom/vitest' -import type { ComponentProps } from 'react' -import { act, cleanup, render, screen, within } from '@testing-library/react' -import userEvent from '@testing-library/user-event' -import { afterEach, expect, it, vi } from 'vitest' -import { PaneManager } from '@/lib/pane-manager/pane-manager' -import { useAppStore } from '@/store/index' -import { TooltipProvider } from '@/components/ui/tooltip' -import type { PtyTransport } from '../terminal-pane/pty-transport/pty-transport-types' -import { - emptyCrew, - type OrchestrationCrewRow -} from '../../../../shared/orchestration/orchestration-crew' -import { makePaneKey } from '../../../../shared/workspace-layout/stable-pane-id' -import { createIpcPtyTransport } from '../terminal-pane/pty-transport/ipc/pty-transport' -import { TerminalCrewPortals } from './TerminalCrewPortals' - -// Exercise dormant panel interactions independently of the suspension gate. -vi.mock('./use-orchestration-panel-enabled', () => ({ - useOrchestrationPanelEnabled: () => true -})) -const rig = vi.hoisted(() => ({ open: vi.fn(), copy: vi.fn(), identity: vi.fn(), crew: vi.fn() })) -vi.mock('./use-terminal-crew-identity', () => ({ - useTerminalCrewIdentity: (paneKey: string) => { - rig.identity(paneKey) - return { target: { kind: 'local' }, party: paneKey } - } -})) -vi.mock('../native-chat/worker-lifetime/use-orchestration-crew', () => ({ - useOrchestrationCrew: () => { - rig.crew() - return { - ...emptyCrew('ok'), - sections: { managedBy: [], managing: [currentWorker], talkingWith: [] } - } - } -})) -// Render the real panel synchronously; the lazy chunk boundary is the app's load detail. -vi.mock('./LazyOrchestrationCrewPanel', async () => ({ - LazyOrchestrationCrewPanel: (await import('./OrchestrationCrewPanel')).OrchestrationCrewPanel -})) -vi.mock('@/lib/open-agent-message-sender', () => ({ openOrchestrationCrewAgent: rig.open })) -vi.mock('@/lib/pane-manager/pane-lifecycle', async (importOriginal) => { - const actual: object = await importOriginal() - return { ...actual, openTerminal: vi.fn() } -}) -vi.mock('sonner', () => ({ toast: { error: vi.fn() } })) - -const worker: OrchestrationCrewRow = { - partyKey: 'worker', - role: 'worker', - label: 'Worker', - agentKind: 'codex', - status: 'working', - sendAddress: 'dispatch:worker', - openTarget: { kind: 'terminal', handle: 'term_worker' }, - latest: { - direction: 'from', - preview: 'Keep this disclosure', - at: '2026-10-30T00:00:00Z', - sequence: 1 - } -} -let currentWorker = worker -type Controller = ComponentProps['controller'] -const managers = new Set() -const ipcTransports: ReturnType[] = [] -function resetFixture() { - cleanup() - for (const transport of ipcTransports.splice(0)) { - transport.destroy?.() - } - for (const manager of managers) { - manager.destroy() - } - managers.clear() - document.body.replaceChildren() - vi.restoreAllMocks() - vi.unstubAllGlobals() - useAppStore.setState(useAppStore.getInitialState(), true) -} -afterEach(() => { - resetFixture() - vi.clearAllMocks() -}) - -function mount(insertion?: { - acceptance: boolean | Promise | undefined - split?: boolean - restored?: boolean -}) { - const link = { connected: !insertion?.restored } - currentWorker = insertion - ? { ...worker, task: { title: 'Inspect', state: 'dispatched' }, sendAddress: 'term_worker' } - : worker - if (insertion) { - vi.stubGlobal('requestAnimationFrame', () => 1) - vi.stubGlobal('cancelAnimationFrame', () => {}) - } - const root = document.createElement('div') - document.body.appendChild(root) - const manager = new PaneManager(root, { linkOpenHint: () => '' }) - managers.add(manager) - const first = manager.createInitialPane( - insertion ? { focus: false, leafId: '11111111-1111-4111-8111-111111111111' } : undefined - ) - const second = insertion && !insertion.split ? first : manager.splitPane(first.id, 'vertical') - if (!second) { - throw new Error('fixture split failed') - } - const panes = insertion && !insertion.split ? [first] : [first, second] - const input = document.createElement('textarea') - const secondInput = document.createElement('textarea') - input.value = 'existing suffix' - input.setSelectionRange(8, 8) - const sendInput = vi.fn(() => true) - const accepted = vi.fn(async () => insertion?.acceptance ?? true) - const transport: PtyTransport = { - connect: vi.fn(), - attach: vi.fn(), - disconnect: vi.fn(), - sendInputImmediate: vi.fn(() => true), - resize: vi.fn(() => true), - getPtyId: () => (link.connected ? 'pty-captured' : null), - isConnected: () => link.connected, - sendInput, - ...(insertion?.acceptance === undefined ? {} : { sendInputAccepted: accepted }) - } - const transports = new Map( - panes.map((pane) => { - if (insertion) { - return [pane.id, transport] as const - } - const ipc = createIpcPtyTransport() - ipcTransports.push(ipc) - return [pane.id, ipc] as const - }) - ) - if (insertion) { - first.container.append(input) - vi.spyOn(first.terminal, 'focus').mockImplementation(() => input.focus()) - if (insertion.split) { - second.container.append(secondInput) - vi.spyOn(second.terminal, 'focus').mockImplementation(() => secondInput.focus()) - } - manager.setActivePane(first.id, { focus: false }) - vi.spyOn(manager, 'setActivePane') - } - let controller: Controller = { - tabId: insertion ? 'tab' : 'retained-tab', - worktreeId: insertion ? 'workspace' : 'worktree', - managerRef: { current: manager }, - paneTransportsRef: { current: transports }, - managedPanes: panes, - effectiveChatViewMode: false, - chatPane: null, - isRendererVisible: true, - expandedPaneId: null, - isolatedPaneKey: null - } - Object.defineProperty(window, 'api', { - configurable: true, - value: { ui: { writeTerminalClipboardText: rig.copy } } - }) - rig.copy.mockResolvedValue(undefined) - const content = () => ( - - - - ) - const view = render(content()) - return { - controller, - first, - second, - manager, - input, - secondInput, - transport, - transports, - link, - accepted, - sendInput, - update: (patch: Partial = {}) => { - controller = { ...controller, ...patch } - view.rerender(content()) - } - } -} - -it('uses numeric expansion and exact activity leaf identity without dismissing another visible split', async () => { - const user = userEvent.setup() - const { controller, first, second, manager, update } = mount() - const a = within(first.container) - const b = within(second.container) - await user.click(a.getByRole('button', { name: /^Show orchestration: 1 worker,/ })) - await user.click(b.getByRole('button', { name: /^Show orchestration: 1 worker,/ })) - await user.click(a.getByRole('button', { name: 'More for Worker' })) - const popup = screen.getByRole('menu') - // Existing overlay focus preservation allows a visible split to remain open when another is active. - manager.setActivePane(second.id) - expect(manager.getActivePane()?.id).toBe(second.id) - act(update) - expect(screen.getByRole('menu')).toBe(popup) - act(() => update({ expandedPaneId: second.id })) - expect(screen.queryByRole('menu')).not.toBeInTheDocument() - expect(a.queryByRole('button', { name: 'More for Worker' })).not.toBeInTheDocument() - expect(b.getByRole('button', { name: 'More for Worker' })).toBeInTheDocument() - act(() => update({ expandedPaneId: null })) - await user.click(a.getByRole('button', { name: 'More for Worker' })) - act(() => update({ isolatedPaneKey: makePaneKey(controller.tabId, second.leafId) })) - expect(screen.queryByRole('menu')).not.toBeInTheDocument() - expect(a.queryByRole('button', { name: 'More for Worker' })).not.toBeInTheDocument() - await user.click(b.getByRole('button', { name: 'More for Worker' })) - act(() => update({ isolatedPaneKey: makePaneKey('other-tab', second.leafId) })) - expect(screen.queryByRole('menu')).not.toBeInTheDocument() - expect(b.queryByRole('button', { name: 'More for Worker' })).not.toBeInTheDocument() - act(() => update({ isolatedPaneKey: null })) - expect(screen.queryByRole('menu')).not.toBeInTheDocument() - expect(rig.open).not.toHaveBeenCalled() - expect(rig.copy).not.toHaveBeenCalled() -}) diff --git a/src/renderer/src/components/native-chat/worker-lifetime/use-orchestration-crew.test.tsx b/src/renderer/src/components/orchestration-crew/use-orchestration-crew.test.tsx similarity index 95% rename from src/renderer/src/components/native-chat/worker-lifetime/use-orchestration-crew.test.tsx rename to src/renderer/src/components/orchestration-crew/use-orchestration-crew.test.tsx index 34d947ce3ae..e6c6b6fc303 100644 --- a/src/renderer/src/components/native-chat/worker-lifetime/use-orchestration-crew.test.tsx +++ b/src/renderer/src/components/orchestration-crew/use-orchestration-crew.test.tsx @@ -5,11 +5,11 @@ import { afterEach, beforeEach, expect, it, vi } from 'vitest' import type { CrewSubscriptionAttemptArgs, CrewSubscriptionAttemptCallbacks -} from '../../../../../shared/remote-runtime/host-catalog/runtime-environment-subscription-setup' -import type { RuntimeClientTarget } from '../../../../../shared/runtime-client-target' +} from '../../../../shared/remote-runtime/host-catalog/runtime-environment-subscription-setup' +import type { RuntimeClientTarget } from '../../../../shared/runtime-client-target' import type { RuntimeRpcSubscriptionHandlers } from '@/runtime/rpc/subscriptions/runtime-rpc-subscribe' import { replaceRuntimeEnvironmentRevisions } from '@/runtime/rpc/environment-revisions/runtime-environment-revision' -import { REMOTE_RUNTIME_SHARED_CONTROL_CAPABILITY } from '../../../../../shared/host-runtime-capabilities' +import { REMOTE_RUNTIME_SHARED_CONTROL_CAPABILITY } from '../../../../shared/host-runtime-capabilities' import { useOrchestrationCrew } from './use-orchestration-crew' const rig = vi.hoisted(() => { @@ -67,7 +67,7 @@ vi.mock('@/runtime/rpc/subscriptions/runtime-rpc-subscribe', () => ({ } ) })) -vi.mock('@/store', () => ({ +vi.mock('@/store/index', () => ({ useAppStore: { getState: () => rig.state, subscribe: (wake: () => void) => { @@ -76,7 +76,7 @@ vi.mock('@/store', () => ({ } } })) -vi.mock('../messages/agent-senders/native-chat-agent-message-sender-view', () => ({ +vi.mock('../native-chat/messages/agent-senders/native-chat-agent-message-sender-view', () => ({ useAgentMessageSenderViews: () => [] })) vi.mock('@/lib/open-agent-message-sender', () => ({ openOrchestrationCrewAgent: vi.fn() })) diff --git a/src/renderer/src/components/native-chat/worker-lifetime/use-orchestration-crew.ts b/src/renderer/src/components/orchestration-crew/use-orchestration-crew.ts similarity index 69% rename from src/renderer/src/components/native-chat/worker-lifetime/use-orchestration-crew.ts rename to src/renderer/src/components/orchestration-crew/use-orchestration-crew.ts index 706c25019a5..7ec081382dd 100644 --- a/src/renderer/src/components/native-chat/worker-lifetime/use-orchestration-crew.ts +++ b/src/renderer/src/components/orchestration-crew/use-orchestration-crew.ts @@ -1,8 +1,8 @@ import { useCallback, useMemo, useSyncExternalStore } from 'react' -import type { RuntimeClientTarget } from '../../../../../shared/runtime-client-target' -import type { OrchestrationCrewSnapshot } from '../../../../../shared/orchestration/orchestration-crew' -import { observeCrew } from '../../orchestration-crew/orchestration-crew-follow' -import { UNKNOWN_CREW } from '../../orchestration-crew/orchestration-crew-subscription' +import type { RuntimeClientTarget } from '../../../../shared/runtime-client-target' +import type { OrchestrationCrewSnapshot } from '../../../../shared/orchestration/orchestration-crew' +import { observeCrew } from './orchestration-crew-follow' +import { UNKNOWN_CREW } from './orchestration-crew-subscription' export function useOrchestrationCrew( target: RuntimeClientTarget | null, diff --git a/src/renderer/src/components/plugins/page/PluginAddsSection.tsx b/src/renderer/src/components/plugins/page/PluginAddsSection.tsx index b4e8ee7c00f..4f2ee9cb711 100644 --- a/src/renderer/src/components/plugins/page/PluginAddsSection.tsx +++ b/src/renderer/src/components/plugins/page/PluginAddsSection.tsx @@ -24,7 +24,7 @@ import type { PluginCatalogType } from '../../../../../shared/plugins/catalog/pl import { translate } from '@/i18n/i18n' import { useIntlLocale, useTranslate, type Translate } from '@/i18n/use-translate' import { useAppStore } from '@/store' -import { getShortcutPlatform } from '@/lib/shortcut-platform' +import { getShortcutPlatform } from '@/lib/keyboard/shortcut-platform' import { usePluginLanguagePackStore } from '@/store/plugins/localization/plugin-language-packs' import type { ActivePluginCommand } from '@/store/plugins/catalog/plugin-contribution-catalog' import { usePluginIconThemes } from '@/store/plugins/appearance/plugin-icon-themes' diff --git a/src/renderer/src/components/plugins/review/PluginInstallConsent.tsx b/src/renderer/src/components/plugins/review/PluginInstallConsent.tsx index 5df26568495..09bfbd8bf4b 100644 --- a/src/renderer/src/components/plugins/review/PluginInstallConsent.tsx +++ b/src/renderer/src/components/plugins/review/PluginInstallConsent.tsx @@ -3,7 +3,7 @@ import { AlertTriangle, Check, Loader2, ShieldAlert } from 'lucide-react' import type { PluginHostListEntry } from '../../../../../preload/api-types' import { getIntlLocale } from '@/i18n/i18n' import { useIntlLocale, useTranslate } from '@/i18n/use-translate' -import { getShortcutPlatform } from '@/lib/shortcut-platform' +import { getShortcutPlatform } from '@/lib/keyboard/shortcut-platform' import { Button } from '../../ui/button' import { SecuritySurface } from '../../ui/security-surface' import { diff --git a/src/renderer/src/components/plugins/view/plugin-view-host-actions.ts b/src/renderer/src/components/plugins/view/plugin-view-host-actions.ts index 2a45aa1b6fb..e1ce203537e 100644 --- a/src/renderer/src/components/plugins/view/plugin-view-host-actions.ts +++ b/src/renderer/src/components/plugins/view/plugin-view-host-actions.ts @@ -3,7 +3,7 @@ import type { PluginViewRequest } from '../../../../../shared/plugins/view/plugin-view-protocol' import { detectLanguage } from '../../../../../shared/editor-language/editor-language-detect' -import { dispatchAppCommand } from '@/lib/app-command-dispatch' +import { dispatchAppCommand } from '@/lib/app-commands/app-command-dispatch' import { useAppStore } from '@/store' import { getIndexedWorktreeMap } from '@/store/slices/worktrees/listing/worktree-catalog-index' import { diff --git a/src/renderer/src/components/provider-accounts/use-antigravity-chat-sign-in.ts b/src/renderer/src/components/provider-accounts/use-antigravity-chat-sign-in.ts index a476f14331a..ebacd45ef9c 100644 --- a/src/renderer/src/components/provider-accounts/use-antigravity-chat-sign-in.ts +++ b/src/renderer/src/components/provider-accounts/use-antigravity-chat-sign-in.ts @@ -1,7 +1,7 @@ import { useEffect, useState, useSyncExternalStore } from 'react' import { translate } from '../../i18n/i18n' import { useTranslate, type Translate } from '../../i18n/use-translate' -import { useClipboardTextCopyFeedback } from '../../hooks/use-clipboard-text-copy-feedback' +import { useClipboardTextCopyFeedback } from '../../hooks/clipboard/use-clipboard-text-copy-feedback' import { antigravityChatSignInRunning as running, cancelAntigravityChatSignIn, diff --git a/src/renderer/src/components/pull-request-page/actions/merge-actions.ts b/src/renderer/src/components/pull-request-page/actions/merge-actions.ts index 1aafa675536..38db102669b 100644 --- a/src/renderer/src/components/pull-request-page/actions/merge-actions.ts +++ b/src/renderer/src/components/pull-request-page/actions/merge-actions.ts @@ -3,7 +3,7 @@ import { callRuntimeRpc } from '@/runtime/rpc/calls/runtime-rpc-call' import type { RuntimeClientTarget } from '../../../../../shared/runtime-client-target' import { notifyWorkItemDetailsMutation } from '@/components/github/github-work-item-comment-mutations' import { runPullRequestStateUpdate } from '@/components/github/github-work-item-edit-mutations' -import { getGitHubRuntimeRepoId } from '@/lib/github-source-runtime-context' +import { getGitHubRuntimeRepoId } from '@/lib/work-items/github/github-source-runtime-context' import { getPullRequestMergeConfirmationTitle, getPullRequestMergeMethodLabel, @@ -19,7 +19,7 @@ import type { TaskSourceContext } from '../../../../../shared/work-items/task-so import type { PullRequestPageProjectOrigin } from '../page-types' import { translate } from '@/i18n/i18n' import type { GitHubPRMergeStatePresentation } from '@/components/github-pr-merge-state' -import { codeHostErrorText } from '@/lib/code-host-error-copy' +import { codeHostErrorText } from '@/lib/source-control/remote/code-host-error-copy' export async function changePullRequestState(args: { canMutateState: boolean diff --git a/src/renderer/src/components/pull-request-page/actions/panel.tsx b/src/renderer/src/components/pull-request-page/actions/panel.tsx index 6054d009c84..38a5f8473c6 100644 --- a/src/renderer/src/components/pull-request-page/actions/panel.tsx +++ b/src/renderer/src/components/pull-request-page/actions/panel.tsx @@ -21,7 +21,7 @@ import { import { useConfirmationDialog } from '@/components/confirmation-dialog-context' import { cn } from '@/lib/utils' import { useAppStore } from '@/store' -import { getGitHubRepoRoutingTarget } from '@/lib/github-source-runtime-context' +import { getGitHubRepoRoutingTarget } from '@/lib/work-items/github/github-source-runtime-context' import { presentGitHubPRMergeState } from '@/components/github-pr-merge-state' import { resolveGitHubPRMergeMethods } from '../../../../../shared/github/pull-request-merge-methods' import { resolvePullRequestRepo } from '@/components/github/github-work-item-identity' diff --git a/src/renderer/src/components/pull-request-page/cache/file-content.ts b/src/renderer/src/components/pull-request-page/cache/file-content.ts index de98b15b055..647e15a164d 100644 --- a/src/renderer/src/components/pull-request-page/cache/file-content.ts +++ b/src/renderer/src/components/pull-request-page/cache/file-content.ts @@ -10,7 +10,7 @@ import { callRuntimeRpc } from '@/runtime/rpc/calls/runtime-rpc-call' import { getGitHubRuntimeRepoId, getGitHubSourceRuntimeHost -} from '@/lib/github-source-runtime-context' +} from '@/lib/work-items/github/github-source-runtime-context' import { PR_FILE_CONTENT_CACHE_MAX_BYTES, getRetainedPRFileContentsByteCount diff --git a/src/renderer/src/components/pull-request-page/checks/details-request.ts b/src/renderer/src/components/pull-request-page/checks/details-request.ts index 6896ac9e596..ec258b68b6e 100644 --- a/src/renderer/src/components/pull-request-page/checks/details-request.ts +++ b/src/renderer/src/components/pull-request-page/checks/details-request.ts @@ -12,7 +12,10 @@ import { type CheckDetailsLoadState, type GitHubChecksTabState } from '@/components/github-checks-tab-state' -import { getGitHubRuntimeRepoId, type GitHubRuntimeHost } from '@/lib/github-source-runtime-context' +import { + getGitHubRuntimeRepoId, + type GitHubRuntimeHost +} from '@/lib/work-items/github/github-source-runtime-context' import { translate } from '@/i18n/i18n' import type { GitHubOwnerRepo } from '../../../../../shared/github/pull-request-types' import type { GitHubWorkItem } from '../../../../../shared/github/work-item-types' diff --git a/src/renderer/src/components/pull-request-page/checks/fix-launch.ts b/src/renderer/src/components/pull-request-page/checks/fix-launch.ts index 05b033b6298..d6279ea6bdc 100644 --- a/src/renderer/src/components/pull-request-page/checks/fix-launch.ts +++ b/src/renderer/src/components/pull-request-page/checks/fix-launch.ts @@ -5,7 +5,7 @@ import { import type { DirectWorkItemLaunchResult } from '@/lib/workspace/creation/work-item/direct-work-item-setup' import { toast } from 'sonner' import { useAppStore } from '@/store' -import { startFixChecksAgent } from '@/lib/fix-checks-agent-launch' +import { startFixChecksAgent } from '@/lib/source-control/checks/fix-checks-agent-launch' import { launchWorkItemDirect } from '@/lib/workspace/creation/work-item/launch-work-item-direct' import { buildFixBrokenChecksPrompt } from '../../../../../shared/source-control/checks/pr-checks-fix-prompt' import type { SourceControlAiWriteTarget } from '../../../../../shared/source-control/ai/settings/source-control-ai-recipe-save' diff --git a/src/renderer/src/components/pull-request-page/checks/refresh.ts b/src/renderer/src/components/pull-request-page/checks/refresh.ts index b56f366111d..fd647303677 100644 --- a/src/renderer/src/components/pull-request-page/checks/refresh.ts +++ b/src/renderer/src/components/pull-request-page/checks/refresh.ts @@ -1,7 +1,10 @@ -import { readPRCheckRefreshOwner } from '@/lib/pr-check-refresh-owner' +import { readPRCheckRefreshOwner } from '@/lib/source-control/checks/pr-check-refresh-owner' import { toast } from 'sonner' import { callRuntimeRpc } from '@/runtime/rpc/calls/runtime-rpc-call' -import { getGitHubRuntimeRepoId, type GitHubRuntimeHost } from '@/lib/github-source-runtime-context' +import { + getGitHubRuntimeRepoId, + type GitHubRuntimeHost +} from '@/lib/work-items/github/github-source-runtime-context' import { resetGitHubChecksTabForSource, updateGitHubChecksTabLocalChecks, diff --git a/src/renderer/src/components/pull-request-page/checks/rerun.ts b/src/renderer/src/components/pull-request-page/checks/rerun.ts index 078d996bb54..b422731ef80 100644 --- a/src/renderer/src/components/pull-request-page/checks/rerun.ts +++ b/src/renderer/src/components/pull-request-page/checks/rerun.ts @@ -1,13 +1,16 @@ import { toast } from 'sonner' import { callRuntimeRpc } from '@/runtime/rpc/calls/runtime-rpc-call' -import { getGitHubRuntimeRepoId, type GitHubRuntimeHost } from '@/lib/github-source-runtime-context' +import { + getGitHubRuntimeRepoId, + type GitHubRuntimeHost +} from '@/lib/work-items/github/github-source-runtime-context' import { translate } from '@/i18n/i18n' import type { GitHubOwnerRepo } from '../../../../../shared/github/pull-request-types' import type { GitHubWorkItem } from '../../../../../shared/github/work-item-types' import type { PRCheckDetail } from '../../../../../shared/github/check-types' import type { TaskSourceContext } from '../../../../../shared/work-items/task-source-context' import type { GitHubChecksTabState } from '../../github-checks-tab-state' -import { codeHostErrorText } from '@/lib/code-host-error-copy' +import { codeHostErrorText } from '@/lib/source-control/remote/code-host-error-copy' /** The checks tab mints one of these per context; only its reference identity is ever read. */ type ChecksContextOwner = GitHubChecksTabState['contextOwner'] diff --git a/src/renderer/src/components/pull-request-page/checks/tab.tsx b/src/renderer/src/components/pull-request-page/checks/tab.tsx index 45c6ab85d74..6b87113606e 100644 --- a/src/renderer/src/components/pull-request-page/checks/tab.tsx +++ b/src/renderer/src/components/pull-request-page/checks/tab.tsx @@ -25,7 +25,7 @@ import { resolvePullRequestRepo } from '@/components/github/github-work-item-ide import { canUseGitHubRepoContext, getGitHubSourceRuntimeHost -} from '@/lib/github-source-runtime-context' +} from '@/lib/work-items/github/github-source-runtime-context' import { getLocalRepoProjectExecutionRuntimeContext } from '@/lib/local-preflight-context' import { CLIENT_PLATFORM } from '@/lib/new-workspace' import { resolveRepoExecutionHostPlatform } from '@/runtime/hosts/platform/execution-host-facts' diff --git a/src/renderer/src/components/pull-request-page/comments/composer.tsx b/src/renderer/src/components/pull-request-page/comments/composer.tsx index 11f86f70cca..49460ff952d 100644 --- a/src/renderer/src/components/pull-request-page/comments/composer.tsx +++ b/src/renderer/src/components/pull-request-page/comments/composer.tsx @@ -11,7 +11,7 @@ import { isScreenSubmitShortcut } from '@/lib/screen-submit-shortcut' import { getCommentBodySubmitState, hasBoundedCommentBodyText -} from '@/lib/comment-body-submit-state' +} from '@/lib/source-control/review/comments/comment-body-submit-state' import { addIssueCommentForRepo } from '@/components/github/github-work-item-comment-mutations' import { translate } from '@/i18n/i18n' import { useTranslate } from '@/i18n/use-translate' @@ -20,7 +20,7 @@ import type { PRComment } from '../../../../../shared/github/comment-types' import type { TaskSourceContext } from '../../../../../shared/work-items/task-source-context' import type { MentionOption } from '../page-types' import { MentionTextarea } from '../mentions/textarea' -import { codeHostErrorText } from '@/lib/code-host-error-copy' +import { codeHostErrorText } from '@/lib/source-control/remote/code-host-error-copy' export function GHCommentComposer({ className, diff --git a/src/renderer/src/components/pull-request-page/comments/reply-form.tsx b/src/renderer/src/components/pull-request-page/comments/reply-form.tsx index ffed7d1b4a9..8e37f90ca60 100644 --- a/src/renderer/src/components/pull-request-page/comments/reply-form.tsx +++ b/src/renderer/src/components/pull-request-page/comments/reply-form.tsx @@ -9,7 +9,7 @@ import { isScreenSubmitShortcut } from '@/lib/screen-submit-shortcut' import { getCommentBodySubmitState, hasBoundedCommentBodyText -} from '@/lib/comment-body-submit-state' +} from '@/lib/source-control/review/comments/comment-body-submit-state' import { translate } from '@/i18n/i18n' import { useTranslate } from '@/i18n/use-translate' import type { MentionOption } from '../page-types' diff --git a/src/renderer/src/components/pull-request-page/conversation/reply.ts b/src/renderer/src/components/pull-request-page/conversation/reply.ts index d54feaf876c..1b67f437a34 100644 --- a/src/renderer/src/components/pull-request-page/conversation/reply.ts +++ b/src/renderer/src/components/pull-request-page/conversation/reply.ts @@ -13,7 +13,7 @@ import type { GitHubOwnerRepo } from '../../../../../shared/github/pull-request- import type { GitHubWorkItem } from '../../../../../shared/github/work-item-types' import type { PRComment } from '../../../../../shared/github/comment-types' import type { TaskSourceContext } from '../../../../../shared/work-items/task-source-context' -import { codeHostErrorText } from '@/lib/code-host-error-copy' +import { codeHostErrorText } from '@/lib/source-control/remote/code-host-error-copy' export async function postConversationReply(args: { canUseRepoMutationContext: boolean diff --git a/src/renderer/src/components/pull-request-page/conversation/tab.tsx b/src/renderer/src/components/pull-request-page/conversation/tab.tsx index ef351efdfc7..5c7027d3881 100644 --- a/src/renderer/src/components/pull-request-page/conversation/tab.tsx +++ b/src/renderer/src/components/pull-request-page/conversation/tab.tsx @@ -1,6 +1,6 @@ import { githubCommentImageUploadAddress } from '@/components/github/github-comment-image-upload-address' import { getGitHubMutationTarget } from '@/components/github/github-work-item-mutation-target' -import { taskSourceRuntimeTarget } from '@/lib/task-source-runtime-target' +import { taskSourceRuntimeTarget } from '@/lib/work-items/task-source-runtime-target' import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useShallow } from 'zustand/react/shallow' import { toast } from 'sonner' @@ -10,8 +10,8 @@ import { useRepoAssignees } from '@/hooks/useGitHubRepoMetadata' import { canUseGitHubRepoContext, getGitHubRepoRoutingTarget -} from '@/lib/github-source-runtime-context' -import { usePRBotAuthorOverrides } from '@/lib/pr-bot-author-overrides' +} from '@/lib/work-items/github/github-source-runtime-context' +import { usePRBotAuthorOverrides } from '@/lib/source-control/review/pr-bot-author-overrides' import { filterPRCommentsByAudience, getPRCommentAudienceCounts, diff --git a/src/renderer/src/components/pull-request-page/edit/section.tsx b/src/renderer/src/components/pull-request-page/edit/section.tsx index 36bba8d6349..a145cb298ea 100644 --- a/src/renderer/src/components/pull-request-page/edit/section.tsx +++ b/src/renderer/src/components/pull-request-page/edit/section.tsx @@ -10,7 +10,7 @@ import { useRepoAssignees, useRepoLabels } from '@/hooks/useGitHubRepoMetadata' import { useRepoAssigneesBySlug, useRepoLabelsBySlug } from '@/hooks/useGitHubSlugMetadata' import type { TaskSourceContext } from '../../../../../shared/work-items/task-source-context' import type { GitHubWorkItem } from '../../../../../shared/github/work-item-types' -import { getGitHubRepoRoutingTarget } from '@/lib/github-source-runtime-context' +import { getGitHubRepoRoutingTarget } from '@/lib/work-items/github/github-source-runtime-context' import { getStateLabel } from '@/components/github/work-item-state-presentation' import { useTranslate } from '@/i18n/use-translate' import type { PullRequestPageProjectOrigin } from '../page-types' diff --git a/src/renderer/src/components/pull-request-page/files/line-comment.ts b/src/renderer/src/components/pull-request-page/files/line-comment.ts index d8a57019938..c569a09c13b 100644 --- a/src/renderer/src/components/pull-request-page/files/line-comment.ts +++ b/src/renderer/src/components/pull-request-page/files/line-comment.ts @@ -5,7 +5,7 @@ import type { DiffSection } from '@/components/editor/diff/sections/diff-section import type { GitHubOwnerRepo } from '../../../../../shared/github/pull-request-types' import type { PRComment } from '../../../../../shared/github/comment-types' import type { TaskSourceContext } from '../../../../../shared/work-items/task-source-context' -import { codeHostErrorText } from '@/lib/code-host-error-copy' +import { codeHostErrorText } from '@/lib/source-control/remote/code-host-error-copy' export async function addPullRequestLineComment(args: { headSha: string | undefined diff --git a/src/renderer/src/components/pull-request-page/mentions/textarea.tsx b/src/renderer/src/components/pull-request-page/mentions/textarea.tsx index 48b5ab231a5..ac3f7c24c33 100644 --- a/src/renderer/src/components/pull-request-page/mentions/textarea.tsx +++ b/src/renderer/src/components/pull-request-page/mentions/textarea.tsx @@ -1,6 +1,6 @@ import { useGitHubTextareaImages } from '@/components/github/use-github-textarea-images' import type { GitHubImageIntake } from '@/components/github/use-github-image-uploads' -import { ImeTextarea } from '@/lib/ime-text-field' +import { ImeTextarea } from '@/lib/text-input/ime/ime-text-field' import React, { useCallback, useId, useMemo, useState } from 'react' import { cn } from '@/lib/utils' import { filterGitHubMentionOptions } from '@/components/github/github-mention-option-filter' diff --git a/src/renderer/src/components/pull-request-page/reviewers/panel.tsx b/src/renderer/src/components/pull-request-page/reviewers/panel.tsx index 678571b3c1d..960b20ad4f3 100644 --- a/src/renderer/src/components/pull-request-page/reviewers/panel.tsx +++ b/src/renderer/src/components/pull-request-page/reviewers/panel.tsx @@ -6,7 +6,7 @@ import { useRepoAssigneesBySlug } from '@/hooks/useGitHubSlugMetadata' import type { TaskSourceContext } from '../../../../../shared/work-items/task-source-context' import type { GitHubAssignableUser } from '../../../../../shared/github/pull-request-types' import type { GitHubWorkItem } from '../../../../../shared/github/work-item-types' -import { getGitHubRepoRoutingTarget } from '@/lib/github-source-runtime-context' +import { getGitHubRepoRoutingTarget } from '@/lib/work-items/github/github-source-runtime-context' import { resolvePullRequestRepo } from '@/components/github/github-work-item-identity' import { getGitHubPRReviewerRows } from '../../../../../shared/github/pr-reviewer-display' import { mergeReviewerSuggestions } from '@/components/github/work-item-state-presentation' diff --git a/src/renderer/src/components/pull-request-page/reviewers/request-actions.ts b/src/renderer/src/components/pull-request-page/reviewers/request-actions.ts index 8bd313480ef..2c13ff20a06 100644 --- a/src/renderer/src/components/pull-request-page/reviewers/request-actions.ts +++ b/src/renderer/src/components/pull-request-page/reviewers/request-actions.ts @@ -6,7 +6,7 @@ import { } from '../../../../../shared/github/pr-reviewer-display' import { notifyWorkItemDetailsMutation } from '@/components/github/github-work-item-comment-mutations' import { buildRequestedReviewUsers } from '@/components/github/work-item-state-presentation' -import { getGitHubRuntimeRepoId } from '@/lib/github-source-runtime-context' +import { getGitHubRuntimeRepoId } from '@/lib/work-items/github/github-source-runtime-context' import { translate } from '@/i18n/i18n' import type { GitHubAssignableUser, @@ -15,7 +15,7 @@ import type { import type { GitHubWorkItem } from '../../../../../shared/github/work-item-types' import type { TaskSourceContext } from '../../../../../shared/work-items/task-source-context' import type { RuntimeClientTarget } from '../../../../../shared/runtime-client-target' -import { codeHostErrorText } from '@/lib/code-host-error-copy' +import { codeHostErrorText } from '@/lib/source-control/remote/code-host-error-copy' const MAX_REQUESTED_REVIEWERS = 15 diff --git a/src/renderer/src/components/right-sidebar/ai-vault/panel/AiVaultPanelHeader.tsx b/src/renderer/src/components/right-sidebar/ai-vault/panel/AiVaultPanelHeader.tsx index 740bb07c78f..c7bea03d91b 100644 --- a/src/renderer/src/components/right-sidebar/ai-vault/panel/AiVaultPanelHeader.tsx +++ b/src/renderer/src/components/right-sidebar/ai-vault/panel/AiVaultPanelHeader.tsx @@ -1,4 +1,4 @@ -import { ImeInput } from '@/lib/ime-text-field' +import { ImeInput } from '@/lib/text-input/ime/ime-text-field' import type { KeyboardEvent, ReactNode, Ref } from 'react' import { LoaderCircle, RefreshCw, Search, X } from 'lucide-react' import { Button } from '@/components/ui/button' diff --git a/src/renderer/src/components/right-sidebar/ai-vault/session-actions/use-ai-vault-session-row-handlers.ts b/src/renderer/src/components/right-sidebar/ai-vault/session-actions/use-ai-vault-session-row-handlers.ts index 9b3a9da0cb8..c9edd6105aa 100644 --- a/src/renderer/src/components/right-sidebar/ai-vault/session-actions/use-ai-vault-session-row-handlers.ts +++ b/src/renderer/src/components/right-sidebar/ai-vault/session-actions/use-ai-vault-session-row-handlers.ts @@ -1,7 +1,7 @@ import { toast } from 'sonner' import type { AiVaultSession } from '../../../../../../shared/ai-vault/contracts/ai-vault-types' import { translate } from '@/i18n/i18n' -import { formatClipboardFilePath } from '@/lib/clipboard-file-path' +import { formatClipboardFilePath } from '@/lib/clipboard/clipboard-file-path' import { useLatestHandler } from '@/lib/use-latest-handler' import { openAiVaultSessionLogInOrca } from './ai-vault-session-log-open' import { revealAiVaultSessionPath } from './ai-vault-session-path-actions' diff --git a/src/renderer/src/components/right-sidebar/checks-panel/comments/comment-controls.tsx b/src/renderer/src/components/right-sidebar/checks-panel/comments/comment-controls.tsx index bf35d6db6cb..43da45f13cb 100644 --- a/src/renderer/src/components/right-sidebar/checks-panel/comments/comment-controls.tsx +++ b/src/renderer/src/components/right-sidebar/checks-panel/comments/comment-controls.tsx @@ -22,7 +22,7 @@ import { useAppStore } from '@/store/index' import { isReviewThreadResolutionInFlight } from '@/store/github/review-threads/review-thread-resolution-in-flight' import { basename } from '@/lib/path' import { isBotPRComment } from '../../../../../../shared/source-control/review-comments/pr-comment-audience' -import { setPRBotAuthorOverride } from '@/lib/pr-bot-author-overrides' +import { setPRBotAuthorOverride } from '@/lib/source-control/review/pr-bot-author-overrides' import { normalizePRCommentAuthorLogin } from '../../../../../../shared/source-control/review-comments/pr-bot-author-overrides' import type { PRComment } from '../../../../../../shared/github/comment-types' import type { PRCommentGroupActionState } from '@/lib/source-control/review/comments/pr-comment-action-state' diff --git a/src/renderer/src/components/right-sidebar/checks-panel/comments/right-panel-comment-composer.tsx b/src/renderer/src/components/right-sidebar/checks-panel/comments/right-panel-comment-composer.tsx index 387d5683b99..fb9ab86bde5 100644 --- a/src/renderer/src/components/right-sidebar/checks-panel/comments/right-panel-comment-composer.tsx +++ b/src/renderer/src/components/right-sidebar/checks-panel/comments/right-panel-comment-composer.tsx @@ -3,25 +3,25 @@ import { useGitHubImagePending, type GitHubImageIntake } from '@/components/github/use-github-image-uploads' -import { ImeTextarea } from '@/lib/ime-text-field' +import { ImeTextarea } from '@/lib/text-input/ime/ime-text-field' import React, { useCallback, useEffect, useRef, useState } from 'react' import { Bold, Code2, Italic, List, Quote } from 'lucide-react' import { Button } from '@/components/ui/button' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { ShortcutKeyCombo } from '@/components/ShortcutKeyCombo' import { cn } from '@/lib/utils' -import { isImeCompositionKeyDown } from '@/lib/ime-composition-keyboard-event' +import { isImeCompositionKeyDown } from '@/lib/text-input/ime/ime-composition-keyboard-event' import { getCommentBodySubmitState, hasBoundedCommentBodyText -} from '@/lib/comment-body-submit-state' +} from '@/lib/source-control/review/comments/comment-body-submit-state' import { clearRightPanelCommentFocusTimer, scheduleRightPanelCommentFocusTimer } from './right-panel-comment-focus-timers' import { translate } from '@/i18n/i18n' import { useTranslate } from '@/i18n/use-translate' -import { codeHostErrorText } from '@/lib/code-host-error-copy' +import { codeHostErrorText } from '@/lib/source-control/remote/code-host-error-copy' export type RightPanelCommentSubmitResult = { ok: true } | { ok: false; error: string } diff --git a/src/renderer/src/components/right-sidebar/checks-panel/comments/use-checks-panel-comment-mutations.tsx b/src/renderer/src/components/right-sidebar/checks-panel/comments/use-checks-panel-comment-mutations.tsx index 3c52b3a8add..7ccd8d54ec7 100644 --- a/src/renderer/src/components/right-sidebar/checks-panel/comments/use-checks-panel-comment-mutations.tsx +++ b/src/renderer/src/components/right-sidebar/checks-panel/comments/use-checks-panel-comment-mutations.tsx @@ -24,7 +24,7 @@ import type { PRComment } from '../../../../../../shared/github/comment-types' import { translate } from '@/i18n/i18n' -import { codeHostErrorText } from '@/lib/code-host-error-copy' +import { codeHostErrorText } from '@/lib/source-control/remote/code-host-error-copy' import { gitHubSlugApiForWorkspace } from '@/runtime/repository-github/github-slug-owner-api' import { errorMessage } from '../../../../../../shared/error-message' diff --git a/src/renderer/src/components/right-sidebar/checks-panel/comments/use-checks-panel-comment-resolution.tsx b/src/renderer/src/components/right-sidebar/checks-panel/comments/use-checks-panel-comment-resolution.tsx index 1ab4e5f8d83..07dfa1e3c01 100644 --- a/src/renderer/src/components/right-sidebar/checks-panel/comments/use-checks-panel-comment-resolution.tsx +++ b/src/renderer/src/components/right-sidebar/checks-panel/comments/use-checks-panel-comment-resolution.tsx @@ -21,7 +21,7 @@ import { useTranslate } from '@/i18n/use-translate' import { useAppStore } from '@/store/index' import { isReviewThreadResolutionInFlight } from '@/store/github/review-threads/review-thread-resolution-in-flight' import { worktreeLaunchWorkspace } from '@/lib/agent-session/launch/routing/agent-launch-route-input' -import { codeHostErrorText } from '@/lib/code-host-error-copy' +import { codeHostErrorText } from '@/lib/source-control/remote/code-host-error-copy' import { errorMessage } from '../../../../../../shared/error-message' import type { GitLabDiscussionResolveResult } from '../../../../../../shared/gitlab-types' diff --git a/src/renderer/src/components/right-sidebar/checks-panel/comments/use-comments-list-state.tsx b/src/renderer/src/components/right-sidebar/checks-panel/comments/use-comments-list-state.tsx index 33c90ee50c5..9f567534d71 100644 --- a/src/renderer/src/components/right-sidebar/checks-panel/comments/use-comments-list-state.tsx +++ b/src/renderer/src/components/right-sidebar/checks-panel/comments/use-comments-list-state.tsx @@ -31,7 +31,7 @@ import { usePRCommentsListSelection, type PRCommentsListSelectionClearRequest } from './pr-comments-list-selection' -import { usePRBotAuthorOverrides } from '@/lib/pr-bot-author-overrides' +import { usePRBotAuthorOverrides } from '@/lib/source-control/review/pr-bot-author-overrides' import { translate } from '@/i18n/i18n' import { useTranslate, type Translate } from '@/i18n/use-translate' import { PRCommentGroupView } from './comment-group' diff --git a/src/renderer/src/components/right-sidebar/checks-panel/presentation/active-content.tsx b/src/renderer/src/components/right-sidebar/checks-panel/presentation/active-content.tsx index c76d6f55186..53bdedb8062 100644 --- a/src/renderer/src/components/right-sidebar/checks-panel/presentation/active-content.tsx +++ b/src/renderer/src/components/right-sidebar/checks-panel/presentation/active-content.tsx @@ -2,7 +2,7 @@ import { forgeCredentialTarget, forgeOwnerHostIdForWorkspace } from '@/runtime/source-control/forge/forge-credential-target' -import { ImeInput } from '@/lib/ime-text-field' +import { ImeInput } from '@/lib/text-input/ime/ime-text-field' import React from 'react' import { Check, LoaderCircle, Pencil, X } from 'lucide-react' import { DetachedHeadBadge } from '@/components/DetachedHeadBadge' diff --git a/src/renderer/src/components/right-sidebar/checks-panel/refresh/use-checks-panel-entry-refresh-and-title-actions.tsx b/src/renderer/src/components/right-sidebar/checks-panel/refresh/use-checks-panel-entry-refresh-and-title-actions.tsx index 558b17ecc5b..3a03c574bf7 100644 --- a/src/renderer/src/components/right-sidebar/checks-panel/refresh/use-checks-panel-entry-refresh-and-title-actions.tsx +++ b/src/renderer/src/components/right-sidebar/checks-panel/refresh/use-checks-panel-entry-refresh-and-title-actions.tsx @@ -11,7 +11,7 @@ import type { ChecksPanelControllerState } from '../use-checks-panel-controller- import type { ChecksPanelReviewState } from '../use-checks-panel-review-state' import type { ChecksPanelPollingState } from './use-checks-panel-polling' import type { ChecksPanelReviewDataState } from '../use-checks-panel-review-data' -import { codeHostErrorText } from '@/lib/code-host-error-copy' +import { codeHostErrorText } from '@/lib/source-control/remote/code-host-error-copy' type ChecksPanelEntryRefreshAndTitleActionsInput = Pick< ChecksPanelContextState, diff --git a/src/renderer/src/components/right-sidebar/checks-panel/review-actions/use-checks-panel-ai-acknowledgement.tsx b/src/renderer/src/components/right-sidebar/checks-panel/review-actions/use-checks-panel-ai-acknowledgement.tsx index d732f6cabab..93561b031c3 100644 --- a/src/renderer/src/components/right-sidebar/checks-panel/review-actions/use-checks-panel-ai-acknowledgement.tsx +++ b/src/renderer/src/components/right-sidebar/checks-panel/review-actions/use-checks-panel-ai-acknowledgement.tsx @@ -22,7 +22,7 @@ import { translate } from '@/i18n/i18n' import { useTranslate } from '@/i18n/use-translate' import type { ChecksAgentComposerState } from '../panel-state-types' import type { ChecksPanelReview } from '../checks-panel-review' -import { codeHostErrorText } from '@/lib/code-host-error-copy' +import { codeHostErrorText } from '@/lib/source-control/remote/code-host-error-copy' type ChecksPanelAiAcknowledgementInput = Pick< ChecksPanelControllerState, diff --git a/src/renderer/src/components/right-sidebar/checks-panel/review-actions/use-checks-panel-check-and-review-actions.tsx b/src/renderer/src/components/right-sidebar/checks-panel/review-actions/use-checks-panel-check-and-review-actions.tsx index 11e0f4247bc..d4b8576a42a 100644 --- a/src/renderer/src/components/right-sidebar/checks-panel/review-actions/use-checks-panel-check-and-review-actions.tsx +++ b/src/renderer/src/components/right-sidebar/checks-panel/review-actions/use-checks-panel-check-and-review-actions.tsx @@ -12,7 +12,7 @@ import { } from '../../../../../../shared/source-control/checks/pr-checks-fix-prompt' import { loadGitLabJobLogDetails } from '@/runtime/repository-gitlab/gitlab-job-trace-client' -import { startFixChecksAgent } from '@/lib/fix-checks-agent-launch' +import { startFixChecksAgent } from '@/lib/source-control/checks/fix-checks-agent-launch' import { openChecksPanelHostedReviewUrl } from './checks-panel-hosted-review-click-routing' import { isMacPlatform } from '../../../terminal-pane/links/terminal-link-open-hints' import { translate } from '@/i18n/i18n' diff --git a/src/renderer/src/components/right-sidebar/file-explorer/content-search/SearchQueryRow.tsx b/src/renderer/src/components/right-sidebar/file-explorer/content-search/SearchQueryRow.tsx index 21282f1c08d..1c76f5e4e99 100644 --- a/src/renderer/src/components/right-sidebar/file-explorer/content-search/SearchQueryRow.tsx +++ b/src/renderer/src/components/right-sidebar/file-explorer/content-search/SearchQueryRow.tsx @@ -1,4 +1,4 @@ -import { ImeInput } from '@/lib/ime-text-field' +import { ImeInput } from '@/lib/text-input/ime/ime-text-field' import React from 'react' import { Search as SearchIcon, CaseSensitive, WholeWord, Regex, X, Loader2 } from 'lucide-react' import { Button } from '@/components/ui/button' diff --git a/src/renderer/src/components/right-sidebar/file-explorer/mutations/file-explorer-inline-input-row.tsx b/src/renderer/src/components/right-sidebar/file-explorer/mutations/file-explorer-inline-input-row.tsx index d30d2ab8bb7..2df47e8f4c5 100644 --- a/src/renderer/src/components/right-sidebar/file-explorer/mutations/file-explorer-inline-input-row.tsx +++ b/src/renderer/src/components/right-sidebar/file-explorer/mutations/file-explorer-inline-input-row.tsx @@ -4,7 +4,7 @@ import { FolderIcon } from '@/components/FileTypeIcon' import { isImeCompositionKeyDown, useImeEnterGestureOwnership -} from '@/lib/ime-composition-keyboard-event' +} from '@/lib/text-input/ime/ime-composition-keyboard-event' import type { TreeNode } from '../tree/file-explorer-types' export type InlineInput = { diff --git a/src/renderer/src/components/right-sidebar/file-explorer/panel/FileExplorerVirtualRows.tsx b/src/renderer/src/components/right-sidebar/file-explorer/panel/FileExplorerVirtualRows.tsx index 61480ffdcaa..e3f13264006 100644 --- a/src/renderer/src/components/right-sidebar/file-explorer/panel/FileExplorerVirtualRows.tsx +++ b/src/renderer/src/components/right-sidebar/file-explorer/panel/FileExplorerVirtualRows.tsx @@ -5,7 +5,10 @@ import { cn } from '@/lib/utils' import type { GitFileStatus } from '../../../../../../shared/git/status/git-status-types' import { FileExplorerRow } from './FileExplorerRow' import { InlineInputRow, type InlineInput } from '../mutations/file-explorer-inline-input-row' -import { shouldShowIgnoredDecoration, STATUS_COLORS } from '../../../../lib/git-path-decoration' +import { + shouldShowIgnoredDecoration, + STATUS_COLORS +} from '../../../../lib/source-control/paths/git-path-decoration' import type { DirCache, FileExplorerOperationOwner, TreeNode } from '../tree/file-explorer-types' import type { FileExplorerRowProjection } from '../tree/file-explorer-row-projection' import type { RuntimeFileOperationArgs } from '@/runtime/files/runtime-file-client-types' diff --git a/src/renderer/src/components/right-sidebar/file-explorer/selection/file-explorer-selection.ts b/src/renderer/src/components/right-sidebar/file-explorer/selection/file-explorer-selection.ts index 87a808fd939..893b9bbe3f2 100644 --- a/src/renderer/src/components/right-sidebar/file-explorer/selection/file-explorer-selection.ts +++ b/src/renderer/src/components/right-sidebar/file-explorer/selection/file-explorer-selection.ts @@ -1,5 +1,5 @@ import type { TreeNode } from '../tree/file-explorer-types' -import { formatClipboardFilePath } from '@/lib/clipboard-file-path' +import { formatClipboardFilePath } from '@/lib/clipboard/clipboard-file-path' import type { LocalPathOpenOwner } from '@/lib/local-path-open-guard' export type FileExplorerSelectionState = { diff --git a/src/renderer/src/components/right-sidebar/file-explorer/selection/useFileExplorerKeys.ts b/src/renderer/src/components/right-sidebar/file-explorer/selection/useFileExplorerKeys.ts index a94c58c8ca6..940c894b977 100644 --- a/src/renderer/src/components/right-sidebar/file-explorer/selection/useFileExplorerKeys.ts +++ b/src/renderer/src/components/right-sidebar/file-explorer/selection/useFileExplorerKeys.ts @@ -2,7 +2,7 @@ import { useEffect, useLayoutEffect, useRef } from 'react' import type React from 'react' import { toast } from 'sonner' import { useAppStore } from '@/store/index' -import { getShortcutPlatform } from '@/lib/shortcut-platform' +import { getShortcutPlatform } from '@/lib/keyboard/shortcut-platform' import type { InlineInput } from '../mutations/file-explorer-inline-input-row' import type { TreeNode } from '../tree/file-explorer-types' import type { FileExplorerRowProjection } from '../tree/file-explorer-row-projection' @@ -20,8 +20,8 @@ import { } from './file-explorer-keyboard-navigation' import { keybindingMatchesAction } from '../../../../../../shared/keybindings' import { translate } from '@/i18n/i18n' -import { isEditableTarget } from '@/lib/editable-target' -import { addAppKeyListener, removeAppKeyListener } from '@/lib/app-keyboard-router' +import { isEditableTarget } from '@/lib/text-input/focus/editable-target' +import { addAppKeyListener, removeAppKeyListener } from '@/lib/keyboard/app-keyboard-router' export function shouldIgnoreFileExplorerKeyTarget(target: EventTarget | null): boolean { return ( diff --git a/src/renderer/src/components/right-sidebar/file-explorer/tree/file-explorer-name-filter-projection.test.ts b/src/renderer/src/components/right-sidebar/file-explorer/tree/file-explorer-name-filter-projection.test.ts index de9b9d69367..6a5c1e31dcd 100644 --- a/src/renderer/src/components/right-sidebar/file-explorer/tree/file-explorer-name-filter-projection.test.ts +++ b/src/renderer/src/components/right-sidebar/file-explorer/tree/file-explorer-name-filter-projection.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import { createNameFilteredFileExplorerProjection } from './file-explorer-name-filter-projection' -import { buildIgnoredSet } from '../../../../lib/git-path-decoration' +import { buildIgnoredSet } from '../../../../lib/source-control/paths/git-path-decoration' describe('name-filter filename identity', () => { function project( diff --git a/src/renderer/src/components/right-sidebar/file-explorer/tree/file-explorer-name-filter-projection.ts b/src/renderer/src/components/right-sidebar/file-explorer/tree/file-explorer-name-filter-projection.ts index 1e3842ce898..7db4e428f49 100644 --- a/src/renderer/src/components/right-sidebar/file-explorer/tree/file-explorer-name-filter-projection.ts +++ b/src/renderer/src/components/right-sidebar/file-explorer/tree/file-explorer-name-filter-projection.ts @@ -16,7 +16,7 @@ import { type FileExplorerRowProjection } from './file-explorer-row-projection' import { isDotfileRelativePath } from './file-explorer-entries' -import { isPathIgnored } from '../../../../lib/git-path-decoration' +import { isPathIgnored } from '../../../../lib/source-control/paths/git-path-decoration' export type FileExplorerNameFilterProjectionSource = { query: string diff --git a/src/renderer/src/components/right-sidebar/file-explorer/tree/use-file-explorer-tree-pane-state.ts b/src/renderer/src/components/right-sidebar/file-explorer/tree/use-file-explorer-tree-pane-state.ts index 96f040e7da1..d34bd9d6b9a 100644 --- a/src/renderer/src/components/right-sidebar/file-explorer/tree/use-file-explorer-tree-pane-state.ts +++ b/src/renderer/src/components/right-sidebar/file-explorer/tree/use-file-explorer-tree-pane-state.ts @@ -9,7 +9,10 @@ import type { Repo } from '../../../../../../shared/repo-types' import type { GitStatusEntry } from '../../../../../../shared/git/status/git-status-types' import type { FileExplorerRowProjection } from './file-explorer-row-projection' import type { TreeNode } from './file-explorer-types' -import { buildFolderStatusMap, buildStatusMap } from '../../../../lib/git-path-decoration' +import { + buildFolderStatusMap, + buildStatusMap +} from '../../../../lib/source-control/paths/git-path-decoration' import { useFileDeletion } from '../mutations/useFileDeletion' import { useFileExplorerDragDrop } from '../mutations/useFileExplorerDragDrop' import { useFileExplorerHandlers } from '../mutations/useFileExplorerHandlers' diff --git a/src/renderer/src/components/right-sidebar/file-explorer/tree/useFileExplorerVisibleRowProjection.ts b/src/renderer/src/components/right-sidebar/file-explorer/tree/useFileExplorerVisibleRowProjection.ts index 6d1365b46b0..39a2e9277f7 100644 --- a/src/renderer/src/components/right-sidebar/file-explorer/tree/useFileExplorerVisibleRowProjection.ts +++ b/src/renderer/src/components/right-sidebar/file-explorer/tree/useFileExplorerVisibleRowProjection.ts @@ -6,7 +6,10 @@ import { createFileExplorerRowProjectionFromParts, type FileExplorerRowProjection } from './file-explorer-row-projection' -import { buildIgnoredSet, isPathIgnored } from '../../../../lib/git-path-decoration' +import { + buildIgnoredSet, + isPathIgnored +} from '../../../../lib/source-control/paths/git-path-decoration' import { createNameFilteredFileExplorerProjection, getFileExplorerNameFilterExpandedPaths, diff --git a/src/renderer/src/components/right-sidebar/hosted-review/provider-actions/use-hosted-review-actions.ts b/src/renderer/src/components/right-sidebar/hosted-review/provider-actions/use-hosted-review-actions.ts index e8c3372c464..492b7056a3a 100644 --- a/src/renderer/src/components/right-sidebar/hosted-review/provider-actions/use-hosted-review-actions.ts +++ b/src/renderer/src/components/right-sidebar/hosted-review/provider-actions/use-hosted-review-actions.ts @@ -19,7 +19,7 @@ import { translate } from '@/i18n/i18n' import { getHostedReviewStateChangeCopy } from './hosted-review-state-change-copy' import { buildGitHubPRStackMergeConfirmation } from './github-pr-stack-confirmation' import { useReadyHostedReviewAction } from './use-ready-hosted-review-action' -import { codeHostErrorText } from '@/lib/code-host-error-copy' +import { codeHostErrorText } from '@/lib/source-control/remote/code-host-error-copy' export type HostedReviewActionInfo = Pick< HostedReviewInfo, diff --git a/src/renderer/src/components/right-sidebar/hosted-review/provider-actions/use-ready-hosted-review-action.ts b/src/renderer/src/components/right-sidebar/hosted-review/provider-actions/use-ready-hosted-review-action.ts index 88041058383..28eb23a865e 100644 --- a/src/renderer/src/components/right-sidebar/hosted-review/provider-actions/use-ready-hosted-review-action.ts +++ b/src/renderer/src/components/right-sidebar/hosted-review/provider-actions/use-ready-hosted-review-action.ts @@ -5,7 +5,7 @@ import type { Repo } from '../../../../../../shared/repo-types' import { translate } from '@/i18n/i18n' import { markGitHubHostedReviewReadyForReview } from './hosted-review-github-actions' import { markGitLabHostedReviewReadyForReview } from './hosted-review-gitlab-actions' -import { codeHostErrorText } from '@/lib/code-host-error-copy' +import { codeHostErrorText } from '@/lib/source-control/remote/code-host-error-copy' export function useReadyHostedReviewAction({ reviewNumber, diff --git a/src/renderer/src/components/right-sidebar/source-control/commit/use-commit-action.ts b/src/renderer/src/components/right-sidebar/source-control/commit/use-commit-action.ts index c19f3ada631..341948f29ac 100644 --- a/src/renderer/src/components/right-sidebar/source-control/commit/use-commit-action.ts +++ b/src/renderer/src/components/right-sidebar/source-control/commit/use-commit-action.ts @@ -1,4 +1,5 @@ -import { GIT_GRAPH_REFRESH_EVENT } from '@/lib/git-graph-tab' +import '@/lib/source-control/graph/git-graph-tab' +import { GIT_GRAPH_REFRESH_EVENT } from '@/lib/source-control/graph/git-graph-refresh-event' import { useCallback } from 'react' import { toast } from 'sonner' import { requireGitOwnerTarget } from '../sync/worktree-git-owner-target' diff --git a/src/renderer/src/components/right-sidebar/source-control/listing/branch-entry-row.tsx b/src/renderer/src/components/right-sidebar/source-control/listing/branch-entry-row.tsx index 671eb75db1b..4ab91678aff 100644 --- a/src/renderer/src/components/right-sidebar/source-control/listing/branch-entry-row.tsx +++ b/src/renderer/src/components/right-sidebar/source-control/listing/branch-entry-row.tsx @@ -15,7 +15,7 @@ import { SOURCE_CONTROL_TREE_FILE_PADDING_PX, SOURCE_CONTROL_TREE_INDENT_PX } from './row-layout' -import { STATUS_COLORS } from '../../../../lib/git-path-decoration' +import { STATUS_COLORS } from '../../../../lib/source-control/paths/git-path-decoration' import { STATUS_LABELS } from '../../../../../../shared/git/status/git-status-labels' import { useTranslate } from '@/i18n/use-translate' diff --git a/src/renderer/src/components/right-sidebar/source-control/listing/entry-context-menu.tsx b/src/renderer/src/components/right-sidebar/source-control/listing/entry-context-menu.tsx index 187fee2e754..f1f97e746cf 100644 --- a/src/renderer/src/components/right-sidebar/source-control/listing/entry-context-menu.tsx +++ b/src/renderer/src/components/right-sidebar/source-control/listing/entry-context-menu.tsx @@ -17,7 +17,7 @@ import { } from '@/lib/reveal-in-file-manager' import { isRevealOrEditorOpenBlocked } from '@/lib/local-path-open-guard' import { runtimeEnvironmentIdOrLocalUnlessTwin } from '@/lib/workspace/ownership/worktree-runtime-owner' -import { formatClipboardFilePath } from '@/lib/clipboard-file-path' +import { formatClipboardFilePath } from '@/lib/clipboard/clipboard-file-path' import type { ConflictResolution } from '../../../../../../shared/source-control/listing/source-control-entry-actions' import { CONFLICT_RESOLUTION_ICONS, getLocalizedConflictResolutionLabel } from './conflict-label' import { OpenInContextSubmenu } from '@/components/file-actions/OpenInContextSubmenu' diff --git a/src/renderer/src/components/right-sidebar/source-control/listing/sequencer-recovery-controls.test.tsx b/src/renderer/src/components/right-sidebar/source-control/listing/sequencer-recovery-controls.test.tsx index 1e40bcf651c..95849f0b337 100644 --- a/src/renderer/src/components/right-sidebar/source-control/listing/sequencer-recovery-controls.test.tsx +++ b/src/renderer/src/components/right-sidebar/source-control/listing/sequencer-recovery-controls.test.tsx @@ -4,7 +4,8 @@ import { createRoot } from 'react-dom/client' import { expect, it, vi } from 'vitest' import { useAppStore } from '@/store' import { SequencerRecoveryControls } from './sequencer-recovery-controls' -import { GIT_GRAPH_REFRESH_EVENT } from '@/lib/git-graph-tab' +import '@/lib/source-control/graph/git-graph-tab' +import { GIT_GRAPH_REFRESH_EVENT } from '@/lib/source-control/graph/git-graph-refresh-event' import { local } from '@/components/editor/git-graph/git-graph-workspace.test-fixtures' import type * as WorktreeGitCopy from '@/lib/worktree-git-copy' import type { GitStatusResult } from '../../../../../../shared/git/status/git-status-types' diff --git a/src/renderer/src/components/right-sidebar/source-control/listing/sequencer-recovery-controls.tsx b/src/renderer/src/components/right-sidebar/source-control/listing/sequencer-recovery-controls.tsx index 08990451968..2c47a44df77 100644 --- a/src/renderer/src/components/right-sidebar/source-control/listing/sequencer-recovery-controls.tsx +++ b/src/renderer/src/components/right-sidebar/source-control/listing/sequencer-recovery-controls.tsx @@ -12,7 +12,8 @@ import { sequencerAbortRuntimeGit, sequencerContinueRuntimeGit } from '@/runtime/source-control/git/runtime-git-commit-actions-client' -import { GIT_GRAPH_REFRESH_EVENT } from '@/lib/git-graph-tab' +import '@/lib/source-control/graph/git-graph-tab' +import { GIT_GRAPH_REFRESH_EVENT } from '@/lib/source-control/graph/git-graph-refresh-event' import { extractIpcErrorMessage } from '@/lib/ipc-error' import { useSourceControlWorktreeContext } from './use-worktree-context' import type { GitCommitAction } from '../../../../../../shared/git/commands/git-commit-action' diff --git a/src/renderer/src/components/right-sidebar/source-control/listing/uncommitted-entry-row.tsx b/src/renderer/src/components/right-sidebar/source-control/listing/uncommitted-entry-row.tsx index d7cc98880f9..7f0db7f23a5 100644 --- a/src/renderer/src/components/right-sidebar/source-control/listing/uncommitted-entry-row.tsx +++ b/src/renderer/src/components/right-sidebar/source-control/listing/uncommitted-entry-row.tsx @@ -35,7 +35,7 @@ import { getSubmoduleWorktreeOnlyLabel, getSubmoduleWorktreeOnlyTooltip } from './row-layout' -import { STATUS_COLORS } from '../../../../lib/git-path-decoration' +import { STATUS_COLORS } from '../../../../lib/source-control/paths/git-path-decoration' import { STATUS_LABELS } from '../../../../../../shared/git/status/git-status-labels' export const UncommittedEntryRow = React.memo(function UncommittedEntryRow({ diff --git a/src/renderer/src/components/right-sidebar/source-control/notes/diff-comments-list.tsx b/src/renderer/src/components/right-sidebar/source-control/notes/diff-comments-list.tsx index 15fdecb3e7b..bcb9315572a 100644 --- a/src/renderer/src/components/right-sidebar/source-control/notes/diff-comments-list.tsx +++ b/src/renderer/src/components/right-sidebar/source-control/notes/diff-comments-list.tsx @@ -1,7 +1,10 @@ import React, { useCallback, useMemo } from 'react' import { Check, Copy, Trash, Trash2 } from 'lucide-react' -import { getDiffCommentLineLabel, getDiffCommentSource } from '@/lib/diff-comment-compat' -import { formatDiffComment } from '@/lib/diff-comments-format' +import { + getDiffCommentLineLabel, + getDiffCommentSource +} from '@/lib/source-control/review/comments/diff-comment-compat' +import { formatDiffComment } from '../../../../../../shared/diff-comments-format' import { translate } from '@/i18n/i18n' import { useTranslate, type Translate } from '@/i18n/use-translate' import type { DiffComment } from '../../../../../../shared/diff-comment-types' diff --git a/src/renderer/src/components/right-sidebar/source-control/notes/use-diff-comment-notes.ts b/src/renderer/src/components/right-sidebar/source-control/notes/use-diff-comment-notes.ts index a496b59310a..039cfc053ad 100644 --- a/src/renderer/src/components/right-sidebar/source-control/notes/use-diff-comment-notes.ts +++ b/src/renderer/src/components/right-sidebar/source-control/notes/use-diff-comment-notes.ts @@ -5,8 +5,8 @@ import { useCallback, useMemo, useState } from 'react' import { toast } from 'sonner' import { translate } from '@/i18n/i18n' import { useTranslate } from '@/i18n/use-translate' -import { formatDiffComments } from '@/lib/diff-comments-format' -import { describeClipboardWriteFailure } from '@/lib/clipboard-write-failure' +import { formatDiffComments } from '../../../../../../shared/diff-comments-format' +import { describeClipboardWriteFailure } from '@/lib/clipboard/clipboard-write-failure' import { useVisibleWorktreeDiffComments } from '../../../diff-comments/use-visible-worktree-diff-comments' import { countPendingDiffCommentsClear, diff --git a/src/renderer/src/components/right-sidebar/source-control/notes/use-note-opening.ts b/src/renderer/src/components/right-sidebar/source-control/notes/use-note-opening.ts index e1e86bdd797..68d65154cb6 100644 --- a/src/renderer/src/components/right-sidebar/source-control/notes/use-note-opening.ts +++ b/src/renderer/src/components/right-sidebar/source-control/notes/use-note-opening.ts @@ -1,6 +1,6 @@ import { useCallback, useRef, type RefObject } from 'react' import { detectLanguage } from '../../../../../../shared/editor-language/editor-language-detect' -import { getDiffCommentSource } from '@/lib/diff-comment-compat' +import { getDiffCommentSource } from '@/lib/source-control/review/comments/diff-comment-compat' import { joinPath } from '@/lib/path' import { useAppStore } from '@/store' import type { diff --git a/src/renderer/src/components/right-sidebar/source-control/panel/dialog-layer.tsx b/src/renderer/src/components/right-sidebar/source-control/panel/dialog-layer.tsx index 653d75ee888..2bf68abe1ed 100644 --- a/src/renderer/src/components/right-sidebar/source-control/panel/dialog-layer.tsx +++ b/src/renderer/src/components/right-sidebar/source-control/panel/dialog-layer.tsx @@ -171,7 +171,8 @@ export function SourceControlDialogLayer({ )} -
+ {/* Why: overflow clips the search field's 3px focus ring; -m-1/p-1 makes room without shifting it. */} +
Promise.resolve()) diff --git a/src/renderer/src/components/right-sidebar/ssh-port-forward-dialog.tsx b/src/renderer/src/components/right-sidebar/ssh-port-forward-dialog.tsx index 2c7302e3786..cc7847c647d 100644 --- a/src/renderer/src/components/right-sidebar/ssh-port-forward-dialog.tsx +++ b/src/renderer/src/components/right-sidebar/ssh-port-forward-dialog.tsx @@ -10,7 +10,7 @@ import { Button } from '@/components/ui/button' import type { PortForwardEntry } from '../../../../shared/ssh-types' import { translate } from '@/i18n/i18n' import { useTranslate } from '@/i18n/use-translate' -import { ImeInput } from '@/lib/ime-text-field' +import { ImeInput } from '@/lib/text-input/ime/ime-text-field' // Remap privileged ports to avoid EACCES when binding locally without root. function safeLocalPort(remotePort: number): number { diff --git a/src/renderer/src/components/settings/__fixtures__/settings-persistence-test-fixture.ts b/src/renderer/src/components/settings/__fixtures__/settings-persistence-test-fixture.ts index 7e30c7ecc3f..39c0a6d6099 100644 --- a/src/renderer/src/components/settings/__fixtures__/settings-persistence-test-fixture.ts +++ b/src/renderer/src/components/settings/__fixtures__/settings-persistence-test-fixture.ts @@ -91,12 +91,12 @@ export function settingsModel(): SettingsPersistenceModel { hasUnsavedBranchPromptChanges: false, hasUnsavedChatPromptChanges: false, hasUnsavedCommitPromptChanges: false, - highlightedSettingsTargetId: null, + highlightedSettingsTarget: null, setFontSuggestions: vi.fn(), setHasUnsavedBranchPromptChanges: vi.fn(), setHasUnsavedChatPromptChanges: vi.fn(), setHasUnsavedCommitPromptChanges: vi.fn(), - setHighlightedSettingsTargetId: vi.fn(), + setHighlightedSettingsTarget: vi.fn(), setSettingsSearchQuery: vi.fn(), setSourceControlAiPromptDiscardSignal: vi.fn() } satisfies SettingsPersistenceModel diff --git a/src/renderer/src/components/settings/accounts/accounts-claude-search.ts b/src/renderer/src/components/settings/accounts/accounts-claude-search.ts index a5ac0bccc7b..6ea25153719 100644 --- a/src/renderer/src/components/settings/accounts/accounts-claude-search.ts +++ b/src/renderer/src/components/settings/accounts/accounts-claude-search.ts @@ -4,7 +4,7 @@ import { translateSearchKeyword } from '../search/settings-search-keywords' export const getAccountsClaudeSearchEntries = createLocalizedCatalog(() => [ { - title: translate('auto.components.settings.accounts.search.75682e1b62', 'Claude Accounts'), + title: translate('auto.components.settings.accounts.search.be3a748ebb', 'Claude accounts'), description: translate( 'auto.components.settings.accounts.search.dd75a73991', 'Optional account switching for Claude while preserving shared chat context.' diff --git a/src/renderer/src/components/settings/accounts/accounts-pane-removal-dialogs.tsx b/src/renderer/src/components/settings/accounts/accounts-pane-removal-dialogs.tsx index efd18c7f1c1..377ad532bcb 100644 --- a/src/renderer/src/components/settings/accounts/accounts-pane-removal-dialogs.tsx +++ b/src/renderer/src/components/settings/accounts/accounts-pane-removal-dialogs.tsx @@ -65,7 +65,7 @@ export function renderAccountsRemovalDialogs( ) }} > - {t('auto.components.settings.AccountsPane.c2d2751587', 'Remove Account')} + {t('auto.components.settings.AccountsPane.699680a328', 'Remove account')} @@ -105,7 +105,7 @@ export function renderAccountsRemovalDialogs( ) }} > - {t('auto.components.settings.AccountsPane.c2d2751587', 'Remove Account')} + {t('auto.components.settings.AccountsPane.699680a328', 'Remove account')} diff --git a/src/renderer/src/components/settings/accounts/accounts-search.ts b/src/renderer/src/components/settings/accounts/accounts-search.ts index fd78aba0d15..dbf6bdb196f 100644 --- a/src/renderer/src/components/settings/accounts/accounts-search.ts +++ b/src/renderer/src/components/settings/accounts/accounts-search.ts @@ -6,7 +6,7 @@ import { getAccountsClaudeSearchEntries } from './accounts-claude-search' export const getAccountsLocationSearchEntries = createLocalizedCatalog(() => [ { - title: translate('auto.components.settings.accounts.search.d09fb5ca92', 'Account Location'), + title: translate('auto.components.settings.accounts.search.5e2101f61f', 'Account location'), description: translate( 'auto.components.settings.accounts.search.b84a5b0c8a', 'Choose whether provider accounts are inspected and added on this device or in WSL.' @@ -25,7 +25,7 @@ export const getAccountsLocationSearchEntries = createLocalizedCatalog(() => [ export const getAccountsCodexSearchEntries = createLocalizedCatalog(() => [ { - title: translate('auto.components.settings.accounts.search.17c5d244eb', 'Codex Accounts'), + title: translate('auto.components.settings.accounts.search.54c541fc6e', 'Codex accounts'), description: translate( 'auto.components.settings.accounts.search.b40d5b6570', 'Optional account switching for Codex and live rate limit fetching.' @@ -55,7 +55,7 @@ export const getAccountsCodexSearchEntries = createLocalizedCatalog(() => [ ] }, { - title: translate('auto.components.settings.accounts.search.a4bcfd6f86', 'Active Codex Account'), + title: translate('auto.components.settings.accounts.search.4518496456', 'Active Codex account'), description: translate( 'auto.components.settings.accounts.search.87a4a8584e', 'Choose which optional saved Codex account powers live quota reads.' @@ -109,8 +109,8 @@ export const getAccountsGeminiSearchEntries = createLocalizedCatalog(() => [ export const getAccountsOpencodeSearchEntries = createLocalizedCatalog(() => [ { title: translate( - 'auto.components.settings.accounts.search.6ed1401020', - 'OpenCode Go Session Cookie' + 'auto.components.settings.accounts.search.f0e6678f99', + 'OpenCode Go session cookie' ), description: translate( 'auto.components.settings.accounts.search.25591bf95b', @@ -130,8 +130,8 @@ export const getAccountsOpencodeSearchEntries = createLocalizedCatalog(() => [ }, { title: translate( - 'auto.components.settings.accounts.search.4ee2029e9c', - 'OpenCode Go Workspace ID' + 'auto.components.settings.accounts.search.8eef999193', + 'OpenCode Go workspace id' ), description: translate( 'auto.components.settings.accounts.search.38d22ff8d6', @@ -153,7 +153,7 @@ export const getAccountsOpencodeSearchEntries = createLocalizedCatalog(() => [ export const getAccountsMiniMaxSearchEntries = createLocalizedCatalog(() => [ { - title: translate('auto.components.settings.accounts.search.733f9e2a93', 'MiniMax Usage'), + title: translate('auto.components.settings.accounts.search.9b6ce52c36', 'MiniMax usage'), description: translate( 'auto.components.settings.accounts.search.f8374c3151', 'Configure MiniMax usage tracking. Pick the overseas or China endpoint, then paste a session cookie or save an API key that works on either host.' @@ -198,7 +198,7 @@ export const getAccountsGrokSearchEntries = createLocalizedCatalog(() => [ export const getAccountsAntigravitySearchEntries = createLocalizedCatalog(() => [ { - title: translate('accounts.antigravity.searchTitle', 'Antigravity Accounts'), + title: translate('accounts.antigravity.fd47b5a5dd', 'Antigravity accounts'), description: translate( 'accounts.antigravity.searchDescription', 'Save and select native agy Google accounts on the execution host.' @@ -216,7 +216,7 @@ export const getAccountsAntigravitySearchEntries = createLocalizedCatalog(() => export const getAccountsCursorSearchEntries = createLocalizedCatalog(() => [ { - title: translate('auto.components.settings.accounts.search.cursor.title', 'Cursor Usage'), + title: translate('auto.components.settings.accounts.search.cursor.39bbdebe13', 'Cursor usage'), description: translate( 'auto.components.settings.accounts.search.cursor.description', 'Plan usage read from the Cursor sign-in already on this computer (cursor-agent login).' @@ -248,7 +248,10 @@ export const getAccountsCursorSearchEntries = createLocalizedCatalog(() => [ export const getAccountsZcodePlanSearchEntries = createLocalizedCatalog(() => [ { - title: translate('auto.components.settings.accounts.search.zcodePlan.title', 'GLM Coding Plan'), + title: translate( + 'auto.components.settings.accounts.search.zcodePlan.0db9d0465f', + 'GLM coding plan' + ), description: translate( 'auto.components.settings.accounts.search.zcodePlan.description', 'Track Z.AI or Zhipu (BigModel) GLM Coding Plan usage. Pick the site and save the plan API key.' diff --git a/src/renderer/src/components/settings/accounts/claude/accounts-pane-claude-section.tsx b/src/renderer/src/components/settings/accounts/claude/accounts-pane-claude-section.tsx index 117ef72b818..bc05589e570 100644 --- a/src/renderer/src/components/settings/accounts/claude/accounts-pane-claude-section.tsx +++ b/src/renderer/src/components/settings/accounts/claude/accounts-pane-claude-section.tsx @@ -98,7 +98,7 @@ export function renderClaudeAccountsSection( )} > )} - {t('auto.components.settings.AccountsPane.b0e948a4f9', 'Add Account')} + {t('auto.components.settings.AccountsPane.7e9977b560', 'Add account')} {!isRemoteAccountScope && canOfferClaudeSignInLink(accountRuntime.runtime) ? ( )} - {t('auto.components.settings.AccountsPane.b0e948a4f9', 'Add Account')} + {t('auto.components.settings.AccountsPane.7e9977b560', 'Add account')} {/* Follow the running addition; hide Cancel only when its server prompt already offers it. */} {codexAction === 'adding' && !accountAdditionShownInPrompt('codex', accountOwner) ? ( diff --git a/src/renderer/src/components/settings/accounts/providers/accounts-pane-opencode-section.tsx b/src/renderer/src/components/settings/accounts/providers/accounts-pane-opencode-section.tsx index 3c166ba7143..251606f4ca9 100644 --- a/src/renderer/src/components/settings/accounts/providers/accounts-pane-opencode-section.tsx +++ b/src/renderer/src/components/settings/accounts/providers/accounts-pane-opencode-section.tsx @@ -32,8 +32,8 @@ export function renderOpenCodeAccountsSection(
{cookieNotice ?

{cookieNotice}

: null} diff --git a/src/renderer/src/components/settings/accounts/usage/accounts-pane-opencode-credentials.tsx b/src/renderer/src/components/settings/accounts/usage/accounts-pane-opencode-credentials.tsx index d56984df1b0..c4d7acc10c9 100644 --- a/src/renderer/src/components/settings/accounts/usage/accounts-pane-opencode-credentials.tsx +++ b/src/renderer/src/components/settings/accounts/usage/accounts-pane-opencode-credentials.tsx @@ -58,8 +58,8 @@ export function OpenCodeGoCredentials({ onSaved }: { onSaved: () => void }): Rea return ( diff --git a/src/renderer/src/components/settings/advanced/ExperimentalPane.tsx b/src/renderer/src/components/settings/advanced/ExperimentalPane.tsx index d50f0698959..941d1a333ea 100644 --- a/src/renderer/src/components/settings/advanced/ExperimentalPane.tsx +++ b/src/renderer/src/components/settings/advanced/ExperimentalPane.tsx @@ -36,18 +36,18 @@ export function ExperimentalPane({ }: ExperimentalPaneProps): React.JSX.Element { const t = useTranslate() const searchQuery = useAppStore((s) => s.settingsSearchQuery) - const showPet = matchesSettingsSearch(searchQuery, [getExperimentalSearchEntry(t).pet]) + const showPet = matchesSettingsSearch(searchQuery, [getExperimentalSearchEntry().pet]) const showAgentDashboard = matchesSettingsSearch(searchQuery, [ - getExperimentalSearchEntry(t).agentDashboard + getExperimentalSearchEntry().agentDashboard ]) const showTerminalAttention = matchesSettingsSearch(searchQuery, [ - getExperimentalSearchEntry(t).terminalAttention + getExperimentalSearchEntry().terminalAttention ]) const showAgentHibernation = matchesSettingsSearch(searchQuery, [ - getExperimentalSearchEntry(t).agentHibernation + getExperimentalSearchEntry().agentHibernation ]) const showNewWorktreeCardStyle = matchesSettingsSearch(searchQuery, [ - getExperimentalSearchEntry(t).newWorktreeCardStyle + getExperimentalSearchEntry().newWorktreeCardStyle ]) const agentHibernationEnabled = settings.experimentalAgentHibernation === true const newWorktreeCardStyleEnabled = settings.experimentalNewWorktreeCardStyle === true @@ -70,7 +70,7 @@ export function ExperimentalPane({ 'auto.components.settings.ExperimentalPane.0e89a574ae', 'Floating animated pet in the bottom-right corner.' )} - keywords={getExperimentalSearchEntry(t).pet.keywords} + keywords={getExperimentalSearchEntry().pet.keywords} className="space-y-3 py-3" id="experimental-pet" > @@ -102,7 +102,7 @@ export function ExperimentalPane({ 'auto.components.settings.ExperimentalPane.88b7613afb', 'Persistent pane highlight for terminal bell and agent-completion events.' )} - keywords={getExperimentalSearchEntry(t).terminalAttention.keywords} + keywords={getExperimentalSearchEntry().terminalAttention.keywords} className="space-y-3 py-3" >
@@ -143,7 +143,7 @@ export function ExperimentalPane({ 'auto.components.settings.ExperimentalPane.agentHibernation.description', 'Stops idle background agent terminals after the configured idle window and resumes supported sessions when you open them again.' )} - keywords={getExperimentalSearchEntry(t).agentHibernation.keywords} + keywords={getExperimentalSearchEntry().agentHibernation.keywords} className="space-y-3 py-3" id="experimental-agent-hibernation" > @@ -216,7 +216,7 @@ export function ExperimentalPane({ 'auto.components.settings.ExperimentalPane.newWorktreeCardStyle.description', 'Preview updated worktree-card layout, metadata placement, card-display menu options, and status presentation.' )} - keywords={getExperimentalSearchEntry(t).newWorktreeCardStyle.keywords} + keywords={getExperimentalSearchEntry().newWorktreeCardStyle.keywords} className="space-y-3 py-3" id="experimental-new-worktree-card-style" > diff --git a/src/renderer/src/components/settings/advanced/HiddenExperimentalGroup.tsx b/src/renderer/src/components/settings/advanced/HiddenExperimentalGroup.tsx index 6de87e81686..cbb7c53bc59 100644 --- a/src/renderer/src/components/settings/advanced/HiddenExperimentalGroup.tsx +++ b/src/renderer/src/components/settings/advanced/HiddenExperimentalGroup.tsx @@ -6,9 +6,9 @@ import { setTerminalRenderDesyncSentinelArmed } from '../../terminal-pane/render-health/terminal-render-desync-trigger' import { useTranslate } from '@/i18n/use-translate' -import { getShortcutPlatform } from '@/lib/shortcut-platform' +import { getShortcutPlatform } from '@/lib/keyboard/shortcut-platform' -// Use shared orange styling to identify unfinished/staff-only controls. +// Warning tokens distinguish unfinished controls across themes. export function HiddenExperimentalGroup(): React.JSX.Element { const t = useTranslate() const isMac = getShortcutPlatform() === 'darwin' @@ -20,23 +20,26 @@ export function HiddenExperimentalGroup(): React.JSX.Element { setRenderDiagnosticsArmed(armed) } return ( -
-

+
+

{t( 'auto.components.settings.HiddenExperimentalGroup.f2c81d904a', 'Hidden experimental settings' )}

-
+
-
diff --git a/src/renderer/src/components/settings/advanced/advanced-search.ts b/src/renderer/src/components/settings/advanced/advanced-search.ts index f252ae2f56c..5b8f97f6205 100644 --- a/src/renderer/src/components/settings/advanced/advanced-search.ts +++ b/src/renderer/src/components/settings/advanced/advanced-search.ts @@ -2,61 +2,72 @@ import type { SettingsSearchEntry } from '../search/settings-search' import { getAdvancedNetworkSearchEntries } from './network/advanced-network-search' import { createLocalizedCatalog } from '@/i18n/localized-catalog' import { translate } from '@/i18n/i18n' -import type { Translate } from '@/i18n/use-translate' import { translateSearchKeyword } from '../search/settings-search-keywords' +export const getAdvancedSearchEntry = createLocalizedCatalog( + () => + ({ + http1Compatibility: { + title: translate( + 'auto.components.settings.advanced.search.11eea3da72', + 'HTTP/1.1 Compatibility' + ), + description: translate( + 'auto.components.settings.advanced.search.585f56fae0', + 'Use HTTP/1.1 for Electron networking when HTTP/2 fails behind a proxy.' + ), + keywords: [ + ...translateSearchKeyword( + 'auto.components.settings.advanced.search.e04e9db503', + 'advanced' + ), + ...translateSearchKeyword( + 'auto.components.settings.advanced.search.2b4d26d11e', + 'networking' + ), + ...translateSearchKeyword( + 'auto.components.settings.advanced.search.4d44352eea', + 'network' + ), + ...translateSearchKeyword('auto.components.settings.advanced.search.48a1c8f534', 'http'), + ...translateSearchKeyword('auto.components.settings.advanced.search.4b4ae4345a', 'http2'), + ...translateSearchKeyword( + 'auto.components.settings.advanced.search.a0f71bd909', + 'http/2' + ), + ...translateSearchKeyword('auto.components.settings.advanced.search.f8ff125ebe', 'http1'), + ...translateSearchKeyword( + 'auto.components.settings.advanced.search.621233008b', + 'http/1.1' + ), + ...translateSearchKeyword( + 'auto.components.settings.advanced.search.65bf6af262', + 'compatibility' + ), + ...translateSearchKeyword('auto.components.settings.advanced.search.f98a60af11', 'proxy'), + ...translateSearchKeyword('auto.components.settings.advanced.search.4383251647', 'vpn'), + ...translateSearchKeyword( + 'auto.components.settings.advanced.search.79e0947e95', + 'support' + ), + ...translateSearchKeyword( + 'auto.components.settings.advanced.search.6576fce4d2', + 'troubleshooting' + ), + ...translateSearchKeyword( + 'auto.components.settings.advanced.search.e61ed8ab33', + 'updates' + ), + ...translateSearchKeyword( + 'auto.components.settings.advanced.search.a7002e1ac4', + 'updater' + ) + ] + } + }) satisfies Record +) + export const getAdvancedPaneSearchEntries = createLocalizedCatalog((): SettingsSearchEntry[] => [ ...getAdvancedNetworkSearchEntries(), - { - title: translate( - 'auto.components.settings.advanced.search.11eea3da72', - 'HTTP/1.1 Compatibility' - ), - description: translate( - 'auto.components.settings.advanced.search.585f56fae0', - 'Use HTTP/1.1 for Electron networking when HTTP/2 fails behind a proxy.' - ), - keywords: [ - ...translateSearchKeyword('auto.components.settings.advanced.search.e04e9db503', 'advanced'), - ...translateSearchKeyword( - 'auto.components.settings.advanced.search.2b4d26d11e', - 'networking' - ), - ...translateSearchKeyword('auto.components.settings.advanced.search.4d44352eea', 'network'), - ...translateSearchKeyword('auto.components.settings.advanced.search.48a1c8f534', 'http'), - ...translateSearchKeyword('auto.components.settings.advanced.search.4b4ae4345a', 'http2'), - ...translateSearchKeyword('auto.components.settings.advanced.search.a0f71bd909', 'http/2'), - ...translateSearchKeyword('auto.components.settings.advanced.search.f8ff125ebe', 'http1'), - ...translateSearchKeyword('auto.components.settings.advanced.search.621233008b', 'http/1.1'), - ...translateSearchKeyword( - 'auto.components.settings.advanced.search.65bf6af262', - 'compatibility' - ), - ...translateSearchKeyword('auto.components.settings.advanced.search.f98a60af11', 'proxy'), - ...translateSearchKeyword('auto.components.settings.advanced.search.4383251647', 'vpn'), - ...translateSearchKeyword('auto.components.settings.advanced.search.79e0947e95', 'support'), - ...translateSearchKeyword( - 'auto.components.settings.advanced.search.6576fce4d2', - 'troubleshooting' - ), - ...translateSearchKeyword('auto.components.settings.advanced.search.e61ed8ab33', 'updates'), - ...translateSearchKeyword('auto.components.settings.advanced.search.a7002e1ac4', 'updater') - ] - } + getAdvancedSearchEntry().http1Compatibility ]) - -function findEntry(title: string): SettingsSearchEntry { - const entry = getAdvancedPaneSearchEntries().find((e) => e.title === title) - if (!entry) { - throw new Error(`Missing advanced-pane search entry: "${title}"`) - } - return entry -} - -export function getAdvancedSearchEntry(t: Translate = translate) { - return { - http1Compatibility: findEntry( - t('auto.components.settings.advanced.search.11eea3da72', 'HTTP/1.1 Compatibility') - ) - } as const -} diff --git a/src/renderer/src/components/settings/advanced/experimental-search.ts b/src/renderer/src/components/settings/advanced/experimental-search.ts index 97f63766797..d69b5528840 100644 --- a/src/renderer/src/components/settings/advanced/experimental-search.ts +++ b/src/renderer/src/components/settings/advanced/experimental-search.ts @@ -1,227 +1,200 @@ import type { SettingsSearchEntry } from '../search/settings-search' import { createLocalizedCatalog } from '@/i18n/localized-catalog' import { translate } from '@/i18n/i18n' -import type { Translate } from '@/i18n/use-translate' import { translateSearchKeyword } from '../search/settings-search-keywords' import { getNewWorktreeCardStyleSearchEntry } from './new-worktree-card-style-search-entry' import { getEphemeralVmsSearchEntry } from '../virtual-machines/recipes/ephemeral-vms-search' import { getBrowserExtensionsExperimentalSearchEntry } from '../browser/extensions/browser-extensions-experimental-search' import { getWslHostsExperimentalSearchEntry } from '../wsl/hosts/wsl-hosts-experimental-search' +export const getExperimentalSearchEntry = createLocalizedCatalog( + () => + ({ + pet: { + title: translate('auto.components.settings.experimental.search.87d99e634b', 'Pet'), + description: translate( + 'auto.components.settings.experimental.search.6b5a56ac35', + 'Floating animated pet in the bottom-right corner.' + ), + keywords: [ + ...translateSearchKeyword( + 'auto.components.settings.experimental.search.0d24759f14', + 'experimental' + ), + ...translateSearchKeyword( + 'auto.components.settings.experimental.search.051203d37c', + 'pet' + ), + ...translateSearchKeyword( + 'auto.components.settings.experimental.search.b54cea709b', + 'sidekick' + ), + ...translateSearchKeyword( + 'auto.components.settings.experimental.search.2a33975d72', + 'mascot' + ), + ...translateSearchKeyword( + 'auto.components.settings.experimental.search.9f5609bfb8', + 'overlay' + ), + ...translateSearchKeyword( + 'auto.components.settings.experimental.search.65df471ab2', + 'animated' + ), + ...translateSearchKeyword( + 'auto.components.settings.experimental.search.791fefc0b0', + 'corner' + ), + ...translateSearchKeyword( + 'auto.components.settings.experimental.search.9af7a518db', + 'character' + ) + ] + }, + agentDashboard: { + title: translate( + 'auto.components.settings.experimental.search.agentDashboard.eaf17a7214', + 'Agent dashboard' + ), + description: translate( + 'auto.components.settings.experimental.search.agentDashboard.description', + 'Kanban board for monitoring agents across worktrees, in-window or as a pop-out.' + ), + keywords: [ + ...translateSearchKeyword( + 'auto.components.settings.experimental.search.0d24759f14', + 'experimental' + ), + ...translateSearchKeyword( + 'auto.components.settings.experimental.search.agentDashboard.agent', + 'agent' + ), + ...translateSearchKeyword( + 'auto.components.settings.experimental.search.agentDashboard.dashboard', + 'dashboard' + ), + ...translateSearchKeyword( + 'auto.components.settings.experimental.search.agentDashboard.kanban', + 'kanban' + ), + ...translateSearchKeyword( + 'auto.components.settings.experimental.search.agentDashboard.popout', + 'pop-out' + ), + ...translateSearchKeyword( + 'auto.components.settings.experimental.search.agentDashboard.board', + 'board' + ), + ...translateSearchKeyword( + 'auto.components.settings.experimental.search.agentDashboard.inWindow', + 'in-window' + ), + ...translateSearchKeyword( + 'auto.components.settings.experimental.search.agentDashboard.worktrees', + 'worktrees' + ) + ] + }, + terminalAttention: { + title: translate( + 'auto.components.settings.experimental.search.9e4ddf776d', + 'Terminal attention' + ), + description: translate( + 'auto.components.settings.experimental.search.11877246fc', + 'Persistent pane highlight for terminal bell and agent-completion events.' + ), + keywords: [ + ...translateSearchKeyword( + 'auto.components.settings.experimental.search.0d24759f14', + 'experimental' + ), + ...translateSearchKeyword( + 'auto.components.settings.experimental.search.9bb3bd5098', + 'terminal' + ), + ...translateSearchKeyword( + 'auto.components.settings.experimental.search.01567f19ca', + 'attention' + ), + ...translateSearchKeyword( + 'auto.components.settings.experimental.search.268e99d957', + 'highlight' + ), + ...translateSearchKeyword( + 'auto.components.settings.experimental.search.edc49480a1', + 'pane' + ), + ...translateSearchKeyword( + 'auto.components.settings.experimental.search.8facf10138', + 'bell' + ), + ...translateSearchKeyword( + 'auto.components.settings.experimental.search.7695fd30e9', + 'notification' + ), + ...translateSearchKeyword( + 'auto.components.settings.experimental.search.5f067ba0f9', + 'agent' + ), + ...translateSearchKeyword( + 'auto.components.settings.experimental.search.f10d307468', + 'completion' + ), + ...translateSearchKeyword( + 'auto.components.settings.experimental.search.7b79081695', + 'unread' + ) + ] + }, + agentHibernation: { + title: translate( + 'auto.components.settings.experimental.search.agentHibernation.title', + 'Agent sleep' + ), + description: translate( + 'auto.components.settings.experimental.search.agentHibernation.description', + 'Stops idle background agent terminals after the configured idle window and resumes supported sessions when opened again. Agent sleep preserves launch options for agents started by Orca; manually started agents may resume with current Orca defaults.' + ), + keywords: [ + ...translateSearchKeyword( + 'auto.components.settings.experimental.search.0d24759f14', + 'experimental' + ), + ...translateSearchKeyword( + 'auto.components.settings.experimental.search.agentHibernation.agent', + 'agent' + ), + ...translateSearchKeyword( + 'auto.components.settings.experimental.search.agentHibernation.agents', + 'agents' + ), + ...translateSearchKeyword( + 'auto.components.settings.experimental.search.agentHibernation.sleep', + 'sleep' + ), + ...translateSearchKeyword( + 'auto.components.settings.experimental.search.agentHibernation.minutes', + 'minutes' + ), + ...translateSearchKeyword( + 'auto.components.settings.experimental.search.agentHibernation.terminal', + 'terminal' + ) + ] + }, + newWorktreeCardStyle: getNewWorktreeCardStyleSearchEntry(), + ephemeralVms: getEphemeralVmsSearchEntry(), + browserExtensions: getBrowserExtensionsExperimentalSearchEntry() + }) satisfies Record +) + export const getExperimentalPaneSearchEntries = createLocalizedCatalog( (): SettingsSearchEntry[] => [ - { - title: translate('auto.components.settings.experimental.search.87d99e634b', 'Pet'), - description: translate( - 'auto.components.settings.experimental.search.6b5a56ac35', - 'Floating animated pet in the bottom-right corner.' - ), - keywords: [ - ...translateSearchKeyword( - 'auto.components.settings.experimental.search.0d24759f14', - 'experimental' - ), - ...translateSearchKeyword('auto.components.settings.experimental.search.051203d37c', 'pet'), - ...translateSearchKeyword( - 'auto.components.settings.experimental.search.b54cea709b', - 'sidekick' - ), - ...translateSearchKeyword( - 'auto.components.settings.experimental.search.2a33975d72', - 'mascot' - ), - ...translateSearchKeyword( - 'auto.components.settings.experimental.search.9f5609bfb8', - 'overlay' - ), - ...translateSearchKeyword( - 'auto.components.settings.experimental.search.65df471ab2', - 'animated' - ), - ...translateSearchKeyword( - 'auto.components.settings.experimental.search.791fefc0b0', - 'corner' - ), - ...translateSearchKeyword( - 'auto.components.settings.experimental.search.9af7a518db', - 'character' - ) - ] - }, - { - title: translate( - 'auto.components.settings.experimental.search.agentDashboard.title', - 'Agent Dashboard' - ), - description: translate( - 'auto.components.settings.experimental.search.agentDashboard.description', - 'Kanban board for monitoring agents across worktrees, in-window or as a pop-out.' - ), - keywords: [ - ...translateSearchKeyword( - 'auto.components.settings.experimental.search.0d24759f14', - 'experimental' - ), - ...translateSearchKeyword( - 'auto.components.settings.experimental.search.agentDashboard.agent', - 'agent' - ), - ...translateSearchKeyword( - 'auto.components.settings.experimental.search.agentDashboard.dashboard', - 'dashboard' - ), - ...translateSearchKeyword( - 'auto.components.settings.experimental.search.agentDashboard.kanban', - 'kanban' - ), - ...translateSearchKeyword( - 'auto.components.settings.experimental.search.agentDashboard.popout', - 'pop-out' - ), - ...translateSearchKeyword( - 'auto.components.settings.experimental.search.agentDashboard.board', - 'board' - ), - ...translateSearchKeyword( - 'auto.components.settings.experimental.search.agentDashboard.inWindow', - 'in-window' - ), - ...translateSearchKeyword( - 'auto.components.settings.experimental.search.agentDashboard.worktrees', - 'worktrees' - ) - ] - }, - { - title: translate( - 'auto.components.settings.experimental.search.9e4ddf776d', - 'Terminal attention' - ), - description: translate( - 'auto.components.settings.experimental.search.11877246fc', - 'Persistent pane highlight for terminal bell and agent-completion events.' - ), - keywords: [ - ...translateSearchKeyword( - 'auto.components.settings.experimental.search.0d24759f14', - 'experimental' - ), - ...translateSearchKeyword( - 'auto.components.settings.experimental.search.9bb3bd5098', - 'terminal' - ), - ...translateSearchKeyword( - 'auto.components.settings.experimental.search.01567f19ca', - 'attention' - ), - ...translateSearchKeyword( - 'auto.components.settings.experimental.search.268e99d957', - 'highlight' - ), - ...translateSearchKeyword( - 'auto.components.settings.experimental.search.edc49480a1', - 'pane' - ), - ...translateSearchKeyword( - 'auto.components.settings.experimental.search.8facf10138', - 'bell' - ), - ...translateSearchKeyword( - 'auto.components.settings.experimental.search.7695fd30e9', - 'notification' - ), - ...translateSearchKeyword( - 'auto.components.settings.experimental.search.5f067ba0f9', - 'agent' - ), - ...translateSearchKeyword( - 'auto.components.settings.experimental.search.f10d307468', - 'completion' - ), - ...translateSearchKeyword( - 'auto.components.settings.experimental.search.7b79081695', - 'unread' - ) - ] - }, - { - title: translate( - 'auto.components.settings.experimental.search.agentHibernation.title', - 'Agent sleep' - ), - description: translate( - 'auto.components.settings.experimental.search.agentHibernation.description', - 'Stops idle background agent terminals after the configured idle window and resumes supported sessions when opened again. Agent sleep preserves launch options for agents started by Orca; manually started agents may resume with current Orca defaults.' - ), - keywords: [ - ...translateSearchKeyword( - 'auto.components.settings.experimental.search.0d24759f14', - 'experimental' - ), - ...translateSearchKeyword( - 'auto.components.settings.experimental.search.agentHibernation.agent', - 'agent' - ), - ...translateSearchKeyword( - 'auto.components.settings.experimental.search.agentHibernation.agents', - 'agents' - ), - ...translateSearchKeyword( - 'auto.components.settings.experimental.search.agentHibernation.sleep', - 'sleep' - ), - ...translateSearchKeyword( - 'auto.components.settings.experimental.search.agentHibernation.minutes', - 'minutes' - ), - ...translateSearchKeyword( - 'auto.components.settings.experimental.search.agentHibernation.terminal', - 'terminal' - ) - ] - }, - getNewWorktreeCardStyleSearchEntry(), - getEphemeralVmsSearchEntry(), - getBrowserExtensionsExperimentalSearchEntry(), + ...Object.values(getExperimentalSearchEntry()), // Index WSL toggles only on Windows desktops that expose them. ...(typeof window !== 'undefined' && window.api?.runtimeEnvironments?.wslHosts ? [getWslHostsExperimentalSearchEntry()] : []) ] ) - -// Resolve by title and fail loudly on renames so changing catalog shape cannot silently retarget numeric indexes. -function findEntry(title: string): SettingsSearchEntry { - const entry = getExperimentalPaneSearchEntries().find((e) => e.title === title) - if (!entry) { - throw new Error(`Missing experimental-pane search entry: "${title}"`) - } - return entry -} - -export function getExperimentalSearchEntry(t: Translate = translate) { - return { - pet: findEntry(t('auto.components.settings.experimental.search.87d99e634b', 'Pet')), - agentDashboard: findEntry( - t('auto.components.settings.experimental.search.agentDashboard.title', 'Agent Dashboard') - ), - terminalAttention: findEntry( - t('auto.components.settings.experimental.search.9e4ddf776d', 'Terminal attention') - ), - agentHibernation: findEntry( - t('auto.components.settings.experimental.search.agentHibernation.title', 'Agent sleep') - ), - newWorktreeCardStyle: findEntry( - t('auto.components.settings.experimental.search.newWorktreeCardStyle.title', 'New card style') - ), - ephemeralVms: findEntry( - t('auto.components.settings.ephemeralVms.search.cloudVmTitle', 'Cloud VM') - ), - browserExtensions: findEntry( - t( - 'auto.components.settings.experimental.search.browserExtensions.title', - 'Browser extensions' - ) - ) - } as const -} diff --git a/src/renderer/src/components/settings/advanced/network/AdvancedNetworkSettingsSection.tsx b/src/renderer/src/components/settings/advanced/network/AdvancedNetworkSettingsSection.tsx index c9ab9f90216..d8943143898 100644 --- a/src/renderer/src/components/settings/advanced/network/AdvancedNetworkSettingsSection.tsx +++ b/src/renderer/src/components/settings/advanced/network/AdvancedNetworkSettingsSection.tsx @@ -245,7 +245,7 @@ export function AdvancedNetworkSettingsSection({

{t( @@ -278,8 +278,8 @@ export function AdvancedNetworkSettingsSection({