diff --git a/.github/actions/install-node-dependencies/action.yml b/.github/actions/install-node-dependencies/action.yml
index 39cb1465852..e6d6f8f15e3 100644
--- a/.github/actions/install-node-dependencies/action.yml
+++ b/.github/actions/install-node-dependencies/action.yml
@@ -1,5 +1,5 @@
name: Install Node dependencies
-description: Installs the Node toolchain and repository dependencies for Linux CI jobs.
+description: Installs the Node toolchain and repository dependencies for CI jobs.
inputs:
native-runtime:
@@ -10,6 +10,18 @@ inputs:
description: Node.js version override; defaults to the version declared in package.json.
required: false
default: ''
+ persist-native-cache:
+ description: Save restored native modules at job end. Set false when a later step overwrites the same path with a different ABI.
+ required: false
+ default: 'true'
+
+outputs:
+ node-version:
+ description: Resolved Node.js version used for the install.
+ value: ${{ steps.requested-node.outputs.node-version || steps.default-node.outputs.node-version }}
+ native-cache-scope:
+ description: Operating-system image scope used by the native module cache.
+ value: ${{ steps.native-cache-scope.outputs.scope }}
runs:
using: composite
@@ -51,7 +63,7 @@ runs:
# pnpm's bundled gyp_main.py is not executable on fresh Linux runners.
- name: Use external node-gyp
- if: inputs.native-runtime != 'none'
+ if: runner.os == 'Linux' && inputs.native-runtime != 'none'
shell: bash
run: |
npm install -g node-gyp@11.5.0
@@ -80,12 +92,42 @@ runs:
# ensure-native-runtime node-gyp-compiles it in every job that asks for a runtime.
# The artifacts are ABI-bound, so the key carries the target runtime, the resolved
# Node version, and the patch whose contents the build has to match.
- - name: Restore compiled native modules
+ # Windows extra globs are empty on Linux. No restore-keys: a partial-match key is
+ # an ABI-mismatched build, and ensure-native-runtime would recompile it anyway.
+ # Native addons built on a newer Linux image can require glibc symbols
+ # missing from an older runner/container. ImageOS distinguishes hosted
+ # Windows/macOS images; /etc/os-release also distinguishes Linux containers.
+ - name: Resolve native cache scope
+ id: native-cache-scope
if: inputs.native-runtime != 'none'
+ shell: bash
+ run: |
+ scope="${ImageOS:-$RUNNER_OS}"
+ if [ -r /etc/os-release ]; then
+ . /etc/os-release
+ scope="${ID:-linux}-${VERSION_ID:-unknown}"
+ fi
+ echo "scope=$scope" >> "$GITHUB_OUTPUT"
+
+ - name: Restore compiled native modules
+ if: inputs.native-runtime != 'none' && inputs.persist-native-cache != 'false'
uses: actions/cache@v5
with:
- path: node_modules/.pnpm/node-pty@*/node_modules/node-pty/build
- key: native-modules-${{ runner.os }}-${{ runner.arch }}-${{ inputs.native-runtime }}-node${{ steps.default-node.outputs.node-version }}${{ steps.requested-node.outputs.node-version }}-${{ hashFiles('pnpm-lock.yaml', 'config/patches/node-pty@1.1.0.patch') }}
+ path: |
+ node_modules/.pnpm/node-pty@*/node_modules/node-pty/build
+ node_modules/.pnpm/windows-native-registry@*/node_modules/windows-native-registry/build
+ node_modules/.pnpm/@vscode+windows-process-tree@*/node_modules/@vscode/windows-process-tree/build
+ key: native-modules-${{ runner.os }}-${{ steps.native-cache-scope.outputs.scope }}-${{ runner.arch }}-${{ inputs.native-runtime }}-node${{ steps.default-node.outputs.node-version }}${{ steps.requested-node.outputs.node-version }}-${{ hashFiles('pnpm-lock.yaml', 'config/patches/node-pty@1.1.0.patch', 'config/patches/@vscode__windows-process-tree@0.8.0.patch') }}
+
+ - name: Restore compiled native modules without saving
+ if: inputs.native-runtime != 'none' && inputs.persist-native-cache == 'false'
+ uses: actions/cache/restore@v5
+ with:
+ path: |
+ node_modules/.pnpm/node-pty@*/node_modules/node-pty/build
+ node_modules/.pnpm/windows-native-registry@*/node_modules/windows-native-registry/build
+ node_modules/.pnpm/@vscode+windows-process-tree@*/node_modules/@vscode/windows-process-tree/build
+ key: native-modules-${{ runner.os }}-${{ steps.native-cache-scope.outputs.scope }}-${{ runner.arch }}-${{ inputs.native-runtime }}-node${{ steps.default-node.outputs.node-version }}${{ steps.requested-node.outputs.node-version }}-${{ hashFiles('pnpm-lock.yaml', 'config/patches/node-pty@1.1.0.patch', 'config/patches/@vscode__windows-process-tree@0.8.0.patch') }}
- name: Prepare native runtime
if: inputs.native-runtime != 'none'
diff --git a/.github/workflows/computer-e2e.yml b/.github/workflows/computer-e2e.yml
index ab1c6ce687b..b24c8a52f2c 100644
--- a/.github/workflows/computer-e2e.yml
+++ b/.github/workflows/computer-e2e.yml
@@ -71,30 +71,17 @@ jobs:
- uses: actions/checkout@v6
with:
persist-credentials: false
- - uses: actions/setup-node@v6
- with:
- node-version-file: package.json
- - uses: pnpm/action-setup@v6
- with:
- run_install: false
- if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y python3 python3-gi gir1.2-atspi-2.0 at-spi2-core gedit xvfb xclip xdotool
- # Why: pnpm's bundled node-gyp can ship gyp_main.py without execute
- # permission on Linux runners; node-pty's install fallback then fails
- # before this smoke job can exercise the native package.
- - name: Use external node-gyp to avoid pnpm's bundled copy (Linux only)
- if: runner.os == 'Linux'
- run: |
- npm install -g node-gyp@11.5.0
- echo "npm_config_node_gyp=$(npm root -g)/node-gyp/bin/node-gyp.js" >> "$GITHUB_ENV"
- - run: pnpm install --frozen-lockfile
+ - uses: ./.github/actions/install-node-dependencies
+ with:
+ native-runtime: node
# Why: without --config, bare vitest ignores config/vitest.config.ts (there
# is no root config) and falls back to the 5s default timeout with no
# Windows worker cap, so the real csc.exe launcher-compile tests time out
# on hosted Windows. Use the shared config so this job matches pnpm test.
- run: >-
pnpm vitest run --config config/vitest.config.ts
- config/scripts/build-windows-cli-launcher.test.mjs
src/main/ssh/ssh-remote-cli-launcher.test.ts
config/scripts/computer-e2e-workflow.test.mjs
config/scripts/macos-computer-helper-owner-loss-group-recovery.test.mjs
@@ -159,13 +146,9 @@ jobs:
- uses: actions/checkout@v6
with:
persist-credentials: false
- - uses: actions/setup-node@v6
+ - uses: ./.github/actions/install-node-dependencies
with:
- node-version-file: package.json
- - uses: pnpm/action-setup@v6
- with:
- run_install: false
- - run: pnpm install --frozen-lockfile
+ native-runtime: electron
- name: Owner-loss benchmark process cleanup
run: >-
pnpm vitest run
diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml
index 24c0ffa0e5b..7a5eff241e6 100644
--- a/.github/workflows/e2e.yml
+++ b/.github/workflows/e2e.yml
@@ -40,36 +40,10 @@ jobs:
with:
ref: ${{ inputs.ref || github.ref }}
- # Why: the E2E build compiles native modules via node-gyp. Mirrors the
- # install step in pr.yml's verify job so E2E doesn't hit missing-toolchain
- # errors.
- - name: Install native build tools
- run: sudo apt-get update && sudo apt-get install -y build-essential python3
-
- # Why pnpm first: setup-node needs pnpm on PATH to locate the store it caches.
- # Without that cache every E2E job re-downloaded the whole dependency set.
- - name: Setup pnpm
- uses: pnpm/action-setup@v6
+ # Why: the build's plain-Node daemon smoke load resolves node-pty.
+ - uses: ./.github/actions/install-node-dependencies
with:
- run_install: false
-
- - name: Setup Node.js
- uses: actions/setup-node@v6
- with:
- node-version-file: package.json
- cache: pnpm
-
- # Why: this job runs the same pnpm install path as pr.yml's verify
- # job, so it needs the same pinned node-gyp override to avoid pnpm's
- # broken bundled gyp_main.py on Linux.
- - name: Use external node-gyp to avoid pnpm's bundled copy (Linux only)
- if: runner.os == 'Linux'
- run: |
- npm install -g node-gyp@11.5.0
- echo "npm_config_node_gyp=$(npm root -g)/node-gyp/bin/node-gyp.js" >> "$GITHUB_ENV"
-
- - name: Install dependencies
- run: pnpm install --frozen-lockfile
+ native-runtime: node
# Why: building here avoids parallel builds inside Playwright globalSetup;
# paired-browser specs also need the standalone web bundle.
@@ -130,11 +104,11 @@ jobs:
with:
ref: ${{ inputs.ref || github.ref }}
- # Why: pnpm install rebuilds native modules, and those postinstall
- # scripts still need the Linux toolchain even though this shard reuses
- # the prebuilt Electron output.
+ # Why: pnpm install used to rebuild native modules here; the composite
+ # action restores them from cache and only compiles on a miss. The
+ # toolchain is still required for that miss path, and for paired Quick
+ # Open coverage which exercises the resource-bounded host search.
- name: Install native build tools
- # Why: paired Quick Open coverage exercises the resource-bounded host search.
run: sudo apt-get update && sudo apt-get install -y build-essential fonts-noto-cjk python3 ripgrep zsh
# Why: Electron on Linux needs an X display even when the app
@@ -143,33 +117,9 @@ jobs:
- name: Install xvfb
run: sudo apt-get install -y xvfb
- # Why pnpm first: setup-node needs pnpm on PATH to locate the store it caches.
- # Without that cache every E2E job re-downloaded the whole dependency set.
- - name: Setup pnpm
- uses: pnpm/action-setup@v6
+ - uses: ./.github/actions/install-node-dependencies
with:
- run_install: false
-
- - name: Setup Node.js
- uses: actions/setup-node@v6
- with:
- node-version-file: package.json
- cache: pnpm
-
- # Why: this job runs the same pnpm install path as pr.yml's verify
- # job, so it needs the same pinned node-gyp override to avoid pnpm's
- # broken bundled gyp_main.py on Linux. Gate on runner.os matches
- # release.yml so the invariant "this workaround is Linux-only" is
- # consistent across all three workflows, even though this job
- # currently pins runs-on: ubuntu-latest.
- - name: Use external node-gyp to avoid pnpm's bundled copy (Linux only)
- if: runner.os == 'Linux'
- run: |
- npm install -g node-gyp@11.5.0
- echo "npm_config_node_gyp=$(npm root -g)/node-gyp/bin/node-gyp.js" >> "$GITHUB_ENV"
-
- - name: Install dependencies
- run: pnpm install --frozen-lockfile
+ native-runtime: electron
- name: Download E2E build output
uses: actions/download-artifact@v8
@@ -222,25 +172,9 @@ jobs:
# lane now receives those specs from pr.yml's SSH source mapping.
run: sudo apt-get update && sudo apt-get install -y build-essential fonts-noto-cjk openssh-client python3 ripgrep xvfb zsh
- # Why pnpm first: setup-node needs pnpm on PATH to locate the store it caches.
- # Without that cache every E2E job re-downloaded the whole dependency set.
- - name: Setup pnpm
- uses: pnpm/action-setup@v6
+ - uses: ./.github/actions/install-node-dependencies
with:
- run_install: false
- - name: Setup Node.js
- uses: actions/setup-node@v6
- with:
- node-version-file: package.json
- cache: pnpm
-
- - name: Use external node-gyp to avoid pnpm's bundled copy
- run: |
- npm install -g node-gyp@11.5.0
- echo "npm_config_node_gyp=$(npm root -g)/node-gyp/bin/node-gyp.js" >> "$GITHUB_ENV"
-
- - name: Install dependencies
- run: pnpm install --frozen-lockfile
+ native-runtime: electron
- name: Download E2E build output
uses: actions/download-artifact@v8
@@ -305,29 +239,9 @@ jobs:
- name: Install native build and headless UI tools
run: sudo apt-get update && sudo apt-get install -y build-essential fonts-noto-cjk openssh-client python3 xvfb zsh
- # Why pnpm first: setup-node needs pnpm on PATH to locate the store it caches.
- # Without that cache every E2E job re-downloaded the whole dependency set.
- - name: Setup pnpm
- uses: pnpm/action-setup@v6
+ - uses: ./.github/actions/install-node-dependencies
with:
- run_install: false
-
- - name: Setup Node.js
- uses: actions/setup-node@v6
- with:
- node-version-file: package.json
- cache: pnpm
-
- # Why: same Linux-only node-gyp pin as build/e2e jobs so the workaround
- # stays consistent across workflows even while this job is ubuntu-latest.
- - name: Use external node-gyp to avoid pnpm's bundled copy (Linux only)
- if: runner.os == 'Linux'
- run: |
- npm install -g node-gyp@11.5.0
- echo "npm_config_node_gyp=$(npm root -g)/node-gyp/bin/node-gyp.js" >> "$GITHUB_ENV"
-
- - name: Install dependencies
- run: pnpm install --frozen-lockfile
+ native-runtime: electron
- name: Download E2E build output
uses: actions/download-artifact@v8
diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml
index a256da5112c..5eadeb9be81 100644
--- a/.github/workflows/pr.yml
+++ b/.github/workflows/pr.yml
@@ -16,15 +16,28 @@ permissions:
contents: read
jobs:
- # Why: a README/docs-only PR used to start the full matrix (32 test shards,
+ # Why: a README/docs-only PR used to start the full matrix (test shards,
# two package jobs, typecheck, git compat, xterm, shell contracts). Path
# filters on `on.pull_request` would drop the `verify` check entirely; this
# detector keeps verify as the required aggregate and skips the expensive jobs.
+ # Per-job outputs also skip git-compat/xterm/packaging/shell when those
+ # inputs are unchanged; empty diffs fail closed and run everything.
code_paths:
name: detect code-relevant changes
runs-on: ubuntu-latest
outputs:
should_run: ${{ steps.filter.outputs.should_run }}
+ static_analysis: ${{ steps.filter.outputs.static_analysis }}
+ typecheck: ${{ steps.filter.outputs.typecheck }}
+ git_compatibility: ${{ steps.filter.outputs.git_compatibility }}
+ xterm_patch_sync: ${{ steps.filter.outputs.xterm_patch_sync }}
+ shell_contracts: ${{ steps.filter.outputs.shell_contracts }}
+ test: ${{ steps.filter.outputs.test }}
+ orcad_browser: ${{ steps.filter.outputs.orcad_browser }}
+ cross-version-wire: ${{ steps.filter.outputs.cross-version-wire }}
+ managed_hook_node18: ${{ steps.filter.outputs.managed_hook_node18 }}
+ package: ${{ steps.filter.outputs.package }}
+ package_windows: ${{ steps.filter.outputs.package_windows }}
steps:
- name: Checkout
uses: actions/checkout@v6
@@ -48,14 +61,12 @@ jobs:
CHANGED="$(git diff --name-only --no-renames --diff-filter=ACDMR --merge-base "$BASE_SHA" "$HEAD_SHA")"
echo "Changed paths:"
printf '%s\n' "$CHANGED"
- SHOULD_RUN="$(printf '%s\n' "$CHANGED" | node config/scripts/pr-code-change-scope.mjs)"
- echo "should_run=$SHOULD_RUN" >> "$GITHUB_OUTPUT"
- echo "should_run=$SHOULD_RUN"
+ printf '%s\n' "$CHANGED" | node config/scripts/pr-code-change-scope.mjs | tee -a "$GITHUB_OUTPUT"
static_analysis:
name: static analysis
needs: [code_paths]
- if: needs.code_paths.outputs.should_run == 'true'
+ if: needs.code_paths.outputs.static_analysis == 'true'
runs-on: ubuntu-latest
steps:
@@ -70,6 +81,8 @@ jobs:
persist-credentials: false
- uses: ./.github/actions/install-node-dependencies
+ with:
+ native-runtime: node
- name: Lint
run: pnpm exec oxlint --format github
@@ -188,7 +201,7 @@ jobs:
typecheck:
needs: [code_paths]
- if: needs.code_paths.outputs.should_run == 'true'
+ if: needs.code_paths.outputs.typecheck == 'true'
runs-on: ubuntu-latest
steps:
@@ -216,7 +229,7 @@ jobs:
git_compatibility:
name: Git compatibility
needs: [code_paths]
- if: needs.code_paths.outputs.should_run == 'true'
+ if: needs.code_paths.outputs.git_compatibility == 'true'
runs-on: ubuntu-latest
steps:
@@ -283,7 +296,7 @@ jobs:
xterm_patch_sync:
name: xterm patch sync
needs: [code_paths]
- if: needs.code_paths.outputs.should_run == 'true'
+ if: needs.code_paths.outputs.xterm_patch_sync == 'true'
runs-on: ubuntu-latest
steps:
@@ -313,7 +326,7 @@ jobs:
shell_contracts:
name: shell contracts
needs: [code_paths]
- if: needs.code_paths.outputs.should_run == 'true'
+ if: needs.code_paths.outputs.shell_contracts == 'true'
runs-on: ubuntu-latest
# Why: this job's cost is almost entirely package download, and a stalled mirror has
# no wall-clock bound of its own. A successful run finishes in ~4.5 minutes, so this
@@ -432,14 +445,14 @@ jobs:
test:
name: tests node ${{ matrix.node }} ${{ matrix.shard }}/${{ matrix.shard_total }}
needs: [code_paths]
- if: needs.code_paths.outputs.should_run == 'true'
+ if: needs.code_paths.outputs.test == 'true'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node: ['24', '26']
- shard: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
- shard_total: [16]
+ shard: [1, 2, 3, 4, 5, 6, 7, 8]
+ shard_total: [8]
steps:
- name: Checkout
@@ -477,12 +490,12 @@ jobs:
--exclude=tests/e2e/cross-version-wire/** \
--shard=${{ matrix.shard }}/${{ matrix.shard_total }}
- # Why a separate job: the test needs a real Chrome, and the 32-way `test` matrix
- # would pay for it 32 times to run one file in whichever shard it landed in.
+ # Why a separate job: the test needs a real Chrome, and the sharded `test` matrix
+ # would pay for it on every shard to run one file in whichever shard it landed in.
orcad_browser:
name: orcad browser provider
needs: [code_paths]
- if: needs.code_paths.outputs.should_run == 'true'
+ if: needs.code_paths.outputs.orcad_browser == 'true'
runs-on: ubuntu-latest
steps:
@@ -518,7 +531,7 @@ jobs:
cross-version-wire:
name: cross-version wire compatibility
needs: [code_paths]
- if: needs.code_paths.outputs.should_run == 'true'
+ if: needs.code_paths.outputs.cross-version-wire == 'true'
runs-on: ubuntu-latest
steps:
@@ -550,7 +563,7 @@ jobs:
managed_hook_node18:
name: managed hooks on Node 18
needs: [code_paths]
- if: needs.code_paths.outputs.should_run == 'true'
+ if: needs.code_paths.outputs.managed_hook_node18 == 'true'
runs-on: ubuntu-latest
steps:
@@ -575,7 +588,7 @@ jobs:
package:
name: package
needs: [code_paths]
- if: needs.code_paths.outputs.should_run == 'true'
+ if: needs.code_paths.outputs.package == 'true'
runs-on: ubuntu-latest
steps:
@@ -647,7 +660,7 @@ jobs:
package_windows:
name: package (windows)
needs: [code_paths]
- if: needs.code_paths.outputs.should_run == 'true'
+ if: needs.code_paths.outputs.package_windows == 'true'
runs-on: windows-2022
timeout-minutes: 30
@@ -657,17 +670,6 @@ jobs:
with:
persist-credentials: false
- - name: Setup pnpm
- uses: pnpm/action-setup@v6
- with:
- run_install: false
-
- - name: Setup Node.js
- uses: actions/setup-node@v6
- with:
- node-version-file: package.json
- cache: pnpm
-
- name: Cache electron-builder downloads
uses: actions/cache@v5
with:
@@ -678,18 +680,22 @@ jobs:
restore-keys: |
electron-builder-windows-
- - name: Install dependencies
- run: pnpm install --frozen-lockfile
+ # Why persist-native-cache false: this job later rebuilds the same path for
+ # Electron. A post-job save would store the Electron ABI under the Node key.
+ - uses: ./.github/actions/install-node-dependencies
+ id: deps
+ with:
+ native-runtime: node
+ persist-native-cache: 'false'
- # Why: node-pty prefers its upstream prebuild, which does not contain
- # Orca's Windows patch, so the job-object exports would be absent and the
- # suite below would test an unpatched binary. build_from_source removes
- # the prebuild, and the package's postinstall restores the ConPTY runtime
- # files that a bare node-gyp rebuild would miss.
- - name: Rebuild node-pty from patched source
- env:
- npm_config_build_from_source: 'true'
- run: pnpm rebuild node-pty
+ - name: Save compiled Node native modules
+ uses: actions/cache/save@v5
+ with:
+ path: |
+ node_modules/.pnpm/node-pty@*/node_modules/node-pty/build
+ node_modules/.pnpm/windows-native-registry@*/node_modules/windows-native-registry/build
+ node_modules/.pnpm/@vscode+windows-process-tree@*/node_modules/@vscode/windows-process-tree/build
+ key: native-modules-${{ runner.os }}-${{ steps.deps.outputs.native-cache-scope }}-${{ runner.arch }}-node-node${{ steps.deps.outputs.node-version }}-${{ hashFiles('pnpm-lock.yaml', 'config/patches/node-pty@1.1.0.patch', 'config/patches/@vscode__windows-process-tree@0.8.0.patch') }}
- name: Test Windows-specific boundaries
run: >-
@@ -724,9 +730,26 @@ jobs:
# Why the :parallel variant: identical to build:release except the three
# electron-vite targets overlap instead of running back to back. The Linux package
# job already packages and smoke-tests an AppImage built that way.
+ - name: Cache Windows CLI launcher
+ uses: actions/cache@v5
+ with:
+ path: native/windows-cli-launcher/.build
+ key: windows-cli-launcher-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('native/windows-cli-launcher/**', 'config/scripts/build-windows-cli-launcher.mjs') }}
+
- name: Build package inputs
+ env:
+ ORCA_REUSE_WINDOWS_CLI_LAUNCHER: '1'
run: pnpm run build:release:parallel
+ - name: Restore compiled Electron native modules
+ uses: actions/cache@v5
+ with:
+ path: |
+ node_modules/.pnpm/node-pty@*/node_modules/node-pty/build
+ node_modules/.pnpm/windows-native-registry@*/node_modules/windows-native-registry/build
+ node_modules/.pnpm/@vscode+windows-process-tree@*/node_modules/@vscode/windows-process-tree/build
+ key: native-modules-${{ runner.os }}-${{ steps.deps.outputs.native-cache-scope }}-${{ runner.arch }}-electron-node${{ steps.deps.outputs.node-version }}-${{ hashFiles('pnpm-lock.yaml', 'config/patches/node-pty@1.1.0.patch', 'config/patches/@vscode__windows-process-tree@0.8.0.patch') }}
+
- name: Prepare Electron native runtime
run: node config/scripts/ensure-native-runtime.mjs --runtime=electron
@@ -807,6 +830,7 @@ jobs:
- shell_contracts
- test
- orcad_browser
+ - cross-version-wire
- managed_hook_node18
- package
- package_windows
@@ -826,57 +850,63 @@ jobs:
CODE_PATHS: ${{ needs.code_paths.result }}
SHOULD_RUN: ${{ needs.code_paths.outputs.should_run }}
STATIC_ANALYSIS: ${{ needs.static_analysis.result }}
+ STATIC_ANALYSIS_SHOULD_RUN: ${{ needs.code_paths.outputs.static_analysis }}
ROOT_DIRECTORY_GUARD: ${{ needs.root_directory_guard.result }}
TYPECHECK: ${{ needs.typecheck.result }}
+ TYPECHECK_SHOULD_RUN: ${{ needs.code_paths.outputs.typecheck }}
GIT_COMPATIBILITY: ${{ needs.git_compatibility.result }}
+ GIT_COMPATIBILITY_SHOULD_RUN: ${{ needs.code_paths.outputs.git_compatibility }}
XTERM_PATCH_SYNC: ${{ needs.xterm_patch_sync.result }}
+ XTERM_PATCH_SYNC_SHOULD_RUN: ${{ needs.code_paths.outputs.xterm_patch_sync }}
SHELL_CONTRACTS: ${{ needs.shell_contracts.result }}
+ SHELL_CONTRACTS_SHOULD_RUN: ${{ needs.code_paths.outputs.shell_contracts }}
TEST: ${{ needs.test.result }}
+ TEST_SHOULD_RUN: ${{ needs.code_paths.outputs.test }}
ORCAD_BROWSER: ${{ needs.orcad_browser.result }}
+ ORCAD_BROWSER_SHOULD_RUN: ${{ needs.code_paths.outputs.orcad_browser }}
+ CROSS_VERSION_WIRE: ${{ needs.cross-version-wire.result }}
+ CROSS_VERSION_WIRE_SHOULD_RUN: ${{ needs.code_paths.outputs.cross-version-wire }}
MANAGED_HOOK_NODE18: ${{ needs.managed_hook_node18.result }}
+ MANAGED_HOOK_NODE18_SHOULD_RUN: ${{ needs.code_paths.outputs.managed_hook_node18 }}
PACKAGE: ${{ needs.package.result }}
+ PACKAGE_SHOULD_RUN: ${{ needs.code_paths.outputs.package }}
PACKAGE_WINDOWS: ${{ needs.package_windows.result }}
+ PACKAGE_WINDOWS_SHOULD_RUN: ${{ needs.code_paths.outputs.package_windows }}
run: |
if [ "$CODE_PATHS" != "success" ]; then
exit 1
fi
+ if [ "$ROOT_DIRECTORY_GUARD" != "success" ]; then
+ exit 1
+ fi
if [ "$SHOULD_RUN" != "true" ]; then
echo "Docs-only change; expensive PR checks skipped."
- if [ "$ROOT_DIRECTORY_GUARD" != "success" ]; then
- exit 1
- fi
- for result in \
- "$STATIC_ANALYSIS" \
- "$TYPECHECK" \
- "$GIT_COMPATIBILITY" \
- "$XTERM_PATCH_SYNC" \
- "$SHELL_CONTRACTS" \
- "$TEST" \
- "$ORCAD_BROWSER" \
- "$MANAGED_HOOK_NODE18" \
- "$PACKAGE" \
- "$PACKAGE_WINDOWS"; do
- if [ "$result" != "skipped" ]; then
- exit 1
- fi
- done
- exit 0
fi
- # Require success when the PR has code-relevant changes
- for result in \
- "$CODE_PATHS" \
- "$STATIC_ANALYSIS" \
- "$ROOT_DIRECTORY_GUARD" \
- "$TYPECHECK" \
- "$GIT_COMPATIBILITY" \
- "$XTERM_PATCH_SYNC" \
- "$SHELL_CONTRACTS" \
- "$TEST" \
- "$ORCAD_BROWSER" \
- "$MANAGED_HOOK_NODE18" \
- "$PACKAGE" \
- "$PACKAGE_WINDOWS"; do
- if [ "$result" != "success" ]; then
- exit 1
+ failed=0
+ check_job() {
+ local name="$1" result="$2" should="$3"
+ if [ "$should" = "true" ]; then
+ if [ "$result" != "success" ]; then
+ echo "$name: expected success, got $result"
+ failed=1
+ fi
+ else
+ if [ "$result" != "skipped" ]; then
+ echo "$name: expected skipped, got $result"
+ failed=1
+ fi
fi
- done
+ }
+ # Require success when the PR has code-relevant changes
+ check_job static_analysis "$STATIC_ANALYSIS" "$STATIC_ANALYSIS_SHOULD_RUN"
+ check_job typecheck "$TYPECHECK" "$TYPECHECK_SHOULD_RUN"
+ check_job git_compatibility "$GIT_COMPATIBILITY" "$GIT_COMPATIBILITY_SHOULD_RUN"
+ check_job xterm_patch_sync "$XTERM_PATCH_SYNC" "$XTERM_PATCH_SYNC_SHOULD_RUN"
+ check_job shell_contracts "$SHELL_CONTRACTS" "$SHELL_CONTRACTS_SHOULD_RUN"
+ check_job test "$TEST" "$TEST_SHOULD_RUN"
+ check_job orcad_browser "$ORCAD_BROWSER" "$ORCAD_BROWSER_SHOULD_RUN"
+ check_job cross-version-wire "$CROSS_VERSION_WIRE" "$CROSS_VERSION_WIRE_SHOULD_RUN"
+ check_job managed_hook_node18 "$MANAGED_HOOK_NODE18" "$MANAGED_HOOK_NODE18_SHOULD_RUN"
+ check_job package "$PACKAGE" "$PACKAGE_SHOULD_RUN"
+ check_job package_windows "$PACKAGE_WINDOWS" "$PACKAGE_WINDOWS_SHOULD_RUN"
+ exit "$failed"
diff --git a/README.md b/README.md
index 8d0b9e79653..8de0ff8d2b9 100644
--- a/README.md
+++ b/README.md
@@ -36,7 +36,7 @@
Monitor and steer your agents from your phone — get notified when an agent finishes and send follow-ups from anywhere.
-[iOS App Store](https://apps.apple.com/us/app/orca-ide/id6766130217) · [TestFlight](https://testflight.apple.com/join/YjeGMQBA) · [Android APK 0.0.44](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.44/app-release.apk) · [Docs →](https://www.onorca.dev/docs/mobile)
+[iOS App Store](https://apps.apple.com/us/app/orca-ide/id6766130217) · [TestFlight](https://testflight.apple.com/join/YjeGMQBA) · [Android APK 0.0.46](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.46/app-release.apk) · [Docs →](https://www.onorca.dev/docs/mobile)
@@ -230,7 +230,7 @@ yay -S stably-orca-bin
Pair with your desktop app to monitor and steer your agents from your phone.
- **iOS:** [Download on the App Store](https://apps.apple.com/us/app/orca-ide/id6766130217) or [join TestFlight](https://testflight.apple.com/join/YjeGMQBA)
-- **Android:** [Download APK 0.0.44](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.44/app-release.apk) · [Install guide](https://www.onorca.dev/docs/android-apk)
+- **Android:** [Download APK 0.0.46](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.46/app-release.apk) · [Install guide](https://www.onorca.dev/docs/android-apk)
---
diff --git a/config/max-lines-baseline.txt b/config/max-lines-baseline.txt
index 664bb4c28b3..75e536a2198 100644
--- a/config/max-lines-baseline.txt
+++ b/config/max-lines-baseline.txt
@@ -2,17 +2,13 @@
# This is a RATCHET: the list may only SHRINK. Do NOT add entries to get CI green —
# split the oversized file instead (AGENTS.md → "Do Not Disable Max Lines").
# Regenerate/prune: pnpm check:max-lines-ratchet --prune (removes stale entries only)
-inline src/cli/handlers/automations.ts
-inline src/cli/help.ts
inline src/main/agent-hooks/server.ts
inline src/main/automations/external-manager.ts
-inline src/main/automations/hermes-cron-output.ts
inline src/main/browser/agent-browser-bridge.ts
inline src/main/browser/browser-cookie-import.ts
inline src/main/browser/browser-manager.ts
inline src/main/browser/cdp-bridge.ts
inline src/main/claude-accounts/runtime-auth-service.ts
-inline src/main/cli/wsl-cli-installer.ts
inline src/main/codex-accounts/runtime-home-service.ts
inline src/main/codex-accounts/service.ts
inline src/main/codex/hook-service.ts
@@ -23,14 +19,7 @@ inline src/main/index.ts
inline src/main/ipc/filesystem-watcher.ts
inline src/main/ipc/filesystem.ts
inline src/main/ipc/worktree-remote.ts
-inline src/main/keybindings/keybinding-file.ts
-inline src/main/linear/issues.ts
-inline src/main/memory/collector.ts
-inline src/main/ports/advertised-url-watcher.ts
-inline src/main/ports/local-workspace-port-scanner.ts
-inline src/main/project-groups/nested-repo-discovery.ts
inline src/main/providers/local-pty-provider.ts
-inline src/main/rate-limits/claude-pty.ts
inline src/main/rate-limits/service.ts
inline src/main/runtime/orca-runtime-browser.ts
inline src/main/runtime/orca-runtime-files.ts
@@ -52,7 +41,6 @@ inline src/relay/git-handler.ts
inline src/relay/pty-handler.ts
inline src/relay/workspace-space-scan.ts
inline src/renderer/src/components/GitLabItemDialog.tsx
-inline src/renderer/src/components/JiraIssueWorkspace.tsx
inline src/renderer/src/components/LinearItemDrawer.tsx
inline src/renderer/src/components/TaskPage.tsx
inline src/renderer/src/components/Terminal.tsx
@@ -61,15 +49,12 @@ inline src/renderer/src/components/activity/ActivityPrototypePage.tsx
inline src/renderer/src/components/automations/AutomationsPage.tsx
inline src/renderer/src/components/editor/CombinedDiffViewer.tsx
inline src/renderer/src/components/editor/MarkdownPreview.tsx
-inline src/renderer/src/components/feature-wall/EditorAnimatedVisual.tsx
inline src/renderer/src/components/floating-terminal/FloatingTerminalPanel.tsx
inline src/renderer/src/components/new-workspace/SmartWorkspaceNameField.tsx
-inline src/renderer/src/components/onboarding/use-onboarding-flow.ts
inline src/renderer/src/components/right-sidebar/PortsPanel.tsx
inline src/renderer/src/components/settings/AccountsPane.tsx
inline src/renderer/src/components/settings/RuntimeEnvironmentsPane.tsx
inline src/renderer/src/components/settings/Settings.tsx
-inline src/renderer/src/components/sidebar/use-workspace-kanban-area-selection.ts
inline src/renderer/src/components/status-bar/ResourceUsageStatusSegment.tsx
inline src/renderer/src/components/status-bar/StatusBar.tsx
inline src/renderer/src/components/status-bar/WorkspaceSpaceManagerPanel.tsx
@@ -82,40 +67,25 @@ inline src/renderer/src/lib/pane-manager/pane-terminal-output-scheduler.ts
inline src/renderer/src/lib/pane-manager/pane-tree-ops.ts
inline src/renderer/src/runtime/remote-runtime-terminal-multiplexer.ts
inline src/renderer/src/runtime/runtime-file-client.ts
-inline src/renderer/src/runtime/runtime-linear-client.ts
inline src/renderer/src/runtime/sync-runtime-graph.ts
inline src/renderer/src/runtime/web-runtime-session.ts
inline src/renderer/src/runtime/web-session-tabs-sync.ts
inline src/renderer/src/store/slices/agent-status.ts
inline src/renderer/src/store/slices/browser.ts
-inline src/renderer/src/store/slices/diffComments.ts
-inline src/renderer/src/store/slices/hosted-review.ts
inline src/renderer/src/store/slices/linear.ts
inline src/renderer/src/store/slices/tabs.ts
inline src/renderer/src/store/slices/ui.ts
-inline src/shared/automation-schedules.ts
-inline src/shared/commit-message-agent-spec.ts
-inline src/shared/constants.ts
inline src/shared/keybindings.ts
-inline src/shared/marine-creatures.ts
inline src/shared/telemetry-events.ts
-inline src/shared/text-search.ts
-inline tests/e2e/helpers/terminal.ts
mobile-config app/h/*/files/*.tsx
mobile-config app/h/*/index.tsx
mobile-config app/h/*/session/*.tsx
mobile-config app/h/*/source-control/*.tsx
mobile-config app/h/*/tasks.tsx
mobile-config app/index.tsx
-mobile-config app/pair-scan.tsx
mobile-config app/troubleshoot.tsx
mobile-config scripts/mock-server.ts
-mobile-config scripts/repro-terminal-colors.ts
-mobile-config scripts/repro-worktree-startup-stream.ts
mobile-config src/browser/MobileBrowserPane.tsx
-mobile-config src/components/CustomKeyModal.tsx
mobile-config src/components/NewWorktreeModal.tsx
-mobile-config src/components/mobile-rich-markdown-editor-html.ts
-mobile-config src/terminal/terminal-accessory-keys.ts
mobile-config src/terminal/terminal-webview-html.ts
mobile-config src/transport/rpc-client.ts
diff --git a/config/reliability-gates.jsonc b/config/reliability-gates.jsonc
index 68dce578bc0..0fc9a8645ed 100644
--- a/config/reliability-gates.jsonc
+++ b/config/reliability-gates.jsonc
@@ -2817,16 +2817,16 @@
},
{
"id": "browser-session.remote-html-preview-ownership",
- "title": "Paired HTML previews render locally as document browser tabs and create no host browser page",
+ "title": "Paired HTML previews keep one host page and one stable client split",
"maturity": "experimental",
"protection": "partial",
"owner": "browser-runtime",
"layer": "renderer-paired-runtime-reconciliation",
"surfaces": [
- "paired HTML file preview served locally over the orca-preview scheme",
- "preview split placement that leaves editor focus alone",
- "grant-scoped document reads, including truncation and out-of-worktree refusal",
- "capability-unknown and mixed-version browser-creation rejection cleanup",
+ "paired remote HTML file preview",
+ "background preview creation and explicit browser activation",
+ "client-only split placement",
+ "capability-unknown and mixed-version preview rejection cleanup",
"post-create reconciliation failure and compensating rollback",
"remote browser close convergence"
],
@@ -2834,9 +2834,8 @@
"providers": ["remote-runtime"],
"coveredPlatforms": ["macos"],
"coveredProviders": ["remote-runtime"],
- "coverageNotes": "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.",
+ "coverageNotes": "A headed macOS desktop host and separate paired client exercise the Explorer eye-button path with a real remote file URL, host browser DOM, renderer split layout, background preview creation, explicit browser activation, browser close, forced capability rejection after caller preflight, a forced post-create reconciliation timeout, and navigation held beyond the client create deadline. The same reconciliation fault oracles pass against a live headless server. Unit contracts cover absent, unknown, and mixed-version capabilities, visible rejection, empty-split cleanup, owning-runtime routing, canonical preallocated page identity, navigation-independent acknowledgement, exact client materialization, confirmed rollback, ambiguous cleanup, and teardown. Linux and Windows live journeys remain gaps.",
"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",
"https://linear.app/stably/issue/STA-4025/p1-browser-create-reports-success-when-tab-reconcile-fails-after-pr",
"https://linear.app/stably/issue/STA-4063/p1-paired-html-preview-rejects-outside-cleanup-after-pr-13909",
@@ -2844,35 +2843,19 @@
"https://github.com/stablyai/orca/pull/14363",
"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 that holds focus and is still bound to a live grant, leaves as an Orca browser tab rather than a native window or a dead click. 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": "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 anti-detection 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. Hydrate a session carrying preview chrome from a build that made previews editor tabs and require it dropped while the ordinary editor tab for the same document survives. 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. Drive that notice directly for a guest bound to a live grant, for repeated attempts inside and outside its interval, for two previews at once, and for a contents no preview is bound to; 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 an unfocused guest, a sender that is no preview guest, a guest with no bound or a revoked grant, and non-web URLs, 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.",
+ "invariant": "Opening one HTML preview from a paired client succeeds only after exactly one browser page on the worktree-owning runtime materializes as exactly one client browser workspace and unified tab in the requested group. 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. Preview focus and close preserve the source editor and terminal.",
+ "oracle": "Run one unchanged contract oracle for direct create and side-preview callers with absent status, unknown capabilities, and a mixed-version host. Require the original unsupported error or visible toast, zero RPCs, and no retained new split. Run it on the scan target, latest main, candidate, and candidate with the cleanup boundary disabled. In headed paired Electron, arm capability rejection after the caller preflight, invoke the visible HTML side-preview action, require the failure toast, and compare exact host/client browser, editor, terminal, and group inventories to baseline. Hold a real navigation response beyond the 15-second client deadline after host creation; require the first RPC to return the exact host inventory page ID, one host page, and no retry. Repeat reconciliation faults against headless serve. Retain the separate exact-page reconciliation rollback oracle.",
"commands": [
"pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/orca-runtime-browser.test.ts src/main/runtime/rpc/methods/browser.test.ts src/renderer/src/lib/file-preview.test.ts src/renderer/src/runtime/web-session-browser-placement.test.ts src/renderer/src/runtime/web-runtime-session.test.ts src/renderer/src/runtime/web-session-tabs-sync.test.ts src/renderer/src/runtime/remote-server-parity.test.ts",
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/runtime/web-runtime-browser-materialization.test.ts",
- "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/runtime/web-runtime-browser-capability-cleanup.test.ts",
- "pnpm exec vitest run --config config/vitest.config.ts src/main/browser/doc-preview-grant-registry.test.ts src/main/browser/doc-preview-file-reader.test.ts src/main/browser/doc-preview-protocol.test.ts src/main/browser/doc-preview-guest-policy.test.ts src/main/browser/doc-preview-failure-notice.test.ts src/shared/doc-preview-scheme.test.ts src/renderer/src/lib/doc-preview-grants.test.ts src/renderer/src/lib/file-preview.test.ts src/renderer/src/lib/file-preview-action-visibility.test.tsx src/main/window/main-window-webview-security.test.ts src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.failure-message.test.tsx src/renderer/src/hooks/ipc-events/doc-preview-external-link-bridge.test.ts src/renderer/src/components/terminal-pane/terminal-file-link-actions.test.ts",
- "pnpm exec vitest run --config config/vitest.config.ts src/main/browser/browser-session-partition-policies.test.ts src/main/browser/doc-preview-protocol.test.ts",
- "pnpm exec vitest run --config config/vitest.config.ts src/main/browser/doc-preview-download-block-notice.test.ts src/main/browser/browser-session-partition-policies.test.ts src/main/browser/doc-preview-guest-policy.test.ts src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.failure-message.test.tsx",
- "pnpm exec vitest run --config config/vitest.config.ts src/main/browser/browser-manager-guest-policy-profile.test.ts src/main/window/main-window-webview-security.test.ts src/main/browser/doc-preview-guest-policy.test.ts",
- "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/store/slices/browser-workspace-doc-location.test.ts src/renderer/src/lib/file-preview.test.ts",
- "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/store/slices/browser-workspace-doc-location.test.ts src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.toolbar.test.tsx src/shared/doc-preview-scheme.test.ts",
- "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/store/slices/browser-workspace-doc-location.test.ts",
- // The hardening round: the third url door, the group projection, and both directions of the focus handoff.
- "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/store/slices/browser-workspace-doc-location.test.ts src/renderer/src/runtime/sync-runtime-graph-editor-diff-tabs.test.ts src/renderer/src/components/browser-pane/workspace-doc/workspace-doc-page-pane.test.tsx src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.toolbar.test.tsx",
- "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.failure-message.test.tsx",
- "pnpm exec vitest run --config config/vitest.config.ts src/preload/doc-preview-link-interception.test.ts src/main/browser/doc-preview-guest-policy.test.ts src/main/window/main-window-webview-security.test.ts src/main/ipc/doc-preview-grant-ipc.test.ts",
- "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/browser-preview-tool-authorization.test.ts src/main/browser/doc-preview-guest-policy.test.ts src/main/ipc/browser.test.ts",
- "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/browser-preview-tool-authorization.test.ts src/main/browser/browser-manager-annotation-bridge.test.ts src/main/browser/browser-manager-guest-lifecycle.test.ts src/shared/doc-preview-scheme.test.ts src/renderer/src/components/browser-pane/workspace-doc/doc-preview-document-actions.test.ts src/renderer/src/components/browser-pane/workspace-doc/use-doc-preview-guest-tools.test.ts src/renderer/src/components/editor/EditorPanelShell.header.test.tsx src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.toolbar.test.tsx",
- "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/browser-preview-tool-authorization.test.ts src/main/browser/browser-manager-annotation-bridge.test.ts src/main/browser/browser-manager-guest-lifecycle.test.ts src/main/browser/offscreen-browser-backend-lifecycle.test.ts src/main/browser/doc-preview-guest-policy.test.ts src/shared/doc-preview-scheme.test.ts src/renderer/src/components/browser-pane/workspace-doc/doc-preview-document-actions.test.ts src/renderer/src/components/browser-pane/workspace-doc/use-doc-preview-guest-tools.test.ts src/renderer/src/components/editor/EditorPanelShell.header.test.tsx src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.toolbar.test.tsx",
- "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/doc-preview-grant-ipc.test.ts src/renderer/src/components/terminal-pane/TerminalLinkActionPopover.test.tsx src/renderer/src/runtime/sync-runtime-graph-editor-diff-tabs.test.ts src/renderer/src/store/slices/tabs-hydration.test.ts",
- "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/browser-preview-tool-authorization.test.ts src/main/ipc/browser-tab-registration-wait.test.ts src/main/ipc/doc-preview-grant-ipc.test.ts src/main/browser/browser-manager-guest-lifecycle.test.ts src/main/browser/browser-manager-guest-policy-profile.test.ts src/main/browser/browser-manager-annotation-bridge.test.ts src/main/browser/offscreen-browser-backend-lifecycle.test.ts src/main/browser/doc-preview-guest-policy.test.ts src/shared/doc-preview-scheme.test.ts src/renderer/src/components/browser-pane/workspace-doc/use-doc-preview-guest-tools.test.ts src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.toolbar.test.tsx",
- "SKIP_BUILD=1 ORCA_E2E_WEB_CLIENT=1 ORCA_E2E_FORCE_HEADFUL=1 pnpm exec playwright test tests/e2e/paired-remote-html-preview-local-render.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1 --reporter=line",
- "SKIP_BUILD=1 ORCA_E2E_WEB_CLIENT=1 ORCA_E2E_FORCE_HEADFUL=1 pnpm exec playwright test tests/e2e/paired-remote-html-preview-local-render.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1 --reporter=line --repeat-each=3",
- // The changed-spec e2e job's own command, which is the only Linux run this gate has.
- "npx playwright test --config tests/playwright.config.ts --project=electron-headless tests/e2e/paired-remote-html-preview-local-render.spec.ts --workers=1",
+ "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/runtime/web-runtime-browser-capability-cleanup.test.ts src/renderer/src/lib/file-preview-capability-cleanup.test.ts",
"pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/orca-runtime-browser.test.ts src/renderer/src/runtime/web-runtime-session.test.ts",
"pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/orca-runtime.test.ts src/main/runtime/orca-runtime-browser.test.ts src/renderer/src/hooks/useIpcEvents-browser-tab-create.test.ts src/renderer/src/hooks/useIpcEvents-close-routing-browser-pages.test.ts src/renderer/src/hooks/useIpcEvents-close-routing-session-tabs.test.ts src/renderer/src/store/slices/browser.test.ts src/renderer/src/runtime/web-runtime-session.test.ts",
"SKIP_BUILD=1 pnpm exec playwright test tests/e2e/paired-browser-create-navigation-deadline.spec.ts --config tests/playwright.config.ts --project electron-headful --workers=1",
+ "ORCA_E2E_WEB_CLIENT=1 ORCA_E2E_FORCE_HEADFUL=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm exec playwright test tests/e2e/paired-remote-html-browser-focus.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1",
+ "ORCA_E2E_WEB_CLIENT=1 ORCA_E2E_FORCE_HEADFUL=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm exec playwright test tests/e2e/paired-remote-html-browser-focus.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1 --repeat-each=3",
+ "SKIP_BUILD=1 ORCA_E2E_WEB_CLIENT=1 ORCA_E2E_FORCE_HEADFUL=1 pnpm exec playwright test tests/e2e/paired-remote-html-browser-focus.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1",
+ "ORCA_E2E_WEB_CLIENT=1 ORCA_E2E_FORCE_HEADFUL=1 pnpm exec playwright test tests/e2e/paired-remote-html-browser-focus.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1",
"SKIP_BUILD=1 ORCA_E2E_FORCE_HEADFUL=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm exec playwright test tests/e2e/paired-browser-creation-reconciliation-failure.spec.ts --config tests/playwright.config.ts --project electron-headful --workers=1 --reporter=line",
"SKIP_BUILD=1 ORCA_E2E_FORCE_HEADFUL=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm exec playwright test tests/e2e/paired-browser-creation-reconciliation-failure.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1 --reporter=line"
],
@@ -2880,6 +2863,7 @@
"src/main/runtime/orca-runtime-browser.test.ts",
"src/main/runtime/rpc/methods/browser.test.ts",
"src/renderer/src/lib/file-preview.test.ts",
+ "src/renderer/src/lib/file-preview-capability-cleanup.test.ts",
"src/renderer/src/runtime/web-session-browser-placement.test.ts",
"src/renderer/src/runtime/web-runtime-browser-materialization.test.ts",
"src/renderer/src/runtime/web-runtime-browser-capability-cleanup.test.ts",
@@ -2890,38 +2874,7 @@
"src/renderer/src/hooks/useIpcEvents-close-routing-browser-pages.test.ts",
"src/renderer/src/hooks/useIpcEvents-close-routing-session-tabs.test.ts",
"src/renderer/src/store/slices/browser.test.ts",
- "src/main/browser/doc-preview-grant-registry.test.ts",
- "src/main/browser/doc-preview-file-reader.test.ts",
- "src/main/browser/doc-preview-protocol.test.ts",
- "src/main/browser/browser-session-partition-policies.test.ts",
- "src/main/browser/doc-preview-download-block-notice.test.ts",
- "src/main/browser/browser-manager-guest-policy-profile.test.ts",
- "src/main/browser/doc-preview-guest-policy.test.ts",
- "src/main/ipc/browser-tab-registration-wait.test.ts",
- "src/main/browser/doc-preview-failure-notice.test.ts",
- "src/renderer/src/store/slices/browser-workspace-doc-location.test.ts",
- "src/preload/doc-preview-link-interception.test.ts",
- "src/main/window/main-window-webview-security.test.ts",
- "src/main/ipc/browser-preview-tool-authorization.test.ts",
- "src/main/browser/browser-manager-annotation-bridge.test.ts",
- "src/main/browser/browser-manager-guest-lifecycle.test.ts",
- "src/main/browser/offscreen-browser-backend-lifecycle.test.ts",
- "src/renderer/src/components/browser-pane/workspace-doc/doc-preview-document-actions.test.ts",
- "src/renderer/src/components/browser-pane/workspace-doc/use-doc-preview-guest-tools.test.ts",
- "src/renderer/src/components/editor/EditorPanelShell.header.test.tsx",
- "src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.toolbar.test.tsx",
- "src/renderer/src/components/browser-pane/workspace-doc/workspace-doc-page-pane.test.tsx",
- "src/shared/doc-preview-scheme.test.ts",
- "src/renderer/src/lib/doc-preview-grants.test.ts",
- "src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.failure-message.test.tsx",
- "src/renderer/src/lib/file-preview-action-visibility.test.tsx",
- "src/renderer/src/hooks/ipc-events/doc-preview-external-link-bridge.test.ts",
- "src/renderer/src/components/terminal-pane/terminal-file-link-actions.test.ts",
- "src/main/ipc/doc-preview-grant-ipc.test.ts",
- "src/renderer/src/store/slices/tabs-hydration.test.ts",
- "src/renderer/src/components/terminal-pane/TerminalLinkActionPopover.test.tsx",
- "src/renderer/src/runtime/sync-runtime-graph-editor-diff-tabs.test.ts",
- "tests/e2e/paired-remote-html-preview-local-render.spec.ts",
+ "tests/e2e/paired-remote-html-browser-focus.spec.ts",
"tests/e2e/paired-browser-create-navigation-deadline.spec.ts",
"tests/e2e/paired-browser-creation-reconciliation-failure.spec.ts"
],
@@ -2933,13 +2886,18 @@
"direct creation removes the caller-declared new split without issuing an RPC"
]
},
+ {
+ "file": "src/renderer/src/lib/file-preview-capability-cleanup.test.ts",
+ "assertions": [
+ "known unavailability reports visibly without creating a split",
+ "capability loss after caller preflight reports visibly without duplicating transaction cleanup"
+ ]
+ },
{
"file": "src/renderer/src/lib/file-preview.test.ts",
"assertions": [
- "a paired-runtime HTML preview opens a local doc preview instead of a runtime browser tab",
- "a local workspace still opens the embedded browser tab at a file URL",
- "a paired document outside the worktree names that boundary and creates no split",
- "an SSH document outside the worktree root still previews"
+ "a failed side preview reports visibly without caller-owned split cleanup",
+ "overlapping side previews reuse the split without the first failure deleting it"
]
},
{
@@ -3015,152 +2973,11 @@
]
},
{
- "file": "tests/e2e/paired-remote-html-preview-local-render.spec.ts",
+ "file": "tests/e2e/paired-remote-html-browser-focus.spec.ts",
"assertions": [
- "the host's HTML document renders inside the client-local orca-preview guest",
- "neither the client browser-workspace count nor the host browser-tab count moves off baseline",
- "the preview opens in its own split with mode html-preview and leaves the source editor active",
- "an explicit click reactivates the preview, and closing it preserves the source editor, its group, and the terminal",
- "the document reports having attempted both its unattended and its post-press egress, so neither absence assertion can pass on a document that never tried",
- "a scripted window.open and location.href driven by a real press elsewhere in the document route nothing and move no browser count",
- "a real target=_blank press in the rendered document routes out as an Orca browser tab while the preview stays open"
- ]
- },
- {
- "file": "src/main/browser/doc-preview-file-reader.test.ts",
- "assertions": [
- "a host-reported truncated read is refused instead of served as partial bytes",
- "an over-cap binary and an empty binary body are reported as too-large and unsupported rather than a bare read failure",
- "a document outside the paired worktree is refused without issuing an RPC",
- "an SSH path that canonicalizes outside the grant root is refused before any read",
- "a 0-byte file the host still typed is served as 0 bytes rather than reported as a refused format"
- ]
- },
- {
- "file": "src/main/browser/doc-preview-grant-registry.test.ts",
- "assertions": [
- "a path escaping the grant root resolves to nothing, including sibling-prefix roots",
- "a revoked grant stops resolving"
- ]
- },
- {
- "file": "src/main/browser/doc-preview-protocol.test.ts",
- "assertions": [
- "an unknown or revoked grant is answered exactly like a missing file, with no read attempted",
- "a failed read pushes its reason for the requested path while a served document pushes nothing",
- "every served document carries the self-only content security policy",
- "the preview session cancels any request that is not in-document",
- "the scheme is handled on the preview session and never on the default one",
- "the scheme claims exactly its five privileges, so a widened one fails"
- ]
- },
- {
- "file": "src/main/browser/doc-preview-failure-notice.test.ts",
- "assertions": ["413 and 415 map to distinct reasons and everything else stays generic"]
- },
- {
- "file": "src/renderer/src/lib/file-preview-action-visibility.test.tsx",
- "assertions": [
- "the Explorer row predicate and the preview plan return one answer for the same file"
- ]
- },
- {
- "file": "src/renderer/src/hooks/ipc-events/doc-preview-external-link-bridge.test.ts",
- "assertions": [
- "an external preview link becomes a browser tab, and a tab that is refused or that fails outright surfaces instead of being dropped"
- ]
- },
- {
- "file": "src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.failure-message.test.tsx",
- "assertions": [
- "each entry-document failure reason surfaces its own sentence instead of one generic unavailable panel",
- "a refused subresource names itself in a notice, deduplicated per path, while the document stays rendered",
- "an entry-document failure replaces the asset notice with the panel",
- "a failure for another preview's grant leaves the rendered document alone"
- ]
- },
- {
- "file": "src/main/browser/doc-preview-guest-policy.test.ts",
- "assertions": [
- "the guest may only navigate within its own live grant, and not at all until a document has bound it to one",
- "every navigation and popup sink refuses without routing, whenever genuine input happened",
- "a subframe cannot navigate outside the grant, and only the main frame binds the guest to one",
- "a reported click routes exactly once, and only from a focused preview guest with a live bound grant",
- "a reported click on a non-web URL, or from a destroyed guest, routes nothing",
- "the guest is denied non-proxied WebRTC UDP when its policy is installed"
- ]
- },
- {
- "file": "src/preload/doc-preview-link-interception.test.ts",
- "assertions": [
- "a click the document dispatched itself is not trusted and routes nothing",
- "a trusted press on an external anchor reports it once and stops the navigation, including through what the anchor wraps and an SVG animated href",
- "a fragment link scrolls in the document, raw or percent-encoded, and a bare hash goes to the top",
- "a sibling preview document is left to the guest's own navigation, and a middle click opens nothing whether or not it is trusted",
- "an SVG link is resolved against the document, so its relative, rooted and fragment forms behave exactly like the identical HTML anchor",
- "the preload reports on exactly the channel main listens on"
- ]
- },
- {
- "file": "src/main/window/main-window-webview-security.test.ts",
- "assertions": [
- "an orca-preview src is admitted only on the preview partition with a live grant",
- "preview guests receive their own policy instead of the browser guest policies",
- "a preview attach is pinned to the preview preload over any renderer-supplied value, and a browsing attach never receives it",
- "grants a renderer can no longer release are cleared at window creation and on reload, and survive an in-document navigation"
- ]
- },
- {
- "file": "src/shared/doc-preview-scheme.test.ts",
- "assertions": ["preview URLs round-trip per segment and reject a malformed grant id"]
- },
- {
- "file": "src/renderer/src/lib/doc-preview-grants.test.ts",
- "assertions": [
- "grants are deduped per preview tab so a StrictMode double-mount cannot revoke the surviving webview",
- "a mint that rejects after its tab was released and reopened leaves the reopened tab's grant revocable"
- ]
- },
- {
- "file": "src/main/ipc/doc-preview-grant-ipc.test.ts",
- "assertions": [
- "a sender that is not the trusted renderer mints no grant and revokes none",
- "a reported link click reaches the guest policy with its own sender, and a non-string report reaches nothing"
- ]
- },
- {
- "file": "src/renderer/src/store/slices/browser-workspace-doc-location.test.ts",
- "assertions": [
- "closing a document browser tab revokes its grant, and closing a URL tab beside it revokes nothing",
- "a document page's url is blank in the store, in the written session and after restore",
- "a navigation committing a url onto a document page leaves the url blank, the document title standing and nothing loading, while an ordinary page beside it takes the url it is given"
- ]
- },
- {
- "file": "src/renderer/src/store/slices/tabs-hydration.test.ts",
- "assertions": [
- "chrome for a preview tab written before previews were browser tabs is dropped, and the ordinary editor tab for the same document is kept"
- ]
- },
- {
- "file": "src/renderer/src/runtime/sync-runtime-graph-editor-diff-tabs.test.ts",
- "assertions": [
- "a browser tab located by a workspace document is not published to mobile clients, while an ordinary browser tab beside it is",
- "with tab groups in play the document is kept out of the published groups too: no group's order, recency or active tab names a tab the snapshot does not carry, the held-back group leaves the published layout tree with it, and the grouped URL tab beside it still publishes"
- ]
- },
- {
- "file": "src/renderer/src/components/browser-pane/workspace-doc/workspace-doc-page-pane.test.tsx",
- "assertions": [
- "the pane offers its guest focus when the reader is in this very preview",
- "it refuses while the reader is in a terminal in front of it, in a different browser tab, or while its own page is not the active one"
- ]
- },
- {
- "file": "src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.toolbar.test.tsx",
- "assertions": [
- "a preview that is the reader's surface hands its guest focus, and one that is not leaves focus alone",
- "the window taking focus back re-offers it to the guest, and re-offers nothing for a preview that was refusing"
+ "one HTML open produces one client workspace, one client unified tab, and one host page",
+ "the source editor and remote frame remain visible in separate groups",
+ "preview creation leaves the editor active, an explicit browser click is authoritative, and close preserves the editor and terminal"
]
},
{
@@ -3182,301 +2999,22 @@
],
"evidenceRuns": [
{
- "date": "2026-08-27",
+ "date": "2026-08-12",
"runner": "local",
"platform": "macos",
- "command": "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/store/slices/browser-workspace-doc-location.test.ts src/renderer/src/runtime/sync-runtime-graph-editor-diff-tabs.test.ts src/renderer/src/components/browser-pane/workspace-doc/workspace-doc-page-pane.test.tsx src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.toolbar.test.tsx",
+ "command": "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/runtime/web-runtime-browser-capability-cleanup.test.ts src/renderer/src/lib/file-preview-capability-cleanup.test.ts",
"result": "failed",
- "durationSeconds": 7,
- "summary": "The adversarial pass answered the request for its own group-projection probe after this round landed, and it had measured a third observable this test was missing: the held-back group also has to leave the published layout tree, or the phone renders a split whose second pane can never hold anything. Adopted as a fourth assertion and re-run against the same sole delta, which now fails on the dangling-reference check and, with that check blinded, on the layout alone — group-left and group-right against the group-right the clean tree publishes. The two probes agreed on everything else, including that the tab list is unchanged under this mutant, which is why an assertion reading only the tab list cannot reach the site at all."
+ "durationSeconds": 0.72,
+ "summary": "The unchanged STA-4063 oracle failed 5/5 on scan e4c278eb52 and latest main bc2e30000b: direct create never closed the new split, and side-preview rejection produced neither cleanup nor a toast. Pre-fix PR #14100 passed the two caller cases but still failed all three direct transaction cases."
},
{
- "date": "2026-08-27",
+ "date": "2026-08-12",
"runner": "local",
"platform": "macos",
- "command": "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/store/slices/browser-workspace-doc-location.test.ts src/renderer/src/runtime/sync-runtime-graph-editor-diff-tabs.test.ts src/renderer/src/components/browser-pane/workspace-doc/workspace-doc-page-pane.test.tsx src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.toolbar.test.tsx",
- "result": "passed",
- "durationSeconds": 3,
- "summary": "The adversarial pass's four coverage findings, landed and pinned: 58/58. The third url door now keeps a document page blank and names it by its document, the group projection holds a document back from published groups as well as from the tab list, the pane's focus handoff is pinned in the widening direction as well as the narrowing one, and the handoff is re-offered when the window takes focus back."
- },
- {
- "date": "2026-08-27",
- "runner": "local",
- "platform": "macos",
- "command": "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/store/slices/browser-workspace-doc-location.test.ts src/renderer/src/runtime/sync-runtime-graph-editor-diff-tabs.test.ts src/renderer/src/components/browser-pane/workspace-doc/workspace-doc-page-pane.test.tsx src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.toolbar.test.tsx",
- "result": "failed",
- "durationSeconds": 3,
- "summary": "Three sole-delta mutants on the third url door, each failing only the new store test: committing the reported url onto a document page, dropping the document from the title normalizer there (which renames the tab New Tab, since a blank url is what that fallback reads), and marking a blank document page loading. The ordinary-page half of the same test passes under all three, so the door is pinned as a document-only refusal rather than as a door that writes nothing."
- },
- {
- "date": "2026-08-27",
- "runner": "local",
- "platform": "macos",
- "command": "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/store/slices/browser-workspace-doc-location.test.ts src/renderer/src/runtime/sync-runtime-graph-editor-diff-tabs.test.ts src/renderer/src/components/browser-pane/workspace-doc/workspace-doc-page-pane.test.tsx src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.toolbar.test.tsx",
- "result": "failed",
- "durationSeconds": 3,
- "summary": "Two sole-delta mutants on the mobile publish boundary. Dropping the document from the group projection's browser ids fails only the new grouped test — the tab list stays correct because the per-tab guard still refuses the tab, and what breaks is a published group whose order and active tab name a tab the phone is never sent. This is the survivor the adversarial pass reported: it survives the whole suite without this test, because no other test in it has tab groups. Dropping the per-tab guard instead fails only the ungrouped test, so the two halves of the boundary are pinned separately."
- },
- {
- "date": "2026-08-27",
- "runner": "local",
- "platform": "macos",
- "command": "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/store/slices/browser-workspace-doc-location.test.ts src/renderer/src/runtime/sync-runtime-graph-editor-diff-tabs.test.ts src/renderer/src/components/browser-pane/workspace-doc/workspace-doc-page-pane.test.tsx src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.toolbar.test.tsx",
- "result": "failed",
- "durationSeconds": 3,
- "summary": "Three sole-delta mutants widening the focus handoff, the direction the adversarial pass found uncovered: dropping the page-activity half, dropping the active-tab-type half, and dropping the active-browser-tab half each fail exactly one of the pane's refusals — the inactive page, the terminal in front, and the other browser tab respectively. The presence half of that test passes under all three, so a pane that had stopped offering focus at all would still fail rather than pass the refusals vacuously."
- },
- {
- "date": "2026-08-27",
- "runner": "local",
- "platform": "macos",
- "command": "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/store/slices/browser-workspace-doc-location.test.ts src/renderer/src/runtime/sync-runtime-graph-editor-diff-tabs.test.ts src/renderer/src/components/browser-pane/workspace-doc/workspace-doc-page-pane.test.tsx src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.toolbar.test.tsx",
- "result": "failed",
- "durationSeconds": 3,
- "summary": "One sole-delta mutant dropping the window-focus re-offer: the guest is focused once at mount and never again after the window comes back, which is the desktop gap behind it — another app taking the front leaves the preview's only route out shut until the pane remounts. It fails only the new re-offer test, and the refusing-preview case beside it stays green, so the re-offer is pinned as gated rather than unconditional."
- },
- {
- "date": "2026-08-27",
- "runner": "local",
- "platform": "macos",
- "command": "SKIP_BUILD=1 ORCA_E2E_WEB_CLIENT=1 ORCA_E2E_FORCE_HEADFUL=1 pnpm exec playwright test tests/e2e/paired-remote-html-preview-local-render.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1 --reporter=line",
- "result": "passed",
- "durationSeconds": 186,
- "summary": "The rewritten journey passed 2/2. The first test proves the inverted claim: the client gains exactly one browser workspace and it is the document one — page and tab url blank, the document path mirrored onto both, the tab named by the document's own title, the chip still naming the file, no editor row of the retired preview species — while the host's page registry and tab snapshot both stay at their pre-action content. Both host oracles carry a presence precondition in the same run: an ordinary URL browser tab this client opens does arrive in browser.tabList and in session.tabs.list, and leaves both again when it is closed, so the preview's absence is a decision rather than an oracle that cannot see browser pages. The document still reaches nothing on its own (zero ICE candidates, unattended and post-press egress both swallowed), and the one route out still works: a real press on a target=_blank anchor routes exactly one call and opens a tab while the preview stays open. The second test quits the client with a document tab open and relaunches it on the same profile: 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."
- },
- {
- "date": "2026-08-27",
- "runner": "local",
- "platform": "macos",
- "command": "SKIP_BUILD=1 ORCA_E2E_WEB_CLIENT=1 ORCA_E2E_FORCE_HEADFUL=1 pnpm exec playwright test tests/e2e/paired-remote-html-preview-local-render.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1 --reporter=line",
- "result": "failed",
- "durationSeconds": 108,
- "summary": "Candidate with the sole delta that the preview action also opens an ordinary browser tab through the runtime — what it did before STA-5557 — failed at the client-workspace count, 2 against the 1 the claim allows. It dies there rather than at the host oracle, so it proves the exactly-one-workspace half; the host half has no single line to mutate, since a document page never calls the runtime at all, and is protected instead by the presence precondition recorded above."
- },
- {
- "date": "2026-08-27",
- "runner": "local",
- "platform": "macos",
- "command": "SKIP_BUILD=1 ORCA_E2E_WEB_CLIENT=1 ORCA_E2E_FORCE_HEADFUL=1 pnpm exec playwright test tests/e2e/paired-remote-html-preview-local-render.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1 --reporter=line",
- "result": "failed",
- "durationSeconds": 190,
- "summary": "Candidate with the sole delta that the workspace row stops mirroring the document its active page shows failed in both tests on workspaceDocFilePath, null against the document path — the tab entry would go on naming nothing while the page it renders is a document. Two earlier mutants survived and are reported as ill-posed rather than as blind spots: flipping the mobile publish filter changes what this desktop publishes to its own phone clients, not what a paired client puts on its host, and flipping the document mirror in the workspace constructor is repaired on the next state write by the active-page mirror, which is the line the mutant above hits."
- },
- {
- "date": "2026-08-27",
- "runner": "local",
- "platform": "macos",
- "command": "SKIP_BUILD=1 ORCA_E2E_WEB_CLIENT=1 ORCA_E2E_FORCE_HEADFUL=1 pnpm exec playwright test tests/e2e/paired-remote-html-preview-local-render.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1 --reporter=line",
- "result": "failed",
- "durationSeconds": 195,
- "summary": "Candidate with the sole delta that the preview pane does not hand focus to its guest — the tree as it stood before this change — failed at the target=_blank press: six real presses reached the anchor, the embedder had the webview as its active element, and main's own log answered each report with registered true, grant live, focused false. This is the gap the rewrite found: since previews became browser pages they had no chrome to pass focus on, so the one route out of a preview was shut."
- },
- {
- "date": "2026-08-27",
- "runner": "local",
- "platform": "macos",
- "command": "pnpm exec vitest run --config config/vitest.config.ts src/main/browser/doc-preview-grant-registry.test.ts src/main/browser/doc-preview-file-reader.test.ts src/main/browser/doc-preview-protocol.test.ts src/main/browser/doc-preview-guest-policy.test.ts src/main/browser/doc-preview-failure-notice.test.ts src/shared/doc-preview-scheme.test.ts src/renderer/src/lib/doc-preview-grants.test.ts src/renderer/src/lib/file-preview.test.ts src/renderer/src/lib/file-preview-action-visibility.test.tsx src/main/window/main-window-webview-security.test.ts src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.failure-message.test.tsx src/renderer/src/hooks/ipc-events/doc-preview-external-link-bridge.test.ts src/renderer/src/components/terminal-pane/terminal-file-link-actions.test.ts",
- "result": "passed",
- "durationSeconds": 1.37,
- "summary": "174/174 over the grant, reader, protocol, guest-policy, scheme-admission, plan-routing, action-visibility, failure-message, external-link and terminal-row contracts, re-issued at the relocated preview paths. This replaces the 2026-08-26 record of the same thirteen files at 152/152, whose command named the pre-relocation paths: it established the stale-mint rejection that must not evict a reopened preview tab, the CSP header and session request filter, the SSH symlink refusal, subframe containment, truncated and over-cap reads, the unservable-asset and empty-file split, the out-of-worktree paired refusal with SSH left unrestricted, reload re-minting a stale grant, and the guest deny-only navigation sinks. The count moved with the cases added since; nothing in those files changed but their directory."
- },
- {
- "date": "2026-08-27",
- "runner": "local",
- "platform": "macos",
- "command": "pnpm exec vitest run --config config/vitest.config.ts src/main/browser/doc-preview-download-block-notice.test.ts src/main/browser/browser-session-partition-policies.test.ts src/main/browser/doc-preview-guest-policy.test.ts src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.failure-message.test.tsx",
- "result": "passed",
- "durationSeconds": 1.78,
- "summary": "64/64 on the visible download refusal, re-issued at the relocated path. This replaces the 2026-08-27 record of the same four files at 62/62, whose ten mutants — removing the notice from the deny listener, adding it to the routing listener, dropping the guest-registry lookup that names which preview to tell, dropping the shell branch and the row, letting the row survive the failure panel, removing the per-preview throttle, sharing one throttle across previews, never expiring the window, and publishing for a contents no preview is bound to — all failed as sole deltas. The relocation moved no line of any of them."
- },
- {
- "date": "2026-08-27",
- "runner": "local",
- "platform": "macos",
- "command": "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/store/slices/browser-workspace-doc-location.test.ts src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.toolbar.test.tsx src/shared/doc-preview-scheme.test.ts",
- "result": "passed",
- "durationSeconds": 2.05,
- "summary": "39/39 on the title-parity trio at the relocated path, replacing the 2026-08-27 record of the same three files, which killed five mutants as sole deltas: disabling the document branch restores New Tab for a named document, dropping the grant-URL refusal puts the scheme into the title, the tab mirror and the written session, returning New Tab instead of the file name breaks the fallback, removing the guest listener leaves the document unable to name its tab, and making the scheme test parse rather than prefix-match lets a title that merely looks like a preview URL through."
- },
- {
- "date": "2026-08-27",
- "runner": "local",
- "platform": "macos",
- "command": "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.failure-message.test.tsx",
- "result": "passed",
- "durationSeconds": 1.54,
- "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."
- },
- {
- "date": "2026-08-27",
- "runner": "local",
- "platform": "macos",
- "command": "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.failure-message.test.tsx",
- "result": "failed",
- "durationSeconds": 1.27,
- "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."
- },
- {
- "date": "2026-08-27",
- "runner": "local",
- "platform": "macos",
- "command": "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/browser-preview-tool-authorization.test.ts src/main/browser/browser-manager-annotation-bridge.test.ts src/main/browser/browser-manager-guest-lifecycle.test.ts src/main/browser/offscreen-browser-backend-lifecycle.test.ts src/main/browser/doc-preview-guest-policy.test.ts src/shared/doc-preview-scheme.test.ts src/renderer/src/components/browser-pane/workspace-doc/doc-preview-document-actions.test.ts src/renderer/src/components/browser-pane/workspace-doc/use-doc-preview-guest-tools.test.ts src/renderer/src/components/editor/EditorPanelShell.header.test.tsx src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.toolbar.test.tsx",
- "result": "passed",
- "durationSeconds": 1.52,
- "summary": "174/174 at the relocated paths, replacing the 2026-08-27 records of the same command at 162/162 and 158/158. Those established, as sole deltas: removing the unregister-channel refusal goes red because the grab disposal beside it drops the intent an in-flight preview grab compares by identity, while an ordinary browser page driven through the identical sequence still has its grab state disposed; removing registration’s already-gone-guest refusal goes red for both a missing and a destroyed guest; removing the map deletion on the refusal path goes red at shutdown; the bridge’s own unregisterGuest branch, restored, tears down a live page over a misaddressed request; and the download fall-through for an unresolved owner hands back a same-named local file under a remote document’s name."
- },
- {
- "date": "2026-08-27",
- "runner": "local",
- "platform": "macos",
- "command": "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/browser-preview-tool-authorization.test.ts src/main/ipc/browser-tab-registration-wait.test.ts src/main/ipc/doc-preview-grant-ipc.test.ts src/main/browser/browser-manager-guest-lifecycle.test.ts src/main/browser/browser-manager-guest-policy-profile.test.ts src/main/browser/browser-manager-annotation-bridge.test.ts src/main/browser/offscreen-browser-backend-lifecycle.test.ts src/main/browser/doc-preview-guest-policy.test.ts src/shared/doc-preview-scheme.test.ts src/renderer/src/components/browser-pane/workspace-doc/use-doc-preview-guest-tools.test.ts src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.toolbar.test.tsx",
- "result": "passed",
- "durationSeconds": 1.5,
- "summary": "175/175 at the relocated paths on the unified tool authority, replacing the 2026-08-27 record of the same command at 174/174. That run killed twelve of fourteen mutants as sole deltas — dropping the document branch from the one authority door, removing either browsing-registration refusal or the mint-side refusal that is its converse, deleting the registration notification or widening it to the worktree-wide and any-tab waits, disposing revoked state by grant instead of by page, dropping the hosting-renderer or destroyed-contents check, rebuilding the renderer tool target from the grant, and deleting the teardown identity check under a re-mint — and reported two as ill-posed rather than as kills. The relocation changed no file it names except their directory."
- },
-{
- "date": "2026-08-27",
- "runner": "local",
- "platform": "macos",
- "command": "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/store/slices/browser-workspace-doc-location.test.ts src/renderer/src/lib/file-preview.test.ts",
- "result": "passed",
- "durationSeconds": 22.09,
- "summary": "29/29 on the candidate that makes the preview a browser tab. The preview action now creates a page located by the document; reopening the same document activates the tab it is already in rather than minting a second grant on one file; and closing that tab revokes its grant, which nothing else does now that the editor tab's close hook is gone. Red-green with each mutant as the sole delta: dropping the reuse lookup opens a second tab for a document already on screen, and dropping the release on close leaves the document readable through a grant nothing revokes until the process ends. Both are paired with presence preconditions in the same runs — a second, different document still gets its own tab, and a URL tab closed beside the document tab revokes nothing, so a release fired for every close would fail rather than pass."
- },
- {
- "date": "2026-08-27",
- "runner": "local",
- "platform": "macos",
- "command": "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/doc-preview-grant-ipc.test.ts src/renderer/src/components/terminal-pane/TerminalLinkActionPopover.test.tsx src/renderer/src/runtime/sync-runtime-graph-editor-diff-tabs.test.ts src/renderer/src/store/slices/tabs-hydration.test.ts",
- "result": "passed",
- "durationSeconds": 4.77,
- "summary": "40/40 on the candidate that removes the editor preview species and moves its two boundary duties onto the browser tab. The publish filter moved with the tab: it used to exclude an editor file by mode, and now excludes a browser workspace by whether it is located by a document — asserted at both the group projection and the tab loop, because a mutant that filters only one of them still publishes. Migration is the other half: sessions written by builds that made previews editor tabs carry chrome whose entity id encodes the document, and whose document was never persisted, so it has always come back naming a surface no restore produces; that chrome is now dropped. Each mutant as the sole delta: filtering neither place publishes the document tab to mobile, filtering only the projection still publishes it, and removing the migration leaves the stale strip entry. Presence preconditions in the same runs: an ordinary browser tab beside the document tab does publish, and the ordinary editor tab for the very same document survives hydration."
- },
- {
- "date": "2026-08-27",
- "runner": "local",
- "platform": "macos",
- "command": "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/store/slices/browser-workspace-doc-location.test.ts",
- "result": "passed",
- "durationSeconds": 1.54,
- "summary": "10/10 on the candidate that gives a browser page a workspace-document location. The trap this is built around: page.url is read by persistence, the mobile publisher, history and the address bar, so a grant URL parked there would outlive the grant that named it and travel to readers that cannot resolve it. Creation overrides the url rather than trusting its caller, and restore re-asserts the same thing, so a session written by another build cannot carry one in. Red-green with each mutant as the sole delta: dropping the creation override writes the grant URL into the store and into the session payload; dropping the restore override brings a foreign session's grant URL back; leaving docLocation out of either browser schema strips it on load and restores the document as a blank New Tab under a strip entry that still names it (both schemas are plain z.object, and this is the trap that already cost one round on the editor schema); dropping it from the workspace build or the mirror leaves the tab entry naming a document its page has left, which the publish boundary reads; dropping the salvage-path copy loses the document when only the tab row survived; and dropping the address-bar exclusion focuses an address bar the surface does not have. The blank-url assertions are paired with a presence precondition in the same run: an ordinary tab created through the same call keeps its URL and does ask for the address bar."
- },
- {
- "date": "2026-08-27",
- "runner": "local",
- "platform": "macos",
- "command": "pnpm exec vitest run --config config/vitest.config.ts src/main/browser/browser-manager-guest-policy-profile.test.ts src/main/window/main-window-webview-security.test.ts src/main/browser/doc-preview-guest-policy.test.ts",
- "result": "passed",
- "durationSeconds": 0.32,
- "summary": "55/55 on the candidate that gives every guest one attach door. A preview used to reach its policies through an installer of its own, which meant the manager's attachment bookkeeping — the set registration consults and the cleanup teardown runs — never knew a preview existed. It is now a profile on `attachGuestPolicies`, and the doc policy is what that profile installs. Red-green with each mutant as the sole delta: deleting the profile branch hands a previewed document the browsing machinery (clicked-link routing, frame routing, popup child windows, anti-detection over the debugger) and lets it navigate off-grant; dropping the manager's destroyed wiring leaves the guest answerable through the grant-keyed authority after it dies and its id marked attached forever; and making the window fork ask for the default profile is caught at the fork itself. The absence assertions are paired with a presence precondition in the same run: a browsing guest attached through the same method observably takes every one of those things, so a profile that fenced nothing, or an attach path that had stopped installing anything at all, fails rather than passing empty."
- },
- {
- "date": "2026-08-27",
- "runner": "local",
- "platform": "macos",
- "command": "pnpm exec vitest run --config config/vitest.config.ts src/main/browser/browser-session-partition-policies.test.ts src/main/browser/doc-preview-protocol.test.ts",
- "result": "passed",
- "durationSeconds": 0.17,
- "summary": "22/22 on the candidate that stops a previewed document writing into this desktop's Downloads folder. The hole was read out of the shipping path, not guessed: the preview partition took the shared browser partition policies whole, including will-download, and a preview guest joins no browser-page registry, so handleGuestWillDownload resolved no owner context, routeBrowserClientDownload answered local for a non-client-hosted contents, and the item got a reserved Downloads name with no prompt and no tab to attribute it to. Downloads are now the one policy the preview partition does not inherit. Red-green with each mutant as the sole delta: making the installer ignore the deny option routes the preview partition's download to the router again, and making the preview protocol installer stop asking for the deny leaves the option unset. The absence half is paired with a presence precondition in the same run — an ordinary browsing partition installed beside the preview one in one test still reaches the download router, so a deny that fenced everything, or a download path that had been removed entirely, would fail rather than pass quietly."
- },
-{
- "date": "2026-08-27",
- "runner": "local",
- "platform": "macos",
- "command": "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/browser-preview-tool-authorization.test.ts src/main/browser/doc-preview-guest-policy.test.ts src/main/ipc/browser.test.ts",
+ "command": "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/runtime/web-runtime-browser-capability-cleanup.test.ts src/renderer/src/lib/file-preview-capability-cleanup.test.ts",
"result": "passed",
"durationSeconds": 0.7,
- "summary": "99/99 on the candidate that binds a preview guest to its grant when the policy installs, not only on a later navigation. Live on a dev instance against a real SSH workspace, the previous shape refused every tool with not-ready for as long as the reader stayed on the page they opened: the embedder hands the guest over after it has already started loading its src, so the did-start-navigation latch never saw the only navigation most previews make. Following an in-document link bound it and the tools began answering, which is what identified the miss. Two cases pin it: a guest already reporting its URL at install, and one whose first navigation only reaches the commit."
- },
- {
- "date": "2026-08-26",
- "runner": "local",
- "platform": "macos",
- "command": "SKIP_BUILD=1 ORCA_E2E_WEB_CLIENT=1 ORCA_E2E_FORCE_HEADFUL=1 pnpm exec playwright test tests/e2e/paired-remote-html-preview-local-render.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1 --reporter=line",
- "result": "passed",
- "durationSeconds": 23.1,
- "summary": "The journey passed 1/1 on the candidate that requires the guest to be bound to a grant before any navigation is allowed, rather than admitting an unbound one. The document still rendered and the target=_blank press still routed out, which is the half that would break if the renderer-set src reached will-navigate: it does not, because that load is browser-initiated, so the permissive branch it was written for is unreachable."
- },
- {
- "date": "2026-08-26",
- "runner": "ci",
- "platform": "linux",
- "command": "npx playwright test --config tests/playwright.config.ts --project=electron-headless tests/e2e/paired-remote-html-preview-local-render.spec.ts --workers=1",
- "result": "failed",
- "durationSeconds": 72,
- "summary": "The journey failed on Ubuntu at the post-press presence precondition, with #post-input-egress still reading 'idle' after 30 seconds. The trace shows the press was dispatched at 769.5,143.9, a point the guest's own hit test had just confirmed was on its heading, and the focus report before it was already isFocused true, so neither focus nor coordinates were the break: the press never entered the guest at all. A standalone Electron 43 probe under Xvfb with this suite's Linux flags reproduced it in roughly two runs in five when pressing immediately after guest attach — the embedder received the pointerdown with the webview element as its target, meaning the browser resolved the press against the embedder because the freshly attached guest had not yet registered its hit-test region. The same probe never dropped a press on macOS, which is why local runs of either project could not see this. The spec now presses until the document answers, mirroring the target=_blank phase."
- },
- {
- "date": "2026-08-26",
- "runner": "local",
- "platform": "macos",
- "command": "SKIP_BUILD=1 ORCA_E2E_WEB_CLIENT=1 ORCA_E2E_FORCE_HEADFUL=1 pnpm exec playwright test tests/e2e/paired-remote-html-preview-local-render.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1 --reporter=line --repeat-each=3",
- "result": "passed",
- "durationSeconds": 72,
- "summary": "The journey passed 3/3 on the trusted-click candidate, which removed the recent-input gate, made both navigation sinks deny-only, dropped the webview's allowpopups attribute and gave the preview guest its own pinned preload. The fixture now also attempts a window.open and location.href from inside a pointerdown handler, so the egress rides a press the reader really made: it reported 'attempted' every run while both browser counts stayed at that phase's baseline and nothing routed, and the later real press on the target=_blank link still opened exactly one Orca browser tab with the preview still open."
- },
- {
- "date": "2026-08-26",
- "runner": "local",
- "platform": "macos",
- "command": "SKIP_BUILD=1 ORCA_E2E_WEB_CLIENT=1 ORCA_E2E_FORCE_HEADFUL=1 pnpm exec playwright test tests/e2e/paired-remote-html-preview-local-render.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1 --reporter=line",
- "result": "failed",
- "durationSeconds": 26.9,
- "summary": "Candidate with the sole delta that the navigation sinks route again behind a two-second recent-input gate failed on clientBrowserWorkspaceCountAllWorktrees 1 versus baseline 0: the document's location.href, run from inside the handler for the reader's press on the heading, opened a real browser tab. That is the case the earlier design could not distinguish from the press's own effect, and it is why the sinks route nothing at all now. The same run still refused the unattended on-load egress, so this measures the new half specifically."
- },
- {
- "date": "2026-08-26",
- "runner": "local",
- "platform": "macos",
- "command": "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/browser-preview-tool-authorization.test.ts src/main/browser/doc-preview-guest-policy.test.ts src/main/ipc/browser.test.ts",
- "result": "passed",
- "durationSeconds": 0.97,
- "summary": "97/97 passed over the preview tool-authorization seam. The channel census asserts the split is total, so a new browser channel cannot be added without classifying it: all six tool channels resolved the preview guest and refused both a renderer that does not host the preview and a grant no preview rendered, while all twenty-three page/session channels never reached the preview authority at all. Five mutations were killed \u2014 dropping the preview branch (12 failures), turning the two authorities into a fallback chain (13), ignoring the hosting renderer id (6), ignoring grant revocation (1), and removing one channel from the census (1)."
- },
- {
- "date": "2026-08-26",
- "runner": "local",
- "platform": "macos",
- "command": "pnpm exec vitest run --config config/vitest.config.ts src/preload/doc-preview-link-interception.test.ts src/main/browser/doc-preview-guest-policy.test.ts src/main/window/main-window-webview-security.test.ts src/main/ipc/doc-preview-grant-ipc.test.ts",
- "result": "passed",
- "durationSeconds": 0.36,
- "summary": "The four surfaces the trusted-click route is built from passed 63/63: the preload's click policy over a real document, including an untrusted dispatched click, an untrusted middle click, an SVG animated href resolved against the document so a relative, rooted or fragment SVG link behaves exactly like the identical HTML anchor, fragment and percent-encoded fragment targets and a trusted middle click; the guest policy's deny-only sinks, its refusal of a navigation that arrives before a document has bound the guest to a grant, and its report gate for an unfocused guest, an unregistered sender, a missing or revoked grant and non-web URLs; will-attach-webview pinning the preview preload in both directions; and the click channel handing main its own sender without a trusted-renderer check."
- },
-{
- "date": "2026-08-26",
- "runner": "local",
- "platform": "macos",
- "command": "SKIP_BUILD=1 ORCA_E2E_WEB_CLIENT=1 ORCA_E2E_FORCE_HEADFUL=1 pnpm exec playwright test tests/e2e/paired-remote-html-preview-local-render.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1 --reporter=line",
- "result": "passed",
- "durationSeconds": 22.8,
- "summary": "The journey passed 1/1 with the fixture document now attempting its own egress on every load: an unattended window.open and location.href to an off-machine URL, and an ICE gathering probe. Both browser counts held at baseline and the probe reported candidates=0, while the document still rendered and the later real target=_blank press still routed out as an Orca browser tab."
- },
- {
- "date": "2026-08-26",
- "runner": "local",
- "platform": "macos",
- "command": "SKIP_BUILD=1 ORCA_E2E_WEB_CLIENT=1 ORCA_E2E_FORCE_HEADFUL=1 pnpm exec playwright test tests/e2e/paired-remote-html-preview-local-render.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1 --reporter=line",
- "result": "failed",
- "durationSeconds": 23.6,
- "summary": "Candidate with the sole delta that the preview guest no longer receives the disable_non_proxied_udp IP-handling policy failed on candidates=6 versus 0, proving the document can gather ICE — including a STUN reflexive candidate against an off-machine server in the standalone probe — whenever that one main-process call is missing."
- },
- {
- "date": "2026-08-26",
- "runner": "local",
- "platform": "macos",
- "command": "SKIP_BUILD=1 ORCA_E2E_WEB_CLIENT=1 ORCA_E2E_FORCE_HEADFUL=1 pnpm exec playwright test tests/e2e/paired-remote-html-preview-local-render.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1 --reporter=line --repeat-each=3",
- "result": "passed",
- "durationSeconds": 66,
- "summary": "The journey passed 3/3 again under --repeat-each=3 on the candidate that clears grants on every fresh shell document and gates the mint channel on the trusted renderer, so neither change disturbs a live preview: the document still rendered, both browser counts held at baseline, and the target=_blank press still routed out as an Orca browser tab."
- },
-{
- "date": "2026-08-26",
- "runner": "local",
- "platform": "macos",
- "command": "SKIP_BUILD=1 ORCA_E2E_WEB_CLIENT=1 ORCA_E2E_FORCE_HEADFUL=1 pnpm exec playwright test tests/e2e/paired-remote-html-preview-local-render.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1 --reporter=line --repeat-each=3",
- "result": "passed",
- "durationSeconds": 59.7,
- "summary": "The renamed journey passed 3/3 under --repeat-each=3 on the hardened candidate: the host document rendered in the orca-preview guest, both browser counts held at baseline through the split, focus, reactivation and close phases, and the final phase's real target=_blank press routed out as an Orca browser tab with the routing call recorded as opened while the preview tab stayed open."
+ "summary": "Rebased candidate 14b5877a75 passed 5/5: absent, unknown, mixed-version, preflight-known, and post-preflight capability failures all reported failure and retained no new empty split."
},
{
"date": "2026-08-12",
@@ -3532,6 +3070,78 @@
"durationSeconds": 3.48,
"summary": "Seven focused host and renderer contract files passed 183/183 tests, covering owning-runtime routing, focused and unfocused creation, placement, reconciliation, failure cleanup, and teardown."
},
+ {
+ "date": "2026-08-12",
+ "runner": "local",
+ "platform": "macos",
+ "command": "ORCA_E2E_WEB_CLIENT=1 ORCA_E2E_FORCE_HEADFUL=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm exec playwright test tests/e2e/paired-remote-html-browser-focus.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1 --repeat-each=3",
+ "result": "passed",
+ "durationSeconds": 16.1,
+ "summary": "The candidate passed exact host/client cardinality, split stability, rendered DOM, focus authority, close convergence, and surviving editor and terminal."
+ },
+ {
+ "date": "2026-08-12",
+ "runner": "local",
+ "platform": "macos",
+ "command": "SKIP_BUILD=1 ORCA_E2E_WEB_CLIENT=1 ORCA_E2E_FORCE_HEADFUL=1 pnpm exec playwright test tests/e2e/paired-remote-html-browser-focus.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1",
+ "result": "failed",
+ "durationSeconds": 11.6,
+ "summary": "Latest-main a90a18d43da333e1cc327779a3f9912a78308848 failed the byte-identical 5806b23f oracle because the HTML preview collapsed into the source editor group instead of retaining its requested split."
+ },
+ {
+ "date": "2026-08-12",
+ "runner": "local",
+ "platform": "macos",
+ "command": "ORCA_E2E_WEB_CLIENT=1 ORCA_E2E_FORCE_HEADFUL=1 pnpm exec playwright test tests/e2e/paired-remote-html-browser-focus.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1",
+ "result": "passed",
+ "durationSeconds": 6.5,
+ "summary": "Rebased candidate b7de9c0961a4edf389a29079b2b4d5250184b57e passed the byte-identical 5806b23f oracle: one host page, one client workspace, requested split, rendered DOM, stable focus, and browser-only close."
+ },
+ {
+ "date": "2026-08-12",
+ "runner": "local",
+ "platform": "macos",
+ "command": "ORCA_E2E_WEB_CLIENT=1 ORCA_E2E_FORCE_HEADFUL=1 pnpm exec playwright test tests/e2e/paired-remote-html-browser-focus.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1",
+ "result": "failed",
+ "durationSeconds": 10.9,
+ "summary": "Rebased candidate b7de9c0961a4edf389a29079b2b4d5250184b57e with only the side-preview owning-runtime routing seam disabled failed the byte-identical 5806b23f oracle with two client workspaces."
+ },
+ {
+ "date": "2026-08-12",
+ "runner": "local",
+ "platform": "macos",
+ "command": "ORCA_E2E_WEB_CLIENT=1 ORCA_E2E_FORCE_HEADFUL=1 pnpm exec playwright test tests/e2e/paired-remote-html-browser-focus.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1",
+ "result": "passed",
+ "durationSeconds": 7.6,
+ "summary": "Final candidate d5767481b982b736e7cc5b4653b2a32a61280638 on merge-base 36d45af062ec4022265aabac8fc6a9d36eecedf1 passed the byte-identical 5806b23f oracle after final review and rebase: one host page, one client workspace, requested split, rendered DOM, stable focus, and browser-only close."
+ },
+ {
+ "date": "2026-08-12",
+ "runner": "local",
+ "platform": "macos",
+ "command": "ORCA_E2E_WEB_CLIENT=1 ORCA_E2E_FORCE_HEADFUL=1 pnpm exec playwright test tests/e2e/paired-remote-html-browser-focus.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1",
+ "result": "failed",
+ "durationSeconds": 11.8,
+ "summary": "Latest-main 346e59c879 failed the byte-identical 2ec1dcf1 oracle with two client HTML browser workspaces instead of one."
+ },
+ {
+ "date": "2026-08-12",
+ "runner": "local",
+ "platform": "macos",
+ "command": "ORCA_E2E_WEB_CLIENT=1 ORCA_E2E_FORCE_HEADFUL=1 pnpm exec playwright test tests/e2e/paired-remote-html-browser-focus.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1",
+ "result": "passed",
+ "durationSeconds": 8.4,
+ "summary": "Candidate d05217c40d passed the byte-identical 2ec1dcf1 oracle: one host page, one client workspace, stable split, rendered DOM, editor focus after creation, explicit-click browser focus, and browser-only close."
+ },
+ {
+ "date": "2026-08-12",
+ "runner": "local",
+ "platform": "macos",
+ "command": "ORCA_E2E_WEB_CLIENT=1 ORCA_E2E_FORCE_HEADFUL=1 pnpm exec playwright test tests/e2e/paired-remote-html-browser-focus.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1",
+ "result": "failed",
+ "durationSeconds": 12.3,
+ "summary": "Candidate d05217c40d with only the side-preview focusOnCreate:false call disabled failed the byte-identical 2ec1dcf1 oracle because preview creation activated the browser instead of preserving editor focus."
+ },
{
"date": "2026-08-13",
"runner": "local",
@@ -3561,7 +3171,7 @@
},
"redGreenEvidence": {
"status": "complete",
- "evidence": "The unchanged STA-4063 five-assertion oracle is red 5/5 on scan e4c278eb52 and latest main bc2e30000b, partially green 2/5 on pre-fix PR #14100, green 5/5 on candidate 14b5877a75, and red 3/5 when only the new transaction cleanup boundary is disabled on final candidate a9e6fb7716. The earlier STA-4025 transaction oracle remains red on the post-#13909 base and green on this candidate. The byte-identical STA-4231 headed oracle is red on latest-main b19d99bd00: both 15-second creates return runtime_timeout without a page ID while the host retains two distinct pages after retry. The relevant behavior is unchanged through 0a0ae974d4. The candidate is green with one returned canonical ID, one host/client page, and no retry; disabling only the navigation-independent acknowledgement is red with the same two orphaned pages. The byte-identical 2ec1dcf1 Electron oracle remains red on latest main 346e59c879, green on candidate d05217c40d, and red with only side-preview focus disabled; historical 5806b23f evidence separately proves owning-runtime routing. For the STA-5557 preview behavior the rewritten journey is green 1/1 on the candidate and red when the sole delta makes the same preview action also create a browser tab, failing on clientBrowserWorkspaceCount 1 versus baseline 0. That absence assertion is gated on first reading the rendered document out of the guest, so it cannot pass by the preview never happening. Against pre-STA-5557 builds the journey is red by construction rather than by measurement: those builds have no html-preview tab mode, so the run would fail earlier, at materialization, and it has not been executed against an old build. The reader-facing half is red-green at the unit layer. The 6/6 green and 2/6 red pair recorded earlier on 2026-08-26 measured the superseded shape, where a subresource failure was filtered out entirely; the shipping shape is green 10/10 and red 3/10 when the sole delta makes every failure, asset or entry, set the blocking reason. The containment lines the second review found uncovered are red-green too: installing the scheme handler on the default session instead of the preview session, and widening the scheme privileges with allowServiceWorkers and bypassCSP, each fail the protocol suite as the sole delta, and deleting the html-preview case from the closed-tab cleanup fails the hook suite. The external-link phase added on 2026-08-26 is red-green by measurement: green 3/3 on the trusted-click design, in which the guest's own preload reports a trusted press on an anchor and main answers only a focused preview guest holding a live grant. Its earlier shape depended on the webview carrying allowpopups and went red with zero recorded routing calls when only that attribute was dropped; the preload now intercepts the press before Chromium considers a popup, so the attribute is gone and that measurement retired with it. One further run failed in the paired-client harness before reaching any preview step, when the client window never exposed its store within the launch timeout; that is a harness flake, not a preview signal. The two egress fences added on 2026-08-26 are red-green by measurement in the same journey, whose fixture attempts its own escape on every load and again from inside the handler for the reader's press on the heading: with deny-only navigation sinks the document opens no browser tab and the counts hold at baseline, and with the sole delta that those sinks route again behind a two-second recent-input gate the post-press location.href opens a real tab, clientBrowserWorkspaceCountAllWorktrees 1 against a baseline of 0. An earlier measurement of that same gate, taken when it was the shipping design, showed the unattended on-load egress opening two tabs once the gate was removed entirely; it was retired with the gate, because a gate that cannot tell a script's navigation from the click it followed was never the fence the claim needed. With the guest's IP-handling policy the document reports candidates=0, and with the sole delta that the policy call is removed it reports candidates=6. Both mutants were applied to the shipping module, rebuilt, and run against the live paired journey rather than reasoned about."
+ "evidence": "The unchanged STA-4063 five-assertion oracle is red 5/5 on scan e4c278eb52 and latest main bc2e30000b, partially green 2/5 on pre-fix PR #14100, green 5/5 on candidate 14b5877a75, and red 3/5 when only the new transaction cleanup boundary is disabled on final candidate a9e6fb7716. The earlier STA-4025 transaction oracle remains red on the post-#13909 base and green on this candidate. The byte-identical STA-4231 headed oracle is red on latest-main b19d99bd00: both 15-second creates return runtime_timeout without a page ID while the host retains two distinct pages after retry. The relevant behavior is unchanged through 0a0ae974d4. The candidate is green with one returned canonical ID, one host/client page, and no retry; disabling only the navigation-independent acknowledgement is red with the same two orphaned pages. The byte-identical 2ec1dcf1 Electron oracle remains red on latest main 346e59c879, green on candidate d05217c40d, and red with only side-preview focus disabled; historical 5806b23f evidence separately proves owning-runtime routing."
},
"performanceBudget": {
"required": true,
@@ -3574,14 +3184,9 @@
"knownGaps": [
"Live paired-server validation currently covers macOS only.",
"Multiple simultaneous viewers are not exercised by the Electron journey.",
- "Local workspaces intentionally keep the embedded browser tab at a file URL and are not covered by the preview journey.",
- "The SSH preview owner has unit coverage only; no live SSH host renders a preview in this gate.",
- "The preview journey exercises one self-contained document. Relative subresources, reload re-reading the workspace disk, and the truncated and out-of-worktree refusals are covered by unit contracts only.",
- "Grant revocation on tab close is asserted at the unit layer; the journey observes the tab and open file disappearing, not the main-process registry.",
- "The external-link route trusts a real press on an anchor in a focused preview guest, so it proves the reader pressed that anchor, not that the anchor's visible text matches its target. The tab it opens is visible, which is the remaining containment.",
- "WebRTC is fenced by the IP-handling policy rather than by removing the API; the guest can still construct a peer connection, and the journey measures that it gathers nothing."
+ "Direct SSH remote file URLs remain intentionally unsupported and local file previews retain their existing path."
],
- "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."
+ "demotionRule": "Demote if one paired HTML action can create duplicate client or host browser identities, lose the requested split, steal editor focus before an explicit browser click, lose browser focus after that click, or remove unrelated editor or terminal state on close."
},
{
"id": "browser-network-tunnel.bounded-fail-closed-route",
@@ -13067,16 +12672,18 @@
"providers": ["local", "daemon", "ssh", "wsl", "remote-runtime"],
"coveredPlatforms": ["macos"],
"coveredProviders": ["local", "ssh"],
- "coverageNotes": "Deterministic service tests cover release-versus-reuse ordering, transactional retain and takeover cancellation, exact host/pane/process identity, dead external/user-owned/transferred/stopped/abandoned reconciliation, conservative unknown provider and legacy metadata handling, immutable transcript and bounded terminal archives, mutation restart, reset cleanup, replay idempotency, and 50-resource accounting. A macOS Electron journey invokes the freshly compiled worker-release CLI after the worker process disappears, then independently checks released SQLite state and coordinator liveness. Injected inventories cover local and SSH provider routing; live SSH, WSL, Windows, paired-runtime, and provider-close lost-ack journeys remain explicit gaps.",
+ "coverageNotes": "Deterministic service tests cover release-versus-reuse ordering, transactional retain and takeover cancellation, exact host/pane/process identity, dead external/user-owned/transferred/stopped/abandoned reconciliation, host-partition persistence and legacy retirement replay with an absent web-terminal layout map, conservative unknown provider and legacy metadata handling, immutable transcript and bounded terminal archives, mutation restart, reset cleanup, replay idempotency, and 50-resource accounting. A macOS Electron journey invokes the freshly compiled worker-release CLI after the worker process disappears, then independently checks released SQLite state and coordinator liveness. Injected inventories cover local and SSH provider routing; live SSH, WSL, Windows, paired-runtime, and provider-close lost-ack journeys remain explicit gaps.",
"motivatingLinks": [
"https://github.com/stablyai/orca/pull/12355",
"https://github.com/stablyai/orca/issues/13860",
"STA-905",
- "STA-3932"
+ "STA-3932",
+ "STA-5122"
],
- "invariant": "A settled Dispatch may close only its one coordinator-created terminal lease. Explicit reuse, real user input, retain, identity or host change, ambiguity, and another resource for the same exact host/pane/process must fence closure. Once the authoritative owning provider positively excludes the resource's exact immutable process incarnation, even an external, user-owned, or transferred dead resource must converge to released without any process close. Unknown host scope, missing incarnation metadata, or unavailable inventory must remain retained. Output preservation and the requested-to-releasing transition are atomic, archives remain readable without the provider file, retries resume idempotently, and orchestration reset removes archive and authority state.",
- "oracle": "Record release intent for a settled owner, attempt exact reuse before close, and require worker-start to fail with terminal_release_in_progress while the terminal stays open; then release the original owner exactly once. Race retain and real user input against a controlled archive promise and require no committed archive or close. For retained external, user-owned, transferred, stopped, and abandoned resources, run one fresh inventory against the exact local/WSL or SSH provider: an exact live incarnation and every unknown inventory shape stay retained, while positive absence atomically sets ownership_state and release_state to released with processAction none and zero closeTerminal calls. Change host or process identity and inject duplicate resource evidence to require retention. Freeze a structured transcript, delete its source file, and require archived worker-read to return the same bounded redacted messages. Restart a pending mutation, reset orchestration state, and create 50 resources while asserting replay convergence, zero orphan rows, two-query worker listing, and no unrelated close.",
+ "invariant": "A settled Dispatch may close only its one coordinator-created terminal lease. Explicit reuse, real user input, retain, identity or host change, ambiguity, and another resource for the same exact host/pane/process must fence closure. Once the authoritative owning provider positively excludes the resource's exact immutable process incarnation, even an external, user-owned, or transferred dead resource must converge to released without any process close. Unknown host scope, missing incarnation metadata, or unavailable inventory must remain retained. Exact terminal-close persistence must settle when a host partition omits renderer-owned layout state. Output preservation and the requested-to-releasing transition are atomic, archives remain readable without the provider file, retries resume idempotently, and orchestration reset removes archive and authority state.",
+ "oracle": "Record release intent for a settled owner, attempt exact reuse before close, and require worker-start to fail with terminal_release_in_progress while the terminal stays open; then release the original owner exactly once. Race retain and real user input against a controlled archive promise and require no committed archive or close. Rebase a closed web-terminal host partition without terminalLayoutsByTabId and require the persistence write to complete while preserving host-authoritative membership; replay a valid legacy retirement under the same omission and require exact membership removal plus revision advancement. For retained external, user-owned, transferred, stopped, and abandoned resources, run one fresh inventory against the exact local/WSL or SSH provider: an exact live incarnation and every unknown inventory shape stay retained, while positive absence atomically sets ownership_state and release_state to released with processAction none and zero closeTerminal calls. Change host or process identity and inject duplicate resource evidence to require retention. Freeze a structured transcript, delete its source file, and require archived worker-read to return the same bounded redacted messages. Restart a pending mutation, reset orchestration state, and create 50 resources while asserting replay convergence, zero orphan rows, two-query worker listing, and no unrelated close.",
"commands": [
+ "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/orca-runtime-process-incarnation-liveness.test.ts src/main/runtime/mobile-session-terminal-persistence-retirement.test.ts src/main/runtime/rpc/methods/orchestration-worker-release.test.ts src/main/runtime/rpc/methods/orchestration-worker-release-recovery.test.ts src/main/runtime/rpc/orchestration-mutation-ledger.test.ts src/main/runtime/orchestration/worker-transcript-read.test.ts src/renderer/src/lib/worker-terminal-takeover-report.test.ts --reporter=dot",
"pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/orca-runtime-process-incarnation-liveness.test.ts src/main/runtime/rpc/methods/orchestration-worker-release.test.ts src/main/runtime/rpc/methods/orchestration-worker-release-recovery.test.ts src/main/runtime/rpc/orchestration-mutation-ledger.test.ts src/main/runtime/orchestration/worker-transcript-read.test.ts src/renderer/src/lib/worker-terminal-takeover-report.test.ts --reporter=dot",
"pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/rpc/methods/orchestration-worker-release.test.ts src/main/runtime/rpc/methods/orchestration-worker-release-recovery.test.ts src/main/runtime/rpc/orchestration-mutation-ledger.test.ts src/main/runtime/orchestration/worker-transcript-read.test.ts src/renderer/src/lib/worker-terminal-takeover-report.test.ts --reporter=dot",
"pnpm exec vitest run --config config/vitest.config.ts tests/e2e/completed-worker-retirement-resume.unit.test.ts --reporter=verbose",
@@ -13084,6 +12691,7 @@
],
"testFiles": [
"src/main/runtime/orca-runtime-process-incarnation-liveness.test.ts",
+ "src/main/runtime/mobile-session-terminal-persistence-retirement.test.ts",
"src/main/runtime/rpc/methods/orchestration-worker-release.test.ts",
"src/main/runtime/rpc/methods/orchestration-worker-release-recovery.test.ts",
"src/main/runtime/rpc/orchestration-mutation-ledger.test.ts",
@@ -13093,6 +12701,13 @@
"tests/e2e/orchestration-worker-settlement-release-cli.spec.ts"
],
"assertionRefs": [
+ {
+ "file": "src/main/runtime/mobile-session-terminal-persistence-retirement.test.ts",
+ "assertions": [
+ "rebases a host partition that no longer carries the closed web-terminal layout",
+ "replays a legacy retirement when its host partition omits terminal layouts"
+ ]
+ },
{
"file": "src/main/runtime/orca-runtime-process-incarnation-liveness.test.ts",
"assertions": [
@@ -13146,6 +12761,15 @@
}
],
"evidenceRuns": [
+ {
+ "date": "2026-08-27",
+ "runner": "local",
+ "platform": "macos",
+ "command": "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/orca-runtime-process-incarnation-liveness.test.ts src/main/runtime/mobile-session-terminal-persistence-retirement.test.ts src/main/runtime/rpc/methods/orchestration-worker-release.test.ts src/main/runtime/rpc/methods/orchestration-worker-release-recovery.test.ts src/main/runtime/rpc/orchestration-mutation-ledger.test.ts src/main/runtime/orchestration/worker-transcript-read.test.ts src/renderer/src/lib/worker-terminal-takeover-report.test.ts --reporter=dot",
+ "result": "passed",
+ "durationSeconds": 8.78,
+ "summary": "Seven deterministic files passed 78 tests, including red-green host-partition rebase and legacy-retirement regressions with an absent web-terminal layout map plus exact lease, reuse, takeover, recovery, restart, archive, and accounting contracts."
+ },
{
"date": "2026-08-12",
"runner": "local",
@@ -13184,11 +12808,11 @@
},
"redGreenEvidence": {
"status": "complete",
- "evidence": "The byte-identical compiled-CLI Electron oracle left the dead resource external/retained on latest main 5ea7df1a5b, passed on combined candidate d697666ce8 with released/released SQLite state and processAction none, and reproduced external/retained after disabling the claimed production files at merge-base 64aec94cb2. The earlier unchanged three-case dead external/user-owned/transferred service oracle likewise failed 3/3 on main, passed 3/3 on candidate, and failed 3/3 with production restored; every run asserted durable state and zero terminal close calls."
+ "evidence": "The version-skew legacy-retirement test deterministically threw at mobile-session-terminal-persistence-retirement.ts:75 before the null-safe layout read and passed with exact tab removal, tombstone cleanup, and topology-revision advancement after the fix. The byte-identical compiled-CLI Electron oracle left the dead resource external/retained on latest main 5ea7df1a5b, passed on combined candidate d697666ce8 with released/released SQLite state and processAction none, and reproduced external/retained after disabling the claimed production files at merge-base 64aec94cb2. The earlier unchanged three-case dead external/user-owned/transferred service oracle likewise failed 3/3 on main, passed 3/3 on candidate, and failed 3/3 with production restored; every run asserted durable state and zero terminal close calls."
},
"performanceBudget": {
"required": true,
- "evidence": "Normal owned release performs constant-count indexed resource and identity queries plus one bounded archive capture. A retained release performs exactly one bounded inventory against its authoritative local/WSL or specific SSH provider, with no retry, polling, timer, subprocess, renderer subscription, or per-session follow-up fanout. Worker-list uses two set queries rather than one resource lookup per worker."
+ "evidence": "Normal owned release performs constant-count indexed resource and identity queries plus one bounded archive capture. Missing layout maps use constant-time empty-record fallbacks inside the existing explicit persistence pass, with no added scan or allocation proportional to terminal history. A retained release performs exactly one bounded inventory against its authoritative local/WSL or specific SSH provider, with no retry, polling, timer, subprocess, renderer subscription, or per-session follow-up fanout. Worker-list uses two set queries rather than one resource lookup per worker."
},
"promotionCriteria": [
"Collect 100 consecutive focused CI passes or 14 days of soak history.",
@@ -13199,7 +12823,7 @@
"knownGaps": [
"Provider close has no durable cross-process operation receipt, so a crash after host mutation but before SQLite settlement remains release_pending until exact inventory returns.",
"Federated release remains explicitly unsupported and retained.",
- "Mobile/direct remote input takeover and live Linux, Windows, WSL, SSH, headed, and headless paired-runtime release are not exercised; the current fix changes provider inventory classification and SQLite state only, not ConPTY, filesystem, update, sleep, firewall, or window lifecycle semantics."
+ "Mobile/direct remote input takeover and live Linux, Windows, WSL, SSH, headed, and headless paired-runtime release are not exercised; the host-partition fix changes persistence normalization only, not ConPTY, filesystem, update, sleep, firewall, or window lifecycle semantics."
],
"demotionRule": "Keep experimental or demote if release can overlap exact reuse, close a conflicting lease, lose immutable output, retain reset archives, fan out per worker, or any focused ordering test flakes without a product or harness defect."
},
diff --git a/config/scripts/build-windows-cli-launcher.mjs b/config/scripts/build-windows-cli-launcher.mjs
index 27c17b3d15c..9d3c9020706 100644
--- a/config/scripts/build-windows-cli-launcher.mjs
+++ b/config/scripts/build-windows-cli-launcher.mjs
@@ -1,41 +1,24 @@
#!/usr/bin/env node
import { spawnSync } from 'node:child_process'
-import { existsSync, mkdirSync } from 'node:fs'
+import { existsSync, mkdirSync, statSync } from 'node:fs'
import { dirname, join, resolve } from 'node:path'
+import { pathToFileURL } from 'node:url'
-if (process.platform !== 'win32') {
- // Why: electron-builder treats a skipped native build like success and can
- // continue toward a Windows package whose declared orca.exe does not exist.
- throw new Error(
- 'Windows CLI launcher compilation requires a Windows host; refusing to package without it.'
- )
-}
-
-const repoRoot = resolve(import.meta.dirname, '../..')
-const sourcePath = join(repoRoot, 'native', 'windows-cli-launcher', 'OrcaCliLauncher.cs')
-const outputPath = readArg('--output') ?? defaultOutputPath(repoRoot)
-const compilerPath = findFrameworkCompiler(process.env)
-
-if (!compilerPath) {
- throw new Error('Unable to find the .NET Framework C# compiler required for orca.exe.')
-}
-
-mkdirSync(dirname(outputPath), { recursive: true })
-const result = spawnSync(
- compilerPath,
- ['/nologo', '/target:exe', '/optimize+', '/warnaserror+', `/out:${outputPath}`, sourcePath],
- { cwd: repoRoot, stdio: 'inherit' }
-)
-
-if (result.signal) {
- process.kill(process.pid, result.signal)
-}
-if (result.error) {
- throw result.error
-}
-if (result.status !== 0) {
- process.exit(result.status ?? 1)
+export function shouldReuseCompiledWindowsCliLauncher(
+ outputPath,
+ sourcePath,
+ { reuseCached = false } = {}
+) {
+ if (!existsSync(outputPath)) {
+ return false
+ }
+ // Why reuseCached: Actions cache keys already hash the C# source, but restore
+ // does not preserve mtimes, so a hit would look stale and recompile anyway.
+ if (reuseCached) {
+ return true
+ }
+ return statSync(outputPath).mtimeMs >= statSync(sourcePath).mtimeMs
}
function defaultOutputPath(projectRoot) {
@@ -58,3 +41,47 @@ function readArg(name) {
const index = process.argv.indexOf(name)
return index !== -1 ? process.argv[index + 1] : undefined
}
+
+if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
+ if (process.platform !== 'win32') {
+ // Why: electron-builder treats a skipped native build like success and can
+ // continue toward a Windows package whose declared orca.exe does not exist.
+ throw new Error(
+ 'Windows CLI launcher compilation requires a Windows host; refusing to package without it.'
+ )
+ }
+
+ const repoRoot = resolve(import.meta.dirname, '../..')
+ const sourcePath = join(repoRoot, 'native', 'windows-cli-launcher', 'OrcaCliLauncher.cs')
+ const outputPath = readArg('--output') ?? defaultOutputPath(repoRoot)
+ const compilerPath = findFrameworkCompiler(process.env)
+
+ if (!compilerPath) {
+ throw new Error('Unable to find the .NET Framework C# compiler required for orca.exe.')
+ }
+
+ mkdirSync(dirname(outputPath), { recursive: true })
+ if (
+ shouldReuseCompiledWindowsCliLauncher(outputPath, sourcePath, {
+ reuseCached: process.env.ORCA_REUSE_WINDOWS_CLI_LAUNCHER === '1'
+ })
+ ) {
+ console.log(`[native-build] reusing Windows CLI launcher at ${outputPath}`)
+ process.exit(0)
+ }
+ const result = spawnSync(
+ compilerPath,
+ ['/nologo', '/target:exe', '/optimize+', '/warnaserror+', `/out:${outputPath}`, sourcePath],
+ { cwd: repoRoot, stdio: 'inherit' }
+ )
+
+ if (result.signal) {
+ process.kill(process.pid, result.signal)
+ }
+ if (result.error) {
+ throw result.error
+ }
+ if (result.status !== 0) {
+ process.exit(result.status ?? 1)
+ }
+}
diff --git a/config/scripts/build-windows-cli-launcher.test.mjs b/config/scripts/build-windows-cli-launcher.test.mjs
index 4bb219a5687..82332897715 100644
--- a/config/scripts/build-windows-cli-launcher.test.mjs
+++ b/config/scripts/build-windows-cli-launcher.test.mjs
@@ -5,12 +5,15 @@ import {
mkdtempSync,
readFileSync,
rmSync,
+ statSync,
+ utimesSync,
writeFileSync
} from 'node:fs'
import { tmpdir } from 'node:os'
import { dirname, join, resolve } from 'node:path'
import { spawnSync } from 'node:child_process'
import { describe, expect, it } from 'vitest'
+import { shouldReuseCompiledWindowsCliLauncher } from './build-windows-cli-launcher.mjs'
const itCrossHost = process.platform === 'win32' ? it.skip : it
const projectRoot = resolve(import.meta.dirname, '../..')
@@ -37,6 +40,33 @@ function itWindows(name, test) {
}
describe('Windows CLI launcher', () => {
+ it('reuses a compiled launcher that is at least as new as the C# source', () => {
+ const root = mkdtempSync(join(tmpdir(), 'orca-cli-launcher-reuse-'))
+ try {
+ const sourcePath = join(root, 'OrcaCliLauncher.cs')
+ const outputPath = join(root, '.build', 'orca.exe')
+ mkdirSync(join(root, '.build'))
+ writeFileSync(sourcePath, 'source\n')
+ writeFileSync(outputPath, 'binary\n')
+ const later = new Date(statSync(sourcePath).mtimeMs + 1_000)
+ utimesSync(outputPath, later, later)
+
+ expect(shouldReuseCompiledWindowsCliLauncher(outputPath, sourcePath)).toBe(true)
+ writeFileSync(sourcePath, 'changed\n')
+ const sourceLater = new Date(statSync(outputPath).mtimeMs + 1_000)
+ utimesSync(sourcePath, sourceLater, sourceLater)
+ expect(shouldReuseCompiledWindowsCliLauncher(outputPath, sourcePath)).toBe(false)
+ expect(
+ shouldReuseCompiledWindowsCliLauncher(outputPath, sourcePath, { reuseCached: true })
+ ).toBe(true)
+ expect(shouldReuseCompiledWindowsCliLauncher(join(root, 'missing.exe'), sourcePath)).toBe(
+ false
+ )
+ } finally {
+ removeFixtureTree(root)
+ }
+ })
+
itCrossHost('fails closed when the Windows launcher cannot be compiled on this host', () => {
const outputRoot = mkdtempSync(join(tmpdir(), 'orca cross-host launcher '))
try {
diff --git a/config/scripts/computer-e2e-workflow.test.mjs b/config/scripts/computer-e2e-workflow.test.mjs
index 11eae65d996..d0489db64cd 100644
--- a/config/scripts/computer-e2e-workflow.test.mjs
+++ b/config/scripts/computer-e2e-workflow.test.mjs
@@ -76,7 +76,6 @@ describe('computer-use e2e workflow', () => {
)
const regressionRun = nativeSmokeRuns.find((run) => run.includes('pnpm vitest run'))
const expectedRegressionFiles = [
- 'config/scripts/computer-e2e-workflow.test.mjs',
'config/scripts/macos-computer-helper-owner-loss-group-recovery.test.mjs',
'config/scripts/macos-computer-helper-owner-loss-processes.test.mjs',
'config/scripts/computer-use-modifier-safety.test.mjs',
@@ -126,10 +125,14 @@ describe('computer-use e2e workflow', () => {
const job = workflow.jobs['mac-native-owner-smoke']
const runs = job.steps.map((step) => step.run).filter((run) => typeof run === 'string')
const checkout = job.steps.find((step) => step.uses === 'actions/checkout@v6')
+ const install = job.steps.find(
+ (step) => step.uses === './.github/actions/install-node-dependencies'
+ )
expect(job.if).toBe("github.event_name == 'pull_request'")
expect(job['runs-on']).toBe('macos-15')
expect(checkout.with['persist-credentials']).toBe(false)
+ expect(install.with['native-runtime']).toBe('electron')
expect(runs).toContain('pnpm bench:macos-computer-helper-owner-loss --expect reaped --trials 1')
const cleanupRun = runs.find((run) =>
run.includes('config/scripts/macos-computer-helper-owner-loss-processes.test.mjs')
diff --git a/config/scripts/ensure-native-runtime.mjs b/config/scripts/ensure-native-runtime.mjs
index b570ac0e7ae..051ea445413 100644
--- a/config/scripts/ensure-native-runtime.mjs
+++ b/config/scripts/ensure-native-runtime.mjs
@@ -4,7 +4,7 @@ import { spawnSync } from 'node:child_process'
import { createRequire } from 'node:module'
import { existsSync, readFileSync } from 'node:fs'
import { release } from 'node:os'
-import { basename, resolve } from 'node:path'
+import { basename, dirname, resolve } from 'node:path'
const require = createRequire(import.meta.url)
const { assertNodePtyJobOwnership } = require('./node-pty-job-ownership.cjs')
@@ -67,7 +67,12 @@ function ensureNodeRuntime() {
if (!initial.ok) {
printCheckError(initial)
}
- runPnpm(['rebuild', 'node-pty'])
+ const failedModules = initial.failures.map((failure) => failure.moduleName)
+ const rebuildModules = [
+ 'node-pty',
+ ...failedModules.filter((moduleName) => moduleName !== 'node-pty')
+ ]
+ rebuildNodeRuntimeModules(rebuildModules)
verifyNodeRuntimeAfterRebuild()
return
}
@@ -77,7 +82,7 @@ function ensureNodeRuntime() {
`[native-runtime] ${formatRuntimeLabel('node')} cannot load native modules; rebuilding ${failedModules.join(', ')} for Node.`
)
printCheckError(initial)
- runPnpm(['rebuild', ...failedModules])
+ rebuildNodeRuntimeModules(failedModules)
verifyNodeRuntimeAfterRebuild()
}
@@ -312,14 +317,10 @@ function getPatchedNodePtyRebuildReason() {
return null
}
- // Why: a loadable upstream node-pty prebuild is not enough; Orca's Unix
- // patch only lands in the source-built build/Release artifacts.
+ // Why: a loadable upstream node-pty prebuild is not enough; Orca's Unix and
+ // Windows patches only land in the source-built build/Release artifacts.
const nodePtyDir = resolve(projectDir, 'node_modules', 'node-pty')
- const artifactPaths = [resolve(nodePtyDir, 'build', 'Release', 'pty.node')]
- // Why: node-pty only builds spawn-helper on macOS; Linux builds only pty.node.
- if (process.platform === 'darwin') {
- artifactPaths.push(resolve(nodePtyDir, 'build', 'Release', 'spawn-helper'))
- }
+ const artifactPaths = patchedNodePtyArtifactPaths(nodePtyDir)
const missingArtifact = artifactPaths.find((artifactPath) => !existsSync(artifactPath))
if (!missingArtifact) {
@@ -329,11 +330,24 @@ function getPatchedNodePtyRebuildReason() {
return 'Patched node-pty build artifacts are missing; rebuilding native deps.'
}
-function requiresPatchedNodePtySourceBuild() {
+function patchedNodePtyArtifactPaths(nodePtyDir) {
if (process.platform === 'win32') {
- return false
+ const releaseDir = resolve(nodePtyDir, 'build', 'Release')
+ return [
+ resolve(releaseDir, 'conpty.node'),
+ ...NODE_PTY_CONPTY_RUNTIME_FILES.map((filename) => resolve(releaseDir, 'conpty', filename))
+ ]
}
+ const artifactPaths = [resolve(nodePtyDir, 'build', 'Release', 'pty.node')]
+ // Why: node-pty only builds spawn-helper on macOS; Linux builds only pty.node.
+ if (process.platform === 'darwin') {
+ artifactPaths.push(resolve(nodePtyDir, 'build', 'Release', 'spawn-helper'))
+ }
+ return artifactPaths
+}
+
+function requiresPatchedNodePtySourceBuild() {
const nodePtyPatchPath = resolve(projectDir, 'config', 'patches', 'node-pty@1.1.0.patch')
if (!existsSync(nodePtyPatchPath)) {
return false
@@ -351,16 +365,28 @@ function getWindowsBuildNumber() {
return match && match.length === 4 ? Number.parseInt(match[3], 10) : 0
}
-function runPnpm(args) {
+function rebuildNodeRuntimeModules(moduleNames) {
+ for (const moduleName of moduleNames) {
+ const moduleDir = dirname(require.resolve(`${moduleName}/package.json`))
+ console.warn(`[native-runtime] Rebuilding ${moduleName} with node-gyp.`)
+ runPnpm(['exec', 'node-gyp', 'rebuild'], { cwd: moduleDir })
+ if (moduleName === 'node-pty' && process.platform === 'win32') {
+ runNodeScript([resolve(moduleDir, 'scripts', 'post-install.js')])
+ }
+ }
+}
+
+function runPnpm(args, { cwd = projectDir } = {}) {
const command = process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm'
const result = spawnSync(command, args, {
- cwd: projectDir,
+ cwd,
stdio: 'inherit',
- shell: process.platform === 'win32'
+ shell: process.platform === 'win32',
+ env: process.env
})
if (result.error || result.status !== 0) {
- console.error(`[native-runtime] ${command} ${args.join(' ')} failed.`)
+ console.error(`[native-runtime] ${command} ${args.join(' ')} failed in ${cwd}.`)
if (result.error) {
console.error(formatError(result.error))
}
diff --git a/config/scripts/ensure-native-runtime.test.mjs b/config/scripts/ensure-native-runtime.test.mjs
index d1ea104b863..2086aad2a54 100644
--- a/config/scripts/ensure-native-runtime.test.mjs
+++ b/config/scripts/ensure-native-runtime.test.mjs
@@ -42,7 +42,8 @@ describe('ensure-native-runtime', () => {
expect(result.status, result.stderr).toBe(0)
const log = readFileSync(logPath, 'utf8')
- expect(log).toContain('pnpm rebuild node-pty\n')
+ expect(log).toContain('pnpm exec node-gyp rebuild\n')
+ expect(log).toContain(join('node_modules', 'node-pty'))
expect(log.split('\n').filter((line) => line.startsWith('node-pty child '))).toEqual([
expect.stringMatching(/^node-pty child (?:conpty|pty) marker=false$/),
expect.stringMatching(/^node-pty child (?:conpty|pty) marker=true$/)
@@ -52,6 +53,41 @@ describe('ensure-native-runtime', () => {
}
})
+ it.skipIf(process.platform !== 'win32')(
+ 'rebuilds other failed Windows addons with patched node-pty',
+ () => {
+ const projectDir = mkTempProject()
+
+ try {
+ const scriptPath = join(projectDir, 'config', 'scripts', 'ensure-native-runtime.mjs')
+ const logPath = join(projectDir, 'native-runtime.log')
+ const markerPath = join(projectDir, 'rebuilt.marker')
+ const binDir = join(projectDir, 'bin')
+ copyFileSync(sourceScriptPath, scriptPath)
+ writeFakeNativeModules(projectDir, { windowsRegistryRequiresMarker: true })
+ writeNodePtyPatchFile(projectDir)
+ writeFakePnpm(binDir)
+
+ const result = spawnSync(process.execPath, [scriptPath, '--runtime=node'], {
+ cwd: projectDir,
+ encoding: 'utf8',
+ env: envWithPrependedPath(binDir, {
+ ORCA_NATIVE_TEST_LOG: logPath,
+ ORCA_NATIVE_TEST_MARKER: markerPath
+ })
+ })
+
+ expect(result.status, result.stderr).toBe(0)
+ const log = readFileSync(logPath, 'utf8')
+ expect(log.match(/pnpm exec node-gyp rebuild\n/g)).toHaveLength(2)
+ expect(log).toContain(join('node_modules', 'node-pty'))
+ expect(log).toContain(join('node_modules', 'windows-native-registry'))
+ } finally {
+ rmSync(projectDir, { recursive: true, force: true })
+ }
+ }
+ )
+
it.skipIf(process.platform === 'win32')(
'rebuilds patched node-pty artifacts even when the Node load check passes',
() => {
@@ -80,7 +116,7 @@ describe('ensure-native-runtime', () => {
expect(result.stderr).toContain(
'Patched node-pty build artifacts are missing; rebuilding native deps.'
)
- expect(readFileSync(logPath, 'utf8')).toContain('pnpm rebuild node-pty\n')
+ expect(readFileSync(logPath, 'utf8')).toContain('pnpm exec node-gyp rebuild\n')
} finally {
rmSync(projectDir, { recursive: true, force: true })
}
@@ -114,7 +150,7 @@ describe('ensure-native-runtime', () => {
expect(result.status, result.stderr).toBe(0)
expect(result.stderr).toContain("expected build/Release so Orca's node-pty patch is active")
- expect(readFileSync(logPath, 'utf8')).toContain('pnpm rebuild node-pty\n')
+ expect(readFileSync(logPath, 'utf8')).toContain('pnpm exec node-gyp rebuild\n')
} finally {
rmSync(projectDir, { recursive: true, force: true })
}
@@ -148,7 +184,7 @@ describe('ensure-native-runtime', () => {
expect(result.status, result.stderr).toBe(0)
expect(result.stderr).not.toContain('Patched node-pty build artifacts are missing')
- expect(readFileSync(logPath, 'utf8')).not.toContain('pnpm rebuild node-pty')
+ expect(readFileSync(logPath, 'utf8')).not.toContain('pnpm exec node-gyp rebuild')
} finally {
rmSync(projectDir, { recursive: true, force: true })
}
@@ -178,9 +214,15 @@ function envWithPrependedPath(binDir, extraEnv) {
}
}
-function writeFakeNativeModules(projectDir) {
+function writeFakeNativeModules(projectDir, { windowsRegistryRequiresMarker = false } = {}) {
const nodePtyDir = join(projectDir, 'node_modules', 'node-pty')
mkdirSync(join(nodePtyDir, 'lib'), { recursive: true })
+ writeFileSync(
+ join(nodePtyDir, 'package.json'),
+ '{"name":"node-pty","version":"1.1.0","main":"index.js"}\n'
+ )
+ mkdirSync(join(nodePtyDir, 'scripts'), { recursive: true })
+ writeFileSync(join(nodePtyDir, 'scripts', 'post-install.js'), '')
writeFileSync(join(nodePtyDir, 'index.js'), 'module.exports = {}\n')
writeFileSync(
@@ -200,12 +242,18 @@ exports.loadNativeModule = function loadNativeModule(nativeName) {
}
`
)
- writeFakeWindowsRegistry(projectDir)
+ writeFakeWindowsRegistry(projectDir, { requiresMarker: windowsRegistryRequiresMarker })
}
function writeLoadableNativeModules(projectDir, { nativeDir = null } = {}) {
const nodePtyDir = join(projectDir, 'node_modules', 'node-pty')
mkdirSync(join(nodePtyDir, 'lib'), { recursive: true })
+ writeFileSync(
+ join(nodePtyDir, 'package.json'),
+ '{"name":"node-pty","version":"1.1.0","main":"index.js"}\n'
+ )
+ mkdirSync(join(nodePtyDir, 'scripts'), { recursive: true })
+ writeFileSync(join(nodePtyDir, 'scripts', 'post-install.js'), '')
writeFileSync(join(nodePtyDir, 'index.js'), 'module.exports = {}\n')
writeFileSync(
@@ -218,23 +266,40 @@ exports.loadNativeModule = function loadNativeModule(nativeName) {
const dir = ${JSON.stringify(nativeDir)} ??
(rebuilt ? '../build/Release/' : '../prebuilds/' + process.platform + '-' + process.arch + '/')
appendFileSync(process.env.ORCA_NATIVE_TEST_LOG, \`node-pty load \${nativeName} dir=\${dir}\\n\`)
- return { dir, module: {} }
+ return {
+ dir,
+ module: {
+ listJobProcessIds: () => [],
+ terminateJob: () => true,
+ assignCurrentProcessToJob: () => true
+ }
+ }
}
`
)
writeFakeWindowsRegistry(projectDir)
}
-function writeFakeWindowsRegistry(projectDir) {
+function writeFakeWindowsRegistry(projectDir, { requiresMarker = false } = {}) {
if (process.platform !== 'win32') {
return
}
const registryDir = join(projectDir, 'node_modules', 'windows-native-registry')
mkdirSync(registryDir, { recursive: true })
writeFileSync(
- join(registryDir, 'index.js'),
- 'exports.HK = { CU: 0x80000001 }; exports.getRegistryKey = () => ({})\n'
+ join(registryDir, 'package.json'),
+ '{"name":"windows-native-registry","version":"3.2.2","main":"index.js"}\n'
)
+ const markerGate = requiresMarker
+ ? `if (!require('node:fs').existsSync(process.env.ORCA_NATIVE_TEST_MARKER)) { throw new Error('registry ABI mismatch sentinel') }`
+ : ''
+ writeFileSync(
+ join(registryDir, 'index.js'),
+ `exports.HK = { CU: 0x80000001 }; exports.getRegistryKey = () => { ${markerGate}; return {} }\n`
+ )
+ const processTreeDir = join(projectDir, 'node_modules', '@vscode', 'windows-process-tree')
+ mkdirSync(processTreeDir, { recursive: true })
+ writeFileSync(join(processTreeDir, 'index.js'), 'module.exports = {}\n')
}
function writeNodePtyPatchFile(projectDir) {
@@ -245,6 +310,13 @@ function writeNodePtyPatchFile(projectDir) {
function writePatchedNodePtyBuildArtifacts(projectDir) {
const buildDir = join(projectDir, 'node_modules', 'node-pty', 'build', 'Release')
mkdirSync(buildDir, { recursive: true })
+ if (process.platform === 'win32') {
+ writeFileSync(join(buildDir, 'conpty.node'), '')
+ mkdirSync(join(buildDir, 'conpty'), { recursive: true })
+ writeFileSync(join(buildDir, 'conpty', 'conpty.dll'), '')
+ writeFileSync(join(buildDir, 'conpty', 'OpenConsole.exe'), '')
+ return
+ }
writeFileSync(join(buildDir, 'pty.node'), '')
if (process.platform === 'darwin') {
writeFileSync(join(buildDir, 'spawn-helper'), '')
@@ -260,6 +332,11 @@ function writeFakePnpm(binDir) {
const { appendFileSync, writeFileSync } = require('node:fs')
appendFileSync(process.env.ORCA_NATIVE_TEST_LOG, \`pnpm \${process.argv.slice(2).join(' ')}\\n\`)
+appendFileSync(process.env.ORCA_NATIVE_TEST_LOG, \`cwd=\${process.cwd()}\\n\`)
+appendFileSync(
+ process.env.ORCA_NATIVE_TEST_LOG,
+ \`npm_config_build_from_source=\${process.env.npm_config_build_from_source || ''}\\n\`
+)
writeFileSync(process.env.ORCA_NATIVE_TEST_MARKER, 'rebuilt')
`
)
diff --git a/config/scripts/pr-code-change-scope.mjs b/config/scripts/pr-code-change-scope.mjs
index d19eee52454..8ccc864aabb 100644
--- a/config/scripts/pr-code-change-scope.mjs
+++ b/config/scripts/pr-code-change-scope.mjs
@@ -16,6 +16,183 @@ const DOCS_ONLY_FILES = new Set([
const DOCS_ONLY_PREFIXES = ['docs/', '.github/ISSUE_TEMPLATE/']
+export const PR_CHECK_JOBS = [
+ 'static_analysis',
+ 'typecheck',
+ 'git_compatibility',
+ 'xterm_patch_sync',
+ 'shell_contracts',
+ 'test',
+ 'orcad_browser',
+ 'cross-version-wire',
+ 'managed_hook_node18',
+ 'package',
+ 'package_windows'
+]
+
+const ALWAYS_ON_CODE_JOBS = new Set(['static_analysis', 'typecheck', 'test'])
+
+const GLOBAL_FORCE_PREFIXES = [
+ '.github/workflows/pr.yml',
+ '.github/actions/install-node-dependencies/',
+ 'config/scripts/pr-code-change-scope'
+]
+
+const GLOBAL_FORCE_FILES = new Set(['package.json', 'pnpm-lock.yaml'])
+
+const GIT_COMPAT_PREFIXES = [
+ 'src/shared/git-',
+ 'src/shared/review-head-tracking-ref',
+ 'src/main/git/',
+ 'src/relay/git-',
+ 'config/scripts/git-binary-compatibility'
+]
+
+const XTERM_PREFIXES = [
+ 'config/patches/xterm-upstream.json',
+ 'config/patches/@xterm',
+ 'config/patches/xterm-src/',
+ 'config/scripts/regenerate-xterm-patches'
+]
+
+const SHELL_PREFIXES = [
+ 'src/main/daemon/repro-13767-shell-ready-marker-lost-to-exec',
+ 'src/main/daemon/shell-ready',
+ 'src/main/daemon/daemon-bash-shell-ready',
+ 'src/main/daemon/daemon-shell-ready-wrapper',
+ 'src/main/daemon/node-pty-fd-leak',
+ 'src/main/providers/local-pty-shell-ready',
+ 'src/main/providers/__tests__/shell-ready-framework-example',
+ 'src/main/pty/',
+ 'src/main/shell-templates',
+ 'src/main/shell-startup-',
+ 'src/main/shell-wrapper-',
+ 'src/main/terminal-history-fish',
+ 'src/main/zsh-',
+ 'src/renderer/src/components/terminal-pane/fish-color-scheme',
+ 'src/shared/fish-',
+ 'src/shared/pty-reply-echo-shapes',
+ 'src/shared/startup-shell-portability',
+ 'src/shared/posix-command-path-lookup',
+ 'config/patches/node-pty@',
+ 'config/scripts/ensure-native-runtime',
+ 'config/scripts/node-pty-job-ownership'
+]
+
+const ORCAD_BROWSER_PREFIXES = [
+ 'src/main/orcad/external-chromium-',
+ 'src/main/orcad/orcad-browser-provider',
+ 'src/main/orcad/orcad-agent-browser-binary',
+ 'src/main/orcad/electron-serve-browser-process'
+]
+
+const CROSS_VERSION_WIRE_PREFIXES = [
+ 'tests/e2e/cross-version-wire/',
+ 'src/shared/protocol-version',
+ 'src/shared/terminal-stream-protocol',
+ 'src/shared/browser-client-host-protocol',
+ 'src/shared/browser-network-tunnel-protocol',
+ 'src/shared/browser-client-host-placement',
+ 'src/main/runtime/rpc/dispatcher',
+ 'src/main/runtime/rpc/methods/browser-tab-create-schema',
+ 'src/main/runtime/rpc/methods/terminal',
+ 'src/renderer/src/runtime/remote-runtime-terminal-multiplexer'
+]
+
+const MANAGED_HOOK_PREFIXES = [
+ 'config/scripts/smoke-managed-hook-runtime-node18',
+ 'config/scripts/build-relay',
+ 'src/relay/',
+ 'src/shared/agent-hook',
+ 'src/main/agent-hooks/'
+]
+
+const NATIVE_RUNTIME_PREFIXES = [
+ 'config/scripts/ensure-native-runtime',
+ 'config/scripts/rebuild-native-deps',
+ 'config/scripts/node-pty-job-ownership',
+ 'config/scripts/electron-builder-native-rebuild',
+ 'config/patches/node-pty@',
+ 'config/patches/@vscode__windows-process-tree'
+]
+
+const SHARED_PACKAGE_PREFIXES = [
+ 'electron.vite.config.ts',
+ 'config/electron-builder',
+ 'config/packaged-runtime',
+ 'config/build-plugins/',
+ 'config/scripts/build-',
+ 'config/scripts/smoke-packaged',
+ 'config/scripts/install-electron-package-binary',
+ 'config/scripts/verify-packaged',
+ 'config/scripts/verify-linux-glibc',
+ 'config/scripts/run-electron-vite',
+ 'skills/',
+ 'skill-guides/',
+ 'resources/build/',
+ 'resources/onboarding/',
+ 'resources/plugins/',
+ 'resources/skills/',
+ ...NATIVE_RUNTIME_PREFIXES
+]
+
+const LINUX_PACKAGE_PREFIXES = [
+ ...SHARED_PACKAGE_PREFIXES,
+ 'native/computer-use-linux/',
+ 'resources/linux/',
+ 'config/scripts/run-headless-serve'
+]
+
+const WINDOWS_PACKAGE_PREFIXES = [
+ ...SHARED_PACKAGE_PREFIXES,
+ 'native/windows-cli-launcher/',
+ 'native/computer-use-windows/',
+ 'resources/win32/',
+ 'config/scripts/build-windows-cli-launcher',
+ 'config/scripts/windows-pty-native-capability',
+ 'tests/tools/windows-pty-native-capability-smoke/'
+]
+
+const LINUX_PACKAGE_TESTS = [
+ 'src/main/browser/browser-client-page-renderer-lifecycle.electron.test.ts',
+ 'src/main/browser/browser-route-tcp-egress.electron.test.ts',
+ 'src/main/browser/browser-route-webrtc-egress.electron.test.ts',
+ 'src/main/browser/browser-route-h3-egress.electron.test.ts',
+ 'src/main/browser/browser-route-dns-prefetch.electron.test.ts'
+]
+
+const WINDOWS_PACKAGE_TESTS = [
+ ...LINUX_PACKAGE_TESTS,
+ 'config/scripts/rebuild-native-deps.test.mjs',
+ 'src/main/providers/windows-conpty-wide-char-duplication.node-pty.test.ts',
+ 'src/main/providers/pty-repaint-wide-char-buffer.node-pty.test.ts',
+ 'src/shared/child-process/windows-command-line.win32.test.ts',
+ 'src/main/agent-hooks/windows-hook-payload-delivery.test.ts',
+ 'src/main/windows/windows-pty-job.win32.test.ts',
+ 'src/main/windows/windows-host-job.win32.test.ts',
+ 'src/main/wsl/wsl-runner.test.ts',
+ 'src/main/wsl/wsl-guest-environment.test.ts',
+ 'src/main/wsl/wsl-invocation-boundary.test.ts',
+ 'src/main/wsl/wsl-executable-path.win32.test.ts',
+ 'src/main/wsl/wsl-w1-w3-contract.test.ts',
+ 'src/shared/source-scan/source-tree-scan.test.ts',
+ 'src/main/cli/wsl-cli-powershell-boundary.test.ts',
+ 'src/main/cursor/hook-service.test.ts',
+ 'src/main/orca-profiles/profile-index-store.test.ts',
+ 'src/main/runtime/repo-worktree-admin-fingerprint.test.ts',
+ 'src/main/runtime/worktree-scan-admin-fingerprint-gate.test.ts',
+ 'src/shared/secure-file-fsync-flags.test.ts',
+ 'src/main/ipc/pty-codex-account-attribution.test.ts',
+ 'src/main/ipc/pty-spawn-env-codex-resume-provenance.test.ts'
+]
+
+const DESKTOP_IRRELEVANT_PREFIXES = [
+ 'mobile/',
+ '.github/workflows/mobile.yml',
+ '.github/workflows/mobile-ios-release.yml',
+ '.github/workflows/mobile-android-release.yml'
+]
+
export function isDocsOnlyPath(file) {
if (DOCS_ONLY_FILES.has(file)) {
return true
@@ -32,10 +209,83 @@ export function shouldRunPrChecks(changedFiles) {
if (changedFiles.length === 0) {
return true
}
- return changedFiles.some((file) => !isDocsOnlyPath(file))
+ return changedFiles.some((file) => !isDocsOnlyPath(file) && !isDesktopIrrelevantPath(file))
+}
+
+export function classifyPrJobs(changedFiles) {
+ const emptyDiff = changedFiles.length === 0
+ const shouldRun = shouldRunPrChecks(changedFiles)
+ const forceAll = emptyDiff || changedFiles.some(isGlobalForcePath)
+ const jobs = Object.fromEntries(
+ PR_CHECK_JOBS.map((job) => [
+ job,
+ shouldRun && (forceAll || ALWAYS_ON_CODE_JOBS.has(job) || jobDetector(job)(changedFiles))
+ ])
+ )
+ return { should_run: shouldRun, ...jobs }
+}
+
+function jobDetector(job) {
+ switch (job) {
+ case 'git_compatibility':
+ return (files) => files.some((file) => matchesPrefix(file, GIT_COMPAT_PREFIXES))
+ case 'xterm_patch_sync':
+ return (files) => files.some((file) => matchesPrefix(file, XTERM_PREFIXES))
+ case 'shell_contracts':
+ return (files) => files.some((file) => matchesPrefix(file, SHELL_PREFIXES))
+ case 'orcad_browser':
+ return (files) => files.some((file) => matchesPrefix(file, ORCAD_BROWSER_PREFIXES))
+ case 'cross-version-wire':
+ return (files) => files.some((file) => matchesPrefix(file, CROSS_VERSION_WIRE_PREFIXES))
+ case 'managed_hook_node18':
+ return (files) => files.some((file) => matchesPrefix(file, MANAGED_HOOK_PREFIXES))
+ case 'package':
+ return (files) => files.some(isLinuxPackagePath)
+ case 'package_windows':
+ return (files) => files.some(isWindowsPackagePath)
+ default:
+ return () => true
+ }
+}
+
+function isLinuxPackagePath(file) {
+ return LINUX_PACKAGE_TESTS.includes(file) || isProductBundlePath(file, LINUX_PACKAGE_PREFIXES)
+}
+
+function isWindowsPackagePath(file) {
+ return WINDOWS_PACKAGE_TESTS.includes(file) || isProductBundlePath(file, WINDOWS_PACKAGE_PREFIXES)
+}
+
+function isProductBundlePath(file, extraPrefixes) {
+ if (isTestFile(file)) {
+ return false
+ }
+ if (file.startsWith('src/')) {
+ return true
+ }
+ return matchesPrefix(file, extraPrefixes)
+}
+
+function isTestFile(file) {
+ return /\.(?:test|spec)\.(?:js|cjs|mjs|ts|tsx)$/.test(file) || file.includes('/__tests__/')
+}
+
+function isDesktopIrrelevantPath(file) {
+ return matchesPrefix(file, DESKTOP_IRRELEVANT_PREFIXES)
+}
+
+function isGlobalForcePath(file) {
+ return GLOBAL_FORCE_FILES.has(file) || matchesPrefix(file, GLOBAL_FORCE_PREFIXES)
+}
+
+function matchesPrefix(file, prefixes) {
+ return prefixes.some((prefix) => file === prefix || file.startsWith(prefix))
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
const files = readFileSync(0, 'utf8').split('\n').filter(Boolean)
- process.stdout.write(shouldRunPrChecks(files) ? 'true\n' : 'false\n')
+ const classification = classifyPrJobs(files)
+ for (const [name, value] of Object.entries(classification)) {
+ process.stdout.write(`${name}=${value ? 'true' : 'false'}\n`)
+ }
}
diff --git a/config/scripts/pr-code-change-scope.test.mjs b/config/scripts/pr-code-change-scope.test.mjs
index f71d6c42934..0ce697d1511 100644
--- a/config/scripts/pr-code-change-scope.test.mjs
+++ b/config/scripts/pr-code-change-scope.test.mjs
@@ -1,13 +1,18 @@
+import { spawnSync } from 'node:child_process'
import { readFileSync } from 'node:fs'
import { join, resolve } from 'node:path'
import { describe, expect, it } from 'vitest'
import { parse } from 'yaml'
-import { isDocsOnlyPath, shouldRunPrChecks } from './pr-code-change-scope.mjs'
+import {
+ classifyPrJobs,
+ isDocsOnlyPath,
+ PR_CHECK_JOBS,
+ shouldRunPrChecks
+} from './pr-code-change-scope.mjs'
const projectDir = resolve(import.meta.dirname, '../..')
const prWorkflow = parse(readFileSync(join(projectDir, '.github/workflows/pr.yml'), 'utf8'))
-const gatedIf = "needs.code_paths.outputs.should_run == 'true'"
const expensiveJobs = [
'static_analysis',
'typecheck',
@@ -15,12 +20,34 @@ const expensiveJobs = [
'xterm_patch_sync',
'shell_contracts',
'test',
+ 'orcad_browser',
'cross-version-wire',
'managed_hook_node18',
'package',
'package_windows'
]
+const ALWAYS_ON = ['static_analysis', 'typecheck', 'test']
+
+function expectedJobs(overrides, { alwaysOn = true } = {}) {
+ return Object.fromEntries(
+ PR_CHECK_JOBS.map((job) => [
+ job,
+ (alwaysOn && ALWAYS_ON.includes(job)) || Boolean(overrides[job])
+ ])
+ )
+}
+
+function expectClassification(files, overrides) {
+ const result = classifyPrJobs(files)
+ const shouldRun = shouldRunPrChecks(files)
+ expect(result.should_run).toBe(shouldRun)
+ expect(result).toMatchObject({
+ should_run: shouldRun,
+ ...expectedJobs(overrides, { alwaysOn: shouldRun })
+ })
+}
+
describe('docs-only path classification', () => {
it('treats the WeChat README PR files as docs-only', () => {
expect(
@@ -54,9 +81,154 @@ describe('docs-only path classification', () => {
it('runs PR Checks when the diff is empty rather than skipping by accident', () => {
expect(shouldRunPrChecks([])).toBe(true)
})
+
+ it('does not start desktop PR Checks for mobile-only diffs', () => {
+ expect(shouldRunPrChecks(['mobile/src/App.tsx', 'mobile/package.json'])).toBe(false)
+ })
})
-describe('PR Checks docs-only skip wiring', () => {
+describe('per-job path classification', () => {
+ it('runs every expensive job on an empty diff rather than skipping by accident', () => {
+ const result = classifyPrJobs([])
+ expect(result.should_run).toBe(true)
+ for (const job of PR_CHECK_JOBS) {
+ expect(result[job], job).toBe(true)
+ }
+ })
+
+ it('skips every expensive job for docs-only diffs', () => {
+ expectClassification(['README.md', 'docs/readme/README.zh-CN.md'], {})
+ })
+
+ it('runs packaging and always-on jobs for product source, not git/xterm/shell lanes', () => {
+ expectClassification(['src/renderer/src/components/tab-bar/TabBar.tsx'], {
+ package: true,
+ package_windows: true
+ })
+ })
+
+ it('runs Git compatibility only when git capability inputs change', () => {
+ expectClassification(['src/shared/git-capability-cache.ts'], {
+ git_compatibility: true,
+ package: true,
+ package_windows: true
+ })
+ expectClassification(['src/shared/git-binary-compatibility.test.ts'], {
+ git_compatibility: true
+ })
+ })
+
+ it('runs xterm patch sync only when xterm inputs change', () => {
+ expectClassification(['config/patches/xterm-upstream.json'], {
+ xterm_patch_sync: true
+ })
+ expectClassification(['config/patches/@xterm__xterm@6.1.0-beta.287.patch'], {
+ xterm_patch_sync: true
+ })
+ })
+
+ it('runs native package jobs only for the platform that ships the changed native', () => {
+ expectClassification(['native/windows-cli-launcher/OrcaCliLauncher.cs'], {
+ package_windows: true
+ })
+ expectClassification(['native/computer-use-linux/runtime.py'], {
+ package: true
+ })
+ expectClassification(['native/computer-use-macos/Package.swift'], {})
+ })
+
+ it('runs shell contracts when live-shell inputs change', () => {
+ expectClassification(['src/main/daemon/shell-ready.ts'], {
+ shell_contracts: true,
+ package: true,
+ package_windows: true
+ })
+ })
+
+ it('runs shell contracts when wrapper templates or live-shell fixtures change', () => {
+ expectClassification(['src/main/shell-templates.ts'], {
+ shell_contracts: true,
+ package: true,
+ package_windows: true
+ })
+ expectClassification(['src/main/shell-startup-launch-intent-fixtures.ts'], {
+ shell_contracts: true,
+ package: true,
+ package_windows: true
+ })
+ })
+
+ it('runs orcad browser when Chrome launch, session, or tab modules change', () => {
+ for (const file of [
+ 'src/main/orcad/external-chromium-browser-session.ts',
+ 'src/main/orcad/external-chromium-command-arguments.ts',
+ 'src/main/orcad/external-chromium-tab-registry.ts',
+ 'src/main/orcad/external-chromium-tab-projection.ts'
+ ]) {
+ expectClassification([file], {
+ orcad_browser: true,
+ package: true,
+ package_windows: true
+ })
+ }
+ expectClassification(['src/main/orcad/orcad-native-preflight.ts'], {
+ package: true,
+ package_windows: true
+ })
+ })
+
+ it('runs cross-version wire checks for every working-tree wire module', () => {
+ for (const file of [
+ 'src/shared/protocol-version.ts',
+ 'src/shared/terminal-stream-protocol.ts',
+ 'src/main/runtime/rpc/dispatcher.ts',
+ 'src/main/runtime/rpc/methods/browser-tab-create-schema.ts',
+ 'src/main/runtime/rpc/methods/terminal.ts',
+ 'src/renderer/src/runtime/remote-runtime-terminal-multiplexer.ts'
+ ]) {
+ expectClassification([file], {
+ 'cross-version-wire': true,
+ package: true,
+ package_windows: true
+ })
+ }
+ expectClassification(
+ ['tests/e2e/cross-version-wire/cross-version-terminal-wire.unit.test.ts'],
+ { 'cross-version-wire': true }
+ )
+ })
+
+ it('runs workflow-self-change and lockfile diffs as force-all', () => {
+ const result = classifyPrJobs(['.github/workflows/pr.yml'])
+ expect(result.should_run).toBe(true)
+ for (const job of PR_CHECK_JOBS) {
+ expect(result[job], job).toBe(true)
+ }
+ expect(classifyPrJobs(['pnpm-lock.yaml']).git_compatibility).toBe(true)
+ })
+
+ it('keeps unit-test-only diffs out of packaging', () => {
+ expectClassification(['src/main/git/git-status.test.ts'], {
+ git_compatibility: true
+ })
+ })
+
+ it('emits GitHub output pairs from the shipped CLI', () => {
+ const result = spawnSync(process.execPath, ['config/scripts/pr-code-change-scope.mjs'], {
+ cwd: projectDir,
+ encoding: 'utf8',
+ input: 'config/patches/xterm-upstream.json\n'
+ })
+ expect(result.status, result.stderr).toBe(0)
+ expect(result.stdout).toContain('should_run=true\n')
+ expect(result.stdout).toContain('xterm_patch_sync=true\n')
+ expect(result.stdout).toContain('git_compatibility=false\n')
+ expect(result.stdout).toContain('package=false\n')
+ expect(result.stdout).toContain('test=true\n')
+ })
+})
+
+describe('PR Checks skip wiring', () => {
it('classifies the PR range with a tested script and expands renames', () => {
const classify = prWorkflow.jobs.code_paths.steps.find(
(step) => step.name === 'Classify changed paths'
@@ -65,9 +237,12 @@ describe('PR Checks docs-only skip wiring', () => {
expect(classify.run).toContain('--no-renames')
expect(classify.run).toContain('--merge-base "$BASE_SHA" "$HEAD_SHA"')
expect(classify.run).toContain('node config/scripts/pr-code-change-scope.mjs')
- expect(prWorkflow.jobs.code_paths.outputs.should_run).toBe(
- '${{ steps.filter.outputs.should_run }}'
- )
+ expect(classify.run).toContain('tee -a "$GITHUB_OUTPUT"')
+ for (const jobName of ['should_run', ...expensiveJobs]) {
+ expect(prWorkflow.jobs.code_paths.outputs[jobName], jobName).toBe(
+ `\${{ steps.filter.outputs.${jobName} }}`
+ )
+ }
})
it('keeps the cheap root-directory guard on docs-only PRs', () => {
@@ -75,10 +250,12 @@ describe('PR Checks docs-only skip wiring', () => {
expect(prWorkflow.jobs.root_directory_guard.needs).toBeUndefined()
})
- it('skips expensive jobs unless the detector says the PR has code', () => {
+ it('gates each expensive job on its own classifier output', () => {
for (const jobName of expensiveJobs) {
expect(prWorkflow.jobs[jobName].needs, jobName).toEqual(['code_paths'])
- expect(prWorkflow.jobs[jobName].if, jobName).toBe(gatedIf)
+ expect(prWorkflow.jobs[jobName].if, jobName).toBe(
+ `needs.code_paths.outputs.${jobName} == 'true'`
+ )
}
})
@@ -89,19 +266,23 @@ describe('PR Checks docs-only skip wiring', () => {
)
})
- it('lets verify pass when expensive jobs are skipped for docs-only PRs', () => {
+ it('lets verify pass skipped jobs the classifier turned off', () => {
const verifyStep = prWorkflow.jobs.verify.steps.find(
(step) => step.name === 'Require successful checks'
)
expect(prWorkflow.jobs.verify.needs[0]).toBe('code_paths')
expect(verifyStep.env.SHOULD_RUN).toBe('${{ needs.code_paths.outputs.should_run }}')
- expect(verifyStep.run).toContain('"$SHOULD_RUN" != "true"')
- const docsOnlyBranch = verifyStep.run.slice(
- 0,
- verifyStep.run.indexOf('# Require success when the PR has code-relevant changes')
- )
- expect(docsOnlyBranch).toContain('if [ "$result" != "skipped" ]')
- expect(docsOnlyBranch).not.toContain('"$result" != "success"')
expect(verifyStep.run).toContain('"$ROOT_DIRECTORY_GUARD" != "success"')
+ expect(verifyStep.run).toContain('# Require success when the PR has code-relevant changes')
+ expect(verifyStep.run).toContain('expected skipped')
+ expect(verifyStep.run).toContain('expected success')
+ for (const job of prWorkflow.jobs.verify.needs) {
+ if (job === 'code_paths' || job === 'root_directory_guard') {
+ continue
+ }
+ const envVar = `${job.replaceAll('-', '_').toUpperCase()}_SHOULD_RUN`
+ expect(verifyStep.env[envVar]).toBe(`\${{ needs.code_paths.outputs.${job} }}`)
+ expect(verifyStep.run).toContain(`"$${envVar}"`)
+ }
})
})
diff --git a/config/scripts/pr-e2e-gate-contract.test.mjs b/config/scripts/pr-e2e-gate-contract.test.mjs
index 2c1243d04fa..665edd01ec0 100644
--- a/config/scripts/pr-e2e-gate-contract.test.mjs
+++ b/config/scripts/pr-e2e-gate-contract.test.mjs
@@ -100,17 +100,19 @@ describe('PR E2E gate contract', () => {
// job without adding it to the strict loop fails here instead of silently
// leaving that job unenforced. This is what caught GIT_COMPATIBILITY and
// SHELL_CONTRACTS being absent from an earlier hardcoded list.
- // Why lastIndexOf: the docs-only branch has its own loop that allows skipped.
const successMarker = '# Require success when the PR has code-relevant changes'
- const successLoop = verifyStep.run.slice(
- verifyStep.run.indexOf(successMarker),
- verifyStep.run.lastIndexOf('done')
- )
+ const successLoop = verifyStep.run.slice(verifyStep.run.indexOf(successMarker))
expect(successLoop.length).toBeGreaterThan(0)
+ expect(verifyStep.run).toContain('"$CODE_PATHS" != "success"')
+ expect(verifyStep.run).toContain('"$ROOT_DIRECTORY_GUARD" != "success"')
for (const job of prWorkflow.jobs.verify.needs) {
- const envVar = job.toUpperCase()
+ const envVar = job.replaceAll('-', '_').toUpperCase()
expect(verifyStep.env[envVar]).toBe(`\${{ needs.${job}.result }}`)
+ if (job === 'code_paths' || job === 'root_directory_guard') {
+ continue
+ }
expect(successLoop).toContain(`"$${envVar}"`)
+ expect(verifyStep.env[`${envVar}_SHOULD_RUN`]).toBe(`\${{ needs.code_paths.outputs.${job} }}`)
}
})
@@ -157,6 +159,18 @@ describe('PR E2E gate contract', () => {
expect(sshDockerRunner).toContain("'electron-headful'")
})
+ it('reuses the composite install action instead of duplicating pnpm setup', () => {
+ const installFor = (jobName) =>
+ e2eWorkflow.jobs[jobName].steps.find(
+ (step) => step.uses === './.github/actions/install-node-dependencies'
+ )
+
+ expect(installFor('build').with['native-runtime']).toBe('node')
+ for (const jobName of ['e2e', 'changed-e2e', 'ssh-docker-watcher-isolation']) {
+ expect(installFor(jobName).with['native-runtime'], jobName).toBe('electron')
+ }
+ })
+
it('installs zsh in every Linux lane that can run paired startup readiness', () => {
for (const jobName of ['e2e', 'changed-e2e', 'ssh-docker-watcher-isolation']) {
const installStep = e2eWorkflow.jobs[jobName].steps.find((step) =>
diff --git a/config/scripts/pr-workflow-parallelism.test.mjs b/config/scripts/pr-workflow-parallelism.test.mjs
index 42540f6bcb4..a61244d42d3 100644
--- a/config/scripts/pr-workflow-parallelism.test.mjs
+++ b/config/scripts/pr-workflow-parallelism.test.mjs
@@ -50,9 +50,9 @@ describe('PR workflow parallelism', () => {
it('shards the general test suite across Node 24 and Node 26', () => {
expect(workflow.jobs.test.strategy.matrix.node).toEqual(['24', '26'])
expect(workflow.jobs.test.strategy.matrix.shard).toEqual(
- Array.from({ length: 16 }, (_, index) => index + 1)
+ Array.from({ length: 8 }, (_, index) => index + 1)
)
- expect(workflow.jobs.test.strategy.matrix.shard_total).toEqual([16])
+ expect(workflow.jobs.test.strategy.matrix.shard_total).toEqual([8])
const testStep = workflow.jobs.test.steps.find((step) => step.name === 'Test shard')
const installStep = workflow.jobs.test.steps.find(
(step) => step.uses === './.github/actions/install-node-dependencies'
@@ -233,21 +233,20 @@ describe('PR workflow parallelism', () => {
(step) => step.uses === './.github/actions/install-node-dependencies'
)
- for (const jobName of [
- 'static_analysis',
- 'typecheck',
- 'git_compatibility',
- 'xterm_patch_sync'
- ]) {
+ for (const jobName of ['typecheck', 'git_compatibility', 'xterm_patch_sync']) {
expect(installFor(jobName).with, jobName).toBeUndefined()
}
+ expect(installFor('static_analysis').with['native-runtime']).toBe('node')
expect(installFor('shell_contracts').with['native-runtime']).toBe('node')
expect(installFor('test').with['native-runtime']).toBe('node')
expect(installFor('package').with['native-runtime']).toBe('electron')
+ expect(installFor('package_windows').with['native-runtime']).toBe('node')
+ expect(installFor('package_windows').with['persist-native-cache']).toBe('false')
+ expect(dependencyAction.inputs['persist-native-cache'].default).toBe('true')
expect(
dependencyAction.runs.steps.find((step) => step.name === 'Use external node-gyp').if
- ).toBe("inputs.native-runtime != 'none'")
+ ).toBe("runner.os == 'Linux' && inputs.native-runtime != 'none'")
const dependencyInstall = dependencyAction.runs.steps.find(
(step) => step.name === 'Install dependencies'
)
@@ -295,14 +294,39 @@ describe('PR workflow parallelism', () => {
// overwritten and one after the rebuild would never save a hit.
expect(installIndex).toBeLessThan(cacheIndex)
expect(cacheIndex).toBeLessThan(prepareIndex)
- expect(steps[cacheIndex].if).toBe("inputs.native-runtime != 'none'")
+ expect(steps[cacheIndex].if).toBe(
+ "inputs.native-runtime != 'none' && inputs.persist-native-cache != 'false'"
+ )
+ const restoreOnly = steps.find(
+ (step) => step.name === 'Restore compiled native modules without saving'
+ )
+ expect(restoreOnly.if).toBe(
+ "inputs.native-runtime != 'none' && inputs.persist-native-cache == 'false'"
+ )
+ expect(restoreOnly.uses).toBe('actions/cache/restore@v5')
// Native artifacts are ABI-bound: a key missing either dimension serves a build
// that cannot load, and ensure-native-runtime would recompile it anyway.
- expect(steps[cacheIndex].with.key).toContain('${{ inputs.native-runtime }}')
- expect(steps[cacheIndex].with.key).toContain('steps.requested-node.outputs.node-version')
- expect(steps[cacheIndex].with.key).toContain('config/patches/node-pty@1.1.0.patch')
- // No restore-keys: a partial-match key is exactly the ABI-mismatched build above.
- expect(steps[cacheIndex].with['restore-keys']).toBeUndefined()
+ for (const cacheStep of [steps[cacheIndex], restoreOnly]) {
+ expect(cacheStep.with.key).toContain('${{ inputs.native-runtime }}')
+ expect(cacheStep.with.key).toContain('${{ runner.os }}')
+ expect(cacheStep.with.key).toContain('${{ runner.arch }}')
+ expect(cacheStep.with.key).toContain('steps.requested-node.outputs.node-version')
+ expect(cacheStep.with.key).toContain('steps.native-cache-scope.outputs.scope')
+ expect(cacheStep.with.key).toContain('config/patches/node-pty@1.1.0.patch')
+ expect(cacheStep.with.key).toContain(
+ 'config/patches/@vscode__windows-process-tree@0.8.0.patch'
+ )
+ expect(cacheStep.with.path).toContain('node-pty@*/node_modules/node-pty/build')
+ expect(cacheStep.with.path).toContain('windows-native-registry@')
+ expect(cacheStep.with.path).toContain('@vscode+windows-process-tree@')
+ expect(cacheStep.with['restore-keys']).toBeUndefined()
+ }
+ const cacheScope = steps.find((step) => step.name === 'Resolve native cache scope')
+ expect(cacheScope.if).toBe("inputs.native-runtime != 'none'")
+ expect(cacheScope.run).toContain('/etc/os-release')
+ expect(dependencyAction.outputs['native-cache-scope'].value).toBe(
+ '${{ steps.native-cache-scope.outputs.scope }}'
+ )
})
it('reuses TypeScript incremental state across typecheck runs', () => {
@@ -346,6 +370,7 @@ describe('PR workflow parallelism', () => {
'shell_contracts',
'test',
'orcad_browser',
+ 'cross-version-wire',
'managed_hook_node18',
'package',
'package_windows'
@@ -359,5 +384,7 @@ describe('PR workflow parallelism', () => {
// ORCA_BROWSER_EXECUTABLE, so it only guards anything if verify actually reads it.
expect(verifyStep.env.ORCAD_BROWSER).toBe('${{ needs.orcad_browser.result }}')
expect(verifyStep.run).toContain('"$ORCAD_BROWSER"')
+ expect(verifyStep.env.CROSS_VERSION_WIRE).toBe('${{ needs.cross-version-wire.result }}')
+ expect(verifyStep.run).toContain('"$CROSS_VERSION_WIRE"')
})
})
diff --git a/config/scripts/rebuild-native-deps.mjs b/config/scripts/rebuild-native-deps.mjs
index 78e25d3634c..4eca4bac33a 100644
--- a/config/scripts/rebuild-native-deps.mjs
+++ b/config/scripts/rebuild-native-deps.mjs
@@ -401,14 +401,23 @@ function getPatchedNodePtyRebuildReason() {
return null
}
- // Why: Orca patches node-pty's native Unix spawn path; upstream prebuilds can
- // load successfully in Electron while missing the patched fd/error handling.
+ // Why: Orca patches node-pty's native Unix spawn path and Windows job-object
+ // exports; upstream prebuilds can load while missing those patches.
const nodePtyDir = resolve(projectDir, 'node_modules', 'node-pty')
- const artifactPaths = [resolve(nodePtyDir, 'build', 'Release', 'pty.node')]
- // Why: node-pty only builds spawn-helper on macOS; Linux builds only pty.node.
- if (process.platform === 'darwin') {
- artifactPaths.push(resolve(nodePtyDir, 'build', 'Release', 'spawn-helper'))
- }
+ const artifactPaths =
+ rebuildPlatform === 'win32'
+ ? [
+ resolve(nodePtyDir, 'build', 'Release', 'conpty.node'),
+ ...NODE_PTY_CONPTY_RUNTIME_FILES.map((filename) =>
+ resolve(nodePtyDir, 'build', 'Release', 'conpty', filename)
+ )
+ ]
+ : [
+ resolve(nodePtyDir, 'build', 'Release', 'pty.node'),
+ ...(osPlatform() === 'darwin'
+ ? [resolve(nodePtyDir, 'build', 'Release', 'spawn-helper')]
+ : [])
+ ]
const missingArtifact = artifactPaths.find((artifactPath) => !existsSync(artifactPath))
if (!missingArtifact) {
@@ -422,9 +431,6 @@ function requiresPatchedNodePtySourceBuild() {
if (!onlyModules.includes('node-pty')) {
return false
}
- if (rebuildPlatform === 'win32') {
- return false
- }
if (rebuildPlatform !== osPlatform() || rebuildArch !== process.arch) {
return false
}
diff --git a/config/scripts/rebuild-native-deps.test.mjs b/config/scripts/rebuild-native-deps.test.mjs
index b158ee57f21..5ebc39dfcaa 100644
--- a/config/scripts/rebuild-native-deps.test.mjs
+++ b/config/scripts/rebuild-native-deps.test.mjs
@@ -586,6 +586,13 @@ function writeNodePtyPatchFile(projectDir) {
function writePatchedNodePtyBuildArtifacts(projectDir) {
const buildDir = join(projectDir, 'node_modules', 'node-pty', 'build', 'Release')
mkdirSync(buildDir, { recursive: true })
+ if (process.platform === 'win32') {
+ writeFileSync(join(buildDir, 'conpty.node'), '')
+ mkdirSync(join(buildDir, 'conpty'), { recursive: true })
+ writeFileSync(join(buildDir, 'conpty', 'conpty.dll'), '')
+ writeFileSync(join(buildDir, 'conpty', 'OpenConsole.exe'), '')
+ return
+ }
writeFileSync(join(buildDir, 'pty.node'), '')
if (process.platform === 'darwin') {
writeFileSync(join(buildDir, 'spawn-helper'), '')
diff --git a/config/scripts/skills-cli-package-workflow.test.mjs b/config/scripts/skills-cli-package-workflow.test.mjs
index 489437dd61f..07f7beb46ac 100644
--- a/config/scripts/skills-cli-package-workflow.test.mjs
+++ b/config/scripts/skills-cli-package-workflow.test.mjs
@@ -14,6 +14,7 @@ describe('packaged skills CLI PR gates', () => {
expect(job['runs-on']).toBe('windows-2022')
expect(buildStep.run).toBe('pnpm run build:release:parallel')
+ expect(buildStep.env.ORCA_REUSE_WINDOWS_CLI_LAUNCHER).toBe('1')
expect(prepareStep.run).toBe('node config/scripts/ensure-native-runtime.mjs --runtime=electron')
expect(packageStep.run).toContain('electron-builder')
expect(packageStep.run).toContain('--dir')
diff --git a/config/scripts/windows-pty-native-capability-workflow.test.mjs b/config/scripts/windows-pty-native-capability-workflow.test.mjs
index 9a6b8b285cf..8d4b7d568fd 100644
--- a/config/scripts/windows-pty-native-capability-workflow.test.mjs
+++ b/config/scripts/windows-pty-native-capability-workflow.test.mjs
@@ -28,8 +28,14 @@ describe('packaged Windows PTY native capability routing', () => {
it('keeps patched source rebuild, release build, runtime reuse, and aggregate routing intact', () => {
const job = workflow.jobs.package_windows
- const sourceRebuild = job.steps.find(
- (step) => step.name === 'Rebuild node-pty from patched source'
+ const install = job.steps.find(
+ (step) => step.uses === './.github/actions/install-node-dependencies'
+ )
+ const nodeCacheSave = job.steps.find(
+ (step) => step.name === 'Save compiled Node native modules'
+ )
+ const electronCache = job.steps.find(
+ (step) => step.name === 'Restore compiled Electron native modules'
)
const build = job.steps.find((step) => step.name === 'Build package inputs')
const prepare = job.steps.find((step) => step.name === 'Prepare Electron native runtime')
@@ -37,10 +43,17 @@ describe('packaged Windows PTY native capability routing', () => {
const verify = workflow.jobs.verify.steps.find(
(step) => step.name === 'Require successful checks'
)
+ const ensureNativeRuntime = readFileSync('config/scripts/ensure-native-runtime.mjs', 'utf8')
- expect(sourceRebuild.env.npm_config_build_from_source).toBe('true')
- expect(sourceRebuild.run).toBe('pnpm rebuild node-pty')
+ expect(install.with['native-runtime']).toBe('node')
+ expect(install.with['persist-native-cache']).toBe('false')
+ expect(nodeCacheSave.uses).toBe('actions/cache/save@v5')
+ expect(nodeCacheSave.with.key).toContain('-node-node')
+ expect(electronCache.with.key).toContain('-electron-node')
+ expect(ensureNativeRuntime).toContain("runPnpm(['exec', 'node-gyp', 'rebuild']")
+ expect(ensureNativeRuntime).toContain("resolve(moduleDir, 'scripts', 'post-install.js')")
expect(build.run).toBe('pnpm run build:release:parallel')
+ expect(build.env.ORCA_REUSE_WINDOWS_CLI_LAUNCHER).toBe('1')
expect(prepare.run).toBe('node config/scripts/ensure-native-runtime.mjs --runtime=electron')
expect(packageStep.env.ORCA_REUSE_PREPARED_NATIVE_RUNTIME).toBe('1')
expect(workflow.jobs.verify.needs).toContain('package_windows')
diff --git a/docs/readme/README.es.md b/docs/readme/README.es.md
index 86f04ea9fdf..638220150b5 100644
--- a/docs/readme/README.es.md
+++ b/docs/readme/README.es.md
@@ -36,7 +36,7 @@
Supervisa y dirige a tus agentes desde el teléfono — recibe una notificación cuando un agente termine y envía instrucciones de seguimiento desde cualquier lugar.
-[App Store de iOS](https://apps.apple.com/us/app/orca-ide/id6766130217) · [APK para Android](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.44/app-release.apk) · [Docs →](https://www.onorca.dev/docs/mobile)
+[App Store de iOS](https://apps.apple.com/us/app/orca-ide/id6766130217) · [APK para Android](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.46/app-release.apk) · [Docs →](https://www.onorca.dev/docs/mobile)
|
@@ -227,7 +227,7 @@ yay -S stably-orca-bin
Vincúlala con tu app de escritorio para supervisar y dirigir a tus agentes desde el teléfono.
- **iOS:** [Descargar desde App Store](https://apps.apple.com/us/app/orca-ide/id6766130217)
-- **Android:** [Descargar el APK](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.44/app-release.apk)
+- **Android:** [Descargar el APK](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.46/app-release.apk)
---
diff --git a/docs/readme/README.fr.md b/docs/readme/README.fr.md
index b015c26449a..c9bb161fe8e 100644
--- a/docs/readme/README.fr.md
+++ b/docs/readme/README.fr.md
@@ -40,7 +40,7 @@
Surveillez et pilotez vos agents depuis votre téléphone — soyez notifié quand un agent termine, et envoyez des instructions de suivi où que vous soyez.
-[App Store iOS](https://apps.apple.com/us/app/orca-ide/id6766130217) · [TestFlight](https://testflight.apple.com/join/YjeGMQBA) · [APK Android 0.0.44](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.44/app-release.apk) · [Docs →](https://www.onorca.dev/docs/mobile)
+[App Store iOS](https://apps.apple.com/us/app/orca-ide/id6766130217) · [TestFlight](https://testflight.apple.com/join/YjeGMQBA) · [APK Android 0.0.46](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.46/app-release.apk) · [Docs →](https://www.onorca.dev/docs/mobile)
|
@@ -235,7 +235,7 @@ yay -S stably-orca-bin
Associez-la à l'app de bureau pour surveiller et piloter vos agents depuis votre téléphone.
- **iOS :** [Télécharger sur l'App Store](https://apps.apple.com/us/app/orca-ide/id6766130217) ou [rejoindre TestFlight](https://testflight.apple.com/join/YjeGMQBA)
-- **Android :** [Télécharger l'APK 0.0.44](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.44/app-release.apk)
+- **Android :** [Télécharger l'APK 0.0.46](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.46/app-release.apk)
---
diff --git a/docs/readme/README.ja.md b/docs/readme/README.ja.md
index 3b1992448fe..cc589be1cc5 100644
--- a/docs/readme/README.ja.md
+++ b/docs/readme/README.ja.md
@@ -36,7 +36,7 @@
スマートフォンからエージェントを監視・操作 — エージェントの完了を通知で受け取り、どこからでもフォローアップを送信できます。
-[iOS App Store](https://apps.apple.com/us/app/orca-ide/id6766130217) · [Android APK](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.44/app-release.apk) · [ドキュメント →](https://www.onorca.dev/docs/mobile)
+[iOS App Store](https://apps.apple.com/us/app/orca-ide/id6766130217) · [Android APK](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.46/app-release.apk) · [ドキュメント →](https://www.onorca.dev/docs/mobile)
|
@@ -227,7 +227,7 @@ yay -S stably-orca-bin
デスクトップアプリとペアリングして、スマートフォンからエージェントを監視・操作できます。
- **iOS:** [App Store からダウンロード](https://apps.apple.com/us/app/orca-ide/id6766130217)
-- **Android:** [APK をダウンロード](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.44/app-release.apk)
+- **Android:** [APK をダウンロード](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.46/app-release.apk)
---
diff --git a/docs/readme/README.ko.md b/docs/readme/README.ko.md
index 6e53e590463..1de67052fcb 100644
--- a/docs/readme/README.ko.md
+++ b/docs/readme/README.ko.md
@@ -36,7 +36,7 @@
휴대폰에서 에이전트를 모니터링하고 조종하세요 — 에이전트가 완료되면 알림을 받고 어디서든 후속 지시를 보낼 수 있습니다.
-[iOS App Store](https://apps.apple.com/us/app/orca-ide/id6766130217) · [TestFlight](https://testflight.apple.com/join/YjeGMQBA) · [Android APK 0.0.44](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.44/app-release.apk) · [문서 →](https://www.onorca.dev/docs/mobile)
+[iOS App Store](https://apps.apple.com/us/app/orca-ide/id6766130217) · [TestFlight](https://testflight.apple.com/join/YjeGMQBA) · [Android APK 0.0.46](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.46/app-release.apk) · [문서 →](https://www.onorca.dev/docs/mobile)
|
@@ -230,7 +230,7 @@ yay -S stably-orca-bin
데스크톱 앱과 페어링해 휴대폰에서 에이전트를 모니터링하고 조종하세요.
- **iOS:** [App Store에서 다운로드](https://apps.apple.com/us/app/orca-ide/id6766130217) 또는 [TestFlight 참여](https://testflight.apple.com/join/YjeGMQBA)
-- **Android:** [APK 0.0.44 다운로드](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.44/app-release.apk) · [설치 가이드](https://www.onorca.dev/docs/android-apk)
+- **Android:** [APK 0.0.46 다운로드](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.46/app-release.apk) · [설치 가이드](https://www.onorca.dev/docs/android-apk)
---
diff --git a/docs/readme/README.pt.md b/docs/readme/README.pt.md
index 9f86dc2cfa3..970fcf6c8a0 100644
--- a/docs/readme/README.pt.md
+++ b/docs/readme/README.pt.md
@@ -36,7 +36,7 @@
Monitore e conduza seus agentes pelo celular — receba uma notificação quando um agente terminar e envie instruções de acompanhamento de qualquer lugar.
-[App Store para iOS](https://apps.apple.com/us/app/orca-ide/id6766130217) · [TestFlight](https://testflight.apple.com/join/YjeGMQBA) · [APK Android 0.0.44](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.44/app-release.apk) · [Docs →](https://www.onorca.dev/docs/mobile)
+[App Store para iOS](https://apps.apple.com/us/app/orca-ide/id6766130217) · [TestFlight](https://testflight.apple.com/join/YjeGMQBA) · [APK Android 0.0.46](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.46/app-release.apk) · [Docs →](https://www.onorca.dev/docs/mobile)
|
@@ -230,7 +230,7 @@ yay -S stably-orca-bin
Conecte ao app desktop para monitorar e conduzir seus agentes pelo celular.
- **iOS:** [Baixar na App Store](https://apps.apple.com/us/app/orca-ide/id6766130217) ou [entrar no TestFlight](https://testflight.apple.com/join/YjeGMQBA)
-- **Android:** [Baixar APK 0.0.44](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.44/app-release.apk)
+- **Android:** [Baixar APK 0.0.46](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.46/app-release.apk)
---
diff --git a/docs/readme/README.zh-CN.md b/docs/readme/README.zh-CN.md
index 0c619ea260d..2e11413aa2a 100644
--- a/docs/readme/README.zh-CN.md
+++ b/docs/readme/README.zh-CN.md
@@ -36,7 +36,7 @@
用手机监控并指挥你的智能体 — 智能体完成时收到通知,随时随地发送后续指令。
-[iOS App Store](https://apps.apple.com/us/app/orca-ide/id6766130217) · [Android APK](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.44/app-release.apk) · [文档 →](https://www.onorca.dev/docs/mobile)
+[iOS App Store](https://apps.apple.com/us/app/orca-ide/id6766130217) · [Android APK](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.46/app-release.apk) · [文档 →](https://www.onorca.dev/docs/mobile)
|
@@ -227,7 +227,7 @@ yay -S stably-orca-bin
与桌面应用配对,用手机监控并指挥你的智能体。
- **iOS:** [从 App Store 下载](https://apps.apple.com/us/app/orca-ide/id6766130217)
-- **Android:** [下载 APK](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.44/app-release.apk)
+- **Android:** [下载 APK](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.46/app-release.apk)
---
diff --git a/docs/reference/windows-process-enumeration.md b/docs/reference/windows-process-enumeration.md
index e4c91329c46..11e39b35b7e 100644
--- a/docs/reference/windows-process-enumeration.md
+++ b/docs/reference/windows-process-enumeration.md
@@ -326,8 +326,6 @@ the Windows CI job rebuilds from source before running the win32 suites.
it is true before asserting anything else, so an unpatched binary fails loudly
instead of passing every case vacuously. That guard is what caught this.
-`requiresPatchedNodePtySourceBuild()` in `ensure-native-runtime.mjs` still
-exempts win32, on the premise that the patch is Unix-only. That premise is now
-false, but lifting the exemption also needs `pnpm rebuild` to force a source
-build — otherwise the assertion fires and the remedy does not fix it. Left as a
-follow-up rather than changed blind.
+`requiresPatchedNodePtySourceBuild()` in `ensure-native-runtime.mjs` now covers
+win32 as well, and `pnpm rebuild node-pty` sets `npm_config_build_from_source`
+so the patched source build actually replaces the upstream prebuild.
diff --git a/electron.vite.config.ts b/electron.vite.config.ts
index 4faa844ee4c..f81dbb93041 100644
--- a/electron.vite.config.ts
+++ b/electron.vite.config.ts
@@ -209,7 +209,6 @@ export const electronViteConfig: UserConfig = {
index: resolve('src/main/index.ts'),
// Why: sandboxed webview preloads cannot load Rollup helper chunks.
'browser-window-close-preload': resolve('src/preload/browser-window-close.ts'),
- 'doc-preview-link-preload': resolve('src/preload/doc-preview-link.ts'),
'daemon-entry': resolve('src/main/daemon/daemon-entry.ts'),
'plugin-host-entry': resolve('src/main/plugins/plugin-host-entry.ts'),
'computer-sidecar': resolve('src/main/computer/sidecar-entry.ts'),
diff --git a/mobile/.oxlintrc.json b/mobile/.oxlintrc.json
index 877a6d7e123..f3338fc3616 100644
--- a/mobile/.oxlintrc.json
+++ b/mobile/.oxlintrc.json
@@ -75,54 +75,18 @@
"max-lines": ["error", { "max": 1074, "skipBlankLines": true, "skipComments": true }]
}
},
- {
- "files": ["src/components/mobile-rich-markdown-editor-html.ts"],
- "rules": {
- "max-lines": ["error", { "max": 648, "skipBlankLines": true, "skipComments": true }]
- }
- },
- {
- "files": ["src/components/CustomKeyModal.tsx"],
- "rules": {
- "max-lines": ["error", { "max": 645, "skipBlankLines": true, "skipComments": true }]
- }
- },
- {
- "files": ["app/pair-scan.tsx"],
- "rules": {
- "max-lines": ["error", { "max": 531, "skipBlankLines": true, "skipComments": true }]
- }
- },
{
"files": ["scripts/mock-server.ts"],
"rules": {
"max-lines": ["error", { "max": 407, "skipBlankLines": true, "skipComments": true }]
}
},
- {
- "files": ["src/terminal/terminal-accessory-keys.ts"],
- "rules": {
- "max-lines": ["error", { "max": 388, "skipBlankLines": true, "skipComments": true }]
- }
- },
{
"files": ["app/troubleshoot.tsx"],
"rules": {
"max-lines": ["error", { "max": 436, "skipBlankLines": true, "skipComments": true }]
}
},
- {
- "files": ["scripts/repro-worktree-startup-stream.ts"],
- "rules": {
- "max-lines": ["error", { "max": 326, "skipBlankLines": true, "skipComments": true }]
- }
- },
- {
- "files": ["scripts/repro-terminal-colors.ts"],
- "rules": {
- "max-lines": ["error", { "max": 319, "skipBlankLines": true, "skipComments": true }]
- }
- },
{
"files": ["app/h/*/files/*.tsx"],
"rules": {
diff --git a/mobile/app/pair-scan.tsx b/mobile/app/pair-scan.tsx
index c78d7a907df..61e99f19f2a 100644
--- a/mobile/app/pair-scan.tsx
+++ b/mobile/app/pair-scan.tsx
@@ -2,7 +2,6 @@ import { useState, useRef, useCallback } from 'react'
import {
View,
Text,
- StyleSheet,
Pressable,
ActivityIndicator,
Linking,
@@ -19,13 +18,14 @@ import {
} from '../src/transport/pre-profile-pairing-coordinator'
import type { ConnectionLogEntry, PairingOffer } from '../src/transport/types'
import { useRefreshHostClient } from '../src/transport/client-context'
-import { colors, spacing, radii, typography } from '../src/theme/mobile-theme'
+import { colors, spacing } from '../src/theme/mobile-theme'
import { TextInputModal } from '../src/components/TextInputModal'
import { ConnectionLog } from '../src/components/ConnectionLog'
import {
loadMobileOnboardingSteps,
mobileOnboardingDestination
} from '../src/onboarding/mobile-onboarding-plan'
+import { pairScanStyles as styles } from '../src/pair-scan-styles'
// Why: see pair-confirm.tsx — cap initial-pair "Connecting…" so a broken
// route surfaces as a real error with the log visible instead of a
@@ -356,186 +356,3 @@ export default function PairScanScreen() {
)
}
-
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: colors.bgBase,
- padding: spacing.lg
- },
- backButton: {
- width: 36,
- height: 36,
- borderRadius: 18,
- alignItems: 'center',
- justifyContent: 'center',
- marginBottom: spacing.sm
- },
- steps: {
- gap: spacing.sm,
- marginBottom: spacing.lg,
- marginLeft: 7
- },
- step: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: spacing.sm
- },
- stepBadge: {
- width: 22,
- height: 22,
- borderRadius: 11,
- backgroundColor: colors.bgRaised,
- alignItems: 'center',
- justifyContent: 'center'
- },
- stepNumber: {
- fontSize: 12,
- fontWeight: '700',
- color: colors.textSecondary
- },
- stepText: {
- fontSize: typography.bodySize,
- color: colors.textSecondary
- },
- cameraWrap: {
- flex: 1,
- borderRadius: radii.camera,
- overflow: 'hidden'
- },
- // Why: holds the layout slot while the camera is unmounted during
- // paste, so the bottom action button doesn't snap up to fill the
- // empty space.
- cameraPlaceholder: {
- flex: 1,
- backgroundColor: colors.bgPanel,
- borderRadius: radii.camera
- },
- camera: {
- ...StyleSheet.absoluteFillObject
- },
- reticle: {
- ...StyleSheet.absoluteFillObject,
- alignItems: 'center',
- justifyContent: 'center'
- },
- reticleFrame: {
- position: 'relative'
- },
- corner: {
- position: 'absolute',
- width: 28,
- height: 28,
- borderColor: 'rgba(255,255,255,0.7)'
- },
- cornerTL: {
- top: 0,
- left: 0,
- borderTopWidth: 2.5,
- borderLeftWidth: 2.5,
- borderTopLeftRadius: 6
- },
- cornerTR: {
- top: 0,
- right: 0,
- borderTopWidth: 2.5,
- borderRightWidth: 2.5,
- borderTopRightRadius: 6
- },
- cornerBL: {
- bottom: 0,
- left: 0,
- borderBottomWidth: 2.5,
- borderLeftWidth: 2.5,
- borderBottomLeftRadius: 6
- },
- cornerBR: {
- bottom: 0,
- right: 0,
- borderBottomWidth: 2.5,
- borderRightWidth: 2.5,
- borderBottomRightRadius: 6
- },
- centered: {
- flex: 1,
- alignItems: 'center',
- justifyContent: 'center'
- },
- title: {
- fontSize: typography.titleSize,
- fontWeight: '600',
- color: colors.textPrimary,
- marginBottom: spacing.sm
- },
- subtitle: {
- maxWidth: 310,
- fontSize: typography.bodySize,
- color: colors.textSecondary,
- textAlign: 'center',
- marginBottom: spacing.xl,
- lineHeight: 20
- },
- connectingText: {
- color: colors.textSecondary,
- fontSize: typography.bodySize,
- marginTop: spacing.lg
- },
- logSlot: {
- width: '100%',
- marginTop: spacing.lg,
- paddingHorizontal: spacing.sm
- },
- errorText: {
- color: colors.statusRed,
- fontSize: typography.bodySize,
- textAlign: 'center',
- marginBottom: spacing.xl,
- lineHeight: 20
- },
- primaryButton: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'center',
- gap: spacing.xs,
- backgroundColor: colors.textPrimary,
- paddingHorizontal: spacing.xl,
- paddingVertical: spacing.sm + 2,
- borderRadius: radii.button
- },
- primaryButtonText: {
- color: colors.bgBase,
- fontSize: typography.bodySize,
- fontWeight: '600'
- },
- pasteButton: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'center',
- gap: spacing.xs,
- marginTop: spacing.md,
- paddingVertical: spacing.sm,
- borderRadius: radii.button
- },
- pasteButtonPressed: {
- opacity: 0.6
- },
- pasteButtonText: {
- color: colors.textSecondary,
- fontSize: typography.bodySize,
- fontWeight: '500'
- },
- errorActions: {
- alignItems: 'center',
- gap: spacing.sm
- },
- secondaryButton: {
- paddingHorizontal: spacing.xl,
- paddingVertical: spacing.sm,
- borderRadius: radii.button
- },
- secondaryButtonText: {
- color: colors.textSecondary,
- fontSize: typography.bodySize,
- fontWeight: '500'
- }
-})
diff --git a/mobile/scripts/repro-terminal-color-output.ts b/mobile/scripts/repro-terminal-color-output.ts
new file mode 100644
index 00000000000..50e4e153b54
--- /dev/null
+++ b/mobile/scripts/repro-terminal-color-output.ts
@@ -0,0 +1,60 @@
+import { mkdirSync, writeFileSync } from 'node:fs'
+import { join } from 'node:path'
+
+const ESC = String.fromCharCode(27)
+
+export type TerminalColorSnapshot = {
+ label: string
+ handle: string
+ cols: number | null
+ rows: number | null
+ serialized: string
+ lines: string
+}
+
+function countMatches(value: string, pattern: RegExp): number {
+ return value.match(pattern)?.length ?? 0
+}
+
+export function summarizeTerminalColorSnapshot(
+ snapshot: TerminalColorSnapshot
+): Record {
+ const data = snapshot.serialized
+ return {
+ label: snapshot.label,
+ handle: snapshot.handle,
+ cols: snapshot.cols,
+ rows: snapshot.rows,
+ serializedBytes: Buffer.byteLength(data),
+ sgrTotal: countMatches(data, new RegExp(`${ESC}\\[[0-9;:]*m`, 'g')),
+ sgrColor: countMatches(
+ data,
+ new RegExp(
+ `${ESC}\\[(?:[0-9;:]*[;:])?(?:3[0-7]|4[0-7]|9[0-7]|10[0-7]|38[;:]|48[;:])[0-9;:]*m`,
+ 'g'
+ )
+ ),
+ sgrReset: countMatches(data, new RegExp(`${ESC}\\[(?:0|39|49|0;39;49)m`, 'g')),
+ altScreen: data.includes(`${ESC}[?1049h`) ? 'yes' : 'no',
+ containsTruecolor: data.includes('38;2') || data.includes('38:2') ? 'yes' : 'no',
+ containsPaletteColor: data.includes('38;5') || data.includes('38:5') ? 'yes' : 'no'
+ }
+}
+
+export function saveTerminalColorSnapshots(snapshots: TerminalColorSnapshot[]): string {
+ const dir = join(process.cwd(), 'terminal-color-repro')
+ mkdirSync(dir, { recursive: true })
+ for (const snapshot of snapshots) {
+ const base = snapshot.label.replace(/[^a-z0-9_-]/gi, '-')
+ writeFileSync(join(dir, `${base}.ansi`), snapshot.serialized || snapshot.lines)
+ writeFileSync(
+ join(dir, `${base}.escaped.txt`),
+ JSON.stringify(snapshot.serialized || snapshot.lines)
+ )
+ }
+ writeFileSync(
+ join(dir, 'summary.json'),
+ JSON.stringify(snapshots.map(summarizeTerminalColorSnapshot), null, 2)
+ )
+ return dir
+}
diff --git a/mobile/scripts/repro-terminal-colors.ts b/mobile/scripts/repro-terminal-colors.ts
index 3c21df64176..e6c302652a0 100644
--- a/mobile/scripts/repro-terminal-colors.ts
+++ b/mobile/scripts/repro-terminal-colors.ts
@@ -10,10 +10,13 @@
* ORCA_MOBILE_WS_URL=ws://127.0.0.1:6768 \
* pnpm exec tsx scripts/repro-terminal-colors.ts [handleA] [handleB]
*/
-import { mkdirSync, writeFileSync } from 'node:fs'
-import { join } from 'node:path'
import nacl from 'tweetnacl'
import WebSocket from 'ws'
+import {
+ saveTerminalColorSnapshots,
+ summarizeTerminalColorSnapshot,
+ type TerminalColorSnapshot
+} from './repro-terminal-color-output'
const WS_URL = process.env.ORCA_MOBILE_WS_URL ?? 'ws://127.0.0.1:6768'
const token = process.argv[2]
@@ -21,7 +24,6 @@ const serverPublicKeyB64 = process.argv[3]
const worktreeSelector = process.argv[4]
const explicitHandleA = process.argv[5]
const explicitHandleB = process.argv[6]
-const ESC = String.fromCharCode(27)
type RpcResponse = {
id: string
@@ -37,15 +39,6 @@ type PendingRequest = {
reject: (error: Error) => void
}
-type Snapshot = {
- label: string
- handle: string
- cols: number | null
- rows: number | null
- serialized: string
- lines: string
-}
-
if (!token || !serverPublicKeyB64 || !worktreeSelector) {
console.error(
'Usage: pnpm exec tsx scripts/repro-terminal-colors.ts [handleA] [handleB]'
@@ -158,9 +151,13 @@ async function ensureSecondHandle(ws: WebSocket, handleA: string): Promise {
+async function captureSnapshot(
+ ws: WebSocket,
+ label: string,
+ handle: string
+): Promise {
const id = nextId()
- const snapshot = await new Promise((resolve, reject) => {
+ const snapshot = await new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
pending.delete(id)
streamListeners.delete(id)
@@ -207,48 +204,6 @@ async function captureSnapshot(ws: WebSocket, label: string, handle: string): Pr
return snapshot
}
-function countMatches(value: string, pattern: RegExp): number {
- return value.match(pattern)?.length ?? 0
-}
-
-function summarize(snapshot: Snapshot): Record {
- const data = snapshot.serialized
- return {
- label: snapshot.label,
- handle: snapshot.handle,
- cols: snapshot.cols,
- rows: snapshot.rows,
- serializedBytes: Buffer.byteLength(data),
- sgrTotal: countMatches(data, new RegExp(`${ESC}\\[[0-9;:]*m`, 'g')),
- sgrColor: countMatches(
- data,
- new RegExp(
- `${ESC}\\[(?:[0-9;:]*[;:])?(?:3[0-7]|4[0-7]|9[0-7]|10[0-7]|38[;:]|48[;:])[0-9;:]*m`,
- 'g'
- )
- ),
- sgrReset: countMatches(data, new RegExp(`${ESC}\\[(?:0|39|49|0;39;49)m`, 'g')),
- altScreen: data.includes(`${ESC}[?1049h`) ? 'yes' : 'no',
- containsTruecolor: data.includes('38;2') || data.includes('38:2') ? 'yes' : 'no',
- containsPaletteColor: data.includes('38;5') || data.includes('38:5') ? 'yes' : 'no'
- }
-}
-
-function saveSnapshots(snapshots: Snapshot[]): string {
- const dir = join(process.cwd(), 'terminal-color-repro')
- mkdirSync(dir, { recursive: true })
- for (const snapshot of snapshots) {
- const base = snapshot.label.replace(/[^a-z0-9_-]/gi, '-')
- writeFileSync(join(dir, `${base}.ansi`), snapshot.serialized || snapshot.lines)
- writeFileSync(
- join(dir, `${base}.escaped.txt`),
- JSON.stringify(snapshot.serialized || snapshot.lines)
- )
- }
- writeFileSync(join(dir, 'summary.json'), JSON.stringify(snapshots.map(summarize), null, 2))
- return dir
-}
-
async function run(ws: WebSocket): Promise {
ws.send(
JSON.stringify({
@@ -303,11 +258,14 @@ async function run(ws: WebSocket): Promise {
const firstB = await captureSnapshot(ws, 'b-during-switch', handleB)
const secondA = await captureSnapshot(ws, 'a-after-switch', handleA)
const snapshots = [firstA, firstB, secondA]
- const dir = saveSnapshots(snapshots)
+ const dir = saveTerminalColorSnapshots(snapshots)
- console.table(snapshots.map(summarize))
+ console.table(snapshots.map(summarizeTerminalColorSnapshot))
console.log(`saved: ${dir}`)
- if (summarize(firstA).sgrColor !== summarize(secondA).sgrColor) {
+ if (
+ summarizeTerminalColorSnapshot(firstA).sgrColor !==
+ summarizeTerminalColorSnapshot(secondA).sgrColor
+ ) {
console.log(
'color SGR count changed between A snapshots: serialization/state changed on desktop'
)
diff --git a/mobile/scripts/repro-worktree-startup-output.ts b/mobile/scripts/repro-worktree-startup-output.ts
new file mode 100644
index 00000000000..83e127a3fa6
--- /dev/null
+++ b/mobile/scripts/repro-worktree-startup-output.ts
@@ -0,0 +1,75 @@
+import { mkdirSync, writeFileSync } from 'node:fs'
+import { join } from 'node:path'
+
+const ESC = String.fromCharCode(27)
+
+export type StartupTerminalCapture = {
+ handle: string
+ title: string | null
+ scrollback: Record | null
+ chunks: string[]
+}
+
+function stripAnsi(value: string): string {
+ return (
+ value
+ // eslint-disable-next-line no-control-regex -- intentional terminal escape stripping for repro summaries
+ .replace(/\u001b\][^\u0007]*(?:\u0007|\u001b\\)/g, '')
+ // eslint-disable-next-line no-control-regex -- intentional terminal escape stripping for repro summaries
+ .replace(/\u001b\[[0-?]*[ -/]*[@-~]/g, '')
+ .replace(/\r/g, '\n')
+ )
+}
+
+function normalizePreview(value: string): string {
+ return stripAnsi(value)
+ .split('\n')
+ .map((line) => line.trim())
+ .filter(Boolean)
+ .slice(0, 8)
+ .join('\n')
+}
+
+export function summarizeStartupCapture(capture: StartupTerminalCapture): Record {
+ const serialized =
+ typeof capture.scrollback?.serialized === 'string' ? capture.scrollback.serialized : ''
+ const live = capture.chunks.join('')
+ const serializedPreview = normalizePreview(serialized)
+ const livePreview = normalizePreview(live)
+ return {
+ handle: capture.handle,
+ title: capture.title,
+ cols: capture.scrollback?.cols ?? null,
+ rows: capture.scrollback?.rows ?? null,
+ serializedBytes: Buffer.byteLength(serialized),
+ liveBytes: Buffer.byteLength(live),
+ liveChunks: capture.chunks.length,
+ serializedSgr: (serialized.match(new RegExp(`${ESC}\\[[0-9;:]*m`, 'g')) ?? []).length,
+ liveSgr: (live.match(new RegExp(`${ESC}\\[[0-9;:]*m`, 'g')) ?? []).length,
+ livePreviewContainedInSerialized:
+ livePreview.length > 0 && stripAnsi(serialized).includes(livePreview.split('\n')[0] ?? ''),
+ serializedPreview,
+ livePreview
+ }
+}
+
+export function saveStartupCaptures(
+ captures: StartupTerminalCapture[],
+ worktreeName: string
+): string {
+ const dir = join(process.cwd(), 'terminal-startup-repro', worktreeName)
+ mkdirSync(dir, { recursive: true })
+ for (const capture of captures) {
+ const base = capture.handle.replace(/[^a-z0-9_-]/gi, '-')
+ const serialized =
+ typeof capture.scrollback?.serialized === 'string' ? capture.scrollback.serialized : ''
+ writeFileSync(join(dir, `${base}.serialized.ansi`), serialized)
+ writeFileSync(join(dir, `${base}.live.ansi`), capture.chunks.join(''))
+ writeFileSync(join(dir, `${base}.json`), JSON.stringify(capture, null, 2))
+ }
+ writeFileSync(
+ join(dir, 'summary.json'),
+ JSON.stringify(captures.map(summarizeStartupCapture), null, 2)
+ )
+ return dir
+}
diff --git a/mobile/scripts/repro-worktree-startup-stream.ts b/mobile/scripts/repro-worktree-startup-stream.ts
index e4db6182f00..f3bbdc0d5d0 100644
--- a/mobile/scripts/repro-worktree-startup-stream.ts
+++ b/mobile/scripts/repro-worktree-startup-stream.ts
@@ -4,10 +4,15 @@
* Usage:
* pnpm exec tsx mobile/scripts/repro-worktree-startup-stream.ts [startupCommand]
*/
-import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
+import { existsSync, readFileSync } from 'node:fs'
import { join } from 'node:path'
import nacl from 'tweetnacl'
import WebSocket from 'ws'
+import {
+ saveStartupCaptures,
+ summarizeStartupCapture,
+ type StartupTerminalCapture
+} from './repro-worktree-startup-output'
const WS_URL = process.env.ORCA_MOBILE_WS_URL ?? 'ws://127.0.0.1:6768'
const USER_DATA =
@@ -15,7 +20,6 @@ const USER_DATA =
const repoSelector = process.argv[2]
const worktreeName = process.argv[3]
const startupCommand = process.argv[4] || 'claude'
-const ESC = String.fromCharCode(27)
type RpcResponse = {
id: string
@@ -36,13 +40,6 @@ type TerminalInfo = {
title: string | null
}
-type Capture = {
- handle: string
- title: string | null
- scrollback: Record | null
- chunks: string[]
-}
-
if (!repoSelector || !worktreeName) {
console.error(
'Usage: pnpm exec tsx mobile/scripts/repro-worktree-startup-stream.ts [startupCommand]'
@@ -141,26 +138,6 @@ function formatError(response: RpcResponse): string {
return JSON.stringify(response.error ?? response.result ?? response).slice(0, 1000)
}
-function stripAnsi(value: string): string {
- return (
- value
- // eslint-disable-next-line no-control-regex -- intentional terminal escape stripping for repro summaries
- .replace(/\u001b\][^\u0007]*(?:\u0007|\u001b\\)/g, '')
- // eslint-disable-next-line no-control-regex -- intentional terminal escape stripping for repro summaries
- .replace(/\u001b\[[0-?]*[ -/]*[@-~]/g, '')
- .replace(/\r/g, '\n')
- )
-}
-
-function normalizePreview(value: string): string {
- return stripAnsi(value)
- .split('\n')
- .map((line) => line.trim())
- .filter(Boolean)
- .slice(0, 8)
- .join('\n')
-}
-
async function waitForTerminals(ws: WebSocket, worktreeId: string): Promise {
const deadline = Date.now() + 60_000
while (Date.now() < deadline) {
@@ -185,7 +162,7 @@ async function waitForTerminals(ws: WebSocket, worktreeId: string): Promise {
+async function subscribe(ws: WebSocket, capture: StartupTerminalCapture): Promise {
const id = nextId()
streamListeners.set(id, (result) => {
if (result.type === 'scrollback') {
@@ -204,44 +181,6 @@ async function subscribe(ws: WebSocket, capture: Capture): Promise {
})
}
-function summarize(capture: Capture): Record {
- const serialized =
- typeof capture.scrollback?.serialized === 'string' ? capture.scrollback.serialized : ''
- const live = capture.chunks.join('')
- const serializedPreview = normalizePreview(serialized)
- const livePreview = normalizePreview(live)
- return {
- handle: capture.handle,
- title: capture.title,
- cols: capture.scrollback?.cols ?? null,
- rows: capture.scrollback?.rows ?? null,
- serializedBytes: Buffer.byteLength(serialized),
- liveBytes: Buffer.byteLength(live),
- liveChunks: capture.chunks.length,
- serializedSgr: (serialized.match(new RegExp(`${ESC}\\[[0-9;:]*m`, 'g')) ?? []).length,
- liveSgr: (live.match(new RegExp(`${ESC}\\[[0-9;:]*m`, 'g')) ?? []).length,
- livePreviewContainedInSerialized:
- livePreview.length > 0 && stripAnsi(serialized).includes(livePreview.split('\n')[0] ?? ''),
- serializedPreview,
- livePreview
- }
-}
-
-function save(captures: Capture[], worktreeName: string): string {
- const dir = join(process.cwd(), 'terminal-startup-repro', worktreeName)
- mkdirSync(dir, { recursive: true })
- for (const capture of captures) {
- const base = capture.handle.replace(/[^a-z0-9_-]/gi, '-')
- const serialized =
- typeof capture.scrollback?.serialized === 'string' ? capture.scrollback.serialized : ''
- writeFileSync(join(dir, `${base}.serialized.ansi`), serialized)
- writeFileSync(join(dir, `${base}.live.ansi`), capture.chunks.join(''))
- writeFileSync(join(dir, `${base}.json`), JSON.stringify(capture, null, 2))
- }
- writeFileSync(join(dir, 'summary.json'), JSON.stringify(captures.map(summarize), null, 2))
- return dir
-}
-
async function run(ws: WebSocket): Promise {
ws.send(
JSON.stringify({
@@ -312,8 +251,8 @@ async function run(ws: WebSocket): Promise {
for (const capture of captures) {
await send(ws, 'terminal.unsubscribe', { subscriptionId: capture.handle }).catch(() => null)
}
- const dir = save(captures, worktreeName)
- console.table(captures.map(summarize))
+ const dir = saveStartupCaptures(captures, worktreeName)
+ console.table(captures.map(summarizeStartupCapture))
console.log(`saved: ${dir}`)
}
diff --git a/mobile/src/components/CustomKeyModal.styles.ts b/mobile/src/components/CustomKeyModal.styles.ts
new file mode 100644
index 00000000000..991e557441f
--- /dev/null
+++ b/mobile/src/components/CustomKeyModal.styles.ts
@@ -0,0 +1,276 @@
+import { StyleSheet } from 'react-native'
+import { colors, spacing, radii, typography } from '../theme/mobile-theme'
+
+export const customKeyModalStyles = StyleSheet.create({
+ header: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingBottom: spacing.sm
+ },
+ backButton: {
+ width: 30,
+ height: 30,
+ borderRadius: 15,
+ alignItems: 'center',
+ justifyContent: 'center'
+ },
+ backButtonPressed: {
+ backgroundColor: colors.bgRaised
+ },
+ backSpacer: {
+ width: 30
+ },
+ title: {
+ flex: 1,
+ fontSize: 15,
+ fontWeight: '600',
+ color: colors.textPrimary,
+ textAlign: 'center'
+ },
+ group: {
+ backgroundColor: colors.bgPanel,
+ borderRadius: 12,
+ overflow: 'hidden'
+ },
+ separator: {
+ height: StyleSheet.hairlineWidth,
+ backgroundColor: colors.borderSubtle,
+ marginHorizontal: spacing.md
+ },
+ row: {
+ paddingVertical: spacing.md,
+ paddingHorizontal: spacing.md + 2
+ },
+ rowPressed: {
+ backgroundColor: colors.bgRaised
+ },
+ rowLabel: {
+ fontSize: typography.bodySize,
+ fontWeight: '500',
+ color: colors.textPrimary,
+ marginBottom: 1
+ },
+ rowHint: {
+ fontSize: 12,
+ color: colors.textMuted
+ },
+ shortcutForm: {
+ paddingTop: spacing.sm
+ },
+ preview: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ gap: spacing.sm,
+ paddingVertical: spacing.lg + spacing.xs,
+ flexWrap: 'wrap'
+ },
+ previewKeycapRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: spacing.sm
+ },
+ previewPlus: {
+ color: colors.textMuted,
+ fontSize: 16
+ },
+ keycap: {
+ minWidth: 48,
+ height: 48,
+ paddingHorizontal: spacing.md,
+ borderRadius: 10,
+ backgroundColor: colors.bgPanel,
+ borderWidth: 1,
+ borderColor: colors.borderSubtle,
+ alignItems: 'center',
+ justifyContent: 'center'
+ },
+ keycapModifier: {
+ minWidth: 0
+ },
+ keycapWarn: {
+ borderColor: colors.statusAmber
+ },
+ keycapText: {
+ color: colors.textPrimary,
+ fontFamily: typography.monoFamily,
+ fontSize: 17,
+ fontWeight: '600'
+ },
+ keycapTextWarn: {
+ color: colors.statusAmber
+ },
+ keycapModifierText: {
+ color: colors.textSecondary,
+ fontFamily: typography.monoFamily,
+ fontSize: 14,
+ fontWeight: '600'
+ },
+ section: {
+ marginTop: spacing.md
+ },
+ sectionLabel: {
+ fontSize: 11,
+ color: colors.textMuted,
+ textTransform: 'uppercase',
+ letterSpacing: 0.8,
+ marginBottom: spacing.sm,
+ paddingLeft: 2
+ },
+ mods: {
+ flexDirection: 'row',
+ gap: spacing.sm
+ },
+ chip: {
+ flex: 1,
+ height: 40,
+ borderRadius: 8,
+ backgroundColor: colors.bgPanel,
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ gap: 4
+ },
+ chipSelected: {
+ backgroundColor: colors.textPrimary
+ },
+ chipPressed: {
+ backgroundColor: colors.bgRaised
+ },
+ chipText: {
+ color: colors.textSecondary,
+ fontSize: 14,
+ fontWeight: '500'
+ },
+ chipTextSelected: {
+ color: colors.bgBase
+ },
+ chipGlyph: {
+ color: colors.textMuted,
+ fontSize: 13,
+ fontFamily: typography.monoFamily
+ },
+ chipGlyphSelected: {
+ color: 'rgba(10,10,10,0.5)'
+ },
+ keyInput: {
+ width: '100%',
+ height: 56,
+ borderRadius: 10,
+ backgroundColor: colors.bgPanel,
+ borderWidth: 1,
+ borderColor: colors.borderSubtle,
+ color: colors.textPrimary,
+ fontFamily: typography.monoFamily,
+ fontSize: 22,
+ fontWeight: '600',
+ textAlign: 'center'
+ },
+ moreLink: {
+ paddingVertical: spacing.sm,
+ alignItems: 'center'
+ },
+ moreLinkPressed: {
+ opacity: 0.6
+ },
+ moreLinkText: {
+ color: colors.textSecondary,
+ fontSize: 13,
+ textDecorationLine: 'underline'
+ },
+ specialKeysForm: {
+ paddingTop: spacing.xs,
+ paddingBottom: spacing.md,
+ gap: spacing.md
+ },
+ specialGroup: {
+ gap: spacing.xs
+ },
+ specialGroupTitle: {
+ fontSize: 11,
+ color: colors.textMuted,
+ textTransform: 'uppercase',
+ letterSpacing: 0.8,
+ paddingLeft: 2,
+ marginBottom: spacing.xs
+ },
+ keyGrid: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ marginHorizontal: -spacing.xs / 2
+ },
+ keyCellWrap: {
+ paddingHorizontal: spacing.xs / 2,
+ paddingVertical: spacing.xs / 2
+ },
+ keyCell: {
+ height: 40,
+ borderRadius: 8,
+ backgroundColor: colors.bgPanel,
+ alignItems: 'center',
+ justifyContent: 'center'
+ },
+ keyCellPressed: {
+ backgroundColor: colors.bgRaised
+ },
+ keyCellSelected: {
+ backgroundColor: colors.textPrimary
+ },
+ keyCellText: {
+ fontSize: 13,
+ fontWeight: '600',
+ color: colors.textPrimary,
+ fontFamily: typography.monoFamily
+ },
+ keyCellTextSelected: {
+ color: colors.bgBase
+ },
+ macroForm: {
+ padding: spacing.md,
+ gap: spacing.sm
+ },
+ fieldLabel: {
+ fontSize: 13,
+ fontWeight: '500',
+ color: colors.textSecondary
+ },
+ fieldInput: {
+ backgroundColor: colors.bgBase,
+ color: colors.textPrimary,
+ borderRadius: radii.input,
+ paddingHorizontal: spacing.md,
+ paddingVertical: spacing.sm,
+ fontSize: 14,
+ fontFamily: typography.monoFamily,
+ borderWidth: 1,
+ borderColor: colors.borderSubtle
+ },
+ switchRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ paddingVertical: spacing.xs
+ },
+ switchLabel: {
+ fontSize: typography.bodySize,
+ color: colors.textPrimary
+ },
+ saveButton: {
+ marginTop: spacing.md,
+ backgroundColor: colors.textPrimary,
+ paddingVertical: spacing.md,
+ borderRadius: 10,
+ alignItems: 'center'
+ },
+ saveButtonDisabled: {
+ backgroundColor: colors.bgRaised
+ },
+ saveButtonText: {
+ color: colors.bgBase,
+ fontSize: 15,
+ fontWeight: '600'
+ },
+ saveButtonTextDisabled: {
+ color: colors.textMuted
+ }
+})
diff --git a/mobile/src/components/CustomKeyModal.tsx b/mobile/src/components/CustomKeyModal.tsx
index 2777e310dc8..0cc24f4c030 100644
--- a/mobile/src/components/CustomKeyModal.tsx
+++ b/mobile/src/components/CustomKeyModal.tsx
@@ -1,8 +1,8 @@
import { useCallback, useMemo, useState } from 'react'
-import { View, Text, Pressable, TextInput, StyleSheet, Switch } from 'react-native'
+import { View, Text, Pressable, TextInput, Switch } from 'react-native'
import { ChevronLeft } from 'lucide-react-native'
import AsyncStorage from '@react-native-async-storage/async-storage'
-import { colors, spacing, radii, typography } from '../theme/mobile-theme'
+import { colors } from '../theme/mobile-theme'
import { BottomDrawer } from './BottomDrawer'
import {
buildTerminalShortcutKey,
@@ -11,6 +11,7 @@ import {
type TerminalShortcutModifier,
type TerminalShortcutSpecialKey
} from '../terminal/terminal-accessory-keys'
+import { customKeyModalStyles as styles } from './CustomKeyModal.styles'
const CUSTOM_ACCESSORY_KEYS_STORAGE_KEY = 'orca:custom-accessory-keys'
@@ -409,277 +410,3 @@ export function CustomKeyModal({ visible, onClose, onKeysChanged, onManageShortc
)
}
-
-const styles = StyleSheet.create({
- header: {
- flexDirection: 'row',
- alignItems: 'center',
- paddingBottom: spacing.sm
- },
- backButton: {
- width: 30,
- height: 30,
- borderRadius: 15,
- alignItems: 'center',
- justifyContent: 'center'
- },
- backButtonPressed: {
- backgroundColor: colors.bgRaised
- },
- backSpacer: {
- width: 30
- },
- title: {
- flex: 1,
- fontSize: 15,
- fontWeight: '600',
- color: colors.textPrimary,
- textAlign: 'center'
- },
- group: {
- backgroundColor: colors.bgPanel,
- borderRadius: 12,
- overflow: 'hidden'
- },
- separator: {
- height: StyleSheet.hairlineWidth,
- backgroundColor: colors.borderSubtle,
- marginHorizontal: spacing.md
- },
- row: {
- paddingVertical: spacing.md,
- paddingHorizontal: spacing.md + 2
- },
- rowPressed: {
- backgroundColor: colors.bgRaised
- },
- rowLabel: {
- fontSize: typography.bodySize,
- fontWeight: '500',
- color: colors.textPrimary,
- marginBottom: 1
- },
- rowHint: {
- fontSize: 12,
- color: colors.textMuted
- },
- shortcutForm: {
- paddingTop: spacing.sm
- },
- preview: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'center',
- gap: spacing.sm,
- paddingVertical: spacing.lg + spacing.xs,
- flexWrap: 'wrap'
- },
- previewKeycapRow: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: spacing.sm
- },
- previewPlus: {
- color: colors.textMuted,
- fontSize: 16
- },
- keycap: {
- minWidth: 48,
- height: 48,
- paddingHorizontal: spacing.md,
- borderRadius: 10,
- backgroundColor: colors.bgPanel,
- borderWidth: 1,
- borderColor: colors.borderSubtle,
- alignItems: 'center',
- justifyContent: 'center'
- },
- keycapModifier: {
- minWidth: 0
- },
- keycapWarn: {
- borderColor: colors.statusAmber
- },
- keycapText: {
- color: colors.textPrimary,
- fontFamily: typography.monoFamily,
- fontSize: 17,
- fontWeight: '600'
- },
- keycapTextWarn: {
- color: colors.statusAmber
- },
- keycapModifierText: {
- color: colors.textSecondary,
- fontFamily: typography.monoFamily,
- fontSize: 14,
- fontWeight: '600'
- },
- section: {
- marginTop: spacing.md
- },
- sectionLabel: {
- fontSize: 11,
- color: colors.textMuted,
- textTransform: 'uppercase',
- letterSpacing: 0.8,
- marginBottom: spacing.sm,
- paddingLeft: 2
- },
- mods: {
- flexDirection: 'row',
- gap: spacing.sm
- },
- chip: {
- flex: 1,
- height: 40,
- borderRadius: 8,
- backgroundColor: colors.bgPanel,
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'center',
- gap: 4
- },
- chipSelected: {
- backgroundColor: colors.textPrimary
- },
- chipPressed: {
- backgroundColor: colors.bgRaised
- },
- chipText: {
- color: colors.textSecondary,
- fontSize: 14,
- fontWeight: '500'
- },
- chipTextSelected: {
- color: colors.bgBase
- },
- chipGlyph: {
- color: colors.textMuted,
- fontSize: 13,
- fontFamily: typography.monoFamily
- },
- chipGlyphSelected: {
- color: 'rgba(10,10,10,0.5)'
- },
- keyInput: {
- width: '100%',
- height: 56,
- borderRadius: 10,
- backgroundColor: colors.bgPanel,
- borderWidth: 1,
- borderColor: colors.borderSubtle,
- color: colors.textPrimary,
- fontFamily: typography.monoFamily,
- fontSize: 22,
- fontWeight: '600',
- textAlign: 'center'
- },
- moreLink: {
- paddingVertical: spacing.sm,
- alignItems: 'center'
- },
- moreLinkPressed: {
- opacity: 0.6
- },
- moreLinkText: {
- color: colors.textSecondary,
- fontSize: 13,
- textDecorationLine: 'underline'
- },
- specialKeysForm: {
- paddingTop: spacing.xs,
- paddingBottom: spacing.md,
- gap: spacing.md
- },
- specialGroup: {
- gap: spacing.xs
- },
- specialGroupTitle: {
- fontSize: 11,
- color: colors.textMuted,
- textTransform: 'uppercase',
- letterSpacing: 0.8,
- paddingLeft: 2,
- marginBottom: spacing.xs
- },
- keyGrid: {
- flexDirection: 'row',
- flexWrap: 'wrap',
- marginHorizontal: -spacing.xs / 2
- },
- keyCellWrap: {
- paddingHorizontal: spacing.xs / 2,
- paddingVertical: spacing.xs / 2
- },
- keyCell: {
- height: 40,
- borderRadius: 8,
- backgroundColor: colors.bgPanel,
- alignItems: 'center',
- justifyContent: 'center'
- },
- keyCellPressed: {
- backgroundColor: colors.bgRaised
- },
- keyCellSelected: {
- backgroundColor: colors.textPrimary
- },
- keyCellText: {
- fontSize: 13,
- fontWeight: '600',
- color: colors.textPrimary,
- fontFamily: typography.monoFamily
- },
- keyCellTextSelected: {
- color: colors.bgBase
- },
- macroForm: {
- padding: spacing.md,
- gap: spacing.sm
- },
- fieldLabel: {
- fontSize: 13,
- fontWeight: '500',
- color: colors.textSecondary
- },
- fieldInput: {
- backgroundColor: colors.bgBase,
- color: colors.textPrimary,
- borderRadius: radii.input,
- paddingHorizontal: spacing.md,
- paddingVertical: spacing.sm,
- fontSize: 14,
- fontFamily: typography.monoFamily,
- borderWidth: 1,
- borderColor: colors.borderSubtle
- },
- switchRow: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'space-between',
- paddingVertical: spacing.xs
- },
- switchLabel: {
- fontSize: typography.bodySize,
- color: colors.textPrimary
- },
- saveButton: {
- marginTop: spacing.md,
- backgroundColor: colors.textPrimary,
- paddingVertical: spacing.md,
- borderRadius: 10,
- alignItems: 'center'
- },
- saveButtonDisabled: {
- backgroundColor: colors.bgRaised
- },
- saveButtonText: {
- color: colors.bgBase,
- fontSize: 15,
- fontWeight: '600'
- },
- saveButtonTextDisabled: {
- color: colors.textMuted
- }
-})
diff --git a/mobile/src/components/mobile-rich-markdown-editor-body-primary.ts b/mobile/src/components/mobile-rich-markdown-editor-body-primary.ts
new file mode 100644
index 00000000000..c4348ad15a2
--- /dev/null
+++ b/mobile/src/components/mobile-rich-markdown-editor-body-primary.ts
@@ -0,0 +1,279 @@
+export const MOBILE_RICH_MARKDOWN_EDITOR_BODY_PRIMARY = [
+ ';',
+ ' --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;',
+ ' --font-sans: Geist, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;',
+ ' }',
+ ' * { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }',
+ ' html, body {',
+ ' width: 100%;',
+ ' min-height: 100%;',
+ ' margin: 0;',
+ ' background: var(--editor-surface);',
+ ' color: var(--foreground);',
+ ' font-family: var(--font-sans);',
+ ' overscroll-behavior: contain;',
+ ' }',
+ ' body { overflow: auto; }',
+ ' #editor {',
+ ' min-height: 100vh;',
+ ' padding: 18px 16px 112px;',
+ ' outline: none;',
+ ' font-size: 14px;',
+ ' line-height: 1.7;',
+ ' word-wrap: break-word;',
+ ' overflow-wrap: anywhere;',
+ ' caret-color: var(--foreground);',
+ ' }',
+ ' #editor[contenteditable="false"] {',
+ ' opacity: 0.78;',
+ ' }',
+ ' #editor:empty::before,',
+ ' #editor p.is-empty:first-child::before {',
+ ' content: attr(data-placeholder);',
+ ' color: var(--muted-foreground);',
+ ' pointer-events: none;',
+ ' }',
+ ' #editor > :first-child { margin-top: 0; }',
+ ' h1, h2, h3, h4, h5, h6 {',
+ ' margin: 1.5em 0 0.5em;',
+ ' font-weight: 600;',
+ ' line-height: 1.3;',
+ ' letter-spacing: 0;',
+ ' }',
+ ' h1 { font-size: 1.85em; font-weight: 700; }',
+ ' h2 { font-size: 1.4em; }',
+ ' h3 { font-size: 1.15em; }',
+ ' p, ul, ol, blockquote { margin: 0.75em 0; }',
+ ' ul, ol { padding-left: 1.5em; }',
+ ' ul { list-style: disc; }',
+ ' ol { list-style: decimal; }',
+ ' li { margin: 0.15em 0; }',
+ ' li > p { margin: 0; }',
+ ' ul[data-type="taskList"] {',
+ ' padding-left: 0;',
+ ' list-style: none;',
+ ' }',
+ ' ul[data-type="taskList"] > li {',
+ ' display: flex;',
+ ' align-items: flex-start;',
+ ' gap: 6px;',
+ ' }',
+ ' ul[data-type="taskList"] > li > label {',
+ ' flex-shrink: 0;',
+ ' display: flex;',
+ ' align-items: center;',
+ ' height: 1.55em;',
+ ' user-select: none;',
+ ' }',
+ ' ul[data-type="taskList"] input[type="checkbox"] {',
+ ' appearance: none;',
+ ' width: 16px;',
+ ' height: 16px;',
+ ' margin: 0;',
+ ' border: 1.5px solid color-mix(in srgb, var(--foreground) 55%, transparent);',
+ ' border-radius: 4px;',
+ ' background: transparent;',
+ ' position: relative;',
+ ' }',
+ ' ul[data-type="taskList"] input[type="checkbox"]:checked {',
+ ' background: var(--primary);',
+ ' border-color: var(--primary);',
+ ' }',
+ ' ul[data-type="taskList"] input[type="checkbox"]:checked::after {',
+ ' content: "";',
+ ' position: absolute;',
+ ' left: 4px;',
+ ' top: 1px;',
+ ' width: 5px;',
+ ' height: 9px;',
+ ' border: solid var(--primary-foreground);',
+ ' border-width: 0 2px 2px 0;',
+ ' transform: rotate(45deg);',
+ ' }',
+ ' ul[data-type="taskList"] input[type="checkbox"]:disabled {',
+ ' opacity: 0.65;',
+ ' }',
+ ' ul[data-type="taskList"] > li > div {',
+ ' flex: 1;',
+ ' min-width: 0;',
+ ' }',
+ ' ul[data-type="taskList"] > li[data-checked="true"] > div {',
+ ' text-decoration: line-through;',
+ ' color: var(--muted-foreground);',
+ ' }',
+ ' blockquote {',
+ ' padding: 0.5em 1em;',
+ ' border-left: 3px solid var(--border);',
+ ' border-radius: 0 6px 6px 0;',
+ ' color: var(--muted-foreground);',
+ ' background: color-mix(in srgb, var(--foreground) 2%, transparent);',
+ ' }',
+ ' table {',
+ ' width: 100%;',
+ ' margin: 1em 0;',
+ ' border-collapse: collapse;',
+ ' font-size: 0.95em;',
+ ' }',
+ ' th, td {',
+ ' padding: 8px 14px;',
+ ' border: 1px solid var(--border);',
+ ' text-align: left;',
+ ' vertical-align: top;',
+ ' }',
+ ' th {',
+ ' font-weight: 600;',
+ ' background: color-mix(in srgb, var(--foreground) 4%, transparent);',
+ ' }',
+ ' tr:nth-child(odd) td {',
+ ' background: color-mix(in srgb, var(--foreground) 1.5%, transparent);',
+ ' }',
+ ' code {',
+ ' padding: 0.2em 0.4em;',
+ ' border-radius: 5px;',
+ ' background: color-mix(in srgb, var(--foreground) 8%, transparent);',
+ ' font-size: 0.88em;',
+ ' font-family: var(--font-mono);',
+ ' }',
+ ' pre {',
+ ' margin: 0.75em 0;',
+ ' padding: 14px 18px;',
+ ' border-radius: 8px;',
+ ' border: 1px solid color-mix(in srgb, var(--foreground) 6%, transparent);',
+ ' overflow-x: auto;',
+ ' line-height: 1.55;',
+ ' background: color-mix(in srgb, var(--foreground) 6%, transparent);',
+ ' font-family: var(--font-mono);',
+ ' white-space: pre-wrap;',
+ ' }',
+ ' pre::before {',
+ ' content: attr(data-language);',
+ ' display: block;',
+ ' min-height: 13px;',
+ ' margin-bottom: 4px;',
+ ' color: var(--muted-foreground);',
+ ' font-size: 11px;',
+ ' text-transform: uppercase;',
+ ' }',
+ ' pre code {',
+ ' padding: 0;',
+ ' border-radius: 0;',
+ ' background: transparent;',
+ ' font-size: 0.92em;',
+ ' white-space: pre-wrap;',
+ ' }',
+ ' hr {',
+ ' margin: 1.5em 0;',
+ ' border: none;',
+ ' border-top: 1px solid var(--border);',
+ ' }',
+ ' a {',
+ ' color: var(--accent-link);',
+ ' text-decoration: underline;',
+ ' text-decoration-color: color-mix(in srgb, currentColor 40%, transparent);',
+ ' text-underline-offset: 2px;',
+ ' }',
+ ' img {',
+ ' display: block;',
+ ' max-width: 100%;',
+ ' margin: 0.75em 0;',
+ ' border-radius: 8px;',
+ ' }',
+ ' ',
+ '',
+ '',
+ ' ',
+ ' ',
+ '',
+ ' |