Merge remote-tracking branch 'origin/main' into skills-share

# Conflicts:
#	src/cli/runtime/websocket-transport.test.ts
#	src/main/runtime/rpc/errors.ts
#	src/renderer/src/web/web-preload-api.test.ts
#	src/shared/remote-runtime-client.test.ts
#	src/shared/remote-runtime-client.ts
#	src/shared/remote-runtime-request-connection.test.ts
#	src/shared/remote-runtime-request-connection.ts
#	src/shared/remote-runtime-shared-control-connection.test.ts
#	src/shared/remote-runtime-shared-control-frame-handler.ts
This commit is contained in:
Jinwoo-H
2026-08-14 17:36:36 -04:00
2735 changed files with 48428 additions and 16486 deletions
+4
View File
@@ -29,6 +29,9 @@ on:
- 'electron.vite.config.ts'
- 'config/build-plugins/**'
- 'src/main/daemon/**'
- 'src/main/providers/local-pty-provider.ts'
- 'src/main/providers/windows-shell-args.ts'
- 'src/main/providers/windows-shell-preflight-runtime.windows.test.ts'
- 'native/computer-use-macos/**'
- 'native/computer-use-linux/**'
- 'native/computer-use-windows/**'
@@ -106,6 +109,7 @@ jobs:
src/main/computer/macos-native-provider-socket.test.ts
src/main/computer/macos-computer-use-permissions.test.ts
src/main/computer/macos-computer-use-permission-status.test.ts
src/main/providers/windows-shell-preflight-runtime.windows.test.ts
src/main/computer/desktop-script-provider-client.test.ts
src/main/computer/desktop-script-provider-cache.test.ts
src/main/computer/desktop-script-provider-actions.test.ts
+5 -1
View File
@@ -244,8 +244,12 @@ jobs:
if printf '%s\n' "${TEST_FILES[@]}" | grep -qx 'tests/e2e/ephemeral-vm-provisioned-root.spec.ts'; then
E2E_ENV+=(ORCA_E2E_SSH_DOCKER=1)
fi
E2E_PROJECT_ARGS=()
if grep -l '@headful' "${TEST_FILES[@]}" >/dev/null; then
E2E_PROJECT_ARGS+=(--project=electron-headful)
fi
xvfb-run --auto-servernum env "${E2E_ENV[@]}" \
pnpm run test:e2e "${TEST_FILES[@]}" --workers=1
pnpm run test:e2e "${TEST_FILES[@]}" --workers=1 "${E2E_PROJECT_ARGS[@]}"
- name: Upload Playwright traces
if: failure()
-2
View File
@@ -234,7 +234,6 @@ inline src/renderer/src/components/sidebar/RemoteFileBrowser.tsx
inline src/renderer/src/components/sidebar/WorkspaceKanbanDrawer.tsx
inline src/renderer/src/components/sidebar/WorktreeContextMenu.tsx
inline src/renderer/src/components/sidebar/WorktreeList.lineage-child-card.test.ts
inline src/renderer/src/components/sidebar/WorktreeList.tsx
inline src/renderer/src/components/sidebar/use-workspace-kanban-area-selection.ts
inline src/renderer/src/components/sidebar/worktree-list-groups.test.ts
inline src/renderer/src/components/sidebar/worktree-list-groups.ts
@@ -259,7 +258,6 @@ inline src/renderer/src/components/terminal-pane/terminal-link-handlers.test.ts
inline src/renderer/src/components/terminal-pane/use-terminal-pane-context-menu.ts
inline src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects.test.ts
inline src/renderer/src/components/terminal-pane/use-terminal-pane-lifecycle.ts
inline src/renderer/src/components/workspace-cleanup/WorkspaceCleanupDialog.tsx
inline src/renderer/src/hooks/useAutomationDispatchEvents.ts
inline src/renderer/src/hooks/useComposerState.ts
inline src/renderer/src/hooks/useEditorExternalWatch.ts
+358 -13
View File
@@ -1,6 +1,6 @@
{
"schemaVersion": 1,
"updatedAt": "2026-08-12",
"updatedAt": "2026-08-13",
"policy": {
"maturityLevels": ["experimental", "soak", "blocking", "accepted-gap", "deprecated"],
"blockingPromotion": {
@@ -1452,6 +1452,181 @@
],
"demotionRule": "Quarantine the Electron journey only with a linked product or harness defect; demote if activation changes the owner/runtime/daemon/PTY identity, loses prior output, opens before provider readiness, or fails to honor a committed quit."
},
{
"id": "runtime.renderer-graph-reload-termination",
"title": "Renderer graph reloads terminate under exact document and window authority",
"maturity": "experimental",
"protection": "partial",
"owner": "runtime-platform",
"layer": "electron-runtime-contract",
"surfaces": [
"desktop renderer reload and crash recovery",
"headless-to-desktop promotion",
"CLI and paired-runtime graph availability"
],
"platforms": ["macos", "linux", "windows"],
"providers": ["local", "daemon", "ssh", "paired-runtime"],
"coveredPlatforms": ["macos"],
"coveredProviders": ["local", "daemon", "paired-runtime"],
"coverageNotes": "Deterministic lifecycle, IPC, window-notification, and runtime ownership tests cover generation replacement, stale callbacks, same-frame old-document publication, blocked external navigation, cancelled renderer navigation, concurrent provisional starts, process loss, timeout cleanup, headless fallback, and competing windows. Isolated macOS Electron validation covers cancelled navigation, forced renderer recovery, and headless serve promotion with live daemon-backed PTY identity and I/O. Linux, Windows, WSL, SSH promotion, and mixed-version desktop/server live runs remain uncollected; the renderer generation stays local to Electron IPC and does not change the remote wire.",
"motivatingLinks": [
"https://github.com/stablyai/orca/issues/14066",
"https://linear.app/stably/issue/STA-4016",
"https://github.com/stablyai/orca/pull/14070"
],
"invariant": "Every genuine renderer graph reload revision settles exactly once, while a blocked external navigation that preserves the current document never starts a revision. A cancelled renderer-document navigation restores authority only when that same renderer survived; cancellation after an earlier document committed remains fenced until publication or timeout. Replacing a reload cancels only the prior revision; its timer and callbacks cannot settle a newer revision. A desktop reload succeeds only from a publication carrying a new renderer-document generation, while a failed headless promotion may recover only from its originating window. Timeout, renderer loss, and close reach a terminal graph state, and successful desktop publication permanently retires headless sentinel fallback until a new headless runtime is established.",
"oracle": "Start a blocked external main-frame navigation and require the current renderer graph and notification path to remain authoritative. Start a same-renderer navigation, cancel it before commit, and require the renderer canary and ready authority to survive; coalesce overlapping provisional starts into one fence, and keep an earlier committed reload fenced if a later navigation is cancelled. Drive two genuine reloads half a timeout apart and require the first deadline to leave the second revision reloading while the second deadline alone makes it unavailable. During the second revision publish from the prior renderer document through the still-current main frame and require rejection without graph mutation or timeout cancellation. Restore headless authority after failed promotion, publish from a competing window, and require rejection while the sentinel remains authoritative. Then force a real renderer crash and separately promote an isolated headless serve owner; require runtime graph readiness, stable runtime/main/daemon/PTY identities, recovered visible terminal output, and post-recovery input/output.",
"commands": [
"pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/runtime-graph-reload-lifecycle.test.ts src/main/window/runtime-renderer-notification-sender.test.ts src/main/window/renderer-document-navigation.test.ts src/main/ipc/runtime.test.ts src/main/window/attach-main-window-services.test.ts src/main/runtime/orca-runtime.test.ts src/renderer/src/runtime/sync-runtime-graph-scheduling.test.ts src/renderer/src/runtime/sync-runtime-graph-payload-partition.test.ts",
"pnpm exec electron-vite build --mode e2e",
"SKIP_BUILD=1 pnpm exec playwright test tests/e2e/renderer-blocked-navigation-graph-authority.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1",
"SKIP_BUILD=1 pnpm exec playwright test e2e/renderer-crash-recovery-terminal-input.spec.ts tests/e2e/renderer-crash-recovery-terminal-input.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1",
"SKIP_BUILD=1 pnpm exec playwright test e2e/headless-serve-desktop-activation.spec.ts tests/e2e/headless-serve-desktop-activation.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1"
],
"testFiles": [
"src/main/runtime/runtime-graph-reload-lifecycle.test.ts",
"src/main/window/runtime-renderer-notification-sender.test.ts",
"src/main/window/renderer-document-navigation.test.ts",
"src/main/ipc/runtime.test.ts",
"src/main/window/attach-main-window-services.test.ts",
"src/main/runtime/orca-runtime.test.ts",
"src/renderer/src/runtime/sync-runtime-graph-scheduling.test.ts",
"src/renderer/src/runtime/sync-runtime-graph-payload-partition.test.ts",
"tests/e2e/renderer-blocked-navigation-graph-authority.spec.ts",
"tests/e2e/renderer-crash-recovery-terminal-input.spec.ts",
"tests/e2e/headless-serve-desktop-activation.spec.ts"
],
"assertionRefs": [
{
"file": "src/main/runtime/runtime-graph-reload-lifecycle.test.ts",
"assertions": [
"each revision settles exactly once",
"replacement cancels the old revision and fences its timeout",
"settlement and timeout release their timers"
]
},
{
"file": "src/main/runtime/orca-runtime.test.ts",
"assertions": [
"only the newest reload owns the terminal timeout",
"a superseded renderer-document generation cannot settle a reload",
"cancelled navigation restores only the renderer or headless authority that actually survived",
"cancellation after an earlier document commit stays fenced until its restarted timeout",
"failed headless promotion remains pinned to its originating window",
"successful desktop publication retires headless fallback"
]
},
{
"file": "src/main/ipc/runtime.test.ts",
"assertions": [
"graph publication requires a non-empty renderer generation",
"only the current BrowserWindow main frame may publish"
]
},
{
"file": "src/main/window/runtime-renderer-notification-sender.test.ts",
"assertions": [
"renderer failure is contained once per load generation",
"late events cannot revive a closed sender"
]
},
{
"file": "src/main/window/renderer-document-navigation.test.ts",
"assertions": [
"blocked external navigation does not start a renderer generation",
"only the packaged renderer file or development origin starts a renderer generation",
"concurrent provisional starts share one reload fence and cancel only after every attempt fails"
]
},
{
"file": "tests/e2e/renderer-blocked-navigation-graph-authority.spec.ts",
"assertions": [
"blocked external navigation starts and stops loading without replacing the renderer document",
"runtime graph epoch, readiness, and authoritative window remain unchanged",
"a later listener can cancel a renderer navigation while its canary and authoritative graph recover"
]
},
{
"file": "tests/e2e/renderer-crash-recovery-terminal-input.spec.ts",
"assertions": [
"main observes renderer process loss and a completed recovery load",
"the same daemon-backed PTY remains reachable through transport and direct-write probes"
]
},
{
"file": "tests/e2e/headless-serve-desktop-activation.spec.ts",
"assertions": [
"promotion preserves main owner, runtime, daemon, and PTY identity",
"terminal output survives promotion and post-promotion input/output remains live"
]
}
],
"evidenceRuns": [
{
"date": "2026-08-14",
"runner": "local",
"platform": "macos",
"command": "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/runtime-graph-reload-lifecycle.test.ts src/main/window/runtime-renderer-notification-sender.test.ts src/main/window/renderer-document-navigation.test.ts src/main/ipc/runtime.test.ts src/main/window/attach-main-window-services.test.ts src/main/runtime/orca-runtime.test.ts src/renderer/src/runtime/sync-runtime-graph-scheduling.test.ts src/renderer/src/runtime/sync-runtime-graph-payload-partition.test.ts",
"result": "passed",
"durationSeconds": 12,
"summary": "Eight focused files passed with 1,175 tests and one pre-existing skip after the merge from latest main. The suite covers exact reload settlement, timeout replacement, cancelled-navigation recovery, concurrent provisional starts, document and window authority, notification containment, preload IPC, and renderer publication scheduling."
},
{
"date": "2026-08-14",
"runner": "local",
"platform": "macos",
"command": "SKIP_BUILD=1 pnpm exec playwright test tests/e2e/renderer-blocked-navigation-graph-authority.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1",
"result": "passed",
"durationSeconds": 8,
"summary": "Two isolated Electron flows passed: blocked external navigation left epoch and authority unchanged, and a same-renderer navigation cancelled by a later listener preserved its canary while returning the incremented graph generation to ready under the same authoritative window."
},
{
"date": "2026-08-13",
"runner": "local",
"platform": "macos",
"command": "SKIP_BUILD=1 pnpm exec playwright test e2e/renderer-crash-recovery-terminal-input.spec.ts tests/e2e/renderer-crash-recovery-terminal-input.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1",
"result": "passed",
"durationSeconds": 18,
"summary": "Two forced renderer process deaths each produced a main-observed recovery load; the same daemon-backed PTY remained live through renderer transport and direct-write probes."
},
{
"date": "2026-08-13",
"runner": "local",
"platform": "macos",
"command": "SKIP_BUILD=1 pnpm exec playwright test e2e/headless-serve-desktop-activation.spec.ts tests/e2e/headless-serve-desktop-activation.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1",
"result": "passed",
"durationSeconds": 10,
"summary": "The isolated headless serve owner promoted to a real renderer while preserving main owner, runtime, daemon, and PTY identity. Pre-promotion output remained visible and post-promotion terminal input/output succeeded; a full-window proof image records both markers."
}
],
"runtimeBudget": {
"p95Seconds": 420,
"scope": "focused deterministic contracts plus isolated forced-crash and headless-promotion Electron journeys"
},
"flakeHistory": {
"status": "unknown",
"evidence": "Fresh deterministic and two isolated Electron journeys are green; CI soak history is pending."
},
"redGreenEvidence": {
"status": "complete",
"evidence": "A byte-identical three-case oracle was rerun on 2026-08-13. Latest main ee8dd4796e failed all three: reload remained reloading after 22.5 seconds, the prior renderer document settled the replacement, and a competing window stole failed-promotion authority. Submitted PR head feeca6c22f passed the bounded replacement timeout but failed both authority cases. The locally rewritten candidate passed all three. Disabling the local document-generation and pending-window fences reproduces the submitted-head failures; disabling the cancelled-navigation classifier leaves the graph reloading and increments its epoch in the new live oracle."
},
"performanceBudget": {
"required": true,
"evidence": "Fresh delegated audit passed with no blocking, major, or actionable minor findings. The candidate retains at most one unref'd timer, reuses the existing renderer document UUID, adds about 67 serialized bytes to each already-coalesced local graph IPC, and performs constant-time generation/window checks. It adds no polling, subprocess, network request, remote frame, provider scan, or steady-state fanout; a dedicated benchmark would measure existing graph/structured-clone noise rather than meaningful incremental work."
},
"promotionCriteria": [
"Collect CI and soak history without unexplained flakes.",
"Collect live Linux and Windows desktop reload evidence.",
"Add live SSH or WSL promotion evidence if provider-specific graph restoration diverges."
],
"knownGaps": [
"The live journeys cover macOS and daemon-backed local terminals, not Linux desktop, Windows, WSL, or SSH promotion.",
"The same-frame stale-document race is deterministic fault injection because Playwright cannot schedule an invoke precisely between did-start-loading and frame replacement.",
"No mixed-version live run is required because rendererGeneration is confined to same-version preload/main Electron IPC and the remote runtime wire is unchanged."
],
"demotionRule": "Demote if a reload revision settles more than once, a stale document or competing window can publish, a timer survives settlement, desktop failure stays indefinitely reloading, headless fallback returns after successful promotion, or either isolated Electron journey loses runtime or PTY identity."
},
{
"id": "runtime.websocket-heartbeat-cadence",
"title": "Runtime WebSockets enter an owned shared heartbeat cadence",
@@ -1817,19 +1992,24 @@
"providers": ["remote-runtime"],
"coveredPlatforms": ["macos"],
"coveredProviders": ["remote-runtime"],
"coverageNotes": "A headed macOS desktop host and separate paired client exercise the Explorer eye-button path with a real remote file URL, host browser DOM, renderer split layout, background preview creation, explicit browser activation, browser close, forced capability rejection after caller preflight, and a forced post-create reconciliation timeout. The same fault oracles pass against a live headless server. Unit contracts cover absent, unknown, and mixed-version capabilities, visible rejection, empty-split cleanup, owning-runtime routing, exact client materialization, confirmed rollback, ambiguous cleanup, and teardown. Linux and Windows live journeys remain gaps.",
"coverageNotes": "A headed macOS desktop host and separate paired client exercise the Explorer eye-button path with a real remote file URL, host browser DOM, renderer split layout, background preview creation, explicit browser activation, browser close, forced capability rejection after caller preflight, a forced post-create reconciliation timeout, and navigation held beyond the client create deadline. The same reconciliation fault oracles pass against a live headless server. Unit contracts cover absent, unknown, and mixed-version capabilities, visible rejection, empty-split cleanup, owning-runtime routing, canonical preallocated page identity, navigation-independent acknowledgement, exact client materialization, confirmed rollback, ambiguous cleanup, and teardown. Linux and Windows live journeys remain gaps.",
"motivatingLinks": [
"https://linear.app/stably/issue/STA-3872/unable-to-open-html-files-in-remote-server",
"https://linear.app/stably/issue/STA-4025/p1-browser-create-reports-success-when-tab-reconcile-fails-after-pr",
"https://linear.app/stably/issue/STA-4063/p1-paired-html-preview-rejects-outside-cleanup-after-pr-13909",
"https://linear.app/stably/issue/STA-4231/p1-owner-pinned-paired-browser-create-can-time-out-after-host-tab",
"https://github.com/stablyai/orca/pull/14363",
"https://github.com/stablyai/orca/pull/13876"
],
"invariant": "Opening one HTML preview from a paired client succeeds only after exactly one browser page on the worktree-owning runtime materializes as exactly one client browser workspace and unified tab in the requested group. Capability rejection before host mutation must preserve the original error, issue no RPC, surface a failure toast, and remove only a caller-declared newly-created empty split. Post-create reconciliation failure requires exact rollback; ambiguous rollback rejects without local fallback. Preview focus and close preserve the source editor and terminal.",
"oracle": "Run one unchanged contract oracle for direct create and side-preview callers with absent status, unknown capabilities, and a mixed-version host. Require the original unsupported error or visible toast, zero RPCs, and no retained new split. Run it on the scan target, latest main, candidate, and candidate with the cleanup boundary disabled. In headed paired Electron, arm capability rejection after the caller preflight, invoke the visible HTML side-preview action, require the failure toast, and compare exact host/client browser, editor, terminal, and group inventories to baseline. Repeat against headless serve. Retain the separate exact-page reconciliation rollback oracle.",
"invariant": "Opening one HTML preview from a paired client succeeds only after exactly one browser page on the worktree-owning runtime materializes as exactly one client browser workspace and unified tab in the requested group. Owner-pinned creation returns the canonical host page identity before navigation readiness; delayed navigation cannot turn a created page into an unidentifiable failure or a duplicate retry. Capability rejection before host mutation must preserve the original error, issue no RPC, surface a failure toast, and remove only a caller-declared newly-created empty split. Post-create reconciliation failure requires exact rollback; ambiguous rollback rejects without local fallback. Preview focus and close preserve the source editor and terminal.",
"oracle": "Run one unchanged contract oracle for direct create and side-preview callers with absent status, unknown capabilities, and a mixed-version host. Require the original unsupported error or visible toast, zero RPCs, and no retained new split. Run it on the scan target, latest main, candidate, and candidate with the cleanup boundary disabled. In headed paired Electron, arm capability rejection after the caller preflight, invoke the visible HTML side-preview action, require the failure toast, and compare exact host/client browser, editor, terminal, and group inventories to baseline. Hold a real navigation response beyond the 15-second client deadline after host creation; require the first RPC to return the exact host inventory page ID, one host page, and no retry. Repeat reconciliation faults against headless serve. Retain the separate exact-page reconciliation rollback oracle.",
"commands": [
"pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/orca-runtime-browser.test.ts src/main/runtime/rpc/methods/browser.test.ts src/renderer/src/lib/file-preview.test.ts src/renderer/src/runtime/web-session-browser-placement.test.ts src/renderer/src/runtime/web-runtime-session.test.ts src/renderer/src/runtime/web-session-tabs-sync.test.ts src/renderer/src/runtime/remote-server-parity.test.ts",
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/runtime/web-runtime-browser-materialization.test.ts",
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/runtime/web-runtime-browser-capability-cleanup.test.ts src/renderer/src/lib/file-preview-capability-cleanup.test.ts",
"pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/orca-runtime-browser.test.ts src/renderer/src/runtime/web-runtime-session.test.ts",
"pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/orca-runtime.test.ts src/main/runtime/orca-runtime-browser.test.ts src/renderer/src/hooks/useIpcEvents.test.ts src/renderer/src/store/slices/browser.test.ts src/renderer/src/runtime/web-runtime-session.test.ts",
"SKIP_BUILD=1 pnpm exec playwright test tests/e2e/paired-browser-create-navigation-deadline.spec.ts --config tests/playwright.config.ts --project electron-headful --workers=1",
"ORCA_E2E_WEB_CLIENT=1 ORCA_E2E_FORCE_HEADFUL=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm exec playwright test tests/e2e/paired-remote-html-browser-focus.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1",
"ORCA_E2E_WEB_CLIENT=1 ORCA_E2E_FORCE_HEADFUL=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm exec playwright test tests/e2e/paired-remote-html-browser-focus.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1 --repeat-each=3",
"SKIP_BUILD=1 ORCA_E2E_WEB_CLIENT=1 ORCA_E2E_FORCE_HEADFUL=1 pnpm exec playwright test tests/e2e/paired-remote-html-browser-focus.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1",
@@ -1848,7 +2028,10 @@
"src/renderer/src/runtime/web-runtime-session.test.ts",
"src/renderer/src/runtime/web-session-tabs-sync.test.ts",
"src/renderer/src/runtime/remote-server-parity.test.ts",
"src/renderer/src/hooks/useIpcEvents.test.ts",
"src/renderer/src/store/slices/browser.test.ts",
"tests/e2e/paired-remote-html-browser-focus.spec.ts",
"tests/e2e/paired-browser-create-navigation-deadline.spec.ts",
"tests/e2e/paired-browser-creation-reconciliation-failure.spec.ts"
],
"assertionRefs": [
@@ -1884,12 +2067,36 @@
"file": "src/renderer/src/runtime/web-runtime-session.test.ts",
"assertions": [
"a post-create timeout returns false only after exact host close and a confirming snapshot",
"owner-pinned creation launches navigation separately and materializes without awaiting it",
"a lost create acknowledgement cleans the capability-gated canonical page ID",
"unconfirmed cleanup rejects so callers cannot create a competing local fallback",
"the canonical placement and a newly-created empty split are released on rollback",
"one failed preview cannot remove a shared split still reserved after an environment ownership change",
"if every overlapping creation fails, the last reservation closes the caller-created empty split"
]
},
{
"file": "src/main/runtime/orca-runtime-browser.test.ts",
"assertions": [
"explicit renderer cleanup bypasses guest registration while retaining typed missing-page behavior",
"offscreen cleanup rejects a page outside the requested worktree"
]
},
{
"file": "src/renderer/src/hooks/useIpcEvents.test.ts",
"assertions": [
"renderer-backed creation preserves the requested canonical page ID while keeping its workspace identity independent",
"closing an absent renderer page returns browser_tab_not_found instead of an untyped failure",
"explicit cleanup cannot close a page owned by another worktree"
]
},
{
"file": "src/renderer/src/store/slices/browser.test.ts",
"assertions": [
"a requested canonical page ID is stored on the page rather than reused as its workspace ID",
"duplicate canonical page IDs are rejected before a second tab can materialize"
]
},
{
"file": "src/renderer/src/runtime/web-session-browser-placement.test.ts",
"assertions": [
@@ -1921,6 +2128,13 @@
"preview creation leaves the editor active, an explicit browser click is authoritative, and close preserves the editor and terminal"
]
},
{
"file": "tests/e2e/paired-browser-create-navigation-deadline.spec.ts",
"assertions": [
"a held navigation exceeds the former create deadline while the first RPC returns the exact authoritative host page ID",
"host inventory contains exactly one new page and the user retry branch is never entered"
]
},
{
"file": "tests/e2e/paired-browser-creation-reconciliation-failure.spec.ts",
"assertions": [
@@ -2075,6 +2289,24 @@
"result": "failed",
"durationSeconds": 12.3,
"summary": "Candidate d05217c40d with only the side-preview focusOnCreate:false call disabled failed the byte-identical 2ec1dcf1 oracle because preview creation activated the browser instead of preserving editor focus."
},
{
"date": "2026-08-13",
"runner": "local",
"platform": "macos",
"command": "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/orca-runtime.test.ts src/main/runtime/orca-runtime-browser.test.ts src/renderer/src/hooks/useIpcEvents.test.ts src/renderer/src/store/slices/browser.test.ts src/renderer/src/runtime/web-runtime-session.test.ts",
"result": "passed",
"durationSeconds": 10.83,
"summary": "Five post-review host, IPC, store, and client lifecycle files passed 1,356 tests with one pre-existing skip, including both shards that failed before the canonical page/workspace identity and typed missing-page cleanup corrections."
},
{
"date": "2026-08-13",
"runner": "local",
"platform": "macos",
"command": "SKIP_BUILD=1 pnpm exec playwright test tests/e2e/paired-browser-create-navigation-deadline.spec.ts --config tests/playwright.config.ts --project electron-headful --workers=1",
"result": "passed",
"durationSeconds": 18.1,
"summary": "Two isolated headed Electron/CDP journeys passed: the direct owner-pinned call returned one canonical host ID before held navigation, and the real remote-pane context-menu route materialized that one page without retry or duplicate."
}
],
"runtimeBudget": {
@@ -2087,11 +2319,11 @@
},
"redGreenEvidence": {
"status": "complete",
"evidence": "The unchanged STA-4063 five-assertion oracle is red 5/5 on scan e4c278eb52 and latest main bc2e30000b, partially green 2/5 on pre-fix PR #14100, green 5/5 on candidate 14b5877a75, and red 3/5 when only the new transaction cleanup boundary is disabled on final candidate a9e6fb7716. The earlier STA-4025 transaction oracle remains red on the post-#13909 base and green on this candidate. The byte-identical 2ec1dcf1 Electron oracle remains red on latest main 346e59c879, green on candidate d05217c40d, and red with only side-preview focus disabled; historical 5806b23f evidence separately proves owning-runtime routing."
"evidence": "The unchanged STA-4063 five-assertion oracle is red 5/5 on scan e4c278eb52 and latest main bc2e30000b, partially green 2/5 on pre-fix PR #14100, green 5/5 on candidate 14b5877a75, and red 3/5 when only the new transaction cleanup boundary is disabled on final candidate a9e6fb7716. The earlier STA-4025 transaction oracle remains red on the post-#13909 base and green on this candidate. The byte-identical STA-4231 headed oracle is red on latest-main b19d99bd00: both 15-second creates return runtime_timeout without a page ID while the host retains two distinct pages after retry. The relevant behavior is unchanged through 0a0ae974d4. The candidate is green with one returned canonical ID, one host/client page, and no retry; disabling only the navigation-independent acknowledgement is red with the same two orphaned pages. The byte-identical 2ec1dcf1 Electron oracle remains red on latest main 346e59c879, green on candidate d05217c40d, and red with only side-preview focus disabled; historical 5806b23f evidence separately proves owning-runtime routing."
},
"performanceBudget": {
"required": true,
"evidence": "The transaction adds no timer, polling, retry, subprocess, RPC method, stream opcode, or host publication. Capability rejection performs zero RPCs and one bounded empty-group close. Successful creation adds one post-operation list; known-page failure adds one exact close and one confirming list. The E2E-only one-shot faults are build-gated, retain one promise/flag and a bounded page-id set, and reset after each run. Pending client placements remain capped at 128 and clear on adoption, failure, or teardown."
"evidence": "The transaction adds no timer, polling, retry, subprocess, RPC method, stream opcode, or global host publication. Owner-pinned success performs one create RPC followed by one asynchronous goto, retains one page, and performs zero retry or cleanup calls. A lost create acknowledgement performs one exact close by the preallocated canonical ID and at most one confirming snapshot. Headless goto publishes one worktree-scoped session snapshot after navigation. Capability rejection performs zero RPCs and one bounded empty-group close. The E2E-only one-shot faults are build-gated, retain one promise/flag and a bounded page-id set, and reset after each run. Pending client placements remain capped at 128 and clear on adoption, failure, or teardown."
},
"promotionCriteria": [
"Collect Linux and physical Windows paired-server evidence.",
@@ -8413,10 +8645,14 @@
"providers": ["local", "daemon", "ssh", "remote-runtime"],
"coveredPlatforms": ["macos"],
"coveredProviders": ["local"],
"coverageNotes": "Local macOS evidence covers the runtime PTY write contract, a live dev-runtime CLI repro, and a Haiku composer trace. The Claude render gate stays on the PTY-owning runtime, so paired and SSH callers use the same provider write/output path without a wire change. SSH, daemon, remote-runtime, Linux, and Windows live journeys remain gaps.",
"motivatingLinks": ["https://github.com/stablyai/orca/issues/7226"],
"invariant": "Injected orchestration task prompts for recognized agent CLIs must send the prompt body inside one bracketed-paste frame, sanitize embedded ESC bytes, preserve chunk boundaries without losing the frame, and submit exactly once only after the agent can accept Enter. Claude must emit a post-paste composer marker and then settle, or reach the bounded fallback first.",
"oracle": "Runtime tests assert the exact PTY write sequence, failure cleanup, and a Claude-shaped multi-frame render where the legacy 500 ms delay and first show-cursor marker are both premature. The candidate waits for post-marker output quiescence before one submit, resets that window on later frames, and still submits once at the hard deadline if output never settles. Orchestration tests assert dispatch/coordinator use the agent prompt path; the live CLI harness covers long Codex-like framing.",
"coverageNotes": "Local macOS evidence covers the runtime PTY write contract, live Codex orchestration, a dev-runtime CLI repro, and Claude and Codex composer traces. The render gate stays on the PTY-owning runtime, so paired and SSH callers use the same provider write/output path without a wire change. SSH, daemon, remote-runtime, Linux, and Windows live journeys remain gaps.",
"motivatingLinks": [
"https://github.com/stablyai/orca/issues/7226",
"https://github.com/stablyai/orca/issues/13821",
"https://github.com/stablyai/orca/issues/14347"
],
"invariant": "Injected orchestration task prompts for recognized agent CLIs must send the prompt body inside one bracketed-paste frame, sanitize embedded ESC bytes, preserve chunk boundaries without losing the frame, and submit exactly once only after the agent can accept Enter. Claude and Codex must emit a post-paste composer marker and then settle, or reach the bounded fallback first; a late marker receives a fresh settlement window.",
"oracle": "Runtime tests assert the exact PTY write sequence, failure cleanup, and Claude- and Codex-shaped multi-frame renders where the legacy platform delay and first show-cursor marker are both premature. The candidate waits for post-marker output quiescence before one submit, resets that window on later frames, gives a late marker a fresh bounded window, and still submits once at the hard deadline if output never settles. Orchestration tests assert dispatch/coordinator use the agent prompt path; the live CLI harness covers long Codex-like framing.",
"commands": [
"pnpm exec vitest run --config config/vitest.config.ts src/shared/agent-prompt-injection.test.ts src/main/runtime/orca-runtime.test.ts src/main/runtime/rpc/methods/orchestration.test.ts src/main/runtime/orchestration/coordinator.test.ts",
"node tests/tools/repro-orchestration-long-prompt.mjs --cli out/bin/orca-dev --mode codex-like --size-kb 32 --timeout-ms 20000"
@@ -8442,7 +8678,8 @@
"file": "src/main/runtime/orca-runtime.test.ts",
"assertions": [
"runtime writes bracketed paste before a delayed submit",
"Claude ignores unrelated and split pre-marker output, then waits for post-marker render quiescence before one submit",
"Claude and Codex ignore unrelated and split pre-marker output, then wait for post-marker render quiescence before one submit",
"a late Codex marker receives a fresh settlement window before one submit",
"a silent Claude composer reaches one bounded fallback submit",
"a marked Claude composer with continuous render output reaches one bounded fallback submit",
"large prompt frames are chunked and reconstructed before submit",
@@ -8474,6 +8711,15 @@
}
],
"evidenceRuns": [
{
"date": "2026-08-14",
"runner": "local",
"platform": "macos",
"command": "pnpm exec vitest run --config config/vitest.config.ts src/shared/agent-prompt-injection.test.ts src/main/runtime/orca-runtime.test.ts src/main/runtime/rpc/methods/orchestration.test.ts src/main/runtime/orchestration/coordinator.test.ts",
"result": "passed",
"durationSeconds": 11.32,
"summary": "4 files and 1,303 tests passed with one skipped. Claude and Codex both wait for post-marker quiescence, and a Codex marker arriving at 7.9 seconds receives a fresh window through its final slow frame. Exact-build live Codex workers accepted injected prompts without manual Enter, replied, called worker_done, and settled successfully in the rendered Electron UI."
},
{
"date": "2026-08-13",
"runner": "local",
@@ -8521,11 +8767,11 @@
},
"redGreenEvidence": {
"status": "partial",
"evidence": "The original Claude-shaped oracle fails on unmodified main and passes with the render gate. The hardened multi-frame oracle then fails on the first-marker candidate, which submits at 751 ms during an intermediate frame, and passes when the gate requires post-marker quiescence. The earlier live harness also reproduced the unsafe raw multiline contract before its framing fix. Intentional-break evidence remains local rather than a separate CI job."
"evidence": "The original Claude-shaped oracle fails on unmodified main and passes with the render gate. The hardened multi-frame oracle then fails on the first-marker candidate, which submits at 751 ms during an intermediate frame, and passes when the gate requires post-marker quiescence. Codex used the legacy platform delay before this candidate; the late-marker oracle proves the original deadline would pre-empt slow-host settlement without re-arming. The earlier live harness also reproduced the unsafe raw multiline contract before its framing fix. Intentional-break evidence remains local rather than a separate CI job."
},
"performanceBudget": {
"required": true,
"evidence": "Agent prompt dispatch remains O(prompt bytes) with existing 16KB chunking. Claude adds one PTY-local output listener until 1.5s of post-marker output quiescence or an 8s fallback; each later frame resets one timer. Other agents retain the platform delay. No polling, provider listing, subprocess churn, hidden-pane wakeups, renderer work, or wire traffic is added."
"evidence": "Agent prompt dispatch remains O(prompt bytes) with existing 16KB chunking. Claude and Codex add one PTY-local output listener until 1.5s of post-marker output quiescence or an 8s fallback; a late first marker restarts the bounded window and each later frame resets one quiet timer. Other agents retain the platform delay. No polling, provider listing, subprocess churn, hidden-pane wakeups, renderer work, or wire traffic is added."
},
"promotionCriteria": [
"Run the live harness in soak with a self-starting dev runtime or provider-contract fixture.",
@@ -11999,6 +12245,105 @@
"Platform-named bundles are loaded on macOS locally; physical Linux and Windows evidence is not yet recorded."
],
"demotionRule": "Demote if any shipped companion stops loading on Node 18, selected installers mutate outside the isolated home, or the CI lane no longer runs the built artifact with Node 18."
},
{
"id": "terminal-history.daemon-checkpoint-session-isolation",
"title": "Daemon history checkpoints isolate stalled terminal sessions",
"maturity": "experimental",
"protection": "partial",
"owner": "daemon-terminal",
"layer": "main-process-daemon-history",
"surfaces": [
"warm daemon terminal reattach",
"deep daemon buffer snapshots",
"periodic and final durable-history checkpoints"
],
"platforms": ["macos", "linux", "windows"],
"providers": ["local-daemon", "ssh-daemon", "wsl-daemon", "paired-runtime-host"],
"coveredPlatforms": ["macos"],
"coveredProviders": ["local-daemon"],
"coverageNotes": "A real daemon server and adapter on macOS cover per-session isolation, same-session deadlines, post-deadline commit, aggregate overlay admission, and live-window fallback. The provider-neutral adapter has no new path or wire assumptions; a live Linux SSH-relay journey covers the unchanged remote consumer, while live Linux daemon, Windows, WSL, and paired-runtime-host journeys remain gaps.",
"motivatingLinks": [
"https://linear.app/stably/issue/STA-4173",
"https://github.com/stablyai/orca/pull/14346"
],
"invariant": "A stalled durable-history checkpoint may delay only its own daemon session. Warm reattach and deep snapshots degrade to the live window after five seconds, non-final background checkpoints defer after fifteen seconds, same-session writes remain serialized, non-final running plus queued work stays bounded to three per session, overlay compacts stay bounded to four across sessions, and final sleep or disconnect checkpoints remain deadline-free.",
"oracle": "Park one session inside the real history checkpoint write, then require an unrelated warm reattach to return within two seconds with its own output while the first write remains parked. Require the stalled session to return its live window after the overlay deadline and later commit the abandoned write. Park four distinct overlay compacts, require a fifth reattach to return its live window without starting another compact, and require fast non-final RPC rejection plus both deadlines to leave diagnostic breadcrumbs.",
"commands": [
"pnpm exec vitest run --config config/vitest.config.ts src/main/daemon/daemon-checkpoint-session-queue.test.ts src/main/daemon/daemon-reattach-checkpoint-isolation.test.ts src/main/daemon/daemon-pty-adapter.test.ts --reporter=dot"
],
"testFiles": [
"src/main/daemon/daemon-checkpoint-session-queue.test.ts",
"src/main/daemon/daemon-reattach-checkpoint-isolation.test.ts",
"src/main/daemon/daemon-pty-adapter.test.ts"
],
"assertionRefs": [
{
"file": "src/main/daemon/daemon-checkpoint-session-queue.test.ts",
"assertions": [
"same-session operations serialize while unrelated sessions run independently",
"deadline fallback leaves the operation running and retains later waiter ordering",
"operation rejection remains distinct from a deadline",
"running plus queued work is bounded per session and drains"
]
},
{
"file": "src/main/daemon/daemon-reattach-checkpoint-isolation.test.ts",
"assertions": [
"an unrelated session reattaches while another checkpoint remains parked",
"same-session deadline fallback returns the live window and logs a breadcrumb",
"an abandoned checkpoint commits when the stalled write resumes",
"four parked overlays turn away a fifth compact without blocking its reattach"
]
},
{
"file": "src/main/daemon/daemon-pty-adapter.test.ts",
"assertions": [
"periodic checkpoint work caps concurrent snapshot, disk operations, and admission probes at four",
"non-final checkpoint RPC failures retain the existing warning path",
"final checkpoints bypass cooldown and remain awaited"
]
}
],
"evidenceRuns": [
{
"date": "2026-08-13",
"runner": "local",
"platform": "macos",
"result": "passed",
"command": "pnpm exec vitest run --config config/vitest.config.ts src/main/daemon/daemon-checkpoint-session-queue.test.ts src/main/daemon/daemon-reattach-checkpoint-isolation.test.ts src/main/daemon/daemon-pty-adapter.test.ts --reporter=dot",
"durationSeconds": 15,
"summary": "Three focused files passed, including unrelated-session reattach, five-second fallback, post-deadline durable commit, aggregate non-final admission, late-rejection diagnostics, and final-checkpoint behavior."
}
],
"runtimeBudget": {
"p95Seconds": 30,
"scope": "focused queue, real daemon-adapter isolation, and daemon adapter regression suite"
},
"flakeHistory": {
"status": "not-started",
"evidence": "Fresh deterministic local runs pass; CI and soak history have not started."
},
"redGreenEvidence": {
"status": "partial",
"evidence": "Origin main routes the parked overlay through one process-wide checkpoint tail, while the candidate oracle observes unrelated reattach at about 79 milliseconds. The exact reverted-source oracle was not executed in this review worktree."
},
"performanceBudget": {
"required": true,
"evidence": "Non-final work holds one admission per session and four globally; a full admission set stops the current periodic pass after four probes, and its diagnostic state stays bounded to the active admissions plus one global marker. Deadline timers are unrefed, abandoned waits retain admission until the actual write settles, dirty-gated polling remains unchanged, final checkpoints remain deadline-free, and the change adds no subprocess, renderer, store, startup probe, or wire work."
},
"promotionCriteria": [
"Execute the oracle against an affected baseline and retain the exact red result.",
"Collect 100 consecutive focused CI passes or 14 days of soak history.",
"Collect live Linux daemon, Windows, WSL, and paired-runtime-host reattach evidence.",
"Keep per-session and aggregate admission, deadline, diagnostics, and final-checkpoint assertions green."
],
"knownGaps": [
"Live Electron evidence covers the unchanged Linux SSH-relay consumer rather than a daemon running on the SSH host.",
"Physical Windows, WSL, Linux daemon, and paired-runtime-host journeys were not run.",
"The reverted-source failure is established from the old process-wide call path but was not executed."
],
"demotionRule": "Demote if unrelated reattach joins a stalled session tail, overlay or periodic work exceeds its admission bound, a deadline cancels durable work, final checkpoints defer, or failures and deadline fallbacks lose their diagnostic breadcrumbs."
}
]
}
+1 -1
View File
@@ -173,7 +173,7 @@ export const KO_VALUE_OVERRIDES = {
'아래 워크스페이스 오류를 확인한 후 새로고침하세요.',
'Review inactive workspaces before deleting their local files and Orca state.':
'로컬 파일과 Orca 상태를 삭제하기 전에 비활성 워크스페이스를 확인하세요.',
'Review inactive workspaces (': '비활성 워크스페이스 확인(',
'Clean up workspaces': '워크스페이스 정리',
'Review the settings that will be imported from your Ghostty config.':
'Ghostty 구성에서 가져올 설정을 확인하세요.',
'GitHub requires review approval before this pull request can merge':
+5 -1
View File
@@ -75,7 +75,11 @@ describe('PR E2E gate contract', () => {
expect(changedRun.run).toContain('. != "tests/e2e/ssh-startup-exec-readiness.spec.ts"')
expect(changedRun.run).toContain('. != "tests/e2e/paired-startup-exec-readiness.spec.ts"')
expect(changedRun.run).toContain('if [ "${#TEST_FILES[@]}" -eq 0 ]')
expect(changedRun.run).toContain('pnpm run test:e2e "${TEST_FILES[@]}" --workers=1')
expect(changedRun.run).toContain('grep -l \'@headful\' "${TEST_FILES[@]}"')
expect(changedRun.run).toContain('E2E_PROJECT_ARGS+=(--project=electron-headful)')
expect(changedRun.run).toContain(
'pnpm run test:e2e "${TEST_FILES[@]}" --workers=1 "${E2E_PROJECT_ARGS[@]}"'
)
})
it('keeps startup-exec live parity in the isolated SSH lane', () => {
@@ -5,7 +5,7 @@ import {
resolveEnvironment,
resolveEnvironmentPairingOffer
} from '../../src/shared/runtime-environment-store'
import type { MemorySnapshot } from '../../src/shared/types'
import type { MemorySnapshot } from '../../src/shared/process-stats-types'
import type { RuntimeStatus } from '../../src/shared/runtime-types'
async function main(): Promise<void> {
+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: 19m">
<title>downloads: 19m</title>
<svg xmlns="http://www.w3.org/2000/svg" width="106" height="20" role="img" aria-label="downloads: 20m">
<title>downloads: 20m</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">19m</text>
<text x="90" y="14">19m</text>
<text x="90" y="15" fill="#010101" fill-opacity=".3">20m</text>
<text x="90" y="14">20m</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 935 B

After

Width:  |  Height:  |  Size: 935 B

+1 -1
View File
@@ -96,7 +96,7 @@ import {
WORKSPACE_SORT_OPTIONS as SORT_OPTIONS
} from '../../../src/worktree/workspace-list-picker-options'
import type { RepoSummary } from '../../../src/worktree/host-worktree-rpc-types'
import type { WorkspaceStatusDefinition } from '../../../../src/shared/types'
import type { WorkspaceStatusDefinition } from '../../../../src/shared/worktree/types'
import { DEFAULT_MOBILE_WORKSPACE_STATUSES } from '../../../src/worktree/mobile-workspace-statuses'
function isErrorVerdict(v: ConnectionVerdict): boolean {
@@ -277,7 +277,8 @@ import {
import { colors } from '../../../../src/theme/mobile-theme'
import { QuickCommandsTabButton } from '../../../../src/session/QuickCommandsTabButton'
import { styles } from '../../../../src/session/mobile-session-styles'
import type { DiffComment, TerminalQuickCommand } from '../../../../../src/shared/types'
import type { DiffComment } from '../../../../../src/shared/diff-comment-types'
import type { TerminalQuickCommand } from '../../../../../src/shared/terminal-quick-command-types'
import type {
DiffCommentActions,
DiffNotesDelivery,
+6 -6
View File
@@ -149,13 +149,13 @@ import {
scheduleMobileTaskCopyFeedbackReset
} from '../../../src/tasks/mobile-task-copy-feedback-timer'
import type {
BaseRefSearchResult,
GitHubOwnerRepo,
ProviderCheckSummary,
PersistedTrustedOrcaHooks,
SparsePreset,
TuiAgent
} from '../../../../src/shared/types'
ProviderCheckSummary
} from '../../../../src/shared/github/pull-request-types'
import type { PersistedTrustedOrcaHooks } from '../../../../src/shared/orca-yaml-hook-types'
import type { BaseRefSearchResult } from '../../../../src/shared/repo-types'
import type { TuiAgent } from '../../../../src/shared/tui-agent'
import type { SparsePreset } from '../../../../src/shared/worktree/create-types'
import type { SshConnectionState } from '../../../../src/shared/ssh-types'
import type { HostedReviewDecision } from '../../../../src/shared/hosted-review'
import {
+1 -1
View File
@@ -7,7 +7,7 @@ import {
applyTerminalQuickCommandMutation,
type TerminalQuickCommandMutation
} from '../../src/shared/terminal-quick-commands'
import type { TerminalQuickCommand } from '../../src/shared/types'
import type { TerminalQuickCommand } from '../../src/shared/terminal-quick-command-types'
import { handleMockFilePreviewRequest } from './mock-server-file-preview-data'
import { handleMockGitRequest } from './mock-server-git-state'
import { handleMockAccountRequest } from './mock-server-account-rpc'
@@ -7,11 +7,11 @@ import { colors } from '../theme/mobile-theme'
import { useHostClient } from '../transport/client-context'
import type { RpcSuccess } from '../transport/types'
import type { RpcClient } from '../transport/rpc-client'
import { readMobileRuntimeHostPlatform } from '../transport/mobile-runtime-host-platform'
import { getWorktreeLabel } from '../session/worktree-label'
import {
buildMobileAiVaultResumeLaunch,
createMobileAiVaultResumeMutationRegistry,
readMobileRuntimeHostPlatform,
readMobileRuntimeTerminalWindowsShell,
resolveMobileAiVaultResumePlatform,
resumeAiVaultSessionInTerminal,
+1 -1
View File
@@ -1,7 +1,7 @@
import { Image, StyleSheet, Text, View } from 'react-native'
import type { ImageSourcePropType } from 'react-native'
import { Terminal } from 'lucide-react-native'
import type { TuiAgent } from '../../../src/shared/types'
import type { TuiAgent } from '../../../src/shared/tui-agent'
import Svg, { Defs, G, LinearGradient, Path, Stop } from 'react-native-svg'
import { colors } from '../theme/mobile-theme'
import { MOBILE_AGENT_CATALOG } from '../tasks/mobile-agent-catalog'
@@ -1,7 +1,7 @@
import { ActivityIndicator, FlatList, Pressable, Text, View } from 'react-native'
import { RefreshCw } from 'lucide-react-native'
import type { RefObject } from 'react'
import type { DiffComment } from '../../../src/shared/types'
import type { DiffComment } from '../../../src/shared/diff-comment-types'
import { colors } from '../theme/mobile-theme'
import { MobileDiffReviewLine } from './MobileDiffReviewLine'
import type {
@@ -1,7 +1,7 @@
import { useMemo } from 'react'
import { KeyboardAvoidingView, Platform, Pressable, Text, TextInput, View } from 'react-native'
import { Check, Copy, FileText, Plus, Send, Trash2, X } from 'lucide-react-native'
import type { DiffComment } from '../../../src/shared/types'
import type { DiffComment } from '../../../src/shared/diff-comment-types'
import { colors } from '../theme/mobile-theme'
import type { ActionSheetAction } from './ActionSheetModal'
import { ActionSheetModal } from './ActionSheetModal'
@@ -1,6 +1,6 @@
import { Pressable, Text, View } from 'react-native'
import { ArrowDown, ArrowUp } from 'lucide-react-native'
import type { DiffComment } from '../../../src/shared/types'
import type { DiffComment } from '../../../src/shared/diff-comment-types'
import { colors } from '../theme/mobile-theme'
import type { MobileDiffReviewQueueItem } from '../session/mobile-diff-review-queue'
import { MOBILE_GIT_STATUS_LABELS } from '../source-control/mobile-git-status'
@@ -1,6 +1,6 @@
import { Pressable, StyleSheet, Text, View } from 'react-native'
import { MessageSquare } from 'lucide-react-native'
import type { DiffComment } from '../../../src/shared/types'
import type { DiffComment } from '../../../src/shared/diff-comment-types'
import type { MobileDiffLine } from '../session/mobile-diff-lines'
import type { MobileHighlightedDiffLine } from '../session/mobile-file-syntax'
import { mobileDiffLineNumber, mobileDiffLinePrefix } from '../source-control/mobile-diff-format'
@@ -44,18 +44,32 @@ vi.mock('./MobileAgentIcon', () => ({ MobileAgentIcon: 'MobileAgentIcon' }))
vi.mock('./TaskProviderLogo', () => ({ TaskProviderLogo: 'TaskProviderLogo' }))
import { setCachedRepos } from '../cache/repo-cache'
import { getLocalExecutionHostLabel } from '../../../src/shared/execution-host'
import { NewWorktreeModal } from './NewWorktreeModal'
const repos = [{ id: 'repo-1', displayName: 'orca', path: '/src/orca', kind: 'git' }]
const LOCAL_HOST_LABEL = getLocalExecutionHostLabel('darwin')
function repoPickerNames(renderer: ReactTestRenderer | null): string[] {
const pickers = renderer?.root.findAll((node) => node.type === 'PickerListDrawer') ?? []
const repoPicker = pickers.find((node) => node.props.title === 'Repository')
return ((repoPicker?.props.items ?? []) as { label: string }[]).map((item) => item.label)
const repos = [
{
id: 'repo-1',
displayName: 'orca',
path: '/src/orca',
kind: 'git',
upstream: { owner: 'stablyai', repo: 'orca' }
}
]
function pickerItems(
renderer: ReactTestRenderer,
title: string
): { label: string; detail: string }[] {
const pickers = renderer.root.findAll((node) => node.type === 'PickerListDrawer')
const picker = pickers.find((node) => node.props.title === title)
return picker?.props.items ?? []
}
describe('NewWorktreeModal repo list', () => {
let renderer: ReactTestRenderer | null = null
describe('NewWorktreeModal project targets', () => {
let renderer: ReactTestRenderer
beforeEach(() => {
setCachedRepos('host-1', repos)
@@ -63,7 +77,6 @@ describe('NewWorktreeModal repo list', () => {
afterEach(() => {
act(() => renderer?.unmount())
renderer = null
})
it('keeps the cached repos when the in-flight repo.list rejects on a dropped connection', async () => {
@@ -71,6 +84,9 @@ describe('NewWorktreeModal repo list', () => {
if (method === 'repo.list') {
return Promise.reject(new Error('connection closed'))
}
if (method === 'status.get') {
return Promise.resolve({ ok: true, result: { hostPlatform: 'darwin' } })
}
return new Promise(() => {})
})
const client = { sendRequest } as unknown as RpcClient
@@ -92,6 +108,60 @@ describe('NewWorktreeModal repo list', () => {
})
expect(sendRequest).toHaveBeenCalledWith('repo.list')
expect(repoPickerNames(renderer)).toEqual(['orca'])
expect(pickerItems(renderer, 'Project')).toEqual([
expect.objectContaining({ label: 'orca', detail: 'stablyai/orca' })
])
expect(pickerItems(renderer, 'Run on')).toEqual([
expect.objectContaining({ label: LOCAL_HOST_LABEL, detail: '/src/orca' })
])
})
it('groups same-name checkouts under one project with separate run targets', async () => {
const listedRepos = [
...repos,
{
id: 'repo-2',
displayName: 'orca',
path: '/home/dev/orca',
connectionId: 'build-server',
kind: 'git',
upstream: { owner: 'stablyai', repo: 'orca' }
}
]
const client = {
sendRequest: vi.fn().mockImplementation((method: string) => {
if (method === 'repo.list') {
return Promise.resolve({ ok: true, result: { repos: listedRepos } })
}
if (method === 'status.get') {
return Promise.resolve({ ok: true, result: { hostPlatform: 'darwin' } })
}
return new Promise(() => {})
})
} as unknown as RpcClient
await act(async () => {
renderer = create(
createElement(NewWorktreeModal, {
visible: true,
client,
hostId: 'host-1',
onCreated: () => {},
onClose: () => {}
})
)
await Promise.resolve()
})
expect(pickerItems(renderer, 'Project')).toEqual([
expect.objectContaining({ label: 'orca', detail: 'stablyai/orca' })
])
expect(pickerItems(renderer, 'Run on')).toEqual([
expect.objectContaining({ label: LOCAL_HOST_LABEL, detail: '/src/orca' }),
expect.objectContaining({
label: 'SSH · build-server',
detail: '/home/dev/orca'
})
])
})
})
+67 -59
View File
@@ -10,7 +10,7 @@ import {
ActivityIndicator,
Keyboard
} from 'react-native'
import { ChevronDown, ChevronUp } from 'lucide-react-native'
import { ChevronDown, ChevronUp, Monitor } from 'lucide-react-native'
import type { RpcClient } from '../transport/rpc-client'
import type { RpcResponse, RpcSuccess } from '../transport/types'
import { colors, spacing, radii, typography } from '../theme/mobile-theme'
@@ -29,8 +29,11 @@ import {
type SetupHookTrust
} from '../tasks/setup-hook-trust'
import { isMobileTuiAgentEnabled } from '../tasks/mobile-tui-agents'
import type { PersistedTrustedOrcaHooks, TuiAgent } from '../../../src/shared/types'
import type { PersistedTrustedOrcaHooks } from '../../../src/shared/orca-yaml-hook-types'
import type { Repo as SharedRepo } from '../../../src/shared/repo-types'
import type { TuiAgent } from '../../../src/shared/tui-agent'
import type { SshConnectionState } from '../../../src/shared/ssh-types'
import { getProjectIdentityKey } from '../../../src/shared/project-host-setup-projection'
import {
NEW_WORKTREE_AGENT_OPTIONS as AGENT_OPTIONS,
NEW_WORKTREE_BLANK_AGENT as BLANK_TERMINAL,
@@ -63,17 +66,26 @@ import { SmartWorkspaceSourceField } from './SmartWorkspaceSourceField'
import { SmartWorkspaceSourceDrawer } from './SmartWorkspaceSourceDrawer'
import { SmartWorkspaceAdvancedFields } from './SmartWorkspaceAdvancedFields'
import { SetupHookTrustDrawer, type SetupTrustPrompt } from './SetupHookTrustDrawer'
import { NewWorktreeProjectTargetFields } from './NewWorktreeProjectTargetFields'
import {
buildNewWorkspaceProjectOptions,
buildNewWorkspaceRunTargetOptions,
getNewWorkspaceRunTarget
} from './new-workspace-project-targets'
type Repo = {
id: string
displayName: string
path: string
badgeColor?: string
connectionId?: string | null
kind?: 'git' | 'folder'
upstream?: { owner: string; repo: string } | null
gitRemoteIdentity?: { remoteUrl?: string; canonicalKey?: string } | null
}
type Repo = Pick<SharedRepo, 'id' | 'displayName' | 'path'> &
Partial<
Pick<
SharedRepo,
| 'badgeColor'
| 'connectionId'
| 'executionHostId'
| 'kind'
| 'upstream'
| 'repoIcon'
| 'gitRemoteIdentity'
>
>
type SetupDecision = 'inherit' | 'run' | 'skip'
type SetupRunPolicy = 'ask' | 'run-by-default' | 'skip-by-default'
@@ -200,10 +212,8 @@ function NewWorktreeModalContent({
const [sshConnectingTargetId, setSshConnectingTargetId] = useState<string | null>(null)
const [note, setNote] = useState('')
const [availableProviders, setAvailableProviders] = useState<TaskProvider[]>([])
const { tasksSupported, getWorktreeCreateCutoverSupport } = useNewWorktreeRuntimeCapabilities(
client,
visible
)
const { tasksSupported, hostPlatform, getWorktreeCreateCutoverSupport } =
useNewWorktreeRuntimeCapabilities(client, visible)
const [showAdvanced, setShowAdvanced] = useState(false)
const [setupHookDetails, setSetupHookDetails] = useState<SetupHookDetails | null>(null)
const [trustedOrcaHooks, setTrustedOrcaHooks] = useState<PersistedTrustedOrcaHooks>({})
@@ -694,10 +704,17 @@ function NewWorktreeModalContent({
isMobileTuiAgentEnabled(agent.id, runtimeSettings?.disabledTuiAgents)
)
const pickerAgentOptions = [...visibleAgentOptions, BLANK_TERMINAL]
const repoPickerItems = useMemo(
() => repos.map((repo) => ({ id: repo.id, label: repo.displayName, repo })),
[repos]
const projectPickerItems = useMemo(() => buildNewWorkspaceProjectOptions(repos), [repos])
const selectedProjectId = selectedRepo ? getProjectIdentityKey(selectedRepo) : null
const selectedProject =
projectPickerItems.find((project) => project.id === selectedProjectId) ?? null
const runTargetPickerItems = useMemo(
() => buildNewWorkspaceRunTargetOptions(repos, selectedProjectId, hostPlatform),
[hostPlatform, repos, selectedProjectId]
)
const selectedRunTarget = selectedRepo
? getNewWorkspaceRunTarget(selectedRepo, hostPlatform)
: null
function prepareSelectionPickerOpen(): void {
// Why: picker taps can beat an open soft keyboard; dismissing it prevents the
@@ -785,10 +802,7 @@ function NewWorktreeModalContent({
>
<BottomDrawer visible={formSheetVisible} interactive={formSheetInteractive} onClose={onClose}>
<View style={styles.header}>
<Text style={styles.title}>Create Workspace</Text>
<Text style={styles.subtitle}>
Pick a repository and agent to spin up a new workspace.
</Text>
<Text style={styles.title}>Create worktree</Text>
</View>
{loading ? (
@@ -797,33 +811,23 @@ function NewWorktreeModalContent({
</View>
) : repos.length === 0 ? (
<View style={styles.loadingContainer}>
<Text style={styles.emptyText}>No repositories found</Text>
<Text style={styles.emptyText}>No projects found</Text>
</View>
) : (
<>
<View style={styles.field}>
<Text style={styles.label}>Repository</Text>
<Pressable
style={styles.fieldButton}
onPress={() => {
prepareSelectionPickerOpen()
transitionDrawer('repo')
}}
>
{selectedRepo ? (
<View
style={[styles.repoDot, { backgroundColor: repoBadgeColor(selectedRepo) }]}
/>
) : null}
<Text
style={[styles.fieldButtonText, !selectedRepo && styles.fieldButtonPlaceholder]}
numberOfLines={1}
>
{selectedRepo?.displayName ?? 'Select a repository'}
</Text>
<ChevronDown size={14} color={colors.textMuted} />
</Pressable>
</View>
<NewWorktreeProjectTargetFields
project={selectedProject}
runTarget={selectedRunTarget}
projectBadgeColor={selectedRepo ? repoBadgeColor(selectedRepo) : null}
onOpenProject={() => {
prepareSelectionPickerOpen()
transitionDrawer('project')
}}
onOpenRunTarget={() => {
prepareSelectionPickerOpen()
transitionDrawer('runTarget')
}}
/>
<SmartWorkspaceSourceField
composer={composer}
@@ -893,7 +897,7 @@ function NewWorktreeModalContent({
>
<MobileAgentIcon agentId={selectedAgent.id} size={16} />
<Text style={styles.fieldButtonText} numberOfLines={1}>
{sshGate.requiresConnection ? 'Connect repository first' : selectedAgent.label}
{sshGate.requiresConnection ? 'Connect target first' : selectedAgent.label}
</Text>
<ChevronDown size={14} color={colors.textMuted} />
</Pressable>
@@ -995,7 +999,7 @@ function NewWorktreeModalContent({
<ActivityIndicator size="small" color={colors.bgBase} />
) : (
<Text style={styles.createText}>
{sshGate.requiresConnection ? 'Connect Repository' : 'Create Workspace'}
{sshGate.requiresConnection ? 'Connect target' : 'Create worktree'}
</Text>
)}
</Pressable>
@@ -1024,10 +1028,10 @@ function NewWorktreeModalContent({
/>
<PickerListDrawer
visible={visible && drawerView === 'repo'}
title="Repository"
items={repoPickerItems}
selectedId={selectedRepo?.id ?? ''}
visible={visible && drawerView === 'project'}
title="Project"
items={projectPickerItems}
selectedId={selectedProjectId ?? ''}
onSelect={(item) => handleRepoSelected(item.repo)}
onClose={() => transitionDrawer('form')}
renderIcon={(item) => {
@@ -1035,6 +1039,16 @@ function NewWorktreeModalContent({
}}
/>
<PickerListDrawer
visible={visible && drawerView === 'runTarget'}
title="Run on"
items={runTargetPickerItems}
selectedId={selectedRepo?.id ?? ''}
onSelect={(item) => handleRepoSelected(item.repo)}
onClose={() => transitionDrawer('form')}
renderIcon={() => <Monitor size={16} color={colors.textMuted} />}
/>
<PickerListDrawer
visible={visible && drawerView === 'agent'}
title="Agent"
@@ -1071,11 +1085,6 @@ const styles = StyleSheet.create({
fontWeight: '600',
color: colors.textPrimary
},
subtitle: {
fontSize: 13,
color: colors.textMuted,
marginTop: 2
},
loadingContainer: {
paddingVertical: spacing.xl,
alignItems: 'center'
@@ -1109,7 +1118,6 @@ const styles = StyleSheet.create({
borderColor: colors.borderSubtle
},
fieldButtonText: {
flex: 1,
fontSize: typography.bodySize,
color: colors.textPrimary
},
@@ -0,0 +1,111 @@
import { View, Text, Pressable, StyleSheet, Platform } from 'react-native'
import { ChevronDown, Monitor } from 'lucide-react-native'
import { colors, radii, spacing, typography } from '../theme/mobile-theme'
type Selection = { label: string; detail?: string }
export function NewWorktreeProjectTargetFields({
project,
runTarget,
projectBadgeColor,
onOpenProject,
onOpenRunTarget
}: {
project: Selection | null
runTarget: Selection | null
projectBadgeColor: string | null
onOpenProject: () => void
onOpenRunTarget: () => void
}) {
return (
<>
<View style={styles.field}>
<Text style={styles.label}>Project</Text>
<Pressable style={styles.fieldButton} onPress={onOpenProject}>
{projectBadgeColor ? (
<View style={[styles.projectDot, { backgroundColor: projectBadgeColor }]} />
) : null}
<SelectionCopy selection={project} placeholder="Select a project" />
<ChevronDown size={14} color={colors.textMuted} />
</Pressable>
</View>
<View style={styles.field}>
<Text style={styles.label}>Run on</Text>
<Pressable style={styles.fieldButton} onPress={onOpenRunTarget}>
<Monitor size={14} color={colors.textMuted} />
<SelectionCopy selection={runTarget} placeholder="Select a run target" />
<ChevronDown size={14} color={colors.textMuted} />
</Pressable>
</View>
</>
)
}
function SelectionCopy({
selection,
placeholder
}: {
selection: Selection | null
placeholder: string
}) {
return (
<View style={styles.fieldButtonCopy}>
<Text
style={[styles.fieldButtonText, !selection && styles.fieldButtonPlaceholder]}
numberOfLines={1}
>
{selection?.label ?? placeholder}
</Text>
{selection?.detail ? (
<Text style={styles.fieldButtonDetail} numberOfLines={1}>
{selection.detail}
</Text>
) : null}
</View>
)
}
const styles = StyleSheet.create({
field: {
marginBottom: spacing.md
},
label: {
fontSize: 13,
fontWeight: '500',
color: colors.textSecondary,
marginBottom: spacing.xs
},
fieldButton: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.sm,
backgroundColor: colors.bgRaised,
borderRadius: radii.input,
borderWidth: 1,
borderColor: colors.borderSubtle,
paddingHorizontal: spacing.md,
paddingVertical: Platform.OS === 'ios' ? spacing.sm + 2 : spacing.sm
},
projectDot: {
width: 8,
height: 8,
borderRadius: 999
},
fieldButtonCopy: {
flex: 1,
minWidth: 0
},
fieldButtonText: {
fontSize: typography.bodySize,
color: colors.textPrimary
},
fieldButtonDetail: {
fontSize: typography.metaSize,
color: colors.textMuted,
marginTop: 1
},
fieldButtonPlaceholder: {
color: colors.textMuted
}
})
+26 -9
View File
@@ -6,7 +6,9 @@ import { colors, spacing, typography } from '../theme/mobile-theme'
import { BottomDrawer } from './BottomDrawer'
import { BOTTOM_DRAWER_HIDE_DURATION_MS } from './bottom-drawer-constants'
type Props<T extends { id: string; label: string }> = {
type PickerListItem = { id: string; label: string; detail?: string }
type Props<T extends PickerListItem> = {
visible: boolean
title: string
items: T[]
@@ -16,7 +18,7 @@ type Props<T extends { id: string; label: string }> = {
renderIcon?: (item: T) => ReactNode
}
export function PickerListDrawer<T extends { id: string; label: string }>({
export function PickerListDrawer<T extends PickerListItem>({
visible,
title,
items,
@@ -87,12 +89,19 @@ export function PickerListDrawer<T extends { id: string; label: string }>({
onPress={() => closeThenSelect(item)}
>
{renderIcon?.(item)}
<Text
style={[styles.itemText, selected && styles.itemTextSelected]}
numberOfLines={1}
>
{item.label}
</Text>
<View style={styles.itemCopy}>
<Text
style={[styles.itemText, selected && styles.itemTextSelected]}
numberOfLines={1}
>
{item.label}
</Text>
{item.detail ? (
<Text style={styles.itemDetail} numberOfLines={1}>
{item.detail}
</Text>
) : null}
</View>
{selected && <Check size={14} color={colors.textPrimary} />}
</Pressable>
)
@@ -142,10 +151,18 @@ const styles = StyleSheet.create({
backgroundColor: colors.bgRaised
},
itemText: {
flex: 1,
fontSize: typography.bodySize,
color: colors.textPrimary
},
itemCopy: {
flex: 1,
minWidth: 0
},
itemDetail: {
fontSize: typography.metaSize,
color: colors.textMuted,
marginTop: 1
},
itemTextSelected: {
fontWeight: '600'
}
@@ -1,7 +1,7 @@
import { existsSync, readFileSync } from 'node:fs'
import path from 'node:path'
import { describe, expect, it } from 'vitest'
import type { TuiAgent } from '../../../src/shared/types'
import type { TuiAgent } from '../../../src/shared/tui-agent'
import { MOBILE_TUI_AGENT_FAVICON_DOMAINS } from '../tasks/mobile-tui-agents'
// Agents that render a hand-authored glyph in MobileAgentIcon and therefore
@@ -1,5 +1,5 @@
import type { ImageSourcePropType } from 'react-native'
import type { TuiAgent } from '../../../src/shared/types'
import type { TuiAgent } from '../../../src/shared/tui-agent'
// Why: mobile previously rendered these agent icons from Google's favicon
// service, which is unreachable in some regions (e.g. mainland China) and
@@ -0,0 +1,94 @@
import { describe, expect, it } from 'vitest'
import { getLocalExecutionHostLabel } from '../../../src/shared/execution-host'
import {
buildNewWorkspaceProjectOptions,
buildNewWorkspaceRunTargetOptions,
getNewWorkspaceRunTarget
} from './new-workspace-project-targets'
const LOCAL_HOST_LABEL = getLocalExecutionHostLabel('darwin')
describe('new workspace project targets', () => {
it('groups local and SSH checkouts of the same project', () => {
const upstream = { owner: 'stablyai', repo: 'orca' }
const options = buildNewWorkspaceProjectOptions([
{ id: 'local', displayName: 'orca', path: '/src/orca', upstream },
{
id: 'ssh',
displayName: 'orca',
path: '/home/dev/orca',
connectionId: 'build-server',
upstream
}
])
expect(options).toHaveLength(1)
expect(options[0]).toMatchObject({ label: 'orca', detail: 'stablyai/orca' })
})
it('shows the provider slug recovered from canonical git identity', () => {
const options = buildNewWorkspaceProjectOptions([
{
id: 'local',
displayName: 'orca',
path: '/src/orca',
gitRemoteIdentity: {
canonicalKey: 'github.com/stablyai/orca',
remoteName: 'origin',
remoteUrl: 'git@github.com:stablyai/orca.git'
}
}
])
expect(options[0]).toMatchObject({ label: 'orca', detail: 'stablyai/orca' })
})
it('labels local, SSH, and paired runtime targets', () => {
expect(
getNewWorkspaceRunTarget({ id: 'local', displayName: 'orca', path: '/src/orca' }, 'darwin')
).toEqual({ label: LOCAL_HOST_LABEL, detail: '/src/orca' })
expect(
getNewWorkspaceRunTarget({ id: 'local', displayName: 'orca', path: 'C:\\src\\orca' })
).toEqual({ label: 'This computer', detail: 'C:\\src\\orca' })
expect(
getNewWorkspaceRunTarget({ id: 'local', displayName: 'orca', path: 'C:\\src\\orca' }, 'win32')
).toEqual({ label: 'Local Windows', detail: 'C:\\src\\orca' })
expect(
getNewWorkspaceRunTarget({
id: 'ssh',
displayName: 'orca',
path: 'C:\\src\\orca',
executionHostId: 'ssh:Windows%20VM'
})
).toEqual({ label: 'SSH · Windows VM', detail: 'C:\\src\\orca' })
expect(
getNewWorkspaceRunTarget({
id: 'runtime',
displayName: 'orca',
path: '/src/orca',
executionHostId: 'runtime:devbox'
})
).toEqual({ label: 'Remote · devbox', detail: '/src/orca' })
})
it('shows one target per host when the project has multiple local worktrees', () => {
const upstream = { owner: 'stablyai', repo: 'orca' }
const repos = [
{ id: 'local-a', displayName: 'orca-a', path: '/src/orca-a', upstream },
{ id: 'local-b', displayName: 'orca-b', path: '/src/orca-b', upstream },
{
id: 'ssh',
displayName: 'orca',
path: '/home/dev/orca',
connectionId: 'build-server',
upstream
}
]
const projectId = buildNewWorkspaceProjectOptions(repos)[0]?.id ?? null
expect(buildNewWorkspaceRunTargetOptions(repos, projectId, 'darwin')).toEqual([
expect.objectContaining({ id: 'local-a', label: LOCAL_HOST_LABEL, detail: '/src/orca-a' }),
expect.objectContaining({ id: 'ssh', label: 'SSH · build-server', detail: '/home/dev/orca' })
])
})
})
@@ -0,0 +1,106 @@
import type { Repo } from '../../../src/shared/repo-types'
import {
getExecutionHostLabel,
getLocalExecutionHostLabel,
getRepoExecutionHostId,
parseExecutionHostId
} from '../../../src/shared/execution-host'
import {
getProjectIdentityKey,
getProjectProviderIdentity
} from '../../../src/shared/project-host-setup-projection'
type WorkspaceRepo = Pick<Repo, 'id' | 'displayName' | 'path'> &
Partial<
Pick<Repo, 'connectionId' | 'executionHostId' | 'upstream' | 'repoIcon' | 'gitRemoteIdentity'>
>
export type NewWorkspaceProjectOption<TRepo extends WorkspaceRepo> = {
id: string
label: string
detail?: string
repo: TRepo
}
export type NewWorkspaceRunTargetOption<TRepo extends WorkspaceRepo> = {
id: string
label: string
detail: string
repo: TRepo
}
export function buildNewWorkspaceProjectOptions<TRepo extends WorkspaceRepo>(
repos: readonly TRepo[]
): NewWorkspaceProjectOption<TRepo>[] {
const options = new Map<string, NewWorkspaceProjectOption<TRepo>>()
const hostIdsByProject = new Map<string, Set<string>>()
for (const repo of repos) {
const id = getProjectIdentityKey(repo)
const hostIds = hostIdsByProject.get(id) ?? new Set<string>()
hostIds.add(getRepoExecutionHostId(repo))
hostIdsByProject.set(id, hostIds)
if (!options.has(id)) {
options.set(id, {
id,
label: repo.displayName,
repo
})
}
}
return [...options.values()].map((option) => {
const providerIdentity = getProjectProviderIdentity(option.repo)
const providerDetail = providerIdentity
? `${providerIdentity.owner}/${providerIdentity.repo}`
: ''
const hostCount = hostIdsByProject.get(option.id)?.size ?? 0
const detail = providerDetail || (hostCount > 1 ? `${hostCount} hosts configured` : '')
return detail ? { ...option, detail } : option
})
}
export function getNewWorkspaceRunTarget(
repo: WorkspaceRepo,
localPlatform: NodeJS.Platform | null = null
): {
label: string
detail: string
} {
const hostId = getRepoExecutionHostId(repo)
const host = parseExecutionHostId(hostId)
const hostLabel = getExecutionHostLabel(hostId)
if (host?.kind === 'ssh') {
return { label: `SSH · ${hostLabel}`, detail: repo.path }
}
if (host?.kind === 'runtime') {
return { label: `Remote · ${hostLabel}`, detail: repo.path }
}
return {
label: localPlatform ? getLocalExecutionHostLabel(localPlatform) : 'This computer',
detail: repo.path
}
}
export function buildNewWorkspaceRunTargetOptions<TRepo extends WorkspaceRepo>(
repos: readonly TRepo[],
projectId: string | null,
localPlatform: NodeJS.Platform | null = null
): NewWorkspaceRunTargetOption<TRepo>[] {
if (!projectId) {
return []
}
const options = new Map<string, NewWorkspaceRunTargetOption<TRepo>>()
for (const repo of repos) {
if (getProjectIdentityKey(repo) !== projectId) {
continue
}
const hostId = getRepoExecutionHostId(repo)
if (!options.has(hostId)) {
options.set(hostId, {
id: repo.id,
...getNewWorkspaceRunTarget(repo, localPlatform),
repo
})
}
}
return [...options.values()]
}
@@ -1,4 +1,4 @@
import type { TuiAgent } from '../../../src/shared/types'
import type { TuiAgent } from '../../../src/shared/tui-agent'
import { MOBILE_AGENT_CATALOG } from '../tasks/mobile-agent-catalog'
import { isMobileTuiAgentEnabled } from '../tasks/mobile-tui-agents'
import { pickWorkspaceAgent } from '../tasks/workspace-agent-selection'
@@ -30,7 +30,7 @@ describe('resolveNewWorktreeFormSheetVisible', () => {
expect(
resolveNewWorktreeFormSheetVisible({
modalVisible: true,
drawerView: 'repo',
drawerView: 'project',
formPinnedUnderSource: false
})
).toBe(false)
@@ -2,7 +2,7 @@ import { useCallback, useState } from 'react'
import { ActivityIndicator, Pressable, Text, View } from 'react-native'
import { GitMerge, Link2Off } from 'lucide-react-native'
import { colors } from '../../theme/mobile-theme'
import type { GitHubPRMergeMethod, PRInfo } from '../../../../src/shared/types'
import type { GitHubPRMergeMethod, PRInfo } from '../../../../src/shared/github/pull-request-types'
import type { RpcClient } from '../../transport/rpc-client'
import type { MobilePrActions } from '../../session/use-mobile-pr-actions'
import { unlinkMobilePr } from '../../source-control/mobile-pr-link'
@@ -1,6 +1,6 @@
import { ActivityIndicator, ScrollView, Text, View } from 'react-native'
import { colors } from '../../theme/mobile-theme'
import type { PRCheckRunDetails } from '../../../../src/shared/types'
import type { PRCheckRunDetails } from '../../../../src/shared/github/check-types'
import { presentCheckDetail, type CheckDetailJob } from './pr-check-detail-content'
import { mobilePrSidebarStyles as styles } from './mobile-pr-sidebar-styles'
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'
import { ActivityIndicator, Linking, Pressable, Text, View } from 'react-native'
import { ChevronDown, ChevronRight, ExternalLink, RotateCw, Sparkles } from 'lucide-react-native'
import { colors } from '../../theme/mobile-theme'
import type { PRCheckDetail } from '../../../../src/shared/types'
import type { PRCheckDetail } from '../../../../src/shared/github/check-types'
import type { RpcClient } from '../../transport/rpc-client'
import { fetchPRCheckDetails, type GitHubPrRepoSlug } from '../../session/github-pr-rpc'
import type { MobilePrActions } from '../../session/use-mobile-pr-actions'
@@ -1,7 +1,11 @@
import { memo, useState } from 'react'
import { Image, Linking, Pressable, Text, View } from 'react-native'
import { Check, CornerDownRight, ExternalLink, Pencil, Trash2, Undo2 } from 'lucide-react-native'
import type { GitHubReaction, GitHubReactionContent, PRComment } from '../../../../src/shared/types'
import type {
GitHubReaction,
GitHubReactionContent,
PRComment
} from '../../../../src/shared/github/comment-types'
import { colors } from '../../theme/mobile-theme'
import { canEditComment, isResolvableComment } from '../../session/pr-comment-actions'
import { ConfirmModal } from '../ConfirmModal'
@@ -1,7 +1,8 @@
import { createElement } from 'react'
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { GitHubWorkItemDetails, PRComment } from '../../../../src/shared/types'
import type { PRComment } from '../../../../src/shared/github/comment-types'
import type { GitHubWorkItemDetails } from '../../../../src/shared/github/work-item-types'
import { PRCommentsSection } from './PRCommentsSection'
vi.mock('react-native', () => ({
@@ -1,7 +1,8 @@
import { useMemo, useState } from 'react'
import { ActivityIndicator, Pressable, Text, View } from 'react-native'
import { ChevronDown, ChevronRight } from 'lucide-react-native'
import type { GitHubWorkItemDetails, PRState } from '../../../../src/shared/types'
import type { PRState } from '../../../../src/shared/github/pull-request-types'
import type { GitHubWorkItemDetails } from '../../../../src/shared/github/work-item-types'
import type { GitHubPrRepoSlug } from '../../session/github-pr-rpc'
import { colors } from '../../theme/mobile-theme'
import { canAddRootComment } from '../../session/pr-comment-actions'
@@ -3,7 +3,7 @@ import { ActivityIndicator, Pressable, ScrollView, Text, View } from 'react-nati
import * as Clipboard from 'expo-clipboard'
import { Check, Copy, FileWarning, Sparkles } from 'lucide-react-native'
import { colors } from '../../theme/mobile-theme'
import type { PRInfo } from '../../../../src/shared/types'
import type { PRInfo } from '../../../../src/shared/github/pull-request-types'
import { PRSection } from './PRSection'
import { resolveConflictDisplay } from './pr-conflict-presentation'
import { prConflictStyles as styles } from './pr-conflict-styles'
@@ -2,7 +2,7 @@ import { useMemo, useState } from 'react'
import { ActivityIndicator, Pressable, Text, View } from 'react-native'
import { UserPlus, X } from 'lucide-react-native'
import { colors } from '../../theme/mobile-theme'
import type { GitHubWorkItemDetails } from '../../../../src/shared/types'
import type { GitHubWorkItemDetails } from '../../../../src/shared/github/work-item-types'
import type { RpcClient } from '../../transport/rpc-client'
import type { MobilePrActions } from '../../session/use-mobile-pr-actions'
import { isPrSidebarDetailsPlaceholder } from '../../session/mobile-pr-sidebar-state'
@@ -2,7 +2,8 @@ import { useState } from 'react'
import { ActivityIndicator, Pressable, Text, TextInput, View } from 'react-native'
import { ArrowRight, ExternalLink, Pencil } from 'lucide-react-native'
import { colors } from '../../theme/mobile-theme'
import type { GitHubWorkItemDetails, PRInfo } from '../../../../src/shared/types'
import type { PRInfo } from '../../../../src/shared/github/pull-request-types'
import type { GitHubWorkItemDetails } from '../../../../src/shared/github/work-item-types'
import type { MobilePrTitleAction } from '../../session/use-mobile-pr-title-action'
import { prStateBadge } from './pr-checks-presentation'
import { statusColor } from './pr-sidebar-status-color'
@@ -2,7 +2,7 @@ import { useEffect, useMemo, useState } from 'react'
import { ActivityIndicator, Pressable, Text, TextInput, View } from 'react-native'
import { Check } from 'lucide-react-native'
import { colors } from '../../theme/mobile-theme'
import type { GitHubAssignableUser } from '../../../../src/shared/types'
import type { GitHubAssignableUser } from '../../../../src/shared/github/pull-request-types'
import type { RpcClient } from '../../transport/rpc-client'
import { fetchAssignableUsers } from '../../session/github-pr-rpc'
import { BottomDrawer } from '../BottomDrawer'
@@ -1,4 +1,4 @@
import type { GitHubPRMergeMethod, PRState } from '../../../../src/shared/types'
import type { GitHubPRMergeMethod, PRState } from '../../../../src/shared/github/pull-request-types'
// Which actions the PR actions section may offer for a given PR state. Merged PRs
// expose only unlink (+ open-on-host elsewhere); closed PRs add reopen; open/draft
@@ -1,4 +1,4 @@
import type { PRInfo } from '../../../../src/shared/types'
import type { PRInfo } from '../../../../src/shared/github/pull-request-types'
type MobilePRAutoMergeAvailabilityInput = Pick<
PRInfo,
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import type { PRCheckRunDetails } from '../../../../src/shared/types'
import type { PRCheckRunDetails } from '../../../../src/shared/github/check-types'
import { presentCheckDetail } from './pr-check-detail-content'
function details(over: Partial<PRCheckRunDetails>): PRCheckRunDetails {
@@ -3,7 +3,7 @@ import type {
PRCheckJob,
PRCheckRunDetails,
PRCheckStep
} from '../../../../src/shared/types'
} from '../../../../src/shared/github/check-types'
// Pure mapping from the github.prCheckDetails payload to the rows the mobile
// expanded check detail renders. No React/native imports so it stays unit-testable
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import type { PRCheckDetail } from '../../../../src/shared/types'
import type { PRCheckDetail } from '../../../../src/shared/github/check-types'
import {
checkOutcome,
firstFailingCheckKey,
@@ -1,4 +1,8 @@
import type { PRCheckDetail, PRState, ProviderCheckSummary } from '../../../../src/shared/types'
import type { PRCheckDetail } from '../../../../src/shared/github/check-types'
import type {
PRState,
ProviderCheckSummary
} from '../../../../src/shared/github/pull-request-types'
import {
classifyCheckOutcome,
summarizeProviderChecks,
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import type { PRInfo } from '../../../../src/shared/types'
import type { PRInfo } from '../../../../src/shared/github/pull-request-types'
import {
buildMergeabilityRefreshCommands,
hasMergeConflicts,
@@ -1,4 +1,4 @@
import type { PRInfo } from '../../../../src/shared/types'
import type { PRInfo } from '../../../../src/shared/github/pull-request-types'
// Pure presentation logic for the PR sidebar's conflicting-files section. No
// React/native imports so it is unit-testable under the node Vitest config (KTD5).
@@ -2,7 +2,14 @@ import { useEffect, useRef, useState } from 'react'
import { BOTTOM_DRAWER_HIDE_DURATION_MS } from './bottom-drawer-constants'
import { resolveNewWorktreeFormSheetVisible } from './new-worktree-form-sheet-visibility'
export type NewWorktreeDrawerView = 'form' | 'transition' | 'source' | 'repo' | 'agent' | 'trust'
export type NewWorktreeDrawerView =
| 'form'
| 'transition'
| 'source'
| 'project'
| 'runTarget'
| 'agent'
| 'trust'
// Why: iOS cannot reliably present a second native modal until the first drawer's
// exit commits; one extra frame keeps transitions sequential on slower devices.
@@ -138,6 +138,17 @@ describe('resolveMobileFileTabDoc', () => {
})
})
// Why: the host now caps oversized diffs with an error envelope, and a client that knows nothing
// about the code must still surface the message instead of rendering an empty diff.
it('propagates a host diff_too_large failure instead of rendering an empty diff', async () => {
const client = clientOf({
'git.diff': fail('diff_too_large', 'This diff is too large to open over a remote connection.')
})
await expect(
resolveMobileFileTabDoc(client, { ...WT, relativePath: 'a.ts', diffSource: 'staged' })
).rejects.toThrow('This diff is too large to open over a remote connection.')
})
it('propagates the RPC error message when a read fails', async () => {
const client = clientOf({ 'files.read': fail('EIO', 'file_too_large') })
await expect(
+1 -1
View File
@@ -4,7 +4,7 @@ import * as Clipboard from 'expo-clipboard'
import { Check, Copy, Pencil, Play, Trash2 } from 'lucide-react-native'
import { colors, spacing, typography } from '../theme/mobile-theme'
import { MobileAgentIcon } from '../components/MobileAgentIcon'
import type { TerminalQuickCommand } from '../../../src/shared/types'
import type { TerminalQuickCommand } from '../../../src/shared/terminal-quick-command-types'
import {
getQuickCommandDisplayPreview,
getTerminalQuickCommandBody,
+2 -1
View File
@@ -3,7 +3,8 @@ import { Check, Plus, Search } from 'lucide-react-native'
import { colors, spacing } from '../theme/mobile-theme'
import { MobileAgentIcon } from '../components/MobileAgentIcon'
import { MOBILE_AGENT_CATALOG } from '../tasks/mobile-agent-catalog'
import type { TerminalQuickCommand, TuiAgent } from '../../../src/shared/types'
import type { TerminalQuickCommand } from '../../../src/shared/terminal-quick-command-types'
import type { TuiAgent } from '../../../src/shared/tui-agent'
import { supportsTerminalAgentQuickCommand } from '../terminal/quick-commands'
import { QuickCommandRow } from './QuickCommandRow'
@@ -1,7 +1,7 @@
import { createElement, type ReactNode } from 'react'
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { TerminalQuickCommand } from '../../../src/shared/types'
import type { TerminalQuickCommand } from '../../../src/shared/terminal-quick-command-types'
import type { RpcClient } from '../transport/rpc-client'
import { MAX_QUICK_COMMANDS } from '../terminal/quick-commands'
import { QuickCommandsSheet } from './QuickCommandsSheet'
+1 -1
View File
@@ -4,7 +4,7 @@ import { ChevronLeft } from 'lucide-react-native'
import { colors, spacing } from '../theme/mobile-theme'
import { BottomDrawer } from '../components/BottomDrawer'
import type { RpcClient } from '../transport/rpc-client'
import type { TerminalQuickCommand } from '../../../src/shared/types'
import type { TerminalQuickCommand } from '../../../src/shared/terminal-quick-command-types'
import {
getQuickCommandPreview,
MAX_QUICK_COMMANDS,
@@ -5,11 +5,11 @@ import {
buildMobileAiVaultResumeLaunch,
buildMobileAiVaultResumeCommand,
createMobileAiVaultResumeMutationRegistry,
readMobileRuntimeHostPlatform,
readMobileRuntimeTerminalWindowsShell,
resolveMobileAiVaultResumePlatform,
resumeAiVaultSessionInTerminal
} from './ai-vault-resume-launch'
import { readMobileRuntimeHostPlatform } from '../transport/mobile-runtime-host-platform'
import { RESUME_RPC_TIMEOUT_MS } from './ai-vault-resume-preparation'
function session(overrides: Partial<AiVaultSession> = {}): AiVaultSession {
+1 -25
View File
@@ -13,7 +13,7 @@ import {
resolveTuiAgentLaunchEnv
} from '../../../src/shared/tui-agent-launch-defaults'
import { normalizeAiVaultResumeFilePath } from '../../../src/shared/ai-vault-resume-path'
import type { TuiAgent } from '../../../src/shared/types'
import type { TuiAgent } from '../../../src/shared/tui-agent'
import { parseWslUncPath } from '../../../src/shared/wsl-paths'
import { resolveWindowsShellStartupFamily } from '../../../src/shared/windows-terminal-shell'
import type { RpcClient } from '../transport/rpc-client'
@@ -24,20 +24,6 @@ import {
} from './mobile-diff-review-rpc'
import type { MobileAiVaultResumeTargetStatus } from '../agent-history/agent-history-resume-target'
const NODE_PLATFORMS = new Set<NodeJS.Platform>([
'aix',
'android',
'darwin',
'freebsd',
'haiku',
'linux',
'openbsd',
'sunos',
'win32',
'cygwin',
'netbsd'
])
export function buildMobileAiVaultResumeCommand(args: {
session: Pick<AiVaultSession, 'agent' | 'sessionId' | 'cwd' | 'codexHome'> &
Partial<Pick<AiVaultSession, 'filePath'>>
@@ -237,16 +223,6 @@ export function createMobileAiVaultResumeMutationRegistry(
}
}
export function readMobileRuntimeHostPlatform(statusResult: unknown): NodeJS.Platform | null {
if (!statusResult || typeof statusResult !== 'object') {
return null
}
const hostPlatform = (statusResult as { hostPlatform?: unknown }).hostPlatform
return typeof hostPlatform === 'string' && NODE_PLATFORMS.has(hostPlatform as NodeJS.Platform)
? (hostPlatform as NodeJS.Platform)
: null
}
export function readMobileRuntimeTerminalWindowsShell(statusResult: unknown): string | null {
if (!statusResult || typeof statusResult !== 'object') {
return null
@@ -1,4 +1,8 @@
import type { GitHubReaction, GitHubReactionContent, PRComment } from '../../../src/shared/types'
import type {
GitHubReaction,
GitHubReactionContent,
PRComment
} from '../../../src/shared/github/comment-types'
import { isRecord, readBoolean, readNumber, readString } from './github-pr-value-readers'
// Defensive parsers for the PR conversation comments carried by
+1 -1
View File
@@ -1,4 +1,4 @@
import type { GitHubPRMergeMethod } from '../../../src/shared/types'
import type { GitHubPRMergeMethod } from '../../../src/shared/github/pull-request-types'
import type { RpcClient } from '../transport/rpc-client'
import { buildGithubPrParams, githubPrRepoSlugParam, type GitHubPrRepoSlug } from './github-pr-rpc'
+10 -6
View File
@@ -1,15 +1,19 @@
import type {
GitHubAssignableUser,
GitHubPRReviewSummary,
GitHubWorkItem,
GitHubWorkItemDetails,
PRCheckAnnotation,
PRCheckDetail,
PRCheckJob,
PRCheckRunDetails,
PRCheckStep,
PRCheckStep
} from '../../../src/shared/github/check-types'
import type {
GitHubAssignableUser,
GitHubPRReviewSummary,
PRInfo
} from '../../../src/shared/types'
} from '../../../src/shared/github/pull-request-types'
import type {
GitHubWorkItem,
GitHubWorkItemDetails
} from '../../../src/shared/github/work-item-types'
import { readPRComments } from './github-pr-comment-parsers'
import type { HostedReviewInfo } from '../../../src/shared/hosted-review'
import {
+3 -7
View File
@@ -1,10 +1,6 @@
import type {
GitHubAssignableUser,
GitHubWorkItemDetails,
PRCheckDetail,
PRCheckRunDetails,
PRInfo
} from '../../../src/shared/types'
import type { PRCheckDetail, PRCheckRunDetails } from '../../../src/shared/github/check-types'
import type { GitHubAssignableUser, PRInfo } from '../../../src/shared/github/pull-request-types'
import type { GitHubWorkItemDetails } from '../../../src/shared/github/work-item-types'
import type { HostedReviewInfo } from '../../../src/shared/hosted-review'
import {
normalizeGitHubPRForBranchOutcome,
@@ -1,16 +1,16 @@
import type { PRCheckDetail } from '../../../src/shared/github/check-types'
import type {
CheckStatus,
GitHubAssignableUser,
ProviderCheckSummary,
GitHubPRMergeMethod,
GitHubPRMergeMethodSettings,
GitHubPRReviewSummary,
GitHubRepositoryIdentity,
PRCheckDetail,
PRMergeableState,
PRReviewDecision,
PRState
} from '../../../src/shared/types'
PRState,
ProviderCheckSummary
} from '../../../src/shared/github/pull-request-types'
import type { HostedReviewProvider } from '../../../src/shared/hosted-review'
// Primitive + enum value readers shared by the github.* PR parsers. Each narrows
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import type { DiffComment } from '../../../src/shared/types'
import type { DiffComment } from '../../../src/shared/diff-comment-types'
import {
clearSentMobileDiffComments,
countUnsentMobileDiffComments,
@@ -1,4 +1,4 @@
import type { DiffComment } from '../../../src/shared/types'
import type { DiffComment } from '../../../src/shared/diff-comment-types'
export type UpdateMobileDiffCommentInput = {
id: string
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import type { DiffComment } from '../../../src/shared/types'
import type { DiffComment } from '../../../src/shared/diff-comment-types'
import {
addMobileDiffComment,
formatDiffComments,
+1 -1
View File
@@ -1,4 +1,4 @@
import type { DiffComment, DiffReviewScope } from '../../../src/shared/types'
import type { DiffComment, DiffReviewScope } from '../../../src/shared/diff-comment-types'
import { formatDiffComment, formatDiffComments } from '../../../src/shared/diff-comments-format'
export { formatDiffComment, formatDiffComments }
@@ -0,0 +1,59 @@
import { describe, expect, it, vi } from 'vitest'
import type { RpcClient } from '../transport/rpc-client'
import type { RpcResponse } from '../transport/types'
import type { MobileDiffReviewQueueItem } from './mobile-diff-review-queue'
import { loadMobileDiffReviewDiff } from './mobile-diff-review-loaders'
const DELETED_ITEM: MobileDiffReviewQueueItem = {
key: 'unstaged\0unstaged\0\0deleted.ts',
scope: 'unstaged',
area: 'unstaged',
filePath: 'deleted.ts',
status: 'deleted',
title: 'deleted.ts',
subtitle: 'Unstaged',
canStage: true,
canUnstage: false,
canDiscard: true,
isGeneratedOrLockFile: false,
diffIdentity: 'deleted-diff',
noteCount: 0,
unsentNoteCount: 0,
staleNoteCount: 0,
isReviewed: false,
changedSinceReview: false
}
function clientWith(response: RpcResponse): RpcClient {
return {
sendRequest: vi.fn().mockResolvedValue(response)
} as unknown as RpcClient
}
function failure(code: string, message: string): RpcResponse {
return { id: 'rpc-1', ok: false, error: { code, message }, _meta: { runtimeId: 'runtime-1' } }
}
describe('loadMobileDiffReviewDiff', () => {
it('shows the too-large state for an oversized deleted diff', async () => {
await expect(
loadMobileDiffReviewDiff({
client: clientWith(failure('diff_too_large', 'Diff too large')),
worktreeId: 'wt-1',
item: DELETED_ITEM,
branchCompare: null
})
).resolves.toEqual({ kind: 'too-large', itemKey: DELETED_ITEM.key })
})
it('keeps the deleted fallback for hosts that cannot return deleted content', async () => {
await expect(
loadMobileDiffReviewDiff({
client: clientWith(failure('internal_error', 'Unable to read deleted file')),
worktreeId: 'wt-1',
item: DELETED_ITEM,
branchCompare: null
})
).resolves.toEqual({ kind: 'deleted', itemKey: DELETED_ITEM.key })
})
})
@@ -130,6 +130,9 @@ export async function loadMobileDiffReviewDiff(input: DiffLoadInput): Promise<Re
staged: item.scope === 'staged'
})
if (!response.ok) {
if (response.error?.code === 'diff_too_large') {
return { kind: 'too-large', itemKey: item.key }
}
if (item.status === 'deleted') {
return { kind: 'deleted', itemKey: item.key }
}
@@ -1,4 +1,4 @@
import type { DiffReviewScope } from '../../../src/shared/types'
import type { DiffReviewScope } from '../../../src/shared/diff-comment-types'
import type { MobileGitStagingArea } from '../source-control/mobile-git-status'
import {
createMobileDiffReviewFileKey,
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import type { DiffComment, MobileDiffReviewState } from '../../../src/shared/types'
import type { DiffComment, MobileDiffReviewState } from '../../../src/shared/diff-comment-types'
import type { MobileGitBranchChangeEntry } from '../source-control/mobile-branch-compare'
import type { MobileGitStatusEntry } from '../source-control/mobile-git-status'
import {
@@ -1,4 +1,8 @@
import type { DiffComment, DiffReviewScope, MobileDiffReviewState } from '../../../src/shared/types'
import type {
DiffComment,
DiffReviewScope,
MobileDiffReviewState
} from '../../../src/shared/diff-comment-types'
import type { MobileGitBranchChangeEntry } from '../source-control/mobile-branch-compare'
import {
isMobileGitDiscardableEntry,
@@ -1,4 +1,4 @@
import type { DiffComment, MobileDiffReviewState } from '../../../src/shared/types'
import type { DiffComment, MobileDiffReviewState } from '../../../src/shared/diff-comment-types'
import type { MobileGitBranchCompareResult } from '../source-control/mobile-branch-compare'
import type { MobileGitStatusResult } from '../source-control/mobile-git-status'
import type { MobileDiffLine } from './mobile-diff-lines'
@@ -2,7 +2,7 @@ import type {
DiffReviewScope,
MobileDiffReviewFileState,
MobileDiffReviewState
} from '../../../src/shared/types'
} from '../../../src/shared/diff-comment-types'
export type MobileDiffReviewFileDescriptor = {
key: string
@@ -1,4 +1,4 @@
import type { TuiAgent } from '../../../src/shared/types'
import type { TuiAgent } from '../../../src/shared/tui-agent'
import {
filterEnabledMobileTuiAgents,
isMobileTuiAgent,
@@ -1,4 +1,6 @@
import type { GitHubWorkItemDetails, PRCheckDetail, PRInfo } from '../../../src/shared/types'
import type { PRCheckDetail } from '../../../src/shared/github/check-types'
import type { PRInfo } from '../../../src/shared/github/pull-request-types'
import type { GitHubWorkItemDetails } from '../../../src/shared/github/work-item-types'
import type { GitHubPrReadOutcome, GitHubPrRepoSlug } from './github-pr-rpc'
import { resolveLinkedPrNumber } from './mobile-pr-sidebar-resolve'
@@ -1,4 +1,5 @@
import type { DiffComment, TuiAgent } from '../../../src/shared/types'
import type { DiffComment } from '../../../src/shared/diff-comment-types'
import type { TuiAgent } from '../../../src/shared/tui-agent'
import type { AgentStatusEntry } from '../../../src/shared/agent-status-types'
import type { MobileBrowserTab } from '../browser/MobileBrowserPane'
import type { MobileTerminalTheme } from '../terminal/terminal-webview-contract'
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest'
import type { AgentStatusEntry } from '../../../src/shared/agent-status-types'
import type { TuiAgent } from '../../../src/shared/types'
import type { TuiAgent } from '../../../src/shared/tui-agent'
import type { MobileSessionTab } from './mobile-session-route-types'
import {
getMobileSessionTabTitle,
@@ -1,7 +1,7 @@
import { stripLeadingAgentTitleDecorationOrEmpty } from '../../../src/shared/agent-title-decoration'
import { resolveExplicitTerminalTitleAgentType } from '../../../src/shared/terminal-title-agent-type'
import type { AgentStatusEntry } from '../../../src/shared/agent-status-types'
import type { TuiAgent } from '../../../src/shared/types'
import type { TuiAgent } from '../../../src/shared/tui-agent'
import { isBlankBrowserUrl } from '../browser/browser-url'
import type { MobileSessionTab } from './mobile-session-route-types'
@@ -1,4 +1,4 @@
import type { GitHubPRMergeMethod } from '../../../src/shared/types'
import type { GitHubPRMergeMethod } from '../../../src/shared/github/pull-request-types'
import type { GitHubPrMutationOutcome } from './github-pr-mutations'
import type { GitHubPrRepoSlug } from './github-pr-rpc'
+1 -1
View File
@@ -1,4 +1,4 @@
import type { GitHubPRMergeMethod, PRState } from '../../../src/shared/types'
import type { GitHubPRMergeMethod, PRState } from '../../../src/shared/github/pull-request-types'
import { githubRepoIdentityKey } from '../../../src/shared/github/repository-identity-key'
import { classifyPrSidebarFailure } from './mobile-pr-sidebar-state'
import { createOptimisticField, type OptimisticField } from './optimistic-write-sequence'
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import type { PRCheckDetail } from '../../../src/shared/types'
import type { PRCheckDetail } from '../../../src/shared/github/check-types'
import {
buildFixChecksPrompt,
buildResolveConflictsPrompt,
+1 -1
View File
@@ -3,7 +3,7 @@ import {
getBrokenChecks
} from '../../../src/shared/pr-checks-fix-prompt'
import { buildResolvePullRequestConflictsPrompt } from '../../../src/shared/source-control-conflict-prompts'
import type { PRCheckDetail } from '../../../src/shared/types'
import type { PRCheckDetail } from '../../../src/shared/github/check-types'
// Pure prompt builders for the mobile PR sidebar's "Fix checks with AI" /
// "Resolve conflicts with AI" triage actions. Kept free of React/native imports
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import type { PRComment } from '../../../src/shared/types'
import type { PRComment } from '../../../src/shared/github/comment-types'
import {
buildAddRootCommentParams,
buildDeleteCommentParams,
+2 -1
View File
@@ -1,4 +1,5 @@
import type { GitHubOwnerRepo, PRComment, PRState } from '../../../src/shared/types'
import type { PRComment } from '../../../src/shared/github/comment-types'
import type { GitHubOwnerRepo, PRState } from '../../../src/shared/github/pull-request-types'
// Pure helpers for the interactive PR comment timeline (reply / resolve / add
// root comment). Kept free of React/native imports so they unit-test under the
+1 -1
View File
@@ -1,4 +1,4 @@
import type { PRState } from '../../../src/shared/types'
import type { PRState } from '../../../src/shared/github/pull-request-types'
// Pure helpers for the inline PR-title edit affordance. Kept free of React/native
// imports so they unit-test under the node Vitest config, mirroring the other
+3 -3
View File
@@ -1,9 +1,9 @@
import type {
TerminalQuickCommand,
TerminalQuickCommandAction,
TerminalQuickCommandScope,
TuiAgent
} from '../../../src/shared/types'
TerminalQuickCommandScope
} from '../../../src/shared/terminal-quick-command-types'
import type { TuiAgent } from '../../../src/shared/tui-agent'
import {
isAgentQuickCommand,
MAX_QUICK_COMMAND_AGENT_PROMPT_LENGTH,
@@ -1,5 +1,5 @@
import { useCallback, type Dispatch, type SetStateAction } from 'react'
import type { DiffComment, MobileDiffReviewState } from '../../../src/shared/types'
import type { DiffComment, MobileDiffReviewState } from '../../../src/shared/diff-comment-types'
import { triggerError, triggerSuccess } from '../platform/haptics'
import type { ConnectionState } from '../transport/types'
import type { RpcClient } from '../transport/rpc-client'
@@ -1,6 +1,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import type { FlatList } from 'react-native'
import type { DiffComment } from '../../../src/shared/types'
import type { DiffComment } from '../../../src/shared/diff-comment-types'
import type { ConnectionState } from '../transport/types'
import type { RpcClient } from '../transport/rpc-client'
import { getWorktreeLabel } from './worktree-label'
@@ -1,7 +1,7 @@
import { createElement } from 'react'
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { DiffComment } from '../../../src/shared/types'
import type { DiffComment } from '../../../src/shared/diff-comment-types'
import type { RpcClient } from '../transport/rpc-client'
import type { ReviewScreenState } from './mobile-diff-review-screen-model'
import {
@@ -1,6 +1,6 @@
import { useCallback, type Dispatch, type SetStateAction } from 'react'
import * as Clipboard from 'expo-clipboard'
import type { DiffComment, MobileDiffReviewState } from '../../../src/shared/types'
import type { DiffComment, MobileDiffReviewState } from '../../../src/shared/diff-comment-types'
import type { ConnectionState } from '../transport/types'
import type { RpcClient } from '../transport/rpc-client'
import { triggerSuccess } from '../platform/haptics'
@@ -1,5 +1,5 @@
import { useCallback, useMemo, useRef, useState } from 'react'
import type { PRComment } from '../../../src/shared/types'
import type { PRComment } from '../../../src/shared/github/comment-types'
import type { ConnectionState } from '../transport/types'
import type { RpcClient } from '../transport/rpc-client'
import type { GitHubPrRepoSlug } from './github-pr-rpc'
@@ -1,5 +1,7 @@
import { describe, expect, it, vi } from 'vitest'
import type { GitHubWorkItemDetails, PRCheckDetail, PRInfo } from '../../../src/shared/types'
import type { PRCheckDetail } from '../../../src/shared/github/check-types'
import type { PRInfo } from '../../../src/shared/github/pull-request-types'
import type { GitHubWorkItemDetails } from '../../../src/shared/github/work-item-types'
import type { HostedReviewInfo } from '../../../src/shared/hosted-review'
import type { GitHubPrReadOutcome } from './github-pr-rpc'
import {
@@ -1,7 +1,7 @@
import { createElement } from 'react'
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { TerminalQuickCommand } from '../../../src/shared/types'
import type { TerminalQuickCommand } from '../../../src/shared/terminal-quick-command-types'
import type { RpcClient } from '../transport/rpc-client'
import { LogicalClientCutoverError } from '../transport/stable-logical-rpc-client'
import type { RpcResponse } from '../transport/types'
+1 -1
View File
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'
import type { RpcClient } from '../transport/rpc-client'
import { isLogicalClientCutoverError } from '../transport/stable-logical-rpc-client'
import type { RpcFailure, RpcResponse, RpcSuccess } from '../transport/types'
import type { TerminalQuickCommand } from '../../../src/shared/types'
import type { TerminalQuickCommand } from '../../../src/shared/terminal-quick-command-types'
import {
applyTerminalQuickCommandMutation,
parseNormalizedTerminalQuickCommands,
@@ -11,7 +11,7 @@ import {
type MobileCommitRow
} from './mobile-git-history'
import { resolveMobileHistoryScreenView } from './mobile-history-screen-state'
import type { GitBranchChangeEntry } from '../../../src/shared/types'
import type { GitBranchChangeEntry } from '../../../src/shared/git-diff-compare-types'
type Props = {
client: RpcClient | null
@@ -1,5 +1,5 @@
import { describe, expect, expectTypeOf, it } from 'vitest'
import type { GitBranchCompareResult } from '../../../src/shared/types'
import type { GitBranchCompareResult } from '../../../src/shared/git-diff-compare-types'
import {
buildMobileBranchCompareSection,
canOpenMobileBranchCompareDiff,
@@ -2,7 +2,7 @@ import type {
GitBranchChangeEntry,
GitBranchCompareResult,
GitBranchCompareSummary
} from '../../../src/shared/types'
} from '../../../src/shared/git-diff-compare-types'
export type MobileGitBranchChangeEntry = GitBranchChangeEntry
export type MobileGitBranchCompareSummary = GitBranchCompareSummary
@@ -1,5 +1,7 @@
import { describe, it, expect } from 'vitest'
import type { PRCheckDetail, PRComment, PRInfo } from '../../../src/shared/types'
import type { PRCheckDetail } from '../../../src/shared/github/check-types'
import type { PRComment } from '../../../src/shared/github/comment-types'
import type { PRInfo } from '../../../src/shared/github/pull-request-types'
import type { PrSidebarState } from '../session/mobile-pr-sidebar-state'
import { buildMobilePrChipSummary, countUnresolvedReviewThreads } from './mobile-pr-chip-summary'
@@ -1,4 +1,4 @@
import type { PRComment } from '../../../src/shared/types'
import type { PRComment } from '../../../src/shared/github/comment-types'
import type { PrSidebarState } from '../session/mobile-pr-sidebar-state'
import { summarizeProviderChecks } from '../../../src/shared/provider-check-summary'
import {
+1 -1
View File
@@ -1,4 +1,4 @@
import type { TuiAgent } from '../../../src/shared/types'
import type { TuiAgent } from '../../../src/shared/tui-agent'
import type { RpcClient } from '../transport/rpc-client'
import { createWorktreeWithNameRetry, type WorktreeCreateResult } from './worktree-create-retry'
import {
@@ -1,5 +1,7 @@
import { describe, expect, it } from 'vitest'
import type { GitHubWorkItem, GitLabWorkItem, LinearIssue } from '../../../src/shared/types'
import type { GitHubWorkItem } from '../../../src/shared/github/work-item-types'
import type { GitLabWorkItem } from '../../../src/shared/gitlab-types'
import type { LinearIssue } from '../../../src/shared/linear/issue-types'
import {
buildGitHubLinkedWorkItem,
buildGitLabLinkedWorkItem,
@@ -1,4 +1,6 @@
import type { GitHubWorkItem, GitLabWorkItem, LinearIssue } from '../../../src/shared/types'
import type { GitHubWorkItem } from '../../../src/shared/github/work-item-types'
import type { GitLabWorkItem } from '../../../src/shared/gitlab-types'
import type { LinearIssue } from '../../../src/shared/linear/issue-types'
import { getLinearIssueWorkspaceName } from '../../../src/shared/workspace-name'
import {
buildGitHubWorkspaceSource,
@@ -1,6 +1,6 @@
import type { RpcClient } from '../transport/rpc-client'
import type { RpcSuccess } from '../transport/types'
import type { GitHubPrStartPoint } from '../../../src/shared/types'
import type { GitHubPrStartPoint } from '../../../src/shared/worktree/types'
// The resolved start point for a linked PR/MR: the base branch to create from
// plus the optional review-compare ref, push target, and exact branch name.

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