Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,27 @@ function seedClosedLastTerminal(worktreeId: string): void {
}

describe('activating a workspace whose last terminal was closed', () => {
it('re-seeds after the asynchronous agent inventory confirms the workspace is empty', async () => {
const worktree = makeWorktree()
seedEmptyActivatableWorktree(worktree)
seedClosedLastTerminal(worktree.id)
useAppStore.setState({
workspaceSessionReady: true,
terminalStartupRestorationReady: true
})
vi.stubGlobal('window', {
api: {
runtime: { call: vi.fn(async () => ({ ok: true, result: { snapshots: [] } })) },
pty: { listSessions: vi.fn(async () => []) }
}
})

activateAndRevealWorktree(worktree.id, { notifyHostRuntime: false })
await waitForWorktreeAgentActivationGateForTests(worktree.id)

expect(useAppStore.getState().tabsByWorktree[worktree.id]).toHaveLength(1)
})

it('re-seeds a terminal when the workspace is opened from elsewhere', () => {
const worktree = makeWorktree()
seedEmptyActivatableWorktree(worktree)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ const SURFACE_PROVIDING_CALLERS = [
]

// The activation seam itself: declares the option and forwards it into the tombstone gate.
const SEAM_FILES = ['src/renderer/src/lib/worktree-activation.ts']
const SEAM_FILES = [
'src/renderer/src/lib/worktree-activation.ts',
'src/renderer/src/lib/worktree-initial-terminal-seeding.ts'
]

function listSourceFiles(dir: string): string[] {
return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
Expand Down
38 changes: 13 additions & 25 deletions src/renderer/src/lib/worktree-activation.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type { FolderWorkspace } from '../../../shared/folder-workspace-types'
import type {
WorktreeDefaultTabsLaunch,
WorktreeSetupLaunch
Expand Down Expand Up @@ -30,7 +29,10 @@ import type { ExecutionHostId } from '../../../shared/execution-host'
import { findFolderWorkspaceOwner } from './folder-workspace-runtime-owner'
import type { WorktreeStartupPayload } from '@/lib/worktree-startup-payload'
import type { IssueCommandLaunch } from '@/lib/worktree-setup-issue-command-queue'
import { ensureWorktreeHasInitialTerminal } from '@/lib/worktree-initial-terminal-seeding'
import {
ensureFolderWorkspaceInitialTerminal,
ensureWorktreeHasInitialTerminal
} from '@/lib/worktree-initial-terminal-seeding'
import { ensureWebRuntimeWorktreeTerminalAfterWake } from '@/lib/web-runtime-worktree-terminal-after-wake'
import { applyWorktreeNavViewEntry } from '@/lib/worktree-nav-view-history-replay'

Expand All @@ -45,28 +47,6 @@ export type ActivateAndRevealResult = {
primaryTabId: string | null
}

function ensureFolderWorkspaceInitialTerminal(
folderWorkspace: FolderWorkspace,
startup?: WorktreeStartupPayload,
providesInitialSurface?: boolean
): string | null {
if (providesInitialSurface === true && startup === undefined) {
return null
}
const state = useAppStore.getState()
const workspaceKey = folderWorkspaceKey(folderWorkspace.id)
const primaryTabId = ensureWorktreeHasInitialTerminal(
state,
workspaceKey,
startup,
undefined,
undefined,
undefined,
{ reseedEmptiedWorkspace: providesInitialSurface !== true }
)
return primaryTabId
}

function canInspectAgentActivationInventory(): boolean {
return (
typeof window !== 'undefined' &&
Expand Down Expand Up @@ -270,7 +250,15 @@ export function activateAndRevealWorktree(
opts?.providesInitialSurface !== true &&
currentState.activeWorktreeId === worktreeId
) {
ensureWorktreeHasInitialTerminal(currentState, worktreeId)
ensureWorktreeHasInitialTerminal(
currentState,
worktreeId,
undefined,
undefined,
undefined,
undefined,
{ reseedEmptiedWorkspace: true }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Revalidate the activation intent before reseeding.

This delayed callback uses the earlier activation's options. If a second activation sets providesInitialSurface: true before the gate resolves, activeWorktreeId remains the same and this call still creates a terminal. Track the latest activation generation, or revalidate the current activation intent before reseeding.

)
}
})
}
Expand Down
21 changes: 21 additions & 0 deletions src/renderer/src/lib/worktree-initial-terminal-seeding.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { FolderWorkspace } from '../../../shared/folder-workspace-types'
import { folderWorkspaceKey } from '../../../shared/workspace-scope'
import type {
WorktreeDefaultTabsLaunch,
WorktreeSetupLaunch
Expand Down Expand Up @@ -214,3 +216,22 @@ export function ensureWorktreeHasInitialTerminal(

return terminalTab.id
}

export function ensureFolderWorkspaceInitialTerminal(
folderWorkspace: FolderWorkspace,
startup?: WorktreeStartupPayload,
providesInitialSurface?: boolean
): string | null {
if (providesInitialSurface === true && startup === undefined) {
return null
}
return ensureWorktreeHasInitialTerminal(
useAppStore.getState(),
folderWorkspaceKey(folderWorkspace.id),
startup,
undefined,
undefined,
undefined,
{ reseedEmptiedWorkspace: providesInitialSurface !== true }
)
}
2 changes: 1 addition & 1 deletion tests/e2e/completed-worker-retirement-resume.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ for (const closeMode of ['terminal-close-cli', 'worker-release'] as const) {

const expectedRecovery = {
origin: 'live',
state: 'working',
state: 'done',
providerSessionId: PROVIDER_SESSION_ID
}
await expect
Expand Down