Merge branch 'ra/max-lines-bottom-22-linear-item-drawer' into ra/max-lines-bottom-23-telemetry-events

This commit is contained in:
Neil
2026-08-29 15:51:38 -07:00
236 changed files with 6795 additions and 2641 deletions
@@ -28,10 +28,9 @@ runs:
steps:
# setup-node needs pnpm on PATH to locate and restore its store.
- name: Setup pnpm
uses: pnpm/action-setup@v6
uses: pnpm/setup@v2
with:
version: 10.24.0
run_install: false
install: false
- name: Setup Node.js
id: default-node
@@ -88,7 +87,7 @@ runs:
pnpm install --frozen-lockfile --ignore-scripts
# Job containers can run composite steps from a source mirror without .git.
if [ "$(git -C "$GITHUB_WORKSPACE" rev-parse --is-inside-work-tree 2>/dev/null)" = true ]; then
git -C "$GITHUB_WORKSPACE" diff --exit-code -- package.json pnpm-lock.yaml
git -C "$GITHUB_WORKSPACE" diff --exit-code -- package.json pnpm-lock.yaml pnpm-workspace.yaml
fi
# Why cached: `--ignore-scripts` leaves node-pty without build/Release, so
+2 -2
View File
@@ -171,9 +171,9 @@ jobs:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@v6
uses: pnpm/setup@v2
with:
run_install: false
install: false
- name: Setup Node.js
uses: actions/setup-node@v6
+9 -42
View File
@@ -46,8 +46,6 @@ on:
- 'src/main/ssh/ssh-remote-cli-launcher.test.ts'
- 'src/shared/computer-use-*.ts'
- 'tests/e2e/computer-linux.e2e.ts'
- 'tests/e2e/computer-mac.e2e.ts'
- 'tests/e2e/computer-mac-safari.e2e.ts'
- 'tests/e2e/computer-windows.e2e.ts'
- 'tests/e2e/computer-windows-store.e2e.ts'
- 'tests/e2e/helpers/computer-cli-driver.ts'
@@ -137,8 +135,9 @@ jobs:
- name: Windows daemon workspace-close repro
if: runner.os == 'Windows'
run: node config/scripts/windows-daemon-workspace-close-repro.mjs
# Hosted macOS runners cannot receive persistent Accessibility/Screen Recording
# grants. Keep the real native build/owner-loss checks on every trigger instead.
mac-native-owner-smoke:
if: github.event_name == 'pull_request'
runs-on: macos-15
permissions:
contents: read
@@ -159,28 +158,6 @@ jobs:
- name: Swift tests and signed universal helper verification
run: pnpm verify:computer-native
mac:
# macOS Accessibility and Screen Recording require user-granted TCC entries.
# Keep this on manual/scheduled permission-bearing runners instead of PR CI.
if: github.event_name != 'pull_request'
runs-on: macos-15
env:
ORCA_COMPUTER_E2E: '1'
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version-file: package.json
- uses: pnpm/action-setup@v6
with:
run_install: false
- run: pnpm install --frozen-lockfile
- run: pnpm build:computer-macos
- run: pnpm verify:computer-native
- run: pnpm build:cli
- run: pnpm build:electron-vite
- run: pnpm test:e2e:computer --reporter=verbose tests/e2e/computer-mac.e2e.ts tests/e2e/computer-mac-safari.e2e.ts
linux:
if: github.event_name != 'pull_request'
runs-on: ubuntu-22.04
@@ -189,20 +166,12 @@ jobs:
ACCESSIBILITY_ENABLED: '1'
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version-file: package.json
- uses: pnpm/action-setup@v6
with:
run_install: false
persist-credentials: false
- run: sudo apt-get update && sudo apt-get install -y build-essential python3 python3-gi gir1.2-atspi-2.0 gedit at-spi2-core xvfb xclip xdotool
# Why: keep scheduled Linux e2e on the same native install path as PR
# smoke and pr.yml's verify job.
- name: Use external node-gyp to avoid pnpm's bundled copy (Linux only)
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: electron
- run: pnpm verify:computer-native
- run: pnpm build:cli
- run: pnpm build:electron-vite
@@ -215,13 +184,11 @@ jobs:
ORCA_COMPUTER_E2E: '1'
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version-file: package.json
- uses: pnpm/action-setup@v6
persist-credentials: false
- uses: ./.github/actions/install-node-dependencies
with:
run_install: false
- run: pnpm install --frozen-lockfile
native-runtime: electron
- run: pnpm verify:computer-native
- run: pnpm build:cli
- run: pnpm build:electron-vite
@@ -43,9 +43,9 @@ jobs:
node-version-file: package.json
- name: Setup pnpm
uses: pnpm/action-setup@v6
uses: pnpm/setup@v2
with:
run_install: false
install: false
- name: Install dependencies
run: pnpm install --frozen-lockfile
+2 -2
View File
@@ -145,9 +145,9 @@ jobs:
- name: Setup pnpm
if: steps.freshness.outputs.should_build == 'true'
uses: pnpm/action-setup@v6
uses: pnpm/setup@v2
with:
run_install: false
install: false
- name: Setup Node.js
if: steps.freshness.outputs.should_build == 'true'
+2 -2
View File
@@ -191,9 +191,9 @@ jobs:
# pnpm must be on PATH before setup-node so setup-node can locate the store.
- name: Setup pnpm
uses: pnpm/action-setup@v6
uses: pnpm/setup@v2
with:
run_install: false
install: false
- name: Setup Node.js
uses: actions/setup-node@v6
+63 -33
View File
@@ -81,26 +81,37 @@ jobs:
fail-fast: false
matrix:
include:
- shard: '1/10'
shard_name: 1-of-10
- shard: '2/10'
shard_name: 2-of-10
- shard: '3/10'
shard_name: 3-of-10
- shard: '4/10'
shard_name: 4-of-10
- shard: '5/10'
shard_name: 5-of-10
- shard: '6/10'
shard_name: 6-of-10
- shard: '7/10'
shard_name: 7-of-10
- shard: '8/10'
shard_name: 8-of-10
- shard: '9/10'
shard_name: 9-of-10
- shard: '10/10'
shard_name: 10-of-10
# Fourteen scheduled runs averaged 24.6 minutes per shard; shards 4
# and 9 repeatedly hit the 30-minute cap. A 12-way trial still left
# one 30-minute shard, so 14 gives the suite enough failure headroom.
- shard: '1/14'
shard_name: 1-of-14
- shard: '2/14'
shard_name: 2-of-14
- shard: '3/14'
shard_name: 3-of-14
- shard: '4/14'
shard_name: 4-of-14
- shard: '5/14'
shard_name: 5-of-14
- shard: '6/14'
shard_name: 6-of-14
- shard: '7/14'
shard_name: 7-of-14
- shard: '8/14'
shard_name: 8-of-14
- shard: '9/14'
shard_name: 9-of-14
- shard: '10/14'
shard_name: 10-of-14
- shard: '11/14'
shard_name: 11-of-14
- shard: '12/14'
shard_name: 12-of-14
- shard: '13/14'
shard_name: 13-of-14
- shard: '14/14'
shard_name: 14-of-14
steps:
- name: Checkout
@@ -108,18 +119,10 @@ jobs:
with:
ref: ${{ inputs.ref || github.ref }}
# 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
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
# suppresses mainWindow.show() via ORCA_E2E_HEADLESS. xvfb provides a
# virtual framebuffer so Chromium can initialize without a real display.
- name: Install xvfb
run: sudo apt-get install -y xvfb
# Native cache misses need the compiler, Electron needs Xvfb, and paired
# Quick Open needs ripgrep. Install them in one apt transaction per shard.
- name: Install native build and headless UI tools
run: sudo apt-get update && sudo apt-get install -y build-essential fonts-noto-cjk python3 ripgrep xvfb zsh
- uses: ./.github/actions/install-node-dependencies
with:
@@ -265,12 +268,31 @@ jobs:
- name: Run Docker SSH watcher isolation E2E
run: xvfb-run --auto-servernum env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm run test:e2e:ssh-docker-watcher-isolation
# Why: Playwright empties test-results/ when it starts, so each step here used to
# destroy the previous step's traces. Only the last lane's failure was ever
# diagnosable from the artifact; set each lane aside before the next one runs.
- name: Keep watcher-isolation traces
if: always()
run: |
if [ -d test-results ]; then
mkdir -p e2e-traces
mv test-results "e2e-traces/watcher-isolation"
fi
# Why always(): this lane gates SSH parking/retention plus startup-exec
# readiness across live SSH, headed paired, and headless serve topologies.
- name: Run Docker SSH terminal parking + startup readiness E2E
if: always()
run: xvfb-run --auto-servernum env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm run test:e2e:ssh-docker-terminal-parking
- name: Keep terminal-parking traces
if: always()
run: |
if [ -d test-results ]; then
mkdir -p e2e-traces
mv test-results "e2e-traces/terminal-parking"
fi
# Why here rather than the sharded lanes: the shards set no ORCA_E2E_SSH_DOCKER, so every
# spec below skipped itself while the shard still reported green. Running them on this one
# VM pays the fixture image build once instead of ten times, and keeps an SSH regression
@@ -279,11 +301,19 @@ jobs:
if: always()
run: xvfb-run --auto-servernum env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm run test:e2e:ssh-docker
- name: Keep remaining-ssh-docker traces
if: always()
run: |
if [ -d test-results ]; then
mkdir -p e2e-traces
mv test-results "e2e-traces/remaining-ssh-docker"
fi
- name: Upload watcher isolation traces
if: failure()
uses: actions/upload-artifact@v7
with:
name: playwright-traces-ssh-docker-watcher-isolation
path: test-results/
path: e2e-traces/
retention-days: 7
if-no-files-found: ignore
+2 -2
View File
@@ -43,9 +43,9 @@ jobs:
node-version-file: package.json
- name: Setup pnpm
uses: pnpm/action-setup@v6
uses: pnpm/setup@v2
with:
run_install: false
install: false
# Why: Linux golden E2E uses the same native install path as PR/release CI,
# which needs pnpm to bypass its non-executable bundled gyp_main.py.
+2 -2
View File
@@ -135,9 +135,9 @@ jobs:
- name: Setup pnpm
if: steps.freshness.outputs.should_build == 'true'
uses: pnpm/action-setup@v6
uses: pnpm/setup@v2
with:
run_install: false
install: false
- name: Setup Node.js
if: steps.freshness.outputs.should_build == 'true'
@@ -24,9 +24,9 @@ jobs:
node-version-file: package.json
- name: Setup pnpm
uses: pnpm/action-setup@v6
uses: pnpm/setup@v2
with:
run_install: false
install: false
# Why: mirrors pr.yml so native module rebuilds do not use pnpm's
# non-executable bundled gyp_main.py on Linux runners.
@@ -43,12 +43,9 @@ jobs:
fi
- name: Install dependencies
# Why: pnpm 10.24's frozen headless fast path can fail on fresh Ubuntu
# runners while creating the root node_modules. Use the normal resolver
# path, then verify package metadata stayed unchanged.
run: |
pnpm install --no-frozen-lockfile --prefer-frozen-lockfile=false
git diff --exit-code package.json pnpm-lock.yaml
pnpm install --frozen-lockfile
git diff --exit-code package.json pnpm-lock.yaml pnpm-workspace.yaml
- name: Start Weston
run: |
+2 -2
View File
@@ -44,9 +44,9 @@ jobs:
node-version: 24
- name: Setup pnpm
uses: pnpm/action-setup@v6
uses: pnpm/setup@v2
with:
run_install: false
install: false
- name: Install dependencies
run: pnpm install --frozen-lockfile
+2 -2
View File
@@ -64,9 +64,9 @@ jobs:
node-version: 24
- name: Setup pnpm
uses: pnpm/action-setup@v6
uses: pnpm/setup@v2
with:
run_install: false
install: false
- name: Install dependencies
run: pnpm install --frozen-lockfile
+2 -2
View File
@@ -51,9 +51,9 @@ jobs:
working-directory: mobile
- name: Setup pnpm
uses: pnpm/action-setup@v6
uses: pnpm/setup@v2
with:
run_install: false
install: false
# Why: the mobile typecheck imports shared types from ../src/shared, and
# some of those files import runtime deps (tweetnacl, ws) resolved from
+4
View File
@@ -595,9 +595,13 @@ jobs:
with:
native-runtime: electron
# Why --no-file-parallelism: every file here launches a full Electron stack twice, and each
# probe carries its own in-process deadline. Four at once on a 4-vCPU runner starve each other
# past those deadlines; serial, every probe owns the runner.
- name: Test Linux Electron lifecycle boundary
run: >-
xvfb-run --auto-servernum pnpm exec vitest run --config config/vitest.config.ts
--no-file-parallelism
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
+6 -6
View File
@@ -864,9 +864,9 @@ jobs:
node-version-file: package.json
- name: Setup pnpm
uses: pnpm/action-setup@v6
uses: pnpm/setup@v2
with:
run_install: false
install: false
# Why: Linux terminal golden E2E uses the same native install path as
# release CI, which needs pnpm to bypass its non-executable gyp_main.py.
@@ -1079,9 +1079,9 @@ jobs:
node-version-file: package.json
- name: Setup pnpm
uses: pnpm/action-setup@v6
uses: pnpm/setup@v2
with:
run_install: false
install: false
# Why: keep the non-blocking evidence lane on the same Linux native
# install path as the blocking golden and release build jobs.
@@ -1193,9 +1193,9 @@ jobs:
# pnpm must be on PATH before setup-node so setup-node can locate the store for caching.
- name: Setup pnpm
uses: pnpm/action-setup@v6
uses: pnpm/setup@v2
with:
run_install: false
install: false
- name: Setup Node.js
uses: actions/setup-node@v6
+2 -2
View File
@@ -38,9 +38,9 @@ jobs:
ref: refs/tags/${{ inputs.tag }}
- name: Setup pnpm
uses: pnpm/action-setup@v6
uses: pnpm/setup@v2
with:
run_install: false
install: false
- name: Setup Node.js
uses: actions/setup-node@v6
+2 -2
View File
@@ -41,9 +41,9 @@ jobs:
node-version-file: package.json
- name: Setup pnpm
uses: pnpm/action-setup@v6
uses: pnpm/setup@v2
with:
run_install: false
install: false
- name: Use external node-gyp to avoid pnpm bundled copy
run: |
+2 -2
View File
@@ -73,9 +73,9 @@ jobs:
node-version-file: package.json
- name: Setup pnpm
uses: pnpm/action-setup@v6
uses: pnpm/setup@v2
with:
run_install: false
install: false
# Why: this scheduled/manual workflow uses the same native install path as
# PR and E2E CI, which needs pnpm to bypass its bundled gyp_main.py.
+2 -2
View File
@@ -46,9 +46,9 @@ jobs:
# Why: setup-node can restore pnpm's content-addressed store only after
# the pnpm binary exists, avoiding repeat dependency downloads per run.
- name: Setup pnpm
uses: pnpm/action-setup@v6
uses: pnpm/setup@v2
with:
run_install: false
install: false
- name: Setup Node.js
uses: actions/setup-node@v6
+2 -2
View File
@@ -85,9 +85,9 @@ jobs:
node-version-file: package.json
- name: Setup pnpm
uses: pnpm/action-setup@v6
uses: pnpm/setup@v2
with:
run_install: false
install: false
# Why: the harness only needs its runtime deps (the Playwright Electron
# driver); it drives already-built installer artifacts, so no app build.
@@ -58,9 +58,9 @@ jobs:
node-version-file: package.json
- name: Setup pnpm
uses: pnpm/action-setup@v6
uses: pnpm/setup@v2
with:
run_install: false
install: false
- name: Install dependencies
run: pnpm install --frozen-lockfile
@@ -46,9 +46,9 @@ jobs:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@v6
uses: pnpm/setup@v2
with:
run_install: false
install: false
- name: Setup Node.js
uses: actions/setup-node@v6
@@ -32,9 +32,9 @@ jobs:
# Why: pnpm must exist before setup-node resolves its dependency cache.
- name: Setup pnpm
uses: pnpm/action-setup@v6
uses: pnpm/setup@v2
with:
run_install: false
install: false
- name: Setup Node.js
uses: actions/setup-node@v6
-2
View File
@@ -1,2 +0,0 @@
shamefully-hoist=true
minimum-release-age=4320
+12
View File
@@ -40,6 +40,18 @@
"react/no-string-refs": "error",
"react/no-unescaped-entities": "error",
"react/require-render-return": "error",
"react/error-boundaries": "off",
"react/globals": "off",
"react/immutability": "off",
"react/incompatible-library": "off",
"react/preserve-manual-memoization": "off",
"react/purity": "off",
"react/refs": "off",
"react/set-state-in-effect": "off",
"react/set-state-in-render": "off",
"react/static-components": "off",
"react/use-memo": "off",
"react/void-use-memo": "off",
"react/jsx-curly-brace-presence": [
"error",
{ "props": "never", "children": "never", "propElementValues": "always" }
@@ -26,6 +26,7 @@ RUN apt-get update \
&& rm -rf /var/lib/apt/lists/*
RUN corepack enable \
&& corepack prepare pnpm@10.24.0 --activate
&& corepack prepare pnpm@12.0.0 --activate \
&& pnpm --version
WORKDIR /workspace
@@ -10,7 +10,7 @@ index 855bd4b86f0a3c18c7594212c0e42b6e35bc4001..33774e7ae296f0de39dd94156673c9e7
],
"conditions": [
['OS=="win"', {
@@ -15,14 +14,13 @@
@@ -15,12 +14,11 @@
"src/process_commandline.cc"
],
- "include_dirs": [],
File diff suppressed because one or more lines are too long
+58 -16
View File
@@ -6321,36 +6321,48 @@
},
{
"id": "terminal-session.startup-cwd-missing-dir-recovery",
"title": "Fresh local terminal creation cannot be bricked by a deleted startup cwd",
"title": "Terminal working-directory recovery stays scoped and provider-safe",
"maturity": "experimental",
"protection": "partial",
"owner": "terminal-runtime",
"layer": "shared-main-renderer-contract",
"surfaces": ["terminal lifecycle", "tab creation", "PTY spawn", "startup cwd persistence"],
"layer": "shared-main-renderer-and-posix-shell-contract",
"surfaces": [
"terminal lifecycle",
"tab creation",
"PTY spawn",
"startup cwd persistence",
"long-lived Bash and Zsh OMP launches"
],
"platforms": ["macos", "linux", "windows", "mobile"],
"providers": ["local", "daemon", "ssh", "wsl", "remote-runtime"],
"coveredPlatforms": ["macos"],
"coveredProviders": ["local", "ssh", "remote-runtime"],
"coverageNotes": "Local macOS evidence covers the shared missing-dir fallback policy, main pty:spawn recovery and metadata, no-flag and reattach strictness, renderer IPC flag routing, SSH-tagged and remote-runtime omission, and the visibility-gated terminal notice. Daemon shares the same pre-provider main cwd decision but lacks a live daemon-provider run; WSL UNC paths are exempt from the probe by design and lack a live run; Linux/Windows and mobile/API strictness are gaps.",
"coverageNotes": "Local macOS evidence covers the shared missing-dir fallback policy, main pty:spawn recovery and metadata, no-flag and reattach strictness, renderer IPC flag routing, SSH-tagged and remote-runtime omission, and the visibility-gated terminal notice. Real Bash and Zsh node-pty tests replace the active cwd inode and exercise the shared POSIX OMP wrapper; generated-file snapshots pin identical local, daemon/SSH, and relay wrappers. Daemon shares the same pre-provider main cwd decision but lacks a live daemon-provider run; WSL UNC paths are exempt from the spawn probe and lack a live run; native PowerShell is unaffected; Linux/Windows and mobile/API strictness remain gaps.",
"motivatingLinks": [
"https://github.com/stablyai/orca/issues/7239",
"https://github.com/stablyai/orca/pull/7750",
"https://github.com/stablyai/orca/pull/7678"
"https://github.com/stablyai/orca/pull/7678",
"https://github.com/stablyai/orca/issues/16457",
"https://github.com/stablyai/orca/pull/17128"
],
"invariant": "A fresh local renderer terminal spawn may recover from a saved startup cwd whose directory no longer exists only by spawning at the selected workspace root and printing a generic in-terminal notice; existing directories — including ones outside the worktree (#7685) — spawn as requested, and reattach, SSH, remote-runtime, runtime/API, and mobile callers keep exact cwd semantics.",
"oracle": "The shared resolver falls back to the workspace root only when the injected existence probe reports the resolved cwd missing and the workspace root present, and never probes floating terminals or a cwd equal to the root. The renderer sends cwdFallback only for fresh local IPC spawns, main honors it only when connectionId and sessionId are absent, WSL UNC paths never engage the probe-based fallback, main returns fallback metadata only after an actual fallback, the IPC transport preserves that metadata, and the connection layer writes a generic notice that omits the missing path.",
"invariant": "A fresh local renderer terminal spawn may recover from a saved startup cwd whose directory no longer exists only by spawning at the selected workspace root and printing a generic in-terminal notice; existing directories — including ones outside the worktree (#7685) — spawn as requested, and reattach, SSH, remote-runtime, runtime/API, and mobile callers keep exact cwd semantics. A long-lived POSIX shell whose cwd inode was deleted and replaced may run a wrapped OMP command from the live path named by its logical PWD only in a subshell; the parent shell cwd, OMP argv, status extension, and exit status remain unchanged, while a genuinely unavailable path fails visibly without invoking OMP. A usable physical cwd remains authoritative when PWD is unset or names a removed logical symlink instead of being remapped to a workspace fallback.",
"oracle": "The shared resolver falls back to the workspace root only when the injected existence probe reports the resolved cwd missing and the workspace root present, and never probes floating terminals or a cwd equal to the root. The renderer sends cwdFallback only for fresh local IPC spawns, main honors it only when connectionId and sessionId are absent, WSL UNC paths never engage the probe-based fallback, main returns fallback metadata only after an actual fallback, the IPC transport preserves that metadata, and the connection layer writes a generic notice that omits the missing path. In real interactive Bash and Zsh PTYs, unset PWD and remove a logical symlink while the physical cwd stays usable, requiring OMP to remain there; then delete and recreate the active project path, require extension-enabled and extension-skipping invocations to observe the replacement inode with byte-exact argv and nonzero status preserved, require the parent shell to remain on the stale inode, unset PWD and require recovery through ORCA_WORKTREE_PATH, clear every logical fallback and require an actionable failure without invoking OMP, and finally delete the replacement and require the same for the missing PWD path.",
"commands": [
"pnpm exec vitest run --config config/vitest.config.ts src/shared/terminal-startup-cwd.test.ts",
"pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/pty-spawn-cwd-fallback.test.ts src/main/ipc/pty-wsl-cwd-validation.test.ts",
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/pty-transport-connect-spawn.test.ts",
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/pty-connection-runtime-owner-spawn-routing.test.ts"
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/pty-connection-runtime-owner-spawn-routing.test.ts",
"pnpm exec vitest run --config config/vitest.config.ts src/main/pty/omp-shell-wrapper.node-pty.test.ts src/main/pty/omp-shell-wrapper-alias-safety.test.ts src/main/shell-wrapper-generated-file-snapshot.test.ts"
],
"testFiles": [
"src/shared/terminal-startup-cwd.test.ts",
"src/main/ipc/pty-spawn-cwd-fallback.test.ts",
"src/main/ipc/pty-wsl-cwd-validation.test.ts",
"src/renderer/src/components/terminal-pane/pty-transport-connect-spawn.test.ts",
"src/renderer/src/components/terminal-pane/pty-connection-runtime-owner-spawn-routing.test.ts"
"src/renderer/src/components/terminal-pane/pty-connection-runtime-owner-spawn-routing.test.ts",
"src/main/pty/omp-shell-wrapper.node-pty.test.ts",
"src/main/pty/omp-shell-wrapper-alias-safety.test.ts",
"src/main/shell-wrapper-generated-file-snapshot.test.ts"
],
"assertionRefs": [
{
@@ -6392,6 +6404,25 @@
"startup cwd fallback metadata prints a generic in-terminal notice",
"remote-runtime worktree spawns are not marked with cwdFallback"
]
},
{
"file": "src/main/pty/omp-shell-wrapper.node-pty.test.ts",
"assertions": [
"real Bash and Zsh PTYs rebind OMP to a recreated cwd inode without changing the parent shell",
"an unset PWD with a usable cwd stays unset and does not remap OMP to ORCA_WORKTREE_PATH",
"a removed logical symlink does not invalidate its still-usable physical cwd",
"extension-skipping commands such as omp --version still recover the cwd without gaining extension argv",
"an unset PWD on a stale inode recovers through ORCA_WORKTREE_PATH",
"an unset PWD on a stale inode without a logical fallback fails visibly before invoking OMP",
"extension argv and a nonzero OMP exit status survive the recovery subshell",
"a subsequently missing logical cwd prints an actionable error and does not invoke OMP again"
]
},
{
"file": "src/main/shell-wrapper-generated-file-snapshot.test.ts",
"assertions": [
"local, daemon/SSH, and relay Bash/Zsh files contain the byte-identical shared OMP wrapper"
]
}
],
"evidenceRuns": [
@@ -6430,11 +6461,20 @@
"result": "passed",
"durationSeconds": 6.5,
"summary": "1 test file passed, 341 tests passed; covers local IPC marking, the generic terminal fallback notice, and remote-runtime omission."
},
{
"date": "2026-08-29",
"runner": "local",
"platform": "macos",
"command": "pnpm exec vitest run --config config/vitest.config.ts src/main/pty/omp-shell-wrapper.node-pty.test.ts src/main/pty/omp-shell-wrapper-alias-safety.test.ts src/main/shell-wrapper-generated-file-snapshot.test.ts",
"result": "passed",
"durationSeconds": 3,
"summary": "3 test files passed; real Bash and Zsh PTYs covered usable physical cwd authority, stale-inode recovery with set and unset PWD, extension-enabled and extension-skipping argv, parent-shell isolation, exit status, missing-path visibility, alias safety, and generated local/daemon/relay parity."
}
],
"runtimeBudget": {
"p95Seconds": 30,
"scope": "focused unit and IPC contract tests"
"p95Seconds": 40,
"scope": "focused unit, IPC, generated-wrapper, and real Bash/Zsh PTY contracts"
},
"flakeHistory": {
"status": "unknown",
@@ -6442,23 +6482,25 @@
},
"redGreenEvidence": {
"status": "partial",
"evidence": "The main IPC missing-cwd tests fail with the provider's 'Working directory ... does not exist.' error when the fallback is removed and pass with it. Full live Electron reproduction from a production persisted session is not captured."
"evidence": "The main IPC missing-cwd tests fail with the provider's 'Working directory ... does not exist.' error when the spawn fallback is removed and pass with it. Before the OMP recovery, the stale-inode reproduction makes the child process fail to resolve its cwd; the candidate rebinds to the recreated path in real Bash and Zsh PTYs. Full live Electron reproduction from a production persisted session is not captured."
},
"performanceBudget": {
"required": true,
"evidence": "The runtime change adds at most two statSync probes on the fresh-local spawn path (the provider already stats the same paths during validation) and one bounded terminal write only when fallback actually occurs; no polling, provider listing, hidden-pane work, startup awaits, subprocesses, or render-loop work was added."
"evidence": "The spawn recovery adds at most two statSync probes on the fresh-local spawn path (the provider already stats the same paths during validation) and one bounded terminal write only when fallback actually occurs. Each wrapped OMP launch adds a constant number of shell filesystem predicates; only a detected stale/unusable cwd creates one short recovery subshell. No polling, retry, provider listing, hidden-pane work, startup awaits, extra OMP subprocesses, or render-loop work was added."
},
"promotionCriteria": [
"Attach CI evidence for all declared test files.",
"Add a live Electron regression that opens a local terminal whose persisted startupCwd was deleted and proves visible shell input/output at the workspace root.",
"Add WSL/mobile/API provider-contract coverage or explicitly narrow their risk scope."
"Add a live stale-inode OMP run on Linux or WSL and through an SSH/relay-generated wrapper.",
"Add WSL/mobile/API spawn provider-contract coverage or explicitly narrow their risk scope."
],
"knownGaps": [
"No live Electron fixture seeds a persisted tab whose startupCwd directory was deleted.",
"Daemon coverage is via the shared pre-provider main cwd decision, not a live daemon provider spawn.",
"WSL UNC paths bypass the probe by design and have no live existence-recovery run; Linux, Windows, and mobile/API strictness are not directly exercised."
"The stale-inode OMP oracle is local macOS Bash/Zsh plus shared generated-file parity; it does not execute through live SSH, relay, Linux, or WSL providers.",
"WSL UNC paths bypass the spawn probe by design and have no live existence-recovery run; Linux, Windows, and mobile/API spawn strictness are not directly exercised."
],
"demotionRule": "Demote or quarantine if the gate flakes without a product bug, if an existing directory is ever remapped away from the requested cwd, or if a reattach/remote/API caller can engage the fallback."
"demotionRule": "Demote or quarantine if the gate flakes without a product bug, if an existing directory is ever remapped away from the requested cwd, if a reattach/remote/API caller can engage the spawn fallback, or if OMP recovery remaps a usable cwd with unset PWD, changes argv/status/parent cwd, or invokes OMP when the logical path remains unavailable."
},
{
"id": "agent-status.pi-hook-liveness",
@@ -37,4 +37,16 @@ describe('client-hosted browser package coverage', () => {
expect(windowsStep.run).toContain(file)
}
})
// Why pinned: each of those files launches a full Electron stack twice under its own in-process
// deadline. Letting the runner interleave four of them starved the probes past those deadlines,
// which is the only way this step has ever failed.
it('gives each Linux Electron probe the runner to itself', () => {
const parsedWorkflow = parse(readFileSync(join(projectDir, '.github/workflows/pr.yml'), 'utf8'))
const linuxStep = parsedWorkflow.jobs.package.steps.find(
(step) => step.name === 'Test Linux Electron lifecycle boundary'
)
expect(linuxStep.run).toContain('--no-file-parallelism')
})
})
+39 -18
View File
@@ -12,7 +12,7 @@ describe('computer-use e2e workflow', () => {
expect(config).toContain('fileParallelism: false')
})
it('guards e2e source against fragile fixed waits and stale element indexes', () => {
it('guards e2e source against fragile waits and Windows Calculator drift', () => {
const driver = readFileSync(join(projectDir, 'tests/e2e/helpers/computer-driver.ts'), 'utf8')
const cliDriver = readFileSync(
join(projectDir, 'tests/e2e/helpers/computer-cli-driver.ts'),
@@ -31,11 +31,14 @@ describe('computer-use e2e workflow', () => {
expect(cliDriver).toContain('Could not read Orca runtime metadata')
expect(cliDriver).toContain("'serve', '--no-pairing', '--json'")
expect(windowsStoreE2e).toMatch(
/for \(const buttonName of \['One', 'Plus', 'Two', 'Equals'\]\) \{[\s\S]*findRoleIndex\(state\.result\.snapshot\.treeText, `button \$\{buttonName\}`\)[\s\S]*state = parseJsonOutput/
expect(windowsStoreE2e).toContain("app.bundleId === 'ApplicationFrameHost'")
expect(windowsStoreE2e).toContain("app.bundleId === 'win32calc'")
expect(windowsStoreE2e).toContain('buttonIndex >= 0')
expect(windowsStoreE2e).toContain('pane(?:\\s|$)/m')
expect(windowsStoreE2e).toContain('String(clickIndex)')
expect(windowsStoreE2e).not.toContain(
"for (const buttonName of ['One', 'Plus', 'Two', 'Equals'])"
)
expect(windowsStoreE2e).not.toMatch(/const one = findRoleIndex/)
expect(windowsStoreE2e).not.toMatch(/for \(const index of \[one, plus, two, equals\]\)/)
})
it('triggers on computer-use shared contracts, scripts, and agent skill changes', () => {
@@ -118,7 +121,7 @@ describe('computer-use e2e workflow', () => {
}
})
it('builds and tests the macOS helper on pull requests without TCC e2e', () => {
it('builds and tests the macOS helper on every trigger without hosted TCC e2e', () => {
const workflow = parse(
readFileSync(join(projectDir, '.github/workflows/computer-e2e.yml'), 'utf8')
)
@@ -129,7 +132,7 @@ describe('computer-use e2e workflow', () => {
(step) => step.uses === './.github/actions/install-node-dependencies'
)
expect(job.if).toBe("github.event_name == 'pull_request'")
expect(job.if).toBeUndefined()
expect(job['runs-on']).toBe('macos-15')
expect(checkout.with['persist-credentials']).toBe(false)
expect(install.with['native-runtime']).toBe('electron')
@@ -142,6 +145,7 @@ describe('computer-use e2e workflow', () => {
)
expect(runs).toContain('pnpm verify:computer-native')
expect(runs.join('\n')).not.toContain('test:e2e:computer')
expect(workflow.jobs.mac).toBeUndefined()
expect(workflow.on.pull_request.paths).toEqual(
expect.arrayContaining([
'config/scripts/macos-computer-helper-owner-loss-benchmark.mjs',
@@ -154,6 +158,29 @@ describe('computer-use e2e workflow', () => {
)
})
it('uses the cached Electron dependency path for scheduled Linux and Windows e2e', () => {
const workflow = parse(
readFileSync(join(projectDir, '.github/workflows/computer-e2e.yml'), 'utf8')
)
for (const jobName of ['linux', 'windows']) {
const job = workflow.jobs[jobName]
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(checkout.with['persist-credentials'], jobName).toBe(false)
expect(install.with['native-runtime'], jobName).toBe('electron')
expect(
job.steps.some((step) => step.uses === 'pnpm/setup@v2'),
jobName
).toBe(false)
expect(
job.steps.some((step) => step.run === 'pnpm install --frozen-lockfile'),
jobName
).toBe(false)
}
})
it('runs deterministic macOS owner-loss benchmark cleanup coverage', () => {
const benchmark = readFileSync(
join(projectDir, 'config/scripts/macos-computer-helper-owner-loss-benchmark.mjs'),
@@ -244,7 +271,7 @@ describe('computer-use e2e workflow', () => {
readFileSync(join(projectDir, '.github/workflows/computer-e2e.yml'), 'utf8')
)
for (const jobName of ['native-smoke', 'mac', 'linux', 'windows']) {
for (const jobName of ['native-smoke', 'linux', 'windows']) {
const runs = workflow.jobs[jobName].steps
.map((step) => step.run)
.filter((run) => typeof run === 'string')
@@ -274,7 +301,6 @@ describe('computer-use e2e workflow', () => {
.filter((run) => typeof run === 'string')
const allRuns = [
...nativeSmokeRuns,
...workflow.jobs.mac.steps.map((step) => step.run).filter((run) => typeof run === 'string'),
...workflow.jobs.linux.steps.map((step) => step.run).filter((run) => typeof run === 'string'),
...workflow.jobs.windows.steps
.map((step) => step.run)
@@ -286,30 +312,25 @@ describe('computer-use e2e workflow', () => {
expect(allRuns.join('\n')).not.toContain('test:e2e:computer -- --reporter')
})
it('runs macOS and Linux computer-use e2e files in scheduled jobs', () => {
it('runs Linux e2e on schedule without advertising hosted macOS TCC coverage', () => {
const workflow = parse(
readFileSync(join(projectDir, '.github/workflows/computer-e2e.yml'), 'utf8')
)
const triggerPaths = workflow.on.pull_request.paths
const macRuns = workflow.jobs.mac.steps
.map((step) => step.run)
.filter((run) => typeof run === 'string')
const linuxRuns = workflow.jobs.linux.steps
.map((step) => step.run)
.filter((run) => typeof run === 'string')
expect(triggerPaths).toEqual(
expect.arrayContaining([
'tests/e2e/computer-mac.e2e.ts',
'tests/e2e/computer-mac-safari.e2e.ts',
'tests/e2e/computer-linux.e2e.ts',
'tests/e2e/helpers/computer-cli-driver.ts',
'tests/e2e/helpers/computer-driver.ts'
])
)
expect(macRuns).toContain(
'pnpm test:e2e:computer --reporter=verbose tests/e2e/computer-mac.e2e.ts tests/e2e/computer-mac-safari.e2e.ts'
)
expect(triggerPaths).not.toContain('tests/e2e/computer-mac.e2e.ts')
expect(triggerPaths).not.toContain('tests/e2e/computer-mac-safari.e2e.ts')
expect(workflow.jobs.mac).toBeUndefined()
expect(linuxRuns).toContain(
'xvfb-run --auto-servernum dbus-run-session -- pnpm test:e2e:computer --reporter=verbose tests/e2e/computer-linux.e2e.ts'
)
+2 -1
View File
@@ -377,7 +377,8 @@ function rebuildNodeRuntimeModules(moduleNames) {
}
function runPnpm(args, { cwd = projectDir } = {}) {
const command = process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm'
// cmd.exe resolves both Corepack's pnpm.cmd and pnpm 12's native pnpm.exe.
const command = 'pnpm'
const env =
process.platform === 'linux' && args.includes('node-gyp')
? { ...process.env, CXXFLAGS: `${process.env.CXXFLAGS ?? ''} -std=gnu++2a`.trim() }
@@ -24,10 +24,15 @@ function createFixture() {
for (const directory of [workspace, detachedCwd]) {
writeFileSync(join(directory, 'package.json'), '{"name":"fixture"}\n')
writeFileSync(join(directory, 'pnpm-lock.yaml'), 'lockfileVersion: 9\n')
writeFileSync(join(directory, 'pnpm-workspace.yaml'), 'packages: []\n')
}
expect(run('git', ['init', '-q'], { cwd: workspace }).status).toBe(0)
expect(run('git', ['add', 'package.json', 'pnpm-lock.yaml'], { cwd: workspace }).status).toBe(0)
expect(
run('git', ['add', 'package.json', 'pnpm-lock.yaml', 'pnpm-workspace.yaml'], {
cwd: workspace
}).status
).toBe(0)
const pnpm = join(bin, 'pnpm')
writeFileSync(pnpm, '#!/bin/sh\nexit 0\n')
@@ -76,7 +81,8 @@ describe('install-node-dependencies action', () => {
it.each([
['package.json', '{"name":"changed"}\n'],
['pnpm-lock.yaml', 'lockfileVersion: 9\nchanged: true\n']
['pnpm-lock.yaml', 'lockfileVersion: 9\nchanged: true\n'],
['pnpm-workspace.yaml', 'packages: []\nchanged: true\n']
])('rejects a changed %s when the composite step cwd is detached', (file, contents) => {
const fixture = createFixture()
try {
@@ -5032,5 +5032,50 @@
},
"auto.components.right.sidebar.AiVaultPanel.noAgentsSelected": {
"ko": "선택된 에이전트가 없습니다"
},
"dashboardPopout.title": {
"ko": "에이전트"
},
"dashboardPopout.total": {
"ko": "총 {{count}}개"
},
"dashboardPopout.close": {
"ko": "대시보드 닫기"
},
"dashboardPopout.placeholder.title": {
"ko": "에이전트 대시보드"
},
"dashboardPopout.placeholder.description": {
"ko": "모든 에이전트를 한눈에 볼 수 있는 곳입니다. 보드는 곧 제공됩니다."
},
"dashboardPopout.recoverableError.title": {
"ko": "Orca 대시보드에서 오류가 발생했습니다."
},
"dashboardPopout.recoverableError.description": {
"ko": "대시보드 렌더링을 완료하지 못했습니다. 다시 시도해 다시 마운트하거나 다시 열어 보세요."
},
"dashboardPopout.bucket.attention": {
"ko": "확인 필요"
},
"dashboardPopout.bucket.working": {
"ko": "작업 중"
},
"dashboardPopout.bucket.idle": {
"ko": "유휴"
},
"dashboardPopout.bucket.empty": {
"ko": "없음"
},
"dashboardPopout.card.you": {
"ko": "나"
},
"dashboardPopout.terminal.closed": {
"ko": "실시간 터미널이 없습니다 — 이 에이전트의 창이 닫혔습니다."
},
"dashboardPopout.terminal.focusWorktree": {
"ko": "워크트리 열기"
},
"dashboardPopout.terminal.close": {
"ko": "닫기"
}
}
@@ -8,14 +8,13 @@ import { relayArtifactFilenames } from '../../src/shared/relay-artifacts.ts'
const projectDir = resolve(import.meta.dirname, '../..')
const require = createRequire(import.meta.url)
const { createPackagedRuntimeNodeModuleResources } = require('../packaged-runtime-node-modules.cjs')
const packageJson = JSON.parse(readFileSync(join(projectDir, 'package.json'), 'utf8'))
const readProject = (file) => readFileSync(join(projectDir, file), 'utf8')
const packageJson = JSON.parse(readProject('package.json'))
const pnpmWorkspace = parse(readProject('pnpm-workspace.yaml'))
describe('Electron runtime package contract', () => {
it('keeps shared WebGL atlas invalidation reproducible from vendored source', () => {
const patch = readFileSync(
join(projectDir, 'config/patches/@xterm__addon-webgl@0.20.0-beta.286.patch'),
'utf8'
)
const patch = readProject('config/patches/@xterm__addon-webgl@0.20.0-beta.286.patch')
expect(patch).toContain('readonly clearModelGeneration: number')
expect(patch).toContain('const generation = this._atlas.clearModelGeneration')
@@ -26,7 +25,7 @@ describe('Electron runtime package contract', () => {
it('keeps root postinstall as the single Electron binary install owner', () => {
expect(packageJson.scripts.postinstall).toBe('node config/scripts/rebuild-native-deps.mjs')
expect(packageJson.pnpm.onlyBuiltDependencies).not.toContain('electron')
expect(pnpmWorkspace.allowBuilds).not.toHaveProperty('electron')
})
it('keeps the native Windows registry addon optional and platform-gated', () => {
@@ -41,7 +40,7 @@ describe('Electron runtime package contract', () => {
expect(packageJson.optionalDependencies['windows-native-registry']).toBe('3.2.2')
// Why: pnpm installs optional target architectures on every host; the root
// Windows-only rebuild owns this addon so macOS/Linux never run node-gyp for it.
expect(packageJson.pnpm.onlyBuiltDependencies).not.toContain('windows-native-registry')
expect(pnpmWorkspace.allowBuilds['windows-native-registry']).toBe(false)
// Why assert the guard and the member separately: the list now carries more
// than one addon, so pinning the whole literal only tested its formatting.
expect(rebuildScript).toContain("rebuildPlatform === 'win32'")
@@ -80,14 +79,14 @@ describe('Electron runtime package contract', () => {
expect(packageJson.optionalDependencies['@vscode/windows-process-tree']).toBe('0.8.0')
// Why: same rule as the registry addon -- pnpm installs optional deps on
// every host, so macOS/Linux must never run node-gyp for a Windows addon.
expect(packageJson.pnpm.onlyBuiltDependencies).not.toContain('@vscode/windows-process-tree')
expect(pnpmWorkspace.allowBuilds['@vscode/windows-process-tree']).toBe(false)
expect(rebuildScript).toContain("'@vscode/windows-process-tree'")
expect(ensureScript).toContain("'@vscode/windows-process-tree'")
// Why pin the patch: the upstream binding.gyp requires Spectre-mitigated
// libraries our build agents do not carry, and the enumeration stops after
// 1024 processes -- on a busy host that silently hides the very descendants
// teardown is looking for.
expect(packageJson.pnpm.patchedDependencies['@vscode/windows-process-tree@0.8.0']).toBe(
expect(pnpmWorkspace.patchedDependencies['@vscode/windows-process-tree@0.8.0']).toBe(
'config/patches/@vscode__windows-process-tree@0.8.0.patch'
)
const packageTargets = {
@@ -138,6 +138,12 @@ describe('PR E2E gate contract', () => {
expect(e2eWorkflow.jobs.e2e.if).toBe("inputs.test_files == ''")
expect(e2eWorkflow.jobs['changed-e2e'].if).toBe("inputs.test_files != ''")
expect(e2eWorkflow.jobs['changed-e2e'].strategy).toBeUndefined()
expect(e2eWorkflow.jobs.e2e.strategy.matrix.include).toEqual(
Array.from({ length: 14 }, (_, index) => ({
shard: `${index + 1}/14`,
shard_name: `${index + 1}-of-14`
}))
)
const changedRun = e2eWorkflow.jobs['changed-e2e'].steps.find(
(step) => step.name === 'Run changed E2E specs'
)
@@ -9,11 +9,13 @@ const dependencyAction = parse(
readFileSync('.github/actions/install-node-dependencies/action.yml', 'utf8')
)
const packageJson = JSON.parse(readFileSync('package.json', 'utf8'))
const pnpmWorkspace = parse(readFileSync('pnpm-workspace.yaml', 'utf8'))
const shellContractFiles = [
'src/main/daemon/repro-13767-shell-ready-marker-lost-to-exec.test.ts',
'src/main/daemon/shell-ready.test.ts',
'src/main/providers/local-pty-shell-ready-zsh-launch-environment.test.ts',
'src/main/providers/__tests__/shell-ready-framework-example.test.ts',
'src/main/pty/omp-shell-wrapper.node-pty.test.ts',
'src/main/shell-startup-feature-channel.test.ts',
'src/main/zsh-scoped-histfile.live-shell.test.ts',
'src/main/zsh-startup-hook-user-config-equivalence.live-shell.test.ts',
@@ -22,7 +24,6 @@ const shellContractFiles = [
]
const patchedNodePtyContractFiles = [
'src/main/daemon/node-pty-fd-leak.test.ts',
'src/main/pty/omp-shell-wrapper.node-pty.test.ts',
'src/shared/fish-query-reply-child-stdin.node-pty.test.ts'
]
const nativeShellContractFiles = [...shellContractFiles, ...patchedNodePtyContractFiles]
@@ -224,7 +225,10 @@ describe('PR workflow parallelism', () => {
expect(pnpmIndex).toBeLessThan(nodeIndex)
expect(pnpmIndex).toBeLessThan(requestedNodeIndex)
const packageManagerVersion = /^pnpm@([^+]+)/.exec(packageJson.packageManager)?.[1]
expect(steps[pnpmIndex].with.version).toBe(packageManagerVersion)
expect(packageManagerVersion).toBe('12.0.0')
expect(steps[pnpmIndex].uses).toBe('pnpm/setup@v2')
expect(steps[pnpmIndex].with.version).toBeUndefined()
expect(steps[pnpmIndex].with.install).toBe(false)
expect(steps[nodeIndex].with.cache).toBe('pnpm')
expect(steps[nodeIndex].if).toBe("inputs.node-version == ''")
expect(steps[requestedNodeIndex].if).toBe("inputs.node-version != ''")
@@ -232,6 +236,23 @@ describe('PR workflow parallelism', () => {
expect(steps[requestedNodeIndex].with.cache).toBe('pnpm')
})
it('uses the repository package-manager version for every direct pnpm setup', () => {
const directSetups = globSync('.github/workflows/*.yml').flatMap((workflowPath) => {
const parsed = parse(readFileSync(workflowPath, 'utf8'))
return Object.values(parsed.jobs ?? {}).flatMap((job) =>
(job.steps ?? [])
.filter((step) => step.uses === 'pnpm/setup@v2')
.map((step) => ({ workflowPath, step }))
)
})
expect(directSetups.length).toBeGreaterThan(0)
for (const { workflowPath, step } of directSetups) {
expect(step.with?.version, workflowPath).toBeUndefined()
expect(step.with?.install, workflowPath).toBe(false)
}
})
it('restores Electron downloads before preparing the package runtime', () => {
const steps = workflow.jobs.package.steps
const cacheIndex = steps.findIndex((step) => step.name === 'Cache electron-builder downloads')
@@ -276,15 +297,15 @@ describe('PR workflow parallelism', () => {
expect(dependencyInstall.run).toContain('--frozen-lockfile')
expect(dependencyInstall.run).not.toContain('--no-frozen-lockfile')
expect(dependencyInstall.run).toContain(
'git -C "$GITHUB_WORKSPACE" diff --exit-code -- package.json pnpm-lock.yaml'
'git -C "$GITHUB_WORKSPACE" diff --exit-code -- package.json pnpm-lock.yaml pnpm-workspace.yaml'
)
expect(dependencyInstall.run).toContain('--ignore-scripts')
expect(dependencyInstall.run).not.toContain('--os=')
expect(dependencyInstall.run).not.toContain('--cpu=')
expect(packageJson.pnpm.supportedArchitectures.os).toEqual(
expect(pnpmWorkspace.supportedArchitectures.os).toEqual(
expect.arrayContaining(['current', 'win32'])
)
expect(packageJson.pnpm.supportedArchitectures.cpu).toContain('current')
expect(pnpmWorkspace.supportedArchitectures.cpu).toContain('current')
const prepareRuntime = dependencyAction.runs.steps.find(
(step) => step.name === 'Prepare native runtime'
)
+9 -8
View File
@@ -22,13 +22,15 @@ const DEFAULT_REPO_ROOT = path.resolve(import.meta.dirname, '..', '..')
const MANIFEST_RELATIVE_PATH = path.join('config', 'patches', 'xterm-upstream.json')
/**
* Flags pnpm@10 passes to `git diff` in its own `diffFolders()`. A patch built
* Flags pnpm@12 passes to `git diff` in its own `diff_folders()`. A patch built
* with anything else is a patch pnpm may re-diff differently on the next
* `pnpm patch-commit`, so the byte-comparison gate would never settle.
*/
export const PNPM_DIFF_FLAGS = [
'-c',
'core.safecrlf=false',
'-c',
'core.quotePath=false',
'diff',
'--src-prefix=a/',
'--dst-prefix=b/',
@@ -38,7 +40,8 @@ export const PNPM_DIFF_FLAGS = [
'--no-index',
'--text',
'--no-ext-diff',
'--no-color'
'--no-color',
'--'
]
/**
@@ -47,14 +50,12 @@ export const PNPM_DIFF_FLAGS = [
*/
export const CHECKOUT_DIFF_FLAGS = PNPM_DIFF_FLAGS.filter((flag) => flag !== '--no-index')
/** Blanks the vars pnpm blanks so user and system git config cannot reach the diff. */
/** Applies pnpm's git config isolation so local machine settings cannot change the patch. */
export function pnpmDiffEnvironment(baseEnvironment = process.env) {
return {
...baseEnvironment,
GIT_CONFIG_NOSYSTEM: '1',
HOME: '',
XDG_CONFIG_HOME: '',
USERPROFILE: ''
GIT_CONFIG_GLOBAL: '/dev/null'
}
}
@@ -216,7 +217,7 @@ export function patchHash(patchText) {
function lockfilePatchHashPattern(packageKey) {
// Unscoped keys such as `node-pty@1.1.0` are emitted unquoted.
return new RegExp(`(^ '?${escapeRegExp(packageKey)}'?:\\n hash: )([0-9a-f]{64})$`, 'm')
return new RegExp(`(^ '?${escapeRegExp(packageKey)}'?: )([0-9a-f]{64})$`, 'm')
}
export function readLockfilePatchHash(lockfileText, packageKey) {
@@ -507,7 +508,7 @@ function diffFolders(folderA, folderB) {
/** The source of truth for the hand-written half: what the checkout itself holds. */
function diffCheckoutSource(packageRoot) {
return run('git', [...CHECKOUT_DIFF_FLAGS, '--', 'src/'], {
return run('git', [...CHECKOUT_DIFF_FLAGS, 'src/'], {
cwd: packageRoot,
env: pnpmDiffEnvironment(),
maxBuffer: 64 * 1024 * 1024
@@ -98,6 +98,8 @@ describe('pnpm diff format', () => {
expect(PNPM_DIFF_FLAGS).toEqual([
'-c',
'core.safecrlf=false',
'-c',
'core.quotePath=false',
'diff',
'--src-prefix=a/',
'--dst-prefix=b/',
@@ -107,18 +109,18 @@ describe('pnpm diff format', () => {
'--no-index',
'--text',
'--no-ext-diff',
'--no-color'
'--no-color',
'--'
])
})
it('blanks the config-bearing environment variables', () => {
it('matches pnpm git config isolation', () => {
const environment = pnpmDiffEnvironment({ PATH: '/usr/bin', HOME: '/Users/someone' })
expect(environment).toMatchObject({
PATH: '/usr/bin',
GIT_CONFIG_NOSYSTEM: '1',
HOME: '',
XDG_CONFIG_HOME: '',
USERPROFILE: ''
GIT_CONFIG_GLOBAL: '/dev/null',
HOME: '/Users/someone'
})
})
@@ -350,12 +352,8 @@ describe('manifest guards', () => {
describe('lockfile coupling', () => {
const lockfile = [
'patchedDependencies:',
" '@xterm/xterm@6.1.0-beta.287':",
` hash: ${'0'.repeat(64)}`,
' path: config/patches/@xterm__xterm@6.1.0-beta.287.patch',
' node-pty@1.1.0:',
` hash: ${'1'.repeat(64)}`,
' path: config/patches/node-pty@1.1.0.patch',
` '@xterm/xterm@6.1.0-beta.287': ${'0'.repeat(64)}`,
` node-pty@1.1.0: ${'1'.repeat(64)}`,
'snapshots:',
` '@xterm/addon-fit@0.12.0-beta.287(@xterm/xterm@6.1.0-beta.287(patch_hash=${'0'.repeat(64)}))':`,
` '@xterm/xterm': 6.1.0-beta.287(patch_hash=${'0'.repeat(64)})`,
@@ -396,7 +394,10 @@ describe('lockfile coupling', () => {
it('reports a lockfile stale in its resolution keys alone', () => {
const key = '@xterm/xterm@6.1.0-beta.287'
const halfUpdated = lockfile.replace(`hash: ${'0'.repeat(64)}`, `hash: ${'a'.repeat(64)}`)
const halfUpdated = lockfile.replace(
`'@xterm/xterm@6.1.0-beta.287': ${'0'.repeat(64)}`,
`'@xterm/xterm@6.1.0-beta.287': ${'a'.repeat(64)}`
)
expect(readLockfilePatchHash(halfUpdated, key)).toBe('a'.repeat(64))
expect(lockfilePatchHashIsStale(halfUpdated, key, 'a'.repeat(64))).toBe(true)
@@ -34,7 +34,7 @@ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
import { join, resolve } from 'node:path'
// Why @electron/asar: canonical replacement for the deprecated `asar` package.
// It's transitively available via electron-builder (and pnpm's
// `shamefully-hoist=true` in `.npmrc` flattens it into the root
// `shamefullyHoist: true` in `pnpm-workspace.yaml` flattens it into the root
// `node_modules`). If electron-builder ever drops it, promote this to a
// direct devDependency in package.json.
import { extractFile, listPackage } from '@electron/asar'
@@ -1,6 +1,7 @@
import { createHash } from 'node:crypto'
import { readFileSync } from 'node:fs'
import { join, resolve } from 'node:path'
import { parseAllDocuments } from 'yaml'
import { describe, expect, it } from 'vitest'
const projectDir = resolve(import.meta.dirname, '../..')
@@ -16,7 +17,9 @@ describe('Windows process-tree patch contract', () => {
const patchHash = createHash('sha256')
.update(patchBytes.toString('utf8').replaceAll('\r\n', '\n'))
.digest('hex')
const lockfile = readFileSync(join(projectDir, 'pnpm-lock.yaml'), 'utf8')
expect(lockfile).toContain(`hash: ${patchHash}`)
const lockfile = parseAllDocuments(readFileSync(join(projectDir, 'pnpm-lock.yaml'), 'utf8'))
.map((document) => document.toJS())
.find((document) => document.patchedDependencies)
expect(lockfile.patchedDependencies['@vscode/windows-process-tree@0.8.0']).toBe(patchHash)
})
})
+4 -4
View File
@@ -1,5 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="106" height="20" role="img" aria-label="downloads: 32m">
<title>downloads: 32m</title>
<svg xmlns="http://www.w3.org/2000/svg" width="106" height="20" role="img" aria-label="downloads: 33m">
<title>downloads: 33m</title>
<linearGradient id="s" x2="0" y2="100%">
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
<stop offset="1" stop-opacity=".1"/>
@@ -15,7 +15,7 @@
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="11">
<text x="37" y="15" fill="#010101" fill-opacity=".3">downloads</text>
<text x="37" y="14">downloads</text>
<text x="90" y="15" fill="#010101" fill-opacity=".3">32m</text>
<text x="90" y="14">32m</text>
<text x="90" y="15" fill="#010101" fill-opacity=".3">33m</text>
<text x="90" y="14">33m</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 935 B

After

Width:  |  Height:  |  Size: 935 B

+39 -4
View File
@@ -95,16 +95,51 @@ negotiated capabilities differ from the contract. Adding an optional field keeps
green (Rule 1); making a client depend on that field turns the new-client/old-host
pairing red.
### Never write down what the old side has
The baseline is whichever release tag is newest, so it moves on every cut. An
expectation of the form "the old side does not have X" — a `not.toHaveProperty`, a
`not.toContain`, a hard-coded field list — stops being true the first time a release
ships X. The suite then reddens on whatever pull request is in flight, with no code
change anywhere, and the job trains people to ignore it. That is worse than no test,
because a rolling baseline eventually contains every additive field the wire has, and
adding one is the sanctioned way to evolve it.
Derive the expectation from the baseline that was actually checked out:
- for a published frame, pair each build against a client of its own version and
compare the skewed pairing against that same-version reference, so the expectation
is whatever that build publishes today (`publishedFieldNames` in
`tests/e2e/cross-version-wire/published-field-shape.ts`);
- for a negotiated surface, read the old build's advertised capabilities and
registered method names from its checkout, and assert they agree with each other
rather than asserting the old build lacks them;
- for a "client too old to know X", derive that client's advertised list by removing
X from the baseline's own list, so the gate stays exercised after X ships.
Name the direction in the assertion. `new client against old server` and `old client
against new server` fail for different reasons, and the host is the only side that
authors a published frame — the terminal `terminalOwner` false positive on 2026-08-29
was misread as a new client sending an unknown field when the old server was
publishing it. Two things are still safe to state literally: the current build's own
contract, and an invariant that holds for every version.
Pinning a legacy ref is the fallback when a contract genuinely needs a release from
before a feature shipped, as `cross-version-browser-placement.unit.test.ts` does with
`LEGACY_BROWSER_PLACEMENT_RELEASE_REF`. It does not rot on a cut, but it is
hand-maintained, so prefer deriving.
`tests/e2e/cross-version-wire/cross-version-agent-session-wire.unit.test.ts` pairs the
same two builds over the structured `agentSession.*` surface. Because a released build
cannot name a capability string its own source never contains, the old side's advertised
list and registered method names are read from the extracted checkout rather than
hand-written. It covers the three skews that surface can fail on:
- an old client — advertising only what the baseline build defines — is told the whole
surface does not exist and reaches no host method;
- a new client against the old dispatcher gets `method_not_found` on every method, and
can see the absence during negotiation instead of by calling;
- an old client — advertising the baseline's list minus this capability — is told the
whole surface does not exist and reaches no host method;
- a new client against the old dispatcher always gets an answer rather than silence,
and `method_not_found` for every method that release does not register, so the
absence is visible during negotiation instead of by calling;
- a cursor survives a host restart: the client's fence is refused as stale with the live
one attached, and resuming from the held cursor replays only what it missed.
@@ -206,7 +206,7 @@ The addon is Windows-only, so it follows the same contract as
`config/scripts/package-electron-runtime-contract.test.mjs`):
- an `optionalDependency`, so a macOS/Linux install tolerates its absence;
- **not** in `pnpm.onlyBuiltDependencies` — pnpm installs optional dependencies
- **not** enabled in `allowBuilds` in `pnpm-workspace.yaml` — pnpm installs optional dependencies
on every host, and macOS/Linux must never run `node-gyp` for it;
- listed in the win32 branch of `rebuild-native-deps.mjs` and
`ensure-native-runtime.mjs`;
-1
View File
@@ -1 +0,0 @@
minimum-release-age=4320
+2 -10
View File
@@ -74,20 +74,12 @@
"esbuild": "0.25.4",
"expo-module-scripts": "^55.0.2",
"happy-dom": "^20.9.0",
"oxfmt": "^0.52.0",
"oxlint": "^1.71.0",
"oxfmt": "^0.65.0",
"oxlint": "^1.80.0",
"react-test-renderer": "19.2.6",
"tsx": "^4.22.4",
"typescript": "6.0.3",
"vite": "^8.0.16",
"vitest": "^4.1.9"
},
"pnpm": {
"overrides": {
"xcode>uuid": "11.1.1"
},
"patchedDependencies": {
"react-native@0.83.9": "patches/react-native@0.83.9.patch"
}
}
}
+384 -417
View File
File diff suppressed because it is too large Load Diff
+5
View File
@@ -1,5 +1,10 @@
allowBuilds:
esbuild: true
minimumReleaseAge: 4320
overrides:
xcode>uuid: 11.1.1
patchedDependencies:
react-native@0.83.9: patches/react-native@0.83.9.patch
+4 -7
View File
@@ -60,13 +60,10 @@ export function directoryCacheFromFileList(files: LegacyMobileFileEntry[]): Dire
Array.from(childrenByDir, ([path, children]) => [
path,
{
entries: Array.from(
children,
([name, isDirectory]): MobileDirEntry => ({
name,
isDirectory
})
)
entries: Array.from(children, ([name, isDirectory]): MobileDirEntry => ({
name,
isDirectory
}))
}
])
)
@@ -72,12 +72,10 @@ function makeHarness(options?: {
} as unknown as RpcClient
const apply =
options?.apply ??
vi.fn(
(value: TestResult): SessionTabsApplyOutcome<string> => ({
accepted: true,
effectiveTabs: value.tabs
})
)
vi.fn((value: TestResult): SessionTabsApplyOutcome<string> => ({
accepted: true,
effectiveTabs: value.tabs
}))
const consumeAccepted = vi.fn()
let recoveryNeeded = false
const controller = new MobileSessionTabsStreamHealth<TestResult, string>({
@@ -306,9 +304,8 @@ describe('MobileSessionTabsStreamHealth', () => {
})
it('does not consume a rejected stream snapshot or update', () => {
const apply = vi.fn(
(value: TestResult): SessionTabsApplyOutcome<string> =>
value.type ? { accepted: false } : { accepted: true, effectiveTabs: value.tabs }
const apply = vi.fn((value: TestResult): SessionTabsApplyOutcome<string> =>
value.type ? { accepted: false } : { accepted: true, effectiveTabs: value.tabs }
)
const harness = makeHarness({ apply })
const subscription = harness.controller.beginSubscription()
@@ -39,12 +39,10 @@ type TestResult = {
}
const fetchTerminals = vi.fn(async () => {})
const applySessionTabs = vi.fn(
(value: TestResult): SessionTabsApplyOutcome<string> => ({
accepted: true,
effectiveTabs: value.tabs
})
)
const applySessionTabs = vi.fn((value: TestResult): SessionTabsApplyOutcome<string> => ({
accepted: true,
effectiveTabs: value.tabs
}))
const consumeAcceptedSessionTabs = vi.fn()
let recoveryNeeded = false
let clearRecoveryAt = Number.POSITIVE_INFINITY
@@ -4,14 +4,12 @@ import { openAuthenticatedDirectEndpoint } from './mobile-direct-endpoint-probe'
import type { ConnectionState, HostProfile, RpcResponse } from './types'
class FakeClient implements RpcClient {
readonly sendRequest = vi.fn(
async (): Promise<RpcResponse> => ({
id: 'rpc-1',
ok: true,
result: {},
_meta: { runtimeId: 'runtime-1' }
})
)
readonly sendRequest = vi.fn(async (): Promise<RpcResponse> => ({
id: 'rpc-1',
ok: true,
result: {},
_meta: { runtimeId: 'runtime-1' }
}))
readonly subscribe = vi.fn(() => () => {})
readonly updateTerminalSubscriptionViewport = vi.fn()
readonly notifyForeground = vi.fn()
@@ -41,14 +41,12 @@ vi.mock('./e2ee', () => ({
}))
class FakeSession implements RpcClient {
readonly sendRequest = vi.fn(
async (): Promise<RpcResponse> => ({
id: 'rpc-1',
ok: true,
result: {},
_meta: { runtimeId: 'runtime-1' }
})
)
readonly sendRequest = vi.fn(async (): Promise<RpcResponse> => ({
id: 'rpc-1',
ok: true,
result: {},
_meta: { runtimeId: 'runtime-1' }
}))
readonly subscribe = vi.fn(() => () => {})
readonly updateTerminalSubscriptionViewport = vi.fn()
readonly notifyForeground = vi.fn()
+1
View File
@@ -1,3 +1,4 @@
setupAgentStartupPolicy: wait-for-setup
scripts:
setup: |
node config/scripts/run-internal-dev-setup.mjs
+5 -37
View File
@@ -185,10 +185,12 @@
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
"@tiptap/extension-code-block": "^3.22.5",
"@tiptap/extension-code-block-lowlight": "^3.22.5",
"@tiptap/extension-details": "^3.22.5",
"@tiptap/extension-image": "^3.22.5",
"@tiptap/extension-link": "^3.22.5",
"@tiptap/extension-list": "^3.22.5",
"@tiptap/extension-mathematics": "3.22.5",
"@tiptap/extension-placeholder": "^3.22.5",
"@tiptap/extension-table": "3.22.4",
@@ -237,8 +239,8 @@
"marked": "^17.0.1",
"mermaid": "^11.16.1",
"monaco-editor": "^0.55.1",
"oxfmt": "^0.52.0",
"oxlint": "^1.77.0",
"oxfmt": "^0.65.0",
"oxlint": "^1.80.0",
"oxlint-plugin-react-doctor": "0.9.1",
"oxlint-tsgolint": "7.0.2001",
"pdfjs-dist": "^6.2.108",
@@ -296,41 +298,7 @@
"engines": {
"node": "24"
},
"packageManager": "pnpm@10.24.0+sha512.01ff8ae71b4419903b65c60fb2dc9d34cf8bb6e06d03bde112ef38f7a34d6904c424ba66bea5cdcf12890230bf39f9580473140ed9c946fef328b6e5238a345a",
"pnpm": {
"overrides": {
"monaco-editor>dompurify": "3.4.13"
},
"supportedArchitectures": {
"os": [
"current",
"darwin",
"linux",
"win32"
],
"cpu": [
"current",
"x64",
"arm64"
]
},
"onlyBuiltDependencies": [
"@parcel/watcher",
"cpu-features",
"esbuild",
"node-pty",
"sherpa-onnx"
],
"patchedDependencies": {
"node-pty@1.1.0": "config/patches/node-pty@1.1.0.patch",
"@xterm/addon-ligatures@0.11.0-beta.287": "config/patches/@xterm__addon-ligatures@0.11.0-beta.287.patch",
"@xterm/addon-webgl@0.20.0-beta.286": "config/patches/@xterm__addon-webgl@0.20.0-beta.286.patch",
"@xterm/addon-serialize@0.15.0-beta.287": "config/patches/@xterm__addon-serialize@0.15.0-beta.287.patch",
"@xterm/xterm@6.1.0-beta.287": "config/patches/@xterm__xterm@6.1.0-beta.287.patch",
"lint-staged@16.4.0": "config/patches/lint-staged@16.4.0.patch",
"@vscode/windows-process-tree@0.8.0": "config/patches/@vscode__windows-process-tree@0.8.0.patch"
}
},
"packageManager": "pnpm@12.0.0+sha512.9e2e3dc3911995868dc94b8175c217c27e95408fa03b4a22749778f2b34f773b77cdd3b39ede8171b22fcd53be6a35342e9fac9948a68ef58df6488ce89a7e67",
"reactDoctor": {
"rules": {
"react-doctor/js-combine-iterations": "off"
+281 -187
View File
@@ -1,3 +1,104 @@
---
lockfileVersion: '9.0'
importers:
.:
configDependencies: {}
packageManagerDependencies:
pnpm:
specifier: 12.0.0
version: 12.0.0
packages:
'@pnpm/exe.darwin-arm64@12.0.0':
resolution: {integrity: sha512-sqeoPfVMIfQhbwzDrKraXY2ynyuWClFqzvfImzAS/yczEru1m5SGvQ9kgFPDvQzJZ9AetedgJeDZC6qYvH8/tQ==}
cpu: [arm64]
os: [darwin]
'@pnpm/exe.darwin-x64@12.0.0':
resolution: {integrity: sha512-Quc3J6c9cGTy+LDgz1cLVgCNOU9IERuyAlDoEj0DCilKqvo50Jx1GV8k74iwn4J9fFSKkm8JrwNvtTDj3uWnUA==}
cpu: [x64]
os: [darwin]
'@pnpm/exe.linux-arm64-musl@12.0.0':
resolution: {integrity: sha512-EVWd3OTmgsMFhXx69b5JxIzoabG9Ma7m4OeTaf0ZKBzMnfYi8u21NDQo92ToMrdYL5dYDDCHsyYIjXzk+d0HhA==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@pnpm/exe.linux-arm64@12.0.0':
resolution: {integrity: sha512-cXHHW8M4rAPsYNkKZO9WVcpLLK55i9EaIsZPfIqUuY2eopd5LqnFyBge54HCh1GC0yCX8ySn0hYIi+4OyAEoDg==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@pnpm/exe.linux-x64-musl@12.0.0':
resolution: {integrity: sha512-UcXwMdFjly0mpddkGigHKTxe27IMv2fUK4IWW/MHmJ3yMguxXmkwNlEI4aE+G1HO2TLo20uNEUWD4ymLe/DaCQ==}
cpu: [x64]
os: [linux]
libc: [musl]
'@pnpm/exe.linux-x64@12.0.0':
resolution: {integrity: sha512-6Rsl+zEWMOmus7v7/9J3OE8EMvHyNAfxYmDfmhQG4J0985OuT3G3Ho9NSGHjkBn4aU4bgklWifRhe1HX8dUSyw==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@pnpm/exe.win32-arm64@12.0.0':
resolution: {integrity: sha512-O5F76A4oVFrpDGdFxEszRIThOSBfjHdH5c006gR+7UTCfiXrukr1XfqPungUI1DXcSR5gb9jBsPQqQZOAoOOxw==}
cpu: [arm64]
os: [win32]
'@pnpm/exe.win32-x64@12.0.0':
resolution: {integrity: sha512-5dKFajIEWJ1ai+KHXFJvskY6vchbunmLwSUV2ywbLymcmJjfY5XJVpgzPCyIoVCMVG0zHorr66+hM8h8b3aRfQ==}
cpu: [x64]
os: [win32]
pnpm@12.0.0:
resolution: {integrity: sha512-ni49w5EZlYaNyUuBdcIXwn6VQI+gO0oidJd48rNPdzt3zdOznt6BcbIvzVO+ajU0Lp+smUimjvWN9kiM6Jp+Zw==}
engines: {node: '>=18.*'}
hasBin: true
snapshots:
'@pnpm/exe.darwin-arm64@12.0.0':
optional: true
'@pnpm/exe.darwin-x64@12.0.0':
optional: true
'@pnpm/exe.linux-arm64-musl@12.0.0':
optional: true
'@pnpm/exe.linux-arm64@12.0.0':
optional: true
'@pnpm/exe.linux-x64-musl@12.0.0':
optional: true
'@pnpm/exe.linux-x64@12.0.0':
optional: true
'@pnpm/exe.win32-arm64@12.0.0':
optional: true
'@pnpm/exe.win32-x64@12.0.0':
optional: true
pnpm@12.0.0:
optionalDependencies:
'@pnpm/exe.darwin-arm64': 12.0.0
'@pnpm/exe.darwin-x64': 12.0.0
'@pnpm/exe.linux-arm64': 12.0.0
'@pnpm/exe.linux-arm64-musl': 12.0.0
'@pnpm/exe.linux-x64': 12.0.0
'@pnpm/exe.linux-x64-musl': 12.0.0
'@pnpm/exe.win32-arm64': 12.0.0
'@pnpm/exe.win32-x64': 12.0.0
---
lockfileVersion: '9.0'
settings:
@@ -8,27 +109,13 @@ overrides:
monaco-editor>dompurify: 3.4.13
patchedDependencies:
'@vscode/windows-process-tree@0.8.0':
hash: 946ffb2b1026bcb554d9920681fb00f546c28fc5de6d95a877d198b49bf73988
path: config/patches/@vscode__windows-process-tree@0.8.0.patch
'@xterm/addon-ligatures@0.11.0-beta.287':
hash: 47405b9994b5acf1b4e90b49250358c1ca03649854d59560e7732b72fe336920
path: config/patches/@xterm__addon-ligatures@0.11.0-beta.287.patch
'@xterm/addon-serialize@0.15.0-beta.287':
hash: af3b156143ed2ee9903b753145b63dd4a2ee72cadd7ef333ab0f4d5d3ebfc32c
path: config/patches/@xterm__addon-serialize@0.15.0-beta.287.patch
'@xterm/addon-webgl@0.20.0-beta.286':
hash: 118f649459ba56a51627d46603e2d5d81d837ac9c4dc393f5cc20263d0ac1b91
path: config/patches/@xterm__addon-webgl@0.20.0-beta.286.patch
'@xterm/xterm@6.1.0-beta.287':
hash: 46796c152f3b73e28238f44499eaf5a867a863809bc7b470b159526a41e354f7
path: config/patches/@xterm__xterm@6.1.0-beta.287.patch
lint-staged@16.4.0:
hash: 7333b3837f80a7fbd045964db6d76ba4fc118e49134bdbabb00585b6b7b60673
path: config/patches/lint-staged@16.4.0.patch
node-pty@1.1.0:
hash: 9a2eedbf2448b8ff1387a8a740ee5e4e968bf8484d7d80e12a3f6a2dc26b0e17
path: config/patches/node-pty@1.1.0.patch
'@vscode/windows-process-tree@0.8.0': 9217ef36c01ed74127fef5512b0c92089cdbf820fd6c109dd671137eebdc7585
'@xterm/addon-ligatures@0.11.0-beta.287': 47405b9994b5acf1b4e90b49250358c1ca03649854d59560e7732b72fe336920
'@xterm/addon-serialize@0.15.0-beta.287': af3b156143ed2ee9903b753145b63dd4a2ee72cadd7ef333ab0f4d5d3ebfc32c
'@xterm/addon-webgl@0.20.0-beta.286': 2c301a06ad9caa635d746147dcb2b1c69aa026904f65e1183564f08a0e601b91
'@xterm/xterm@6.1.0-beta.287': 46796c152f3b73e28238f44499eaf5a867a863809bc7b470b159526a41e354f7
lint-staged@16.4.0: 7333b3837f80a7fbd045964db6d76ba4fc118e49134bdbabb00585b6b7b60673
node-pty@1.1.0: 9a2eedbf2448b8ff1387a8a740ee5e4e968bf8484d7d80e12a3f6a2dc26b0e17
importers:
@@ -146,6 +233,9 @@ importers:
'@testing-library/user-event':
specifier: ^14.6.1
version: 14.6.1(@testing-library/dom@10.4.1)
'@tiptap/extension-code-block':
specifier: ^3.22.5
version: 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5)
'@tiptap/extension-code-block-lowlight':
specifier: ^3.22.5
version: 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/extension-code-block@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5)(highlight.js@11.11.1)(lowlight@3.3.0)
@@ -158,6 +248,9 @@ importers:
'@tiptap/extension-link':
specifier: ^3.22.5
version: 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5)
'@tiptap/extension-list':
specifier: ^3.22.5
version: 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5)
'@tiptap/extension-mathematics':
specifier: 3.22.5
version: 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5)(katex@0.16.45)
@@ -235,7 +328,7 @@ importers:
version: 0.13.0-beta.287(@xterm/xterm@6.1.0-beta.287(patch_hash=46796c152f3b73e28238f44499eaf5a867a863809bc7b470b159526a41e354f7))
'@xterm/addon-webgl':
specifier: 0.20.0-beta.286
version: 0.20.0-beta.286(patch_hash=118f649459ba56a51627d46603e2d5d81d837ac9c4dc393f5cc20263d0ac1b91)(@xterm/xterm@6.1.0-beta.287(patch_hash=46796c152f3b73e28238f44499eaf5a867a863809bc7b470b159526a41e354f7))
version: 0.20.0-beta.286(patch_hash=2c301a06ad9caa635d746147dcb2b1c69aa026904f65e1183564f08a0e601b91)(@xterm/xterm@6.1.0-beta.287(patch_hash=46796c152f3b73e28238f44499eaf5a867a863809bc7b470b159526a41e354f7))
'@xterm/xterm':
specifier: 6.1.0-beta.287
version: 6.1.0-beta.287(patch_hash=46796c152f3b73e28238f44499eaf5a867a863809bc7b470b159526a41e354f7)
@@ -303,11 +396,11 @@ importers:
specifier: ^0.55.1
version: 0.55.1
oxfmt:
specifier: ^0.52.0
version: 0.52.0
specifier: ^0.65.0
version: 0.65.0
oxlint:
specifier: ^1.77.0
version: 1.77.0(oxlint-tsgolint@7.0.2001)
specifier: ^1.80.0
version: 1.80.0(oxlint-tsgolint@7.0.2001)
oxlint-plugin-react-doctor:
specifier: 0.9.1
version: 0.9.1
@@ -413,7 +506,7 @@ importers:
optionalDependencies:
'@vscode/windows-process-tree':
specifier: 0.8.0
version: 0.8.0(patch_hash=946ffb2b1026bcb554d9920681fb00f546c28fc5de6d95a877d198b49bf73988)
version: 0.8.0(patch_hash=9217ef36c01ed74127fef5512b0c92089cdbf820fd6c109dd671137eebdc7585)
sherpa-onnx-darwin-arm64:
specifier: 1.12.37
version: 1.12.37
@@ -1351,124 +1444,124 @@ packages:
'@oxc-project/types@0.141.0':
resolution: {integrity: sha512-S4as7z0j0xQkXcJlyY5ehntwK8/wRkQb9Cyqw+J/N2rkWGQGK0SxD6X6DhQTc7qsxVTBxXbxZtBJh3mr3PtIzQ==}
'@oxfmt/binding-android-arm-eabi@0.52.0':
resolution: {integrity: sha512-17EMSJnQ9g+upVHrAUYDMfH5lvRKQ9Nvg8WtEoH72oDr1VpWz+7/o3tD97U1EToen2YAQ/68JmtDYkQUi20dfQ==}
'@oxfmt/binding-android-arm-eabi@0.65.0':
resolution: {integrity: sha512-M10Gs1SSpTNI6ahGx3M/OlIdUF4hkaP6OgUb+MS79t/Pgflk3r1nW5gPFqsZGUAXg0H1AfANT9AvLdBSTIhZKg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [android]
'@oxfmt/binding-android-arm64@0.52.0':
resolution: {integrity: sha512-A2G1IdwGEW2lLJkIxcvuirRH1CzSl/e0NX11zTlW1gvxJThfwbI/BEoaKrTNpm7M2FchvIf6guvIQU7d5iz+OQ==}
'@oxfmt/binding-android-arm64@0.65.0':
resolution: {integrity: sha512-6DXH5sftNlaHpWJG50hFMF+Qxtq5D2TmahvcDPxWNcGIf8qrC9Y0YgHYcYZ2hlWzaccKXh/f3GcssH8vtkl4JA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [android]
'@oxfmt/binding-darwin-arm64@0.52.0':
resolution: {integrity: sha512-f9+bLvOYxy7NttCLFTvQ7afmqDOWY4wIP9xdvfj5trQ1qj6f2UFAGwZESlfsMjvJNTyRpXfIlOanCI9FOvoeQA==}
'@oxfmt/binding-darwin-arm64@0.65.0':
resolution: {integrity: sha512-K9m7lr53pcOLETNsC88sWes/GWHUGjZyHx95UhYcSXy0r30haLdeXlSufSenEAtoLaW753WN8/l4M7GYcRt6cg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [darwin]
'@oxfmt/binding-darwin-x64@0.52.0':
resolution: {integrity: sha512-YSTB9sJ5nnQd/Q0ddHkgof0ZCHPAnWZT1IW2SJ8omz7CP7KluJhO1fNHrpqdxCtpztJwSs4hY1uAee35wKxxaw==}
'@oxfmt/binding-darwin-x64@0.65.0':
resolution: {integrity: sha512-sTNwIx1gre3MyiHOPLu7IGW4UyMScYL4DTmJT01p4vzB0En+OJUQz6KuH8t0PpsClRSaMuY3b0QmtoPItfO8Lg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [darwin]
'@oxfmt/binding-freebsd-x64@0.52.0':
resolution: {integrity: sha512-NIrRNTTPCs4UbmVs0bxLSCDlLCtIRMJIXklNKaXa5Oj2/K1UIMBvgE8+uPVo01Io3N9HF0+GAX+aAHjUgZS7vA==}
'@oxfmt/binding-freebsd-x64@0.65.0':
resolution: {integrity: sha512-lYZMVIiIpnjGu5hJb2jxA8NYQ/e0OTGuaiAf4dqlGPNnPmUTu23FZRMltmjro/KkQm1uE4NT4n5yJ2zWmKcpfA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [freebsd]
'@oxfmt/binding-linux-arm-gnueabihf@0.52.0':
resolution: {integrity: sha512-JXUCde8mn3GpgQouz2PXUokgy/uT1QrRJBL2s983VWcSQp62wTFYiNXgTKdeo1Jgbr0IgUnKKvzIk/YBlj/nVQ==}
'@oxfmt/binding-linux-arm-gnueabihf@0.65.0':
resolution: {integrity: sha512-gIdXFAt/bURnjxuoedDEWdZ0PEWEmdDcm8qdpoFYYvW3QMk/5D4vUaH4mlMeRpeTdST4izUgHVO6RawQ4QulJw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [linux]
'@oxfmt/binding-linux-arm-musleabihf@0.52.0':
resolution: {integrity: sha512-psbUXaRZ+V8DaXz10Qf7LSHtdtdKAmC8fxXgeU608jjzrmWK4quamZMOpl6sf+dikoFHA85uE93Q0BqxrCdQrQ==}
'@oxfmt/binding-linux-arm-musleabihf@0.65.0':
resolution: {integrity: sha512-jJVyADto7gA2AaX5qAjAexrxx9PJQaKWOe8PICE7yKMbjBRyOHcmj9TtVJ+MZYDUQ3hodU0AcoTj0jFQ1W4C6Q==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [linux]
'@oxfmt/binding-linux-arm64-gnu@0.52.0':
resolution: {integrity: sha512-Jw7MgWUU9lcLCcy82updISP3EthTlfvAwR6gWNxPzqly7+fLvOi2gHQE9xXQjpqaVLm/8P+gOzlv9ODuoVlaaw==}
'@oxfmt/binding-linux-arm64-gnu@0.65.0':
resolution: {integrity: sha512-p3RFkB+u7u+8up99b/NEcI1hdpLDiGgJYNwDorB60n7eH+eKposAKuMBxx+NqB3b+sJP4CZmYDh9G7X62tUsKg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@oxfmt/binding-linux-arm64-musl@0.52.0':
resolution: {integrity: sha512-wZg6bLjDvh2KibyI3QFUYo8GTXneIFsd0JvehtvJiUmQ8WRPERgxd/VM4ctWb86U5FT1FkqgS8/wZKVB+AZScg==}
'@oxfmt/binding-linux-arm64-musl@0.65.0':
resolution: {integrity: sha512-5Prb0uFzJHr+OUD/qS/TmU526wD+PaHDsm3KoRiUXbMIDpTSErjeQYkK3OQeshAvD/PuLa9WGEi9WPajjdOZJg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@oxfmt/binding-linux-ppc64-gnu@0.52.0':
resolution: {integrity: sha512-IngE8uxhNvxcMrLjZNDo9xNLY7rEK33AKnaMd2B46he1e/mz2CfcW6If/U1wUjdRZddm1QzQaciqZkuMkdh1FA==}
'@oxfmt/binding-linux-ppc64-gnu@0.65.0':
resolution: {integrity: sha512-S8svxTp81obnF3admN9yd+u2rOYXtyzThLGBTg1PY6TPtGcC09BaaXLQD+TBSMa7yvqhCDZ8DFri+S/yG60qCg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@oxfmt/binding-linux-riscv64-gnu@0.52.0':
resolution: {integrity: sha512-H3+DdFMv/efN3Efmhsv18jDrpiWWqKG7wsfAlQBqAt6z/E2Bx+TwEj2Nowe51CPOWB8/mFBC2dAMSgVFLvvowA==}
'@oxfmt/binding-linux-riscv64-gnu@0.65.0':
resolution: {integrity: sha512-WtXBr75G/h2qOHy8SiGtC1R6aS3jt4mE52v1D8AtwMXIgoOmSNP9lKvbSaTRoL0e5wsMPoi6T72QWDYPu+S+nA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@oxfmt/binding-linux-riscv64-musl@0.52.0':
resolution: {integrity: sha512-zji+1kb7lJKohSDjzC1IsS+K/cKRs1hdVf0ZH0VbdbiakmtLvN9twBoXo/k8VdjFax7kfo+DyPxS7vv52br1aw==}
'@oxfmt/binding-linux-riscv64-musl@0.65.0':
resolution: {integrity: sha512-YwSLVvpaz4o/nv/miiPEBJz+eJ+VmbgNIrao6RccK9ce+L5EA8wP+ZD0uFeq6wKOza6zoWv/dR0sj6lip6R3EA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@oxfmt/binding-linux-s390x-gnu@0.52.0':
resolution: {integrity: sha512-hcLBYedpCy7ToUvvBidWk7+11Yhg1oAZ4+6hKPic/mQI6NaqXJSXMps5nFlwUuX2ewhtLZZDPg63TI042qGKBg==}
'@oxfmt/binding-linux-s390x-gnu@0.65.0':
resolution: {integrity: sha512-XQTPqgvyrgkKcFq+Tp2eK6JS7sqqJ+nRmy2Fav4j3I+i4dJoPJm7YwEdoeSDX9xkqj9jZ/lWfF3bXUWztIrn6A==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@oxfmt/binding-linux-x64-gnu@0.52.0':
resolution: {integrity: sha512-IDO2loXK2OtTOhSPchU9MW25mWL2QCDGdJbjN8MXKZVS80qXe5gMTwQWu/gMJ3juoBHbkuUZNB2N1LHzNT7DoA==}
'@oxfmt/binding-linux-x64-gnu@0.65.0':
resolution: {integrity: sha512-cjZlx6S/VkeCNWCbwZriTnLnZeTcV3DEyeRGSw/2wwLP9viq+C0bJ4bC1k/ZLkFxDcB1lUgSasPkYGP1bdraOg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@oxfmt/binding-linux-x64-musl@0.52.0':
resolution: {integrity: sha512-mAV2Hjn0SatJ+KoAzKUC3eJhdJ8wv+3m1KyuS0dTsbF0c5weq+QrCt/DRZZM+uj/XiKzCDEUKYsBF30e2qkcyw==}
'@oxfmt/binding-linux-x64-musl@0.65.0':
resolution: {integrity: sha512-2azCjxdLtK4zCcIOU1dlXlU0xxfbPi6EjwWx7Ac7teWPidIIDOcIhudup83xNCKYhtqeVd/gaVDOxbUq4syXWA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@oxfmt/binding-openharmony-arm64@0.52.0':
resolution: {integrity: sha512-vd4npaUIwChxp7XzkqmepBWTT9YMcSe/NBApVGPC30/lLyOVaV3dvma1SKo03t8O73BPRAG7EyJzGlN5cJM5hQ==}
'@oxfmt/binding-openharmony-arm64@0.65.0':
resolution: {integrity: sha512-KXQ7xi1e/voP0IQaw6fG6XY4Z5+Llf1XmRSZS1t7pVFCecFJ0iXaboKmVwjFtp5MLlT5iWQrJ2U1C3GJdZ2u+Q==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [openharmony]
'@oxfmt/binding-win32-arm64-msvc@0.52.0':
resolution: {integrity: sha512-k2sz6gWQdMfh5HPpIS+Bw/0UEV/kaK2xuqJRrWL233sEHx9WLlsmvlPFM4HUNThkYbSN0U0vPW7LVKZWDS8hPQ==}
'@oxfmt/binding-win32-arm64-msvc@0.65.0':
resolution: {integrity: sha512-2FbbjG5jEqLSLKVJwBap84uJfpn5Y5A53KEO0aUNr+zeiRB9nyPUIFMcSbZVMFLitfBytFWRNngozXYjb6Rsbw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [win32]
'@oxfmt/binding-win32-ia32-msvc@0.52.0':
resolution: {integrity: sha512-rhke69GTcArodLHpjMTfNnvjTEBryDeZcUCKK/VjXDMtfTULl6QRh0ymX5/hbCUv2WjYm9h/QbW++q2vE15gWQ==}
'@oxfmt/binding-win32-ia32-msvc@0.65.0':
resolution: {integrity: sha512-LJ+ZacAPSjegDOnSLyA1TMWAhdDrsK4el3REdr1oL2UtVBCMhO2II/Sb3cEW6mF2MfLhl8hDNCSvc7KSbgk3LQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ia32]
os: [win32]
'@oxfmt/binding-win32-x64-msvc@0.52.0':
resolution: {integrity: sha512-q5xL7oeXkZdEtNZWBdvehJcmt+GRu9l2bK40yJs1jJXlqq+r0Hygb1rTjq+FM2o/2xyt4cufH6KRplHp3Jjsvw==}
'@oxfmt/binding-win32-x64-msvc@0.65.0':
resolution: {integrity: sha512-higu9cWEO6XXFzATD1jf0mCK34rNfN2H9JrJie7QB1IhleVpTh0QlLH9Ip2C1H/Nd5n0v5pvRtC+5R0uE4HpVg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [win32]
@@ -1503,124 +1596,124 @@ packages:
cpu: [x64]
os: [win32]
'@oxlint/binding-android-arm-eabi@1.77.0':
resolution: {integrity: sha512-E06sKWS6PiI6HRxS1wyQg22HvApt01hI7fV+T3wUk3OSbaaP4a3hYGY/MIQDmASqCiRjBdpRQYkgMkqH82cWmQ==}
'@oxlint/binding-android-arm-eabi@1.80.0':
resolution: {integrity: sha512-RM3Plj+biQpxa5d1GOOX6ciDlcUROmm4OZ/pLTpitkQt2mJv4jhtY4cbgaetOm5UKWZe05/TGQ6o1Vl8EOHkrA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [android]
'@oxlint/binding-android-arm64@1.77.0':
resolution: {integrity: sha512-NvsKz0KZxTp9cYWPLf+FXaSZwB3oO3peAjtukpOMBgse2vhQSoIIVqeO1yR0lEo/UcdZIDL18uq+kL0LzQ0ytA==}
'@oxlint/binding-android-arm64@1.80.0':
resolution: {integrity: sha512-YlO5JEf0Yr2bUUlu8O8daVcUxtcGGbcSmyV7E7nSbJbfAdxTE0PFPwgnIlw7wXJaTYjb+qs5hI5q3jxUkI7cAw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [android]
'@oxlint/binding-darwin-arm64@1.77.0':
resolution: {integrity: sha512-bgjTn6nW4bQCFBvSvuHCpDD+sONvmpo4lGI4PxzMt1quBA+xYxhczk6RiCn3GZ9gY8uhaBbwhj9MdKGfu6T9DA==}
'@oxlint/binding-darwin-arm64@1.80.0':
resolution: {integrity: sha512-BULDOyO3AhsmdWfQeIUCykDt3dd7XZBGLhp1eIh56skRv01O+cNjNPwXMIbeW1x4+pxcln5if72wcRgViVo7PA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [darwin]
'@oxlint/binding-darwin-x64@1.77.0':
resolution: {integrity: sha512-aotaIttH1R6j1Rwhx0M0htgeZyGtVQqYNTVEYMN/UcgHPquGA6kmk9OyuDc3a2GKUQBC+3C3GVQCcrRPMYqAFA==}
'@oxlint/binding-darwin-x64@1.80.0':
resolution: {integrity: sha512-YJ4JzLw7N5TDSQFlA0hAQGHvnDZgyypm1yunObVWcWiF9KM7eGCJKYKLgTC2Fi/57OdnBhbj4OkzPGdFQJ6HyA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [darwin]
'@oxlint/binding-freebsd-x64@1.77.0':
resolution: {integrity: sha512-nNx/wta7ksRAdYvq+l4AWjXkLxEXHALhENxjj2cYbQAIR4ybaA5L+hCbE63HOmft5czQ6ks+hb8vmEAnn7YGPg==}
'@oxlint/binding-freebsd-x64@1.80.0':
resolution: {integrity: sha512-AYUIk5QnL0s8oWAYsREZwkRYy1SupJTXALo93J1TgzHywxQtdM99FecRMQ87MXEdPQ0j1TmEpeeq3fGNkpvMqg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [freebsd]
'@oxlint/binding-linux-arm-gnueabihf@1.77.0':
resolution: {integrity: sha512-tMLLjM7xXtzXisVCzkOTXNCy9bZVId2wteNwjohlFDR/jY6WagpEDA1c1wu4xRc20Hojaxj+V6DSR7gbKxijWA==}
'@oxlint/binding-linux-arm-gnueabihf@1.80.0':
resolution: {integrity: sha512-9hBZVANupQ89W9dXyE0n8doCyaW5pDyGn3y6XlIMPZ+rIKuyqkr3SNUXmVJIhuvUq0NBU3RBiSXXE69l4XI6KA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [linux]
'@oxlint/binding-linux-arm-musleabihf@1.77.0':
resolution: {integrity: sha512-MiAFDFaqR0tmHTAyo0YDcZ5hyLREdYw/RQhc2R3cbT+8O3tB+zqPM2th9TTQ+Uo3jn/embS+DO+HyX9ztCPkOQ==}
'@oxlint/binding-linux-arm-musleabihf@1.80.0':
resolution: {integrity: sha512-SvS2uKqzY+pbfuvAHzH4338R6Zwo805GAwrIMVvK1KxoOWCIjZUdfzTCvilD7z6JK91v011+zYMryabhDo2AsQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [linux]
'@oxlint/binding-linux-arm64-gnu@1.77.0':
resolution: {integrity: sha512-/xqQ3B16i1T4cyt/9Mn+4CpzhUXoBXp7kVpIwzOXNFLj5JmK1bIjsbSnX296Gg8A/o7oDtKWikFgBx0SLwztkw==}
'@oxlint/binding-linux-arm64-gnu@1.80.0':
resolution: {integrity: sha512-tCLadyqRVL3pQTRPNg7cjXKvcvS4fbyXeQHhKk5BTJ1oftQln5/yIIWbu/Xom/DX41zv2P9QGt6+D/TtQVtY3A==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@oxlint/binding-linux-arm64-musl@1.77.0':
resolution: {integrity: sha512-LSbwuRKiNCenPDcbARqAZ5RfBy7gmj7vOvfJRLeCDU3gFtSxWbhv/+VTlaUqzUhNj1gFLHB8h7ALnxa/Az6z6g==}
'@oxlint/binding-linux-arm64-musl@1.80.0':
resolution: {integrity: sha512-XfpCNRlOPcLlJl4Bn/FUhjqlR6BVavEykERBf/MV7YA9VZDa5g5znVqYhyviMafcxS9Pe/i/kPvHNO0U6svEHQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@oxlint/binding-linux-ppc64-gnu@1.77.0':
resolution: {integrity: sha512-QWdcH31mXEUe5Nq1s0CfCpceaKjIo9uZtwDjAuL681g1axf+5x8xrg/eXWaw//4NCxYZ4V4e5Hu5tvdR+pTBlg==}
'@oxlint/binding-linux-ppc64-gnu@1.80.0':
resolution: {integrity: sha512-3I4yMwcFG9NeO8ioY6JBBuKsIm5GL/x7MATt1S4tVWaxPu5HcJ+XnLUbcVBTxG8q2Wu56HSj+NmXQiVYb1lp6A==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@oxlint/binding-linux-riscv64-gnu@1.77.0':
resolution: {integrity: sha512-GnOfYgJxbcElOiPZaDFDl406ONddwvOWk2jvAAAEjwAl4GofNoHF+/HHUIBYa6bFCArlcGPi0XjC4cU1pkgF/Q==}
'@oxlint/binding-linux-riscv64-gnu@1.80.0':
resolution: {integrity: sha512-E1wAKymkpe1/E8helzBKdm81OBOF+ezxRyXRMEuik3ZpWDER5CPOKZwF66RsdwW98uwZv8UTFremUQtC1CzdJA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@oxlint/binding-linux-riscv64-musl@1.77.0':
resolution: {integrity: sha512-AyEMTUCf0xY+hHF+IxqXFQIX0yQOIR8ykpY0lJNOw9xYqOzUX8dyZfRvlG0RfXwuQn2eonf/8NrMmDSZJjdqsA==}
'@oxlint/binding-linux-riscv64-musl@1.80.0':
resolution: {integrity: sha512-+gLRGD4sIo3+VA++iham5UxD9tKSoJ/VOrROCEXIcknrYtQg6iIQgvjN0cpiRF7N6UYC7pJbvHJlDnMge5LRpQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@oxlint/binding-linux-s390x-gnu@1.77.0':
resolution: {integrity: sha512-sPLzEcNvxd/oyVQ5oZo92CiHkFkpBeRop13E/P3TPY+hZfXHKCOWKI70TE2RYwMKFJDc20EMjH16L7NZICtKTw==}
'@oxlint/binding-linux-s390x-gnu@1.80.0':
resolution: {integrity: sha512-aR0PrzHj9leW3NmzBAAP4EzdoBNoJcs9sjnIQPIwyRnBGYrRbXUIpEB5Q39AqK3PLY5JK5uEhDQDiUa1QSAstw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@oxlint/binding-linux-x64-gnu@1.77.0':
resolution: {integrity: sha512-1Oh2ssH2L7lwyvkdSqaMUfsGfwU2Wfvew+obBUYjRVqhpBcUpwnsPSEr1IzVi9XqkuY10geiLsNKecqaZC34Dw==}
'@oxlint/binding-linux-x64-gnu@1.80.0':
resolution: {integrity: sha512-vSVh5cSo3Xxs6ghBCcFJlpbkbENzDog1qXtoXLa/HC3aCrR4XO76GZbXmQoCPHnu99nQpdCeC3H9tdNICfDh7A==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@oxlint/binding-linux-x64-musl@1.77.0':
resolution: {integrity: sha512-0j/2wRgNGO+Qj/M1uu/p57h/hFTTWWcfie0ufkbabeus2s5+/QqkCflnMOwLLN5m2GsNeWp4xdl4cPa4n7QCOQ==}
'@oxlint/binding-linux-x64-musl@1.80.0':
resolution: {integrity: sha512-FfzBXpNQ8u7/ZI/p8bl73MeZ508Ax3hxWp3SiJpEFiC+BB9XcXy5FAZHTLKDPSzrUpxQZSZJAVdDmuJp/+HDBQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@oxlint/binding-openharmony-arm64@1.77.0':
resolution: {integrity: sha512-BJ/j54qS0usEnyDkLYURMj2iiD9h5Cyy+ppzeMSXBGRXaGRNWnj1Mw14NqWMR5E/PzdgB30OOCCzLzbRoduafw==}
'@oxlint/binding-openharmony-arm64@1.80.0':
resolution: {integrity: sha512-zMzbkumtmprCgRwoYNzcB3iC39fXdJIMLMU33KdCjEGLlJGOEt1+LwQ4LF8ndLzAEKVz4BR0y3V6Xrkk3Nm3yA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [openharmony]
'@oxlint/binding-win32-arm64-msvc@1.77.0':
resolution: {integrity: sha512-Yh8w+g2Lpx7StrvtYkoz9JJvXjB9wxgFChFNb85nrXm/wj/XTwGWS1hve9+900HL7llrntYB3YP+y32E3tRqzA==}
'@oxlint/binding-win32-arm64-msvc@1.80.0':
resolution: {integrity: sha512-ib6iRcrXsk4t1fm3iKcwksyWh1ZkZXC/2mEzakl0ai2+6HZunf1WWMZ/xP9EJAvw9g9K4UVTC3NF/+G2qLrbTQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [win32]
'@oxlint/binding-win32-ia32-msvc@1.77.0':
resolution: {integrity: sha512-zja5b7+6a7UsRFgAQSrnax5vrzliEyNPLCjfXONu/vTWswaIVZGFajJZptaeRvPE4LghtFdAzVFlexTm7MVTGA==}
'@oxlint/binding-win32-ia32-msvc@1.80.0':
resolution: {integrity: sha512-xhRWBMpLxZvgKAH6+DJZmpP+W8Y8UdQOSU1JfxSWNXsaBaRGW77j+1hCuNHlzj7OH4SPN8fYd1q0o2qrDtoVyw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ia32]
os: [win32]
'@oxlint/binding-win32-x64-msvc@1.77.0':
resolution: {integrity: sha512-+teyvPDZ2RjUvo+SuCqS/UhaJl1QtdW5fWT5NJTV61V5MIuIS90Db9LixmtEGvXixyttiK62P96MSu3UlpviBw==}
'@oxlint/binding-win32-x64-msvc@1.80.0':
resolution: {integrity: sha512-yAnO7lwBYQnz2pcfBPIGQQZWIX5zd5R/1aAKIF3oE+TVj7IhoHcROjOkz3sRDngzqhfPKfFaXqug5j5rE5dn6Q==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [win32]
@@ -3399,6 +3492,7 @@ packages:
'@xmldom/xmldom@0.8.13':
resolution: {integrity: sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==}
engines: {node: '>=10.0.0'}
'@xterm/addon-fit@0.12.0-beta.287':
resolution: {integrity: sha512-2MDj+J4x67bjOS/SuBPxSYEWH38NbX6ENV18RbKVOhfRYCX3yERnuHBOrgH9hYdY8rCtsLQmuVgF6cmvCJiV2w==}
peerDependencies:
@@ -5544,8 +5638,8 @@ packages:
resolution: {integrity: sha512-uFkGGr1KMWd6aWv9UAqooYrN78trw8MWWmoPvgWokfBEUq1+eiIQ+qfj3wokhy0fxtZWZk+0dHoS7/yRTJtd6w==}
engines: {node: ^20.19.0 || >=22.12.0}
oxfmt@0.52.0:
resolution: {integrity: sha512-nJlYM35F64zTDMecCNhoHNkf+D/eHv7xcjj9XDSj+bFAVtN93m7v8DQMdHd6nDG6Akf/kEYYHmDUBs2Dz27Sug==}
oxfmt@0.65.0:
resolution: {integrity: sha512-SgS5VgnP42T0zl3zWD+xoH8FCqg1SAFnSRoOT/qeoa6gxcYIqrDMOmcXIg/EWSN92Du4ogB4riuKhKd6Y4CGhw==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
peerDependencies:
@@ -5565,8 +5659,8 @@ packages:
resolution: {integrity: sha512-KjK/XLcXr1DSyonKhsuFqJRiuKqcyG9j3LJ8nkOsrLzGvodBPqzHOKauy10asLMDI0sUpvb+1sxlzff3udZvfg==}
hasBin: true
oxlint@1.77.0:
resolution: {integrity: sha512-qnGh8XJHaQ0dprrDXNQZgS0FgjI6v+V3+X8DwmaV++5Aamy6jGKfDdQ1TUvhUxtmKFAbEf4/WeO5QZX+5WSngg==}
oxlint@1.80.0:
resolution: {integrity: sha512-5nTiSps4qdbCWLbxzuO00alHkEO2exR9YMN/ig6QXWrLsYSG0KaObOAM+l6oU2LcKPWoSAGYbkZIGEu1ViiWKA==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
peerDependencies:
@@ -7758,61 +7852,61 @@ snapshots:
'@oxc-project/types@0.141.0': {}
'@oxfmt/binding-android-arm-eabi@0.52.0':
'@oxfmt/binding-android-arm-eabi@0.65.0':
optional: true
'@oxfmt/binding-android-arm64@0.52.0':
'@oxfmt/binding-android-arm64@0.65.0':
optional: true
'@oxfmt/binding-darwin-arm64@0.52.0':
'@oxfmt/binding-darwin-arm64@0.65.0':
optional: true
'@oxfmt/binding-darwin-x64@0.52.0':
'@oxfmt/binding-darwin-x64@0.65.0':
optional: true
'@oxfmt/binding-freebsd-x64@0.52.0':
'@oxfmt/binding-freebsd-x64@0.65.0':
optional: true
'@oxfmt/binding-linux-arm-gnueabihf@0.52.0':
'@oxfmt/binding-linux-arm-gnueabihf@0.65.0':
optional: true
'@oxfmt/binding-linux-arm-musleabihf@0.52.0':
'@oxfmt/binding-linux-arm-musleabihf@0.65.0':
optional: true
'@oxfmt/binding-linux-arm64-gnu@0.52.0':
'@oxfmt/binding-linux-arm64-gnu@0.65.0':
optional: true
'@oxfmt/binding-linux-arm64-musl@0.52.0':
'@oxfmt/binding-linux-arm64-musl@0.65.0':
optional: true
'@oxfmt/binding-linux-ppc64-gnu@0.52.0':
'@oxfmt/binding-linux-ppc64-gnu@0.65.0':
optional: true
'@oxfmt/binding-linux-riscv64-gnu@0.52.0':
'@oxfmt/binding-linux-riscv64-gnu@0.65.0':
optional: true
'@oxfmt/binding-linux-riscv64-musl@0.52.0':
'@oxfmt/binding-linux-riscv64-musl@0.65.0':
optional: true
'@oxfmt/binding-linux-s390x-gnu@0.52.0':
'@oxfmt/binding-linux-s390x-gnu@0.65.0':
optional: true
'@oxfmt/binding-linux-x64-gnu@0.52.0':
'@oxfmt/binding-linux-x64-gnu@0.65.0':
optional: true
'@oxfmt/binding-linux-x64-musl@0.52.0':
'@oxfmt/binding-linux-x64-musl@0.65.0':
optional: true
'@oxfmt/binding-openharmony-arm64@0.52.0':
'@oxfmt/binding-openharmony-arm64@0.65.0':
optional: true
'@oxfmt/binding-win32-arm64-msvc@0.52.0':
'@oxfmt/binding-win32-arm64-msvc@0.65.0':
optional: true
'@oxfmt/binding-win32-ia32-msvc@0.52.0':
'@oxfmt/binding-win32-ia32-msvc@0.65.0':
optional: true
'@oxfmt/binding-win32-x64-msvc@0.52.0':
'@oxfmt/binding-win32-x64-msvc@0.65.0':
optional: true
'@oxlint-tsgolint/darwin-arm64@7.0.2001':
@@ -7833,61 +7927,61 @@ snapshots:
'@oxlint-tsgolint/win32-x64@7.0.2001':
optional: true
'@oxlint/binding-android-arm-eabi@1.77.0':
'@oxlint/binding-android-arm-eabi@1.80.0':
optional: true
'@oxlint/binding-android-arm64@1.77.0':
'@oxlint/binding-android-arm64@1.80.0':
optional: true
'@oxlint/binding-darwin-arm64@1.77.0':
'@oxlint/binding-darwin-arm64@1.80.0':
optional: true
'@oxlint/binding-darwin-x64@1.77.0':
'@oxlint/binding-darwin-x64@1.80.0':
optional: true
'@oxlint/binding-freebsd-x64@1.77.0':
'@oxlint/binding-freebsd-x64@1.80.0':
optional: true
'@oxlint/binding-linux-arm-gnueabihf@1.77.0':
'@oxlint/binding-linux-arm-gnueabihf@1.80.0':
optional: true
'@oxlint/binding-linux-arm-musleabihf@1.77.0':
'@oxlint/binding-linux-arm-musleabihf@1.80.0':
optional: true
'@oxlint/binding-linux-arm64-gnu@1.77.0':
'@oxlint/binding-linux-arm64-gnu@1.80.0':
optional: true
'@oxlint/binding-linux-arm64-musl@1.77.0':
'@oxlint/binding-linux-arm64-musl@1.80.0':
optional: true
'@oxlint/binding-linux-ppc64-gnu@1.77.0':
'@oxlint/binding-linux-ppc64-gnu@1.80.0':
optional: true
'@oxlint/binding-linux-riscv64-gnu@1.77.0':
'@oxlint/binding-linux-riscv64-gnu@1.80.0':
optional: true
'@oxlint/binding-linux-riscv64-musl@1.77.0':
'@oxlint/binding-linux-riscv64-musl@1.80.0':
optional: true
'@oxlint/binding-linux-s390x-gnu@1.77.0':
'@oxlint/binding-linux-s390x-gnu@1.80.0':
optional: true
'@oxlint/binding-linux-x64-gnu@1.77.0':
'@oxlint/binding-linux-x64-gnu@1.80.0':
optional: true
'@oxlint/binding-linux-x64-musl@1.77.0':
'@oxlint/binding-linux-x64-musl@1.80.0':
optional: true
'@oxlint/binding-openharmony-arm64@1.77.0':
'@oxlint/binding-openharmony-arm64@1.80.0':
optional: true
'@oxlint/binding-win32-arm64-msvc@1.77.0':
'@oxlint/binding-win32-arm64-msvc@1.80.0':
optional: true
'@oxlint/binding-win32-ia32-msvc@1.77.0':
'@oxlint/binding-win32-ia32-msvc@1.80.0':
optional: true
'@oxlint/binding-win32-x64-msvc@1.77.0':
'@oxlint/binding-win32-x64-msvc@1.80.0':
optional: true
'@parcel/watcher-android-arm64@2.5.6':
@@ -9617,7 +9711,7 @@ snapshots:
convert-source-map: 2.0.0
tinyrainbow: 3.1.0
'@vscode/windows-process-tree@0.8.0(patch_hash=946ffb2b1026bcb554d9920681fb00f546c28fc5de6d95a877d198b49bf73988)':
'@vscode/windows-process-tree@0.8.0(patch_hash=9217ef36c01ed74127fef5512b0c92089cdbf820fd6c109dd671137eebdc7585)':
dependencies:
node-addon-api: 7.1.0
optional: true
@@ -9650,7 +9744,7 @@ snapshots:
dependencies:
'@xterm/xterm': 6.1.0-beta.287(patch_hash=46796c152f3b73e28238f44499eaf5a867a863809bc7b470b159526a41e354f7)
'@xterm/addon-webgl@0.20.0-beta.286(patch_hash=118f649459ba56a51627d46603e2d5d81d837ac9c4dc393f5cc20263d0ac1b91)(@xterm/xterm@6.1.0-beta.287(patch_hash=46796c152f3b73e28238f44499eaf5a867a863809bc7b470b159526a41e354f7))':
'@xterm/addon-webgl@0.20.0-beta.286(patch_hash=2c301a06ad9caa635d746147dcb2b1c69aa026904f65e1183564f08a0e601b91)(@xterm/xterm@6.1.0-beta.287(patch_hash=46796c152f3b73e28238f44499eaf5a867a863809bc7b470b159526a41e354f7))':
dependencies:
'@xterm/xterm': 6.1.0-beta.287(patch_hash=46796c152f3b73e28238f44499eaf5a867a863809bc7b470b159526a41e354f7)
@@ -12170,29 +12264,29 @@ snapshots:
'@oxc-parser/binding-win32-ia32-msvc': 0.141.0
'@oxc-parser/binding-win32-x64-msvc': 0.141.0
oxfmt@0.52.0:
oxfmt@0.65.0:
dependencies:
tinypool: 2.1.0
optionalDependencies:
'@oxfmt/binding-android-arm-eabi': 0.52.0
'@oxfmt/binding-android-arm64': 0.52.0
'@oxfmt/binding-darwin-arm64': 0.52.0
'@oxfmt/binding-darwin-x64': 0.52.0
'@oxfmt/binding-freebsd-x64': 0.52.0
'@oxfmt/binding-linux-arm-gnueabihf': 0.52.0
'@oxfmt/binding-linux-arm-musleabihf': 0.52.0
'@oxfmt/binding-linux-arm64-gnu': 0.52.0
'@oxfmt/binding-linux-arm64-musl': 0.52.0
'@oxfmt/binding-linux-ppc64-gnu': 0.52.0
'@oxfmt/binding-linux-riscv64-gnu': 0.52.0
'@oxfmt/binding-linux-riscv64-musl': 0.52.0
'@oxfmt/binding-linux-s390x-gnu': 0.52.0
'@oxfmt/binding-linux-x64-gnu': 0.52.0
'@oxfmt/binding-linux-x64-musl': 0.52.0
'@oxfmt/binding-openharmony-arm64': 0.52.0
'@oxfmt/binding-win32-arm64-msvc': 0.52.0
'@oxfmt/binding-win32-ia32-msvc': 0.52.0
'@oxfmt/binding-win32-x64-msvc': 0.52.0
'@oxfmt/binding-android-arm-eabi': 0.65.0
'@oxfmt/binding-android-arm64': 0.65.0
'@oxfmt/binding-darwin-arm64': 0.65.0
'@oxfmt/binding-darwin-x64': 0.65.0
'@oxfmt/binding-freebsd-x64': 0.65.0
'@oxfmt/binding-linux-arm-gnueabihf': 0.65.0
'@oxfmt/binding-linux-arm-musleabihf': 0.65.0
'@oxfmt/binding-linux-arm64-gnu': 0.65.0
'@oxfmt/binding-linux-arm64-musl': 0.65.0
'@oxfmt/binding-linux-ppc64-gnu': 0.65.0
'@oxfmt/binding-linux-riscv64-gnu': 0.65.0
'@oxfmt/binding-linux-riscv64-musl': 0.65.0
'@oxfmt/binding-linux-s390x-gnu': 0.65.0
'@oxfmt/binding-linux-x64-gnu': 0.65.0
'@oxfmt/binding-linux-x64-musl': 0.65.0
'@oxfmt/binding-openharmony-arm64': 0.65.0
'@oxfmt/binding-win32-arm64-msvc': 0.65.0
'@oxfmt/binding-win32-ia32-msvc': 0.65.0
'@oxfmt/binding-win32-x64-msvc': 0.65.0
oxlint-plugin-react-doctor@0.9.1:
dependencies:
@@ -12210,27 +12304,27 @@ snapshots:
'@oxlint-tsgolint/win32-arm64': 7.0.2001
'@oxlint-tsgolint/win32-x64': 7.0.2001
oxlint@1.77.0(oxlint-tsgolint@7.0.2001):
oxlint@1.80.0(oxlint-tsgolint@7.0.2001):
optionalDependencies:
'@oxlint/binding-android-arm-eabi': 1.77.0
'@oxlint/binding-android-arm64': 1.77.0
'@oxlint/binding-darwin-arm64': 1.77.0
'@oxlint/binding-darwin-x64': 1.77.0
'@oxlint/binding-freebsd-x64': 1.77.0
'@oxlint/binding-linux-arm-gnueabihf': 1.77.0
'@oxlint/binding-linux-arm-musleabihf': 1.77.0
'@oxlint/binding-linux-arm64-gnu': 1.77.0
'@oxlint/binding-linux-arm64-musl': 1.77.0
'@oxlint/binding-linux-ppc64-gnu': 1.77.0
'@oxlint/binding-linux-riscv64-gnu': 1.77.0
'@oxlint/binding-linux-riscv64-musl': 1.77.0
'@oxlint/binding-linux-s390x-gnu': 1.77.0
'@oxlint/binding-linux-x64-gnu': 1.77.0
'@oxlint/binding-linux-x64-musl': 1.77.0
'@oxlint/binding-openharmony-arm64': 1.77.0
'@oxlint/binding-win32-arm64-msvc': 1.77.0
'@oxlint/binding-win32-ia32-msvc': 1.77.0
'@oxlint/binding-win32-x64-msvc': 1.77.0
'@oxlint/binding-android-arm-eabi': 1.80.0
'@oxlint/binding-android-arm64': 1.80.0
'@oxlint/binding-darwin-arm64': 1.80.0
'@oxlint/binding-darwin-x64': 1.80.0
'@oxlint/binding-freebsd-x64': 1.80.0
'@oxlint/binding-linux-arm-gnueabihf': 1.80.0
'@oxlint/binding-linux-arm-musleabihf': 1.80.0
'@oxlint/binding-linux-arm64-gnu': 1.80.0
'@oxlint/binding-linux-arm64-musl': 1.80.0
'@oxlint/binding-linux-ppc64-gnu': 1.80.0
'@oxlint/binding-linux-riscv64-gnu': 1.80.0
'@oxlint/binding-linux-riscv64-musl': 1.80.0
'@oxlint/binding-linux-s390x-gnu': 1.80.0
'@oxlint/binding-linux-x64-gnu': 1.80.0
'@oxlint/binding-linux-x64-musl': 1.80.0
'@oxlint/binding-openharmony-arm64': 1.80.0
'@oxlint/binding-win32-arm64-msvc': 1.80.0
'@oxlint/binding-win32-ia32-msvc': 1.80.0
'@oxlint/binding-win32-x64-msvc': 1.80.0
oxlint-tsgolint: 7.0.2001
p-cancelable@2.1.1: {}
+40
View File
@@ -3,3 +3,43 @@
# package graph. Without this file `pnpm -r` auto-discovers mobile/ and its
# nested package, and the root's patchedDependencies fail as ERR_PNPM_UNUSED_PATCH.
packages: []
minimumReleaseAge: 4320
shamefullyHoist: true
supportedArchitectures:
os:
- current
- darwin
- linux
- win32
cpu:
- current
- x64
- arm64
allowBuilds:
'@parcel/watcher': true
'@swc/core': false
'@vscode/windows-process-tree': false
agent-browser: false
cpu-features: true
electron-winstaller: false
esbuild: true
msw: false
node-pty: true
sherpa-onnx: true
ssh2: false
windows-native-registry: false
overrides:
monaco-editor>dompurify: 3.4.13
patchedDependencies:
node-pty@1.1.0: config/patches/node-pty@1.1.0.patch
'@xterm/addon-ligatures@0.11.0-beta.287': config/patches/@xterm__addon-ligatures@0.11.0-beta.287.patch
'@xterm/addon-webgl@0.20.0-beta.286': config/patches/@xterm__addon-webgl@0.20.0-beta.286.patch
'@xterm/addon-serialize@0.15.0-beta.287': config/patches/@xterm__addon-serialize@0.15.0-beta.287.patch
'@xterm/xterm@6.1.0-beta.287': config/patches/@xterm__xterm@6.1.0-beta.287.patch
lint-staged@16.4.0: config/patches/lint-staged@16.4.0.patch
'@vscode/windows-process-tree@0.8.0': config/patches/@vscode__windows-process-tree@0.8.0.patch
@@ -44,9 +44,20 @@ __orca_omp_should_skip_extension() {
esac
return 1
}
__orca_omp() {
local __orca_use_extension=1
__orca_omp_should_skip_extension "${1:-}" && __orca_use_extension=0
__orca_omp_cwd_is_usable() {
local __orca_physical_cwd
[[ -x . ]] || return 1
if [[ -n "${PWD:-}" && -d "${PWD:-}" ]]; then
[[ "${PWD}" -ef . ]]
else
# Why compare the path: shell builtins can print a cached path for a deleted cwd.
__orca_physical_cwd="$(builtin pwd -P 2>/dev/null)" || return 1
[[ -d "$__orca_physical_cwd" && "$__orca_physical_cwd" -ef . ]]
fi
}
__orca_omp_invoke() {
local __orca_use_extension="$1"
shift
if [[ $__orca_use_extension -eq 1 && -n "${ORCA_OMP_STATUS_EXTENSION:-}" && -f "${ORCA_OMP_STATUS_EXTENSION}" ]]; then
if [[ "${1:-}" == "launch" ]]; then
shift
@@ -58,6 +69,27 @@ __orca_omp() {
command omp "$@"
fi
}
__orca_omp() {
local __orca_use_extension=1
__orca_omp_should_skip_extension "${1:-}" && __orca_use_extension=0
if ! __orca_omp_cwd_is_usable; then
local __orca_logical_cwd="${PWD:-${ORCA_WORKTREE_PATH:-${ORCA_ROOT_PATH:-}}}"
# Why: a restored shell can retain the deleted directory inode after its path is recreated.
(
if [[ -z "$__orca_logical_cwd" ]]; then
printf 'Orca: OMP cannot start because no terminal working directory is available. Open a new terminal in an existing directory.\n' >&2
return 1
fi
if ! builtin cd -P -- "$__orca_logical_cwd" 2>/dev/null; then
printf 'Orca: OMP cannot access the terminal working directory "%s". Open a new terminal in an existing directory.\n' "$__orca_logical_cwd" >&2
return 1
fi
__orca_omp_invoke "$__orca_use_extension" "$@"
)
else
__orca_omp_invoke "$__orca_use_extension" "$@"
fi
}
if [[ -n "${ORCA_OMP_STATUS_EXTENSION:-}" ]]; then
# Why the function reserved word: it suppresses alias expansion of the name, which
# an `alias omp` otherwise rewrites at parse time, aborting the rest of the file.
@@ -82,9 +82,20 @@ __orca_deferred_init() {
esac
return 1
}
__orca_omp() {
local __orca_use_extension=1
__orca_omp_should_skip_extension "${1:-}" && __orca_use_extension=0
__orca_omp_cwd_is_usable() {
local __orca_physical_cwd
[[ -x . ]] || return 1
if [[ -n "${PWD:-}" && -d "${PWD:-}" ]]; then
[[ "${PWD}" -ef . ]]
else
# Why compare the path: shell builtins can print a cached path for a deleted cwd.
__orca_physical_cwd="$(builtin pwd -P 2>/dev/null)" || return 1
[[ -d "$__orca_physical_cwd" && "$__orca_physical_cwd" -ef . ]]
fi
}
__orca_omp_invoke() {
local __orca_use_extension="$1"
shift
if [[ $__orca_use_extension -eq 1 && -n "${ORCA_OMP_STATUS_EXTENSION:-}" && -f "${ORCA_OMP_STATUS_EXTENSION}" ]]; then
if [[ "${1:-}" == "launch" ]]; then
shift
@@ -96,6 +107,27 @@ __orca_deferred_init() {
command omp "$@"
fi
}
__orca_omp() {
local __orca_use_extension=1
__orca_omp_should_skip_extension "${1:-}" && __orca_use_extension=0
if ! __orca_omp_cwd_is_usable; then
local __orca_logical_cwd="${PWD:-${ORCA_WORKTREE_PATH:-${ORCA_ROOT_PATH:-}}}"
# Why: a restored shell can retain the deleted directory inode after its path is recreated.
(
if [[ -z "$__orca_logical_cwd" ]]; then
printf 'Orca: OMP cannot start because no terminal working directory is available. Open a new terminal in an existing directory.\n' >&2
return 1
fi
if ! builtin cd -P -- "$__orca_logical_cwd" 2>/dev/null; then
printf 'Orca: OMP cannot access the terminal working directory "%s". Open a new terminal in an existing directory.\n' "$__orca_logical_cwd" >&2
return 1
fi
__orca_omp_invoke "$__orca_use_extension" "$@"
)
else
__orca_omp_invoke "$__orca_use_extension" "$@"
fi
}
if [[ -n "${ORCA_OMP_STATUS_EXTENSION:-}" ]]; then
# Why the function reserved word: it suppresses alias expansion of the name, which
# an `alias omp` otherwise rewrites at parse time, aborting the rest of the file.
@@ -47,9 +47,20 @@ __orca_omp_should_skip_extension() {
esac
return 1
}
__orca_omp() {
local __orca_use_extension=1
__orca_omp_should_skip_extension "${1:-}" && __orca_use_extension=0
__orca_omp_cwd_is_usable() {
local __orca_physical_cwd
[[ -x . ]] || return 1
if [[ -n "${PWD:-}" && -d "${PWD:-}" ]]; then
[[ "${PWD}" -ef . ]]
else
# Why compare the path: shell builtins can print a cached path for a deleted cwd.
__orca_physical_cwd="$(builtin pwd -P 2>/dev/null)" || return 1
[[ -d "$__orca_physical_cwd" && "$__orca_physical_cwd" -ef . ]]
fi
}
__orca_omp_invoke() {
local __orca_use_extension="$1"
shift
if [[ $__orca_use_extension -eq 1 && -n "${ORCA_OMP_STATUS_EXTENSION:-}" && -f "${ORCA_OMP_STATUS_EXTENSION}" ]]; then
if [[ "${1:-}" == "launch" ]]; then
shift
@@ -61,6 +72,27 @@ __orca_omp() {
command omp "$@"
fi
}
__orca_omp() {
local __orca_use_extension=1
__orca_omp_should_skip_extension "${1:-}" && __orca_use_extension=0
if ! __orca_omp_cwd_is_usable; then
local __orca_logical_cwd="${PWD:-${ORCA_WORKTREE_PATH:-${ORCA_ROOT_PATH:-}}}"
# Why: a restored shell can retain the deleted directory inode after its path is recreated.
(
if [[ -z "$__orca_logical_cwd" ]]; then
printf 'Orca: OMP cannot start because no terminal working directory is available. Open a new terminal in an existing directory.\n' >&2
return 1
fi
if ! builtin cd -P -- "$__orca_logical_cwd" 2>/dev/null; then
printf 'Orca: OMP cannot access the terminal working directory "%s". Open a new terminal in an existing directory.\n' "$__orca_logical_cwd" >&2
return 1
fi
__orca_omp_invoke "$__orca_use_extension" "$@"
)
else
__orca_omp_invoke "$__orca_use_extension" "$@"
fi
}
if [[ -n "${ORCA_OMP_STATUS_EXTENSION:-}" ]]; then
# Why the function reserved word: it suppresses alias expansion of the name, which
# an `alias omp` otherwise rewrites at parse time, aborting the rest of the file.
@@ -82,9 +82,20 @@ __orca_deferred_init() {
esac
return 1
}
__orca_omp() {
local __orca_use_extension=1
__orca_omp_should_skip_extension "${1:-}" && __orca_use_extension=0
__orca_omp_cwd_is_usable() {
local __orca_physical_cwd
[[ -x . ]] || return 1
if [[ -n "${PWD:-}" && -d "${PWD:-}" ]]; then
[[ "${PWD}" -ef . ]]
else
# Why compare the path: shell builtins can print a cached path for a deleted cwd.
__orca_physical_cwd="$(builtin pwd -P 2>/dev/null)" || return 1
[[ -d "$__orca_physical_cwd" && "$__orca_physical_cwd" -ef . ]]
fi
}
__orca_omp_invoke() {
local __orca_use_extension="$1"
shift
if [[ $__orca_use_extension -eq 1 && -n "${ORCA_OMP_STATUS_EXTENSION:-}" && -f "${ORCA_OMP_STATUS_EXTENSION}" ]]; then
if [[ "${1:-}" == "launch" ]]; then
shift
@@ -96,6 +107,27 @@ __orca_deferred_init() {
command omp "$@"
fi
}
__orca_omp() {
local __orca_use_extension=1
__orca_omp_should_skip_extension "${1:-}" && __orca_use_extension=0
if ! __orca_omp_cwd_is_usable; then
local __orca_logical_cwd="${PWD:-${ORCA_WORKTREE_PATH:-${ORCA_ROOT_PATH:-}}}"
# Why: a restored shell can retain the deleted directory inode after its path is recreated.
(
if [[ -z "$__orca_logical_cwd" ]]; then
printf 'Orca: OMP cannot start because no terminal working directory is available. Open a new terminal in an existing directory.\n' >&2
return 1
fi
if ! builtin cd -P -- "$__orca_logical_cwd" 2>/dev/null; then
printf 'Orca: OMP cannot access the terminal working directory "%s". Open a new terminal in an existing directory.\n' "$__orca_logical_cwd" >&2
return 1
fi
__orca_omp_invoke "$__orca_use_extension" "$@"
)
else
__orca_omp_invoke "$__orca_use_extension" "$@"
fi
}
if [[ -n "${ORCA_OMP_STATUS_EXTENSION:-}" ]]; then
# Why the function reserved word: it suppresses alias expansion of the name, which
# an `alias omp` otherwise rewrites at parse time, aborting the rest of the file.
@@ -36,9 +36,20 @@ __orca_omp_should_skip_extension() {
esac
return 1
}
__orca_omp() {
local __orca_use_extension=1
__orca_omp_should_skip_extension "${1:-}" && __orca_use_extension=0
__orca_omp_cwd_is_usable() {
local __orca_physical_cwd
[[ -x . ]] || return 1
if [[ -n "${PWD:-}" && -d "${PWD:-}" ]]; then
[[ "${PWD}" -ef . ]]
else
# Why compare the path: shell builtins can print a cached path for a deleted cwd.
__orca_physical_cwd="$(builtin pwd -P 2>/dev/null)" || return 1
[[ -d "$__orca_physical_cwd" && "$__orca_physical_cwd" -ef . ]]
fi
}
__orca_omp_invoke() {
local __orca_use_extension="$1"
shift
if [[ $__orca_use_extension -eq 1 && -n "${ORCA_OMP_STATUS_EXTENSION:-}" && -f "${ORCA_OMP_STATUS_EXTENSION}" ]]; then
if [[ "${1:-}" == "launch" ]]; then
shift
@@ -50,6 +61,27 @@ __orca_omp() {
command omp "$@"
fi
}
__orca_omp() {
local __orca_use_extension=1
__orca_omp_should_skip_extension "${1:-}" && __orca_use_extension=0
if ! __orca_omp_cwd_is_usable; then
local __orca_logical_cwd="${PWD:-${ORCA_WORKTREE_PATH:-${ORCA_ROOT_PATH:-}}}"
# Why: a restored shell can retain the deleted directory inode after its path is recreated.
(
if [[ -z "$__orca_logical_cwd" ]]; then
printf 'Orca: OMP cannot start because no terminal working directory is available. Open a new terminal in an existing directory.\n' >&2
return 1
fi
if ! builtin cd -P -- "$__orca_logical_cwd" 2>/dev/null; then
printf 'Orca: OMP cannot access the terminal working directory "%s". Open a new terminal in an existing directory.\n' "$__orca_logical_cwd" >&2
return 1
fi
__orca_omp_invoke "$__orca_use_extension" "$@"
)
else
__orca_omp_invoke "$__orca_use_extension" "$@"
fi
}
if [[ -n "${ORCA_OMP_STATUS_EXTENSION:-}" ]]; then
# Why the function reserved word: it suppresses alias expansion of the name, which
# an `alias omp` otherwise rewrites at parse time, aborting the rest of the file.
@@ -56,9 +56,20 @@ __orca_deferred_init() {
esac
return 1
}
__orca_omp() {
local __orca_use_extension=1
__orca_omp_should_skip_extension "${1:-}" && __orca_use_extension=0
__orca_omp_cwd_is_usable() {
local __orca_physical_cwd
[[ -x . ]] || return 1
if [[ -n "${PWD:-}" && -d "${PWD:-}" ]]; then
[[ "${PWD}" -ef . ]]
else
# Why compare the path: shell builtins can print a cached path for a deleted cwd.
__orca_physical_cwd="$(builtin pwd -P 2>/dev/null)" || return 1
[[ -d "$__orca_physical_cwd" && "$__orca_physical_cwd" -ef . ]]
fi
}
__orca_omp_invoke() {
local __orca_use_extension="$1"
shift
if [[ $__orca_use_extension -eq 1 && -n "${ORCA_OMP_STATUS_EXTENSION:-}" && -f "${ORCA_OMP_STATUS_EXTENSION}" ]]; then
if [[ "${1:-}" == "launch" ]]; then
shift
@@ -70,6 +81,27 @@ __orca_deferred_init() {
command omp "$@"
fi
}
__orca_omp() {
local __orca_use_extension=1
__orca_omp_should_skip_extension "${1:-}" && __orca_use_extension=0
if ! __orca_omp_cwd_is_usable; then
local __orca_logical_cwd="${PWD:-${ORCA_WORKTREE_PATH:-${ORCA_ROOT_PATH:-}}}"
# Why: a restored shell can retain the deleted directory inode after its path is recreated.
(
if [[ -z "$__orca_logical_cwd" ]]; then
printf 'Orca: OMP cannot start because no terminal working directory is available. Open a new terminal in an existing directory.\n' >&2
return 1
fi
if ! builtin cd -P -- "$__orca_logical_cwd" 2>/dev/null; then
printf 'Orca: OMP cannot access the terminal working directory "%s". Open a new terminal in an existing directory.\n' "$__orca_logical_cwd" >&2
return 1
fi
__orca_omp_invoke "$__orca_use_extension" "$@"
)
else
__orca_omp_invoke "$__orca_use_extension" "$@"
fi
}
if [[ -n "${ORCA_OMP_STATUS_EXTENSION:-}" ]]; then
# Why the function reserved word: it suppresses alias expansion of the name, which
# an `alias omp` otherwise rewrites at parse time, aborting the rest of the file.
+20 -20
View File
@@ -85,23 +85,21 @@ export async function scanAiVaultSessions(
const candidates = dedupeCodexRolloutFileAliases(
discoveries
.flatMap((discovery) =>
discovery.files.map(
(file): SessionFileCandidate => ({
agent: discovery.agent,
file,
codexHome:
discovery.agent === 'codex'
? codexHomeForSessionsDir(
discovery.rootDir,
options.defaultCodexHomeDir ?? DEFAULT_CODEX_HOME_DIR
)
: null,
antigravityHistoryPath:
discovery.agent === 'antigravity'
? antigravityHistoryPathForBrainDir(discovery.rootDir)
: undefined
})
)
discovery.files.map((file): SessionFileCandidate => ({
agent: discovery.agent,
file,
codexHome:
discovery.agent === 'codex'
? codexHomeForSessionsDir(
discovery.rootDir,
options.defaultCodexHomeDir ?? DEFAULT_CODEX_HOME_DIR
)
: null,
antigravityHistoryPath:
discovery.agent === 'antigravity'
? antigravityHistoryPathForBrainDir(discovery.rootDir)
: undefined
}))
)
.sort((left, right) => right.file.mtimeMs - left.file.mtimeMs),
{
@@ -203,9 +201,11 @@ async function scanInScopeSessions(args: {
excludedFilePaths: args.alreadyParsedFilePaths,
issues: args.issues
})
const candidates = files.map(
(file): SessionFileCandidate => ({ agent: 'claude', file, codexHome: null })
)
const candidates = files.map((file): SessionFileCandidate => ({
agent: 'claude',
file,
codexHome: null
}))
if (candidates.length === 0) {
return []
}
@@ -5,6 +5,7 @@ import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterAll, describe, expect, it } from 'vitest'
import { build as buildVite } from 'vite'
import { resolveElectronProbeLaunch } from './electron-probe-display-launch'
const electronBinary = createRequire(import.meta.url)('electron') as string
const fixtureRoots: string[] = []
@@ -276,11 +277,12 @@ async function runFixture(): Promise<FixtureResult> {
const { ELECTRON_RUN_AS_NODE: _electronRunAsNode, ...env } = process.env
const electronArgs = [mainPath, `--user-data-dir=${join(root, 'profile')}`]
const executable = process.platform === 'linux' ? 'xvfb-run' : electronBinary
const args =
process.platform === 'linux'
? ['--auto-servernum', electronBinary, ...electronArgs, '--no-sandbox']
: electronArgs
const { executable, args } = resolveElectronProbeLaunch({
electronBinary,
electronArgs,
platform: process.platform,
display: env.DISPLAY
})
const run = spawnSync(executable, args, { encoding: 'utf8', env, timeout: 60_000 })
const rawResult = existsSync(resultPath) ? readFileSync(resultPath, 'utf8') : 'no result'
expect(run.error).toBeUndefined()
@@ -160,7 +160,9 @@ export function forwardGuestShortcutInput(
if (isFloatingGuest) {
renderer.send('ui:closeFloatingItem', { sourceId: browserTabId })
} else {
renderer.send('ui:closeActiveTab')
// Why: carry the guest's page id — activeTabType/activeBrowserTabId can be stale in split
// layouts (guest focus doesn't reach the group's focus-capture), silently dropping the close.
renderer.send('ui:closeActiveTab', { sourceId: browserTabId })
}
} else if (keybindingMatchesAction('tab.nextSameType', input, process.platform, keybindings)) {
renderer.send('ui:switchTab', 1)
@@ -872,7 +872,9 @@ describe('setupGuestShortcutForwarding', () => {
expect(rendererSendMock).toHaveBeenCalledWith('ui:closeFloatingItem', {
sourceId: browserTabId
})
expect(rendererSendMock).not.toHaveBeenCalledWith('ui:closeActiveTab')
expect(rendererSendMock.mock.calls.some(([channel]) => channel === 'ui:closeActiveTab')).toBe(
false
)
})
it('routes workspace/tab index chords to ui:selectFloatingIndex for a floating guest', () => {
@@ -903,7 +905,7 @@ describe('setupGuestShortcutForwarding', () => {
triggerBeforeInput(closeInput)
triggerBeforeInput(workspaceIndexInput)
expect(rendererSendMock).toHaveBeenCalledWith('ui:closeActiveTab')
expect(rendererSendMock).toHaveBeenCalledWith('ui:closeActiveTab', { sourceId: browserTabId })
expect(rendererSendMock).toHaveBeenCalledWith('ui:jumpToWorktreeIndex', 0)
expect(rendererSendMock).not.toHaveBeenCalledWith('ui:closeFloatingItem', expect.anything())
expect(rendererSendMock).not.toHaveBeenCalledWith('ui:selectFloatingIndex', expect.anything())
@@ -918,7 +920,7 @@ describe('setupGuestShortcutForwarding', () => {
triggerBeforeInput(closeInput)
expect(rendererSendMock).toHaveBeenCalledWith('ui:closeActiveTab')
expect(rendererSendMock).toHaveBeenCalledWith('ui:closeActiveTab', { sourceId: browserTabId })
expect(rendererSendMock).not.toHaveBeenCalledWith('ui:closeFloatingItem', expect.anything())
})
@@ -297,7 +297,9 @@ describe('browserManager', () => {
expect(rendererSendMock).toHaveBeenNthCalledWith(1, 'ui:newBrowserTab')
expect(rendererSendMock).toHaveBeenNthCalledWith(2, 'ui:newTerminalTab')
expect(rendererSendMock).toHaveBeenNthCalledWith(3, 'ui:closeActiveTab')
expect(rendererSendMock).toHaveBeenNthCalledWith(3, 'ui:closeActiveTab', {
sourceId: 'browser-1'
})
expect(rendererSendMock).toHaveBeenNthCalledWith(4, 'ui:switchTabAcrossAllTypes', 1)
expect(rendererSendMock).toHaveBeenNthCalledWith(5, 'ui:switchTab', 1)
expect(rendererSendMock).toHaveBeenNthCalledWith(6, 'ui:switchTerminalTab', 1)
@@ -2,6 +2,7 @@ import { spawn, spawnSync, type ChildProcess } from 'node:child_process'
import { existsSync, readFileSync } from 'node:fs'
import { createRequire } from 'node:module'
import { join } from 'node:path'
import { resolveElectronProbeLaunch } from './electron-probe-display-launch'
const electronBinary = createRequire(import.meta.url)('electron') as string
@@ -20,12 +21,13 @@ export async function runBrowserRouteEgressElectron(
`--user-data-dir=${join(root, 'profile')}`,
...extraElectronArgs
]
const executable = process.platform === 'linux' ? 'xvfb-run' : electronBinary
const args =
process.platform === 'linux'
? ['--auto-servernum', electronBinary, ...electronArgs, '--no-sandbox']
: electronArgs
const { ELECTRON_RUN_AS_NODE: _electronRunAsNode, ...env } = process.env
const { executable, args } = resolveElectronProbeLaunch({
electronBinary,
electronArgs,
platform: process.platform,
display: env.DISPLAY
})
const child = spawn(executable, args, {
detached: true,
env,
@@ -2,6 +2,7 @@ import { spawn, spawnSync, type ChildProcess } from 'node:child_process'
import { existsSync, readFileSync, rmSync } from 'node:fs'
import { createRequire } from 'node:module'
import { join } from 'node:path'
import { resolveElectronProbeLaunch } from './electron-probe-display-launch'
const electronBinary = createRequire(import.meta.url)('electron') as string
@@ -20,12 +21,13 @@ export async function runPersistedWorkerElectron(
const resultPath = join(root, 'result.json')
rmSync(resultPath, { force: true })
const electronArgs = [mainPath, configPath, mode, `--user-data-dir=${join(root, 'profile')}`]
const executable = process.platform === 'linux' ? 'xvfb-run' : electronBinary
const args =
process.platform === 'linux'
? ['--auto-servernum', electronBinary, ...electronArgs, '--no-sandbox']
: electronArgs
const { ELECTRON_RUN_AS_NODE: _electronRunAsNode, ...env } = process.env
const { executable, args } = resolveElectronProbeLaunch({
electronBinary,
electronArgs,
platform: process.platform,
display: env.DISPLAY
})
const child = spawn(executable, args, {
detached: true,
env,
@@ -4,6 +4,7 @@ import { createRequire } from 'node:module'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterAll, describe, expect, it } from 'vitest'
import { resolveElectronProbeLaunch } from './electron-probe-display-launch'
const electronBinary = createRequire(import.meta.url)('electron') as string
const fixtureRoots: string[] = []
@@ -138,11 +139,12 @@ function runProbe(protectedGuest: boolean): ProbeResult {
writeFileSync(mainPath, probeMain(resultPath, protectedGuest))
const { ELECTRON_RUN_AS_NODE: _electronRunAsNode, ...env } = process.env
const electronArgs = [mainPath, `--user-data-dir=${join(root, 'profile')}`]
const executable = process.platform === 'linux' ? 'xvfb-run' : electronBinary
const args =
process.platform === 'linux'
? ['--auto-servernum', electronBinary, ...electronArgs, '--no-sandbox']
: electronArgs
const { executable, args } = resolveElectronProbeLaunch({
electronBinary,
electronArgs,
platform: process.platform,
display: env.DISPLAY
})
const run = spawnSync(executable, args, { encoding: 'utf8', env, timeout: 30_000 })
const rawResult = existsSync(resultPath) ? readFileSync(resultPath, 'utf8') : 'no result'
expect(run.error).toBeUndefined()
+1 -1
View File
@@ -76,7 +76,7 @@ export class CdpTabCommands extends CdpBridgeCommandModule {
tabSwitch(index: number): BrowserTabSwitchResult {
// Why: filter to live tabs so indices match tabList(), skipping destroyed-but-uncleaned entries.
const liveEntries = [...this.getRegisteredTabs()].filter(([_, wcId]) => {
const liveEntries = [...this.getRegisteredTabs()].filter(([_tabId, wcId]) => {
const guest = webContents.fromId(wcId)
return guest && !guest.isDestroyed()
})
@@ -0,0 +1,59 @@
import { describe, expect, it } from 'vitest'
import { resolveElectronProbeLaunch } from './electron-probe-display-launch'
const electronBinary = '/tmp/electron'
const electronArgs = ['/tmp/main.cjs', '--user-data-dir=/tmp/profile']
describe('electron probe launch resolution', () => {
it('reuses an inherited X display instead of nesting another xvfb-run server', () => {
expect(
resolveElectronProbeLaunch({
electronBinary,
electronArgs,
platform: 'linux',
display: ':99'
})
).toEqual({
executable: electronBinary,
args: ['/tmp/main.cjs', '--user-data-dir=/tmp/profile', '--no-sandbox']
})
})
it('owns a display when Linux hands the probe none', () => {
expect(
resolveElectronProbeLaunch({
electronBinary,
electronArgs,
platform: 'linux',
display: undefined
})
).toEqual({
executable: 'xvfb-run',
args: [
'--auto-servernum',
electronBinary,
'/tmp/main.cjs',
'--user-data-dir=/tmp/profile',
'--no-sandbox'
]
})
})
it('treats an empty DISPLAY as no display', () => {
expect(
resolveElectronProbeLaunch({ electronBinary, electronArgs, platform: 'linux', display: '' })
.executable
).toBe('xvfb-run')
})
it('leaves non-Linux launches on the plain Electron binary', () => {
expect(
resolveElectronProbeLaunch({
electronBinary,
electronArgs,
platform: 'darwin',
display: undefined
})
).toEqual({ executable: electronBinary, args: electronArgs })
})
})
@@ -0,0 +1,31 @@
export type ElectronProbeLaunch = {
executable: string
args: string[]
}
export type ElectronProbeLaunchInput = {
electronBinary: string
electronArgs: readonly string[]
platform: NodeJS.Platform
display: string | undefined
}
// Why: these probes each start a full Electron stack, and CI already runs the whole vitest
// invocation under one `xvfb-run`. Nesting a private `--auto-servernum` server per probe adds an
// X server per concurrent file and races the sibling probes for a free display number, so reuse
// the display we were handed and only own one when there is none.
export function resolveElectronProbeLaunch({
electronBinary,
electronArgs,
platform,
display
}: ElectronProbeLaunchInput): ElectronProbeLaunch {
if (platform !== 'linux') {
return { executable: electronBinary, args: [...electronArgs] }
}
const linuxArgs = [...electronArgs, '--no-sandbox']
if (display) {
return { executable: electronBinary, args: linuxArgs }
}
return { executable: 'xvfb-run', args: ['--auto-servernum', electronBinary, ...linuxArgs] }
}
@@ -33,8 +33,8 @@ describe('estimateCostUsd cache-write TTL rates', () => {
})
it('never lowers a long-context estimate as writes shift from 5-minute to 1-hour', () => {
const costs = [0, 50_000, 100_000, 200_000, 300_000, 400_000].map(
(write1h) => estimateCostUsd('claude-sonnet-4-6', 0, 0, 0, 400_000, write1h)!
const costs = [0, 50_000, 100_000, 200_000, 300_000, 400_000].map((write1h) =>
estimateCostUsd('claude-sonnet-4-6', 0, 0, 0, 400_000, write1h)!
)
for (let index = 1; index < costs.length; index++) {
expect(costs[index]).toBeGreaterThan(costs[index - 1])
@@ -205,9 +205,10 @@ export function createRuntimeHome(): RuntimeHomeStub {
return {
syncForCurrentSelection: vi.fn(),
clearLastWrittenAuthJson: vi.fn(),
prepareForRateLimitFetch: vi.fn(
(): CodexRateLimitHomeResolution => ({ kind: 'ready', codexHomePath: null })
)
prepareForRateLimitFetch: vi.fn((): CodexRateLimitHomeResolution => ({
kind: 'ready',
codexHomePath: null
}))
}
}
@@ -191,13 +191,11 @@ describe('Codex state DB backfill recovery', () => {
const summary = await runCodexStateDbBackfillRecovery('/managed-home', controller.signal, {
spawnProcess: spawnProcess as never,
readStatus: vi.fn(
(): CodexStateDbBackfillStatus => ({
kind: 'incomplete',
stateDbPath: '/state.sqlite',
status: 'running'
})
),
readStatus: vi.fn((): CodexStateDbBackfillStatus => ({
kind: 'incomplete',
stateDbPath: '/state.sqlite',
status: 'running'
})),
terminate,
sleep,
now: () => now
@@ -270,15 +268,14 @@ describe('Codex state DB backfill recovery', () => {
new AbortController().signal,
{
spawnProcess: spawnProcess as never,
readStatus: vi.fn(
(): CodexStateDbBackfillStatus =>
spawnProcess.mock.calls.length >= 3
? { kind: 'complete', stateDbPath: '/state.sqlite' }
: {
kind: 'incomplete',
stateDbPath: '/state.sqlite',
status: 'running'
}
readStatus: vi.fn((): CodexStateDbBackfillStatus =>
spawnProcess.mock.calls.length >= 3
? { kind: 'complete', stateDbPath: '/state.sqlite' }
: {
kind: 'incomplete',
stateDbPath: '/state.sqlite',
status: 'running'
}
),
terminate: terminate as never,
sleep,
@@ -328,11 +325,10 @@ describe('Codex state DB backfill recovery', () => {
await expect(
runCodexStateDbBackfillRecovery('/managed-home', new AbortController().signal, {
spawnProcess: spawnProcess as never,
readStatus: vi.fn(
(): CodexStateDbBackfillStatus =>
spawnCount >= 2
? { kind: 'complete', stateDbPath: '/state.sqlite' }
: { kind: 'incomplete', stateDbPath: '/state.sqlite', status: 'running' }
readStatus: vi.fn((): CodexStateDbBackfillStatus =>
spawnCount >= 2
? { kind: 'complete', stateDbPath: '/state.sqlite' }
: { kind: 'incomplete', stateDbPath: '/state.sqlite', status: 'running' }
),
terminate,
sleep: vi.fn(async (ms: number) => {
@@ -427,11 +423,10 @@ describe('Codex state DB backfill recovery', () => {
}
return child
})
const readStatus = vi.fn(
(): CodexStateDbBackfillStatus =>
spawnCount >= 2
? { kind: 'complete', stateDbPath: '/state.sqlite' }
: { kind: 'incomplete', stateDbPath: '/state.sqlite', status: 'running' }
const readStatus = vi.fn((): CodexStateDbBackfillStatus =>
spawnCount >= 2
? { kind: 'complete', stateDbPath: '/state.sqlite' }
: { kind: 'incomplete', stateDbPath: '/state.sqlite', status: 'running' }
)
await expect(
@@ -0,0 +1,241 @@
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { describe, expect, it, vi } from 'vitest'
import { buildGpuCrashDiagnostics, GpuCrashDiagnosticsRecorder } from './gpu-crash-diagnostics'
const FEATURE_STATUS = {
gpu_compositing: 'enabled',
rasterization: 'enabled',
webgl: 'enabled',
webgl2: 'enabled',
video_decode: 'enabled'
}
const BASIC_INFO = {
gpuDevice: [
{
active: false,
vendorId: 0x8086,
deviceId: 0x9a49,
vendorString: 'Intel',
deviceString: 'Integrated GPU'
},
{
active: true,
vendorId: 0x10de,
deviceId: 0x2684,
vendorString: 'NVIDIA',
deviceString: 'Discrete GPU',
driverVendor: 'NVIDIA',
driverVersion: '32.0.15.6094'
}
],
auxAttributes: {
glVendor: 'Google Inc.',
glRenderer: 'ANGLE (NVIDIA, D3D11)',
glVersion: 'OpenGL ES 3.0'
}
}
function deferred<T>(): {
promise: Promise<T>
resolve: (value: T) => void
} {
let resolvePromise: ((value: T) => void) | undefined
const promise = new Promise<T>((resolve) => {
resolvePromise = resolve
})
return {
promise,
resolve: (value) => resolvePromise?.(value)
}
}
describe('buildGpuCrashDiagnostics', () => {
it('keeps only the active GPU identity, driver, rendering backend, and feature status', () => {
expect(
buildGpuCrashDiagnostics({ info: BASIC_INFO, level: 'complete' }, FEATURE_STATUS)
).toEqual({
gpuInfoLevel: 'complete',
gpuDeviceCount: 2,
gpuVendorId: 0x10de,
gpuDeviceId: 0x2684,
gpuVendor: 'NVIDIA',
gpuDevice: 'Discrete GPU',
gpuDriverVendor: 'NVIDIA',
gpuDriverVersion: '32.0.15.6094',
gpuGlVendor: 'Google Inc.',
gpuGlRenderer: 'ANGLE (NVIDIA, D3D11)',
gpuGlVersion: 'OpenGL ES 3.0',
gpuCompositingStatus: 'enabled',
gpuRasterizationStatus: 'enabled',
gpuWebglStatus: 'enabled',
gpuWebgl2Status: 'enabled',
gpuVideoDecodeStatus: 'enabled'
})
})
it('degrades malformed or unavailable GPU info without copying arbitrary fields', () => {
expect(
buildGpuCrashDiagnostics(
{
level: 'basic',
info: {
gpuDevice: [{ active: true, vendorId: Number.NaN, secret: 'do not copy' }],
machineModelName: 'do not copy'
}
},
{ webgl: 'unavailable', unexpected: 'do not copy' }
)
).toEqual({
gpuInfoLevel: 'basic',
gpuDeviceCount: 1,
gpuWebglStatus: 'unavailable'
})
expect(buildGpuCrashDiagnostics(null, null)).toEqual({ gpuInfoLevel: 'unavailable' })
})
})
describe('GpuCrashDiagnosticsRecorder', () => {
it('warms only complete info and records one breadcrumb across a crash burst', async () => {
const recordBreadcrumb = vi.fn()
const provider = {
getGPUInfo: vi.fn(async (level: 'basic' | 'complete') => ({
...BASIC_INFO,
gpuDevice: BASIC_INFO.gpuDevice.map((device) => ({
...device,
...(level === 'complete' ? { driverVersion: 'complete-driver' } : {})
}))
})),
getGPUFeatureStatus: vi.fn(() => FEATURE_STATUS)
}
const recorder = new GpuCrashDiagnosticsRecorder({ provider, recordBreadcrumb })
recorder.warm()
await vi.waitFor(() => {
expect(provider.getGPUInfo).toHaveBeenCalledTimes(1)
})
await Promise.resolve()
await recorder.record()
await recorder.record()
expect(provider.getGPUInfo).toHaveBeenCalledOnce()
expect(provider.getGPUInfo).toHaveBeenCalledWith('complete')
expect(recordBreadcrumb).toHaveBeenCalledTimes(1)
expect(recordBreadcrumb).toHaveBeenCalledWith(
expect.objectContaining({
gpuInfoLevel: 'complete',
gpuVendorId: 0x10de,
gpuDeviceId: 0x2684,
gpuDriverVersion: 'complete-driver'
})
)
})
it('uses promptly available basic info when complete collection is still pending', async () => {
const complete = deferred<unknown>()
const recordBreadcrumb = vi.fn()
const provider = {
getGPUInfo: vi.fn((level: 'basic' | 'complete') =>
level === 'basic' ? Promise.resolve(BASIC_INFO) : complete.promise
),
getGPUFeatureStatus: vi.fn(() => FEATURE_STATUS)
}
const recorder = new GpuCrashDiagnosticsRecorder({ provider, recordBreadcrumb })
recorder.warm()
expect(provider.getGPUInfo).toHaveBeenCalledOnce()
expect(provider.getGPUInfo).toHaveBeenCalledWith('complete')
await recorder.record()
expect(recordBreadcrumb).toHaveBeenCalledWith(
expect.objectContaining({
gpuInfoLevel: 'basic',
gpuVendorId: 0x10de,
gpuDriverVersion: '32.0.15.6094'
})
)
complete.resolve(BASIC_INFO)
})
it('shares pending capture work and releases it when complete info arrives first', async () => {
const complete = deferred<unknown>()
const basic = deferred<unknown>()
const recordBreadcrumb = vi.fn()
const provider = {
getGPUInfo: vi.fn((level: 'basic' | 'complete') =>
level === 'basic' ? basic.promise : complete.promise
),
getGPUFeatureStatus: vi.fn(() => FEATURE_STATUS)
}
const recorder = new GpuCrashDiagnosticsRecorder({ provider, recordBreadcrumb })
recorder.warm()
const first = recorder.record()
const second = recorder.record()
expect(second).toBe(first)
expect(recordBreadcrumb).not.toHaveBeenCalled()
complete.resolve(BASIC_INFO)
await first
expect(recordBreadcrumb).toHaveBeenCalledOnce()
expect(recordBreadcrumb).toHaveBeenCalledWith(
expect.objectContaining({ gpuInfoLevel: 'complete' })
)
})
it('does not let stalled GPU info block crash recovery', async () => {
const never = Promise.withResolvers<unknown>().promise
const recordBreadcrumb = vi.fn()
const provider = {
getGPUInfo: vi.fn(() => never),
getGPUFeatureStatus: vi.fn(() => FEATURE_STATUS)
}
const recorder = new GpuCrashDiagnosticsRecorder({
provider,
recordBreadcrumb,
recordTimeoutMs: 0
})
await recorder.record()
expect(recordBreadcrumb).toHaveBeenCalledWith(
expect.objectContaining({ gpuInfoLevel: 'unavailable' })
)
})
it('still records collection status when Electron throws during both GPU info calls', async () => {
const recordBreadcrumb = vi.fn()
const provider = {
getGPUInfo: vi.fn(() => {
throw new Error('GPU access disabled')
}),
getGPUFeatureStatus: vi.fn(() => {
throw new Error('GPU teardown')
})
}
const recorder = new GpuCrashDiagnosticsRecorder({ provider, recordBreadcrumb })
recorder.warm()
await recorder.record()
expect(recordBreadcrumb).toHaveBeenCalledWith({ gpuInfoLevel: 'unavailable' })
})
})
describe('GPU crash diagnostics production wiring', () => {
it('starts diagnostics without delaying safe-graphics fallback', () => {
const source = readFileSync(join(__dirname, '..', 'index.ts'), 'utf8')
const listenerStart = source.indexOf("app.on('child-process-gone'")
expect(listenerStart).toBeGreaterThan(0)
const listener = source.slice(listenerStart, source.indexOf('\n })', listenerStart))
expect(source).toMatch(
/recordBreadcrumb: \(data\) =>\s*recordDurableCrashBreadcrumb\('gpu_crash_hardware', data\)/
)
expect(listener).toMatch(
/const crashedAt = performance\.now\(\)[\s\S]*?void gpuCrashDiagnostics\?\.record\(\)[\s\S]*?void handleGpuChildCrash\(details\.reason, details\.exitCode \?\? null, crashedAt\)/
)
expect(listener).not.toMatch(/gpuCrashDiagnostics\?\.record\(\)[\s\S]*?\.then\(/)
})
})
@@ -0,0 +1,222 @@
import type { CrashReportBreadcrumbData } from '../../shared/crash-reporting'
type GpuInfoLevel = 'basic' | 'complete'
const DEFAULT_GPU_CRASH_DIAGNOSTICS_WAIT_MS = 1_000
type GpuInfoProvider = {
getGPUInfo(infoType: GpuInfoLevel): Promise<unknown>
getGPUFeatureStatus(): unknown
}
type GpuCrashDiagnosticsRecorderOptions = {
provider: GpuInfoProvider
recordBreadcrumb: (data: CrashReportBreadcrumbData) => void
recordTimeoutMs?: number
}
type GpuInfoSnapshot = {
info: unknown
level: GpuInfoLevel
}
async function waitAtMost(promise: Promise<void>, timeoutMs: number): Promise<void> {
const timeoutGate = Promise.withResolvers<void>()
const timeout = setTimeout(timeoutGate.resolve, timeoutMs)
try {
await Promise.race([promise, timeoutGate.promise])
} finally {
clearTimeout(timeout)
}
}
function waitForFirstAvailable(promises: Promise<boolean>[]): Promise<void> {
const availableGate = Promise.withResolvers<void>()
let remaining = promises.length
for (const promise of promises) {
void promise.then((available) => {
remaining -= 1
if (available || remaining === 0) {
availableGate.resolve()
}
})
}
return availableGate.promise
}
function recordValue(value: unknown): Record<string, unknown> | null {
return typeof value === 'object' && value !== null && !Array.isArray(value)
? (value as Record<string, unknown>)
: null
}
function nonEmptyString(value: unknown): string | undefined {
return typeof value === 'string' && value.length > 0 ? value : undefined
}
function finiteNumber(value: unknown): number | undefined {
return typeof value === 'number' && Number.isFinite(value) ? value : undefined
}
function addString(target: CrashReportBreadcrumbData, key: string, value: unknown): void {
const safe = nonEmptyString(value)
if (safe !== undefined) {
target[key] = safe
}
}
function addNumber(target: CrashReportBreadcrumbData, key: string, value: unknown): void {
const safe = finiteNumber(value)
if (safe !== undefined) {
target[key] = safe
}
}
function activeGpuDevice(info: Record<string, unknown>): {
device: Record<string, unknown> | null
count: number
} {
const devices = Array.isArray(info.gpuDevice)
? info.gpuDevice.map(recordValue).filter((device) => device !== null)
: []
return {
device: devices.find((device) => device.active === true) ?? devices[0] ?? null,
count: devices.length
}
}
function addFeatureStatuses(details: CrashReportBreadcrumbData, featureStatus: unknown): void {
const status = recordValue(featureStatus)
if (!status) {
return
}
addString(details, 'gpuCompositingStatus', status.gpu_compositing)
addString(details, 'gpuRasterizationStatus', status.rasterization)
addString(details, 'gpuWebglStatus', status.webgl)
addString(details, 'gpuWebgl2Status', status.webgl2)
addString(details, 'gpuVideoDecodeStatus', status.video_decode)
}
export function buildGpuCrashDiagnostics(
snapshot: GpuInfoSnapshot | null,
featureStatus: unknown
): CrashReportBreadcrumbData {
const details: CrashReportBreadcrumbData = {
gpuInfoLevel: snapshot?.level ?? 'unavailable'
}
addFeatureStatuses(details, featureStatus)
const info = recordValue(snapshot?.info)
if (!info) {
return details
}
const { device, count } = activeGpuDevice(info)
details.gpuDeviceCount = count
if (device) {
addNumber(details, 'gpuVendorId', device.vendorId)
addNumber(details, 'gpuDeviceId', device.deviceId)
addString(details, 'gpuVendor', device.vendorString)
addString(details, 'gpuDevice', device.deviceString)
addString(details, 'gpuDriverVendor', device.driverVendor)
addString(details, 'gpuDriverVersion', device.driverVersion)
}
const aux = recordValue(info.auxAttributes)
if (aux) {
addString(details, 'gpuGlVendor', aux.glVendor)
addString(details, 'gpuGlRenderer', aux.glRenderer)
addString(details, 'gpuGlVersion', aux.glVersion)
}
return details
}
/** Captures GPU identity before a crash and emits it once when a Windows GPU burst starts. */
export class GpuCrashDiagnosticsRecorder {
private readonly provider: GpuInfoProvider
private readonly recordBreadcrumb: (data: CrashReportBreadcrumbData) => void
private readonly recordTimeoutMs: number
private basicInfoPromise: Promise<boolean> | null = null
private completeInfoPromise: Promise<boolean> | null = null
private recordingPromise: Promise<void> | null = null
private basicInfo: unknown = null
private completeInfo: unknown = null
constructor(options: GpuCrashDiagnosticsRecorderOptions) {
this.provider = options.provider
this.recordBreadcrumb = options.recordBreadcrumb
this.recordTimeoutMs = options.recordTimeoutMs ?? DEFAULT_GPU_CRASH_DIAGNOSTICS_WAIT_MS
}
warm(): void {
void this.ensureCompleteInfo()
}
record(): Promise<void> {
this.recordingPromise ??= this.recordOnce()
return this.recordingPromise
}
private async recordOnce(): Promise<void> {
let featureStatus: unknown = null
try {
featureStatus = this.provider.getGPUFeatureStatus()
} catch {
// GPU teardown can race this read; device identity is still useful.
}
if (this.completeInfo === null) {
await waitAtMost(
waitForFirstAvailable([this.ensureBasicInfo(), this.ensureCompleteInfo()]),
this.recordTimeoutMs
)
}
const snapshot = this.preferredSnapshot()
try {
this.recordBreadcrumb(buildGpuCrashDiagnostics(snapshot, featureStatus))
} catch {
// Diagnostics must never block safe-graphics recovery.
}
}
private ensureBasicInfo(): Promise<boolean> {
if (this.basicInfoPromise === null) {
try {
this.basicInfoPromise = this.provider.getGPUInfo('basic').then(
(info) => {
this.basicInfo = info
return true
},
() => false
)
} catch {
this.basicInfoPromise = Promise.resolve(false)
}
}
return this.basicInfoPromise
}
private ensureCompleteInfo(): Promise<boolean> {
if (this.completeInfoPromise === null) {
try {
this.completeInfoPromise = this.provider.getGPUInfo('complete').then(
(info) => {
this.completeInfo = info
return true
},
() => false
)
} catch {
this.completeInfoPromise = Promise.resolve(false)
}
}
return this.completeInfoPromise
}
private preferredSnapshot(): GpuInfoSnapshot | null {
if (this.completeInfo !== null) {
return { info: this.completeInfo, level: 'complete' }
}
if (this.basicInfo !== null) {
return { info: this.basicInfo, level: 'basic' }
}
return null
}
}
@@ -0,0 +1,130 @@
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
import {
DEFAULT_GPU_CRASH_FALLBACK_THRESHOLD,
DEFAULT_GPU_CRASH_FALLBACK_WINDOW_MS,
GpuCrashFallbackTracker,
isGpuFallbackCrashCandidate
} from './gpu-crash-fallback-decision'
import { shouldRecordProcessGoneCrash } from './process-gone-classification'
/**
* Replays the win32 GPU-child deaths from the 1.4.190 'crashed' renderer cluster
* through the production decision path, so the reason no host was ever offered
* Safe Graphics Mode is pinned rather than argued about.
*
* Each session below is one crash report's `Recent activity`; a breadcrumb's
* `suppressedSinceLast=N` means N further identical GPU deaths were coalesced
* into it, so the session saw N+1 GPU crashes.
*/
type FieldSession = {
/** Crash-report id from the field bundle. */
report: string
/** GPU child crash times, ms since main-process start. */
gpuCrashesMsSinceLaunch: number[]
}
// crashed.txt: every win32 report in the cluster. Times are (breadcrumb ts -
// mainProcessStartedAt); coalesced repeats are placed inside the same second,
// which is the only interval the emitted breadcrumb pins them to.
const CRASHED_CLUSTER_SESSIONS: FieldSession[] = [
// 23:36:54.200 - 23:36:49.931, suppressedSinceLast=1 -> 2 crashes
{ report: 'db1f1ee2', gpuCrashesMsSinceLaunch: [4_269, 4_800] },
// 02:58:25.287 - 02:58:20.749, no suppression -> 1 crash
{ report: '66cc54d8', gpuCrashesMsSinceLaunch: [4_538] },
// 21:47:05.776 - 21:46:59.584, suppressedSinceLast=1 -> 2 crashes
{ report: '1f5564de', gpuCrashesMsSinceLaunch: [6_192, 6_240] },
// 00:17:17.840 - 00:17:15.732, no suppression -> 1 crash
{ report: '96d8c63b', gpuCrashesMsSinceLaunch: [2_108] }
]
/** index.ts's `child-process-gone` listener body — the wiring these claims rest on. */
function readChildProcessGoneListener(): string {
const source = readFileSync(join(__dirname, '..', 'index.ts'), 'utf8')
const start = source.indexOf("app.on('child-process-gone'")
expect(start).toBeGreaterThan(0)
return source.slice(start, source.indexOf('\n })', start))
}
function newTracker(): GpuCrashFallbackTracker {
return new GpuCrashFallbackTracker({
windowMs: DEFAULT_GPU_CRASH_FALLBACK_WINDOW_MS,
threshold: DEFAULT_GPU_CRASH_FALLBACK_THRESHOLD
})
}
const FIELD_GPU_EVENT = {
source: 'child',
processType: 'GPU',
serviceName: 'GPU',
reason: 'crashed',
expectedTeardown: 'none'
} as const
describe('1.4.190 win32 GPU-child crash cluster', () => {
it('does not let process_gone_suppressed gate the fallback candidate check', () => {
// The GPU death is suppressed as recoverable churn (no user-facing report)...
expect(shouldRecordProcessGoneCrash({ ...FIELD_GPU_EVENT, exitCode: -2147483645 })).toBe(false)
// ...but the fallback path reads the raw child-process-gone event, so the
// suppression cannot hide a broken driver from recovery.
expect(
isGpuFallbackCrashCandidate({
platform: 'win32',
processType: 'GPU',
reason: 'crashed'
})
).toBe(true)
// Both assertions above still pass if the candidate check is moved behind the
// suppressed-report path, so pin that nothing branches ahead of it in index.ts.
const listener = readChildProcessGoneListener()
const guardStart = listener.indexOf('isGpuFallbackCrashCandidate(')
expect(guardStart).toBeGreaterThan(0)
expect(listener.slice(0, guardStart).match(/\bif\s*\(/g) ?? []).toHaveLength(1)
expect(listener).toMatch(
/isGpuFallbackCrashCandidate\([\s\S]*?gpuCrashDiagnostics\?\.record\(\)[\s\S]*?handleGpuChildCrash\(/
)
// The `if (` count alone still allows `recorded && isGpuFallbackCrashCandidate(...)`, which
// re-couples recovery to the suppression decision, so pin the guard to that check alone.
const recoveryGuard = listener.slice(
listener.lastIndexOf('if (', guardStart),
listener.indexOf('handleGpuChildCrash(')
)
expect(recoveryGuard).not.toMatch(/&&|\|\|/)
})
it('never reaches the burst threshold in any observed cluster session', () => {
const outcomes = CRASHED_CLUSTER_SESSIONS.map((session) => {
// Each launch constructs a fresh tracker (src/main/index.ts), so evidence
// does not survive the relaunch these users performed after every crash.
const tracker = newTracker()
const engaged = session.gpuCrashesMsSinceLaunch.some(
(at) => tracker.recordGpuCrash(at).shouldEngageFallback
)
return {
report: session.report,
gpuCrashes: session.gpuCrashesMsSinceLaunch.length,
engagedSafeGraphicsPrompt: engaged
}
})
expect(outcomes).toEqual([
{ report: 'db1f1ee2', gpuCrashes: 2, engagedSafeGraphicsPrompt: false },
{ report: '66cc54d8', gpuCrashes: 1, engagedSafeGraphicsPrompt: false },
{ report: '1f5564de', gpuCrashes: 2, engagedSafeGraphicsPrompt: false },
{ report: '96d8c63b', gpuCrashes: 1, engagedSafeGraphicsPrompt: false }
])
})
it('engages on the session that did reach three crashes (field launch 51b9e93c)', () => {
// oom.txt, win32: GPU crashed/exitCode=34 with suppressedSinceLast=2, then
// `gpu_fallback_engaged (crashesInWindow=3)` and `gpu_fallback_restart_deferred`.
const tracker = newTracker()
expect(tracker.recordGpuCrash(3_600_000).shouldEngageFallback).toBe(false)
expect(tracker.recordGpuCrash(3_601_000).shouldEngageFallback).toBe(false)
expect(tracker.recordGpuCrash(3_601_890)).toEqual({
shouldEngageFallback: true,
crashesInWindow: 3
})
})
})
@@ -0,0 +1,113 @@
import { describe, expect, it, vi } from 'vitest'
import {
engageGpuFallbackAfterCrashBurst,
type GpuFallbackEngagement,
type GpuFallbackEngagementHandlers
} from './gpu-fallback-engagement'
import type { GpuFallbackRestartDecision } from './gpu-fallback-restart-prompt'
const ENGAGEMENT: GpuFallbackEngagement = {
reason: 'crashed',
// 0x80000003 STATUS_BREAKPOINT — the production Windows signature.
exitCode: -2147483645,
crashesInWindow: 3,
engagedAt: 1_700_000_000_000
}
function createHandlers(overrides: Partial<GpuFallbackEngagementHandlers> = {}): {
handlers: GpuFallbackEngagementHandlers
order: string[]
} {
const order: string[] = []
const handlers: GpuFallbackEngagementHandlers = {
isQuitting: () => false,
persistMarker: vi.fn(() => {
order.push('persistMarker')
return true
}),
confirmMarker: vi.fn(() => order.push('confirmMarker')),
clearMarker: vi.fn(() => order.push('clearMarker')),
promptForRestart: vi.fn(async () => {
order.push('prompt')
return 'restart' as GpuFallbackRestartDecision
}),
onPromptFailed: vi.fn(),
onEngaged: vi.fn(),
onRestartDeferred: vi.fn(() => order.push('restartDeferred')),
restartIntoSafeGraphics: vi.fn(() => order.push('restart')),
...overrides
}
return { handlers, order }
}
describe('engageGpuFallbackAfterCrashBurst', () => {
// Repro for the Windows startup GPU cluster (v1.4.190, exit -2147483645).
// Measured on Windows 11 26200 / Electron 43.1.0: Chromium's own ladder aborts
// the whole browser ("GPU process isn't usable. Goodbye.") on the 6th GPU
// crash, 1.285s after the 3rd — the crash that trips this threshold. If the
// marker is only written after the user answers the modal, the app dies first
// and the next launch retries hardware acceleration, looping forever.
it('persists the safe-graphics marker before the restart prompt is answered', async () => {
let resolvePrompt: ((decision: GpuFallbackRestartDecision) => void) | undefined
const { handlers } = createHandlers({
promptForRestart: vi.fn(
() =>
new Promise<GpuFallbackRestartDecision>((resolve) => {
resolvePrompt = resolve
})
)
})
const engaging = engageGpuFallbackAfterCrashBurst(ENGAGEMENT, handlers)
await Promise.resolve()
// Chromium kills the process here; whatever is on disk now is all the next launch gets.
expect(handlers.persistMarker).toHaveBeenCalledWith(ENGAGEMENT)
resolvePrompt?.('restart')
await engaging
})
it('relaunches into safe graphics when the user accepts', async () => {
const { handlers, order } = createHandlers()
await engageGpuFallbackAfterCrashBurst(ENGAGEMENT, handlers)
expect(order).toEqual(['persistMarker', 'prompt', 'confirmMarker', 'restart'])
expect(handlers.clearMarker).not.toHaveBeenCalled()
})
it('undoes the marker when the user chooses Keep Running', async () => {
const { handlers, order } = createHandlers({
promptForRestart: vi.fn(async () => 'continue' as GpuFallbackRestartDecision)
})
await engageGpuFallbackAfterCrashBurst(ENGAGEMENT, handlers)
expect(order).toEqual(['persistMarker', 'clearMarker', 'restartDeferred'])
expect(handlers.restartIntoSafeGraphics).not.toHaveBeenCalled()
})
it('keeps the marker when the prompt itself fails, so the next launch is still safe', async () => {
const error = new Error('no display')
const { handlers } = createHandlers({
promptForRestart: vi.fn(async () => {
throw error
})
})
await engageGpuFallbackAfterCrashBurst(ENGAGEMENT, handlers)
expect(handlers.persistMarker).toHaveBeenCalledTimes(1)
expect(handlers.clearMarker).not.toHaveBeenCalled()
expect(handlers.onPromptFailed).toHaveBeenCalledWith(error)
})
it('does not relaunch when a quit began while the prompt was open', async () => {
const { handlers } = createHandlers({ isQuitting: () => true })
await engageGpuFallbackAfterCrashBurst(ENGAGEMENT, handlers)
expect(handlers.restartIntoSafeGraphics).not.toHaveBeenCalled()
// The marker stays: a quit is not the user declining safe graphics.
expect(handlers.clearMarker).not.toHaveBeenCalled()
})
it('does not claim a restart when the marker could not be written', async () => {
const { handlers } = createHandlers({ persistMarker: vi.fn(() => false) })
await engageGpuFallbackAfterCrashBurst(ENGAGEMENT, handlers)
expect(handlers.restartIntoSafeGraphics).not.toHaveBeenCalled()
})
})
@@ -0,0 +1,66 @@
import type { GpuFallbackRestartDecision } from './gpu-fallback-restart-prompt'
/** The GPU-crash burst that tripped the software-rendering fallback. */
export type GpuFallbackEngagement = {
reason: string
exitCode: number | null
crashesInWindow: number
/** Wall-clock ms stamped into the persisted marker. */
engagedAt: number
}
export type GpuFallbackEngagementHandlers = {
isQuitting: () => boolean
/** Writes the build-scoped safe-graphics marker; false when it could not be persisted. */
persistMarker: (engagement: GpuFallbackEngagement) => boolean
/** Marks that the user explicitly accepted safe graphics before relaunching. */
confirmMarker: (engagement: GpuFallbackEngagement) => void
clearMarker: () => void
promptForRestart: () => Promise<GpuFallbackRestartDecision>
onPromptFailed: (error: unknown) => void
onEngaged: (engagement: GpuFallbackEngagement) => void
onRestartDeferred: (engagement: GpuFallbackEngagement) => void
restartIntoSafeGraphics: (engagement: GpuFallbackEngagement) => void
}
/**
* Drives the safe-graphics handover once a GPU crash burst trips the threshold.
*
* Why the marker is written before the prompt: measured on Windows 11 26200 /
* Electron 43.1.0, Chromium's own GPU ladder aborts the whole browser process
* (`FATAL gpu_data_manager_impl_private.cc: GPU process isn't usable. Goodbye.`)
* on the 6th GPU crash — 1.285s after the 3rd, which is the crash that trips
* this threshold. Persisting only after the user answers a modal means the app
* dies first, no marker survives, and the next launch retries hardware
* acceleration: the crash loop never ends. An explicit "Keep Running" undoes it.
*/
export async function engageGpuFallbackAfterCrashBurst(
engagement: GpuFallbackEngagement,
handlers: GpuFallbackEngagementHandlers
): Promise<void> {
handlers.onEngaged(engagement)
const persisted = handlers.persistMarker(engagement)
let decision: GpuFallbackRestartDecision
try {
decision = await handlers.promptForRestart()
} catch (error) {
// Marker stays: the next launch is safe even though the user was never asked.
handlers.onPromptFailed(error)
return
}
if (handlers.isQuitting()) {
return
}
if (decision !== 'restart') {
if (persisted) {
handlers.clearMarker()
}
handlers.onRestartDeferred(engagement)
return
}
if (!persisted) {
return
}
handlers.confirmMarker(engagement)
handlers.restartIntoSafeGraphics(engagement)
}
@@ -0,0 +1,119 @@
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const { showMessageBoxMock } = vi.hoisted(() => ({
showMessageBoxMock: vi.fn()
}))
vi.mock('electron', () => ({
dialog: { showMessageBox: showMessageBoxMock }
}))
import {
handleGpuFallbackRecoveredLaunch,
promptForGpuFallbackRecoveredLaunch,
type GpuFallbackRecoveredLaunchDecision,
type GpuFallbackRecoveredLaunchHandlers
} from './gpu-fallback-recovered-launch'
beforeEach(() => {
showMessageBoxMock.mockReset()
})
function createHandlers(
decision: GpuFallbackRecoveredLaunchDecision = 'keep-safe',
overrides: Partial<GpuFallbackRecoveredLaunchHandlers> = {}
): { handlers: GpuFallbackRecoveredLaunchHandlers; order: string[] } {
const order: string[] = []
const handlers: GpuFallbackRecoveredLaunchHandlers = {
isQuitting: () => false,
prompt: vi.fn(async () => {
order.push('prompt')
return decision
}),
confirmSafeGraphics: vi.fn(() => order.push('confirm')),
clearSafeGraphics: vi.fn(() => order.push('clear')),
onPromptFailed: vi.fn(),
onSafeGraphicsKept: vi.fn(() => order.push('kept')),
restartWithHardware: vi.fn(() => order.push('restart')),
...overrides
}
return { handlers, order }
}
describe('promptForGpuFallbackRecoveredLaunch', () => {
it('defaults dismissal to the stable safe-graphics choice', async () => {
const parentWindow = { id: 1 }
showMessageBoxMock.mockResolvedValue({ response: 0 })
await expect(promptForGpuFallbackRecoveredLaunch(parentWindow as never)).resolves.toBe(
'keep-safe'
)
expect(showMessageBoxMock).toHaveBeenCalledWith(parentWindow, {
type: 'info',
buttons: ['Keep Safe Graphics Mode', 'Try Hardware Acceleration'],
defaultId: 0,
cancelId: 0,
title: 'Safe Graphics Mode is Active',
message: 'Orca recovered in Safe Graphics Mode.',
detail:
'Safe Graphics Mode was enabled after repeated graphics crashes. Keep it for stability, or restart and try hardware acceleration again.'
})
})
it('returns the explicit hardware retry choice', async () => {
showMessageBoxMock.mockResolvedValue({ response: 1 })
await expect(promptForGpuFallbackRecoveredLaunch()).resolves.toBe('retry-hardware')
})
})
describe('handleGpuFallbackRecoveredLaunch', () => {
it('confirms safe graphics so later launches do not prompt again', async () => {
const { handlers, order } = createHandlers()
await handleGpuFallbackRecoveredLaunch(handlers)
expect(order).toEqual(['prompt', 'confirm', 'kept'])
expect(handlers.clearSafeGraphics).not.toHaveBeenCalled()
expect(handlers.restartWithHardware).not.toHaveBeenCalled()
})
it('clears the marker before restarting with hardware acceleration', async () => {
const { handlers, order } = createHandlers('retry-hardware')
await handleGpuFallbackRecoveredLaunch(handlers)
expect(order).toEqual(['prompt', 'clear', 'restart'])
expect(handlers.confirmSafeGraphics).not.toHaveBeenCalled()
})
it('leaves the unconfirmed marker intact when the prompt fails', async () => {
const error = new Error('dialog failed')
const { handlers } = createHandlers('keep-safe', {
prompt: vi.fn(async () => {
throw error
})
})
await handleGpuFallbackRecoveredLaunch(handlers)
expect(handlers.onPromptFailed).toHaveBeenCalledWith(error)
expect(handlers.confirmSafeGraphics).not.toHaveBeenCalled()
expect(handlers.clearSafeGraphics).not.toHaveBeenCalled()
})
it('does not mutate the marker when shutdown starts while the prompt is open', async () => {
const { handlers } = createHandlers('retry-hardware', { isQuitting: () => true })
await handleGpuFallbackRecoveredLaunch(handlers)
expect(handlers.confirmSafeGraphics).not.toHaveBeenCalled()
expect(handlers.clearSafeGraphics).not.toHaveBeenCalled()
expect(handlers.restartWithHardware).not.toHaveBeenCalled()
})
})
describe('recovered safe-graphics production wiring', () => {
it('prompts only after the recovered window is shown and persists both consent states', () => {
const source = readFileSync(join(__dirname, '..', 'index.ts'), 'utf8')
expect(source).toMatch(
/window\.once\('show',[\s\S]*?presentGpuFallbackRecoveredLaunchPrompt\(window\)/
)
expect(source).toMatch(
/persistMarker:[\s\S]*?userConfirmed: false[\s\S]*?confirmMarker:[\s\S]*?userConfirmed: true/
)
})
})
@@ -0,0 +1,56 @@
import { dialog, type BrowserWindow, type MessageBoxOptions } from 'electron'
export type GpuFallbackRecoveredLaunchDecision = 'keep-safe' | 'retry-hardware'
const GPU_FALLBACK_RECOVERED_LAUNCH_OPTIONS: MessageBoxOptions = {
type: 'info',
buttons: ['Keep Safe Graphics Mode', 'Try Hardware Acceleration'],
defaultId: 0,
cancelId: 0,
title: 'Safe Graphics Mode is Active',
message: 'Orca recovered in Safe Graphics Mode.',
detail:
'Safe Graphics Mode was enabled after repeated graphics crashes. Keep it for stability, or restart and try hardware acceleration again.'
}
export async function promptForGpuFallbackRecoveredLaunch(
parentWindow?: BrowserWindow
): Promise<GpuFallbackRecoveredLaunchDecision> {
const { response } = parentWindow
? await dialog.showMessageBox(parentWindow, GPU_FALLBACK_RECOVERED_LAUNCH_OPTIONS)
: await dialog.showMessageBox(GPU_FALLBACK_RECOVERED_LAUNCH_OPTIONS)
return response === 1 ? 'retry-hardware' : 'keep-safe'
}
export type GpuFallbackRecoveredLaunchHandlers = {
isQuitting: () => boolean
prompt: () => Promise<GpuFallbackRecoveredLaunchDecision>
confirmSafeGraphics: () => void
clearSafeGraphics: () => void
onPromptFailed: (error: unknown) => void
onSafeGraphicsKept: () => void
restartWithHardware: () => void
}
/** Resolves consent after an unanswered crash-time prompt recovered into safe graphics. */
export async function handleGpuFallbackRecoveredLaunch(
handlers: GpuFallbackRecoveredLaunchHandlers
): Promise<void> {
let decision: GpuFallbackRecoveredLaunchDecision
try {
decision = await handlers.prompt()
} catch (error) {
handlers.onPromptFailed(error)
return
}
if (handlers.isQuitting()) {
return
}
if (decision === 'retry-hardware') {
handlers.clearSafeGraphics()
handlers.restartWithHardware()
return
}
handlers.confirmSafeGraphics()
handlers.onSafeGraphicsKept()
}
@@ -13,15 +13,13 @@ function makeBackend(): {
const resolveDeviceId = vi.fn(async () => {
throw new Error('shutdown AVD cannot resolve before boot')
})
const startSession = vi.fn(
async (): Promise<EmulatorSessionInfo> => ({
deviceUdid: 'emulator-5554',
streamUrl: 'scrcpy://emulator-5554',
wsUrl: '',
streamCodec: 'h264',
backend: 'android'
})
)
const startSession = vi.fn(async (): Promise<EmulatorSessionInfo> => ({
deviceUdid: 'emulator-5554',
streamUrl: 'scrcpy://emulator-5554',
wsUrl: '',
streamCodec: 'h264',
backend: 'android'
}))
const stopHelperForDevice = vi.fn(async () => {})
const shutdownDevice = vi.fn(async () => {})
const backend: EmulatorBackend = {
+19 -4
View File
@@ -218,6 +218,21 @@ export function parseAndFilterSearchRefDetails(
.slice(0, Math.max(0, limit))
}
export function resolveConfiguredRemoteBranchName(
fullRef: string,
longestFirstRemoteNames: readonly string[]
): string | null {
const remoteRefPrefix = 'refs/remotes/'
if (!fullRef.startsWith(remoteRefPrefix)) {
return null
}
const remoteAndBranch = fullRef.slice(remoteRefPrefix.length)
const remote = longestFirstRemoteNames.find((candidate) =>
remoteAndBranch.startsWith(`${candidate}/`)
)
return remote ? remoteAndBranch.slice(remote.length + 1) || null : null
}
export function resolveLocalBranchName(
fullRef: string,
shortRef: string,
@@ -227,11 +242,11 @@ export function resolveLocalBranchName(
if (!fullRef.startsWith(remoteRefPrefix)) {
return shortRef
}
const remoteAndBranch = fullRef.slice(remoteRefPrefix.length)
const remote = remotes.find((candidate) => remoteAndBranch.startsWith(`${candidate}/`))
if (remote) {
return remoteAndBranch.slice(remote.length + 1)
const configuredBranch = resolveConfiguredRemoteBranchName(fullRef, remotes)
if (configuredBranch) {
return configuredBranch
}
const remoteAndBranch = fullRef.slice(remoteRefPrefix.length)
return remoteAndBranch.split('/').slice(1).join('/') || shortRef
}
+41 -22
View File
@@ -1,45 +1,50 @@
import { listRemoteNames, resolveLocalBranchName } from './repo-base-ref-search'
import { gitExecOptions, type LocalGitExecOptions } from './repo-default-base-ref'
import { resolveConfiguredRemoteBranchName } from './repo-base-ref-search'
import { gitExecOptions, type GitExec, type LocalGitExecOptions } from './repo-default-base-ref'
import { gitExecFileAsync } from './runner'
export type BranchConflictKind = 'local' | 'remote'
async function hasGitRefAsync(
path: string,
ref: string,
options: LocalGitExecOptions = {}
): Promise<boolean> {
async function hasGitRefAsync(exec: GitExec, ref: string): Promise<boolean> {
try {
await gitExecFileAsync(['rev-parse', '--verify', ref], gitExecOptions(path, options))
return true
const { stdout } = await exec(['rev-parse', '--verify', ref])
return stdout.trim().length > 0
} catch {
return false
}
}
export async function getBranchConflictKind(
path: string,
async function listRemoteNamesViaExec(exec: GitExec): Promise<string[]> {
try {
const { stdout } = await exec(['remote'])
return stdout
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean)
.sort((a, b) => b.length - a.length)
} catch {
return []
}
}
/** Run branch-conflict policy through the host that owns Git execution. */
export async function getBranchConflictKindViaExec(
exec: GitExec,
branchName: string,
allowedBaseRef?: string,
options: LocalGitExecOptions = {}
allowedBaseRef?: string
): Promise<BranchConflictKind | null> {
if (await hasGitRefAsync(path, `refs/heads/${branchName}`, options)) {
if (await hasGitRefAsync(exec, `refs/heads/${branchName}`)) {
return 'local'
}
try {
const remoteNames = (await listRemoteNames(path, options)).sort((a, b) => b.length - a.length)
const { stdout } = await gitExecFileAsync(
['for-each-ref', '--format=%(refname)', 'refs/remotes'],
gitExecOptions(path, options)
)
const hasRemoteConflict = stdout.split('\n').some((ref) => {
const remoteNames = await listRemoteNamesViaExec(exec)
const { stdout } = await exec(['for-each-ref', '--format=%(refname)', 'refs/remotes'])
const hasRemoteConflict = stdout.split(/\r?\n/).some((ref) => {
const trimmed = ref.trim()
if (isAllowedRemoteBaseRef(trimmed, allowedBaseRef)) {
return false
}
const shortRef = trimmed.replace(/^refs\/remotes\//, '')
return resolveLocalBranchName(trimmed, shortRef, remoteNames) === branchName
return resolveConfiguredRemoteBranchName(trimmed, remoteNames) === branchName
})
return hasRemoteConflict ? 'remote' : null
@@ -48,6 +53,20 @@ export async function getBranchConflictKind(
}
}
export function getBranchConflictKind(
path: string,
branchName: string,
allowedBaseRef?: string,
options: LocalGitExecOptions = {}
): Promise<BranchConflictKind | null> {
const execOptions = gitExecOptions(path, options)
return getBranchConflictKindViaExec(
(argv) => gitExecFileAsync(argv, execOptions),
branchName,
allowedBaseRef
)
}
function isAllowedRemoteBaseRef(refName: string, allowedBaseRef: string | undefined): boolean {
if (!allowedBaseRef) {
return false
+37
View File
@@ -215,6 +215,7 @@ describe('searchBaseRefs (widened glob)', () => {
it('allows creating a local branch from the selected matching remote base ref', async () => {
const sha = getHeadSha(tmpDir)
git(tmpDir, ['remote', 'add', 'origin', 'https://example.invalid/repo.git'])
createRemoteRef(tmpDir, 'origin/feature/something', sha)
const result = await getBranchConflictKind(
@@ -228,6 +229,10 @@ describe('searchBaseRefs (widened glob)', () => {
it('still reports a remote conflict for a different tracking ref with the same branch name', async () => {
const sha = getHeadSha(tmpDir)
// Why register both: a ref only tracks a branch when a remote actually owns its
// prefix, and this case is about a second *tracking* ref, not an orphan.
git(tmpDir, ['remote', 'add', 'origin', 'https://example.invalid/repo.git'])
git(tmpDir, ['remote', 'add', 'upstream', 'https://example.invalid/upstream.git'])
createRemoteRef(tmpDir, 'origin/feature/something', sha)
createRemoteRef(tmpDir, 'upstream/feature/something', sha)
@@ -236,6 +241,38 @@ describe('searchBaseRefs (widened glob)', () => {
).toBe('remote')
})
it('ignores a ref whose prefix matches no configured remote', async () => {
const sha = getHeadSha(tmpDir)
git(tmpDir, ['remote', 'add', 'origin', 'https://example.invalid/repo.git'])
createRemoteRef(tmpDir, 'mimic-fork/my-task', sha)
expect(await getBranchConflictKind(tmpDir, 'my-task', 'origin/main')).toBeNull()
})
it('stops treating leftover refs from a removed remote as conflicts', async () => {
const sha = getHeadSha(tmpDir)
git(tmpDir, ['remote', 'add', 'origin', 'https://example.invalid/repo.git'])
git(tmpDir, ['remote', 'add', 'fork', 'https://example.invalid/fork.git'])
createRemoteRef(tmpDir, 'fork/my-task', sha)
expect(await getBranchConflictKind(tmpDir, 'my-task', 'origin/main')).toBe('remote')
// Why not `remote remove`: that prunes the tracking refs too, which would pass
// with or without the ownership guard. Dropping only the config leaves the
// orphan ref behind, which is the state this guard exists for.
git(tmpDir, ['config', '--remove-section', 'remote.fork'])
expect(await getBranchConflictKind(tmpDir, 'my-task', 'origin/main')).toBeNull()
})
it('still reports a local conflict when an unrelated ref shares the name', async () => {
const sha = getHeadSha(tmpDir)
git(tmpDir, ['remote', 'add', 'origin', 'https://example.invalid/repo.git'])
createRemoteRef(tmpDir, 'mimic-fork/my-task', sha)
git(tmpDir, ['branch', 'my-task'])
expect(await getBranchConflictKind(tmpDir, 'my-task', 'origin/main')).toBe('local')
})
it('reports remote conflicts when the remote name contains a slash', async () => {
const sha = getHeadSha(tmpDir)
git(tmpDir, ['remote', 'add', 'foo/bar', 'https://example.invalid/repo.git'])
+31 -37
View File
@@ -149,19 +149,17 @@ export async function getPRComments(
if (!pullRequest) {
console.warn('Review threads response missing pullRequest; keeping REST results')
}
const graphQLIssueComments = (pullRequest?.comments?.nodes ?? []).map(
(c): PRComment => ({
id: c.databaseId,
author: c.author?.login ?? 'ghost',
authorAvatarUrl: c.author?.avatarUrl ?? '',
body: c.body ?? '',
createdAt: c.createdAt,
url: c.url,
isBot: c.author?.__typename === 'Bot',
reactionSubjectId: c.id,
reactions: mapGraphQLReactionGroups(c.reactionGroups)
})
)
const graphQLIssueComments = (pullRequest?.comments?.nodes ?? []).map((c): PRComment => ({
id: c.databaseId,
author: c.author?.login ?? 'ghost',
authorAvatarUrl: c.author?.avatarUrl ?? '',
body: c.body ?? '',
createdAt: c.createdAt,
url: c.url,
isBot: c.author?.__typename === 'Bot',
reactionSubjectId: c.id,
reactions: mapGraphQLReactionGroups(c.reactionGroups)
}))
if (graphQLIssueComments.length > 0) {
issueComments = graphQLIssueComments
}
@@ -169,19 +167,17 @@ export async function getPRComments(
graphQLReviewSummaries = pullRequest
? (pullRequest.reviews?.nodes ?? [])
.filter((review) => review.body?.trim())
.map(
(review): PRComment => ({
id: review.databaseId,
author: review.author?.login ?? 'ghost',
authorAvatarUrl: review.author?.avatarUrl ?? '',
body: review.body,
createdAt: review.createdAt,
url: review.url,
isBot: review.author?.__typename === 'Bot',
reactionSubjectId: review.id,
reactions: mapGraphQLReactionGroups(review.reactionGroups)
})
)
.map((review): PRComment => ({
id: review.databaseId,
author: review.author?.login ?? 'ghost',
authorAvatarUrl: review.author?.avatarUrl ?? '',
body: review.body,
createdAt: review.createdAt,
url: review.url,
isBot: review.author?.__typename === 'Bot',
reactionSubjectId: review.id,
reactions: mapGraphQLReactionGroups(review.reactionGroups)
}))
: undefined
const threads = pullRequest?.reviewThreads?.nodes ?? []
@@ -228,17 +224,15 @@ export async function getPRComments(
} else if (reviewsResult.status === 'fulfilled') {
reviewSummaries = (JSON.parse(reviewsResult.value.stdout) as RESTReview[])
.filter((r) => r.body?.trim())
.map(
(r): PRComment => ({
id: r.id,
author: r.user?.login ?? 'ghost',
authorAvatarUrl: r.user?.avatar_url ?? '',
body: r.body,
createdAt: r.submitted_at,
url: r.html_url,
isBot: r.user?.type === 'Bot'
})
)
.map((r): PRComment => ({
id: r.id,
author: r.user?.login ?? 'ghost',
authorAvatarUrl: r.user?.avatar_url ?? '',
body: r.body,
createdAt: r.submitted_at,
url: r.html_url,
isBot: r.user?.type === 'Bot'
}))
} else {
console.warn('Failed to fetch review summaries:', reviewsResult.reason)
}
+8 -10
View File
@@ -238,16 +238,14 @@ export async function getIssueBodyAndComments(
}[]
assignees?: { login: string }[]
}
const comments = (data.comments ?? []).map(
(comment, index): PRComment => ({
id: index,
author: comment.author?.login ?? 'ghost',
authorAvatarUrl: '',
body: comment.body ?? '',
createdAt: comment.createdAt,
url: comment.url ?? ''
})
)
const comments = (data.comments ?? []).map((comment, index): PRComment => ({
id: index,
author: comment.author?.login ?? 'ghost',
authorAvatarUrl: '',
body: comment.body ?? '',
createdAt: comment.createdAt,
url: comment.url ?? ''
}))
return {
body: data.body ?? '',
comments,
+13
View File
@@ -22,6 +22,19 @@ describe('parseOrcaYaml', () => {
})
})
it('parses a project requirement to finish setup before agent startup', () => {
const yaml = [
'setupAgentStartupPolicy: wait-for-setup',
'scripts:',
' setup: node install-project-skills.mjs'
].join('\n')
expect(parseOrcaYaml(yaml)).toEqual({
scripts: { setup: 'node install-project-skills.mjs' },
setupAgentStartupPolicy: 'wait-for-setup'
})
})
it('parses YAML with archive script only', () => {
const yaml = `scripts:\n archive: |\n echo "archiving"\n`
const result = parseOrcaYaml(yaml)
+21 -1
View File
@@ -302,7 +302,7 @@ describe('createSetupRunnerScript', () => {
}
})
it('omits waitForAgentStartup unless the repo explicitly waits for setup', async () => {
it('waits when either local or project policy requires completed setup', async () => {
gitExecFileSyncMock.mockReset()
gitExecFileSyncMock.mockReturnValue('/test/repo/.git/orca/setup-runner.sh\n')
const { createSetupRunnerScript } = await import('./worktree-runner-script')
@@ -318,6 +318,26 @@ describe('createSetupRunnerScript', () => {
createSetupRunnerScript(makeRepo('wait-for-setup'), '/test/worktree', 'echo setup')
.waitForAgentStartup
).toBe(true)
expect(
createSetupRunnerScript(
makeRepo('start-immediately'),
'/test/worktree',
'echo setup',
undefined,
undefined,
'wait-for-setup'
).waitForAgentStartup
).toBe(true)
expect(
createSetupRunnerScript(
makeRepo('wait-for-setup'),
'/test/worktree',
'echo setup',
undefined,
undefined,
'start-immediately'
).waitForAgentStartup
).toBe(true)
})
it('marks setup-runner terminals for the always-on credential guard', async () => {
+1
View File
@@ -55,6 +55,7 @@ export function hasHooksFile(repoPath: string): boolean {
// Why: detect unrecognised keys so the UI can suggest an update instead of showing a "could not be parsed" error.
const RECOGNIZED_ORCA_YAML_KEYS = new Set([
'scripts',
'setupAgentStartupPolicy',
'issueCommand',
'defaultTabs',
'environmentRecipes',
+131 -49
View File
@@ -154,6 +154,7 @@ import {
configureDevUserDataPath,
configureOrcaUserDataPathEnv,
disableUnsupportedChromiumFeatures,
optOutOfHiddenPageWakeUpThrottling,
enableMainProcessGpuFeatures,
installDevParentDisconnectQuit,
installDevParentSignalQuit,
@@ -170,8 +171,10 @@ import { enableRendererHeapHeadroom } from './startup/renderer-heap-headroom'
import { argvRequestsServeMode, normalizeServeModeArgv } from './startup/serve-mode-argv'
import { ensureVirtualDisplayForHeadlessServe } from './startup/ensure-virtual-display'
import {
clearGpuFallbackMarker,
readActiveGpuFallbackMarker,
writeGpuFallbackMarker,
type GpuFallbackMarker,
type GpuFallbackEnvironment,
type WindowsGpuFallbackEnvironment
} from './startup/gpu-fallback-marker'
@@ -182,10 +185,13 @@ import {
GpuCrashFallbackTracker,
isGpuFallbackCrashCandidate
} from './crash-reporting/gpu-crash-fallback-decision'
import { promptForGpuFallbackRestart } from './crash-reporting/gpu-fallback-restart-prompt'
import { engageGpuFallbackAfterCrashBurst } from './crash-reporting/gpu-fallback-engagement'
import { GpuCrashDiagnosticsRecorder } from './crash-reporting/gpu-crash-diagnostics'
import {
promptForGpuFallbackRestart,
type GpuFallbackRestartDecision
} from './crash-reporting/gpu-fallback-restart-prompt'
handleGpuFallbackRecoveredLaunch,
promptForGpuFallbackRecoveredLaunch
} from './crash-reporting/gpu-fallback-recovered-launch'
import {
shouldSuppressDevEducation,
suppressDevEducationForStore
@@ -462,8 +468,19 @@ const gpuCrashFallbackTracker = new GpuCrashFallbackTracker({
windowMs: DEFAULT_GPU_CRASH_FALLBACK_WINDOW_MS,
threshold: DEFAULT_GPU_CRASH_FALLBACK_THRESHOLD
})
let activeGpuFallbackMarker: GpuFallbackMarker | null = null
let gpuFallbackActiveThisLaunch = false
let gpuFeatureStatus: Electron.GPUFeatureStatus | null = null
const gpuCrashDiagnostics =
process.platform === 'win32'
? new GpuCrashDiagnosticsRecorder({
provider: {
getGPUInfo: (infoType) => app.getGPUInfo(infoType),
getGPUFeatureStatus: () => app.getGPUFeatureStatus()
},
recordBreadcrumb: (data) => recordDurableCrashBreadcrumb('gpu_crash_hardware', data)
})
: null
let localPtyStartupReady: Promise<void> = Promise.resolve()
let localPtyProviderStartupReady: Promise<void> = Promise.resolve()
const AGENT_STATE_CRASH_BREADCRUMB_MIN_INTERVAL_MS = 30_000
@@ -550,6 +567,7 @@ function updateGpuAccelerationAboutPanel(): void {
app.on('gpu-info-update', () => {
gpuFeatureStatus = app.getGPUFeatureStatus()
gpuCrashDiagnostics?.warm()
if (app.isReady()) {
updateGpuAccelerationAboutPanel()
}
@@ -982,6 +1000,8 @@ if (hasSingleInstanceLock) {
...getMainProcessLifecycleIdentity()
})
disableUnsupportedChromiumFeatures()
// Why: unconditional — a GPU-fallback launch skips enableMainProcessGpuFeatures() below.
optOutOfHiddenPageWakeUpThrottling()
configureElectronNetworkCompatibility()
enableRendererHeapHeadroom()
maybeApplyGpuFallbackForThisLaunch()
@@ -1573,6 +1593,7 @@ function openMainWindow(options: { revealOnDidFinishLoad?: boolean } = {}): Brow
})
window.once('show', () => {
logStartupMilestone('window-shown')
void presentGpuFallbackRecoveredLaunchPrompt(window)
})
const trayCreateFallback = setTimeout(createSystemTrayDeferred, TRAY_CREATE_FALLBACK_MS)
trayCreateFallback.unref?.()
@@ -1859,7 +1880,25 @@ function getWindowsGpuFallbackEnvironment(): WindowsGpuFallbackEnvironment | nul
return { ...environment, platform: 'win32' }
}
// Why: read the GPU-fallback marker before app.whenReady() so app.disableHardwareAcceleration() takes effect. Windows desktop only.
// Writes both crash-time and post-recovery consent states through one build-scoped path.
function persistGpuFallbackMarker(
userDataPath: string,
info: { engagedAt: number; crashesInWindow: number; userConfirmed: boolean }
): boolean {
const environment = getWindowsGpuFallbackEnvironment()
if (!environment) {
return false
}
try {
writeGpuFallbackMarker(userDataPath, info, environment)
return true
} catch (error) {
console.warn('[gpu-fallback] failed to persist marker:', error)
return false
}
}
// Read before app.whenReady() so app.disableHardwareAcceleration() takes effect. Windows desktop only.
function maybeApplyGpuFallbackForThisLaunch(): void {
if (isServeMode || process.platform !== 'win32') {
return
@@ -1868,6 +1907,7 @@ function maybeApplyGpuFallbackForThisLaunch(): void {
if (!marker) {
return
}
activeGpuFallbackMarker = marker
app.disableHardwareAcceleration()
const appliedSwitches = applyGpuFallbackCommandLineSwitches(app.commandLine, process.platform)
gpuFallbackActiveThisLaunch = true
@@ -1879,64 +1919,104 @@ function maybeApplyGpuFallbackForThisLaunch(): void {
})
}
async function presentGpuFallbackRecoveredLaunchPrompt(window: BrowserWindow): Promise<void> {
const marker = activeGpuFallbackMarker
if (!marker || marker.userConfirmed || window.isDestroyed() || isQuitting) {
return
}
// One prompt per process. A failure leaves the on-disk marker unconfirmed so the next launch retries.
activeGpuFallbackMarker = null
const userDataPath = app.getPath('userData')
await handleGpuFallbackRecoveredLaunch({
isQuitting: () => isQuitting,
prompt: () => promptForGpuFallbackRecoveredLaunch(window),
confirmSafeGraphics: () => {
persistGpuFallbackMarker(userDataPath, {
engagedAt: marker.engagedAt,
crashesInWindow: marker.crashesInWindow,
userConfirmed: true
})
},
clearSafeGraphics: () => clearGpuFallbackMarker(userDataPath),
onPromptFailed: (error) =>
console.warn('[gpu-fallback] failed to show recovered-launch prompt:', error),
onSafeGraphicsKept: () =>
recordDurableCrashBreadcrumb('gpu_fallback_safe_graphics_kept', {
crashesInWindow: marker.crashesInWindow
}),
restartWithHardware: () => {
isQuitting = true
relaunchApp('gpu-fallback', {
mode: 'hardware-retry',
crashesInWindow: marker.crashesInWindow
})
destroySystemTray()
app.exit(0)
}
})
}
// Why: a burst of GPU child crashes means HW acceleration is unusable — persist a build-scoped marker and offer software rendering.
async function handleGpuChildCrash(reason: string, exitCode: number | null): Promise<void> {
async function handleGpuChildCrash(
reason: string,
exitCode: number | null,
crashedAt: number
): Promise<void> {
// Software rendering already active or shutting down: nothing more to do.
if (gpuFallbackActiveThisLaunch || isQuitting || isServeMode) {
return
}
const result = gpuCrashFallbackTracker.recordGpuCrash(performance.now())
const result = gpuCrashFallbackTracker.recordGpuCrash(crashedAt)
if (!result.shouldEngageFallback) {
return
}
recordCrashBreadcrumb('gpu_fallback_engaged', {
reason,
exitCode,
crashesInWindow: result.crashesInWindow
})
const engagedAt = Date.now()
const window = mainWindow && !mainWindow.isDestroyed() ? mainWindow : undefined
let restartDecision: GpuFallbackRestartDecision
try {
restartDecision = await promptForGpuFallbackRestart(window)
} catch (error) {
console.warn('[gpu-fallback] failed to show restart prompt:', error)
return
}
const fallbackData = {
processReason: reason,
exitCode,
crashesInWindow: result.crashesInWindow
}
if (isQuitting) {
return
}
if (restartDecision !== 'restart') {
recordDurableCrashBreadcrumb('gpu_fallback_restart_deferred', fallbackData)
return
}
const environment = getWindowsGpuFallbackEnvironment()
if (!environment) {
return
}
try {
writeGpuFallbackMarker(
app.getPath('userData'),
{
engagedAt,
crashesInWindow: result.crashesInWindow
const userDataPath = app.getPath('userData')
await engageGpuFallbackAfterCrashBurst(
{ reason, exitCode, crashesInWindow: result.crashesInWindow, engagedAt: Date.now() },
{
isQuitting: () => isQuitting,
onEngaged: (engagement) =>
recordCrashBreadcrumb('gpu_fallback_engaged', {
reason: engagement.reason,
exitCode: engagement.exitCode,
crashesInWindow: engagement.crashesInWindow
}),
persistMarker: (engagement) =>
persistGpuFallbackMarker(userDataPath, {
engagedAt: engagement.engagedAt,
crashesInWindow: engagement.crashesInWindow,
userConfirmed: false
}),
confirmMarker: (engagement) => {
persistGpuFallbackMarker(userDataPath, {
engagedAt: engagement.engagedAt,
crashesInWindow: engagement.crashesInWindow,
userConfirmed: true
})
},
environment
)
} catch (error) {
console.warn('[gpu-fallback] failed to persist marker:', error)
return
}
isQuitting = true
relaunchApp('gpu-fallback', fallbackData)
// Why: app.exit(0) skips before-quit, so destroy the Windows tray manually to avoid a stale icon.
destroySystemTray()
app.exit(0)
clearMarker: () => clearGpuFallbackMarker(userDataPath),
promptForRestart: () =>
promptForGpuFallbackRestart(
mainWindow && !mainWindow.isDestroyed() ? mainWindow : undefined
),
onPromptFailed: (error) =>
console.warn('[gpu-fallback] failed to show restart prompt:', error),
onRestartDeferred: () =>
recordDurableCrashBreadcrumb('gpu_fallback_restart_deferred', fallbackData),
restartIntoSafeGraphics: () => {
isQuitting = true
relaunchApp('gpu-fallback', fallbackData)
// Why: app.exit(0) skips before-quit, so destroy the Windows tray manually to avoid a stale icon.
destroySystemTray()
app.exit(0)
}
}
)
}
function recordProcessGoneCrash(
@@ -3155,7 +3235,9 @@ void app.whenReady().then(async () => {
reason: details.reason
})
) {
void handleGpuChildCrash(details.reason, details.exitCode ?? null)
const crashedAt = performance.now()
void gpuCrashDiagnostics?.record()
void handleGpuChildCrash(details.reason, details.exitCode ?? null, crashedAt)
}
})
@@ -8,10 +8,10 @@ import {
describe('queueWatcherEvents', () => {
function events(count: number): WatcherEvent[] {
return Array.from(
{ length: count },
(_, index): WatcherEvent => ({ type: 'update', path: `/repo/file-${index}.ts` })
)
return Array.from({ length: count }, (_, index): WatcherEvent => ({
type: 'update',
path: `/repo/file-${index}.ts`
}))
}
it('keeps precise events while the batch remains under the overflow limit', () => {
@@ -73,10 +73,10 @@ describe('local filesystem watcher large batches', () => {
{ worktreePath }
)
const events = Array.from(
{ length: 200_000 },
(_, index): WatcherEvent => ({ type: 'delete', path: join(worktreePath, `file-${index}`) })
)
const events = Array.from({ length: 200_000 }, (_, index): WatcherEvent => ({
type: 'delete',
path: join(worktreePath, `file-${index}`)
}))
expect(() => watcherCallback?.(null, events)).not.toThrow()
await closeAllWatchers()
@@ -97,13 +97,10 @@ describe('local filesystem watcher large batches', () => {
await handlers['fs:watchWorktree']({ sender }, { worktreePath })
watcherCallback?.(
null,
Array.from(
{ length: 5_001 },
(_, index): WatcherEvent => ({
type: 'update',
path: join(worktreePath, `file-${index}.txt`)
})
)
Array.from({ length: 5_001 }, (_, index): WatcherEvent => ({
type: 'update',
path: join(worktreePath, `file-${index}.txt`)
}))
)
await vi.advanceTimersByTimeAsync(150)
@@ -107,13 +107,10 @@ describe('remote filesystem watcher batching', () => {
await handlers['fs:watchWorktree']({ sender }, WATCH_ARGS)
watchCallbacks[0](
Array.from(
{ length: 6_000 },
(_unused, index): FsChangeEvent => ({
kind: 'update',
absolutePath: `${WORKTREE_PATH}/file-${index}.ts`
})
)
Array.from({ length: 6_000 }, (_unused, index): FsChangeEvent => ({
kind: 'update',
absolutePath: `${WORKTREE_PATH}/file-${index}.ts`
}))
)
await vi.advanceTimersByTimeAsync(150)
+6 -10
View File
@@ -169,17 +169,13 @@ export function registerOrcaProfileHandlers(
store: Store,
options: RegisterOrcaProfileHandlersOptions = {}
): void {
ipcMain.handle(
'orcaProfiles:list',
(): OrcaProfileListResult => ({
...getOrcaProfileListState(),
multiProfileUi: isMultiProfileUiEnabled()
})
)
ipcMain.handle('orcaProfiles:list', (): OrcaProfileListResult => ({
...getOrcaProfileListState(),
multiProfileUi: isMultiProfileUiEnabled()
}))
ipcMain.handle(
'orcaProfiles:authStatus',
(): OrcaProfileAuthStatus => getCurrentOrcaProfileAuthStatus(getProfileUserDataPath())
ipcMain.handle('orcaProfiles:authStatus', (): OrcaProfileAuthStatus =>
getCurrentOrcaProfileAuthStatus(getProfileUserDataPath())
)
ipcMain.handle(
+2 -3
View File
@@ -81,9 +81,8 @@ export function registerPetHandlers(): void {
}
})
ipcMain.handle(
'pet:importPetBundle',
async (event): Promise<CustomPet | null> => importPetBundle(event)
ipcMain.handle('pet:importPetBundle', async (event): Promise<CustomPet | null> =>
importPetBundle(event)
)
ipcMain.handle(
@@ -182,13 +182,10 @@ describe('createRemoteWatcherEventBatch', () => {
for (let i = 0; i < 3; i++) {
batch.push(
Array.from(
{ length: 50 },
(_unused, index): FsChangeEvent => ({
kind: 'update',
absolutePath: `${ROOT}/wave-${i}-${index}.ts`
})
)
Array.from({ length: 50 }, (_unused, index): FsChangeEvent => ({
kind: 'update',
absolutePath: `${ROOT}/wave-${i}-${index}.ts`
}))
)
}
vi.advanceTimersByTime(150)

Some files were not shown because too many files have changed in this diff Show More