diff --git a/config/scripts/pr-e2e-layout-oracle-routing.test.mjs b/config/scripts/pr-e2e-layout-oracle-routing.test.mjs index a7944d3eec6..9c55e042ff3 100644 --- a/config/scripts/pr-e2e-layout-oracle-routing.test.mjs +++ b/config/scripts/pr-e2e-layout-oracle-routing.test.mjs @@ -18,9 +18,9 @@ const LAYOUT_AUTHORITIES = [ 'src/main/persistence/restoring-sessions/session-owner-fields.ts', 'src/main/persistence/loading-store/pty-binding-persistence.ts', 'src/main/remote-layout-snapshot/index.ts', - 'src/main/runtime/orca-runtime-split-pty-backed-terminal.ts', + 'src/main/runtime/terminal/launch/terminal-split-surface.ts', 'src/main/runtime/session-tabs/headless-tab-group-moves.ts', - 'src/main/runtime/orca-runtime-create-terminal.ts', + 'src/main/runtime/terminal/launch/terminal-create.ts', 'src/renderer/src/runtime/mobile-session-tab-close.ts', 'src/renderer/src/runtime/runtime-layout-client.ts', 'src/renderer/src/store/slices/tabs.ts', @@ -36,7 +36,7 @@ const STACK_WRITERS = [ 'src/renderer/src/components/terminal-pane/TerminalPane.tsx', 'src/renderer/src/runtime/sync-runtime-graph.ts', 'src/renderer/src/runtime/web-session-tabs-sync/store-patch.ts', - 'src/main/runtime/orca-runtime-split-terminal.ts', + 'src/main/runtime/terminal/launch/terminal-split.ts', 'src/main/runtime/session-graph/session-window-graph.ts', 'src/main/runtime/session-tabs/mobile-session-hydration.ts', 'src/main/runtime/session-tabs/mobile-session-reconciliation.ts', diff --git a/config/scripts/pr-layout-oracle-route.mjs b/config/scripts/pr-layout-oracle-route.mjs index 8af3645833f..8ccdc0c8223 100644 --- a/config/scripts/pr-layout-oracle-route.mjs +++ b/config/scripts/pr-layout-oracle-route.mjs @@ -65,6 +65,12 @@ const LAYOUT_ORACLE_PREFIXES = [ 'config/scripts/pr-layout-oracle-route.mjs' ] +const LAUNCH_LAYOUT_WRITERS = [ + 'src/main/runtime/terminal/launch/terminal-create.ts', + 'src/main/runtime/terminal/launch/terminal-split.ts', + 'src/main/runtime/terminal/launch/terminal-split-surface.ts' +] + // Runtime matches only the modules that create, move, close, persist or publish layout. const LAYOUT_ORACLE_RUNTIME_MODULE = /^src\/(?:main|renderer\/src)\/runtime\/(?:[^/]+\/)*[^/]*(?:pane-admission|workspace-layout|session-tab|headless-[^/]*tab|layout-client|layout-command-router|layout-projection|create-terminal|split-terminal|split-pty-backed-terminal|terminal-split-layout|spawn-placement|sync-window-graph|runtime-graph|leaf-move)[^/]*\.ts$/ @@ -81,6 +87,7 @@ export function isLayoutOracleSource(file) { return false } return ( + LAUNCH_LAYOUT_WRITERS.includes(file) || LAYOUT_ORACLE_PREFIXES.some((prefix) => file.startsWith(prefix)) || LAYOUT_ORACLE_RUNTIME_MODULE.test(file) ) diff --git a/config/ts-nocheck-baseline.txt b/config/ts-nocheck-baseline.txt index 35b0643c8b3..928c2b4788e 100644 --- a/config/ts-nocheck-baseline.txt +++ b/config/ts-nocheck-baseline.txt @@ -9,11 +9,6 @@ src/main/runtime/orca-runtime-build-headless-mobile-session-browser-tabs.ts src/main/runtime/orca-runtime-build-pty-terminal-summary.ts src/main/runtime/orca-runtime-close-mobile-session-tab.ts src/main/runtime/orca-runtime-controller-knows-pty-is-live.ts -src/main/runtime/orca-runtime-create-agent-session.ts -src/main/runtime/orca-runtime-create-mobile-session-terminal.ts -src/main/runtime/orca-runtime-create-runtime-owned-mobile-session-terminal.ts -src/main/runtime/orca-runtime-create-terminal-desktop.ts -src/main/runtime/orca-runtime-create-terminal.ts src/main/runtime/orca-runtime-deliver-pending-messages.ts src/main/runtime/orca-runtime-automation-operations.ts src/main/runtime/orca-runtime-file-commands.ts @@ -33,7 +28,6 @@ src/main/runtime/orca-runtime-notify-ssh-state-changed.ts src/main/runtime/orca-runtime-on-pty-exit.ts src/main/runtime/orca-runtime-perform-mobile-session-pty-records-refresh.ts src/main/runtime/orca-runtime-persist-headless-terminal-title.ts -src/main/runtime/orca-runtime-prepare-pty-execution-context.ts src/main/runtime/orca-runtime-preserved-branch-cleanup.ts src/main/runtime/orca-runtime-prune-mobile-session-tab-group-layout.ts src/main/runtime/orca-runtime-pty-foreground-process-reads.ts @@ -43,22 +37,17 @@ src/main/runtime/orca-runtime-refresh-pty-worktree-records-from-controller.ts src/main/runtime/orca-runtime-refresh-pty-worktree-records-with-controller-inventory.ts src/main/runtime/orca-runtime-refuse-unattributed-mobile-session-tab-close.ts src/main/runtime/orca-runtime-register-pty.ts -src/main/runtime/orca-runtime-resolve-browser-network-execution-host-for-worktree.ts src/main/runtime/orca-runtime-resolve-known-workspace-file-target.ts src/main/runtime/orca-runtime-resolve-terminal-pane.ts src/main/runtime/orca-runtime-resolve-terminal-split-source-authority.ts -src/main/runtime/orca-runtime-run-create-mobile-session-terminal.ts src/main/runtime/orca-runtime-runtime-id.ts src/main/runtime/orca-runtime-schedule-mobile-session-tabs-changed.ts src/main/runtime/orca-runtime-serialize-agent-prompt-submission.ts src/main/runtime/orca-runtime-sleep-resolved-worktree-terminals.ts -src/main/runtime/orca-runtime-split-pty-backed-terminal.ts -src/main/runtime/orca-runtime-split-terminal.ts src/main/runtime/orca-runtime-state-fields.ts src/main/runtime/orca-runtime-stop-exact-terminals-for-worktree.ts src/main/runtime/orca-runtime-stop-requested-pty-ids.ts src/main/runtime/orca-runtime-stop-terminals-for-worktree.ts -src/main/runtime/orca-runtime-terminal-create-deduplication.ts src/main/runtime/orca-runtime-terminal-drivers.ts src/main/runtime/orca-runtime-touch-mobile-session-tabs-for-worktree.ts src/main/runtime/orca-runtime-verify-orchestration-compatibility-caller.ts diff --git a/src/main/ipc/floating-workspace-directory.ts b/src/main/ipc/floating-workspace-directory.ts index 02142248117..bceee27cd11 100644 --- a/src/main/ipc/floating-workspace-directory.ts +++ b/src/main/ipc/floating-workspace-directory.ts @@ -6,6 +6,8 @@ import type { GlobalSettings } from '../../shared/global-settings-types' import type { FloatingTerminalCwdRequest } from '../../shared/ui-chrome-types' import type { Store } from '../persistence' +type FloatingTerminalSettings = Pick + const FLOATING_WORKSPACE_DIRNAME = 'floating-workspace' function expandHomePath(input: string, home: string): string { @@ -41,7 +43,7 @@ async function canonicalizeAccessibleDirectory(dirPath: string): Promise { +function getTrustedFloatingWorkspaceDirectories(settings: FloatingTerminalSettings): Set { return new Set( (settings.floatingTerminalTrustedCwds ?? []) .map((trustedPath) => trustedPath.trim()) @@ -63,7 +65,7 @@ async function getPreservedTrustedFloatingWorkspaceDirectories( function isTrustedFloatingWorkspaceDirectory( canonicalDirPath: string, - settings: GlobalSettings + settings: FloatingTerminalSettings ): boolean { return getTrustedFloatingWorkspaceDirectories(settings).has(path.resolve(canonicalDirPath)) } @@ -81,7 +83,7 @@ export async function ensureDefaultFloatingWorkspacePath(): Promise { } export async function resolveFloatingTerminalCwd( - store: Store, + store: { getSettings(): FloatingTerminalSettings }, args?: FloatingTerminalCwdRequest ): Promise { const configuredPath = typeof args?.path === 'string' ? args.path.trim() : '' diff --git a/src/main/ipc/worktree-remote-push-target-materialization.test.ts b/src/main/ipc/worktree-remote-push-target-materialization.test.ts index 0267099023b..c91beaaa7a5 100644 --- a/src/main/ipc/worktree-remote-push-target-materialization.test.ts +++ b/src/main/ipc/worktree-remote-push-target-materialization.test.ts @@ -82,6 +82,62 @@ describe('materializeWorktreePushTargetRemote', () => { ]) }) + it('preserves the legacy null-store failure after existing-remote adoption work', async () => { + const commands: string[][] = [] + gitExecFileAsyncMock.mockImplementation(async (args: string[]) => { + commands.push(args) + if (args[0] === 'remote' && args[1] === 'get-url') { + return { stdout: `${FORK_URL}\n`, stderr: '' } + } + if (args[0] === 'config' && args[1] === '--get-all') { + throw new Error('no such section') + } + if (args[0] === 'symbolic-ref') { + return { stdout: 'contributor/fix\n', stderr: '' } + } + return { stdout: '', stderr: '' } + }) + await expect( + materializeWorktreePushTargetRemote(REPO_PATH, forkTarget(), null, 'repo-1') + ).rejects.toBeInstanceOf(TypeError) + expect(commands).toEqual([ + ['remote', 'get-url', FORK_REMOTE], + ['config', '--get-all', `remote.${FORK_REMOTE}.fetch`], + [ + 'config', + '--add', + `remote.${FORK_REMOTE}.fetch`, + `+refs/heads/contributor/fix*:refs/remotes/${FORK_REMOTE}/contributor/fix*` + ], + ['config', `remote.${FORK_REMOTE}.tagOpt`, '--no-tags'], + ['rev-parse', '--verify', '--quiet', `refs/remotes/${FORK_REMOTE}/contributor/fix`], + ['symbolic-ref', '--short', 'HEAD'], + ['branch', '--set-upstream-to', `${FORK_REMOTE}/contributor/fix`, 'contributor/fix'] + ]) + }) + + it('accepts a null store when both persistence identifiers are omitted', async () => { + gitExecFileAsyncMock.mockImplementation(async (args: string[]) => { + if (args[0] === 'remote' && args[1] === 'get-url') { + return { stdout: `${FORK_URL}\n`, stderr: '' } + } + if (args[0] === 'config' && args[1] === '--get-all') { + throw new Error('no such section') + } + if (args[0] === 'symbolic-ref') { + return { stdout: 'contributor/fix\n', stderr: '' } + } + return { stdout: '', stderr: '' } + }) + const target = forkTarget() + + await expect(materializeWorktreePushTargetRemote(REPO_PATH, target, null)).resolves.toBe(target) + expect(gitExecFileAsyncMock).toHaveBeenLastCalledWith( + ['branch', '--set-upstream-to', `${FORK_REMOTE}/${target.branchName}`, 'contributor/fix'], + { cwd: REPO_PATH } + ) + }) + it('materializes the remote (add + provenance + fetch) when the probe misses', async () => { gitExecFileAsyncMock.mockImplementation(async (args: string[]) => { if (args[0] === 'remote' && args[1] === 'get-url') { diff --git a/src/main/ipc/worktree-remote.ts b/src/main/ipc/worktree-remote.ts index 62852004af0..76f50a0082a 100644 --- a/src/main/ipc/worktree-remote.ts +++ b/src/main/ipc/worktree-remote.ts @@ -641,7 +641,7 @@ async function getSelectedHostedReviewForBranch( export async function prepareWorktreePushTarget( repoPath: string, target: GitPushTarget, - store?: WorktreePushTargetStore, + store?: WorktreePushTargetStore | null, repoId?: string, gitOptions: { wslDistro?: string } = {} ): Promise { @@ -686,7 +686,7 @@ export async function prepareWorktreePushTarget( export async function materializeWorktreePushTargetRemote( repoPath: string, target: GitPushTarget, - store?: WorktreePushTargetStore, + store?: WorktreePushTargetStore | null, repoId?: string, gitOptions: { wslDistro?: string } = {}, worktreeId?: string @@ -757,7 +757,7 @@ async function adoptExistingForkRemoteForBranch( repoPath: string, target: GitPushTarget, gitOptions: { wslDistro?: string }, - store: WorktreePushTargetStore | undefined, + store: WorktreePushTargetStore | null | undefined, repoId: string | undefined, worktreeId: string | undefined ): Promise { @@ -791,7 +791,8 @@ async function adoptExistingForkRemoteForBranch( const owned = store !== undefined && ownerRepoId !== undefined && - isPushTargetRemoteCreatedByKnownWorktree(store, restored, ownerRepoId) + // SAFETY: Legacy null keeps this branch's original failure rather than introducing a fallback. + isPushTargetRemoteCreatedByKnownWorktree(store!, restored, ownerRepoId) const adopted = owned ? { ...restored, remoteCreated: true } : restored persistMaterializedPushTargetIfCreated(store, worktreeId, adopted) return adopted @@ -831,7 +832,7 @@ function runForkRemoteAdoption( // predates `markRemoteOrcaCreated` (no git-config marker either). `setWorktreeMeta` is // optional on `WorktreePushTargetStore` so narrow test/reconciliation stores keep compiling. function persistMaterializedPushTargetIfCreated( - store: WorktreePushTargetStore | undefined, + store: WorktreePushTargetStore | null | undefined, worktreeId: string | undefined, target: GitPushTarget ): void { diff --git a/src/main/runtime/orca-runtime-adopt-terminal-orphans-from-inventory.ts b/src/main/runtime/orca-runtime-adopt-terminal-orphans-from-inventory.ts index c1245fba4c4..49e48227149 100644 --- a/src/main/runtime/orca-runtime-adopt-terminal-orphans-from-inventory.ts +++ b/src/main/runtime/orca-runtime-adopt-terminal-orphans-from-inventory.ts @@ -22,7 +22,7 @@ import { getLatestPtyTitle } from './runtime-worktree-status-projection' import type { PtyLivenessVerdict } from '../../shared/pty-liveness-verdict' export abstract class OrcaRuntimeWithAdoptTerminalOrphansFromInventory extends OrcaRuntimeWithSubscribeToTerminalResize { - protected abstract assertGraphReady(): void + abstract assertGraphReady(): void protected async adoptTerminalOrphansFromInventoryUnderMutation( request: RuntimeTerminalOrphanAdoptionRequest, diff --git a/src/main/runtime/orca-runtime-create-agent-session.ts b/src/main/runtime/orca-runtime-create-agent-session.ts index a74335fe285..5f18cba3c7f 100644 --- a/src/main/runtime/orca-runtime-create-agent-session.ts +++ b/src/main/runtime/orca-runtime-create-agent-session.ts @@ -1,4 +1,5 @@ -// @ts-nocheck -- mechanically split from OrcaRuntimeService; behavior is covered by AST equivalence and characterization tests. +import type { BrowserWindow } from 'electron' +import type { TerminalWorkspaceLaunchScope } from './runtime-legacy-worker-terminal-recovery-types' import { OrcaRuntimeWithGetAgentSessionExecutionNamespace } from './orca-runtime-get-agent-session-execution-namespace' import type { RuntimeAgentSessionRpcCaller, @@ -27,7 +28,30 @@ import { import { agentStartedTelemetry } from '../agent-launch/agent-started-telemetry' import { withAgentSessionAccountEnv } from './agent-session-account-env' -export class OrcaRuntimeWithCreateAgentSession extends OrcaRuntimeWithGetAgentSessionExecutionNamespace { +export abstract class OrcaRuntimeWithCreateAgentSession extends OrcaRuntimeWithGetAgentSessionExecutionNamespace { + abstract resolveWorkspaceTerminalStartupCwd( + workspace: Pick, + requestedCwd?: string | null + ): string | undefined + + protected abstract buildTerminalWorkspaceEnv( + scope: TerminalWorkspaceLaunchScope, + baseEnv: Record, + paneKey: string, + tabId: string, + agentTeamsEnv?: Record + ): Promise> + + abstract getAuthoritativeWindow(): BrowserWindow + + abstract waitForTerminalHandle(tabId: string, timeoutMs?: number): Promise + + protected abstract reconcileRemoteTerminalCreate( + worktreeId: string, + terminalHandle: string, + connectionId?: string | null + ): Promise + async createAgentSession( request: RuntimeCreateAgentSessionRequest, caller: RuntimeAgentSessionRpcCaller = {} diff --git a/src/main/runtime/orca-runtime-create-terminal-desktop.ts b/src/main/runtime/orca-runtime-create-terminal-desktop.ts index ce0afee9024..93656a1ce1d 100644 --- a/src/main/runtime/orca-runtime-create-terminal-desktop.ts +++ b/src/main/runtime/orca-runtime-create-terminal-desktop.ts @@ -1,6 +1,5 @@ -// @ts-nocheck -- mechanically split from OrcaRuntimeService; behavior is covered by AST equivalence and characterization tests. import * as dependencies from './orca-runtime-create-terminal-dependencies' -import type { OrcaRuntimeWithCreateTerminal } from './orca-runtime-create-terminal' +import type { OrcaRuntimeWithCreateTerminal } from './terminal/launch/terminal-create' import type { RuntimeTerminalPresentation } from '../../shared/runtime-types' // A create with no workspace lands in whatever the window is showing, so only the window can make it. diff --git a/src/main/runtime/orca-runtime-default-tab-agent-status.test.ts b/src/main/runtime/orca-runtime-default-tab-agent-status.test.ts index dc2586ed902..0d79d6ced42 100644 --- a/src/main/runtime/orca-runtime-default-tab-agent-status.test.ts +++ b/src/main/runtime/orca-runtime-default-tab-agent-status.test.ts @@ -32,7 +32,7 @@ class DefaultTabRuntime extends OrcaRuntimeService { super(store) } - protected override async resolveTerminalWorkspaceLaunchScope(): Promise { + override async resolveTerminalWorkspaceLaunchScope(): Promise { return { id: WORKTREE_ID, path: '/repo/app', diff --git a/src/main/runtime/orca-runtime-migrated-pane-restore-cwd.test.ts b/src/main/runtime/orca-runtime-migrated-pane-restore-cwd.test.ts index 8ab0f8b55b6..cb8a2c990f9 100644 --- a/src/main/runtime/orca-runtime-migrated-pane-restore-cwd.test.ts +++ b/src/main/runtime/orca-runtime-migrated-pane-restore-cwd.test.ts @@ -35,7 +35,7 @@ class MigratedPaneRuntime extends OrcaRuntimeService { super(store) } - protected override async resolveTerminalWorkspaceLaunchScope(): Promise { + override async resolveTerminalWorkspaceLaunchScope(): Promise { return { id: WORKTREE_ID, path: worktreePath, diff --git a/src/main/runtime/orca-runtime-preserved-branch-cleanup.ts b/src/main/runtime/orca-runtime-preserved-branch-cleanup.ts index 19e86089beb..95e0dc289b2 100644 --- a/src/main/runtime/orca-runtime-preserved-branch-cleanup.ts +++ b/src/main/runtime/orca-runtime-preserved-branch-cleanup.ts @@ -60,7 +60,7 @@ export abstract class OrcaRuntimeWithPreservedBranchCleanup extends OrcaRuntimeW folderWorkspace: FolderWorkspace ): ResolvedWorktree - protected abstract resolveTerminalWorkspaceLaunchScope( + abstract resolveTerminalWorkspaceLaunchScope( selector: string, createdWorktree?: Worktree ): Promise diff --git a/src/main/runtime/orca-runtime-resolve-terminal-pane.ts b/src/main/runtime/orca-runtime-resolve-terminal-pane.ts index 32fe977b6e3..99d5923c1a5 100644 --- a/src/main/runtime/orca-runtime-resolve-terminal-pane.ts +++ b/src/main/runtime/orca-runtime-resolve-terminal-pane.ts @@ -1,5 +1,5 @@ // @ts-nocheck -- mechanically split from OrcaRuntimeService; behavior is covered by AST equivalence and characterization tests. -import type { OrcaRuntimeWithCreateTerminal } from './orca-runtime-create-terminal' +import type { OrcaRuntimeWithCreateTerminal } from './terminal/launch/terminal-create' import { OrcaRuntimeWithGetTerminalInteractiveWait } from './terminal/wait/terminal-interactive-wait' import type { RuntimeTerminalRead, diff --git a/src/main/runtime/orca-runtime-resolve-terminal-split-source-authority.ts b/src/main/runtime/orca-runtime-resolve-terminal-split-source-authority.ts index 41bd565825e..97d46e2a7f6 100644 --- a/src/main/runtime/orca-runtime-resolve-terminal-split-source-authority.ts +++ b/src/main/runtime/orca-runtime-resolve-terminal-split-source-authority.ts @@ -1,5 +1,5 @@ // @ts-nocheck -- mechanically split from OrcaRuntimeService; behavior is covered by AST equivalence and characterization tests. -import { OrcaRuntimeWithSplitPtyBackedTerminal } from './orca-runtime-split-pty-backed-terminal' +import { OrcaRuntimeWithSplitPtyBackedTerminal } from './terminal/launch/terminal-split-surface' import { resolveTerminalSessionWorktreeId, runtimeWorktreeIdsEqual diff --git a/src/main/runtime/orca-runtime-resolve-waiter.ts b/src/main/runtime/orca-runtime-resolve-waiter.ts index 2306d56a612..32fe47cec78 100644 --- a/src/main/runtime/orca-runtime-resolve-waiter.ts +++ b/src/main/runtime/orca-runtime-resolve-waiter.ts @@ -25,7 +25,7 @@ export class OrcaRuntimeWithResolveWaiter extends OrcaRuntimeWithDeliverPendingM return `${tabId}::${leafId}` } - protected getAuthoritativeWindow(): BrowserWindow { + getAuthoritativeWindow(): BrowserWindow { const win = this.getAvailableAuthoritativeWindow() if (!win || win.isDestroyed()) { throw new Error('No renderer window available') diff --git a/src/main/runtime/orca-runtime-runtime-id.ts b/src/main/runtime/orca-runtime-runtime-id.ts index 8b7168c884e..54f5ad439d2 100644 --- a/src/main/runtime/orca-runtime-runtime-id.ts +++ b/src/main/runtime/orca-runtime-runtime-id.ts @@ -289,7 +289,7 @@ export class OrcaRuntimeWithRuntimeId { // ptyId keeps active TUI redraws independent of the total open terminal count. protected leavesByPtyId = new Map() - protected handles = new Map() + handles = new Map() protected handleByLeafKey = new Map() diff --git a/src/main/runtime/orca-runtime-terminal-create-idempotency.test.ts b/src/main/runtime/orca-runtime-terminal-create-idempotency.test.ts index 5f267ff8ce4..1537312d786 100644 --- a/src/main/runtime/orca-runtime-terminal-create-idempotency.test.ts +++ b/src/main/runtime/orca-runtime-terminal-create-idempotency.test.ts @@ -1,6 +1,9 @@ +import type { TerminalWorkspaceLaunchScope } from './runtime-legacy-worker-terminal-recovery-types' +import type { RuntimePtyController } from './runtime-pty-controller-contract' import { describe, expect, it, vi } from 'vitest' import type { PtyProcessInfo } from '../providers/pty-provider-contract' import type { RuntimeTerminalCreate } from '../../shared/runtime-types' +import type { RuntimeTerminalSplit } from '../../shared/runtime-terminal-contracts' import { OrcaRuntimeService } from './orca-runtime' import { deriveRemoteRuntimeTerminalCreateHandle } from './remote-runtime-terminal-create-identity' import { RemoteRuntimeTerminalCreateIdempotency } from './remote-runtime-terminal-create-idempotency' @@ -380,3 +383,401 @@ describe('terminal create reconciliation scopes inventory to the owning executio expect(listProcesses).toHaveBeenCalledWith('local') }) }) + +function scope(): TerminalWorkspaceLaunchScope { + return { + id: 'repo::workspace', + path: '/workspace', + connectionId: null, + repo: null, + folderWorkspace: null + } +} +function replacement(): never { + throw new Error('an argument getter replaced an already captured runtime method') +} + +describe('launch runtime dispatch and allocation phases', () => { + it('retains native async method metadata on the original runtime frame', () => { + const runtime = new OrcaRuntimeService(null) + expect(runtime.dedupeTerminalCreate.constructor.name).toBe('AsyncFunction') + expect(runtime.dedupeTerminalCreate.length).toBe(5) + expect(runtime.dedupeWorktreeCreate.constructor.name).toBe('Function') + expect(runtime.dedupeWorktreeCreate.length).toBe(3) + }) + + it('retains first-Promise behavior when caller operation keys collide', async () => { + const runtime = new OrcaRuntimeService(null) + const firstFactory = vi.fn(async () => ({ worktree: 'first' })) + const secondFactory = vi.fn(async () => ({ agentLaunch: 'second' })) + const first = runtime.dedupeWorktreeCreate('repo', 'agent.launch:mutation', firstFactory) + const second = runtime.dedupeWorktreeCreate('repo', 'agent.launch:mutation', secondFactory) + expect(second).toBe(first) + await expect(first).resolves.toEqual({ worktree: 'first' }) + expect(firstFactory).toHaveBeenCalledTimes(1) + expect(secondFactory).not.toHaveBeenCalled() + }) + + it('captures private agent-session reconciliation before identity getters', async () => { + const runtime = new OrcaRuntimeService(null) + const result = createdTerminal('reclaimed') + const events: string[] = [] + Object.defineProperty(runtime, 'reconcileRemoteTerminalCreate', { + configurable: true, + get: () => { + events.push('read-reconcile') + return async function (this: OrcaRuntimeService, ...args: unknown[]) { + expect(this).toBe(runtime) + expect(args).toEqual(['worktree-1', 'reclaimed', null]) + return result + } + } + }) + const identity = { + get worktreeId() { + events.push('read-worktree') + Object.defineProperty(runtime, 'reconcileRemoteTerminalCreate', { value: replacement }) + return 'worktree-1' + }, + terminalHandle: 'reclaimed', + connectionId: null + } + await expect(runtime['reclaimFencedAgentSessionSpawn'](identity)).resolves.toBe(result) + expect(events).toEqual(['read-reconcile', 'read-worktree']) + }) + + it('retains direct spawn failure and withdrawal after awaited admission removes spawn', async () => { + const runtime = new OrcaRuntimeService(null) + const entered = Promise.withResolvers() + const ready = Promise.withResolvers() + const events: string[] = [] + const spawn = vi.fn(async () => { + throw new Error('removed spawn must not run') + }) + const controller: Pick = { spawn } + const store = { + admitTerminalPane: async function () { + expect(this).toBe(store) + events.push('admit') + entered.resolve() + await ready.promise + delete controller.spawn + events.push('removed') + return 'admitted' + }, + withdrawTerminalPane: vi.fn(async function (this: typeof store): Promise { + expect(this).toBe(store) + events.push('withdraw') + }) + } + Object.defineProperty(runtime, 'store', { value: store }) + Object.defineProperty(runtime, 'ptyController', { value: controller }) + Object.defineProperty(runtime, 'getAvailableAuthoritativeWindow', { value: () => null }) + Object.defineProperty(runtime, 'resolveTerminalWorkspaceLaunchScope', { + value: async () => scope() + }) + Object.defineProperty(runtime, 'resolveAgentTerminalCreateOptions', { + value: async (_workspace: unknown, opts: unknown) => opts + }) + Object.defineProperty(runtime, 'buildTerminalWorkspaceEnv', { value: async () => ({}) }) + const pending = runtime.createTerminal('workspace') + await entered.promise + expect(events).toEqual(['admit']) + expect(store.withdrawTerminalPane).not.toHaveBeenCalled() + ready.resolve() + await expect(pending).rejects.toBeInstanceOf(TypeError) + expect(events).toEqual(['admit', 'removed', 'withdraw']) + expect(store.withdrawTerminalPane).toHaveBeenCalledTimes(1) + expect(spawn).not.toHaveBeenCalled() + }) + + it('reenters the split override and captures it before the PTY argument getter', async () => { + const runtime = new OrcaRuntimeService(null) + const pty = runtime['recordPtyWorktree']('pty', 'repo::workspace') + const result: RuntimeTerminalSplit = { + handle: 'split', + tabId: 'tab', + leafId: 'leaf', + paneRuntimeId: 1 + } + const events: string[] = [] + Object.defineProperty(runtime, 'splitPtyBackedTerminal', { + configurable: true, + get: () => { + events.push('read-split') + return async function ( + this: OrcaRuntimeService, + actual: unknown + ): Promise { + expect(this).toBe(runtime) + expect(actual).toBe(pty) + return result + } + } + }) + Object.defineProperty(runtime, 'getLivePtyForHandle', { + value: () => ({ + record: null, + get pty() { + events.push('read-pty') + Object.defineProperty(runtime, 'splitPtyBackedTerminal', { value: replacement }) + return pty + } + }) + }) + await expect(runtime.splitTerminal('handle')).resolves.toBe(result) + expect(events).toEqual(['read-split', 'read-pty']) + }) + + it('reenters workspace target resolution on the runtime', async () => { + const runtime = new OrcaRuntimeService(null) + const workspace = scope() + Object.defineProperty(runtime, 'resolveTerminalWorkspaceLaunchTarget', { + value: async function (this: OrcaRuntimeService, selector: string) { + expect(this).toBe(runtime) + expect(selector).toBe('folder') + return { scope: workspace, managedWorktree: null } + } + }) + await expect(runtime['resolveTerminalWorkspaceLaunchScope']('folder')).resolves.toBe(workspace) + }) + + it('reads reconciliation and its canonical cache after workspace resolution', async () => { + const runtime = new OrcaRuntimeService(null) + const ready = Promise.withResolvers() + const result: RuntimeTerminalCreate = { + handle: 'adopted', + worktreeId: 'repo::workspace', + title: null + } + Object.defineProperty(runtime, 'resolveTerminalWorkspaceLaunchScope', { + value: () => ready.promise + }) + const run = vi.fn(async () => result) + const pending = runtime.dedupeTerminalCreate('device', 'workspace', 'mutation', true, run) + const events: string[] = [] + Object.defineProperty(runtime, 'reconcileRemoteTerminalCreate', { + value: async function ( + this: OrcaRuntimeService, + worktreeId: string, + _handle: string, + connection: null + ) { + expect(this).toBe(runtime) + expect(worktreeId).toBe('repo::workspace') + expect(connection).toBeNull() + events.push('reconcile') + return result + } + }) + const cache = runtime['terminalCreateIdempotency'] + Object.defineProperty(runtime, 'terminalCreateIdempotency', { + get: () => { + events.push('read-cache') + return cache + } + }) + ready.resolve(scope()) + await expect(pending).resolves.toBe(result) + expect(events).toEqual(['read-cache', 'reconcile']) + expect(run).not.toHaveBeenCalled() + }) + + it('captures startup and create overrides before option getters', async () => { + const runtime = new OrcaRuntimeService(null) + const repo = { + id: 'repo', + path: '/workspace', + displayName: 'repo', + badgeColor: '#000', + addedAt: 0 + } + const terminal: RuntimeTerminalCreate = { + handle: 'terminal', + worktreeId: 'repo::workspace', + title: null + } + Object.defineProperty(runtime, 'store', { + value: { getRepos: () => [repo], getRepo: () => repo } + }) + Object.defineProperty(runtime, 'resolveWorktreeSelector', { + value: async () => ({ id: 'repo::workspace', repoId: 'repo', path: '/workspace' }) + }) + const events: string[] = [] + Object.defineProperty(runtime, 'buildStartupForAgent', { + configurable: true, + get: () => { + events.push('read-startup') + return function (this: OrcaRuntimeService) { + expect(this).toBe(runtime) + return { agent: 'codex', startup: { command: 'codex' } } + } + } + }) + Object.defineProperty(runtime, 'createTerminal', { + configurable: true, + get: () => { + events.push('read-create') + return async function (this: OrcaRuntimeService) { + expect(this).toBe(runtime) + return terminal + } + } + }) + await expect( + runtime.launchAgentTerminal('workspace', { + get agent(): 'codex' { + events.push('read-agent') + Object.defineProperty(runtime, 'buildStartupForAgent', { value: replacement }) + return 'codex' + }, + prompt: 'prompt', + get title(): string { + events.push('read-title') + Object.defineProperty(runtime, 'createTerminal', { value: replacement }) + return 'title' + } + }) + ).resolves.toBe(terminal) + expect(events).toEqual(['read-startup', 'read-agent', 'read-create', 'read-title']) + }) + + it('captures admission store and controller before spawn argument getters', async () => { + const runtime = new OrcaRuntimeService(null) + const expected = new Error('captured controller refusal') + const admission = vi.fn(async () => undefined) + const originalStore = { admitTerminalPane: admission } + let currentStore = originalStore + const originalController = { + spawn: async () => { + throw expected + } + } + let currentController = originalController + Object.defineProperty(runtime, 'store', { get: () => currentStore }) + Object.defineProperty(runtime, 'ptyController', { get: () => currentController }) + Object.defineProperty(runtime, 'getAvailableAuthoritativeWindow', { value: () => null }) + Object.defineProperty(runtime, 'resolveTerminalWorkspaceLaunchScope', { + value: async () => scope() + }) + Object.defineProperty(runtime, 'resolveAgentTerminalCreateOptions', { + value: async (_workspace: unknown, opts: unknown) => opts + }) + Object.defineProperty(runtime, 'buildTerminalWorkspaceEnv', { value: async () => ({}) }) + await expect( + runtime.createTerminal('workspace', { + get resumeProviderSession() { + currentStore = { + admitTerminalPane: vi.fn(async () => { + throw new Error('late store') + }) + } + currentController = { + spawn: async () => { + throw new Error('late controller') + } + } + return undefined + } + }) + ).rejects.toBe(expected) + expect(admission).toHaveBeenCalledTimes(1) + }) + + it('rereads the canonical mobile mutation map after creation and in failure cleanup', async () => { + const runtime = new OrcaRuntimeService(null) + const first = new Map>() + const second = new Map>() + let current = first + const reads: string[] = [] + Object.defineProperty(runtime, 'mobileTerminalCreateByMutationId', { + get: () => { + reads.push(current === first ? 'first' : 'second') + return current + } + }) + const ready = Promise.withResolvers() + const expected = new Error('creation refusal') + Object.defineProperty(runtime, 'runCreateMobileSessionTerminal', { + value: () => { + current = second + return ready.promise + } + }) + const pending = runtime.createMobileSessionTerminal('workspace', { + clientMutationId: 'mutation', + select: false + }) + expect(first.size).toBe(0) + expect(second.size).toBe(1) + ready.reject(expected) + await expect(pending).rejects.toBe(expected) + expect(second.size).toBe(0) + expect(reads).toEqual(['first', 'second', 'second', 'second']) + }) + + it('checks graph readiness on the runtime before resolving its desktop window', async () => { + const runtime = new OrcaRuntimeService(null) + const expected = new Error('desktop window refusal') + const events: string[] = [] + Object.defineProperty(runtime, 'getAvailableAuthoritativeWindow', { value: () => null }) + Object.defineProperty(runtime, 'assertGraphReady', { + get: () => { + events.push('read-ready') + return function (this: OrcaRuntimeService) { + expect(this).toBe(runtime) + events.push('invoke-ready') + } + } + }) + Object.defineProperty(runtime, 'getAuthoritativeWindow', { + value: () => { + events.push('read-window') + throw expected + } + }) + await expect(runtime.createTerminal()).rejects.toBe(expected) + expect(events).toEqual(['read-ready', 'invoke-ready', 'read-window']) + }) + + it('retains the canonical cache allocations after their original preceding clocks', () => { + const events: { kind: 'map' | 'clock'; value?: object; stack: string }[] = [] + const OriginalMap = Map + const originalNow = Date.now + class TracedMap extends OriginalMap { + constructor(entries?: Iterable | null) { + super(entries) + events.push({ + kind: 'map', + value: this, + stack: new Error('allocation phase trace').stack ?? '' + }) + } + } + vi.stubGlobal('Map', TracedMap) + const clock = vi.spyOn(Date, 'now').mockImplementation(() => { + events.push({ kind: 'clock', stack: new Error('allocation phase trace').stack ?? '' }) + return originalNow() + }) + try { + const runtime = new OrcaRuntimeService(null) + const mobileCache = runtime['mobileTerminalCreateByMutationId'] + const cacheIndex = events.findIndex((event) => event.value === mobileCache) + const rootClock = events.findIndex((event) => event.kind === 'clock') + expect(cacheIndex).toBeGreaterThanOrEqual(0) + expect(cacheIndex).toBeGreaterThan(rootClock) + expect( + events.filter( + (event) => + event.kind === 'map' && + /terminal\/launch\/(?:mobile-terminal-create|terminal-create-identity)\.ts/.test( + event.stack.replaceAll('\\', '/') + ) + ) + ).toEqual([]) + } finally { + clock.mockRestore() + vi.unstubAllGlobals() + } + }) +}) diff --git a/src/main/runtime/orca-runtime-wait-for-mobile-terminal-surface.ts b/src/main/runtime/orca-runtime-wait-for-mobile-terminal-surface.ts index 11b5d6619a6..32aca336e6a 100644 --- a/src/main/runtime/orca-runtime-wait-for-mobile-terminal-surface.ts +++ b/src/main/runtime/orca-runtime-wait-for-mobile-terminal-surface.ts @@ -1,6 +1,6 @@ /* eslint-disable unicorn/no-useless-spread */ // @ts-nocheck -- mechanically split from OrcaRuntimeService; behavior is covered by AST equivalence and characterization tests. -import { OrcaRuntimeWithCreateRuntimeOwnedMobileSessionTerminal } from './orca-runtime-create-runtime-owned-mobile-session-terminal' +import { OrcaRuntimeWithCreateRuntimeOwnedMobileSessionTerminal } from './terminal/launch/mobile-terminal-owned-create' import type { RuntimeMobileSessionCreateTerminalResult, RuntimeMobileSessionTerminalClientTab diff --git a/src/main/runtime/runtime-pane-admission.ts b/src/main/runtime/runtime-pane-admission.ts index 0dda455757b..1867b869e8d 100644 --- a/src/main/runtime/runtime-pane-admission.ts +++ b/src/main/runtime/runtime-pane-admission.ts @@ -61,7 +61,7 @@ export async function withdrawRuntimePane( */ export async function spawnInAdmittedPane( store: RuntimeStore | null | undefined, - controller: Required>, + controller: Pick, args: AdmittedSpawnArgs ): ReturnType { const outcome = await store?.admitTerminalPane?.(admissionFor(args), hostIdOf(args)) @@ -72,7 +72,8 @@ export async function spawnInAdmittedPane( const { groupId: _groupId, ...spawnArgs } = args let result: Awaited> try { - result = await controller.spawn(spawnArgs) + // SAFETY: Callers require spawn; a lost capability during admission keeps the original failure. + result = await controller.spawn!(spawnArgs) } catch (error) { if (outcome === 'admitted') { await withdrawRuntimePane(store, args) diff --git a/src/main/runtime/runtime-store-contract.ts b/src/main/runtime/runtime-store-contract.ts index 035a0499d94..891568c0738 100644 --- a/src/main/runtime/runtime-store-contract.ts +++ b/src/main/runtime/runtime-store-contract.ts @@ -111,6 +111,8 @@ export type RuntimeStore = { // Read by the launch-line carry rule to name the shell a local line is typed into. terminalDefaultShell?: GlobalSettings['terminalDefaultShell'] floatingTerminalEnabled?: GlobalSettings['floatingTerminalEnabled'] + floatingTerminalCwd?: GlobalSettings['floatingTerminalCwd'] + floatingTerminalTrustedCwds?: GlobalSettings['floatingTerminalTrustedCwds'] agentStatusHooksEnabled?: GlobalSettings['agentStatusHooksEnabled'] terminalCopyTrimsGutter?: GlobalSettings['terminalCopyTrimsGutter'] experimentalNativeChat?: GlobalSettings['experimentalNativeChat'] diff --git a/src/main/runtime/runtime-terminal-spawn-push-target-materialization.ts b/src/main/runtime/runtime-terminal-spawn-push-target-materialization.ts index c9b683887f2..98c2b79825c 100644 --- a/src/main/runtime/runtime-terminal-spawn-push-target-materialization.ts +++ b/src/main/runtime/runtime-terminal-spawn-push-target-materialization.ts @@ -2,7 +2,10 @@ import { materializeWorktreePushTargetRemote } from '../ipc/worktree-remote' import { getLocalProjectWorktreeGitOptions } from '../project-runtime-git-options' import type { GitPushTarget } from '../../shared/worktree/types' import type { Repo } from '../../shared/repo-types' -import type { Store } from '../persistence' +import type { WorktreePushTargetStore } from '../ipc/worktree-push-target-cleanup' +import type { ProjectRuntimeResolutionStore } from '../local-project-runtime-resolution' + +type TerminalPushTargetStore = WorktreePushTargetStore & ProjectRuntimeResolutionStore // Why (#17828): a fork-PR remote deferred at worktree-create time must exist before an // autonomous agent's raw git commands run in a freshly opened terminal -- "sync through @@ -13,7 +16,7 @@ export function triggerTerminalSpawnPushTargetMaterialization( worktreePath: string, pushTarget: GitPushTarget | undefined, repo: Repo | null | undefined, - store: Store | undefined, + store: TerminalPushTargetStore | null | undefined, repoId?: string, worktreeId?: string ): void { @@ -37,7 +40,7 @@ export function triggerTerminalSpawnPushTargetMaterialization( } function localGitOptionsForTerminalSpawn( - store: Store | undefined, + store: ProjectRuntimeResolutionStore | null | undefined, repo: Repo | null | undefined ): { wslDistro?: string } { if (!store || !repo) { diff --git a/src/main/runtime/session-graph/session-graph-authority.ts b/src/main/runtime/session-graph/session-graph-authority.ts index fe3faee892a..4edc65f9019 100644 --- a/src/main/runtime/session-graph/session-graph-authority.ts +++ b/src/main/runtime/session-graph/session-graph-authority.ts @@ -1,4 +1,4 @@ -import type { OrcaRuntimeWithResolveBrowserNetworkExecutionHostForWorktree as ResolveBrowserNetworkExecutionHostForWorktreeMethods } from '../orca-runtime-resolve-browser-network-execution-host-for-worktree' +import type { OrcaRuntimeWithResolveBrowserNetworkExecutionHostForWorktree as ResolveBrowserNetworkExecutionHostForWorktreeMethods } from '../terminal/launch/terminal-workspace-scope' import { OrcaRuntimeWithHasTerminalsForWorktree } from './session-graph-reload-lifecycle' import { HEADLESS_RUNTIME_WINDOW_ID } from '../../../shared/runtime-types' import type { FolderWorkspace } from '../../../shared/folder-workspace-types' @@ -65,7 +65,7 @@ export abstract class OrcaRuntimeWithTransitionGraphReloadToTerminalState extend this.markSessionTabsInventoryPublished() } - protected assertGraphReady(): void { + assertGraphReady(): void { if (this.graphStatus !== 'ready') { throw new Error('runtime_unavailable') } diff --git a/src/main/runtime/session-tabs/renderer-session-terminal-rescue.ts b/src/main/runtime/session-tabs/renderer-session-terminal-rescue.ts index 684e109d62a..ce1f1cc78b4 100644 --- a/src/main/runtime/session-tabs/renderer-session-terminal-rescue.ts +++ b/src/main/runtime/session-tabs/renderer-session-terminal-rescue.ts @@ -134,7 +134,7 @@ export abstract class OrcaRuntimeWithRestoreLivePairedRendererSessionOwnedMobile } } - protected waitForTerminalHandle(tabId: string, timeoutMs = 10_000): Promise { + waitForTerminalHandle(tabId: string, timeoutMs = 10_000): Promise { const existing = this.resolveHandleForTab(tabId) if (existing) { return Promise.resolve(existing) diff --git a/src/main/runtime/session-tabs/session-tabs-inventory.ts b/src/main/runtime/session-tabs/session-tabs-inventory.ts index 81b580f1f39..592bd1034a3 100644 --- a/src/main/runtime/session-tabs/session-tabs-inventory.ts +++ b/src/main/runtime/session-tabs/session-tabs-inventory.ts @@ -1,6 +1,6 @@ import type { RuntimeSessionTabsInventoryPublication as SessionTabsInventoryPublicationMethods } from './session-tabs-inventory-publication' import type { OrcaRuntimeWithScheduleMobileSessionTabsChanged as ScheduleMobileSessionTabsChangedMethods } from '../orca-runtime-schedule-mobile-session-tabs-changed' -import type { OrcaRuntimeWithResolveBrowserNetworkExecutionHostForWorktree as ResolveBrowserNetworkExecutionHostForWorktreeMethods } from '../orca-runtime-resolve-browser-network-execution-host-for-worktree' +import type { OrcaRuntimeWithResolveBrowserNetworkExecutionHostForWorktree as ResolveBrowserNetworkExecutionHostForWorktreeMethods } from '../terminal/launch/terminal-workspace-scope' import type { OrcaRuntimeWithHydrateHeadlessMobileSessionTabsFromWorkspaceSession as HydrateHeadlessMobileSessionTabsFromWorkspaceSessionMethods } from './mobile-session-hydration' import type { OrcaRuntimeWithBuildHeadlessMobileSessionBrowserTabs as BuildHeadlessMobileSessionBrowserTabsMethods } from '../orca-runtime-build-headless-mobile-session-browser-tabs' import type { OrcaRuntimeWithRestoreLivePairedRendererSessionOwnedMobileTerminals as RestoreLivePairedRendererSessionOwnedMobileTerminalsMethods } from './renderer-session-terminal-rescue' diff --git a/src/main/runtime/terminal/launch/agent-teams-launch-plan.ts b/src/main/runtime/terminal/launch/agent-teams-launch-plan.ts index 0f15769b26b..abb29be8a14 100644 --- a/src/main/runtime/terminal/launch/agent-teams-launch-plan.ts +++ b/src/main/runtime/terminal/launch/agent-teams-launch-plan.ts @@ -1,3 +1,4 @@ +import type { RuntimePtyController } from '../../runtime-pty-controller-contract' import { addClaudeTeammateModeAuto, addClaudeTeammateModeInProcess, @@ -13,7 +14,10 @@ export async function buildRuntimeAgentTeamsLaunchPlan(args: { claudeAgentTeamsSourceCommand?: string claudeAgentTeamsMode?: ClaudeAgentTeamsMode baseEnv: Record - adoptedBeforeLaunch: boolean + adoptedBeforeLaunch: + | boolean + | Awaited>> + | undefined createTeamEnv: (shimDir: string, shimBin: string) => Record }): Promise<{ plan: Awaited> | undefined diff --git a/src/main/runtime/orca-runtime-create-mobile-session-terminal.ts b/src/main/runtime/terminal/launch/mobile-terminal-create.ts similarity index 69% rename from src/main/runtime/orca-runtime-create-mobile-session-terminal.ts rename to src/main/runtime/terminal/launch/mobile-terminal-create.ts index 8f6d71a9e89..4f41e965b79 100644 --- a/src/main/runtime/orca-runtime-create-mobile-session-terminal.ts +++ b/src/main/runtime/terminal/launch/mobile-terminal-create.ts @@ -1,14 +1,29 @@ -// @ts-nocheck -- mechanically split from OrcaRuntimeService; behavior is covered by AST equivalence and characterization tests. -import { OrcaRuntimeWithCreateTerminal } from './orca-runtime-create-terminal' -import type { WorktreeStartupLaunch } from '../../shared/worktree/launch-types' -import type { TuiAgent } from '../../shared/tui-agent' -import type { SleepingAgentLaunchConfig } from '../../shared/agent-session-resume' -import type { RuntimeNavigationTarget } from '../../shared/runtime-navigation' -import type { RuntimeMobileSessionCreateTerminalResult } from '../../shared/runtime-types' -import { navigationTargetsHost } from '../../shared/runtime-navigation' -import { MOBILE_TERMINAL_CREATE_RESULT_TTL_MS } from './orca-runtime-core' +import type { MobileTerminalCreateOptions } from './mobile-terminal-renderer-create' +import type { RuntimeMobileSessionTabsResult } from '../../../../shared/runtime-session-contracts' +import { OrcaRuntimeWithCreateTerminal } from './terminal-create' +import type { WorktreeStartupLaunch } from '../../../../shared/worktree/launch-types' +import type { TuiAgent } from '../../../../shared/tui-agent' +import type { SleepingAgentLaunchConfig } from '../../../../shared/agent-session-resume' +import type { RuntimeNavigationTarget } from '../../../../shared/runtime-navigation' +import type { RuntimeMobileSessionCreateTerminalResult } from '../../../../shared/runtime-types' +import { navigationTargetsHost } from '../../../../shared/runtime-navigation' +import { MOBILE_TERMINAL_CREATE_RESULT_TTL_MS } from '../../orca-runtime-core' + +export abstract class OrcaRuntimeWithCreateMobileSessionTerminal extends OrcaRuntimeWithCreateTerminal { + protected abstract runCreateMobileSessionTerminal( + worktreeSelector: string, + opts?: MobileTerminalCreateOptions + ): Promise + + protected abstract getValidatedExplicitWorktreeIdSelector( + selector: string | undefined + ): string | null + + protected abstract getMobileSessionTabsForWorktree( + worktreeId: string, + clientNavigationId?: string + ): RuntimeMobileSessionTabsResult -export class OrcaRuntimeWithCreateMobileSessionTerminal extends OrcaRuntimeWithCreateTerminal { async createMobileSessionTerminal( worktreeSelector: string, opts: { diff --git a/src/main/runtime/orca-runtime-create-runtime-owned-mobile-session-terminal.ts b/src/main/runtime/terminal/launch/mobile-terminal-owned-create.ts similarity index 86% rename from src/main/runtime/orca-runtime-create-runtime-owned-mobile-session-terminal.ts rename to src/main/runtime/terminal/launch/mobile-terminal-owned-create.ts index 24cad7a936b..fe3173cd189 100644 --- a/src/main/runtime/orca-runtime-create-runtime-owned-mobile-session-terminal.ts +++ b/src/main/runtime/terminal/launch/mobile-terminal-owned-create.ts @@ -1,28 +1,45 @@ -// @ts-nocheck -- mechanically split from OrcaRuntimeService; behavior is covered by AST equivalence and characterization tests. -import { OrcaRuntimeWithResolveMobileSessionTerminalCommand } from './orca-runtime-resolve-mobile-session-terminal-command' -import type { WorktreeStartupLaunch } from '../../shared/worktree/launch-types' -import type { TuiAgent } from '../../shared/tui-agent' -import type { SleepingAgentLaunchConfig } from '../../shared/agent-session-resume' -import type { AgentLaunchRuntime } from '../../shared/agent-launch-runtime' +import { OrcaRuntimeWithResolveMobileSessionTerminalCommand } from './mobile-terminal-startup' +import type { WorktreeStartupLaunch } from '../../../../shared/worktree/launch-types' +import type { TuiAgent } from '../../../../shared/tui-agent' +import type { SleepingAgentLaunchConfig } from '../../../../shared/agent-session-resume' +import type { AgentLaunchRuntime } from '../../../../shared/agent-launch-runtime' import type { RuntimeMobileSessionCreateTerminalResult, RuntimeMobileSessionTabsSnapshot, RuntimeMobileSessionTerminalTab -} from '../../shared/runtime-types' +} from '../../../../shared/runtime-types' import { randomUUID } from 'node:crypto' -import { copySleepingAgentLaunchConfig } from './runtime-agent-launch-resolution' -import { deriveRemoteRuntimeTerminalCreateHandle } from './remote-runtime-terminal-create-identity' -import { parsePaneKey } from '../../shared/stable-pane-id' -import { placeCreatedSessionTab } from '../../shared/session-tab-placement' +import { copySleepingAgentLaunchConfig } from '../../runtime-agent-launch-resolution' +import { deriveRemoteRuntimeTerminalCreateHandle } from '../../remote-runtime-terminal-create-identity' +import { parsePaneKey } from '../../../../shared/stable-pane-id' +import { placeCreatedSessionTab } from '../../../../shared/session-tab-placement' import { buildHeadlessMobileSessionTabGroups, buildMaterializedHeadlessParentLayout, getHeadlessMobileSessionGroupId -} from './mobile-session-layout-projection' +} from '../../mobile-session-layout-projection' const OWNED_MOBILE_DISPATCH_RECIPE_TTL_MS = 15 * 60_000 -export class OrcaRuntimeWithCreateRuntimeOwnedMobileSessionTerminal extends OrcaRuntimeWithResolveMobileSessionTerminalCommand { +export type OwnedMobileTerminalCreateOptions = { + command?: string + cwd?: string + env?: Record + envToDelete?: string[] + startupCommandDelivery?: WorktreeStartupLaunch['startupCommandDelivery'] + identity?: { tabId: string; leafId: string; sessionId?: string } + createMutation?: { clientIdentity: string; id: string } + launchAgent?: TuiAgent + viewMode?: 'terminal' | 'chat' + targetGroupId?: string + supportsSplitGroupPlacement?: boolean + launchConfig?: SleepingAgentLaunchConfig + launchRuntime?: AgentLaunchRuntime + signal?: AbortSignal + refuseSleptWorktree?: boolean +} + +export abstract class OrcaRuntimeWithCreateRuntimeOwnedMobileSessionTerminal extends OrcaRuntimeWithResolveMobileSessionTerminalCommand { private readonly ownedMobileDispatchRecipes = new Map< string, { @@ -46,23 +63,7 @@ export class OrcaRuntimeWithCreateRuntimeOwnedMobileSessionTerminal extends Orca worktreeId: string, activate: boolean, afterTabId?: string, - opts: { - command?: string - cwd?: string - env?: Record - envToDelete?: string[] - startupCommandDelivery?: WorktreeStartupLaunch['startupCommandDelivery'] - identity?: { tabId: string; leafId: string; sessionId?: string } - createMutation?: { clientIdentity: string; id: string } - launchAgent?: TuiAgent - viewMode?: 'terminal' | 'chat' - targetGroupId?: string - supportsSplitGroupPlacement?: boolean - launchConfig?: SleepingAgentLaunchConfig - launchRuntime?: AgentLaunchRuntime - signal?: AbortSignal - refuseSleptWorktree?: boolean - } = {} + opts: OwnedMobileTerminalCreateOptions = {} ): Promise { this.pruneOwnedMobileDispatchRecipes() const workspace = await this.resolveTerminalWorkspaceLaunchScope(`id:${worktreeId}`) diff --git a/src/main/runtime/orca-runtime-run-create-mobile-session-terminal.ts b/src/main/runtime/terminal/launch/mobile-terminal-renderer-create.ts similarity index 76% rename from src/main/runtime/orca-runtime-run-create-mobile-session-terminal.ts rename to src/main/runtime/terminal/launch/mobile-terminal-renderer-create.ts index 0813497d2de..99b1d9ebebe 100644 --- a/src/main/runtime/orca-runtime-run-create-mobile-session-terminal.ts +++ b/src/main/runtime/terminal/launch/mobile-terminal-renderer-create.ts @@ -1,42 +1,96 @@ -// @ts-nocheck -- mechanically split from OrcaRuntimeService; behavior is covered by AST equivalence and characterization tests. -import { OrcaRuntimeWithCreateMobileSessionTerminal } from './orca-runtime-create-mobile-session-terminal' -import type { WorktreeStartupLaunch } from '../../shared/worktree/launch-types' -import type { TuiAgent } from '../../shared/tui-agent' -import type { SleepingAgentLaunchConfig } from '../../shared/agent-session-resume' -import type { RuntimeMobileSessionCreateTerminalResult } from '../../shared/runtime-types' +import type { MobileTerminalCommandOptions, MobileTerminalCommand } from './mobile-terminal-startup' +import type { OwnedMobileTerminalCreateOptions } from './mobile-terminal-owned-create' +import type { RuntimePtyWorktreeRecord } from '../../runtime-terminal-state-records' +import type { TerminalWorkspaceLaunchScope } from '../../runtime-legacy-worker-terminal-recovery-types' +import { OrcaRuntimeWithCreateMobileSessionTerminal } from './mobile-terminal-create' +import type { WorktreeStartupLaunch } from '../../../../shared/worktree/launch-types' +import type { TuiAgent } from '../../../../shared/tui-agent' +import type { SleepingAgentLaunchConfig } from '../../../../shared/agent-session-resume' +import type { RuntimeMobileSessionCreateTerminalResult } from '../../../../shared/runtime-types' import { randomUUID } from 'node:crypto' -import { getRuntimeDesktopSurface } from './runtime-desktop-surface' +import { getRuntimeDesktopSurface } from '../../runtime-desktop-surface' import type { IpcMainEvent } from 'electron' import { MOBILE_TERMINAL_READY_FALLBACK_MS, MOBILE_TERMINAL_SURFACE_TIMEOUT_MS, isClientDisconnectedError -} from './orca-runtime-core' -import { rendererPublicationThrottle } from '../window/renderer-publication-throttle' -import { deterministicAgentSessionUuid } from './runtime-agent-launch-resolution' +} from '../../orca-runtime-core' +import { rendererPublicationThrottle } from '../../../window/renderer-publication-throttle' +import { deterministicAgentSessionUuid } from '../../runtime-agent-launch-resolution' + +export type MobileTerminalCreateOptions = { + afterTabId?: string + targetGroupId?: string + command?: string + cwd?: string + env?: Record + envToDelete?: string[] + startupCommandDelivery?: WorktreeStartupLaunch['startupCommandDelivery'] + agent?: TuiAgent + agentPrompt?: string + launchConfig?: SleepingAgentLaunchConfig + launchAgent?: TuiAgent + viewMode?: 'terminal' | 'chat' + activate?: boolean + clientNavigationId?: string + clientMutationId?: string + supportsSplitGroupPlacement?: boolean + signal?: AbortSignal +} + +export abstract class OrcaRuntimeWithRunCreateMobileSessionTerminal extends OrcaRuntimeWithCreateMobileSessionTerminal { + protected abstract captureReadyGraphEpoch(): number + + protected abstract resolveMobileSessionTerminalCommand( + workspace: TerminalWorkspaceLaunchScope, + opts: MobileTerminalCommandOptions + ): Promise + + protected abstract assertStableReadyGraph(expectedGraphEpoch: number): void + + protected abstract createRuntimeOwnedMobileSessionTerminal( + worktreeId: string, + activate: boolean, + afterTabId?: string, + opts?: OwnedMobileTerminalCreateOptions + ): Promise + + protected abstract ensurePtyBackedMobileSurfaceForRendererTab( + worktreeId: string, + tabId: string + ): RuntimeMobileSessionCreateTerminalResult | null + + protected abstract waitForMobileTerminalSurface( + worktreeId: string, + parentTabId: string, + options?: { timeoutMs?: number; requireReady?: boolean; signal?: AbortSignal } + ): Promise + + protected abstract isReadyMobileTerminalSurface( + surface: RuntimeMobileSessionCreateTerminalResult | null + ): boolean + + protected abstract deliverPendingStartupCommandToBareRendererPty( + worktreeId: string, + tabId: string + ): void + + protected abstract findMobileTerminalSurface( + worktreeId: string, + parentTabId: string, + options?: { requireReady?: boolean } + ): RuntimeMobileSessionCreateTerminalResult | null + + protected abstract findLiveRegisteredPtyForRendererTab( + worktreeId: string, + tabId: string + ): RuntimePtyWorktreeRecord | null + + protected abstract hasLiveShellForRendererTab(worktreeId: string, tabId: string): boolean -export class OrcaRuntimeWithRunCreateMobileSessionTerminal extends OrcaRuntimeWithCreateMobileSessionTerminal { protected async runCreateMobileSessionTerminal( worktreeSelector: string, - opts: { - afterTabId?: string - targetGroupId?: string - command?: string - cwd?: string - env?: Record - envToDelete?: string[] - startupCommandDelivery?: WorktreeStartupLaunch['startupCommandDelivery'] - agent?: TuiAgent - agentPrompt?: string - launchConfig?: SleepingAgentLaunchConfig - launchAgent?: TuiAgent - viewMode?: 'terminal' | 'chat' - activate?: boolean - clientNavigationId?: string - clientMutationId?: string - supportsSplitGroupPlacement?: boolean - signal?: AbortSignal - } = {} + opts: MobileTerminalCreateOptions = {} ): Promise { const pairedCreate = Boolean(opts.clientNavigationId) const graphEpoch = this.captureReadyGraphEpoch() diff --git a/src/main/runtime/orca-runtime-resolve-mobile-session-terminal-command.ts b/src/main/runtime/terminal/launch/mobile-terminal-startup.ts similarity index 60% rename from src/main/runtime/orca-runtime-resolve-mobile-session-terminal-command.ts rename to src/main/runtime/terminal/launch/mobile-terminal-startup.ts index 6803b4422d5..487a6d3a1ea 100644 --- a/src/main/runtime/orca-runtime-resolve-mobile-session-terminal-command.ts +++ b/src/main/runtime/terminal/launch/mobile-terminal-startup.ts @@ -1,36 +1,43 @@ -import { OrcaRuntimeWithRunCreateMobileSessionTerminal } from './orca-runtime-run-create-mobile-session-terminal' -import type { TerminalWorkspaceLaunchScope } from './runtime-legacy-worker-terminal-recovery-types' -import type { WorktreeStartupLaunch } from '../../shared/worktree/launch-types' -import type { TuiAgent } from '../../shared/tui-agent' -import type { SleepingAgentLaunchConfig } from '../../shared/agent-session-resume' -import { isTuiAgentEnabled } from '../../shared/tui-agent-selection' -import { buildAgentStartupPlan } from '../../shared/tui-agent-startup' -import { resolveAgentStartupPlanInputs } from '../../shared/agent-startup-plan-inputs' -import { agentLaunchQuotingShell, type AgentLaunchRuntime } from '../../shared/agent-launch-runtime' +import { OrcaRuntimeWithRunCreateMobileSessionTerminal } from './mobile-terminal-renderer-create' +import type { TerminalWorkspaceLaunchScope } from '../../runtime-legacy-worker-terminal-recovery-types' +import type { WorktreeStartupLaunch } from '../../../../shared/worktree/launch-types' +import type { TuiAgent } from '../../../../shared/tui-agent' +import type { SleepingAgentLaunchConfig } from '../../../../shared/agent-session-resume' +import { isTuiAgentEnabled } from '../../../../shared/tui-agent-selection' +import { buildAgentStartupPlan } from '../../../../shared/tui-agent-startup' +import { resolveAgentStartupPlanInputs } from '../../../../shared/agent-startup-plan-inputs' +import { + agentLaunchQuotingShell, + type AgentLaunchRuntime +} from '../../../../shared/agent-launch-runtime' + +export type MobileTerminalCommandOptions = { + command?: string + env?: Record + envToDelete?: string[] + startupCommandDelivery?: WorktreeStartupLaunch['startupCommandDelivery'] + agent?: TuiAgent + agentPrompt?: string + launchConfig?: SleepingAgentLaunchConfig + launchAgent?: TuiAgent +} + +export type MobileTerminalCommand = { + command?: string + env?: Record + envToDelete?: string[] + startupCommandDelivery?: WorktreeStartupLaunch['startupCommandDelivery'] + launchConfig?: SleepingAgentLaunchConfig + launchAgent?: TuiAgent + /** What the command was quoted for; the create spawns with it. */ + launchRuntime?: AgentLaunchRuntime +} export abstract class OrcaRuntimeWithResolveMobileSessionTerminalCommand extends OrcaRuntimeWithRunCreateMobileSessionTerminal { protected async resolveMobileSessionTerminalCommand( workspace: TerminalWorkspaceLaunchScope, - opts: { - command?: string - env?: Record - envToDelete?: string[] - startupCommandDelivery?: WorktreeStartupLaunch['startupCommandDelivery'] - agent?: TuiAgent - agentPrompt?: string - launchConfig?: SleepingAgentLaunchConfig - launchAgent?: TuiAgent - } - ): Promise<{ - command?: string - env?: Record - envToDelete?: string[] - startupCommandDelivery?: WorktreeStartupLaunch['startupCommandDelivery'] - launchConfig?: SleepingAgentLaunchConfig - launchAgent?: TuiAgent - /** What the command was quoted for; the create spawns with it. */ - launchRuntime?: AgentLaunchRuntime - }> { + opts: MobileTerminalCommandOptions + ): Promise { if (opts.command || !opts.agent) { return { command: opts.command, diff --git a/src/main/runtime/orca-runtime-terminal-create-deduplication.ts b/src/main/runtime/terminal/launch/terminal-create-deduplication.ts similarity index 87% rename from src/main/runtime/orca-runtime-terminal-create-deduplication.ts rename to src/main/runtime/terminal/launch/terminal-create-deduplication.ts index 7d756c188d9..bdaacf24c80 100644 --- a/src/main/runtime/orca-runtime-terminal-create-deduplication.ts +++ b/src/main/runtime/terminal/launch/terminal-create-deduplication.ts @@ -1,21 +1,20 @@ -// @ts-nocheck -- mechanically split from OrcaRuntimeService; behavior is covered by AST equivalence and characterization tests. -import { OrcaRuntimeWithCreateAgentSession } from './orca-runtime-create-agent-session' -import type { RuntimeTerminalCreate } from '../../shared/runtime-types' -import { WORKTREE_CREATE_RESULT_TTL_MS } from './orca-runtime-core' -import { deriveRemoteRuntimeTerminalCreateHandle } from './remote-runtime-terminal-create-identity' -import { withTimeoutResult } from './runtime-async-boundaries' -import { PTY_CONTROLLER_LIST_TIMEOUT_MS } from './orca-runtime-postlude' -import { inferWorktreeIdFromPtyId } from './runtime-worktree-path-identity' -import { getRegisteredSshState } from '../ssh/ssh-target-registry' +import { OrcaRuntimeWithCreateAgentSession } from '../../orca-runtime-create-agent-session' +import type { RuntimeTerminalCreate } from '../../../../shared/runtime-types' +import { WORKTREE_CREATE_RESULT_TTL_MS } from '../../orca-runtime-core' +import { deriveRemoteRuntimeTerminalCreateHandle } from '../../remote-runtime-terminal-create-identity' +import { withTimeoutResult } from '../../runtime-async-boundaries' +import { PTY_CONTROLLER_LIST_TIMEOUT_MS } from '../../orca-runtime-postlude' +import { inferWorktreeIdFromPtyId } from '../../runtime-worktree-path-identity' +import { getRegisteredSshState } from '../../../ssh/ssh-target-registry' import { getConnectionExecutionHostId, LOCAL_EXECUTION_HOST_ID, toSshExecutionHostId -} from '../../shared/execution-host' -import { resolveWorktreeLaunchHost } from './worktree-launch-host-repo' -import type { TuiAgent } from '../../shared/tui-agent' +} from '../../../../shared/execution-host' +import { resolveWorktreeLaunchHost } from '../../worktree-launch-host-repo' +import type { TuiAgent } from '../../../../shared/tui-agent' -export class OrcaRuntimeWithTerminalCreateDeduplication extends OrcaRuntimeWithCreateAgentSession { +export abstract class OrcaRuntimeWithTerminalCreateDeduplication extends OrcaRuntimeWithCreateAgentSession { async dedupeTerminalCreate( clientIdentity: string, worktreeSelector: string | undefined, @@ -126,7 +125,7 @@ export class OrcaRuntimeWithTerminalCreateDeduplication extends OrcaRuntimeWithC } } - protected getPtyExecutionHostMetadata( + getPtyExecutionHostMetadata( ptyId: string | null ): Pick { if (!ptyId) { @@ -207,6 +206,7 @@ export class OrcaRuntimeWithTerminalCreateDeduplication extends OrcaRuntimeWithC const key = `${repoSelector}\0${clientMutationId}` const inflight = this.worktreeCreateByMutationId.get(key) if (inflight) { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Callers must use one operation/result type per repo and mutation key. return inflight as Promise } const created = run() diff --git a/src/main/runtime/orca-runtime-create-terminal.ts b/src/main/runtime/terminal/launch/terminal-create.ts similarity index 93% rename from src/main/runtime/orca-runtime-create-terminal.ts rename to src/main/runtime/terminal/launch/terminal-create.ts index 434762e4149..87029d46d6c 100644 --- a/src/main/runtime/orca-runtime-create-terminal.ts +++ b/src/main/runtime/terminal/launch/terminal-create.ts @@ -1,14 +1,13 @@ -// @ts-nocheck -- mechanically split from OrcaRuntimeService; behavior is covered by AST equivalence and characterization tests. -import { OrcaRuntimeWithTerminalCreateDeduplication } from './orca-runtime-terminal-create-deduplication' -import * as dependencies from './orca-runtime-create-terminal-dependencies' -import { createDesktopTerminal } from './orca-runtime-create-terminal-desktop' -import { buildRuntimeAgentTeamsLaunchPlan } from './terminal/launch/agent-teams-launch-plan' -import { createIdempotentCallback } from './orca-runtime-report-pty-spawn-commit' -import { recordPtySurface, spawnSurfaceClaimSequence } from './pty-recorded-surface-topology' -import { agentLaunchTokenFor } from '../agent-launch/agent-launch-token' -import { migratedPaneRestoreHint } from './migrated-pane-restore-cwd' +import { OrcaRuntimeWithTerminalCreateDeduplication } from './terminal-create-deduplication' +import * as dependencies from '../../orca-runtime-create-terminal-dependencies' +import { createDesktopTerminal } from '../../orca-runtime-create-terminal-desktop' +import { buildRuntimeAgentTeamsLaunchPlan } from './agent-teams-launch-plan' +import { createIdempotentCallback } from '../../orca-runtime-report-pty-spawn-commit' +import { recordPtySurface, spawnSurfaceClaimSequence } from '../../pty-recorded-surface-topology' +import { agentLaunchTokenFor } from '../../../agent-launch/agent-launch-token' +import { migratedPaneRestoreHint } from '../../migrated-pane-restore-cwd' -export class OrcaRuntimeWithCreateTerminal extends OrcaRuntimeWithTerminalCreateDeduplication { +export abstract class OrcaRuntimeWithCreateTerminal extends OrcaRuntimeWithTerminalCreateDeduplication { async createTerminal( worktreeSelector?: string, opts: dependencies.TerminalCreateOptions = {}, @@ -85,7 +84,7 @@ export class OrcaRuntimeWithCreateTerminal extends OrcaRuntimeWithTerminalCreate ...launchOpts.env, ...(launchToken ? { ORCA_AGENT_LAUNCH_TOKEN: launchToken } : {}) } - let agentTeamsPlan: Awaited> + let agentTeamsPlan: Awaited>['plan'] let sequencedStartupCommand: string | undefined let effectiveLaunchConfig = launchOpts.launchConfig try { diff --git a/src/main/runtime/orca-runtime-prepare-pty-execution-context.ts b/src/main/runtime/terminal/launch/terminal-execution-context.ts similarity index 86% rename from src/main/runtime/orca-runtime-prepare-pty-execution-context.ts rename to src/main/runtime/terminal/launch/terminal-execution-context.ts index b30742cf1b0..e93e9427bc7 100644 --- a/src/main/runtime/orca-runtime-prepare-pty-execution-context.ts +++ b/src/main/runtime/terminal/launch/terminal-execution-context.ts @@ -1,7 +1,10 @@ -// @ts-nocheck -- mechanically split from OrcaRuntimeService; behavior is covered by AST equivalence and characterization tests. -import { OrcaRuntimeWithRegisterPty } from './orca-runtime-register-pty' +import { OrcaRuntimeWithRegisterPty } from '../../orca-runtime-register-pty' + +export abstract class OrcaRuntimeWithPreparePtyExecutionContext extends OrcaRuntimeWithRegisterPty { + protected abstract disposeHeadlessTerminal(ptyId: string): void + + protected abstract replaceHeadlessTerminalAfterExecutionContextChange(ptyId: string): void -export class OrcaRuntimeWithPreparePtyExecutionContext extends OrcaRuntimeWithRegisterPty { preparePtyExecutionContext( ptyId: string, wslDistro: string | null, diff --git a/src/main/runtime/orca-runtime-split-pty-backed-terminal.ts b/src/main/runtime/terminal/launch/terminal-split-surface.ts similarity index 79% rename from src/main/runtime/orca-runtime-split-pty-backed-terminal.ts rename to src/main/runtime/terminal/launch/terminal-split-surface.ts index f23987ca2ea..9c819f1ea70 100644 --- a/src/main/runtime/orca-runtime-split-pty-backed-terminal.ts +++ b/src/main/runtime/terminal/launch/terminal-split-surface.ts @@ -1,30 +1,40 @@ -// @ts-nocheck -- mechanically split from OrcaRuntimeService; behavior is covered by AST equivalence and characterization tests. -import { OrcaRuntimeWithSplitTerminal } from './orca-runtime-split-terminal' -import type { RuntimePtyWorktreeRecord } from './runtime-terminal-state-records' -import type { TerminalPaneSplitSource } from '../../shared/feature-education-telemetry' -import type { RuntimeTerminalSplit } from '../../shared/runtime-types' -import { makePaneKey, parsePaneKey } from '../../shared/stable-pane-id' -import { recordPtySurface, spawnSurfaceClaimSequence } from './pty-recorded-surface-topology' +import { OrcaRuntimeWithSplitTerminal } from './terminal-split' +import type { OrcaRuntimeWithResolveTerminalSplitSourceAuthority } from '../../orca-runtime-resolve-terminal-split-source-authority' +import type { RuntimePtyWorktreeRecord } from '../../runtime-terminal-state-records' +import type { TerminalPaneSplitSource } from '../../../../shared/feature-education-telemetry' +import type { RuntimeTerminalSplit } from '../../../../shared/runtime-types' +import { makePaneKey, parsePaneKey } from '../../../../shared/stable-pane-id' +import { recordPtySurface, spawnSurfaceClaimSequence } from '../../pty-recorded-surface-topology' import { randomUUID } from 'node:crypto' -import { runtimeSplitPlacement } from './runtime-terminal-spawn-placement' -import { REJECTED_SPLIT_PTY_STOP_TIMEOUT_MS, ownerSurfacing } from './orca-runtime-core' -import type { Worktree } from '../../shared/worktree/types' -import { spawnInAdmittedPane, withdrawRuntimePane } from './runtime-pane-admission' +import { runtimeSplitPlacement } from '../../runtime-terminal-spawn-placement' +import { REJECTED_SPLIT_PTY_STOP_TIMEOUT_MS, ownerSurfacing } from '../../orca-runtime-core' +import type { Worktree } from '../../../../shared/worktree/types' +import { spawnInAdmittedPane, withdrawRuntimePane } from '../../runtime-pane-admission' + +export type TerminalSplitOptions = { + direction?: 'horizontal' | 'vertical' + command?: string + env?: Record + envToDelete?: string[] + activate?: boolean + // Why: same split as createTerminal — adopt the pane without revealing its + // workspace, for splits the user never asked to see. + surfaceOwner?: false + telemetrySource?: TerminalPaneSplitSource +} + +export abstract class OrcaRuntimeWithSplitPtyBackedTerminal extends OrcaRuntimeWithSplitTerminal { + protected abstract resolveTerminalSplitSourceAuthority( + ...args: Parameters< + OrcaRuntimeWithResolveTerminalSplitSourceAuthority['resolveTerminalSplitSourceAuthority'] + > + ): ReturnType< + OrcaRuntimeWithResolveTerminalSplitSourceAuthority['resolveTerminalSplitSourceAuthority'] + > -export class OrcaRuntimeWithSplitPtyBackedTerminal extends OrcaRuntimeWithSplitTerminal { protected async splitPtyBackedTerminal( pty: RuntimePtyWorktreeRecord, - opts: { - direction?: 'horizontal' | 'vertical' - command?: string - env?: Record - envToDelete?: string[] - activate?: boolean - // Why: same split as createTerminal — adopt the pane without revealing its - // workspace, for splits the user never asked to see. - surfaceOwner?: false - telemetrySource?: TerminalPaneSplitSource - } = {}, + opts: TerminalSplitOptions = {}, createdWorktree?: Worktree ): Promise { if (!this.ptyController?.spawn) { @@ -57,7 +67,7 @@ export class OrcaRuntimeWithSplitPtyBackedTerminal extends OrcaRuntimeWithSplitT const leafId = randomUUID() const preAllocatedHandle = this.createPreAllocatedTerminalHandle() const paneKey = makePaneKey(parentTabId, leafId) - const splitArgs = { + const splitArgs: Parameters[2] = { cols: 120, rows: 40, cwd: workspace.path, diff --git a/src/main/runtime/orca-runtime-split-terminal.ts b/src/main/runtime/terminal/launch/terminal-split.ts similarity index 61% rename from src/main/runtime/orca-runtime-split-terminal.ts rename to src/main/runtime/terminal/launch/terminal-split.ts index a782c515e39..1530f52ec47 100644 --- a/src/main/runtime/orca-runtime-split-terminal.ts +++ b/src/main/runtime/terminal/launch/terminal-split.ts @@ -1,11 +1,24 @@ -// @ts-nocheck -- mechanically split from OrcaRuntimeService; behavior is covered by AST equivalence and characterization tests. -import { OrcaRuntimeWithStopExplicitlyClosedTabPtys } from './session-tabs/explicit-terminal-close-stops' -import type { TerminalPaneSplitSource } from '../../shared/feature-education-telemetry' -import type { RuntimeTerminalSplit } from '../../shared/runtime-types' +import type { TerminalSplitOptions } from './terminal-split-surface' +import type { RuntimePtyWorktreeRecord } from '../../runtime-terminal-state-records' +import { OrcaRuntimeWithStopExplicitlyClosedTabPtys } from '../../session-tabs/explicit-terminal-close-stops' +import type { TerminalPaneSplitSource } from '../../../../shared/feature-education-telemetry' +import type { RuntimeTerminalSplit } from '../../../../shared/runtime-types' import { randomUUID } from 'node:crypto' -import type { Worktree } from '../../shared/worktree/types' +import type { Worktree } from '../../../../shared/worktree/types' + +export abstract class OrcaRuntimeWithSplitTerminal extends OrcaRuntimeWithStopExplicitlyClosedTabPtys { + protected abstract splitPtyBackedTerminal( + pty: RuntimePtyWorktreeRecord, + opts?: TerminalSplitOptions, + createdWorktree?: Worktree + ): Promise + + protected abstract waitForLeafInTab( + tabId: string, + leafId: string, + timeoutMs?: number + ): Promise -export class OrcaRuntimeWithSplitTerminal extends OrcaRuntimeWithStopExplicitlyClosedTabPtys { async splitTerminal( handle: string, opts: { diff --git a/src/main/runtime/orca-runtime-resolve-browser-network-execution-host-for-worktree.ts b/src/main/runtime/terminal/launch/terminal-workspace-scope.ts similarity index 75% rename from src/main/runtime/orca-runtime-resolve-browser-network-execution-host-for-worktree.ts rename to src/main/runtime/terminal/launch/terminal-workspace-scope.ts index 1073782474b..99da35e209f 100644 --- a/src/main/runtime/orca-runtime-resolve-browser-network-execution-host-for-worktree.ts +++ b/src/main/runtime/terminal/launch/terminal-workspace-scope.ts @@ -1,38 +1,37 @@ -// @ts-nocheck -- mechanically split from OrcaRuntimeService; behavior is covered by AST equivalence and characterization tests. -import { OrcaRuntimeWithTransitionGraphReloadToTerminalState } from './session-graph/session-graph-authority' -import type { ExecutionHostId } from '../../shared/execution-host' +import { OrcaRuntimeWithTransitionGraphReloadToTerminalState } from '../../session-graph/session-graph-authority' +import type { ExecutionHostId } from '../../../../shared/execution-host' import { floatingWorkspaceToWorktree, isFloatingWorkspaceSelector -} from '../../shared/floating-workspace-worktree' -import { resolveFloatingTerminalCwd } from '../ipc/floating-workspace-directory' -import type { BrowserNetworkExecutionHost } from '../../shared/browser-client-host-protocol' +} from '../../../../shared/floating-workspace-worktree' +import { resolveFloatingTerminalCwd } from '../../../ipc/floating-workspace-directory' +import type { BrowserNetworkExecutionHost } from '../../../../shared/browser-client-host-protocol' import { LOCAL_EXECUTION_HOST_ID, getWorktreeExecutionHostId, parseExecutionHostId -} from '../../shared/execution-host' -import { resolveRuntimeBrowserNetworkExecutionHost } from './runtime-browser-network-execution-host' -import { resolveLocalProjectRuntimeForWorktreeId } from '../local-project-runtime-resolution' -import { getRegisteredSshState } from '../ssh/ssh-target-registry' -import { resolveWorktreeLaunchHost } from './worktree-launch-host-repo' -import { folderWorkspaceKey, parseWorkspaceKey } from '../../shared/workspace-scope' -import type { FolderWorkspace } from '../../shared/folder-workspace-types' -import type { ResolvedWorktree } from './runtime-worktree-path-identity' -import type { Worktree } from '../../shared/worktree/types' -import { folderWorkspaceToWorktree } from '../../shared/folder-workspace-worktree' -import type { TerminalWorkspaceLaunchScope } from './runtime-legacy-worker-terminal-recovery-types' -import { resolveTerminalStartupCwd } from '../../shared/terminal-startup-cwd' -import type { ResolvedTerminalWorkspaceLaunchTarget } from './orca-runtime-core' -import { AGENT_HOOK_RUNTIME_ENV_KEYS } from './orca-runtime-core' -import { ensureJcodeRuntimeDir } from '../../shared/jcode-runtime-dir' -import { FLOATING_TERMINAL_WORKTREE_ID } from '../../shared/constants' +} from '../../../../shared/execution-host' +import { resolveRuntimeBrowserNetworkExecutionHost } from '../../runtime-browser-network-execution-host' +import { resolveLocalProjectRuntimeForWorktreeId } from '../../../local-project-runtime-resolution' +import { getRegisteredSshState } from '../../../ssh/ssh-target-registry' +import { resolveWorktreeLaunchHost } from '../../worktree-launch-host-repo' +import { folderWorkspaceKey, parseWorkspaceKey } from '../../../../shared/workspace-scope' +import type { FolderWorkspace } from '../../../../shared/folder-workspace-types' +import type { ResolvedWorktree } from '../../runtime-worktree-path-identity' +import type { Worktree } from '../../../../shared/worktree/types' +import { folderWorkspaceToWorktree } from '../../../../shared/folder-workspace-worktree' +import type { TerminalWorkspaceLaunchScope } from '../../runtime-legacy-worker-terminal-recovery-types' +import { resolveTerminalStartupCwd } from '../../../../shared/terminal-startup-cwd' +import type { ResolvedTerminalWorkspaceLaunchTarget } from '../../orca-runtime-core' +import { AGENT_HOOK_RUNTIME_ENV_KEYS } from '../../orca-runtime-core' +import { ensureJcodeRuntimeDir } from '../../../../shared/jcode-runtime-dir' +import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../../shared/constants' import { homedir } from 'node:os' -import { getExplicitWorktreeIdSelector } from './runtime-worktree-selection' -import { WORKTREE_ID_SEPARATOR } from '../../shared/worktree/id' -import { WorktreeIdRequiresFullPathError } from './runtime-worktree-lineage-resolution' -import { triggerTerminalSpawnPushTargetMaterialization } from './runtime-terminal-spawn-push-target-materialization' -import { resolveCreatedWorktreeTerminalTarget } from './runtime-created-worktree-terminal-target' +import { getExplicitWorktreeIdSelector } from '../../runtime-worktree-selection' +import { WORKTREE_ID_SEPARATOR } from '../../../../shared/worktree/id' +import { WorktreeIdRequiresFullPathError } from '../../runtime-worktree-lineage-resolution' +import { triggerTerminalSpawnPushTargetMaterialization } from '../../runtime-terminal-spawn-push-target-materialization' +import { resolveCreatedWorktreeTerminalTarget } from '../../runtime-created-worktree-terminal-target' // Why: folder and floating workspaces have no worktree row, so no parent, children or lineage. function withoutWorktreeLineage(worktree: Worktree): ResolvedWorktree { @@ -51,7 +50,7 @@ function withoutWorktreeLineage(worktree: Worktree): ResolvedWorktree { } } -export class OrcaRuntimeWithResolveBrowserNetworkExecutionHostForWorktree extends OrcaRuntimeWithTransitionGraphReloadToTerminalState { +export abstract class OrcaRuntimeWithResolveBrowserNetworkExecutionHostForWorktree extends OrcaRuntimeWithTransitionGraphReloadToTerminalState { protected resolveBrowserNetworkExecutionHostForWorktree(worktree?: { id: string repoId?: string @@ -105,14 +104,14 @@ export class OrcaRuntimeWithResolveBrowserNetworkExecutionHostForWorktree extend return withoutWorktreeLineage(folderWorkspaceToWorktree(folderWorkspace)) } - protected resolveWorkspaceTerminalStartupCwd( + resolveWorkspaceTerminalStartupCwd( workspace: Pick, requestedCwd?: string | null ): string | undefined { return resolveTerminalStartupCwd(workspace.path, requestedCwd) } - protected async resolveTerminalWorkspaceLaunchScope( + async resolveTerminalWorkspaceLaunchScope( selector: string, createdWorktree?: Worktree ): Promise { diff --git a/src/main/runtime/terminal/output/pty-output-ingestion.ts b/src/main/runtime/terminal/output/pty-output-ingestion.ts index 2248669a3e3..7753710b03b 100644 --- a/src/main/runtime/terminal/output/pty-output-ingestion.ts +++ b/src/main/runtime/terminal/output/pty-output-ingestion.ts @@ -5,7 +5,7 @@ import type { RuntimePtyTitleTrackerEntry } from '../../runtime-terminal-state-records' import type { RuntimeSyncedLeaf } from '../../../../shared/runtime-types' -import { OrcaRuntimeWithPreparePtyExecutionContext } from '../../orca-runtime-prepare-pty-execution-context' +import { OrcaRuntimeWithPreparePtyExecutionContext } from '../launch/terminal-execution-context' import { advertisedUrlWatcher } from '../../../ports/advertised-url-watcher' import { appendNormalizedToTailBuffer } from '../../terminal-tail-buffer' import { normalizeTerminalChunk } from '../../terminal-ansi-normalization' diff --git a/src/main/runtime/terminal/output/terminal-transient-facts.ts b/src/main/runtime/terminal/output/terminal-transient-facts.ts index caf6a8e5867..bb98499117a 100644 --- a/src/main/runtime/terminal/output/terminal-transient-facts.ts +++ b/src/main/runtime/terminal/output/terminal-transient-facts.ts @@ -22,7 +22,6 @@ import type { TerminalTitleFactMeta } from '../../../../shared/terminal-output-s export abstract class OrcaRuntimeWithEmitDaemonPtyTransientFact extends OrcaRuntimeWithScheduleWaitBlockedCheck { protected abstract retirePtyAgentLaunchAuthority(ptyId: string): void - protected abstract disposeHeadlessTerminal(ptyId: string): void /** A transient fact the daemon detected while it held scan authority — * emitted through the same fact channel as byte-scanned facts. Arrives diff --git a/src/main/runtime/workspaces/workspace-removal-target.ts b/src/main/runtime/workspaces/workspace-removal-target.ts index 6983f10a0ad..56df8b4055d 100644 --- a/src/main/runtime/workspaces/workspace-removal-target.ts +++ b/src/main/runtime/workspaces/workspace-removal-target.ts @@ -251,7 +251,7 @@ export abstract class OrcaRuntimeWithResolveWorktreeRemovalTarget extends OrcaRu return { handle, tabId: leaf.tabId, title } } - protected async resolveAgentTerminalCreateOptions( + async resolveAgentTerminalCreateOptions( workspace: TerminalWorkspaceLaunchScope, opts: TerminalCreateOptions ): Promise { diff --git a/src/main/runtime/workspaces/workspace-selectors.ts b/src/main/runtime/workspaces/workspace-selectors.ts index ddd3e7c00b4..0468d7115a9 100644 --- a/src/main/runtime/workspaces/workspace-selectors.ts +++ b/src/main/runtime/workspaces/workspace-selectors.ts @@ -1,4 +1,4 @@ -import { OrcaRuntimeWithResolveBrowserNetworkExecutionHostForWorktree } from '../orca-runtime-resolve-browser-network-execution-host-for-worktree' +import { OrcaRuntimeWithResolveBrowserNetworkExecutionHostForWorktree } from '../terminal/launch/terminal-workspace-scope' import type { ResolvedWorktree } from '../runtime-worktree-path-identity' import { splitWorktreeIdForFilesystem, worktreeIdComparisonKey } from '../../../shared/worktree/id' import { branchSelectorMatches, runtimePathsEqual } from '../runtime-worktree-path-identity'