mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
refactor(runtime): check workspace host, file and persistence boundaries (#27945)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -44,7 +44,9 @@ export type FirstWorkBranchRenameEvent = {
|
||||
}
|
||||
|
||||
export type FirstWorkBranchRenameDeps = {
|
||||
getSettings: () => GlobalSettings
|
||||
getSettings: () => Parameters<typeof resolveTextGenerationParams>[0] &
|
||||
Parameters<typeof getConfiguredBranchPrefix>[0] &
|
||||
Partial<Pick<GlobalSettings, 'autoRenameBranchFromWork'>>
|
||||
getRepo: (repoId: string) => Repo | undefined
|
||||
getAgentEnvResolvers: () => CommitMessageAgentEnvironmentResolvers | undefined
|
||||
/** Current sidebar display name for the worktree, if one is stored. */
|
||||
|
||||
@@ -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<typeof planWorktreeFolderRename>[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
|
||||
|
||||
@@ -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<Pick<Store, 'migrateWorktreeIdentity' | 'getWorktreeIdForTab'>>,
|
||||
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()
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -24,7 +24,17 @@ export function applyClaudeEnvPatch(
|
||||
baseEnv: Record<string, string>,
|
||||
patch: ClaudeEnvPatch,
|
||||
options?: { stripAuthEnv?: boolean; platform?: NodeJS.Platform }
|
||||
): Record<string, string> {
|
||||
): Record<string, string>
|
||||
export function applyClaudeEnvPatch(
|
||||
baseEnv: Record<string, string | undefined>,
|
||||
patch: ClaudeEnvPatch,
|
||||
options?: { stripAuthEnv?: boolean; platform?: NodeJS.Platform }
|
||||
): Record<string, string | undefined>
|
||||
export function applyClaudeEnvPatch(
|
||||
baseEnv: Record<string, string | undefined>,
|
||||
patch: ClaudeEnvPatch,
|
||||
options?: { stripAuthEnv?: boolean; platform?: NodeJS.Platform }
|
||||
): Record<string, string | undefined> {
|
||||
if (options?.stripAuthEnv) {
|
||||
for (const key of CLAUDE_AUTH_ENV_VARS) {
|
||||
delete baseEnv[key]
|
||||
|
||||
@@ -27,9 +27,8 @@ export function normalizeClaudeAccountSelectionTarget(
|
||||
}
|
||||
|
||||
export function normalizeClaudeRuntimeSelection(
|
||||
settings: Pick<
|
||||
GlobalSettings,
|
||||
'activeClaudeManagedAccountId' | 'activeClaudeManagedAccountIdsByRuntime'
|
||||
settings: Partial<
|
||||
Pick<GlobalSettings, 'activeClaudeManagedAccountId' | 'activeClaudeManagedAccountIdsByRuntime'>
|
||||
>
|
||||
): ClaudeManagedAccountRuntimeSelection {
|
||||
return {
|
||||
@@ -42,9 +41,8 @@ export function normalizeClaudeRuntimeSelection(
|
||||
}
|
||||
|
||||
export function getSelectedClaudeAccountIdForTarget(
|
||||
settings: Pick<
|
||||
GlobalSettings,
|
||||
'activeClaudeManagedAccountId' | 'activeClaudeManagedAccountIdsByRuntime'
|
||||
settings: Partial<
|
||||
Pick<GlobalSettings, 'activeClaudeManagedAccountId' | 'activeClaudeManagedAccountIdsByRuntime'>
|
||||
>,
|
||||
target?: ClaudeAccountSelectionTarget | null
|
||||
): string | null {
|
||||
|
||||
@@ -22,9 +22,8 @@ export type {
|
||||
} from '../../shared/codex-selection-lane'
|
||||
|
||||
export function normalizeCodexRuntimeSelection(
|
||||
settings: Pick<
|
||||
GlobalSettings,
|
||||
'activeCodexManagedAccountId' | 'activeCodexManagedAccountIdsByRuntime'
|
||||
settings: Partial<
|
||||
Pick<GlobalSettings, 'activeCodexManagedAccountId' | 'activeCodexManagedAccountIdsByRuntime'>
|
||||
>
|
||||
): CodexManagedAccountRuntimeSelection {
|
||||
return {
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
* check-ref-format rejects.
|
||||
*/
|
||||
export function getConfiguredBranchPrefix(
|
||||
settings: BranchPrefixSettings,
|
||||
settings: Parameters<typeof selectBranchPrefixInput>[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<typeof selectBranchPrefixInput>[0],
|
||||
gitUsername: string | null
|
||||
): string {
|
||||
const prefix = getConfiguredBranchPrefix(settings, gitUsername)
|
||||
|
||||
@@ -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([])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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<Pick<GlobalSettings, 'agentDefaultArgs' | 'terminalWindowsShell'>>,
|
||||
platform: NodeJS.Platform = process.platform
|
||||
): string[] {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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())
|
||||
|
||||
|
||||
@@ -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<ReturnType<typeof placeWorkerAgent>>['worktree'] | undefined =
|
||||
creationWorktree
|
||||
? undefined
|
||||
: requestedWorktree === 'current'
|
||||
? await runtime.showManagedTerminalWorkspace(`id:${coordinatorWorktreeId}`)
|
||||
: await runtime.showManagedTerminalWorkspace(requestedWorktree)
|
||||
if (terminal) {
|
||||
await assertExplicitWorkerUsable({
|
||||
runtime,
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export type WorkerStartMutation = {
|
||||
callerFingerprint: string
|
||||
requestId: string
|
||||
method: string
|
||||
payloadHash: string
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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<typeof OrcaRuntimeService>
|
||||
|
||||
const WORKTREE_ID = 'repo::/workspace'
|
||||
|
||||
function constructRuntime(observe: (runtime: Runtime) => void): Runtime {
|
||||
class ConstructorObservedRuntime extends OrcaRuntimeService {
|
||||
protected override get canonicalFetchKeyCache(): ReadonlyMap<string, string> {
|
||||
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<unknown> | 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<unknown> | 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<unknown> | 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<string> | 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<RuntimeStore, 'getWorktreeMeta' | 'removeWorktreeMeta' | 'getRepos'> = {
|
||||
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<typeof OrcaRuntimeService>): Promise<void> {
|
||||
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<Value>(
|
||||
mutate: () => DurableProfileStateMutation<Value>
|
||||
): Promise<Value> {
|
||||
expect(this).toBe(reservation)
|
||||
const deferred = Promise.withResolvers<Value>()
|
||||
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<Value>(
|
||||
mutate: () => DurableProfileStateMutation<Value>
|
||||
): Promise<Value> {
|
||||
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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
+5
-6
@@ -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,
|
||||
@@ -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'
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 =>
|
||||
|
||||
@@ -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([])
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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<Store, 'getSettings'>,
|
||||
getStore: () => {
|
||||
getSettings: () => Parameters<typeof resolveTextGenerationParams>[0] &
|
||||
ReturnType<StructuredChatNamingDeps['getSettings']>
|
||||
},
|
||||
runtime: StructuredChatNamingRuntime,
|
||||
logger: StructuredAgentSessionLogger
|
||||
): StructuredChatNamingDeps {
|
||||
|
||||
@@ -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<typeof runtime.showManagedTerminalWorkspace>
|
||||
) => Promise<Awaited<ReturnType<typeof placeWorkerAgent>>['worktree']>
|
||||
}
|
||||
|
||||
const WORKER = formatOrcaSessionAddress(testOrcaSessionId(PEER_CHAT))
|
||||
|
||||
beforeEach(() => {
|
||||
vi.spyOn(runtime, 'showManagedTerminalWorkspace').mockImplementation(async (selector) => ({
|
||||
vi.spyOn<WorkspaceLookup, 'showManagedTerminalWorkspace'>(
|
||||
runtime,
|
||||
'showManagedTerminalWorkspace'
|
||||
).mockImplementation(async (selector) => ({
|
||||
id: selector.replace(/^id:/, ''),
|
||||
repoId: 'repo'
|
||||
}))
|
||||
|
||||
+15
-9
@@ -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<OrcaRuntimeWithPruneMobileSessionTabGroupLayout['getTerminalHandleForPaneKey']>
|
||||
|
||||
export class OrcaRuntimeWithResolveTerminalSplitSourceAuthority extends OrcaRuntimeWithSplitPtyBackedTerminal {
|
||||
protected resolveTerminalSplitSourceAuthority(
|
||||
worktreeId: string,
|
||||
tabId: string,
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
+50
-9
@@ -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<RuntimeWorkspaceCatalog['listResolvedWorktrees']>
|
||||
): ReturnType<RuntimeWorkspaceCatalog['listResolvedWorktrees']>
|
||||
protected abstract buildRuntimeVisibilitySourceMatchersByRepoId(
|
||||
...args: Parameters<
|
||||
OrcaRuntimeWithListManagedWorktrees['buildRuntimeVisibilitySourceMatchersByRepoId']
|
||||
>
|
||||
): ReturnType<OrcaRuntimeWithListManagedWorktrees['buildRuntimeVisibilitySourceMatchersByRepoId']>
|
||||
protected abstract isRuntimeWorktreeVisible(
|
||||
...args: Parameters<OrcaRuntimeWithListManagedWorktrees['isRuntimeWorktreeVisible']>
|
||||
): ReturnType<OrcaRuntimeWithListManagedWorktrees['isRuntimeWorktreeVisible']>
|
||||
protected abstract resolveFolderWorkspaceConnectionId(
|
||||
...args: Parameters<
|
||||
OrcaRuntimeWithTransitionGraphReloadToTerminalState['resolveFolderWorkspaceConnectionId']
|
||||
>
|
||||
): ReturnType<
|
||||
OrcaRuntimeWithTransitionGraphReloadToTerminalState['resolveFolderWorkspaceConnectionId']
|
||||
>
|
||||
protected abstract invalidatePtyIncarnationHandle(
|
||||
...args: Parameters<OrcaRuntimeWithBindPtyIncarnationHandle['invalidatePtyIncarnationHandle']>
|
||||
): ReturnType<OrcaRuntimeWithBindPtyIncarnationHandle['invalidatePtyIncarnationHandle']>
|
||||
protected abstract invalidateAllHandlesForPty(
|
||||
...args: Parameters<OrcaRuntimeWithInvalidateAllHandlesForPty['invalidateAllHandlesForPty']>
|
||||
): ReturnType<OrcaRuntimeWithInvalidateAllHandlesForPty['invalidateAllHandlesForPty']>
|
||||
protected abstract isTerminalHandleAdoptionBlocked(
|
||||
...args: Parameters<
|
||||
OrcaRuntimeWithInvalidateAllHandlesForPty['isTerminalHandleAdoptionBlocked']
|
||||
>
|
||||
): ReturnType<OrcaRuntimeWithInvalidateAllHandlesForPty['isTerminalHandleAdoptionBlocked']>
|
||||
protected abstract replaceSyntheticTerminalHandlesForRestoredPty(
|
||||
...args: Parameters<
|
||||
OrcaRuntimeWithInvalidateAllHandlesForPty['replaceSyntheticTerminalHandlesForRestoredPty']
|
||||
>
|
||||
): ReturnType<
|
||||
OrcaRuntimeWithInvalidateAllHandlesForPty['replaceSyntheticTerminalHandlesForRestoredPty']
|
||||
>
|
||||
|
||||
export class OrcaRuntimeWithResolveKnownWorkspaceFileTarget extends OrcaRuntimeWithPersistHeadlessTerminalTitle {
|
||||
protected async resolveKnownWorkspaceFileTarget(
|
||||
absolutePath: string,
|
||||
executionHostId: ExecutionHostId
|
||||
+26
-19
@@ -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<this>,
|
||||
runtime: this,
|
||||
getLocalProvider: () => this.getLocalProvider(),
|
||||
onPtyStopped: this.onPtyStopped ?? undefined
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
+40
-9
@@ -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<string, BrowserTabInfo>
|
||||
protected abstract resolveMobileMarkdownWorktreeId(
|
||||
...args: Parameters<
|
||||
OrcaRuntimeWithScheduleMobileSessionTabsChanged['resolveMobileMarkdownWorktreeId']
|
||||
>
|
||||
): ReturnType<OrcaRuntimeWithScheduleMobileSessionTabsChanged['resolveMobileMarkdownWorktreeId']>
|
||||
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(
|
||||
+41
-13
@@ -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<RuntimeWorkspaceRepoScanRefresh['invalidateSshWorktreeScanCacheInternal']>
|
||||
): ReturnType<RuntimeWorkspaceRepoScanRefresh['invalidateSshWorktreeScanCacheInternal']>
|
||||
|
||||
abstract invalidateResolvedWorktreeCache(): void
|
||||
abstract invalidateWorktreeScanCacheForRepo(repoId: string): void
|
||||
|
||||
@@ -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<RuntimeWorkspaceCatalog['listResolvedWorktreeSnapshot']>
|
||||
): ReturnType<RuntimeWorkspaceCatalog['listResolvedWorktreeSnapshot']>
|
||||
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<OrcaRuntimeWithGetPtyRecordForPaneKey['onStructuredSessionStatusForMail']>
|
||||
): ReturnType<OrcaRuntimeWithGetPtyRecordForPaneKey['onStructuredSessionStatusForMail']>
|
||||
abstract onStructuredSessionTabHidden(
|
||||
...args: Parameters<OrcaRuntimeWithGetPtyRecordForPaneKey['onStructuredSessionTabHidden']>
|
||||
): ReturnType<OrcaRuntimeWithGetPtyRecordForPaneKey['onStructuredSessionTabHidden']>
|
||||
}
|
||||
+42
-38
@@ -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<GlobalSettings, 'agentDefaultEnv'>
|
||||
) {
|
||||
return {
|
||||
...resolveTuiAgentLaunchEnv(agent, settings.agentDefaultEnv),
|
||||
...this.accounts.hostLaunchEnvFor(agent)
|
||||
@@ -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<Repo>()
|
||||
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<DetectedWorktreeListResult> = 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<string>()
|
||||
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<string, string> {
|
||||
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<WorkspacePortProbe[]> | undefined
|
||||
class ConstructionRuntime extends OrcaRuntimeService {
|
||||
protected override get canonicalFetchKeyCache(): ReadonlyMap<string, string> {
|
||||
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<string, BrowserTabInfo>())
|
||||
const original = function (
|
||||
this: InstanceType<typeof OrcaRuntimeService>,
|
||||
worktreeId: string
|
||||
) {
|
||||
expect(this).toBe(runtime)
|
||||
expect(worktreeId).toBe(WORKTREE_ID)
|
||||
order.push('original call')
|
||||
return new Map<string, BrowserTabInfo>()
|
||||
}
|
||||
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<typeof OrcaRuntimeService>,
|
||||
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<typeof firstWorkRenameDeps>[0]
|
||||
type RenameDeps = ReturnType<typeof firstWorkRenameDeps>
|
||||
|
||||
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<RenameStore['updateFolderWorkspace']> = 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'])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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<typeof resolveSourceControlAiForOperation>[0]['settings'],
|
||||
discoveryHostKey = LOCAL_COMMIT_MESSAGE_HOST_KEY,
|
||||
operation: AiTextOperation = 'commitMessage',
|
||||
repo?: Pick<Repo, 'sourceControlAi'> | null
|
||||
@@ -78,7 +77,7 @@ export function resolveCommitMessageSettings(
|
||||
}
|
||||
|
||||
export function resolveTextGenerationParams(
|
||||
settings: GlobalSettings,
|
||||
settings: Parameters<typeof resolveSourceControlAiForOperation>[0]['settings'],
|
||||
discoveryHostKey = LOCAL_COMMIT_MESSAGE_HOST_KEY,
|
||||
operation: AiTextOperation = 'commitMessage',
|
||||
repo?: Pick<Repo, 'sourceControlAi'> | null
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
@@ -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<BranchPrefixSettings, 'branchPrefix'> & { branchPrefix: string },
|
||||
gitUsername: string | null
|
||||
): string | null {
|
||||
): string | null | void {
|
||||
switch (settings.branchPrefix) {
|
||||
case 'git-username':
|
||||
return gitUsername
|
||||
|
||||
@@ -64,11 +64,8 @@ export type ResolveSourceControlAiResult =
|
||||
| { ok: false; error: string }
|
||||
|
||||
type ResolveSourceControlAiInput = {
|
||||
settings: Pick<
|
||||
GlobalSettings,
|
||||
'defaultTuiAgent' | 'agentCmdOverrides' | 'commitMessageAi' | 'sourceControlAi'
|
||||
> &
|
||||
Partial<Pick<GlobalSettings, 'disabledTuiAgents'>>
|
||||
settings: Pick<GlobalSettings, 'commitMessageAi' | 'sourceControlAi'> &
|
||||
Partial<Pick<GlobalSettings, 'agentCmdOverrides' | 'defaultTuiAgent' | 'disabledTuiAgents'>>
|
||||
repo?: Pick<Repo, 'sourceControlAi'> | null
|
||||
operation: AiTextOperation
|
||||
discoveryHostKey?: string
|
||||
|
||||
@@ -14,7 +14,8 @@ const UNSUPPORTED_TUI_AGENT_ARGS: Partial<Record<TuiAgent, readonly string[]>> =
|
||||
kilo: ['--dangerously-skip-permissions']
|
||||
}
|
||||
|
||||
export const DEFAULT_TUI_AGENT_ARGS: Partial<Record<TuiAgent, string>> = YOLO_TUI_AGENT_ARGS
|
||||
export const DEFAULT_TUI_AGENT_ARGS: Partial<Record<TuiAgent, string>> &
|
||||
Readonly<Record<string, string | undefined>> = YOLO_TUI_AGENT_ARGS
|
||||
|
||||
export const DEFAULT_TUI_AGENT_ENV: Partial<Record<string, Record<string, string>>> =
|
||||
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<string, string> {
|
||||
}
|
||||
|
||||
export function resolveTuiAgentLaunchArgs(
|
||||
agent: TuiAgent,
|
||||
configuredArgs: Partial<Record<TuiAgent, string>> | null | undefined
|
||||
agent: string,
|
||||
configuredArgs: Readonly<Record<string, string | undefined>> | null | undefined
|
||||
): string {
|
||||
if (
|
||||
configuredArgs &&
|
||||
|
||||
Reference in New Issue
Block a user