mirror of
https://github.com/stablyai/orca.git
synced 2026-11-15 18:37:24 +00:00
fix(cloud): CLOUD-CHAT-FIRST-TAB a create whose first agent opens as a chat seeds no shell in front of it (chat counts as the first surface; the create holds the seeder off while its chat opens)
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
import { useAppStore } from '@/store/index'
|
||||
import { getExecutionHostIdForWorktree } from '@/lib/workspace/ownership/worktree-runtime-owner'
|
||||
import { parseExecutionHostId } from '../../../../../shared/execution-host'
|
||||
import {
|
||||
beginWebRuntimeInitialTerminalBootstrap,
|
||||
endWebRuntimeInitialTerminalBootstrap
|
||||
} from '@/runtime/web-session-tabs-sync/initial-terminal/web-runtime-initial-terminal-bootstrap'
|
||||
|
||||
/**
|
||||
* CLOUD-CHAT-FIRST-TAB: a create on a paired host (an Orca Cloud VM) whose first agent opens as a
|
||||
* chat claims the workspace's initial-terminal bootstrap while that chat launches, like the
|
||||
* after-wake first agent does. Without it the mirror's first empty frame seeds a plain shell,
|
||||
* activated, in front of the chat. Released once the launch has answered: by then the chat tab
|
||||
* counts as the workspace's first surface, and a launch that opened nothing gets its shell.
|
||||
*/
|
||||
export function claimFirstSurfaceForStructuredCreate(worktreeId: string): () => void {
|
||||
const host = parseExecutionHostId(
|
||||
getExecutionHostIdForWorktree(useAppStore.getState(), worktreeId)
|
||||
)
|
||||
if (host?.kind !== 'runtime') {
|
||||
return () => undefined
|
||||
}
|
||||
const { environmentId } = host
|
||||
if (!beginWebRuntimeInitialTerminalBootstrap(environmentId, worktreeId)) {
|
||||
// Someone else's create is already in flight; it owns the release.
|
||||
return () => undefined
|
||||
}
|
||||
let released = false
|
||||
return () => {
|
||||
if (!released) {
|
||||
released = true
|
||||
endWebRuntimeInitialTerminalBootstrap(environmentId, worktreeId)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useAppStore } from '@/store/index'
|
||||
import { claimFirstSurfaceForStructuredCreate } from './structured-create-first-surface-claim'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { activateAndRevealWorktree } from '@/lib/workspace/activation/worktree-activation'
|
||||
import type { ActivateAndRevealResult } from '@/lib/workspace/activation/worktree-activation-outcome'
|
||||
@@ -49,6 +50,18 @@ type LaunchStructuredWorktreeSessionArgs = {
|
||||
|
||||
export async function launchStructuredWorktreeSession(
|
||||
args: LaunchStructuredWorktreeSessionArgs
|
||||
): Promise<WorktreeCreationStructuredSessionResult> {
|
||||
// CLOUD-CHAT-FIRST-TAB: no seeded shell in front of the chat while it opens.
|
||||
const releaseFirstSurface = claimFirstSurfaceForStructuredCreate(args.worktreeId)
|
||||
try {
|
||||
return await launchClaimedStructuredWorktreeSession(args)
|
||||
} finally {
|
||||
releaseFirstSurface()
|
||||
}
|
||||
}
|
||||
|
||||
async function launchClaimedStructuredWorktreeSession(
|
||||
args: LaunchStructuredWorktreeSessionArgs
|
||||
): Promise<WorktreeCreationStructuredSessionResult> {
|
||||
let { activation, primaryTabId } = args
|
||||
const settled = { accepted: true, cancelled: false }
|
||||
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
// CLOUD-CHAT-FIRST-TAB (END-PASS n51): an Orca Cloud create whose first agent opens as a chat got a
|
||||
// second plain "Terminal" tab, in front: the mirror's first empty frame saw no terminal tabs (a chat
|
||||
// is not one) and seeded a shell. A chat now counts as the workspace's first surface, and the create
|
||||
// holds the seeder off while its chat launches.
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const state = vi.hoisted(() => ({
|
||||
tabsByWorktree: {} as Record<string, unknown[]>,
|
||||
unifiedTabsByWorktree: {} as Record<string, { id: string; contentType: string }[]>,
|
||||
owner: 'runtime:env-vm' as string
|
||||
}))
|
||||
vi.mock('../../store', () => ({ useAppStore: { getState: () => state } }))
|
||||
vi.mock('@/store/index', () => ({ useAppStore: { getState: () => state } }))
|
||||
vi.mock('@/lib/workspace/ownership/worktree-runtime-owner', () => ({
|
||||
getExecutionHostIdForWorktree: () => state.owner
|
||||
}))
|
||||
vi.mock('./frame-drawn-server-workspace-set', () => ({ isFrameDrawnServerWorkspace: () => false }))
|
||||
vi.mock('./initial-terminal/web-runtime-initial-terminal-bootstrap-dispatch', () => ({
|
||||
dispatchWebRuntimeInitialTerminalBootstrap: async () => true
|
||||
}))
|
||||
|
||||
const { createInitialTerminalSeeder } = await import('./initial-terminal-seeder')
|
||||
const { claimFirstSurfaceForStructuredCreate } =
|
||||
await import('@/lib/workspace/creation/structured-create-first-surface-claim')
|
||||
const { resetWebRuntimeInitialTerminalBootstrapForTests } =
|
||||
await import('./initial-terminal/web-runtime-initial-terminal-bootstrap')
|
||||
|
||||
const WT = 'wt-cloud'
|
||||
// The host's first published answer: the workspace has no tabs yet.
|
||||
const emptyFrame = {
|
||||
type: 'snapshot',
|
||||
worktree: WT,
|
||||
tabs: [],
|
||||
publicationEpoch: 'epoch-1'
|
||||
} as const
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the decision reads only type, worktree, tabs and publicationEpoch
|
||||
const frame = emptyFrame as unknown as Parameters<
|
||||
ReturnType<typeof createInitialTerminalSeeder>['decide']
|
||||
>[0]
|
||||
|
||||
afterEach(() => {
|
||||
state.tabsByWorktree = {}
|
||||
state.unifiedTabsByWorktree = {}
|
||||
state.owner = 'runtime:env-vm'
|
||||
resetWebRuntimeInitialTerminalBootstrapForTests()
|
||||
})
|
||||
|
||||
describe('the initial-terminal seeder beside a first chat', () => {
|
||||
it('an empty new workspace still gets its shell', () => {
|
||||
expect(createInitialTerminalSeeder('env-vm', WT).decide(frame, true)).toBe(true)
|
||||
})
|
||||
|
||||
it('a workspace whose first tab is a chat gets no shell beside it', () => {
|
||||
state.unifiedTabsByWorktree[WT] = [{ id: 'agent-session:s1', contentType: 'agent-session' }]
|
||||
expect(createInitialTerminalSeeder('env-vm', WT).decide(frame, true)).toBe(false)
|
||||
})
|
||||
|
||||
it('a non-chat unified tab (an editor) does not count as the first surface', () => {
|
||||
state.unifiedTabsByWorktree[WT] = [{ id: 'editor:1', contentType: 'editor' }]
|
||||
expect(createInitialTerminalSeeder('env-vm', WT).decide(frame, true)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('claimFirstSurfaceForStructuredCreate', () => {
|
||||
it('holds the seeder off while the chat launches, and lets go once it has answered', () => {
|
||||
const release = claimFirstSurfaceForStructuredCreate(WT)
|
||||
// The chat tab is not there yet: the empty frame must not seed a shell.
|
||||
expect(createInitialTerminalSeeder('env-vm', WT).decide(frame, true)).toBe(false)
|
||||
release()
|
||||
// A launch that opened nothing gets its shell after all.
|
||||
expect(createInitialTerminalSeeder('env-vm', WT).decide(frame, true)).toBe(true)
|
||||
// Releasing twice is harmless.
|
||||
release()
|
||||
})
|
||||
|
||||
it('a create on this machine claims nothing', () => {
|
||||
state.owner = 'local'
|
||||
const release = claimFirstSurfaceForStructuredCreate(WT)
|
||||
expect(createInitialTerminalSeeder('env-vm', WT).decide(frame, true)).toBe(true)
|
||||
release()
|
||||
})
|
||||
|
||||
it('a claim someone else holds is left to them', () => {
|
||||
const first = claimFirstSurfaceForStructuredCreate(WT)
|
||||
const second = claimFirstSurfaceForStructuredCreate(WT)
|
||||
second()
|
||||
expect(createInitialTerminalSeeder('env-vm', WT).decide(frame, true)).toBe(false)
|
||||
first()
|
||||
})
|
||||
})
|
||||
@@ -52,7 +52,12 @@ export function createInitialTerminalSeeder(
|
||||
requestedInitialTerminal ||
|
||||
isWebRuntimeInitialTerminalBootstrapInFlight(environmentId, worktreeId),
|
||||
snapshotIsFresh: true,
|
||||
localTerminalCount: (state.tabsByWorktree[worktreeId] ?? []).length,
|
||||
// CLOUD-CHAT-FIRST-TAB: a chat is the workspace's first surface too; no shell is seeded beside it.
|
||||
localTerminalCount:
|
||||
(state.tabsByWorktree[worktreeId] ?? []).length +
|
||||
(state.unifiedTabsByWorktree?.[worktreeId] ?? []).filter(
|
||||
(tab) => tab.contentType === 'agent-session'
|
||||
).length,
|
||||
hasPersistedTerminalState: Object.hasOwn(state.tabsByWorktree, worktreeId),
|
||||
firstHostAnswer
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user