mirror of
https://github.com/stablyai/orca.git
synced 2026-09-28 08:02:43 +00:00
refactor(workspaces): make gated reseed policy explicit
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { useAppStore } from '@/store'
|
||||
import { getRemoteConnectionIdForWorktree } from '@/store/terminals/terminal-workspace-routing'
|
||||
import {
|
||||
activateAndRevealFolderWorkspace,
|
||||
activateAndRevealWorkspace,
|
||||
@@ -75,10 +76,7 @@ describe('activating a workspace whose last terminal was closed', () => {
|
||||
}
|
||||
)
|
||||
|
||||
// Why: an agent selection suppresses the *pre-emptive* seed at create time, but the gate's
|
||||
// `empty` outcome is its evidence that the agent surface never arrived (dead PTY, unreadable
|
||||
// census, null startup plan). Suppressing the re-seed on that evidence leaves the workspace with
|
||||
// no tabs and no recovery; only an explicit caller promise may. See #19940 and STA-5701.
|
||||
// An empty gate invalidates agent-based suppression; only an explicit caller surface survives.
|
||||
it.each([
|
||||
['an agent selection', { agent: 'codex' as const }, 1],
|
||||
[
|
||||
@@ -344,31 +342,45 @@ describe('activating a folder workspace whose last terminal was closed', () => {
|
||||
}
|
||||
)
|
||||
|
||||
// Why: folder workspaces activate through their own entry point — setActiveWorktree(folderKey)
|
||||
// never reaches it — so the split has to be pinned on this path too, not just the git one.
|
||||
// This must use the folder entry point; setActiveWorktree(folderWorkspaceKey(...)) takes the git path.
|
||||
it.each([
|
||||
['an agent selection', { agent: 'codex' as const }, 1],
|
||||
['a local agent selection', { agent: 'codex' as const }, 'local' as const, 1],
|
||||
['an SSH agent selection', { agent: 'codex' as const }, SSH_HOST_ID, 1],
|
||||
[
|
||||
'an agent selection whose caller promised its own surface',
|
||||
{ agent: 'codex' as const, providesInitialSurface: true },
|
||||
'local' as const,
|
||||
0
|
||||
]
|
||||
])('re-seeds a gate-reported empty workspace for %s', async (_label, selection, expectedTabs) => {
|
||||
seedEmptiedFolderWorkspaceOnTwoHosts()
|
||||
useAppStore.setState({
|
||||
sleepingAgentSessionsByPaneKey: {
|
||||
'pane-1': { worktreeId: FOLDER_KEY }
|
||||
} as never
|
||||
})
|
||||
const gate = vi.spyOn(activationGate, 'gateWorktreeAgentActivation')
|
||||
gate.mockResolvedValue('empty')
|
||||
])(
|
||||
're-seeds a gate-reported empty workspace for %s',
|
||||
async (_label, selection, executionHostId, expectedTabs) => {
|
||||
seedEmptiedFolderWorkspaceOnTwoHosts()
|
||||
const folderWorkspaces = useAppStore
|
||||
.getState()
|
||||
.folderWorkspaces.filter((workspace) => workspace.executionHostId === executionHostId)
|
||||
useAppStore.setState({
|
||||
folderWorkspaces,
|
||||
sleepingAgentSessionsByPaneKey: {
|
||||
'pane-1': { worktreeId: FOLDER_KEY }
|
||||
} as never
|
||||
})
|
||||
const gate = vi.spyOn(activationGate, 'gateWorktreeAgentActivation')
|
||||
gate.mockResolvedValue('empty')
|
||||
|
||||
activateAndRevealFolderWorkspace(FOLDER_ID, { ...selection, executionHostId: 'local' })
|
||||
await gate.mock.results[0]?.value
|
||||
activateAndRevealFolderWorkspace(FOLDER_ID, { ...selection, executionHostId })
|
||||
await gate.mock.results[0]?.value
|
||||
|
||||
expect(useAppStore.getState().activeWorktreeId).toBe(FOLDER_KEY)
|
||||
expect(useAppStore.getState().tabsByWorktree[FOLDER_KEY] ?? []).toHaveLength(expectedTabs)
|
||||
})
|
||||
expect(useAppStore.getState().activeWorktreeId).toBe(FOLDER_KEY)
|
||||
expect(useAppStore.getState().activeWorkspaceExecutionHostId).toBe(executionHostId)
|
||||
expect(useAppStore.getState().tabsByWorktree[FOLDER_KEY] ?? []).toHaveLength(expectedTabs)
|
||||
if (expectedTabs > 0) {
|
||||
expect(getRemoteConnectionIdForWorktree(useAppStore.getState(), FOLDER_KEY)).toBe(
|
||||
executionHostId === 'local' ? null : 'conn-1'
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
it('re-seeds a terminal when the workspace is opened', () => {
|
||||
seedEmptiedFolderWorkspaceOnTwoHosts()
|
||||
|
||||
@@ -32,20 +32,17 @@ export type WorktreeActivationOptions = WorktreeActivationSurfaceSelection & {
|
||||
clearSidebarFilters?: boolean
|
||||
}
|
||||
|
||||
/** Create time: a shell seeded now would land beside the agent/chat surface this selection is
|
||||
* about to open, so an agent selection counts as a promised surface (#19940). */
|
||||
export function activationProvidesInitialSurface(
|
||||
selection?: WorktreeActivationSurfaceSelection
|
||||
): boolean {
|
||||
return selection?.providesInitialSurface === true || selection?.agent != null
|
||||
}
|
||||
|
||||
/** Deliberately narrower than `activationProvidesInitialSurface`: the async gate's `empty`
|
||||
* outcome is evidence the agent surface never materialised (dead PTY, unreadable census, null
|
||||
* startup plan), so only an explicit caller promise may suppress the fail-closed re-seed —
|
||||
* honouring the agent half strands the workspace with zero tabs and no recovery (STA-5701). */
|
||||
export function gatedEmptyOutcomeReseedSuppressed(
|
||||
export type GatedEmptyWorkspaceReseedPolicy = 'reseed' | 'caller-provides-surface'
|
||||
|
||||
/** An empty gate invalidates agent selection; only an explicit caller surface suppresses reseeding. */
|
||||
export function gatedEmptyWorkspaceReseedPolicy(
|
||||
selection?: WorktreeActivationSurfaceSelection
|
||||
): boolean {
|
||||
return selection?.providesInitialSurface === true
|
||||
): GatedEmptyWorkspaceReseedPolicy {
|
||||
return selection?.providesInitialSurface === true ? 'caller-provides-surface' : 'reseed'
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ import { ensureWebRuntimeWorktreeTerminalAfterWake } from '@/lib/web-runtime-wor
|
||||
import { applyWorktreeNavViewEntry } from '@/lib/worktree-nav-view-history-replay'
|
||||
import {
|
||||
activationProvidesInitialSurface,
|
||||
gatedEmptyOutcomeReseedSuppressed,
|
||||
gatedEmptyWorkspaceReseedPolicy,
|
||||
type WorktreeActivationOptions,
|
||||
type WorktreeActivationSurfaceSelection
|
||||
} from './worktree-activation-surface-selection'
|
||||
@@ -150,8 +150,8 @@ export function activateAndRevealFolderWorkspace(
|
||||
}
|
||||
if (shouldGateAgentActivation) {
|
||||
void gateWorktreeAgentActivation(workspaceKey).then((outcome) => {
|
||||
if (outcome === 'empty') {
|
||||
reseedGatedEmptyWorkspace(workspaceKey, gatedEmptyOutcomeReseedSuppressed(opts))
|
||||
if (outcome === 'empty' && gatedEmptyWorkspaceReseedPolicy(opts) === 'reseed') {
|
||||
reseedGatedEmptyWorkspace(workspaceKey, false)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -249,8 +249,8 @@ export function activateAndRevealWorktree(
|
||||
}
|
||||
if (shouldGateAgentActivation) {
|
||||
void gateWorktreeAgentActivation(worktreeId).then((outcome) => {
|
||||
if (outcome === 'empty') {
|
||||
reseedGatedEmptyWorkspace(worktreeId, gatedEmptyOutcomeReseedSuppressed(opts))
|
||||
if (outcome === 'empty' && gatedEmptyWorkspaceReseedPolicy(opts) === 'reseed') {
|
||||
reseedGatedEmptyWorkspace(worktreeId, false)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -36,9 +36,7 @@ function getSetupRunnerCommandPlatformForLaunch(setup: WorktreeSetupLaunch): 'wi
|
||||
}
|
||||
|
||||
/** After the async activation gate reports an empty workspace: re-seed a shell unless the caller
|
||||
* promised its own surface or the user has already moved on.
|
||||
* Callers must derive the flag from `gatedEmptyOutcomeReseedSuppressed`, never from an agent
|
||||
* selection — `empty` means that agent surface never arrived. */
|
||||
* promised its own surface or the user has already moved on. */
|
||||
export function reseedGatedEmptyWorkspace(
|
||||
workspaceKey: string,
|
||||
callerProvidesSurface: boolean | undefined
|
||||
|
||||
Reference in New Issue
Block a user