mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
fix(agent-launch): resolve a launch scope, not a git worktree record (#21193)
* fix(agent-launch): resolve a launch scope, not a git worktree record `agent.launch` asked the runtime for a managed worktree record and then read exactly one field off it, `.id`. That record does not exist for every workspace a launch can run in, so the request refused launches the method could otherwise run: the floating workspace resolves to a scope with an id and a path but no worktree row, and `showManagedTerminalWorkspace` throws `selector_not_found` rather than hand back the id it had already resolved. A folder workspace survived that only because the resolver fabricates a worktree row for it. The scope is the answer that is real for all three kinds, so the launch asks for that instead. `showManagedTerminalWorkspace` is unchanged - callers that genuinely need the git record still get it, and still get the refusal. With floating now reaching the mode decision, the host must know which kind of workspace it resolved. The kind is derived from the id it resolved itself, never accepted from a caller, and the route module's existing `floating` blocker does the rest: a workspace with nowhere to keep a session runs a terminal agent. Behaviour change, deliberate: a floating-workspace `agent.launch` used to fail with `selector_not_found` and now succeeds as a terminal agent. That is what lets the floating titlebar agent button move onto the shared launch command instead of driving tab startup itself. No wire change: `AgentLaunchTarget` is untouched. * test(agent-launch): cover floating RPC workspace resolution
This commit is contained in:
@@ -15,6 +15,7 @@ import {
|
||||
type AgentLaunchExecution
|
||||
} from './agent-launch-executor'
|
||||
import type { AgentLaunchIntent } from '../../shared/agent-launch-intent'
|
||||
import { FLOATING_TERMINAL_WORKTREE_ID } from '../../shared/constants'
|
||||
|
||||
const STRUCTURED_PREFERENCE = {
|
||||
experimentalNativeChat: true,
|
||||
@@ -247,3 +248,50 @@ describe('the prompt receipt', () => {
|
||||
expect((await h.run(CREATE_INTENT)).prompt).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* The kind is read off the resolved workspace id, so a workspace with nowhere to keep a session is
|
||||
* decided here rather than offered to a host probe that cannot answer for it.
|
||||
*/
|
||||
describe('a launch into an existing workspace, by workspace kind', () => {
|
||||
it('runs the floating workspace as a terminal, never a structured session', async () => {
|
||||
const h = harness({})
|
||||
const result = await h.run({
|
||||
agent: 'claude',
|
||||
target: { kind: 'existing', worktree: FLOATING_TERMINAL_WORKTREE_ID }
|
||||
})
|
||||
|
||||
// The invariant, not the call order: the floating sentinel has no session store to open into.
|
||||
expect(h.createStructuredSession).not.toHaveBeenCalled()
|
||||
expect(result.outcome).toEqual({ kind: 'terminal', handle: 'term_1' })
|
||||
expect(result.receipt).toMatchObject({
|
||||
mode: 'terminal',
|
||||
reason: 'structured_unsupported_on_host'
|
||||
})
|
||||
})
|
||||
|
||||
it('still opens a structured session in a folder workspace', async () => {
|
||||
const h = harness({})
|
||||
const result = await h.run({
|
||||
agent: 'claude',
|
||||
target: { kind: 'existing', worktree: 'folder:fw-1' }
|
||||
})
|
||||
|
||||
// A folder workspace has no git worktree either; it must not be swept up with the sentinel.
|
||||
expect(h.createTerminalAgent).not.toHaveBeenCalled()
|
||||
expect(result.outcome).toEqual({
|
||||
kind: 'structured',
|
||||
sessionId: 'sess-1',
|
||||
handle: 'handle_structured'
|
||||
})
|
||||
expect(result.receipt).toMatchObject({ mode: 'structured' })
|
||||
})
|
||||
|
||||
it('still opens a structured session in a git worktree', async () => {
|
||||
const h = harness({})
|
||||
const result = await h.run({ agent: 'claude', target: { kind: 'existing', worktree: 'wt-7' } })
|
||||
|
||||
expect(result.outcome.kind).toBe('structured')
|
||||
expect(result.receipt).toMatchObject({ mode: 'structured' })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -32,6 +32,10 @@ import type {
|
||||
} from '../../shared/agent-launch-intent'
|
||||
import { withoutReservedAgentCreateFields } from '../../shared/agent-launch-intent'
|
||||
import type { TuiAgent } from '../../shared/tui-agent'
|
||||
import {
|
||||
workspaceKindForWorktreeId,
|
||||
type WorkspaceLaunchKind
|
||||
} from '../../shared/workspace-launch-kind'
|
||||
import type { OrcaRuntimeService } from '../runtime/orca-runtime'
|
||||
import { isDefinitiveAgentSessionCreateRefusal } from '../../shared/agent-session-definitive-refusal'
|
||||
import {
|
||||
@@ -107,6 +111,7 @@ export async function executeAgentLaunch(
|
||||
const preflight = decideAgentLaunchMode({
|
||||
placement: {
|
||||
agent: intent.agent,
|
||||
workspaceKind: launchWorkspaceKind(intent.target),
|
||||
...(intent.reuseTerminal ? { terminal: intent.reuseTerminal.handle } : {})
|
||||
},
|
||||
settings,
|
||||
@@ -279,6 +284,15 @@ function existingWorktreeId(target: AgentLaunchTarget): string {
|
||||
return target.kind === 'existing' ? target.worktree : ''
|
||||
}
|
||||
|
||||
/**
|
||||
* Read from the id rather than carried alongside it, so the kind cannot disagree with the workspace
|
||||
* it describes. `worktree` here is never a caller's selector — the method resolved it to an id
|
||||
* before building the intent — and a create always produces a git worktree.
|
||||
*/
|
||||
function launchWorkspaceKind(target: AgentLaunchTarget): WorkspaceLaunchKind {
|
||||
return target.kind === 'existing' ? workspaceKindForWorktreeId(target.worktree) : 'git-worktree'
|
||||
}
|
||||
|
||||
/** Prompt delivery is the caller's, not the executor's: a PTY paste is observed by whoever owns
|
||||
* the pane, and a structured first turn is sent through the session. The executor reports the
|
||||
* requested delivery back as not delivered so a caller cannot mistake silence for delivery. */
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
} from '../../shared/structured-native-chat-launch-route'
|
||||
import type { TuiAgent } from '../../shared/tui-agent'
|
||||
import { hasExplicitTuiLaunchCommand } from '../../shared/tui-agent-launch-command-override'
|
||||
import type { WorkspaceLaunchKind } from '../../shared/workspace-launch-kind'
|
||||
import type { OrcaRuntimeService } from '../runtime/orca-runtime'
|
||||
|
||||
// The receipt is part of the launch contract, so it is declared with the rest of it; re-exported
|
||||
@@ -67,6 +68,10 @@ export type AgentLaunchModePlacement = {
|
||||
on?: string
|
||||
/** An existing terminal being reused. */
|
||||
terminal?: string
|
||||
/** Which kind of workspace the launch lands in, derived by the host from the workspace it
|
||||
* resolved — never accepted from a caller, which would let one route around this decision.
|
||||
* Absent means the kind was never established, and is not read as any particular kind. */
|
||||
workspaceKind?: WorkspaceLaunchKind
|
||||
}
|
||||
|
||||
const DOWNGRADE_DETAIL: Record<Exclude<AgentLaunchModeReason, 'user_default'>, string> = {
|
||||
@@ -131,9 +136,11 @@ export function decideAgentLaunchMode(args: {
|
||||
executionHostId: placement.on ? `runtime:${placement.on}` : 'local',
|
||||
reusesTerminal: Boolean(placement.terminal),
|
||||
hostCapabilities: RUNTIME_CAPABILITIES,
|
||||
// A resolved managed worktree or folder workspace is never a floating terminal. WSL is left to
|
||||
// the executing host's own create-support probe, which reads the resolved workspace rather
|
||||
// than guessing from a client-side project runtime.
|
||||
// The floating workspace has nowhere to keep a session, so it is decided here rather than left
|
||||
// to the host probe below, which cannot answer for a workspace with no record. WSL still is:
|
||||
// the create-support probe reads the resolved workspace rather than guessing from a
|
||||
// client-side project runtime.
|
||||
...(placement.workspaceKind ? { workspaceKind: placement.workspaceKind } : {}),
|
||||
requiresTuiLaunchCommand: hasExplicitTuiLaunchCommand(settings, agent)
|
||||
})
|
||||
if (!support.supported) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { stopMissingWorktreeTerminals } from './missing-worktree-terminal-reconc
|
||||
import type { RuntimeCommandSurfaceHost } from './orca-runtime-core'
|
||||
import type { WorktreeVisibilitySourceMatcher } from '../../shared/worktree/visibility-sources'
|
||||
import type { RuntimeStore } from './runtime-store-contract'
|
||||
import type { TerminalWorkspaceLaunchScope } from './runtime-legacy-worker-terminal-recovery-types'
|
||||
import type {
|
||||
WorkspacePortKillRequest,
|
||||
WorkspacePortKillResult,
|
||||
@@ -118,6 +119,10 @@ export class OrcaRuntimeWithListManagedWorktrees extends OrcaRuntimeWithRestoreS
|
||||
return await this.resolveWorktreeSelector(worktreeSelector)
|
||||
}
|
||||
|
||||
/**
|
||||
* The git worktree record behind a terminal workspace. Refuses the floating sentinel, which has
|
||||
* no such record — callers that only need to address the workspace want the scope below instead.
|
||||
*/
|
||||
async showManagedTerminalWorkspace(worktreeSelector: string) {
|
||||
const target = await this.resolveTerminalWorkspaceLaunchTarget(worktreeSelector)
|
||||
if (!target.managedWorktree) {
|
||||
@@ -126,6 +131,18 @@ export class OrcaRuntimeWithListManagedWorktrees extends OrcaRuntimeWithRestoreS
|
||||
return target.managedWorktree
|
||||
}
|
||||
|
||||
/**
|
||||
* Where a terminal workspace is, for every kind one can be: a git worktree, a folder workspace,
|
||||
* or the floating sentinel. This is the general answer — `id` and `path` are resolved the same
|
||||
* way for all three — so a caller that reads only those must ask for this rather than demand a
|
||||
* worktree record it never reads and lose the floating workspace to a `selector_not_found`.
|
||||
*/
|
||||
async showTerminalWorkspaceLaunchScope(
|
||||
worktreeSelector: string
|
||||
): Promise<TerminalWorkspaceLaunchScope> {
|
||||
return await this.resolveTerminalWorkspaceLaunchScope(worktreeSelector)
|
||||
}
|
||||
|
||||
async scanWorkspacePorts(repoId?: string): Promise<WorkspacePortScanResult> {
|
||||
return scanWorkspacePortProbes(await this.getWorkspacePortProbes(repoId))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { homedir } from 'node:os'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../../shared/constants'
|
||||
import { OrcaRuntimeService } from '../../orca-runtime'
|
||||
import { AGENT_LAUNCH_METHODS } from './agent-launch'
|
||||
import { CAPABLE_CLIENT, methodNamed, STRUCTURED_PREFERENCE } from './agent-launch.test-fixture'
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
BrowserWindow: { fromId: vi.fn(() => null) },
|
||||
webContents: { fromId: vi.fn(() => null) },
|
||||
ipcMain: { on: vi.fn(), removeListener: vi.fn() },
|
||||
app: { getPath: vi.fn(() => '/tmp') }
|
||||
}))
|
||||
|
||||
const launch = methodNamed(AGENT_LAUNCH_METHODS, 'agent.launch')
|
||||
const selectors = [FLOATING_TERMINAL_WORKTREE_ID, `id:${FLOATING_TERMINAL_WORKTREE_ID}`]
|
||||
|
||||
afterEach(() => vi.restoreAllMocks())
|
||||
|
||||
describe('agent.launch with the real floating workspace resolver', () => {
|
||||
it.each(selectors)('resolves %s without a managed worktree record', async (selector) => {
|
||||
const runtime = new OrcaRuntimeService()
|
||||
|
||||
await expect(runtime.showManagedTerminalWorkspace(selector)).rejects.toThrow(
|
||||
'selector_not_found'
|
||||
)
|
||||
await expect(runtime.showTerminalWorkspaceLaunchScope(selector)).resolves.toEqual({
|
||||
id: FLOATING_TERMINAL_WORKTREE_ID,
|
||||
path: homedir(),
|
||||
connectionId: null,
|
||||
repo: null,
|
||||
folderWorkspace: null
|
||||
})
|
||||
})
|
||||
|
||||
describe.each([true, false])('structured preference %s', (structuredPreference) => {
|
||||
it.each(selectors)('launches a terminal through %s', async (selector) => {
|
||||
const runtime = new OrcaRuntimeService()
|
||||
vi.spyOn(runtime, 'getClientSettings').mockReturnValue(
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the launch reads only these preferences and optional agentCmdOverrides; no other settings consumer runs because terminal creation is stubbed.
|
||||
{
|
||||
...STRUCTURED_PREFERENCE,
|
||||
openAgentTabsInChatByDefault: structuredPreference
|
||||
} as ReturnType<OrcaRuntimeService['getClientSettings']>
|
||||
)
|
||||
const scope = vi.spyOn(runtime, 'showTerminalWorkspaceLaunchScope')
|
||||
const createSupport = vi.spyOn(runtime, 'getStructuredAgentSessionCreateSupport')
|
||||
const structuredHost = vi.spyOn(runtime, 'ensureStructuredAgentSessionHost')
|
||||
const createTerminal = vi.spyOn(runtime, 'createTerminal').mockResolvedValue({
|
||||
handle: 'term_floating',
|
||||
tabId: 'tab_floating',
|
||||
worktreeId: FLOATING_TERMINAL_WORKTREE_ID,
|
||||
title: 'Claude',
|
||||
surface: 'background'
|
||||
})
|
||||
|
||||
const result = await launch.handler(
|
||||
launch.params.parse({ agent: 'claude', target: { kind: 'existing', worktree: selector } }),
|
||||
{ runtime, ...CAPABLE_CLIENT }
|
||||
)
|
||||
|
||||
expect(scope).toHaveBeenCalledExactlyOnceWith(selector)
|
||||
expect(createSupport).not.toHaveBeenCalled()
|
||||
expect(structuredHost).not.toHaveBeenCalled()
|
||||
expect(createTerminal).toHaveBeenCalledExactlyOnceWith(
|
||||
`id:${FLOATING_TERMINAL_WORKTREE_ID}`,
|
||||
{ startupAgent: 'claude' }
|
||||
)
|
||||
expect(result).toMatchObject({
|
||||
worktreeId: FLOATING_TERMINAL_WORKTREE_ID,
|
||||
outcome: { kind: 'terminal', handle: 'term_floating' },
|
||||
receipt: {
|
||||
mode: 'terminal',
|
||||
reason: structuredPreference ? 'structured_unsupported_on_host' : 'user_default'
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -339,7 +339,7 @@ describe('an uncertain launch stays uncertain', () => {
|
||||
|
||||
it('records a failure that happened before anything could be created', async () => {
|
||||
const runtime = runtimeStub()
|
||||
runtime.showManagedTerminalWorkspace.mockRejectedValueOnce(new Error('worktree_not_found'))
|
||||
runtime.showTerminalWorkspaceLaunchScope.mockRejectedValueOnce(new Error('worktree_not_found'))
|
||||
|
||||
await expect(
|
||||
launch(
|
||||
@@ -481,7 +481,7 @@ describe('an unreadable launch payload costs one replay, never the store', () =>
|
||||
describe('a recorded failure replays as the failure it was', () => {
|
||||
it('answers with the code the launch actually raised, not the ledger vocabulary', async () => {
|
||||
const runtime = runtimeStub()
|
||||
runtime.showManagedTerminalWorkspace.mockRejectedValue(new Error('worktree_not_found'))
|
||||
runtime.showTerminalWorkspaceLaunchScope.mockRejectedValue(new Error('worktree_not_found'))
|
||||
const params = createLaunch({
|
||||
operationId: OPERATION_ID,
|
||||
target: { kind: 'existing', worktree: 'gone' }
|
||||
@@ -493,14 +493,14 @@ describe('a recorded failure replays as the failure it was', () => {
|
||||
// malformed" signal, which tells a client to mint a fresh id when the truthful answer is that
|
||||
// this launch definitively did not run.
|
||||
const replayed = runtimeStub()
|
||||
replayed.showManagedTerminalWorkspace.mockRejectedValue(new Error('worktree_not_found'))
|
||||
replayed.showTerminalWorkspaceLaunchScope.mockRejectedValue(new Error('worktree_not_found'))
|
||||
await expect(launch(params, replayed)).rejects.toThrow('worktree_not_found')
|
||||
expect(replayed.showManagedTerminalWorkspace).not.toHaveBeenCalled()
|
||||
expect(replayed.showTerminalWorkspaceLaunchScope).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('bounds the code it persists, because a code is an identifier and a message is not', async () => {
|
||||
const runtime = runtimeStub()
|
||||
runtime.showManagedTerminalWorkspace.mockRejectedValue(
|
||||
runtime.showTerminalWorkspaceLaunchScope.mockRejectedValue(
|
||||
new Error(`ENOENT: no such file or directory, stat '${'/very/long/path'.repeat(400)}'`)
|
||||
)
|
||||
|
||||
|
||||
@@ -73,6 +73,15 @@ export function runtimeStub(options: AgentLaunchRuntimeStubOptions = {}) {
|
||||
showManagedTerminalWorkspace: vi.fn(async (selector: string) => ({
|
||||
id: selector.replace(/^id:/, '')
|
||||
})),
|
||||
// The scope resolves for every workspace kind, so unlike the worktree record above it never
|
||||
// refuses the floating sentinel — which is the whole reason the launch asks for this one.
|
||||
showTerminalWorkspaceLaunchScope: vi.fn(async (selector: string) => ({
|
||||
id: selector.replace(/^id:/, ''),
|
||||
path: '/tmp/wt-7',
|
||||
connectionId: null,
|
||||
repo: null,
|
||||
folderWorkspace: null
|
||||
})),
|
||||
ensureStructuredAgentSessionHost: vi.fn(async () => {}),
|
||||
waitForSetupTerminalCompletion
|
||||
}
|
||||
|
||||
@@ -449,7 +449,9 @@ describe('the terminal factory', () => {
|
||||
)
|
||||
|
||||
expect(runtime.createManagedWorktree).not.toHaveBeenCalled()
|
||||
expect(runtime.showManagedTerminalWorkspace).toHaveBeenCalledWith('id:wt-7')
|
||||
// The scope, not the worktree record: asking for the record refused any workspace without one.
|
||||
expect(runtime.showTerminalWorkspaceLaunchScope).toHaveBeenCalledWith('id:wt-7')
|
||||
expect(runtime.showManagedTerminalWorkspace).not.toHaveBeenCalled()
|
||||
// Resolved to an id first: everything below re-prefixes it, so a raw selector reaches the
|
||||
// runtime as `id:id:wt-7`.
|
||||
expect(runtime.createTerminal).toHaveBeenCalledWith('id:wt-7', { startupAgent: 'grok' })
|
||||
|
||||
@@ -59,17 +59,22 @@ export function supportsAgentLaunch(
|
||||
/**
|
||||
* A client addresses a workspace by selector, but the result's `worktreeId` is an id and every
|
||||
* step below the executor re-prefixes it as `id:<worktreeId>`. Resolving here is what keeps a
|
||||
* caller's `id:wt-7` from reaching the runtime as `id:id:wt-7`; the terminal-workspace resolver is
|
||||
* used rather than the git-worktree one so a folder workspace is addressable too.
|
||||
* caller's `id:wt-7` from reaching the runtime as `id:id:wt-7`.
|
||||
*
|
||||
* The launch *scope* is what is asked for, because the id below is the only thing read off it. The
|
||||
* git-worktree record is the narrower answer — it does not exist for the floating workspace, so
|
||||
* asking for one refused a launch this method can perfectly well run, on a workspace whose id it
|
||||
* had already resolved. A folder workspace survived that only because the resolver fabricates a
|
||||
* worktree row for it; the scope is the answer that is real for all three kinds.
|
||||
*/
|
||||
async function agentLaunchTarget(
|
||||
params: AgentLaunchParams,
|
||||
runtime: Pick<OrcaRuntimeService, 'showManagedTerminalWorkspace'>
|
||||
runtime: Pick<OrcaRuntimeService, 'showTerminalWorkspaceLaunchScope'>
|
||||
): Promise<AgentLaunchTarget> {
|
||||
if (params.target.kind === 'create-worktree') {
|
||||
return { kind: 'create-worktree', create: { ...params.target.create } }
|
||||
}
|
||||
const workspace = await runtime.showManagedTerminalWorkspace(params.target.worktree)
|
||||
const workspace = await runtime.showTerminalWorkspaceLaunchScope(params.target.worktree)
|
||||
return { kind: 'existing', worktree: workspace.id }
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../shared/constants'
|
||||
import {
|
||||
LOCAL_EXECUTION_HOST_ID,
|
||||
parseExecutionHostId,
|
||||
toRuntimeExecutionHostId
|
||||
} from '../../../shared/execution-host'
|
||||
import type { TuiAgent } from '../../../shared/tui-agent'
|
||||
import { parseWorkspaceKey } from '../../../shared/workspace-scope'
|
||||
import { workspaceKindForWorktreeId } from '../../../shared/workspace-launch-kind'
|
||||
import {
|
||||
hasExplicitTuiLaunchCommand,
|
||||
type AgentLaunchRoutingInput
|
||||
@@ -57,12 +56,7 @@ export type AgentLaunchRouteArgs = {
|
||||
initialSessionOptions?: Readonly<Record<string, unknown>>
|
||||
}
|
||||
|
||||
export function workspaceKindForWorktreeId(worktreeId: string): ProspectiveWorkspaceKind {
|
||||
if (worktreeId === FLOATING_TERMINAL_WORKTREE_ID) {
|
||||
return 'floating'
|
||||
}
|
||||
return parseWorkspaceKey(worktreeId)?.type === 'folder' ? 'folder' : 'git-worktree'
|
||||
}
|
||||
export { workspaceKindForWorktreeId }
|
||||
|
||||
function resolveExecutionHostId(store: AgentLaunchRouteStore, workspace: ProspectiveWorkspace) {
|
||||
if (workspace.worktreeId) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
resolveStructuredNativeChatSupport
|
||||
} from '../../../shared/structured-native-chat-launch-route'
|
||||
import type { TuiAgent } from '../../../shared/tui-agent'
|
||||
import type { WorkspaceLaunchKind } from '../../../shared/workspace-launch-kind'
|
||||
import {
|
||||
decideInitialAgentTabViewMode,
|
||||
type NativeChatLaunchPromptDelivery
|
||||
@@ -28,7 +29,7 @@ export type AgentLaunchRoutingInput = {
|
||||
executionHostId: string
|
||||
/** Capabilities of the target host; `null` = not yet established. */
|
||||
hostCapabilities: readonly string[] | null
|
||||
workspaceKind?: 'git-worktree' | 'folder' | 'floating'
|
||||
workspaceKind?: WorkspaceLaunchKind
|
||||
projectRuntime?: ProjectExecutionRuntimeResolution | null
|
||||
promptDelivery?: NativeChatLaunchPromptDelivery
|
||||
launchText?: string
|
||||
|
||||
@@ -13,6 +13,7 @@ import type { GlobalSettings } from './global-settings-types'
|
||||
import type { ProjectExecutionRuntimeResolution } from './project-execution-runtime'
|
||||
import { STRUCTURED_AGENT_SESSION_RUNTIME_CAPABILITY } from './protocol-version'
|
||||
import type { TuiAgent } from './tui-agent'
|
||||
import type { WorkspaceLaunchKind } from './workspace-launch-kind'
|
||||
|
||||
export type NativeChatDefaultSettings = Pick<
|
||||
GlobalSettings,
|
||||
@@ -44,7 +45,8 @@ export type StructuredNativeChatSupportInput = {
|
||||
executionHostId: string
|
||||
/** Capabilities of the host this launch would run on. `null` = not yet established. */
|
||||
hostCapabilities: readonly string[] | null
|
||||
workspaceKind?: 'git-worktree' | 'folder' | 'floating'
|
||||
/** Host-derived. Absent means the kind was never established, which is not evidence of any kind. */
|
||||
workspaceKind?: WorkspaceLaunchKind
|
||||
projectRuntime?: ProjectExecutionRuntimeResolution | null
|
||||
requiresTuiLaunchCommand?: boolean
|
||||
/** An existing PTY agent keeps its execution transport. */
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Which kind of workspace a launch lands in, read from the workspace's own id.
|
||||
*
|
||||
* The three kinds are not interchangeable to a launch: only a git worktree and a folder workspace
|
||||
* have somewhere a structured session can live, and the floating terminal — a sentinel with no
|
||||
* backing repo, worktree or folder row — can host a PTY and nothing else.
|
||||
*
|
||||
* It lives in `shared` because both sides of the launch ask the same question: the renderer when a
|
||||
* user opens an agent tab, and the host when it resolves an `agent.launch` target. A host must
|
||||
* never take the answer from a caller, so it derives it here from the id it resolved itself.
|
||||
*/
|
||||
|
||||
import { FLOATING_TERMINAL_WORKTREE_ID } from './constants'
|
||||
import { parseWorkspaceKey } from './workspace-scope'
|
||||
|
||||
export type WorkspaceLaunchKind = 'git-worktree' | 'folder' | 'floating'
|
||||
|
||||
export function workspaceKindForWorktreeId(worktreeId: string): WorkspaceLaunchKind {
|
||||
if (worktreeId === FLOATING_TERMINAL_WORKTREE_ID) {
|
||||
return 'floating'
|
||||
}
|
||||
return parseWorkspaceKey(worktreeId)?.type === 'folder' ? 'folder' : 'git-worktree'
|
||||
}
|
||||
Reference in New Issue
Block a user