mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
AI buttons stop writing a launch record (#28209)
* fix(agent-launch): an AI button's launch writes no launch record, so it starts even when the record store cannot open * fix(agent-launch): a failed AI-button tab the window closes is not a user close, and E's leftover prompt store write, hold release and stale comment go * test(agent-launch): the launch funnel's store double keeps a tab made with its own id, as the real store does, so an AI-button launch is not read as closed by the user * docs(agent-launch): a failed AI-button tab's cleanup close still records the tab, so an agent that starts late is stopped, not shown
This commit is contained in:
@@ -76,6 +76,19 @@ describe('a launch into an existing workspace', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// An AI button's click names no operation: the host writes no record for it, so a record store
|
||||
// that cannot open never stops it.
|
||||
it('starts a reserved pane with no operation id even when the launch record cannot open', async () => {
|
||||
setAgentLaunchRecordStore(null)
|
||||
const runtime = runtimeStub({ settings: TERMINAL_ONLY })
|
||||
|
||||
const result = await launch({ ...EXISTING_LAUNCH, paneKey: PANE_KEY }, runtime)
|
||||
|
||||
expect(result.outcome.kind).toBe('terminal')
|
||||
expect(terminalOptions(runtime)).toMatchObject({ tabId: TAB_ID, leafId: LEAF_ID })
|
||||
expect(runtime.openAgentSessionRecordStore).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('leaves the pane to the runtime when the caller reserved none', async () => {
|
||||
// Every shipped caller sends nothing; its options must not gain a key.
|
||||
const runtime = runtimeStub({ settings: TERMINAL_ONLY })
|
||||
|
||||
@@ -51,8 +51,10 @@ export function createLaunchFunnelStore(): {
|
||||
seedNativeChatLaunchPrompt: ReturnType<typeof vi.fn>
|
||||
seedNativeChatLaunchDraft: ReturnType<typeof vi.fn>
|
||||
markNativeChatLaunchPromptFailed: ReturnType<typeof vi.fn>
|
||||
setTabAgentLaunchPane: ReturnType<typeof vi.fn>
|
||||
closeTab: ReturnType<typeof vi.fn>
|
||||
} {
|
||||
return {
|
||||
const store = {
|
||||
settings: launchFunnelSettings(),
|
||||
repos: [],
|
||||
allWorktrees: vi.fn(() => []),
|
||||
@@ -60,7 +62,7 @@ export function createLaunchFunnelStore(): {
|
||||
openFiles: [],
|
||||
browserTabsByWorktree: {},
|
||||
tabBarOrderByWorktree: {},
|
||||
createTab: vi.fn(() => ({ id: 'tab-1' })),
|
||||
createTab: vi.fn(),
|
||||
queueTabInitialCwd: vi.fn(),
|
||||
queueTabStartupCommand: vi.fn(),
|
||||
setActiveTabType: vi.fn(),
|
||||
@@ -68,7 +70,23 @@ export function createLaunchFunnelStore(): {
|
||||
setAgentStatus: vi.fn(),
|
||||
seedNativeChatLaunchPrompt: vi.fn(),
|
||||
seedNativeChatLaunchDraft: vi.fn(),
|
||||
markNativeChatLaunchPromptFailed: vi.fn()
|
||||
markNativeChatLaunchPromptFailed: vi.fn(),
|
||||
setTabAgentLaunchPane: vi.fn(),
|
||||
closeTab: vi.fn()
|
||||
}
|
||||
store.createTab.mockImplementation(createTabInto(store))
|
||||
return store
|
||||
}
|
||||
|
||||
/** A tab made with its own id joins the workspace, as the real store's does; launches read it back. */
|
||||
function createTabInto(store: { tabsByWorktree: Record<string, { id: string }[]> }) {
|
||||
return (worktreeId: string, _groupId?: string, _x?: unknown, opts?: { id?: string }) => {
|
||||
if (!opts?.id) {
|
||||
return { id: 'tab-1' }
|
||||
}
|
||||
const tab = { id: opts.id }
|
||||
store.tabsByWorktree[worktreeId] = [...(store.tabsByWorktree[worktreeId] ?? []), tab]
|
||||
return tab
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +102,7 @@ export function resetLaunchFunnelStore(
|
||||
store.openFiles = []
|
||||
store.browserTabsByWorktree = {}
|
||||
store.tabBarOrderByWorktree = {}
|
||||
store.createTab.mockReturnValue({ id: 'tab-1' })
|
||||
store.createTab.mockImplementation(createTabInto(store))
|
||||
}
|
||||
|
||||
/** The startup command `queueTabStartupCommand` received, or undefined when no tab was queued. */
|
||||
@@ -109,9 +127,9 @@ export function createdTabGroupId(store: LaunchFunnelStore): string | undefined
|
||||
return store.createTab.mock.calls[0]?.[1]
|
||||
}
|
||||
|
||||
/** The `agent.launchReplay` request a launch sent its host, or undefined when the window planned it. */
|
||||
/** The `agent.launch` request a launch sent its host, or undefined when the window planned it. */
|
||||
export function hostLaunchRequest(
|
||||
callRuntimeRpc: ReturnType<typeof vi.fn>
|
||||
): Record<string, unknown> | undefined {
|
||||
return callRuntimeRpc.mock.calls.find(([, method]) => method === 'agent.launchReplay')?.[2]
|
||||
return callRuntimeRpc.mock.calls.find(([, method]) => method === 'agent.launch')?.[2]
|
||||
}
|
||||
|
||||
@@ -3,9 +3,8 @@
|
||||
*
|
||||
* Such a pane must not spawn until the host has taken it: before that, main knows of no launch for
|
||||
* it and would give it a shell, which the host's own spawn would then be refused over. The host
|
||||
* takes the pane when it asks this window to show the launch's tab, so the hold ends there, or when
|
||||
* the launch is over. Session-only: after a reload the pane's spawn asks main, which waits for a launch
|
||||
* still running there.
|
||||
* takes the pane when it reveals the agent's terminal there, so the hold ends there, or when the
|
||||
* launch is over. Session-only: after a reload the pane spawns as any pane does.
|
||||
*/
|
||||
|
||||
type Hold = { released: Promise<void>; release: () => void }
|
||||
|
||||
@@ -122,27 +122,6 @@ describe('publishing a launch tab before its agent exists', () => {
|
||||
expect(launchPane()).toEqual({ leafId: LEAF_ID, operationId: 'op-2' })
|
||||
})
|
||||
|
||||
it('hands a tab this window made for the launch to the host, as the launch to take back', async () => {
|
||||
const { holdAgentLaunchPaneSpawn, agentLaunchPaneSpawnHold } =
|
||||
await import('./agent-launch-pane-spawn-hold')
|
||||
holdAgentLaunchPaneSpawn(TAB_ID, LEAF_ID)
|
||||
store.getState().createTab(WT, undefined, undefined, {
|
||||
id: TAB_ID,
|
||||
initialLeafId: LEAF_ID,
|
||||
agentLaunchPane: { leafId: LEAF_ID }
|
||||
})
|
||||
const held = agentLaunchPaneSpawnHold(TAB_ID, LEAF_ID)
|
||||
|
||||
const published = publishAgentLaunchTab(request())
|
||||
|
||||
// The host now owns the pane, so a refused launch takes the tab back rather than leaving a shell.
|
||||
expect(published).toMatchObject({ tabId: TAB_ID, created: true })
|
||||
await expect(held).resolves.toBeUndefined()
|
||||
expect(agentLaunchPaneSpawnHold(TAB_ID, LEAF_ID)).toBeNull()
|
||||
// A retry finds no hold: the tab is no longer only this launch's.
|
||||
expect(publishAgentLaunchTab(request({ requestId: 'request-2' })).created).toBe(false)
|
||||
})
|
||||
|
||||
it("remounts a pane that showed an earlier launch's outcome, so it spawns for the new launch", () => {
|
||||
const generation = () =>
|
||||
store.getState().tabsByWorktree[WT]?.find((tab) => tab.id === TAB_ID)?.generation ?? 0
|
||||
|
||||
@@ -17,7 +17,6 @@ import { useAppStore } from '../store'
|
||||
import { resolveUnifiedTabCreatePlacement } from '../store/slices/tabs/tabs-create-placement'
|
||||
import { insertUnifiedTabAfterAnchor } from './unified-tab-anchor-insertion'
|
||||
import { rememberAgentLaunchPanePrompt } from './agent-launch-pane-prompt'
|
||||
import { releaseAgentLaunchPaneSpawn } from './agent-launch-pane-spawn-hold'
|
||||
|
||||
function landedGroupId(worktreeId: string, tabId: string): string | undefined {
|
||||
return useAppStore
|
||||
@@ -77,9 +76,7 @@ export function publishAgentLaunchTab(
|
||||
remount: kept?.leafId === leafId && kept.outcome !== undefined
|
||||
})
|
||||
}
|
||||
// A tab this window made for this launch is the launch's to take back if it never runs.
|
||||
const madeForThisLaunch = releaseAgentLaunchPaneSpawn(tabId, leafId)
|
||||
return { tabId, created: madeForThisLaunch, placement: { groupId } }
|
||||
return { tabId, created: false, placement: { groupId } }
|
||||
}
|
||||
|
||||
const placement = resolveUnifiedTabCreatePlacement({
|
||||
|
||||
@@ -37,7 +37,6 @@ createTabsSliceMockApi()
|
||||
const { launchAgentThroughHost } = await import('./agent-launch-through-host')
|
||||
const { agentLaunchPaneSpawnHold, releaseAgentLaunchPaneSpawn } =
|
||||
await import('./agent-launch-pane-spawn-hold')
|
||||
const { agentLaunchPanePrompt } = await import('./agent-launch-pane-prompt')
|
||||
|
||||
const WT = 'repo1::/tmp/feature'
|
||||
let store: ReturnType<typeof createTestStore>
|
||||
@@ -117,32 +116,31 @@ describe('a desktop launch through the host', () => {
|
||||
expect(tab).toMatchObject({ ptyId: null, launchAgent: 'claude' })
|
||||
const leafId = tab.agentLaunchPane!.leafId
|
||||
expect(agentLaunchPaneSpawnHold(tab.id, leafId)).not.toBeNull()
|
||||
expect(agentLaunchPanePrompt(tab.id)).toBe('fix the failing checks')
|
||||
expect(store.getState().activeTabId).toBe(tabId)
|
||||
// No prompt: the window pastes it, as main does, once the agent runs.
|
||||
expect(callRuntimeRpc).toHaveBeenCalledWith({ kind: 'local' }, 'agent.launchReplay', {
|
||||
expect(callRuntimeRpc).toHaveBeenCalledWith({ kind: 'local' }, 'agent.launch', {
|
||||
agent: 'claude',
|
||||
target: { kind: 'existing', worktree: `id:${WT}` },
|
||||
agentArgs: null,
|
||||
launchSource: 'source_control_recovery',
|
||||
placement: { groupId: store.getState().activeGroupIdByWorktree[WT] },
|
||||
presentation: 'focused',
|
||||
operationId: expect.stringMatching(/^\d+-[0-9a-f]{32}$/),
|
||||
paneKey: `${tabId}:${leafId}`
|
||||
})
|
||||
})
|
||||
|
||||
it('names every click as its own operation', () => {
|
||||
// Nothing reads a click's launch record, so a record store that cannot open never stops one.
|
||||
it('names no operation, so the host writes no launch record; each click has its own pane', () => {
|
||||
callRuntimeRpc.mockReturnValue(new Promise(() => {}))
|
||||
launch()
|
||||
const first = lastParams()
|
||||
launch()
|
||||
const second = lastParams()
|
||||
expect(second.operationId).not.toBe(first.operationId)
|
||||
expect(first).not.toHaveProperty('operationId')
|
||||
expect(second.paneKey).not.toBe(first.paneKey)
|
||||
})
|
||||
|
||||
it('says the agent started only once the host answers, with its pane attached', async () => {
|
||||
it('says the agent started only once the host answers, and its tab is then an ordinary one', async () => {
|
||||
const reply = deferred<unknown>()
|
||||
callRuntimeRpc.mockReturnValue(reply.promise)
|
||||
const { tabId, outcome } = launch()
|
||||
@@ -156,35 +154,27 @@ describe('a desktop launch through the host', () => {
|
||||
|
||||
await expect(outcome).resolves.toEqual({ kind: 'started' })
|
||||
expect(launchTab(tabId)).toBeDefined()
|
||||
expect(launchTab(tabId)?.agentLaunchPane).toBeUndefined()
|
||||
})
|
||||
|
||||
it('takes its tab back on a refusal, before the pane ever spawns', async () => {
|
||||
const reply = deferred<unknown>()
|
||||
callRuntimeRpc.mockReturnValue(reply.promise)
|
||||
const { tabId, outcome } = launch()
|
||||
|
||||
reply.reject(rpcError('agent_session_operation_conflict'))
|
||||
|
||||
await expect(outcome).resolves.toEqual({
|
||||
kind: 'not-started',
|
||||
unconfirmed: false,
|
||||
code: 'agent_session_operation_conflict'
|
||||
})
|
||||
expect(launchTab(tabId)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('leaves a launch the host took to its pane, which says how it ended', async () => {
|
||||
// Temporary until the pane's own spawn starts its agent: the window says it, not the pane.
|
||||
it('keeps a tab the host took and then failed in, and says it could not confirm the start', async () => {
|
||||
const reply = deferred<unknown>()
|
||||
callRuntimeRpc.mockReturnValue(reply.promise)
|
||||
const { tabId, outcome } = launch()
|
||||
hostTakesPane(tabId)
|
||||
|
||||
reply.reject(rpcError('agent_session_operation_unknown'))
|
||||
reply.reject(rpcError('terminal_reveal_failed'))
|
||||
|
||||
await expect(outcome).resolves.toEqual({ kind: 'pane-says' })
|
||||
await expect(outcome).resolves.toEqual({
|
||||
kind: 'not-started',
|
||||
unconfirmed: true,
|
||||
code: 'terminal_reveal_failed'
|
||||
})
|
||||
expect(launchTab(tabId)).toBeDefined()
|
||||
})
|
||||
|
||||
// Temporary until the pane's own spawn starts its agent: the tab goes and the window says why.
|
||||
it('never leaves a pane the host did not take, which would open as a shell', async () => {
|
||||
const reply = deferred<unknown>()
|
||||
callRuntimeRpc.mockReturnValue(reply.promise)
|
||||
@@ -202,7 +192,13 @@ describe('a desktop launch through the host', () => {
|
||||
|
||||
// Why: the close was the user's own, as on the phone's "+".
|
||||
it('says nothing more when the user closed the tab while it started', async () => {
|
||||
callRuntimeRpc.mockRejectedValueOnce(rpcError('agent_launch_tab_closed'))
|
||||
await expect(launch().outcome).resolves.toEqual({ kind: 'closed-by-user' })
|
||||
const reply = deferred<unknown>()
|
||||
callRuntimeRpc.mockReturnValue(reply.promise)
|
||||
const { tabId, outcome } = launch()
|
||||
store.getState().closeTab(tabId)
|
||||
|
||||
reply.resolve(terminalResult(lastPaneKey()))
|
||||
|
||||
await expect(outcome).resolves.toEqual({ kind: 'closed-by-user' })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,16 +2,16 @@
|
||||
* A desktop launch started through the host's `agent.launch`, with no prompt.
|
||||
*
|
||||
* This window owns the workspace's tab layout, so it makes the agent's tab at the click, in the
|
||||
* split it was asked for, as any new agent tab is made. The host records the launch under this
|
||||
* click's operation id and starts the agent into that tab's pane. The pane's spawn waits until the
|
||||
* host has taken it (`agent-launch-pane-spawn-hold`), then attaches to the agent or says why it could
|
||||
* not start. The prompt stays this window's to paste, as main pastes it, once the agent has started.
|
||||
* split it was asked for, as any new agent tab is made. The host starts the agent into that tab's
|
||||
* pane and writes no launch record: nothing reads one for a click, and a record store that cannot
|
||||
* open must not stop it. The pane's spawn waits until the host has taken it
|
||||
* (`agent-launch-pane-spawn-hold`), then attaches to the agent. The prompt stays this window's to
|
||||
* paste, as main pastes it, once the agent has started.
|
||||
*/
|
||||
|
||||
import { useAppStore } from '@/store'
|
||||
import { createBrowserUuid } from '@/lib/browser-uuid'
|
||||
import { persistAgentLaunchTabOrder } from '@/lib/launch-agent-tab-order'
|
||||
import { rememberAgentLaunchPanePrompt } from '@/lib/agent-launch-pane-prompt'
|
||||
import {
|
||||
holdAgentLaunchPaneSpawn,
|
||||
isAgentLaunchPaneSpawnHeld
|
||||
@@ -19,9 +19,7 @@ import {
|
||||
import { seedNativeChatAppliedSessionOptions } from '@/components/native-chat/native-chat-session-option-cache'
|
||||
import { callRuntimeRpc } from '@/runtime/runtime-rpc-client'
|
||||
import { RuntimeRpcCallError } from '@/runtime/runtime-rpc-result'
|
||||
import { createAgentSessionOperationId } from '@/runtime/agent-session-operation-id'
|
||||
import { isAgentLaunchResult } from '../../../shared/agent-launch-intent'
|
||||
import { AGENT_LAUNCH_TAB_CLOSED_CODE } from '../../../shared/agent-launch-tab-closed'
|
||||
import { makePaneKey } from '../../../shared/stable-pane-id'
|
||||
import { isNativeChatEnabled } from '../../../shared/structured-native-chat-launch-route'
|
||||
import type { TuiAgent } from '../../../shared/tui-agent'
|
||||
@@ -53,36 +51,32 @@ export type HostAgentLaunchArgs = {
|
||||
export type HostAgentLaunchOutcome =
|
||||
/** The agent was started in this tab's pane, which is attached to it. */
|
||||
| { kind: 'started' }
|
||||
/** The pane shows how the launch ended: couldn't start, or couldn't confirm it started. */
|
||||
| { kind: 'pane-says' }
|
||||
/** The user closed the tab while it started, and with it the launch: nothing more to say. */
|
||||
| { kind: 'closed-by-user' }
|
||||
/** The tab is gone, so the window says it: nothing started, or whether it did is unknown. */
|
||||
/** The window says it: nothing started (its tab is gone), or the host took the pane and then
|
||||
* failed, so whether its agent runs there is unknown. */
|
||||
| { kind: 'not-started'; unconfirmed: boolean; code?: string }
|
||||
|
||||
/** Refused at admission: nothing ran under this click, and the host takes back the tab it was shown. */
|
||||
const ADMISSION_REFUSAL_CODES = new Set([
|
||||
'agent_session_operation_invalid',
|
||||
'agent_session_operation_conflict',
|
||||
'agent_session_operation_expired',
|
||||
'agent_session_operation_capacity'
|
||||
])
|
||||
|
||||
function tabExists(worktreeId: string, tabId: string): boolean {
|
||||
return (useAppStore.getState().tabsByWorktree[worktreeId] ?? []).some((tab) => tab.id === tabId)
|
||||
}
|
||||
|
||||
function closeLaunchTab(worktreeId: string, tabId: string): void {
|
||||
if (tabExists(worktreeId, tabId)) {
|
||||
useAppStore.getState().closeTab(tabId, { recordInteraction: false })
|
||||
// Not the user's close, so not reopenable; its tombstone still stops an agent that starts late.
|
||||
useAppStore.getState().closeTab(tabId, {
|
||||
reason: 'cleanup',
|
||||
recordInteraction: false,
|
||||
captureRecentlyClosed: false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Only a terminal in this pane is one the window can paste into; anything else, its pane explains.
|
||||
// Only a terminal in this pane is one the window can paste into.
|
||||
function outcomeFromResult(result: unknown): HostAgentLaunchOutcome {
|
||||
return isAgentLaunchResult(result) && result.outcome.kind === 'terminal'
|
||||
? { kind: 'started' }
|
||||
: { kind: 'pane-says' }
|
||||
: { kind: 'not-started', unconfirmed: true }
|
||||
}
|
||||
|
||||
function stringSessionOptions(options: Record<string, SessionOptionValue> | undefined): {
|
||||
@@ -113,27 +107,29 @@ async function settleLaunch(
|
||||
send: Promise<unknown>,
|
||||
releaseHold: () => void
|
||||
): Promise<HostAgentLaunchOutcome> {
|
||||
const closedByUser = (): boolean => !tabExists(args.worktreeId, pane.tabId)
|
||||
try {
|
||||
return outcomeFromResult(await send)
|
||||
const result = await send
|
||||
// Closed while it started: the window stopped its agent as the tab's reveal arrived.
|
||||
return closedByUser() ? { kind: 'closed-by-user' } : outcomeFromResult(result)
|
||||
} catch (error) {
|
||||
const code = error instanceof RuntimeRpcCallError ? error.code : undefined
|
||||
if (code === AGENT_LAUNCH_TAB_CLOSED_CODE) {
|
||||
if (closedByUser()) {
|
||||
return { kind: 'closed-by-user' }
|
||||
}
|
||||
// The host took the pane once it showed the tab; a pane it never took would open as a shell,
|
||||
// and a refused one is the host's to take back.
|
||||
const code = error instanceof RuntimeRpcCallError ? error.code : undefined
|
||||
// The host revealed the pane once its agent spawned; a pane it never took would open as a
|
||||
// shell, so its tab goes. A taken pane holds whatever runs there.
|
||||
const hostTookPane = !isAgentLaunchPaneSpawnHeld(pane.tabId, pane.leafId)
|
||||
if (!hostTookPane || (code !== undefined && ADMISSION_REFUSAL_CODES.has(code))) {
|
||||
if (!hostTookPane) {
|
||||
closeLaunchTab(args.worktreeId, pane.tabId)
|
||||
return {
|
||||
kind: 'not-started',
|
||||
unconfirmed: code === 'agent_session_operation_unknown',
|
||||
...(code ? { code } : {})
|
||||
}
|
||||
}
|
||||
return { kind: 'pane-says' }
|
||||
return { kind: 'not-started', unconfirmed: hostTookPane, ...(code ? { code } : {}) }
|
||||
} finally {
|
||||
releaseHold()
|
||||
// The launch is over: its tab is an ordinary one, as a settled verdict leaves it.
|
||||
if (!closedByUser()) {
|
||||
useAppStore.getState().setTabAgentLaunchPane(pane.tabId, undefined)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,10 +148,9 @@ export function launchAgentThroughHost(args: HostAgentLaunchArgs): {
|
||||
const leafId = createBrowserUuid()
|
||||
// Before the tab exists, so its first mount already waits.
|
||||
const releaseHold = holdAgentLaunchPaneSpawn(tabId, leafId)
|
||||
const send = callRuntimeRpc<unknown>({ kind: 'local' }, 'agent.launchReplay', {
|
||||
const send = callRuntimeRpc<unknown>({ kind: 'local' }, 'agent.launch', {
|
||||
...launchParams(args),
|
||||
// A new click is a new operation; the pane is this click's too.
|
||||
operationId: createAgentSessionOperationId(),
|
||||
// The pane is this click's.
|
||||
paneKey: makePaneKey(tabId, leafId)
|
||||
})
|
||||
store.createTab(args.worktreeId, args.groupId, undefined, {
|
||||
@@ -167,7 +162,6 @@ export function launchAgentThroughHost(args: HostAgentLaunchArgs): {
|
||||
...(args.pendingActivationSpawn ? { pendingActivationSpawn: true } : {}),
|
||||
...(args.viewMode ? { viewMode: args.viewMode } : {})
|
||||
})
|
||||
rememberAgentLaunchPanePrompt(tabId, args.prompt)
|
||||
seedNativeChatAppliedSessionOptions(tabId, args.agent, args.sessionOptions)
|
||||
// Why: without it an activated launch can stay hidden behind an editor.
|
||||
store.setActiveTabType('terminal', args.worktreeId)
|
||||
|
||||
@@ -145,7 +145,7 @@ describe('an AI button whose agent is ready before the host answers', () => {
|
||||
|
||||
attachHostPty('pty-1')
|
||||
testState.ptyWatchers.get('pty-1')?.('\x1b[?2004h> ')
|
||||
answer({ kind: 'pane-says' })
|
||||
answer({ kind: 'not-started', unconfirmed: true })
|
||||
await vi.advanceTimersByTimeAsync(10_000)
|
||||
|
||||
await expect(promptDeliveryResult).resolves.toEqual({ delivered: false, failureNotified: true })
|
||||
|
||||
@@ -83,15 +83,13 @@ describe('an AI button launched through the host', () => {
|
||||
expect(toast.error).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('says nothing and pastes nothing when the user closed the tab, or its pane explains', async () => {
|
||||
for (const outcome of [{ kind: 'closed-by-user' }, { kind: 'pane-says' }] as const) {
|
||||
deferredOutcome()(outcome)
|
||||
await expect(launch().promptDeliveryResult).resolves.toEqual({
|
||||
delivered: false,
|
||||
failureNotified: true
|
||||
})
|
||||
await expect(sendGate()).resolves.toBe(false)
|
||||
}
|
||||
it('says nothing and pastes nothing when the user closed the tab', async () => {
|
||||
deferredOutcome()({ kind: 'closed-by-user' })
|
||||
await expect(launch().promptDeliveryResult).resolves.toEqual({
|
||||
delivered: false,
|
||||
failureNotified: true
|
||||
})
|
||||
await expect(sendGate()).resolves.toBe(false)
|
||||
expect(toast.error).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -86,7 +86,7 @@ export function launchNewTabPromptThroughHost(
|
||||
if (launched.kind === 'started') {
|
||||
return pasted
|
||||
}
|
||||
// The pane, or this notice for a tab that went, already says why: never a second notice.
|
||||
// This notice says why, once; a tab the user closed says nothing.
|
||||
showLaunchNotStartedNotice(launched, args.prompt)
|
||||
return { delivered: false, failureNotified: true }
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user