From 0ec009a8779814287f489e352d041ea5877762da Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Wed, 14 Oct 2026 22:23:57 -0700 Subject: [PATCH] refactor(runtime): check workspace host, file and persistence boundaries (#27945) --- config/main-layout-writer-baseline.txt | 2 +- .../scripts/pr-e2e-route-contracts.test.mjs | 1 + config/scripts/pr-layout-oracle-route.mjs | 7 +- config/ts-nocheck-baseline.txt | 7 - .../agent-hooks/first-work-branch-rename.ts | 4 +- .../agent-hooks/first-work-folder-rename.ts | 3 +- .../agent-hooks/first-work-rename-runtime.ts | 31 +- .../claude-structured-auth-policy.ts | 12 +- src/main/claude-accounts/environment.ts | 12 +- src/main/claude-accounts/runtime-selection.ts | 10 +- src/main/codex-accounts/runtime-selection.ts | 5 +- src/main/ipc/worktree-branch-name.ts | 4 +- .../structured-agent-configured-args.test.ts | 16 + .../structured-agent-configured-args.ts | 3 +- ...issing-worktree-terminal-reconciliation.ts | 5 +- ...e-prune-mobile-session-tab-group-layout.ts | 2 +- ...untime-terminal-create-idempotency.test.ts | 16 + .../orca-runtime-workspace-ports-host.test.ts | 2 +- .../worker/local-worker-start.ts | 19 +- .../worker/worker-start-mutation.ts | 6 + ...e-created-worktree-terminal-target.test.ts | 2 +- src/main/runtime/runtime-store-contract.ts | 8 + .../session-graph/session-graph-promotion.ts | 4 +- ...session-tabs-inventory-publication.test.ts | 410 +++++++++++++ .../session-tabs/headless-tab-group-moves.ts | 2 +- .../headless-terminal-tab-close.ts | 2 +- ...mobile-session-publication-preservation.ts | 2 +- .../session-tabs-change-publication.ts} | 11 +- .../session-tabs/session-tabs-inventory.ts | 2 +- .../structured-session-create-support.ts | 2 +- .../structured-session-tab-restoration.ts | 2 +- .../runtime/structured-agent-account-home.ts | 10 +- ...red-agent-session-mobile-attention.test.ts | 13 + ...ructured-agent-session-mobile-attention.ts | 4 +- .../runtime/structured-chat-naming-runtime.ts | 6 +- .../structured-chat-worker-start.test.ts | 13 +- .../terminal-split-source-authority.ts} | 24 +- .../lifecycle/pty-handle-invalidation.ts | 2 +- .../terminal/stop/worktree-terminal-stop.ts | 2 +- .../terminal/view/mobile-terminal-surface.ts | 2 +- .../known-workspace-file-target.ts} | 59 +- .../managed-workspace-operations.ts} | 45 +- .../workspaces/workspace-activation.ts | 2 +- .../workspace-file-session-targets.ts} | 49 +- .../workspace-host-notifications.ts} | 54 +- .../workspace-process-listing-dependencies.ts | 49 ++ .../workspace-process-listing.ts} | 80 +-- .../workspace-query-prototype.test.ts | 564 +++++++++++++++++- .../commit-message-text-generation.ts | 5 +- src/shared/branch-prefix.test.ts | 4 + src/shared/branch-prefix.ts | 4 +- src/shared/source-control-ai.ts | 7 +- src/shared/tui-agent-launch-defaults.ts | 9 +- 53 files changed, 1422 insertions(+), 199 deletions(-) create mode 100644 src/main/runtime/rpc/methods/orchestration/worker/worker-start-mutation.ts rename src/main/runtime/{orca-runtime-schedule-mobile-session-tabs-changed.ts => session-tabs/session-tabs-change-publication.ts} (94%) rename src/main/runtime/{orca-runtime-resolve-terminal-split-source-authority.ts => terminal/launch/terminal-split-source-authority.ts} (88%) rename src/main/runtime/{orca-runtime-resolve-known-workspace-file-target.ts => workspaces/known-workspace-file-target.ts} (69%) rename src/main/runtime/{orca-runtime-list-managed-worktrees.ts => workspaces/managed-workspace-operations.ts} (82%) rename src/main/runtime/{orca-runtime-persist-headless-terminal-title.ts => workspaces/workspace-file-session-targets.ts} (79%) rename src/main/runtime/{orca-runtime-notify-ssh-state-changed.ts => workspaces/workspace-host-notifications.ts} (75%) create mode 100644 src/main/runtime/workspaces/workspace-process-listing-dependencies.ts rename src/main/runtime/{orca-runtime-get-worktree-ps.ts => workspaces/workspace-process-listing.ts} (78%) diff --git a/config/main-layout-writer-baseline.txt b/config/main-layout-writer-baseline.txt index 6dc7d167aaa..aa8dc523cd2 100644 --- a/config/main-layout-writer-baseline.txt +++ b/config/main-layout-writer-baseline.txt @@ -58,7 +58,7 @@ 2 src/main/runtime/orca-runtime-build-headless-mobile-session-browser-tabs.ts 1 src/main/runtime/session-tabs/structured-session-tab-close.ts 1 src/main/runtime/orca-runtime-get-runtime-id.ts -2 src/main/runtime/orca-runtime-persist-headless-terminal-title.ts +2 src/main/runtime/workspaces/workspace-file-session-targets.ts 1 src/main/runtime/session-tabs/terminal-surface-retirements.ts 1 src/main/runtime/client/runtime-client-state.ts 3 src/main/runtime/terminal/stop/worktree-terminal-stop.ts diff --git a/config/scripts/pr-e2e-route-contracts.test.mjs b/config/scripts/pr-e2e-route-contracts.test.mjs index bccecf440ae..3b4474ba4e9 100644 --- a/config/scripts/pr-e2e-route-contracts.test.mjs +++ b/config/scripts/pr-e2e-route-contracts.test.mjs @@ -144,6 +144,7 @@ describe('pr-e2e-layout-oracle-routing', () => { 'src/renderer/src/runtime/sync-runtime-graph.ts', 'src/renderer/src/runtime/web-session-tabs-sync/store-patch.ts', 'src/main/runtime/terminal/launch/terminal-split.ts', + 'src/main/runtime/workspaces/workspace-file-session-targets.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 14e7f84b712..f3180299607 100644 --- a/config/scripts/pr-layout-oracle-route.mjs +++ b/config/scripts/pr-layout-oracle-route.mjs @@ -65,11 +65,12 @@ const LAYOUT_ORACLE_PREFIXES = [ 'config/scripts/pr-layout-oracle-route.mjs' ] -const LAUNCH_LAYOUT_WRITERS = [ +const RUNTIME_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', - 'src/main/runtime/terminal/stop/worktree-terminal-stop.ts' + 'src/main/runtime/terminal/stop/worktree-terminal-stop.ts', + 'src/main/runtime/workspaces/workspace-file-session-targets.ts' ] // Runtime matches only the modules that create, move, close, persist or publish layout. @@ -88,7 +89,7 @@ export function isLayoutOracleSource(file) { return false } return ( - LAUNCH_LAYOUT_WRITERS.includes(file) || + RUNTIME_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 1e00f15573c..92d021cb80e 100644 --- a/config/ts-nocheck-baseline.txt +++ b/config/ts-nocheck-baseline.txt @@ -14,24 +14,17 @@ src/main/runtime/orca-runtime-fit-override-listeners.ts src/main/runtime/orca-runtime-focus-terminal.ts src/main/runtime/orca-runtime-get-agent-session-execution-namespace.ts src/main/runtime/orca-runtime-get-runtime-id.ts -src/main/runtime/orca-runtime-get-worktree-ps.ts src/main/runtime/orca-runtime-has-exact-persisted-terminal-surface-identity.ts src/main/runtime/orca-runtime-has-live-or-persisted-serve-or-ssh-owned-pty-binding.ts src/main/runtime/orca-runtime-linear-commands.ts -src/main/runtime/orca-runtime-list-managed-worktrees.ts -src/main/runtime/orca-runtime-notify-ssh-state-changed.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-preserved-branch-cleanup.ts src/main/runtime/orca-runtime-prune-mobile-session-tab-group-layout.ts src/main/runtime/orca-runtime-publish-pty-backed-mobile-session-terminal.ts src/main/runtime/orca-runtime-reconcile-headless-mobile-session-browser-tabs.ts src/main/runtime/orca-runtime-refuse-unattributed-mobile-session-tab-close.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-runtime-id.ts -src/main/runtime/orca-runtime-schedule-mobile-session-tabs-changed.ts src/main/runtime/orca-runtime-state-fields.ts src/main/runtime/orca-runtime-stop-requested-pty-ids.ts src/main/runtime/orca-runtime-terminal-drivers.ts diff --git a/src/main/agent-hooks/first-work-branch-rename.ts b/src/main/agent-hooks/first-work-branch-rename.ts index 8497b1e41a3..d8d4a5a02bc 100644 --- a/src/main/agent-hooks/first-work-branch-rename.ts +++ b/src/main/agent-hooks/first-work-branch-rename.ts @@ -44,7 +44,9 @@ export type FirstWorkBranchRenameEvent = { } export type FirstWorkBranchRenameDeps = { - getSettings: () => GlobalSettings + getSettings: () => Parameters[0] & + Parameters[0] & + Partial> getRepo: (repoId: string) => Repo | undefined getAgentEnvResolvers: () => CommitMessageAgentEnvironmentResolvers | undefined /** Current sidebar display name for the worktree, if one is stored. */ diff --git a/src/main/agent-hooks/first-work-folder-rename.ts b/src/main/agent-hooks/first-work-folder-rename.ts index df1a46b53ee..ae445723d73 100644 --- a/src/main/agent-hooks/first-work-folder-rename.ts +++ b/src/main/agent-hooks/first-work-folder-rename.ts @@ -5,7 +5,6 @@ // worktree identity so meta, tabs, and the live PTY session carry over. It is // best-effort and local-only — remote/Windows/locked/dest-taken all degrade to // "folder kept" without disturbing the rename that already succeeded. -import type { GlobalSettings } from '../../shared/global-settings-types' import type { Repo } from '../../shared/repo-types' import { FOLDER_WORKSPACE_INSTANCE_SEPARATOR, @@ -17,7 +16,7 @@ import { planWorktreeFolderRename } from '../ipc/worktree-folder-rename-target' export type FirstWorkFolderRenameDeps = { getRepo: (repoId: string) => Repo | undefined - getSettings: () => GlobalSettings + getSettings: () => Parameters[0]['settings'] /** Re-key all worktreeId-keyed state from the old (path-derived) id to the new. */ migrateWorktreeIdentity: (oldWorktreeId: string, newWorktreeId: string) => void /** Invalidate caches + tell the renderer the worktree's id changed (old->new) so diff --git a/src/main/agent-hooks/first-work-rename-runtime.ts b/src/main/agent-hooks/first-work-rename-runtime.ts index 2c42a623dd5..4a73ac54e45 100644 --- a/src/main/agent-hooks/first-work-rename-runtime.ts +++ b/src/main/agent-hooks/first-work-rename-runtime.ts @@ -7,12 +7,22 @@ import { renameWorktreeFolderOnFirstWork } from './first-work-folder-rename' import { moveWorktree } from '../git/worktree' import { isWslMoveFolderWorkspaceBlocked } from '../persistence/moving-wsl-projects/wsl-move-background-backoff' import type { Store } from '../persistence' +import type { RuntimeStore } from '../runtime/runtime-store-contract' import type { OrcaRuntimeService } from '../runtime/orca-runtime' const ENABLE_FIRST_WORK_FOLDER_RENAME = false export function firstWorkRenameDeps( - store: Store, + store: Pick< + RuntimeStore, + | 'getSettings' + | 'getRepo' + | 'getWorktreeMeta' + | 'setWorktreeMeta' + | 'getFolderWorkspace' + | 'updateFolderWorkspace' + > & + Partial>, runtime: Pick< OrcaRuntimeService, | 'getCommitMessageAgentEnvironmentResolvers' @@ -28,25 +38,26 @@ export function firstWorkRenameDeps( getCurrentDisplayName: (worktreeId) => { const scope = parseWorkspaceKey(worktreeId) return scope?.type === 'folder' - ? store.getFolderWorkspace(scope.folderWorkspaceId)?.name + ? store.getFolderWorkspace!(scope.folderWorkspaceId)?.name : store.getWorktreeMeta(worktreeId)?.displayName }, getFolderWorkspacePath: (worktreeId) => { const scope = parseWorkspaceKey(worktreeId) return scope?.type === 'folder' - ? store.getFolderWorkspace(scope.folderWorkspaceId)?.folderPath + ? store.getFolderWorkspace!(scope.folderWorkspaceId)?.folderPath : undefined }, isFolderWorkspaceWslMoveBlocked: (worktreeId) => { const scope = parseWorkspaceKey(worktreeId) const folder = - scope?.type === 'folder' ? store.getFolderWorkspace(scope.folderWorkspaceId) : undefined + scope?.type === 'folder' ? store.getFolderWorkspace!(scope.folderWorkspaceId) : undefined return !!folder && isWslMoveFolderWorkspaceBlocked(folder) }, isPendingFirstAgentMessageRename: (worktreeId) => { const scope = parseWorkspaceKey(worktreeId) return scope?.type === 'folder' - ? store.getFolderWorkspace(scope.folderWorkspaceId)?.pendingFirstAgentMessageRename === true + ? store.getFolderWorkspace!(scope.folderWorkspaceId)?.pendingFirstAgentMessageRename === + true : store.getWorktreeMeta(worktreeId)?.pendingFirstAgentMessageRename === true }, canRenameOrcaCreatedBranch: (worktreeId) => { @@ -58,7 +69,7 @@ export function firstWorkRenameDeps( rememberBranchRenameFailureOutput(worktreeId, null) const scope = parseWorkspaceKey(worktreeId) if (scope?.type === 'folder') { - store.updateFolderWorkspace(scope.folderWorkspaceId, { + store.updateFolderWorkspace!(scope.folderWorkspaceId, { name: displayName, pendingFirstAgentMessageRename: false, firstAgentMessageRenameError: null @@ -81,7 +92,7 @@ export function firstWorkRenameDeps( renameWorktreeFolderOnFirstWork(worktreeId, newLeaf, { getRepo: (repoId) => store.getRepo(repoId), getSettings: () => store.getSettings(), - migrateWorktreeIdentity: (oldId, newId) => store.migrateWorktreeIdentity(oldId, newId), + migrateWorktreeIdentity: (oldId, newId) => store.migrateWorktreeIdentity!(oldId, newId), notifyWorktreeRenamed: (repoId, oldId, newId) => runtime.notifyWorktreeFolderRenamed(repoId, oldId, newId), pathExists: async (candidate) => existsSync(candidate), @@ -94,13 +105,13 @@ export function firstWorkRenameDeps( // Skip the write + push when unchanged — most settled worktrees never had an error to clear. const scope = parseWorkspaceKey(worktreeId) if (scope?.type === 'folder') { - const current = store.getFolderWorkspace( + const current = store.getFolderWorkspace!( scope.folderWorkspaceId )?.firstAgentMessageRenameError if ((current ?? null) === (error ?? null)) { return } - store.updateFolderWorkspace(scope.folderWorkspaceId, { + store.updateFolderWorkspace!(scope.folderWorkspaceId, { firstAgentMessageRenameError: error }) runtime.notifyFolderWorkspaceChanged() @@ -114,7 +125,7 @@ export function firstWorkRenameDeps( // Why: the hook only knows the worktreeId, so derive the repoId notifyBranchRenamed expects. runtime.notifyBranchRenamed(getRepoIdFromWorktreeId(worktreeId)) }, - resolveWorktreeIdForTab: (tabId) => store.getWorktreeIdForTab(tabId), + resolveWorktreeIdForTab: (tabId) => store.getWorktreeIdForTab!(tabId), onRenamed: (repoIdOrWorktreeId) => { if (parseWorkspaceKey(repoIdOrWorktreeId)?.type === 'folder') { runtime.notifyFolderWorkspaceChanged() diff --git a/src/main/claude-accounts/claude-structured-auth-policy.ts b/src/main/claude-accounts/claude-structured-auth-policy.ts index 38ed700bc1f..8a8de959255 100644 --- a/src/main/claude-accounts/claude-structured-auth-policy.ts +++ b/src/main/claude-accounts/claude-structured-auth-policy.ts @@ -23,11 +23,13 @@ export type ClaudeStructuredAuthPolicy = { * selection, not the platform default target, owns its auth. */ export function claudeStructuredAuthPolicyForSettings( - settings: Pick< - GlobalSettings, - | 'claudeManagedAccounts' - | 'activeClaudeManagedAccountId' - | 'activeClaudeManagedAccountIdsByRuntime' + settings: Partial< + Pick< + GlobalSettings, + | 'claudeManagedAccounts' + | 'activeClaudeManagedAccountId' + | 'activeClaudeManagedAccountIdsByRuntime' + > > ): ClaudeStructuredAuthPolicy { // Why the router first: it decides whether the account or System default runs, as for terminals. diff --git a/src/main/claude-accounts/environment.ts b/src/main/claude-accounts/environment.ts index 11059a0f7a3..b8d5da39077 100644 --- a/src/main/claude-accounts/environment.ts +++ b/src/main/claude-accounts/environment.ts @@ -24,7 +24,17 @@ export function applyClaudeEnvPatch( baseEnv: Record, patch: ClaudeEnvPatch, options?: { stripAuthEnv?: boolean; platform?: NodeJS.Platform } -): Record { +): Record +export function applyClaudeEnvPatch( + baseEnv: Record, + patch: ClaudeEnvPatch, + options?: { stripAuthEnv?: boolean; platform?: NodeJS.Platform } +): Record +export function applyClaudeEnvPatch( + baseEnv: Record, + patch: ClaudeEnvPatch, + options?: { stripAuthEnv?: boolean; platform?: NodeJS.Platform } +): Record { if (options?.stripAuthEnv) { for (const key of CLAUDE_AUTH_ENV_VARS) { delete baseEnv[key] diff --git a/src/main/claude-accounts/runtime-selection.ts b/src/main/claude-accounts/runtime-selection.ts index fe2d1373ac4..1fc5137bb17 100644 --- a/src/main/claude-accounts/runtime-selection.ts +++ b/src/main/claude-accounts/runtime-selection.ts @@ -27,9 +27,8 @@ export function normalizeClaudeAccountSelectionTarget( } export function normalizeClaudeRuntimeSelection( - settings: Pick< - GlobalSettings, - 'activeClaudeManagedAccountId' | 'activeClaudeManagedAccountIdsByRuntime' + settings: Partial< + Pick > ): ClaudeManagedAccountRuntimeSelection { return { @@ -42,9 +41,8 @@ export function normalizeClaudeRuntimeSelection( } export function getSelectedClaudeAccountIdForTarget( - settings: Pick< - GlobalSettings, - 'activeClaudeManagedAccountId' | 'activeClaudeManagedAccountIdsByRuntime' + settings: Partial< + Pick >, target?: ClaudeAccountSelectionTarget | null ): string | null { diff --git a/src/main/codex-accounts/runtime-selection.ts b/src/main/codex-accounts/runtime-selection.ts index c60dbdb4c9d..0aa6671bae2 100644 --- a/src/main/codex-accounts/runtime-selection.ts +++ b/src/main/codex-accounts/runtime-selection.ts @@ -22,9 +22,8 @@ export type { } from '../../shared/codex-selection-lane' export function normalizeCodexRuntimeSelection( - settings: Pick< - GlobalSettings, - 'activeCodexManagedAccountId' | 'activeCodexManagedAccountIdsByRuntime' + settings: Partial< + Pick > ): CodexManagedAccountRuntimeSelection { return { diff --git a/src/main/ipc/worktree-branch-name.ts b/src/main/ipc/worktree-branch-name.ts index 4690e216364..5fdabf215b3 100644 --- a/src/main/ipc/worktree-branch-name.ts +++ b/src/main/ipc/worktree-branch-name.ts @@ -17,7 +17,7 @@ import { * check-ref-format rejects. */ export function getConfiguredBranchPrefix( - settings: BranchPrefixSettings, + settings: Parameters[0], gitUsername: string | null ): string | null { const raw = selectBranchPrefixInput(settings, gitUsername) @@ -29,7 +29,7 @@ export function getConfiguredBranchPrefix( */ export function computeBranchName( sanitizedName: string, - settings: BranchPrefixSettings, + settings: Parameters[0], gitUsername: string | null ): string { const prefix = getConfiguredBranchPrefix(settings, gitUsername) diff --git a/src/main/native-chat/structured-agent-configured-args.test.ts b/src/main/native-chat/structured-agent-configured-args.test.ts index 4daf3556527..a61f004dc27 100644 --- a/src/main/native-chat/structured-agent-configured-args.test.ts +++ b/src/main/native-chat/structured-agent-configured-args.test.ts @@ -45,3 +45,19 @@ describe('structured chat configured Arguments', () => { ).toThrow('Arguments are invalid') }) }) + +describe('future structured provider argument lookup', () => { + it('keeps configured arguments and empty fallback for an unregistered provider', () => { + const agentDefaultArgs = { claude: '', 'future-provider': '--label "future value"' } + expect(structuredAgentConfiguredArgs('future-provider', { agentDefaultArgs })).toEqual([ + '--label', + 'future value' + ]) + expect(structuredAgentConfiguredArgs('future-provider', {})).toEqual([]) + }) + + it('retains the existing inherited default-map lookup behavior', () => { + expect(structuredAgentConfiguredArgs('constructor', {})).toEqual(['undefined']) + expect(structuredAgentConfiguredArgs('toString', {})).toEqual([]) + }) +}) diff --git a/src/main/native-chat/structured-agent-configured-args.ts b/src/main/native-chat/structured-agent-configured-args.ts index 49aac2e398b..aa9cec790b7 100644 --- a/src/main/native-chat/structured-agent-configured-args.ts +++ b/src/main/native-chat/structured-agent-configured-args.ts @@ -1,4 +1,3 @@ -import type { TuiAgent } from '../../shared/tui-agent' import type { GlobalSettings } from '../../shared/global-settings-types' import { resolveTuiAgentLaunchArgs } from '../../shared/tui-agent-launch-defaults' import { tokenizeStartupCommand } from '../../shared/tui-agent-startup-shell' @@ -6,7 +5,7 @@ import { resolveLocalWindowsAgentStartupShell } from '../../shared/windows-termi /** Use the same grouping rules as terminal launches before the provider filters its owned flags. */ export function structuredAgentConfiguredArgs( - agent: TuiAgent, + agent: string, settings: Partial>, platform: NodeJS.Platform = process.platform ): string[] { diff --git a/src/main/runtime/missing-worktree-terminal-reconciliation.ts b/src/main/runtime/missing-worktree-terminal-reconciliation.ts index e28a6e1bed1..cd3afba2df7 100644 --- a/src/main/runtime/missing-worktree-terminal-reconciliation.ts +++ b/src/main/runtime/missing-worktree-terminal-reconciliation.ts @@ -2,8 +2,7 @@ import type { IPtyProvider } from '../providers/pty-provider-contract' import type { Repo } from '../../shared/repo-types' import { splitWorktreeId } from '../../shared/worktree/id' import { mapWithConcurrency } from '../../shared/map-with-concurrency' -import type { OrcaRuntimeService } from './orca-runtime' -import { killAllProcessesForWorktree } from './worktree-teardown' +import { killAllProcessesForWorktree, type WorktreeTeardownRuntime } from './worktree-teardown' const MISSING_WORKTREE_TEARDOWN_CONCURRENCY = 4 @@ -60,7 +59,7 @@ function hostFence( } type MissingWorktreeTerminalReconciliationDeps = { - runtime: OrcaRuntimeService + runtime: WorktreeTeardownRuntime getLocalProvider: () => IPtyProvider | null onPtyStopped?: (ptyId: string) => void } diff --git a/src/main/runtime/orca-runtime-prune-mobile-session-tab-group-layout.ts b/src/main/runtime/orca-runtime-prune-mobile-session-tab-group-layout.ts index 38eca5e6efa..0b370d09f6a 100644 --- a/src/main/runtime/orca-runtime-prune-mobile-session-tab-group-layout.ts +++ b/src/main/runtime/orca-runtime-prune-mobile-session-tab-group-layout.ts @@ -1,6 +1,6 @@ // @ts-nocheck -- mechanically split from OrcaRuntimeService; behavior is covered by AST equivalence and characterization tests. import { selectFreshAgentRowForMobileTab } from './runtime-hook-agent-row-selection' -import { OrcaRuntimeWithScheduleMobileSessionTabsChanged } from './orca-runtime-schedule-mobile-session-tabs-changed' +import { OrcaRuntimeWithScheduleMobileSessionTabsChanged } from './session-tabs/session-tabs-change-publication' import type { TabGroupLayoutNode } from '../../shared/tab-types' import type { RuntimeMobileSessionSnapshotTab, 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 1537312d786..4d6634d61ef 100644 --- a/src/main/runtime/orca-runtime-terminal-create-idempotency.test.ts +++ b/src/main/runtime/orca-runtime-terminal-create-idempotency.test.ts @@ -781,3 +781,19 @@ describe('launch runtime dispatch and allocation phases', () => { } }) }) + +describe('workspace-create-idempotency characterization', () => { + describe('worktree mutation promise identity', () => { + it('retains the original promise for a reused key even when callers violate its result-type precondition', async () => { + const runtime = new OrcaRuntimeService(null) + const created = Promise.resolve(42) + const later = vi.fn(() => Promise.resolve('different result')) + const first = runtime.dedupeWorktreeCreate('repo', 'agent.launch:mutation', () => created) + const reused = runtime.dedupeWorktreeCreate('repo', 'agent.launch:mutation', later) + expect(first).toBe(created) + expect(reused).toBe(first) + expect(later).not.toHaveBeenCalled() + await expect(reused).resolves.toBe(42) + }) + }) +}) diff --git a/src/main/runtime/orca-runtime-workspace-ports-host.test.ts b/src/main/runtime/orca-runtime-workspace-ports-host.test.ts index 67ff11c0b92..46378708d0e 100644 --- a/src/main/runtime/orca-runtime-workspace-ports-host.test.ts +++ b/src/main/runtime/orca-runtime-workspace-ports-host.test.ts @@ -1,6 +1,6 @@ import './rpc/unused-default-rpc-methods.test-fixture' import { afterEach, describe, expect, it, vi } from 'vitest' -import { OrcaRuntimeWithListManagedWorktrees } from './orca-runtime-list-managed-worktrees' +import { OrcaRuntimeWithListManagedWorktrees } from './workspaces/managed-workspace-operations' const scanWorkspacePortsMock = vi.hoisted(() => vi.fn()) diff --git a/src/main/runtime/rpc/methods/orchestration/worker/local-worker-start.ts b/src/main/runtime/rpc/methods/orchestration/worker/local-worker-start.ts index cf8bb51fad8..c6138a8a568 100644 --- a/src/main/runtime/rpc/methods/orchestration/worker/local-worker-start.ts +++ b/src/main/runtime/rpc/methods/orchestration/worker/local-worker-start.ts @@ -35,13 +35,7 @@ import { releaseRetriedWorkerTerminal } from './retried-worker-terminal-release' import { requireWorkerAuthority, type WorkerEffect } from './worker-topology' import { prepareLocalWorkerStart } from './worker-start-validation' import { deliverAndSettleWorkerStartReadiness } from './worker-start-readiness-settlement' - -type WorkerStartMutation = { - callerFingerprint: string - requestId: string - method: string - payloadHash: string -} +import type { WorkerStartMutation } from './worker-start-mutation' export async function startLocalWorker(args: { params: WorkerStartInput @@ -114,11 +108,12 @@ export async function startLocalWorker(args: { existingPlacement: 'current or an exact existing folder workspace' }) } - let resolvedWorktree = creationWorktree - ? undefined - : requestedWorktree === 'current' - ? await runtime.showManagedTerminalWorkspace(`id:${coordinatorWorktreeId}`) - : await runtime.showManagedTerminalWorkspace(requestedWorktree) + let resolvedWorktree: Awaited>['worktree'] | undefined = + creationWorktree + ? undefined + : requestedWorktree === 'current' + ? await runtime.showManagedTerminalWorkspace(`id:${coordinatorWorktreeId}`) + : await runtime.showManagedTerminalWorkspace(requestedWorktree) if (terminal) { await assertExplicitWorkerUsable({ runtime, diff --git a/src/main/runtime/rpc/methods/orchestration/worker/worker-start-mutation.ts b/src/main/runtime/rpc/methods/orchestration/worker/worker-start-mutation.ts new file mode 100644 index 00000000000..1a21f553ec1 --- /dev/null +++ b/src/main/runtime/rpc/methods/orchestration/worker/worker-start-mutation.ts @@ -0,0 +1,6 @@ +export type WorkerStartMutation = { + callerFingerprint: string + requestId: string + method: string + payloadHash: string +} diff --git a/src/main/runtime/runtime-created-worktree-terminal-target.test.ts b/src/main/runtime/runtime-created-worktree-terminal-target.test.ts index 29857f67e0f..89b4274a296 100644 --- a/src/main/runtime/runtime-created-worktree-terminal-target.test.ts +++ b/src/main/runtime/runtime-created-worktree-terminal-target.test.ts @@ -5,7 +5,7 @@ import { Store } from '../persistence/loading-store/store' import { ProfileStateSqliteAuthority } from '../persistence/profile-state/profile-state-sqlite-authority' import { RuntimeRemoteFetchController } from './runtime-remote-fetch-controller' import type { RemoteTrackingBase } from './runtime-remote-fetch-controller' -import type { OrcaRuntimeWithNotifySshStateChanged } from './orca-runtime-notify-ssh-state-changed' +import type { OrcaRuntimeWithNotifySshStateChanged } from './workspaces/workspace-host-notifications' import type { RuntimeManagedWorktreeCreateArgs } from './runtime-managed-worktree-create-types' import type { PreparationRearmHolder } from '../worktree-create-preparation' import type { RuntimeWorkspaceCreateEvents } from '../workspace-create-telemetry' diff --git a/src/main/runtime/runtime-store-contract.ts b/src/main/runtime/runtime-store-contract.ts index 80c65e1b88d..e2d694488ed 100644 --- a/src/main/runtime/runtime-store-contract.ts +++ b/src/main/runtime/runtime-store-contract.ts @@ -89,6 +89,14 @@ export type RuntimeStore = { getMobileClientTabSelections?: Store['getMobileClientTabSelections'] setMobileClientTabSelections?: Store['setMobileClientTabSelections'] getSettings(): { + autoRenameBranchFromWork?: GlobalSettings['autoRenameBranchFromWork'] + nativeChatAutoName?: GlobalSettings['nativeChatAutoName'] + notifications?: GlobalSettings['notifications'] + claudeManagedAccounts?: GlobalSettings['claudeManagedAccounts'] + activeClaudeManagedAccountId?: GlobalSettings['activeClaudeManagedAccountId'] + activeClaudeManagedAccountIdsByRuntime?: GlobalSettings['activeClaudeManagedAccountIdsByRuntime'] + activeCodexManagedAccountId?: GlobalSettings['activeCodexManagedAccountId'] + activeCodexManagedAccountIdsByRuntime?: GlobalSettings['activeCodexManagedAccountIdsByRuntime'] agentWorkspaceTrustEnabled?: GlobalSettings['agentWorkspaceTrustEnabled'] codexManagedAccounts?: GlobalSettings['codexManagedAccounts'] nestedWorkerMaxDepth?: GlobalSettings['nestedWorkerMaxDepth'] diff --git a/src/main/runtime/session-graph/session-graph-promotion.ts b/src/main/runtime/session-graph/session-graph-promotion.ts index 0c203603292..5a389a18668 100644 --- a/src/main/runtime/session-graph/session-graph-promotion.ts +++ b/src/main/runtime/session-graph/session-graph-promotion.ts @@ -9,9 +9,9 @@ import type { OrcaRuntimeWithRecordPtyWorktree as RecordPtyWorktreeMethods } fro import type { RuntimeSessionTabsInventoryPublication as SessionTabsInventoryPublicationMethods } from '../session-tabs/session-tabs-inventory-publication' import type { OrcaRuntimeWithRefreshFloatingWorkspacePtyLiveness as RefreshFloatingWorkspacePtyLivenessMethods } from '../terminal/lifecycle/floating-session-liveness' import type { OrcaRuntimeWithTouchMobileSessionTabsForWorktree as TouchMobileSessionTabsForWorktreeMethods } from '../orca-runtime-touch-mobile-session-tabs-for-worktree' -import type { OrcaRuntimeWithScheduleMobileSessionTabsChanged as ScheduleMobileSessionTabsChangedMethods } from '../orca-runtime-schedule-mobile-session-tabs-changed' +import type { OrcaRuntimeWithScheduleMobileSessionTabsChanged as ScheduleMobileSessionTabsChangedMethods } from '../session-tabs/session-tabs-change-publication' import type { OrcaRuntimeWithSyncMobileSessionTabs as SyncMobileSessionTabsMethods } from '../session-tabs/mobile-session-reconciliation' -import { OrcaRuntimeWithNotifySshStateChanged } from '../orca-runtime-notify-ssh-state-changed' +import { OrcaRuntimeWithNotifySshStateChanged } from '../workspaces/workspace-host-notifications' import { HEADLESS_RUNTIME_WINDOW_ID } from '../../../shared/runtime-types' import type { ExecutionHostId } from '../../../shared/execution-host' import type { WorkspaceSessionState } from '../../../shared/workspace-session-state-types' diff --git a/src/main/runtime/session-tabs-inventory-publication.test.ts b/src/main/runtime/session-tabs-inventory-publication.test.ts index 672456da02d..2cfdc6a3b59 100644 --- a/src/main/runtime/session-tabs-inventory-publication.test.ts +++ b/src/main/runtime/session-tabs-inventory-publication.test.ts @@ -1,6 +1,10 @@ import { describe, expect, it, vi } from 'vitest' import { OrcaRuntimeService } from './orca-runtime' import { getSessionTabsInventoryPublication } from './session-tabs/session-tabs-inventory-test-fixture' +import type { RuntimeStore } from './runtime-store-contract' +import { getDefaultWorkspaceSession } from '../../shared/constants' +import type { WorkspaceSessionState } from '../../shared/workspace-session-state-types' +import type { DurableProfileStateMutation } from '../persistence/loading-store/store-runtime-state' function createInventoryRuntime(): OrcaRuntimeService { const runtime = new OrcaRuntimeService() @@ -411,3 +415,409 @@ describe('authoritative session tab inventory publication', () => { } }) }) + +describe('workspace-persistence-construction characterization', () => { + type Runtime = InstanceType + + const WORKTREE_ID = 'repo::/workspace' + + function constructRuntime(observe: (runtime: Runtime) => void): Runtime { + class ConstructorObservedRuntime extends OrcaRuntimeService { + protected override get canonicalFetchKeyCache(): ReadonlyMap { + const cache = super.canonicalFetchKeyCache + observe(this) + return cache + } + } + return new ConstructorObservedRuntime(null) + } + + function getNativeMethod( + runtime: Runtime, + name: string + ): (this: Runtime, ...args: unknown[]) => unknown { + let prototype = Object.getPrototypeOf(runtime) + while (prototype) { + const method = Object.getOwnPropertyDescriptor(prototype, name)?.value + if (typeof method === 'function') { + return method + } + prototype = Object.getPrototypeOf(prototype) + } + throw new Error(`Missing runtime method: ${name}`) + } + + describe('workspace persistence during full runtime construction', () => { + // The fetch-cache getter runs in the base constructor before later runtime fields initialize. + it('returns a native promise for absent active-workspace context', async () => { + let observed: Promise | undefined + const observe = vi.fn((runtime: Runtime) => { + expect(runtime['store']).toBeNull() + observed = runtime.resolveActiveWorktreeContext() + expect(observed).toBeInstanceOf(Promise) + }) + constructRuntime(observe) + expect(observe).toHaveBeenCalledOnce() + await expect(observed).resolves.toBeNull() + }) + + it('collects no public tab ids when no snapshot exists', () => { + const observe = vi.fn((runtime: Runtime) => { + const ids = runtime['collectPublicMobileSessionTabIds'](undefined) + expect(ids).toBeInstanceOf(Set) + expect(ids.size).toBe(0) + }) + constructRuntime(observe) + expect(observe).toHaveBeenCalledOnce() + }) + + it('resolves no host tab id when no snapshot exists', () => { + const observe = vi.fn((runtime: Runtime) => { + expect(runtime['resolveMobileSessionHostTabId'](undefined, 'absent-tab')).toBeNull() + }) + constructRuntime(observe) + expect(observe).toHaveBeenCalledOnce() + }) + + it('normalizes an empty group order without a snapshot', () => { + const observe = vi.fn((runtime: Runtime) => { + const order = runtime['normalizeMobileSessionTabOrder']( + undefined, + { id: 'empty-group', activeTabId: null, tabOrder: [] }, + [] + ) + expect(order).toEqual([]) + }) + constructRuntime(observe) + expect(observe).toHaveBeenCalledOnce() + }) + + it('leaves a title unchanged when persistence is unavailable', () => { + const observe = vi.fn((runtime: Runtime) => { + expect(runtime['store']).toBeNull() + expect(runtime['persistHeadlessTerminalTitle'](WORKTREE_ID, 'absent-tab', 'Title')).toBe( + undefined + ) + }) + constructRuntime(observe) + expect(observe).toHaveBeenCalledOnce() + }) + + it('finds no split source in an empty runtime', () => { + const observe = vi.fn((runtime: Runtime) => { + expect( + runtime['resolveTerminalSplitSourceAuthority']( + WORKTREE_ID, + 'absent-tab', + 'absent-leaf', + 'absent-pty' + ) + ).toBeNull() + }) + constructRuntime(observe) + expect(observe).toHaveBeenCalledOnce() + }) + + it('rejects destructive removal asynchronously when no local provider exists', async () => { + let observed: Promise | undefined + const observe = vi.fn((runtime: Runtime) => { + observed = runtime['stopPtysForDestructiveWorktreeRemoval'](WORKTREE_ID) + expect(observed).toBeInstanceOf(Promise) + }) + constructRuntime(observe) + expect(observe).toHaveBeenCalledOnce() + await expect(observed).rejects.toThrow( + `PTY provider unavailable for worktree deletion: ${WORKTREE_ID}` + ) + }) + + it('rejects resume-record clearing asynchronously when persistence is unavailable', async () => { + let observed: Promise | undefined + const observe = vi.fn((runtime: Runtime) => { + const clear = getNativeMethod(runtime, 'clearWorktreeTerminalResumeRecords') + const result = clear.call(runtime, WORKTREE_ID, 'local', ['absent-tab']) + expect(result).toBeInstanceOf(Promise) + if (!(result instanceof Promise)) { + throw new Error('Resume clearing must return a native promise') + } + observed = result + }) + constructRuntime(observe) + expect(observe).toHaveBeenCalledOnce() + await expect(observed).rejects.toThrow('workspace_session_unavailable') + }) + + it('keeps the root mutation map and pending promise after construction finishes', async () => { + const created = Promise.resolve('created') + const run = vi.fn(() => created) + let mapDuringConstruction: unknown + let promiseDuringConstruction: Promise | undefined + const observe = vi.fn((runtime: Runtime) => { + mapDuringConstruction = runtime['worktreeCreateByMutationId'] + expect(mapDuringConstruction).toBeInstanceOf(Map) + expect(Object.getOwnPropertyDescriptor(runtime, 'worktreeCreateByMutationId')).toEqual({ + value: mapDuringConstruction, + writable: true, + enumerable: true, + configurable: true + }) + promiseDuringConstruction = runtime.dedupeWorktreeCreate('repo', 'mutation', run) + expect(promiseDuringConstruction).toBe(created) + expect(runtime['worktreeCreateByMutationId'].get('repo\0mutation')).toBe(created) + }) + const runtime = constructRuntime(observe) + expect(observe).toHaveBeenCalledOnce() + expect(runtime['worktreeCreateByMutationId']).toBe(mapDuringConstruction) + expect(runtime['worktreeCreateByMutationId'].get('repo\0mutation')).toBe(created) + const duplicate = vi.fn(() => Promise.resolve('duplicate')) + expect(runtime.dedupeWorktreeCreate('repo', 'mutation', duplicate)).toBe(created) + expect(run).toHaveBeenCalledOnce() + expect(duplicate).not.toHaveBeenCalled() + await expect(promiseDuringConstruction).resolves.toBe('created') + }) + + it('removes metadata through the narrow store capabilities during construction', () => { + const worktreeId = 'constructor-metadata::/workspace' + const removeWorktreeMeta = vi.fn() + const store: Pick = { + getWorktreeMeta: () => undefined, + removeWorktreeMeta, + getRepos: () => [] + } + let removedMapDuringConstruction: unknown + let snapshotsDuringConstruction: unknown + let acceptedSnapshotsDuringConstruction: unknown + const observe = vi.fn((runtime: Runtime) => { + expect(runtime['store']).toBeNull() + removedMapDuringConstruction = runtime['removedMobileSessionWorktreeIds'] + snapshotsDuringConstruction = runtime['mobileSessionTabsByWorktree'] + acceptedSnapshotsDuringConstruction = runtime['acceptedRendererMobileSnapshotByWorktree'] + expect(Object.getOwnPropertyDescriptor(runtime, 'removedMobileSessionWorktreeIds')).toEqual( + { + value: removedMapDuringConstruction, + writable: true, + enumerable: true, + configurable: true + } + ) + const remove = getNativeMethod(runtime, 'removeWorktreeMetadataAndHistory') + expect(remove.call(runtime, store, worktreeId, 'local')).toBeUndefined() + expect(runtime['removedMobileSessionWorktreeIds'].get(worktreeId)).toEqual({}) + expect(removeWorktreeMeta).toHaveBeenCalledExactlyOnceWith(worktreeId, 'local') + }) + const runtime = constructRuntime(observe) + expect(observe).toHaveBeenCalledOnce() + expect(runtime['removedMobileSessionWorktreeIds']).toBe(removedMapDuringConstruction) + expect(runtime['mobileSessionTabsByWorktree']).toBe(snapshotsDuringConstruction) + expect(runtime['acceptedRendererMobileSnapshotByWorktree']).toBe( + acceptedSnapshotsDuringConstruction + ) + expect(runtime['removedMobileSessionWorktreeIds'].get(worktreeId)).toEqual({}) + expect(runtime['mobileSessionTabsByWorktree'].has(worktreeId)).toBe(false) + expect(runtime['acceptedRendererMobileSnapshotByWorktree'].has(worktreeId)).toBe(false) + }) + + it('preserves the native metadata-removal failure for an invalid null-store call', () => { + const observe = vi.fn((runtime: Runtime) => { + const remove = getNativeMethod(runtime, 'removeWorktreeMetadataAndHistory') + // This intentionally unsupported input distinguishes body execution from a missing late owner. + expect(() => remove.call(runtime, null, WORKTREE_ID)).toThrow( + new TypeError( + process.versions.bun + ? "null is not an object (evaluating 'store.getWorktreeMeta')" + : "Cannot read properties of null (reading 'getWorktreeMeta')" + ) + ) + }) + constructRuntime(observe) + expect(observe).toHaveBeenCalledOnce() + }) + }) +}) + +describe('workspace-terminal-resume-records characterization', () => { + type ResumeStore = Pick< + RuntimeStore, + 'getWorkspaceSession' | 'setWorkspaceSession' | 'runDurableMutation' + > + const WORKTREE_ID = 'repo::/workspace' + const HOST_ID = 'runtime:original-partition' + + function clear(runtime: InstanceType): Promise { + return runtime['clearWorktreeTerminalResumeRecords'](WORKTREE_ID, HOST_ID, ['closed']) + } + + describe('workspace terminal resume mutation ownership', () => { + it('reads the current mutation and rollback stores with their original receivers', async () => { + const runtime = new OrcaRuntimeService(null) + const reads: string[] = [] + let runMutation = (): void => { + throw new Error('mutation was not reserved') + } + let rollback = (): void => { + throw new Error('rollback was not installed') + } + let finish = (): void => { + throw new Error('mutation was not staged') + } + const before: WorkspaceSessionState = { + ...getDefaultWorkspaceSession(), + terminalPtyIncarnationsByPaneKey: { 'closed:leaf': 'old', 'other:leaf': 'other' } + } + let mutationSession = before + let rollbackSession = before + const reservation: ResumeStore = { + getWorkspaceSession: () => { + throw new Error('reservation store read') + }, + setWorkspaceSession: () => { + throw new Error('reservation store write') + }, + runDurableMutation( + mutate: () => DurableProfileStateMutation + ): Promise { + expect(this).toBe(reservation) + const deferred = Promise.withResolvers() + runMutation = () => { + const staged = mutate() + rollback = + staged.rollback ?? + (() => { + throw new Error('rollback absent') + }) + finish = () => deferred.resolve(staged.value) + } + return deferred.promise + } + } + const mutation: ResumeStore = { + getWorkspaceSession(hostId) { + expect(this).toBe(mutation) + expect(hostId).toBe(HOST_ID) + return mutationSession + }, + setWorkspaceSession(session, hostId) { + expect(this).toBe(mutation) + expect(hostId).toBe(HOST_ID) + mutationSession = session + } + } + const recovery: ResumeStore = { + getWorkspaceSession(hostId) { + expect(this).toBe(recovery) + expect(hostId).toBe(HOST_ID) + return rollbackSession + }, + setWorkspaceSession(session, hostId) { + expect(this).toBe(recovery) + expect(hostId).toBe(HOST_ID) + rollbackSession = session + } + } + let current: ResumeStore | null = reservation + let phase = 'reserve' + Object.defineProperty(runtime, 'store', { + get: () => { + reads.push(phase) + return current + } + }) + const pending = clear(runtime) + expect(reads).toEqual(['reserve', 'reserve', 'reserve', 'reserve']) + current = mutation + phase = 'mutation' + runMutation() + expect(reads.filter((entry) => entry === 'mutation')).toHaveLength(3) + expect(mutationSession.terminalPtyIncarnationsByPaneKey).toEqual({ 'other:leaf': 'other' }) + rollbackSession = { + ...mutationSession, + activeWorktreeId: 'another-worktree', + terminalPtyIncarnationsByPaneKey: { + ...mutationSession.terminalPtyIncarnationsByPaneKey, + 'new:leaf': 'new' + } + } + current = recovery + phase = 'rollback' + rollback() + expect(reads.filter((entry) => entry === 'rollback')).toHaveLength(2) + expect(rollbackSession.activeWorktreeId).toBe('another-worktree') + expect(rollbackSession.terminalPtyIncarnationsByPaneKey).toEqual({ + 'closed:leaf': 'old', + 'other:leaf': 'other', + 'new:leaf': 'new' + }) + finish() + await pending + }) + + it.each([ + { label: 'null', store: null, reads: 1 }, + { label: 'missing reader', store: {}, reads: 1 }, + { + label: 'missing writer', + store: { getWorkspaceSession: () => getDefaultWorkspaceSession() }, + reads: 2 + }, + { + label: 'missing reservation', + store: { + getWorkspaceSession: () => getDefaultWorkspaceSession(), + setWorkspaceSession: () => {} + }, + reads: 3 + } + ])('preserves admission refusal and lookup count for $label', async ({ store, reads }) => { + const runtime = new OrcaRuntimeService(null) + let lookups = 0 + Object.defineProperty(runtime, 'store', { + get: () => { + lookups += 1 + return store + } + }) + await expect(clear(runtime)).rejects.toThrow('workspace_session_unavailable') + expect(lookups).toBe(reads) + }) + + it('keeps callback-time missing methods at the original late failure boundary', async () => { + const runtime = new OrcaRuntimeService(null) + const store: ResumeStore = { + getWorkspaceSession: () => getDefaultWorkspaceSession(), + setWorkspaceSession: () => {}, + async runDurableMutation( + mutate: () => DurableProfileStateMutation + ): Promise { + store.getWorkspaceSession = undefined + return mutate().value + } + } + Object.defineProperty(runtime, 'store', { get: () => store }) + await expect(clear(runtime)).rejects.toThrow( + 'this.store.getWorkspaceSession is not a function' + ) + }) + it('keeps a store disappearing during admission at the original property lookup', async () => { + const runtime = new OrcaRuntimeService(null) + const initial: ResumeStore = {} + let current: ResumeStore | null = initial + Object.defineProperty(initial, 'getWorkspaceSession', { + get: () => { + current = null + return () => getDefaultWorkspaceSession() + } + }) + let lookups = 0 + Object.defineProperty(runtime, 'store', { + get: () => { + lookups += 1 + return current + } + }) + await expect(clear(runtime)).rejects.toThrow( + /^(?:Cannot read properties of null \(reading 'setWorkspaceSession'\)|null is not an object \(evaluating 'this\.store\.setWorkspaceSession'\))$/ + ) + expect(lookups).toBe(2) + }) + }) +}) diff --git a/src/main/runtime/session-tabs/headless-tab-group-moves.ts b/src/main/runtime/session-tabs/headless-tab-group-moves.ts index d0bcb6d962a..e502687bbb3 100644 --- a/src/main/runtime/session-tabs/headless-tab-group-moves.ts +++ b/src/main/runtime/session-tabs/headless-tab-group-moves.ts @@ -12,7 +12,7 @@ import { import { randomUUID } from 'node:crypto' import { withSelectedTabs } from '../headless-tab-group-selection' import type { TabGroupLayoutNode } from '../../../shared/tab-types' -import type { OrcaRuntimeWithPersistHeadlessTerminalTitle } from '../orca-runtime-persist-headless-terminal-title' +import type { OrcaRuntimeWithPersistHeadlessTerminalTitle } from '../workspaces/workspace-file-session-targets' import type { OrcaRuntimeWithPruneMobileSessionTabGroupLayout } from '../orca-runtime-prune-mobile-session-tab-group-layout' export abstract class RuntimeHeadlessTabGroups extends RuntimeHeadlessTabProperties { diff --git a/src/main/runtime/session-tabs/headless-terminal-tab-close.ts b/src/main/runtime/session-tabs/headless-terminal-tab-close.ts index 04482de7438..fdde61c1cdf 100644 --- a/src/main/runtime/session-tabs/headless-terminal-tab-close.ts +++ b/src/main/runtime/session-tabs/headless-terminal-tab-close.ts @@ -1,6 +1,6 @@ import type { RuntimeHeadlessTabProperties as HeadlessTabPropertiesMethods } from './headless-tab-properties' import type { RuntimeHeadlessTabGroups as HeadlessTabGroupsMethods } from './headless-tab-group-moves' -import type { OrcaRuntimeWithPersistHeadlessTerminalTitle as PersistHeadlessTerminalTitleMethods } from '../orca-runtime-persist-headless-terminal-title' +import type { OrcaRuntimeWithPersistHeadlessTerminalTitle as PersistHeadlessTerminalTitleMethods } from '../workspaces/workspace-file-session-targets' import type { OrcaRuntimeWithRestoreLivePairedRendererSessionOwnedMobileTerminals as RestoreLivePairedRendererSessionOwnedMobileTerminalsMethods } from './renderer-session-terminal-rescue' import { OrcaRuntimeWithCloseStructuredAgentSessionTab } from './structured-session-tab-close' import type { diff --git a/src/main/runtime/session-tabs/mobile-session-publication-preservation.ts b/src/main/runtime/session-tabs/mobile-session-publication-preservation.ts index 03e6c289e51..bd96b4f5f22 100644 --- a/src/main/runtime/session-tabs/mobile-session-publication-preservation.ts +++ b/src/main/runtime/session-tabs/mobile-session-publication-preservation.ts @@ -1,4 +1,4 @@ -import type { OrcaRuntimeWithScheduleMobileSessionTabsChanged as ScheduleMobileSessionTabsChangedMethods } from '../orca-runtime-schedule-mobile-session-tabs-changed' +import type { OrcaRuntimeWithScheduleMobileSessionTabsChanged as ScheduleMobileSessionTabsChangedMethods } from './session-tabs-change-publication' import { OrcaRuntimeWithMergePreservedHeadlessMobileSessionTabs } from './mobile-session-preservation' import type { RuntimeMobileSessionSnapshotTab, diff --git a/src/main/runtime/orca-runtime-schedule-mobile-session-tabs-changed.ts b/src/main/runtime/session-tabs/session-tabs-change-publication.ts similarity index 94% rename from src/main/runtime/orca-runtime-schedule-mobile-session-tabs-changed.ts rename to src/main/runtime/session-tabs/session-tabs-change-publication.ts index 75acde4d898..eaf9a14ca1f 100644 --- a/src/main/runtime/orca-runtime-schedule-mobile-session-tabs-changed.ts +++ b/src/main/runtime/session-tabs/session-tabs-change-publication.ts @@ -1,16 +1,15 @@ -// @ts-nocheck -- mechanically split from OrcaRuntimeService; behavior is covered by AST equivalence and characterization tests. -import { OrcaRuntimeWithStoredMobileSnapshotHasStalePreservedTab } from './session-tabs/mobile-session-publication-preservation' +import { OrcaRuntimeWithStoredMobileSnapshotHasStalePreservedTab } from './mobile-session-publication-preservation' import type { BrowserTabInfo, RuntimeMobileSessionClientTab, RuntimeMobileSessionMarkdownTab, RuntimeMobileSessionTabGroup, RuntimeMobileSessionTabsResult -} from '../../shared/runtime-types' -import { UNPUBLISHED_WORKTREE_PUBLICATION_EPOCH } from '../../shared/runtime-types' -import { getRuntimeBrowserPageRegistry } from './runtime-browser-page-registry' +} from '../../../shared/runtime-types' +import { UNPUBLISHED_WORKTREE_PUBLICATION_EPOCH } from '../../../shared/runtime-types' +import { getRuntimeBrowserPageRegistry } from '../runtime-browser-page-registry' -export class OrcaRuntimeWithScheduleMobileSessionTabsChanged extends OrcaRuntimeWithStoredMobileSnapshotHasStalePreservedTab { +export abstract class OrcaRuntimeWithScheduleMobileSessionTabsChanged extends OrcaRuntimeWithStoredMobileSnapshotHasStalePreservedTab { protected scheduleMobileSessionTabsChanged(worktreeId: string): void { this.pendingMobileSessionTabsChangeSequenceByWorktree.set( worktreeId, diff --git a/src/main/runtime/session-tabs/session-tabs-inventory.ts b/src/main/runtime/session-tabs/session-tabs-inventory.ts index 592bd1034a3..01f47f36a21 100644 --- a/src/main/runtime/session-tabs/session-tabs-inventory.ts +++ b/src/main/runtime/session-tabs/session-tabs-inventory.ts @@ -1,5 +1,5 @@ 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 { OrcaRuntimeWithScheduleMobileSessionTabsChanged as ScheduleMobileSessionTabsChangedMethods } from './session-tabs-change-publication' 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' diff --git a/src/main/runtime/session-tabs/structured-session-create-support.ts b/src/main/runtime/session-tabs/structured-session-create-support.ts index 7a947b738d5..69b71691da5 100644 --- a/src/main/runtime/session-tabs/structured-session-create-support.ts +++ b/src/main/runtime/session-tabs/structured-session-create-support.ts @@ -1,6 +1,6 @@ import type { OrcaRuntimeWithRestoreStructuredAgentSessionTabsOnce as RestoreStructuredAgentSessionTabsOnceMethods } from './structured-session-tab-restoration' import { agentSessionRefusalError } from '../../../shared/agent-session-wire-refusals' -import { OrcaRuntimeWithGetWorktreePs } from '../orca-runtime-get-worktree-ps' +import { OrcaRuntimeWithGetWorktreePs } from '../workspaces/workspace-process-listing' import { getStructuredAgentSessionHost } from '../../native-chat/agent-session-wire/structured-agent-session-registry' import { resolveCommittedStructuredAgentSessionAdoptionIntent, diff --git a/src/main/runtime/session-tabs/structured-session-tab-restoration.ts b/src/main/runtime/session-tabs/structured-session-tab-restoration.ts index 91a46c8c617..8b5f7ded74f 100644 --- a/src/main/runtime/session-tabs/structured-session-tab-restoration.ts +++ b/src/main/runtime/session-tabs/structured-session-tab-restoration.ts @@ -1,4 +1,4 @@ -import type { OrcaRuntimeWithScheduleMobileSessionTabsChanged as ScheduleMobileSessionTabsChangedMethods } from '../orca-runtime-schedule-mobile-session-tabs-changed' +import type { OrcaRuntimeWithScheduleMobileSessionTabsChanged as ScheduleMobileSessionTabsChangedMethods } from './session-tabs-change-publication' import { defaultAgentChatLabel } from '../../../shared/agent-session-chat-label' import { OrcaRuntimeWithGetStructuredAgentSessionCreateSupport } from './structured-session-create-support' import { getStructuredAgentSessionHost } from '../../native-chat/agent-session-wire/structured-agent-session-registry' diff --git a/src/main/runtime/structured-agent-account-home.ts b/src/main/runtime/structured-agent-account-home.ts index 5eb76a9168d..e5db6a3584d 100644 --- a/src/main/runtime/structured-agent-account-home.ts +++ b/src/main/runtime/structured-agent-account-home.ts @@ -68,9 +68,13 @@ export async function resolveStructuredCodexAccountHomePath( export function resolveStructuredCodexAccountKind( home: string, - settings: Pick< - GlobalSettings, - 'codexManagedAccounts' | 'activeCodexManagedAccountId' | 'activeCodexManagedAccountIdsByRuntime' + settings: Partial< + Pick< + GlobalSettings, + | 'codexManagedAccounts' + | 'activeCodexManagedAccountId' + | 'activeCodexManagedAccountIdsByRuntime' + > > ): AgentSessionAccountKind | undefined { const same = (other: string): boolean => diff --git a/src/main/runtime/structured-agent-session-mobile-attention.test.ts b/src/main/runtime/structured-agent-session-mobile-attention.test.ts index b873070e781..13e46d5334e 100644 --- a/src/main/runtime/structured-agent-session-mobile-attention.test.ts +++ b/src/main/runtime/structured-agent-session-mobile-attention.test.ts @@ -192,3 +192,16 @@ describe('readStructuredAttentionWorkspaceLabels', () => { expect(labels).toEqual({ worktreeLabel: 'Notes' }) }) }) + +it('preserves the missing notification settings failure before dispatch', () => { + let reads = 0 + const h = harness({ + readNotificationSettings() { + reads += 1 + return undefined + } + }) + expect(() => h.delivery.deliver(PROMPT, SUMMARY)).toThrow(TypeError) + expect(reads).toBe(1) + expect(h.sent).toEqual([]) +}) diff --git a/src/main/runtime/structured-agent-session-mobile-attention.ts b/src/main/runtime/structured-agent-session-mobile-attention.ts index f609615c74b..8ab5cd3a976 100644 --- a/src/main/runtime/structured-agent-session-mobile-attention.ts +++ b/src/main/runtime/structured-agent-session-mobile-attention.ts @@ -30,7 +30,7 @@ import { getRuntimeDesktopSurface } from './runtime-desktop-surface' export type StructuredAttentionWorkspaceLabels = { repoLabel?: string; worktreeLabel?: string } export type StructuredAttentionMobileDeliveryDeps = { - readNotificationSettings: () => NotificationSettings + readNotificationSettings: () => NotificationSettings | undefined /** In-memory metadata only: a lookup that could wait would let the alert outlive its prompt. */ readWorkspaceLabels: (scope: AgentSessionExecutionLocation) => StructuredAttentionWorkspaceLabels /** In-memory only, like the labels: whether the desktop's Filters switches hide this workspace. */ @@ -85,7 +85,7 @@ export function createStructuredAttentionMobileDelivery( return } const scope = edge.type === 'prompt' ? edge.prompt.scope : edge.completion.scope - const settings = deps.readNotificationSettings() + const settings = deps.readNotificationSettings()! if (!settings.notifyHiddenWorkspaces && deps.isWorkspaceHidden(scope)) { return } diff --git a/src/main/runtime/structured-chat-naming-runtime.ts b/src/main/runtime/structured-chat-naming-runtime.ts index 49f37c3f6f8..48dd829a53e 100644 --- a/src/main/runtime/structured-chat-naming-runtime.ts +++ b/src/main/runtime/structured-chat-naming-runtime.ts @@ -1,4 +1,3 @@ -import type { Store } from '../persistence' import { LOCAL_EXECUTION_HOST_ID, type ExecutionHostId } from '../../shared/execution-host' import type { CommitMessageAgentEnvironmentResolvers } from '../text-generation/commit-message-agent-environment' import type { StructuredChatNamingDeps } from '../native-chat/structured-chat-naming' @@ -22,7 +21,10 @@ type StructuredChatNamingRuntime = { } export function structuredChatNamingDeps( - getStore: () => Pick, + getStore: () => { + getSettings: () => Parameters[0] & + ReturnType + }, runtime: StructuredChatNamingRuntime, logger: StructuredAgentSessionLogger ): StructuredChatNamingDeps { diff --git a/src/main/runtime/structured-chat-worker-start.test.ts b/src/main/runtime/structured-chat-worker-start.test.ts index e2f2b21a25b..907373c1196 100644 --- a/src/main/runtime/structured-chat-worker-start.test.ts +++ b/src/main/runtime/structured-chat-worker-start.test.ts @@ -24,10 +24,21 @@ import { turnText } from './structured-chat-coordinator-mail-rig.test-fixture' +import type { placeWorkerAgent } from './rpc/methods/orchestration/worker/worker-start-agent-placement' + +type WorkspaceLookup = { + showManagedTerminalWorkspace: ( + ...args: Parameters + ) => Promise>['worktree']> +} + const WORKER = formatOrcaSessionAddress(testOrcaSessionId(PEER_CHAT)) beforeEach(() => { - vi.spyOn(runtime, 'showManagedTerminalWorkspace').mockImplementation(async (selector) => ({ + vi.spyOn( + runtime, + 'showManagedTerminalWorkspace' + ).mockImplementation(async (selector) => ({ id: selector.replace(/^id:/, ''), repoId: 'repo' })) diff --git a/src/main/runtime/orca-runtime-resolve-terminal-split-source-authority.ts b/src/main/runtime/terminal/launch/terminal-split-source-authority.ts similarity index 88% rename from src/main/runtime/orca-runtime-resolve-terminal-split-source-authority.ts rename to src/main/runtime/terminal/launch/terminal-split-source-authority.ts index 97d46e2a7f6..879b6a7619d 100644 --- a/src/main/runtime/orca-runtime-resolve-terminal-split-source-authority.ts +++ b/src/main/runtime/terminal/launch/terminal-split-source-authority.ts @@ -1,22 +1,28 @@ -// @ts-nocheck -- mechanically split from OrcaRuntimeService; behavior is covered by AST equivalence and characterization tests. -import { OrcaRuntimeWithSplitPtyBackedTerminal } from './terminal/launch/terminal-split-surface' +import type { OrcaRuntimeWithPruneMobileSessionTabGroupLayout } from '../../orca-runtime-prune-mobile-session-tab-group-layout' +import { OrcaRuntimeWithSplitPtyBackedTerminal } from './terminal-split-surface' import { resolveTerminalSessionWorktreeId, runtimeWorktreeIdsEqual -} from './runtime-worktree-path-identity' -import { makePaneKey } from '../../shared/stable-pane-id' -import { terminalLayoutContainsLeaf } from '../../shared/workspace-session-pane-ownership' +} from '../../runtime-worktree-path-identity' +import { makePaneKey } from '../../../../shared/stable-pane-id' +import { terminalLayoutContainsLeaf } from '../../../../shared/workspace-session-pane-ownership' import type { AgentTeamsTmuxCompatRequest, AgentTeamsTmuxCompatResponse -} from './claude-agent-teams-service' +} from '../../claude-agent-teams-service' import { ensureClaudeAgentTeamsShimDir, resolveClaudeAgentTeamsShimBin -} from './claude-agent-teams-shim-env' -import { applyClaudeEnvPatch } from '../claude-accounts/environment' +} from '../../claude-agent-teams-shim-env' +import { applyClaudeEnvPatch } from '../../../claude-accounts/environment' + +export abstract class OrcaRuntimeWithResolveTerminalSplitSourceAuthority extends OrcaRuntimeWithSplitPtyBackedTerminal { + abstract getTerminalHandleForPaneKey( + ...args: Parameters< + OrcaRuntimeWithPruneMobileSessionTabGroupLayout['getTerminalHandleForPaneKey'] + > + ): ReturnType -export class OrcaRuntimeWithResolveTerminalSplitSourceAuthority extends OrcaRuntimeWithSplitPtyBackedTerminal { protected resolveTerminalSplitSourceAuthority( worktreeId: string, tabId: string, diff --git a/src/main/runtime/terminal/lifecycle/pty-handle-invalidation.ts b/src/main/runtime/terminal/lifecycle/pty-handle-invalidation.ts index c56ad56d162..04d2f457c8c 100644 --- a/src/main/runtime/terminal/lifecycle/pty-handle-invalidation.ts +++ b/src/main/runtime/terminal/lifecycle/pty-handle-invalidation.ts @@ -1,4 +1,4 @@ -import { OrcaRuntimeWithResolveKnownWorkspaceFileTarget } from '../../orca-runtime-resolve-known-workspace-file-target' +import { OrcaRuntimeWithResolveKnownWorkspaceFileTarget } from '../../workspaces/known-workspace-file-target' import type { PtyIncarnationId } from '../../../../shared/pty-incarnation' import { getPtyExecutionHost } from '../../../../shared/terminal-execution-host' import { diff --git a/src/main/runtime/terminal/stop/worktree-terminal-stop.ts b/src/main/runtime/terminal/stop/worktree-terminal-stop.ts index f241ef81ed4..351ec80519c 100644 --- a/src/main/runtime/terminal/stop/worktree-terminal-stop.ts +++ b/src/main/runtime/terminal/stop/worktree-terminal-stop.ts @@ -1,4 +1,4 @@ -import { OrcaRuntimeWithResolveTerminalSplitSourceAuthority } from '../../orca-runtime-resolve-terminal-split-source-authority' +import { OrcaRuntimeWithResolveTerminalSplitSourceAuthority } from '../launch/terminal-split-source-authority' import { type ResolvedWorktree, runtimeWorktreeIdentityKey, diff --git a/src/main/runtime/terminal/view/mobile-terminal-surface.ts b/src/main/runtime/terminal/view/mobile-terminal-surface.ts index 242c4de48f8..de9633613e9 100644 --- a/src/main/runtime/terminal/view/mobile-terminal-surface.ts +++ b/src/main/runtime/terminal/view/mobile-terminal-surface.ts @@ -1,6 +1,6 @@ /* eslint-disable unicorn/no-useless-spread */ import { OrcaRuntimeWithCreateRuntimeOwnedMobileSessionTerminal } from '../launch/mobile-terminal-owned-create' -import type { OrcaRuntimeWithResolveTerminalSplitSourceAuthority } from '../../orca-runtime-resolve-terminal-split-source-authority' +import type { OrcaRuntimeWithResolveTerminalSplitSourceAuthority } from '../launch/terminal-split-source-authority' import type { RuntimeMobileSessionCreateTerminalResult, RuntimeMobileSessionTerminalClientTab diff --git a/src/main/runtime/orca-runtime-resolve-known-workspace-file-target.ts b/src/main/runtime/workspaces/known-workspace-file-target.ts similarity index 69% rename from src/main/runtime/orca-runtime-resolve-known-workspace-file-target.ts rename to src/main/runtime/workspaces/known-workspace-file-target.ts index 7f6f29af8a3..35a6fa2abef 100644 --- a/src/main/runtime/orca-runtime-resolve-known-workspace-file-target.ts +++ b/src/main/runtime/workspaces/known-workspace-file-target.ts @@ -1,18 +1,59 @@ -// @ts-nocheck -- mechanically split from OrcaRuntimeService; behavior is covered by AST equivalence and characterization tests. -import { OrcaRuntimeWithPersistHeadlessTerminalTitle } from './orca-runtime-persist-headless-terminal-title' +import type { RuntimeWorkspaceCatalog } from './workspace-catalog' +import type { OrcaRuntimeWithListManagedWorktrees } from './managed-workspace-operations' +import type { OrcaRuntimeWithTransitionGraphReloadToTerminalState } from '../session-graph/session-graph-authority' +import type { OrcaRuntimeWithBindPtyIncarnationHandle } from '../terminal/identity/terminal-handle-bindings' +import type { OrcaRuntimeWithInvalidateAllHandlesForPty } from '../terminal/lifecycle/pty-handle-invalidation' +import { OrcaRuntimeWithPersistHeadlessTerminalTitle } from './workspace-file-session-targets' import { getConnectionExecutionHostId, LOCAL_EXECUTION_HOST_ID, type ExecutionHostId -} from '../../shared/execution-host' -import type { ResolvedWorktree } from './runtime-worktree-path-identity' -import { resolveWorktreeHostRouting } from './worktree-launch-host-repo' -import { findRuntimeWorkspaceFileOwner } from '../../shared/runtime-workspace-file-owner' -import type { RuntimeMobileSessionTabsResult } from '../../shared/runtime-types' +} from '../../../shared/execution-host' +import type { ResolvedWorktree } from '../runtime-worktree-path-identity' +import { resolveWorktreeHostRouting } from '../worktree-launch-host-repo' +import { findRuntimeWorkspaceFileOwner } from '../../../shared/runtime-workspace-file-owner' +import type { RuntimeMobileSessionTabsResult } from '../../../shared/runtime-types' import { randomUUID } from 'node:crypto' -import type { PtyIncarnationId } from '../../shared/pty-incarnation' +import type { PtyIncarnationId } from '../../../shared/pty-incarnation' + +export abstract class OrcaRuntimeWithResolveKnownWorkspaceFileTarget extends OrcaRuntimeWithPersistHeadlessTerminalTitle { + protected abstract listResolvedWorktrees( + ...args: Parameters + ): ReturnType + protected abstract buildRuntimeVisibilitySourceMatchersByRepoId( + ...args: Parameters< + OrcaRuntimeWithListManagedWorktrees['buildRuntimeVisibilitySourceMatchersByRepoId'] + > + ): ReturnType + protected abstract isRuntimeWorktreeVisible( + ...args: Parameters + ): ReturnType + protected abstract resolveFolderWorkspaceConnectionId( + ...args: Parameters< + OrcaRuntimeWithTransitionGraphReloadToTerminalState['resolveFolderWorkspaceConnectionId'] + > + ): ReturnType< + OrcaRuntimeWithTransitionGraphReloadToTerminalState['resolveFolderWorkspaceConnectionId'] + > + protected abstract invalidatePtyIncarnationHandle( + ...args: Parameters + ): ReturnType + protected abstract invalidateAllHandlesForPty( + ...args: Parameters + ): ReturnType + protected abstract isTerminalHandleAdoptionBlocked( + ...args: Parameters< + OrcaRuntimeWithInvalidateAllHandlesForPty['isTerminalHandleAdoptionBlocked'] + > + ): ReturnType + protected abstract replaceSyntheticTerminalHandlesForRestoredPty( + ...args: Parameters< + OrcaRuntimeWithInvalidateAllHandlesForPty['replaceSyntheticTerminalHandlesForRestoredPty'] + > + ): ReturnType< + OrcaRuntimeWithInvalidateAllHandlesForPty['replaceSyntheticTerminalHandlesForRestoredPty'] + > -export class OrcaRuntimeWithResolveKnownWorkspaceFileTarget extends OrcaRuntimeWithPersistHeadlessTerminalTitle { protected async resolveKnownWorkspaceFileTarget( absolutePath: string, executionHostId: ExecutionHostId diff --git a/src/main/runtime/orca-runtime-list-managed-worktrees.ts b/src/main/runtime/workspaces/managed-workspace-operations.ts similarity index 82% rename from src/main/runtime/orca-runtime-list-managed-worktrees.ts rename to src/main/runtime/workspaces/managed-workspace-operations.ts index 9d7fd618d5d..6c7b4dc592b 100644 --- a/src/main/runtime/orca-runtime-list-managed-worktrees.ts +++ b/src/main/runtime/workspaces/managed-workspace-operations.ts @@ -1,15 +1,14 @@ -// @ts-nocheck -- mechanically split from OrcaRuntimeService; behavior is covered by AST equivalence and characterization tests. -import { OrcaRuntimeWithRestoreStructuredAgentSessionTabsOnce } from './session-tabs/structured-session-tab-restoration' -import { DEFAULT_WORKTREE_LIST_LIMIT } from './orca-runtime-postlude' -import type { RuntimeWorktreeListResult } from '../../shared/runtime-types' -import type { DetectedWorktreeListResult, Worktree } from '../../shared/worktree/types' -import type { Repo } from '../../shared/repo-types' -import { stopMissingWorktreeTerminals } from './missing-worktree-terminal-reconciliation' -import type { RuntimeCommandSurfaceHost } from './orca-runtime-core' -import type { WorktreeVisibilitySourceMatcher } from '../../shared/worktree/visibility-sources' -import type { RuntimeStore } from './runtime-store-contract' -import { getWorkspaceAttachments } from '../../shared/workspace-attachments' -import type { TerminalWorkspaceLaunchScope } from './runtime-legacy-worker-terminal-recovery-types' +import type { OrcaRuntimeWithResolveBrowserNetworkExecutionHostForWorktree } from '../terminal/launch/terminal-workspace-scope' +import { OrcaRuntimeWithRestoreStructuredAgentSessionTabsOnce } from '../session-tabs/structured-session-tab-restoration' +import { DEFAULT_WORKTREE_LIST_LIMIT } from '../orca-runtime-postlude' +import type { RuntimeWorktreeListResult } from '../../../shared/runtime-types' +import type { DetectedWorktreeListResult, Worktree } from '../../../shared/worktree/types' +import type { Repo } from '../../../shared/repo-types' +import { stopMissingWorktreeTerminals } from '../missing-worktree-terminal-reconciliation' +import type { WorktreeVisibilitySourceMatcher } from '../../../shared/worktree/visibility-sources' +import type { RuntimeStore } from '../runtime-store-contract' +import { getWorkspaceAttachments } from '../../../shared/workspace-attachments' +import type { TerminalWorkspaceLaunchScope } from '../runtime-legacy-worker-terminal-recovery-types' import type { WorkspacePortHostKillRequest, WorkspacePortHostScanResult, @@ -17,21 +16,29 @@ import type { WorkspacePortKillResult, WorkspacePortProbe, WorkspacePortScanResult -} from '../../shared/workspace-ports' +} from '../../../shared/workspace-ports' import { filterWorkspacePortProbes, killWorkspacePort, scanWorkspacePortProbes -} from '../ports/workspace-port-ownership' +} from '../../ports/workspace-port-ownership' import { killWorkspacePortOnExecutionHost, scanWorkspacePortsOnExecutionHost, type WorkspacePortExecutionHostDeps -} from '../ports/workspace-port-execution-host' -import { resolveWorktreeHostRouting } from './worktree-launch-host-repo' -import { LOCAL_EXECUTION_HOST_ID } from '../../shared/execution-host' +} from '../../ports/workspace-port-execution-host' +import { resolveWorktreeHostRouting } from '../worktree-launch-host-repo' +import { LOCAL_EXECUTION_HOST_ID } from '../../../shared/execution-host' + +export abstract class OrcaRuntimeWithListManagedWorktrees extends OrcaRuntimeWithRestoreStructuredAgentSessionTabsOnce { + protected abstract resolveTerminalWorkspaceLaunchTarget( + ...args: Parameters< + OrcaRuntimeWithResolveBrowserNetworkExecutionHostForWorktree['resolveTerminalWorkspaceLaunchTarget'] + > + ): ReturnType< + OrcaRuntimeWithResolveBrowserNetworkExecutionHostForWorktree['resolveTerminalWorkspaceLaunchTarget'] + > -export class OrcaRuntimeWithListManagedWorktrees extends OrcaRuntimeWithRestoreStructuredAgentSessionTabsOnce { listManagedWorktrees( repoSelector?: string, limit = DEFAULT_WORKTREE_LIST_LIMIT, @@ -84,7 +91,7 @@ export class OrcaRuntimeWithListManagedWorktrees extends OrcaRuntimeWithRestoreS knownWorktreeIds, detected.worktrees.map((worktree) => worktree.id), { - runtime: this as RuntimeCommandSurfaceHost, + runtime: this, getLocalProvider: () => this.getLocalProvider(), onPtyStopped: this.onPtyStopped ?? undefined } diff --git a/src/main/runtime/workspaces/workspace-activation.ts b/src/main/runtime/workspaces/workspace-activation.ts index 234212aecf8..c8678f7a054 100644 --- a/src/main/runtime/workspaces/workspace-activation.ts +++ b/src/main/runtime/workspaces/workspace-activation.ts @@ -1,4 +1,4 @@ -import { OrcaRuntimeWithListManagedWorktrees } from '../orca-runtime-list-managed-worktrees' +import { OrcaRuntimeWithListManagedWorktrees } from './managed-workspace-operations' import type { RuntimeNavigationTarget } from '../../../shared/runtime-navigation' import { navigationTargetsClients, diff --git a/src/main/runtime/orca-runtime-persist-headless-terminal-title.ts b/src/main/runtime/workspaces/workspace-file-session-targets.ts similarity index 79% rename from src/main/runtime/orca-runtime-persist-headless-terminal-title.ts rename to src/main/runtime/workspaces/workspace-file-session-targets.ts index 33bb80e240e..530746ee984 100644 --- a/src/main/runtime/orca-runtime-persist-headless-terminal-title.ts +++ b/src/main/runtime/workspaces/workspace-file-session-targets.ts @@ -1,23 +1,54 @@ -// @ts-nocheck -- mechanically split from OrcaRuntimeService; behavior is covered by AST equivalence and characterization tests. -import { RuntimeHeadlessTabGroups } from './session-tabs/headless-tab-group-moves' +import type { OrcaRuntimeWithScheduleMobileSessionTabsChanged } from '../session-tabs/session-tabs-change-publication' +import type { OrcaRuntimeWithResolveBrowserNetworkExecutionHostForWorktree } from '../terminal/launch/terminal-workspace-scope' +import type { OrcaRuntimeWithTransitionGraphReloadToTerminalState } from '../session-graph/session-graph-authority' +import { RuntimeHeadlessTabGroups } from '../session-tabs/headless-tab-group-moves' import type { + BrowserTabInfo, RuntimeMarkdownReadTabResult, RuntimeMarkdownSaveTabResult, RuntimeMobileSessionTabGroup, RuntimeMobileSessionTabsSnapshot -} from '../../shared/runtime-types' -import type { ResolvedWorktree } from './runtime-worktree-path-identity' -import type { Repo } from '../../shared/repo-types' +} from '../../../shared/runtime-types' +import type { ResolvedWorktree } from '../runtime-worktree-path-identity' +import type { Repo } from '../../../shared/repo-types' import { getConnectionExecutionHostId, LOCAL_EXECUTION_HOST_ID, type ExecutionHostId -} from '../../shared/execution-host' -import { getLocalProjectWorktreeGitOptions } from '../project-runtime-git-options' -import { resolveWorktreeHostRouting } from './worktree-launch-host-repo' -import { isFloatingWorkspaceSelector } from '../../shared/floating-workspace-worktree' +} from '../../../shared/execution-host' +import { getLocalProjectWorktreeGitOptions } from '../../project-runtime-git-options' +import { resolveWorktreeHostRouting } from '../worktree-launch-host-repo' +import { isFloatingWorkspaceSelector } from '../../../shared/floating-workspace-worktree' export abstract class OrcaRuntimeWithPersistHeadlessTerminalTitle extends RuntimeHeadlessTabGroups { + protected abstract getListedBrowserTabsByPageId(worktreeId: string): Map + protected abstract resolveMobileMarkdownWorktreeId( + ...args: Parameters< + OrcaRuntimeWithScheduleMobileSessionTabsChanged['resolveMobileMarkdownWorktreeId'] + > + ): ReturnType + protected abstract floatingWorkspaceToResolvedWorktree( + ...args: Parameters< + OrcaRuntimeWithResolveBrowserNetworkExecutionHostForWorktree['floatingWorkspaceToResolvedWorktree'] + > + ): ReturnType< + OrcaRuntimeWithResolveBrowserNetworkExecutionHostForWorktree['floatingWorkspaceToResolvedWorktree'] + > + protected abstract resolveFloatingWorkspacePath( + ...args: Parameters< + OrcaRuntimeWithResolveBrowserNetworkExecutionHostForWorktree['resolveFloatingWorkspacePath'] + > + ): ReturnType< + OrcaRuntimeWithResolveBrowserNetworkExecutionHostForWorktree['resolveFloatingWorkspacePath'] + > + protected abstract resolveFolderWorkspaceLaunchScope( + ...args: Parameters< + OrcaRuntimeWithTransitionGraphReloadToTerminalState['resolveFolderWorkspaceLaunchScope'] + > + ): ReturnType< + OrcaRuntimeWithTransitionGraphReloadToTerminalState['resolveFolderWorkspaceLaunchScope'] + > + // Persist a manual terminal rename so a headless rebuild keeps the title // instead of reverting to the generated/default one. protected persistHeadlessTerminalTitle( diff --git a/src/main/runtime/orca-runtime-notify-ssh-state-changed.ts b/src/main/runtime/workspaces/workspace-host-notifications.ts similarity index 75% rename from src/main/runtime/orca-runtime-notify-ssh-state-changed.ts rename to src/main/runtime/workspaces/workspace-host-notifications.ts index 264437219fb..8426f40b720 100644 --- a/src/main/runtime/orca-runtime-notify-ssh-state-changed.ts +++ b/src/main/runtime/workspaces/workspace-host-notifications.ts @@ -1,18 +1,46 @@ -// @ts-nocheck -- mechanically split from OrcaRuntimeService; behavior is covered by AST equivalence and characterization tests. -import { OrcaRuntimeWithGetStatus } from './orca-runtime-get-status' -import type { SshConnectionState } from '../../shared/ssh-types' -import { getPublicSshState } from './public-ssh-state' -import { splitWorktreeId } from '../../shared/worktree/id' -import type { CreateWorktreeResult } from '../../shared/worktree/create-types' -import type { WorktreeStartupLaunch } from '../../shared/worktree/launch-types' -import type { RuntimeNavigationTarget } from '../../shared/runtime-navigation' -import { navigationTargetsClients, navigationTargetsHost } from '../../shared/runtime-navigation' -import { toRuntimeActivateWorktreeEvent } from '../../shared/runtime-client-events' -import type { AgentBrowserBridge } from '../browser/agent-browser-bridge' -import type { BrowserBackend } from '../browser/browser-backend' -import type { EmulatorBridge } from '../emulator/emulator-bridge' +import type { OrcaRuntimeWithHydrateHeadlessMobileSessionTabsFromWorkspaceSession } from '../session-tabs/mobile-session-hydration' +import type { OrcaRuntimeWithBuildHeadlessMobileSessionBrowserTabs } from '../orca-runtime-build-headless-mobile-session-browser-tabs' +import type { OrcaRuntimeWithScheduleMobileSessionTabsChanged } from '../session-tabs/session-tabs-change-publication' +import type { RuntimeWorkspaceRepoScanRefresh } from './workspace-repo-scan-refresh' +import { OrcaRuntimeWithGetStatus } from '../orca-runtime-get-status' +import type { SshConnectionState } from '../../../shared/ssh-types' +import { getPublicSshState } from '../public-ssh-state' +import { splitWorktreeId } from '../../../shared/worktree/id' +import type { CreateWorktreeResult } from '../../../shared/worktree/create-types' +import type { WorktreeStartupLaunch } from '../../../shared/worktree/launch-types' +import type { RuntimeNavigationTarget } from '../../../shared/runtime-navigation' +import { navigationTargetsClients, navigationTargetsHost } from '../../../shared/runtime-navigation' +import { toRuntimeActivateWorktreeEvent } from '../../../shared/runtime-client-events' +import type { AgentBrowserBridge } from '../../browser/agent-browser-bridge' +import type { BrowserBackend } from '../../browser/browser-backend' +import type { EmulatorBridge } from '../../emulator/emulator-bridge' export abstract class OrcaRuntimeWithNotifySshStateChanged extends OrcaRuntimeWithGetStatus { + protected abstract hydrateHeadlessMobileSessionTabsFromWorkspaceSession( + ...args: Parameters< + OrcaRuntimeWithHydrateHeadlessMobileSessionTabsFromWorkspaceSession['hydrateHeadlessMobileSessionTabsFromWorkspaceSession'] + > + ): ReturnType< + OrcaRuntimeWithHydrateHeadlessMobileSessionTabsFromWorkspaceSession['hydrateHeadlessMobileSessionTabsFromWorkspaceSession'] + > + protected abstract refreshMobileSessionPtyRecords( + ...args: Parameters< + OrcaRuntimeWithBuildHeadlessMobileSessionBrowserTabs['refreshMobileSessionPtyRecords'] + > + ): ReturnType< + OrcaRuntimeWithBuildHeadlessMobileSessionBrowserTabs['refreshMobileSessionPtyRecords'] + > + protected abstract notifyMobileSessionTabsChangedNow( + ...args: Parameters< + OrcaRuntimeWithScheduleMobileSessionTabsChanged['notifyMobileSessionTabsChangedNow'] + > + ): ReturnType< + OrcaRuntimeWithScheduleMobileSessionTabsChanged['notifyMobileSessionTabsChangedNow'] + > + protected abstract invalidateSshWorktreeScanCacheInternal( + ...args: Parameters + ): ReturnType + abstract invalidateResolvedWorktreeCache(): void abstract invalidateWorktreeScanCacheForRepo(repoId: string): void diff --git a/src/main/runtime/workspaces/workspace-process-listing-dependencies.ts b/src/main/runtime/workspaces/workspace-process-listing-dependencies.ts new file mode 100644 index 00000000000..18132355fb1 --- /dev/null +++ b/src/main/runtime/workspaces/workspace-process-listing-dependencies.ts @@ -0,0 +1,49 @@ +import type { RuntimeWorkspaceCatalog } from './workspace-catalog' +import type { OrcaRuntimeWithRefreshPtyWorktreeRecordsFromController } from '../terminal/lifecycle/pty-worktree-refresh' +import type { RuntimeAccountController } from '../runtime-account-controller' +import type { OrcaRuntimeWithPruneMobileSessionTabGroupLayout } from '../orca-runtime-prune-mobile-session-tab-group-layout' +import type { OrcaRuntimeWithGetStructuredAgentSessionCreateSupport } from '../session-tabs/structured-session-create-support' +import type { OrcaRuntimeWithGetPtyRecordForPaneKey } from '../orchestration/terminal-pane-mail-routing' +import { OrcaRuntimeWithStartTuiIdleVisibleReadProbe } from '../terminal/wait/terminal-wait-visible-probe' + +/** Members the worktree listing reads from later runtime layers; declarations only. */ +export abstract class OrcaRuntimeWithWorktreePsDependencies extends OrcaRuntimeWithStartTuiIdleVisibleReadProbe { + protected abstract listResolvedWorktreeSnapshot( + ...args: Parameters + ): ReturnType + protected abstract refreshPtyWorktreeRecordsFromController( + ...args: Parameters< + OrcaRuntimeWithRefreshPtyWorktreeRecordsFromController['refreshPtyWorktreeRecordsFromController'] + > + ): ReturnType< + OrcaRuntimeWithRefreshPtyWorktreeRecordsFromController['refreshPtyWorktreeRecordsFromController'] + > + abstract notifyFolderWorkspaceChanged(): void + abstract notifyBranchRenamed(repoId: string): void + abstract notifyWorktreeFolderRenamed( + repoId: string, + oldWorktreeId: string, + newWorktreeId: string + ): void + declare getCommitMessageAgentEnvironmentResolvers: RuntimeAccountController['getCommitMessageAgentEnvironment'] + abstract refreshStructuredConversationTabTitle( + ...args: Parameters< + OrcaRuntimeWithPruneMobileSessionTabGroupLayout['refreshStructuredConversationTabTitle'] + > + ): ReturnType< + OrcaRuntimeWithPruneMobileSessionTabGroupLayout['refreshStructuredConversationTabTitle'] + > + abstract resolveStructuredAgentAccountHome( + ...args: Parameters< + OrcaRuntimeWithGetStructuredAgentSessionCreateSupport['resolveStructuredAgentAccountHome'] + > + ): ReturnType< + OrcaRuntimeWithGetStructuredAgentSessionCreateSupport['resolveStructuredAgentAccountHome'] + > + abstract onStructuredSessionStatusForMail( + ...args: Parameters + ): ReturnType + abstract onStructuredSessionTabHidden( + ...args: Parameters + ): ReturnType +} diff --git a/src/main/runtime/orca-runtime-get-worktree-ps.ts b/src/main/runtime/workspaces/workspace-process-listing.ts similarity index 78% rename from src/main/runtime/orca-runtime-get-worktree-ps.ts rename to src/main/runtime/workspaces/workspace-process-listing.ts index 7ae72c48b0b..d33788516dc 100644 --- a/src/main/runtime/orca-runtime-get-worktree-ps.ts +++ b/src/main/runtime/workspaces/workspace-process-listing.ts @@ -1,64 +1,65 @@ -// @ts-nocheck -- mechanically split from OrcaRuntimeService; behavior is covered by AST equivalence and characterization tests. -import { resolveStructuredCodexAccountKind } from './structured-agent-account-home' -import { collectRuntimeWorktreeAgentSources } from './runtime-worktree-agent-sources' -import { OrcaRuntimeWithStartTuiIdleVisibleReadProbe } from './terminal/wait/terminal-wait-visible-probe' -import { DEFAULT_WORKTREE_PS_LIMIT } from './orca-runtime-postlude' -import type { RuntimeWorktreePsResult } from '../../shared/runtime-types' -import { buildRuntimeWorktreePsSummaries } from './runtime-worktree-ps-summaries' +import { resolveStructuredCodexAccountKind } from '../structured-agent-account-home' +import { collectRuntimeWorktreeAgentSources } from '../runtime-worktree-agent-sources' +import { OrcaRuntimeWithWorktreePsDependencies } from './workspace-process-listing-dependencies' +import { DEFAULT_WORKTREE_PS_LIMIT } from '../orca-runtime-postlude' +import type { RuntimeWorktreePsResult } from '../../../shared/runtime-types' +import { buildRuntimeWorktreePsSummaries } from '../runtime-worktree-ps-summaries' import { buildRuntimeWorktreeSummaryPathIndex, createRuntimeWorktreePsSummaryLookup -} from './runtime-worktree-summary-paths' +} from '../runtime-worktree-summary-paths' import { applyRuntimeWorktreePsSessionActivity, applyRuntimeWorktreePsTerminalActivity -} from './runtime-worktree-ps-activity' -import { applyRuntimeWorktreePsUnverifiableTerminals } from './runtime-worktree-ps-unverifiable-terminals' -import { attachRuntimeWorktreeAgentRows } from './runtime-worktree-agent-rows' -import { compareWorktreePs } from './runtime-worktree-status-projection' -import type { Repo } from '../../shared/repo-types' -import { enrichMissingRepoGitRemoteIdentities } from '../repo-git-remote-identity-enrichment' -import { ensureStructuredAgentSessionHost as installStructuredAgentSessionHost } from './structured-agent-session-runtime' +} from '../runtime-worktree-ps-activity' +import { applyRuntimeWorktreePsUnverifiableTerminals } from '../runtime-worktree-ps-unverifiable-terminals' +import { attachRuntimeWorktreeAgentRows } from '../runtime-worktree-agent-rows' +import { compareWorktreePs } from '../runtime-worktree-status-projection' +import type { Repo } from '../../../shared/repo-types' +import type { StructuredAgentId } from '../../../shared/agent-session-provider-handle' +import type { GlobalSettings } from '../../../shared/global-settings-types' +import { enrichMissingRepoGitRemoteIdentities } from '../../repo-git-remote-identity-enrichment' +import { ensureStructuredAgentSessionHost as installStructuredAgentSessionHost } from '../structured-agent-session-runtime' import { createStructuredAttentionMobileDelivery, readStructuredAttentionWorkspaceLabels -} from './structured-agent-session-mobile-attention' +} from '../structured-agent-session-mobile-attention' import { createStructuredAgentSessionLogger, neverThrowingStructuredAgentSessionLogger -} from '../native-chat/agent-session-wire/structured-agent-session-logger' -import { openAgentSessionRecordStoreOnce } from './agent-session-record-store-slot' -import type { AgentSessionRecordStore } from './agent-session-record-store' -import { maybeAutoRenameWorkspaceOnFirstStructuredTurn } from '../agent-hooks/first-work-structured-session-rename' -import { firstWorkRenameDeps } from '../agent-hooks/first-work-rename-runtime' -import { createStructuredChatNamingHandler } from '../native-chat/structured-chat-naming' -import { structuredChatNamingDeps } from './structured-chat-naming-runtime' -import { getProfileUserDataPath } from '../orca-profiles/profile-storage-paths' -import { LOCAL_EXECUTION_HOST_ID } from '../../shared/execution-host' -import { buildWorktreeListingPage } from './worktree-listing-host-scope' +} from '../../native-chat/agent-session-wire/structured-agent-session-logger' +import { openAgentSessionRecordStoreOnce } from '../agent-session-record-store-slot' +import type { AgentSessionRecordStore } from '../agent-session-record-store' +import { maybeAutoRenameWorkspaceOnFirstStructuredTurn } from '../../agent-hooks/first-work-structured-session-rename' +import { firstWorkRenameDeps } from '../../agent-hooks/first-work-rename-runtime' +import { createStructuredChatNamingHandler } from '../../native-chat/structured-chat-naming' +import { structuredChatNamingDeps } from '../structured-chat-naming-runtime' +import { getProfileUserDataPath } from '../../orca-profiles/profile-storage-paths' +import { LOCAL_EXECUTION_HOST_ID } from '../../../shared/execution-host' +import { buildWorktreeListingPage } from '../worktree-listing-host-scope' import { structuredWorkerOpenTaskTitle, structuredWorkerOwesWork -} from './structured-worker-custody' +} from '../structured-worker-custody' import { resolvedTuiAgentArgsBypassPermissions, resolveTuiAgentLaunchEnv -} from '../../shared/tui-agent-launch-defaults' -import { isTuiAgent } from '../../shared/tui-agent-config' -import { nativeChatShellEnvironmentPolicy } from '../../shared/native-chat-shell-environment' -import { claudeStructuredAuthPolicyForSettings } from '../claude-accounts/claude-structured-auth-policy' -import { resolveStructuredAgentCommand } from '../native-chat/structured-agent-command-resolution' -import { structuredAgentConfiguredArgs } from '../native-chat/structured-agent-configured-args' +} from '../../../shared/tui-agent-launch-defaults' +import { isTuiAgent } from '../../../shared/tui-agent-config' +import { nativeChatShellEnvironmentPolicy } from '../../../shared/native-chat-shell-environment' +import { claudeStructuredAuthPolicyForSettings } from '../../claude-accounts/claude-structured-auth-policy' +import { resolveStructuredAgentCommand } from '../../native-chat/structured-agent-command-resolution' +import { structuredAgentConfiguredArgs } from '../../native-chat/structured-agent-configured-args' +import { claudeCliFlagSupport } from '../../claude/claude-cli-flag-support' import { runtimeHiddenHostIds, runtimeHiddenRepoIds, runtimeVisibleRepos, withoutHiddenHosts -} from './runtime-visible-catalog' -import { claudeCliFlagSupport } from '../claude/claude-cli-flag-support' -import { isStructuredAttentionWorkspaceHidden } from './structured-attention-hidden-workspace' +} from '../runtime-visible-catalog' +import { isStructuredAttentionWorkspaceHidden } from '../structured-attention-hidden-workspace' -export class OrcaRuntimeWithGetWorktreePs extends OrcaRuntimeWithStartTuiIdleVisibleReadProbe { +export abstract class OrcaRuntimeWithGetWorktreePs extends OrcaRuntimeWithWorktreePsDependencies { async getWorktreePs( limit = DEFAULT_WORKTREE_PS_LIMIT, sourceDefaultsSupported = true @@ -154,7 +155,10 @@ export class OrcaRuntimeWithGetWorktreePs extends OrcaRuntimeWithStartTuiIdleVis /** The agent's configured env, with the account a desktop that owns this host's accounts chose * laid over it, as its own launches here carry. */ - protected structuredAgentLaunchEnv(agent: StructuredAgentId, settings) { + protected structuredAgentLaunchEnv( + agent: StructuredAgentId, + settings: Pick + ) { return { ...resolveTuiAgentLaunchEnv(agent, settings.agentDefaultEnv), ...this.accounts.hostLaunchEnvFor(agent) diff --git a/src/main/runtime/workspaces/workspace-query-prototype.test.ts b/src/main/runtime/workspaces/workspace-query-prototype.test.ts index 601b601f2dc..c372a17891b 100644 --- a/src/main/runtime/workspaces/workspace-query-prototype.test.ts +++ b/src/main/runtime/workspaces/workspace-query-prototype.test.ts @@ -1,10 +1,23 @@ -import { describe, expect, it } from 'vitest' +import { describe, expect, it, vi } from 'vitest' import { OrcaRuntimeService } from '../orca-runtime-test-mocks.spec' import { store, TEST_WORKTREE_ID } from '../orca-runtime-test-fixtures.spec' import type { Repo } from '../../../shared/repo-types' import type { ExecutionHostId } from '../../../shared/execution-host' import type { ResolvedWorktree } from '../runtime-worktree-path-identity' import type { ResolvedWorktreeSnapshot } from '../runtime-resolved-worktree-cache' +import type { DetectedWorktreeListResult } from '../../../shared/worktree/types' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { + FLOATING_TERMINAL_WORKTREE_ID, + getDefaultWorkspaceSession +} from '../../../shared/constants' +import type { WorkspacePortExecutionHostDeps } from '../../ports/workspace-port-execution-host' +import type { WorkspacePortProbe } from '../../../shared/workspace-ports' +import type { BrowserTabInfo } from '../../../shared/runtime-browser-contracts' +import type { RuntimeMobileSessionTabsSnapshot } from '../../../shared/runtime-session-contracts' +import type { WorkspaceSessionState } from '../../../shared/workspace-session-state-types' +import { firstWorkRenameDeps } from '../../agent-hooks/first-work-rename-runtime' const repo: Repo = { id: 'workspace-query', @@ -192,3 +205,552 @@ describe('workspace query prototype dispatch', () => { expect(result.worktrees.map((row) => row.path)).toEqual(['/remote/stored-worktree']) }) }) + +describe('workspace-detected-listing-capture characterization', () => { + describe('detected workspace listing during repo resolution', () => { + it('captures the listing method before awaiting resolution and keeps its runtime receiver', async () => { + const pending = Promise.withResolvers() + const events: string[] = [] + const repo: Repo = { + id: 'repo', + path: '/repo', + displayName: 'repo', + badgeColor: 'blue', + addedAt: 0 + } + const result: DetectedWorktreeListResult = { + repoId: repo.id, + authoritative: true, + source: 'git', + worktrees: [] + } + const runtime = new (class extends OrcaRuntimeService { + override resolveRepoSelectorForConnection(selector: string, connectionId?: string | null) { + events.push('resolve') + expect([selector, connectionId]).toEqual(['repo', 'ssh:box']) + return pending.promise + } + })() + const original = vi.fn(function ( + this: OrcaRuntimeService, + resolved: Repo, + sourceDefaultsSupported?: boolean + ) { + events.push('invoke original') + expect(this).toBe(runtime) + expect([resolved, sourceDefaultsSupported]).toEqual([repo, false]) + return Promise.resolve(result) + }) + const replacement = vi.fn(() => Promise.resolve({ ...result, repoId: 'replacement' })) + let listing: ( + repo: Repo, + sourceDefaultsSupported?: boolean + ) => Promise = original + Object.defineProperty(runtime, 'listDetectedWorktreesForResolvedRepo', { + get: () => { + events.push('capture listing') + return listing + } + }) + + const listingPromise = runtime.listDetectedManagedWorktrees('repo', 'ssh:box', false) + listing = replacement + pending.resolve(repo) + + await expect(listingPromise).resolves.toBe(result) + expect(events).toEqual(['capture listing', 'resolve', 'invoke original']) + expect(original).toHaveBeenCalledOnce() + expect(replacement).not.toHaveBeenCalled() + }) + }) + + describe('workspace methods captured before request property reads', () => { + it('captures port probes before reading repoId', async () => { + const runtime = new OrcaRuntimeService() + const events: string[] = [] + const original = vi.fn(async () => []) + const replacement = vi.fn(async () => []) + let probes = original + Object.defineProperty(runtime, 'getWorkspacePortProbes', { + get: () => { + events.push('capture probes') + return probes + } + }) + await runtime.killWorkspacePort({ + pid: 4242, + port: 3020, + get repoId() { + events.push('read repoId') + probes = replacement + return 'repo' + } + }) + expect(events).toEqual(['capture probes', 'read repoId']) + expect(original).toHaveBeenCalledWith('repo') + expect(original.mock.contexts[0]).toBe(runtime) + expect(replacement).not.toHaveBeenCalled() + }) + + it('captures file routing before reading the requested workspace', async () => { + const runtime = new OrcaRuntimeService() + const events: string[] = [] + const original = vi.fn(async () => ({ executionHostId: 'ssh:box' })) + const replacement = vi.fn(async () => ({ executionHostId: 'ssh:box' })) + let routing = original + Object.defineProperty(runtime, 'resolveRuntimeFileTarget', { + get: () => { + events.push('capture routing') + return routing + } + }) + await runtime.killWorkspacePortOnHost({ + executionHostId: 'ssh:box', + pid: 4242, + port: 3020, + get worktree() { + events.push('read worktree') + routing = replacement + return 'folder:remote' + } + }) + expect(events).toEqual(['capture routing', 'read worktree']) + expect(original).toHaveBeenCalledWith('folder:remote') + expect(original.mock.contexts[0]).toBe(runtime) + expect(replacement).not.toHaveBeenCalled() + }) + + it('captures scan invalidation before reading the resolved repo id', async () => { + const runtime = new OrcaRuntimeService() + const events: string[] = [] + const original = vi.fn() + const replacement = vi.fn() + let invalidate = original + const repo: Repo = { + get id() { + events.push('read repo id') + invalidate = replacement + return 'repo' + }, + path: '/repo', + displayName: 'repo', + badgeColor: 'blue', + addedAt: 0 + } + Object.defineProperty(runtime, 'invalidateWorktreeScanCacheForRepo', { + get: () => { + events.push('capture invalidation') + return invalidate + } + }) + Object.defineProperty(runtime, 'resolveRepoSelectorForConnection', { + value: async () => repo + }) + const detected: DetectedWorktreeListResult = { + repoId: 'repo', + authoritative: false, + source: 'metadata-fallback', + worktrees: [] + } + Object.defineProperty(runtime, 'listDetectedWorktreesForResolvedRepo', { + value: async () => detected + }) + + await expect(runtime.teardownMissingManagedWorktreeTerminals('repo', [])).resolves.toEqual({ + stoppedWorktreeIds: [] + }) + expect(events).toEqual(['capture invalidation', 'read repo id']) + expect(original).toHaveBeenCalledWith('repo') + expect(original.mock.contexts[0]).toBe(runtime) + expect(replacement).not.toHaveBeenCalled() + }) + }) +}) + +describe('workspace-file-notification-construction characterization', () => { + const cases = [ + ['resolveRuntimeGitTarget', 'reject', 'runtime_unavailable'], + ['resolveRuntimeFileTarget', 'reject', 'runtime_unavailable'], + ['resolveKnownWorkspaceFileTarget', 'promise', null], + ['readMobileMarkdownTab', 'reject', 'tab_not_found'], + ['saveMobileMarkdownTab', 'reject', 'tab_not_found'], + ['resolveMobileMarkdownWorktreeId', 'reject', 'tab_not_found'], + ['invalidateSshWorktreeScanCache', 'sync', undefined], + ['notifyWorktreesChangedForRemoteClients', 'sync', undefined], + ['publishWorktreeRemovalChange', 'sync', undefined], + ['notifyWorktreeCatalogChangedForRemoteClients', 'sync', undefined], + ['notifyReposChangedForRemoteClients', 'sync', undefined], + ['notifyActivateWorktree', 'sync', undefined], + ['notifyClientsActivateWorktree', 'sync', undefined], + ['onMobileSessionTabsChanged', 'subscription', undefined] + ] as const + + type ProbeName = (typeof cases)[number][0] + let probeName: ProbeName = 'notifyReposChangedForRemoteClients' + let result: unknown + let synchronousFailure: unknown + let unsubscribe: (() => void) | undefined + let seamCalls = 0 + + class ConstructorProbe extends OrcaRuntimeService { + protected override get canonicalFetchKeyCache() { + seamCalls += 1 + try { + result = this.inspectDuringConstruction(probeName) + } catch (error) { + synchronousFailure = error + } + return super.canonicalFetchKeyCache + } + + listenerCount(): number { + return this.mobileSessionTabListeners.size + } + + private inspectDuringConstruction(name: ProbeName): unknown { + switch (name) { + case 'resolveRuntimeGitTarget': + return this.resolveRuntimeGitTarget('id:folder-proof') + case 'resolveRuntimeFileTarget': + return this.resolveRuntimeFileTarget('id:folder-proof') + case 'resolveKnownWorkspaceFileTarget': + return this.resolveKnownWorkspaceFileTarget(join(tmpdir(), 'file-proof'), 'local') + case 'readMobileMarkdownTab': + return this.readMobileMarkdownTab('id:folder-proof', 'tab-proof') + case 'saveMobileMarkdownTab': + return this.saveMobileMarkdownTab( + 'id:folder-proof', + 'tab-proof', + 'version-proof', + 'content' + ) + case 'resolveMobileMarkdownWorktreeId': + return this.resolveMobileMarkdownWorktreeId('id:folder-proof', 'tab-proof') + case 'invalidateSshWorktreeScanCache': + return this.invalidateSshWorktreeScanCache('host-proof') + case 'notifyWorktreesChangedForRemoteClients': + return this.notifyWorktreesChangedForRemoteClients('repo-proof') + case 'publishWorktreeRemovalChange': + return this.publishWorktreeRemovalChange('repo-proof') + case 'notifyWorktreeCatalogChangedForRemoteClients': + return this.notifyWorktreeCatalogChangedForRemoteClients('repo-proof') + case 'notifyReposChangedForRemoteClients': + return this.notifyReposChangedForRemoteClients() + case 'notifyActivateWorktree': + return this.notifyActivateWorktree('repo-proof', 'folder-proof') + case 'notifyClientsActivateWorktree': + return this.notifyClientsActivateWorktree('repo-proof', 'folder-proof') + case 'onMobileSessionTabsChanged': + unsubscribe = this.onMobileSessionTabsChanged(() => undefined, 'constructor-proof') + return unsubscribe + } + } + } + + describe('workspace file notifications during runtime construction', () => { + it.each(cases)('preserves %s before later fields initialize', async (name, phase, expected) => { + probeName = name + result = undefined + synchronousFailure = undefined + seamCalls = 0 + const runtime = new ConstructorProbe(null) + expect(seamCalls).toBe(1) + expect(synchronousFailure).toBeUndefined() + if (phase === 'sync') { + expect(result).not.toBeInstanceOf(Promise) + expect(result).toEqual(expected) + return + } + if (phase === 'subscription') { + expect(typeof result).toBe('function') + expect(runtime.listenerCount()).toBe(1) + unsubscribe?.() + expect(runtime.listenerCount()).toBe(0) + return + } + expect(result).toBeInstanceOf(Promise) + await (phase === 'reject' + ? expect(result).rejects.toThrow(expected) + : expect(result).resolves.toEqual(expected)) + }) + }) +}) + +describe('workspace-file-target-capture characterization', () => { + describe('floating workspace file target resolution', () => { + it('captures the converter before awaiting the path and keeps its runtime receiver', async () => { + const runtime = new OrcaRuntimeService() + const pending = Promise.withResolvers() + const events: string[] = [] + const convert = runtime['floatingWorkspaceToResolvedWorktree'] + const original = vi.fn(function (this: OrcaRuntimeService, path: string) { + events.push('invoke original') + expect(this).toBe(runtime) + return convert.call(this, path) + }) + const replacement = vi.fn((path: string) => convert.call(runtime, path)) + let converter: typeof convert = original + Object.defineProperty(runtime, 'floatingWorkspaceToResolvedWorktree', { + get: () => { + events.push('capture converter') + return converter + } + }) + Object.defineProperty(runtime, 'resolveFloatingWorkspacePath', { + value: () => { + events.push('resolve path') + return pending.promise + } + }) + const result = runtime['resolveRuntimeFileTarget'](FLOATING_TERMINAL_WORKTREE_ID) + converter = replacement + const path = join(tmpdir(), 'floating') + pending.resolve(path) + + await expect(result).resolves.toMatchObject({ + worktree: { path }, + executionHostId: 'local' + }) + expect(events).toEqual(['capture converter', 'resolve path', 'invoke original']) + expect(original).toHaveBeenCalledWith(path) + expect(replacement).not.toHaveBeenCalled() + }) + }) +}) + +describe('workspace-host-ports-construction characterization', () => { + describe('workspace ports during runtime construction', () => { + it('creates lazy port capabilities before later constructor fields initialize', async () => { + let ports: WorkspacePortExecutionHostDeps | undefined + class ConstructionRuntime extends OrcaRuntimeService { + protected override get canonicalFetchKeyCache(): ReadonlyMap { + ports = this.getWorkspacePortHostDeps() + return super.canonicalFetchKeyCache + } + } + expect(() => new ConstructionRuntime(null)).not.toThrow() + expect(Object.keys(ports ?? {})).toEqual(['getLocalProbes']) + await expect(ports?.getLocalProbes()).rejects.toThrow('runtime_unavailable') + }) + + it('keeps the original asynchronous missing-store refusal during construction', async () => { + let probes: Promise | undefined + class ConstructionRuntime extends OrcaRuntimeService { + protected override get canonicalFetchKeyCache(): ReadonlyMap { + probes = this.getWorkspacePortProbes() + return super.canonicalFetchKeyCache + } + } + expect(() => new ConstructionRuntime(null)).not.toThrow() + await expect(probes).rejects.toThrow('runtime_unavailable') + }) + }) +}) + +describe('workspace-session-read-order characterization', () => { + const WORKTREE_ID = 'repo::/workspace' + + describe('workspace session callable lookup order', () => { + it('keeps the native refusal for a launcher without a workspace selector', async () => { + const runtime = new OrcaRuntimeService(null) + await expect( + runtime.resolveOrchestrationAgentLauncherForTarget('missing-launcher', {}) + ).rejects.toBeInstanceOf(TypeError) + }) + + it('captures the browser reader before evaluating the snapshot workspace getter', () => { + const runtime = new OrcaRuntimeService(null) + const order: string[] = [] + const replacement = vi.fn(() => new Map()) + const original = function ( + this: InstanceType, + worktreeId: string + ) { + expect(this).toBe(runtime) + expect(worktreeId).toBe(WORKTREE_ID) + order.push('original call') + return new Map() + } + Object.defineProperty(runtime, 'getListedBrowserTabsByPageId', { + configurable: true, + get: () => { + order.push('reader lookup') + return original + } + }) + const snapshot: RuntimeMobileSessionTabsSnapshot = { + worktree: WORKTREE_ID, + publicationEpoch: 'seed', + snapshotVersion: 1, + tabs: [], + activeGroupId: null, + activeTabId: null, + activeTabType: null + } + Object.defineProperty(snapshot, 'worktree', { + get: () => { + order.push('argument lookup') + Object.defineProperty(runtime, 'getListedBrowserTabsByPageId', { value: replacement }) + return WORKTREE_ID + } + }) + expect(runtime['collectPublicMobileSessionTabIds'](snapshot)).toEqual(new Set()) + expect(order).toEqual(['reader lookup', 'argument lookup', 'original call']) + expect(replacement).not.toHaveBeenCalled() + }) + + it('captures the title writer before constructing a session with getter-backed fields', () => { + const runtime = new OrcaRuntimeService(null) + const order: string[] = [] + const replacement = vi.fn() + const original = function ( + this: InstanceType, + worktreeId: string, + session: WorkspaceSessionState + ) { + expect(this).toBe(runtime) + expect(worktreeId).toBe(WORKTREE_ID) + expect(session.tabsByWorktree[WORKTREE_ID]?.[0]?.customTitle).toBe('renamed') + order.push('original write') + } + const session = getDefaultWorkspaceSession() + const tabs: WorkspaceSessionState['tabsByWorktree'] = { + [WORKTREE_ID]: [ + { + id: 'tab', + ptyId: null, + worktreeId: WORKTREE_ID, + title: 'Terminal', + customTitle: null, + color: null, + sortOrder: 0, + createdAt: 1 + } + ] + } + let tabReads = 0 + Object.defineProperty(session, 'tabsByWorktree', { + enumerable: true, + get: () => { + tabReads += 1 + if (tabReads === 2) { + order.push('session spread') + Object.defineProperty(runtime, 'setWorkspaceSessionForWorktree', { value: replacement }) + } + return tabs + } + }) + Object.defineProperty(runtime, 'store', { value: { setWorkspaceSession: () => {} } }) + Object.defineProperty(runtime, 'getWorkspaceSessionForWorktree', { value: () => session }) + Object.defineProperty(runtime, 'setWorkspaceSessionForWorktree', { + configurable: true, + get: () => { + order.push('writer lookup') + return original + } + }) + runtime['persistHeadlessTerminalTitle'](WORKTREE_ID, 'tab', 'renamed') + expect(order).toEqual(['writer lookup', 'session spread', 'original write']) + expect(replacement).not.toHaveBeenCalled() + }) + }) +}) + +describe('first-work rename lazy Store capabilities', () => { + type RenameStore = Parameters[0] + type RenameDeps = ReturnType + + function renameStore(): RenameStore { + return { + getSettings: () => ({ + workspaceDir: '/workspace', + nestWorkspaces: false, + refreshLocalBaseRefOnWorktreeCreate: false, + branchPrefix: 'none', + branchPrefixCustom: '' + }), + getRepo: () => undefined, + getWorktreeMeta: () => undefined, + setWorktreeMeta: vi.fn() + } + } + + const missingCalls: { + name: string + invoke: (deps: RenameDeps) => unknown + provideFolder?: true + }[] = [ + { name: 'display name', invoke: (deps) => deps.getCurrentDisplayName('folder:sample') }, + { name: 'folder path', invoke: (deps) => deps.getFolderWorkspacePath?.('folder:sample') }, + { + name: 'pending rename', + invoke: (deps) => deps.isPendingFirstAgentMessageRename?.('folder:sample') + }, + { name: 'display write', invoke: (deps) => deps.setDisplayName('folder:sample', 'New') }, + { name: 'error read', invoke: (deps) => deps.setRenameError('folder:sample', 'Failure') }, + { + name: 'error write', + provideFolder: true, + invoke: (deps) => deps.setRenameError('folder:sample', 'Failure') + }, + { name: 'tab owner', invoke: (deps) => deps.resolveWorktreeIdForTab('tab') } + ] + + it.each(missingCalls)( + 'preserves the direct missing $name capability failure', + ({ invoke, provideFolder }) => { + const store = renameStore() + if (provideFolder) { + store.getFolderWorkspace = () => undefined + } + const runtime = new OrcaRuntimeService(null) + let notified = false + runtime.notifyFolderWorkspaceChanged = () => { + notified = true + } + const deps = firstWorkRenameDeps(store, runtime) + expect(() => invoke(deps)).toThrow(TypeError) + expect(notified).toBe(false) + expect(deps.renameWorktreeFolder).toBeUndefined() + } + ) + + it('looks up the current writer on each call and retains Store and Root receivers', () => { + const store = renameStore() + const runtime = new OrcaRuntimeService(null) + const order: string[] = [] + runtime.notifyFolderWorkspaceChanged = function () { + expect(this).toBe(runtime) + order.push('notify') + } + const deps = firstWorkRenameDeps(store, runtime) + const write: NonNullable = function ( + this: RenameStore, + id, + updates + ) { + expect(this).toBe(store) + expect(arguments).toHaveLength(2) + expect(id).toBe('sample') + expect(updates).toEqual({ + name: 'New', + pendingFirstAgentMessageRename: false, + firstAgentMessageRenameError: null + }) + order.push('write') + return null + } + Object.defineProperty(store, 'updateFolderWorkspace', { + configurable: true, + get() { + order.push('lookup') + return write + } + }) + deps.setDisplayName('folder:sample', 'New') + expect(order).toEqual(['lookup', 'write', 'notify']) + delete store.updateFolderWorkspace + expect(() => deps.setDisplayName('folder:sample', 'Again')).toThrow(TypeError) + expect(order).toEqual(['lookup', 'write', 'notify']) + }) +}) diff --git a/src/main/text-generation/commit-message-text-generation.ts b/src/main/text-generation/commit-message-text-generation.ts index 05e611b97d6..dcf399e7841 100644 --- a/src/main/text-generation/commit-message-text-generation.ts +++ b/src/main/text-generation/commit-message-text-generation.ts @@ -3,7 +3,6 @@ import type { ConversationNameContext } from '../../shared/conversation-name-gen import type { CommitMessageDraftContext } from '../../shared/commit-message-generation' import { LOCAL_COMMIT_MESSAGE_HOST_KEY } from '../../shared/commit-message-host-key' import type { CommandTemplateBackslash } from '../../shared/commit-message-prompt' -import type { GlobalSettings } from '../../shared/global-settings-types' import type { GeneratedPullRequestFields, PullRequestDraftContext @@ -63,7 +62,7 @@ export function trimGeneratedCommitMessage(message: string): string { } export function resolveCommitMessageSettings( - settings: GlobalSettings, + settings: Parameters[0]['settings'], discoveryHostKey = LOCAL_COMMIT_MESSAGE_HOST_KEY, operation: AiTextOperation = 'commitMessage', repo?: Pick | null @@ -78,7 +77,7 @@ export function resolveCommitMessageSettings( } export function resolveTextGenerationParams( - settings: GlobalSettings, + settings: Parameters[0]['settings'], discoveryHostKey = LOCAL_COMMIT_MESSAGE_HOST_KEY, operation: AiTextOperation = 'commitMessage', repo?: Pick | null diff --git a/src/shared/branch-prefix.test.ts b/src/shared/branch-prefix.test.ts index 270a1fbfda8..f22613367cb 100644 --- a/src/shared/branch-prefix.test.ts +++ b/src/shared/branch-prefix.test.ts @@ -143,3 +143,7 @@ describe('assertBranchPrefixValid', () => { ) }) }) + +it('keeps an unknown prefix strategy as the original implicit undefined result', () => { + expect(selectBranchPrefixInput({ branchPrefix: 'future-prefix' }, 'user')).toBeUndefined() +}) diff --git a/src/shared/branch-prefix.ts b/src/shared/branch-prefix.ts index 1c2abf92276..004fe33e17c 100644 --- a/src/shared/branch-prefix.ts +++ b/src/shared/branch-prefix.ts @@ -12,9 +12,9 @@ export type BranchPrefixSettings = { * the renderer's live settings feedback agree on which field each strategy uses. */ export function selectBranchPrefixInput( - settings: BranchPrefixSettings, + settings: Omit & { branchPrefix: string }, gitUsername: string | null -): string | null { +): string | null | void { switch (settings.branchPrefix) { case 'git-username': return gitUsername diff --git a/src/shared/source-control-ai.ts b/src/shared/source-control-ai.ts index f40af9e6a10..470f2c25aac 100644 --- a/src/shared/source-control-ai.ts +++ b/src/shared/source-control-ai.ts @@ -64,11 +64,8 @@ export type ResolveSourceControlAiResult = | { ok: false; error: string } type ResolveSourceControlAiInput = { - settings: Pick< - GlobalSettings, - 'defaultTuiAgent' | 'agentCmdOverrides' | 'commitMessageAi' | 'sourceControlAi' - > & - Partial> + settings: Pick & + Partial> repo?: Pick | null operation: AiTextOperation discoveryHostKey?: string diff --git a/src/shared/tui-agent-launch-defaults.ts b/src/shared/tui-agent-launch-defaults.ts index 8230e30ff9f..a8cc8b4e436 100644 --- a/src/shared/tui-agent-launch-defaults.ts +++ b/src/shared/tui-agent-launch-defaults.ts @@ -14,7 +14,8 @@ const UNSUPPORTED_TUI_AGENT_ARGS: Partial> = kilo: ['--dangerously-skip-permissions'] } -export const DEFAULT_TUI_AGENT_ARGS: Partial> = YOLO_TUI_AGENT_ARGS +export const DEFAULT_TUI_AGENT_ARGS: Partial> & + Readonly> = YOLO_TUI_AGENT_ARGS export const DEFAULT_TUI_AGENT_ENV: Partial>> = YOLO_TUI_AGENT_ENV @@ -107,7 +108,7 @@ export function normalizeTuiAgentEnvRecord( return normalized } -export function getTuiAgentDefaultArgs(agent: TuiAgent): string { +export function getTuiAgentDefaultArgs(agent: string): string { return DEFAULT_TUI_AGENT_ARGS[agent] ?? '' } @@ -116,8 +117,8 @@ export function getTuiAgentDefaultEnv(agent: string): Record { } export function resolveTuiAgentLaunchArgs( - agent: TuiAgent, - configuredArgs: Partial> | null | undefined + agent: string, + configuredArgs: Readonly> | null | undefined ): string { if ( configuredArgs &&