fix(launch-prompt): title a launch file's session by its prompt; say when the agent exited

Stack QA 1.2: with a launch file, the tab label and the sidebar agent row showed
"The full task is in the file ..." while the status prompt held the real text.
They read the provider session title, taken from the transcript's first
message (the pointer, cut to 96 characters). Title requests now carry their
pane, and the host that spawned it maps the title through
launchPromptShownForPane, which also recognises a pointer cut short. The pane
stays on this host: it is not sent to an SSH relay.

Stack QA 1.3: "The agent started, but your prompt wasn't sent" also showed when
the agent crashed at startup with the prompt on its command line. The receipt
now reports 'agent-exited' when the PTY exits or the shell is back in front,
and that outcome gets its own notice with Copy prompt, in every catalog.
This commit is contained in:
Brennan Benson
2026-10-02 07:53:23 -07:00
parent e92f38d722
commit c4b2cbfc46
23 changed files with 323 additions and 23 deletions
@@ -1,4 +1,4 @@
import { isLaunchFilePointer, type LaunchFile } from '../../shared/launch-prompt-file'
import { opensLikeLaunchFilePointer, type LaunchFile } from '../../shared/launch-prompt-file'
// Why bounded: an entry outlives its launch until newer ones push it out; the rename reads only a
// prompt's opening, and a pane's rename runs within minutes of its launch.
@@ -27,8 +27,8 @@ export function rememberLaunchFilePrompt(
}
}
/** The one place Orca reads a pane's reported prompt: a launch file's pointer reads as the prompt
* it points at, when this host kept it. */
/** The one place Orca reads a pane's reported prompt or session title: a launch file's pointer,
* whole or cut short, reads as the prompt it points at, when this host kept it. */
export function launchPromptShownForPane(paneKey: string, prompt: string): string {
return isLaunchFilePointer(prompt) ? (promptByPane.get(paneKey) ?? prompt) : prompt
return opensLikeLaunchFilePointer(prompt) ? (promptByPane.get(paneKey) ?? prompt) : prompt
}
@@ -0,0 +1,39 @@
import type {
AiVaultSessionTitleRequest,
AiVaultSessionTitlesResult
} from '../../shared/ai-vault-session-title'
import { launchPromptShownForPane } from '../agent-hooks/launch-file-prompt-by-pane'
import { normalizeTitleText } from './session-scanner-text-normalization'
/**
* Titles a launch file's session with the prompt it carried. Its transcript opens with Orca's
* pointer, so the tab label and agent row would otherwise show "The full task is in the file …".
*/
export function showLaunchPromptsInSessionTitles(
requests: readonly AiVaultSessionTitleRequest[],
result: AiVaultSessionTitlesResult
): AiVaultSessionTitlesResult {
const paneBySession = new Map(
requests.flatMap((request) =>
request.paneKey ? [[`${request.agent}\0${request.sessionId}`, request.paneKey] as const] : []
)
)
if (paneBySession.size === 0) {
return result
}
return {
titles: result.titles.map((title) => {
const paneKey = paneBySession.get(`${title.agent}\0${title.sessionId}`)
const shown = paneKey ? launchPromptShownForPane(paneKey, title.title) : title.title
return shown === title.title
? title
: { ...title, title: normalizeTitleText(shown) ?? title.title }
})
}
}
/** A request as another host receives it: the pane is this host's to read. */
export function withoutPaneKey(request: AiVaultSessionTitleRequest): AiVaultSessionTitleRequest {
const { paneKey: _paneKey, ...forwarded } = request
return forwarded
}
@@ -0,0 +1,67 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { resolveLocalAiVaultSessionTitles } from '../ai-vault/session-title-resolver'
import { rememberLaunchFilePrompt } from '../agent-hooks/launch-file-prompt-by-pane'
import { carryInLaunchFile, buildLaunchFilePointer } from '../../shared/launch-prompt-file'
import { resolveAiVaultSessionTitlesByHost } from './ai-vault-session-title-routing'
import { requestActiveSshAiVaultSessionTitles } from './ssh'
vi.mock('../ai-vault/session-title-resolver', () => ({
resolveLocalAiVaultSessionTitles: vi.fn()
}))
vi.mock('./ssh', () => ({ requestActiveSshAiVaultSessionTitles: vi.fn() }))
const PANE = 'tab-title:11111111-1111-4111-8111-111111111111'
// The transcript's first message is the pointer; its title is cut to 96 characters.
const POINTER_TITLE = buildLaunchFilePointer(
'/Users/ada/Library/Caches/orca-launch-file-1-a/task-context.md'
).slice(0, 96)
// Why: a launch file's agent opens its transcript with Orca's pointer, and the tab label and the
// sidebar agent row read the session title from it (stack QA 1.2).
describe('a session title for a launch file’s agent', () => {
beforeEach(() => {
rememberLaunchFilePrompt(PANE, carryInLaunchFile('QA-STACK fix the flaky test').launchFile)
})
it('shows the prompt the launch file carried, not the pointer', async () => {
vi.mocked(resolveLocalAiVaultSessionTitles).mockResolvedValue({
titles: [{ agent: 'claude', sessionId: 's-1', title: POINTER_TITLE }]
})
const result = await resolveAiVaultSessionTitlesByHost({
requests: [{ agent: 'claude', sessionId: 's-1', paneKey: PANE }]
})
expect(result.titles).toEqual([
{ agent: 'claude', sessionId: 's-1', title: 'QA-STACK fix the flaky test' }
])
})
it('leaves another pane’s title, and a request without a pane, as the transcript has it', async () => {
vi.mocked(resolveLocalAiVaultSessionTitles).mockResolvedValue({
titles: [
{ agent: 'claude', sessionId: 's-2', title: POINTER_TITLE },
{ agent: 'codex', sessionId: 's-3', title: 'fix the build' }
]
})
const result = await resolveAiVaultSessionTitlesByHost({
requests: [
{ agent: 'claude', sessionId: 's-2' },
{ agent: 'codex', sessionId: 's-3', paneKey: PANE }
]
})
expect(result.titles.map((title) => title.title)).toEqual([POINTER_TITLE, 'fix the build'])
})
it('maps an SSH session’s title here, never sending the relay this host’s pane', async () => {
vi.mocked(requestActiveSshAiVaultSessionTitles).mockResolvedValue({
titles: [{ agent: 'claude', sessionId: 's-4', title: POINTER_TITLE }]
})
const result = await resolveAiVaultSessionTitlesByHost({
executionHostScope: 'ssh:dev-box',
requests: [{ agent: 'claude', sessionId: 's-4', paneKey: PANE }]
})
expect(requestActiveSshAiVaultSessionTitles).toHaveBeenCalledWith('dev-box', {
requests: [{ agent: 'claude', sessionId: 's-4' }]
})
expect(result.titles[0]?.title).toBe('QA-STACK fix the flaky test')
})
})
+16 -1
View File
@@ -10,6 +10,10 @@ import {
import { resolveLocalAiVaultSessionTitles } from '../ai-vault/session-title-resolver'
import { parseAiVaultSessionTitlesResult } from '../ai-vault/session-title-result-validation'
import { requestActiveSshAiVaultSessionTitles } from './ssh'
import {
showLaunchPromptsInSessionTitles,
withoutPaneKey
} from '../ai-vault/session-title-launch-prompt'
export type RuntimeAiVaultSessionTitleResolver = (
environmentId: string,
@@ -19,6 +23,17 @@ export type RuntimeAiVaultSessionTitleResolver = (
export async function resolveAiVaultSessionTitlesByHost(
args: AiVaultSessionTitlesArgs,
resolveRuntime?: RuntimeAiVaultSessionTitleResolver
): Promise<AiVaultSessionTitlesResult> {
// Why here: this host spawned the panes it shows, local and SSH alike, so it kept their prompts.
return showLaunchPromptsInSessionTitles(
args.requests,
await resolveTitlesOnHost(args, resolveRuntime)
)
}
async function resolveTitlesOnHost(
args: AiVaultSessionTitlesArgs,
resolveRuntime?: RuntimeAiVaultSessionTitleResolver
): Promise<AiVaultSessionTitlesResult> {
const executionHostScope = requestedExecutionHostScope(args.executionHostScope)
if (executionHostScope === LOCAL_EXECUTION_HOST_ID) {
@@ -28,7 +43,7 @@ export async function resolveAiVaultSessionTitlesByHost(
if (parsed?.kind === 'ssh') {
try {
const result = await requestActiveSshAiVaultSessionTitles(parsed.targetId, {
requests: args.requests
requests: args.requests.map(withoutPaneKey)
})
return result === null ? { titles: [] } : parseAiVaultSessionTitlesResult(result)
} catch {
@@ -0,0 +1,32 @@
import { describe, expect, it, vi } from 'vitest'
import { resolveLocalAiVaultSessionTitles } from '../ai-vault/session-title-resolver'
import { rememberLaunchFilePrompt } from '../agent-hooks/launch-file-prompt-by-pane'
import { buildLaunchFilePointer, carryInLaunchFile } from '../../shared/launch-prompt-file'
import { RuntimeAiVaultCommands } from './runtime-ai-vault-commands'
vi.mock('../ai-vault/cached-session-list', () => ({ listAiVaultSessions: vi.fn() }))
vi.mock('../ai-vault/session-title-resolver', () => ({
resolveLocalAiVaultSessionTitles: vi.fn()
}))
// Why: a paired web client and the phone ask this host for titles over RPC; the host spawned the
// pane, so it shows the launch file's prompt there too.
describe('session titles a paired client asks this host for', () => {
it('show a launch file’s prompt, not the pointer its transcript opens with', async () => {
const pane = 'tab-rpc:22222222-2222-4222-8222-222222222222'
rememberLaunchFilePrompt(pane, carryInLaunchFile('QA-STACK review the diff').launchFile)
vi.mocked(resolveLocalAiVaultSessionTitles).mockResolvedValue({
titles: [
{
agent: 'codex',
sessionId: 's-rpc',
title: buildLaunchFilePointer('/tmp/orca-launch-file-1-b/task-context.md').slice(0, 96)
}
]
})
const result = await new RuntimeAiVaultCommands(() => null).resolveTitles([
{ agent: 'codex', sessionId: 's-rpc', paneKey: pane }
])
expect(result.titles[0]?.title).toBe('QA-STACK review the diff')
})
})
@@ -9,6 +9,7 @@ import type {
import type { AiVaultListArgs, AiVaultListResult } from '../../shared/ai-vault-types'
import { listAiVaultSessions } from '../ai-vault/cached-session-list'
import { resolveLocalAiVaultSessionTitles } from '../ai-vault/session-title-resolver'
import { showLaunchPromptsInSessionTitles } from '../ai-vault/session-title-launch-prompt'
export class RuntimeAiVaultCommands {
constructor(
@@ -21,11 +22,14 @@ export class RuntimeAiVaultCommands {
return listAiVaultSessions(args)
}
resolveTitles(
async resolveTitles(
requests: AiVaultSessionTitleRequest[],
signal?: AbortSignal
): Promise<AiVaultSessionTitlesResult> {
return resolveLocalAiVaultSessionTitles(requests, signal)
return showLaunchPromptsInSessionTitles(
requests,
await resolveLocalAiVaultSessionTitles(requests, signal)
)
}
prepare(args: AiVaultPrepareSessionResumeArgs): Promise<AiVaultPrepareSessionResumeResult> {
+5
View File
@@ -669,6 +669,11 @@
}
}
},
"exited": {
"notice": {
"message": "The agent exited at startup, before it received your prompt. Copy the prompt and start the agent again."
}
},
"unconfirmed": {
"notice": {
"message": "Orca couldn't confirm the agent received your prompt. Check the agent before sending it again."
+5
View File
@@ -423,6 +423,11 @@
}
}
},
"exited": {
"notice": {
"message": "El agente se cerró al iniciarse, antes de recibir tu prompt. Copia el prompt e inicia el agente de nuevo."
}
},
"unconfirmed": {
"notice": {
"message": "Orca no pudo confirmar que el agente recibió tu prompt. Revisa el agente antes de enviarlo de nuevo."
+5
View File
@@ -633,6 +633,11 @@
}
}
},
"exited": {
"notice": {
"message": "L’agent s’est arrêté au démarrage, avant de recevoir votre prompt. Copiez le prompt et relancez l’agent."
}
},
"unconfirmed": {
"notice": {
"message": "Orca n’a pas pu confirmer que l’agent a reçu votre prompt. Vérifiez l’agent avant de le renvoyer."
+5
View File
@@ -534,6 +534,11 @@
}
}
},
"exited": {
"notice": {
"message": "Agent は起動時に終了したため、プロンプトを受け取っていません。プロンプトをコピーして、Agent をもう一度起動してください。"
}
},
"unconfirmed": {
"notice": {
"message": "Agent がプロンプトを受け取ったことを Orca で確認できませんでした。再送する前に Agent を確認してください。"
+5
View File
@@ -534,6 +534,11 @@
}
}
},
"exited": {
"notice": {
"message": "에이전트가 시작 중에 종료되어 프롬프트를 받지 못했습니다. 프롬프트를 복사한 뒤 에이전트를 다시 시작하세요."
}
},
"unconfirmed": {
"notice": {
"message": "에이전트가 프롬프트를 받았는지 Orca가 확인하지 못했습니다. 다시 보내기 전에 에이전트를 확인하세요."
+5
View File
@@ -534,6 +534,11 @@
}
}
},
"exited": {
"notice": {
"message": "代理在启动时退出,未收到你的提示词。请复制提示词并重新启动代理。"
}
},
"unconfirmed": {
"notice": {
"message": "Orca 无法确认代理已收到你的提示词。再次发送前请先检查代理。"
@@ -47,6 +47,21 @@ export function showAgentLaunchPromptUnconfirmedNotice(args: {
})
}
/**
* The agent had the prompt on its command line but exited at startup, before it read it. It did
* not really start, so the user is handed the text to start it again with.
*/
export function showAgentLaunchExitedNotice(args: { agent: TuiAgent; prompt: string }): void {
showPromptCopyNotice(
translate(
'auto.lib.agent.launch.prompt.exited.notice.message',
'The agent exited at startup, before it received your prompt. Copy the prompt and start the agent again.'
),
args.prompt
)
track('agent_error', { error_class: 'unknown', agent_kind: tuiAgentToAgentKind(args.agent) })
}
/**
* The host refused to start the agent because it could not write the launch file that carries the
* prompt. Nothing ran, so the user is handed the text to launch again with.
@@ -92,12 +92,13 @@ describe('whether a prompt that rode the launch command reached the agent', () =
})
// Why: a shell back at its prompt looks ready too; readiness alone never counts.
it('is not delivered when the read finds the shell, however ready it looked', async () => {
// Why: the agent had the prompt on its line and quit at startup, before reading it.
it('reports the agent exited when the read finds the shell, however ready it looked', async () => {
mocks.readForeground.mockResolvedValue('shell')
mocks.setState({ agentStatusByPaneKey: { [PANE]: { updatedAt: 50 } } })
const pending = receipt()
mocks.setState({ ptyIdsByTabId: { 'tab-1': ['pty-1'] } })
await expect(pending).resolves.toBe('not-delivered')
await expect(pending).resolves.toBe('agent-exited')
})
it('is unconfirmed when the host cannot tell and no hook turn arrives', async () => {
@@ -119,12 +120,12 @@ describe('whether a prompt that rode the launch command reached the agent', () =
await expect(pending).resolves.toBe('delivered')
})
it('is not delivered when the PTY exits first', async () => {
it('reports the agent exited when the PTY exits first', async () => {
mocks.readiness.mockReturnValue(new Promise(() => {}))
const pending = receipt()
mocks.setState({ ptyIdsByTabId: { 'tab-1': ['pty-1'] } })
mocks.setState({ ptyIdsByTabId: { 'tab-1': [] } })
await expect(pending).resolves.toBe('not-delivered')
await expect(pending).resolves.toBe('agent-exited')
})
it('is not delivered when the PTY never spawns, as when the host refused its launch file', async () => {
@@ -9,7 +9,7 @@ import { getSettingsForAgentTabRuntimeOwner, PTY_SPAWN_TIMEOUT_MS } from './agen
type AppState = ReturnType<typeof useAppStore.getState>
export type LaunchPromptReceipt = 'delivered' | 'not-delivered' | 'unconfirmed'
export type LaunchPromptReceipt = 'delivered' | 'not-delivered' | 'agent-exited' | 'unconfirmed'
/** How long a hook turn may still arrive after a read that could not tell. */
const HOOK_GRACE_AFTER_UNKNOWN_READ_MS = 2000
@@ -41,8 +41,9 @@ async function readLaunchedAgentForeground(
* a fresh read on the execution host that finds the launched agent, not its shell, in front of the
* terminal (`readLaunchedAgentForeground`). A ready signal never counts on its own: it only times
* that read, and bracketed paste turned off (`2004l`) revokes it. Not delivered when the PTY never
* spawns (a refused launch file included), exits first, the tab closes, or the read finds the
* shell. Unconfirmed when nothing proves either, as on a Windows host without hooks.
* spawns (a refused launch file included) or the tab closes. Agent exited when the PTY exits or
* the read finds the shell back in front: the agent had the prompt on its line and quit before
* reading it. Unconfirmed when nothing proves either, as on a Windows host without hooks.
*/
export function waitForLaunchPromptReceipt(args: {
tabId: string
@@ -78,7 +79,7 @@ export function waitForLaunchPromptReceipt(args: {
if (foreground === 'agent') {
finish('delivered')
} else if (foreground === 'shell') {
finish('not-delivered')
finish('agent-exited')
} else {
timers.push(
window.setTimeout(() => finish('unconfirmed'), HOOK_GRACE_AFTER_UNKNOWN_READ_MS)
@@ -101,8 +102,10 @@ export function waitForLaunchPromptReceipt(args: {
if (ptyId && !boundPtyId) {
boundPtyId = ptyId
readAfterReadiness(ptyId)
} else if ((boundPtyId && !ptyId) || !tabExists(state, tabId)) {
} else if (!tabExists(state, tabId)) {
finish('not-delivered')
} else if (boundPtyId && !ptyId) {
finish('agent-exited')
}
}
unsubscribe = useAppStore.subscribe(observe)
@@ -14,6 +14,8 @@ export type AiVaultTitleRequest = {
providerSession: AgentProviderSessionMetadata
refresh: boolean
tabId: string
/** The pane the session runs in, so its host can title a launch file's session by its prompt. */
paneKey: string
worktreeId: string
}
@@ -62,6 +64,7 @@ function registerCandidate(
providerSession: args.providerSession,
refresh: args.refresh,
tabId,
paneKey: args.paneKey,
worktreeId,
priority
})
@@ -192,6 +192,8 @@ describe('AI Vault tab title sync', () => {
{
agent,
sessionId: `${agent}-session`,
// Why: the host titles a launch file's session by the prompt it kept for this pane.
paneKey: 'tab-1:leaf-1',
transcriptPath: `/sessions/${agent}.jsonl`
}
]
@@ -373,6 +375,7 @@ describe('AI Vault tab title sync', () => {
providerSession: { key: 'session_id', id: `session-${index}` },
refresh: true,
tabId: `tab-${index}`,
paneKey: `tab-${index}:leaf-${index}`,
worktreeId: `worktree-${index}`
})
const groups = batchAiVaultTitleRequests(
@@ -391,6 +394,7 @@ describe('AI Vault tab title sync', () => {
providerSession: { key: 'session_id' as const, id: `session-${index}` },
refresh: true,
tabId: `tab-${index}`,
paneKey: `tab-${index}:leaf-${index}`,
worktreeId: `worktree-${index}`
})
const requests = [
@@ -94,6 +94,7 @@ export function startAiVaultTabTitleSync(dependencies: SyncDependencies): () =>
requests: requests.map((request) => ({
agent: request.agent,
sessionId: request.providerSession.id,
paneKey: request.paneKey,
...(request.providerSession.transcriptPath
? { transcriptPath: request.providerSession.transcriptPath }
: {})
@@ -0,0 +1,67 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
const mocks = vi.hoisted(() => ({
receipt: vi.fn(),
exited: vi.fn(),
unconfirmed: vi.fn(),
notDelivered: vi.fn()
}))
vi.mock('@/lib/agent-launch-prompt-receipt', () => ({ waitForLaunchPromptReceipt: mocks.receipt }))
vi.mock('@/lib/agent-launch-prompt-not-delivered-notice', () => ({
showAgentLaunchExitedNotice: mocks.exited,
showAgentLaunchPromptUnconfirmedNotice: mocks.unconfirmed,
showAgentLaunchPromptNotDeliveredNotice: mocks.notDelivered
}))
vi.mock('@/lib/agent-launch-prompt-delivery', () => ({
deliverLaunchPromptToAgentTab: vi.fn(),
seedNativeChatLaunchDraftForAgentTab: vi.fn(),
seedNativeChatLaunchPromptForAgentTab: vi.fn()
}))
vi.mock('@/store', () => ({
useAppStore: {
getState: () => ({ tabsByWorktree: { 'wt-1': [{ id: 'tab-1', ptyId: 'pty-1' }] } })
}
}))
vi.mock('@/lib/telemetry', () => ({ track: vi.fn(), tuiAgentToAgentKind: vi.fn() }))
import { deliverNewTabLaunchPrompt } from './launch-agent-new-tab-prompt-delivery'
function deliver() {
return deliverNewTabLaunchPrompt({
worktreeId: 'wt-1',
tabId: 'tab-1',
agent: 'claude',
prompt: 'fix the failing checks',
promptDelivery: 'submit-after-ready',
pasteDraftAfterLaunch: null,
submitPastedPrompt: false,
promptInLaunchFile: false,
launchedAt: 0
})
}
// Why: the notice must say what happened (stack QA 1.3); "the agent started, but your prompt
// wasn't sent" was shown for an agent that crashed with the prompt already on its command line.
describe('the notice for a prompt the launch line carried', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('says the agent exited at startup when it quit before reading the prompt', async () => {
mocks.receipt.mockResolvedValue('agent-exited')
await expect(deliver()).resolves.toEqual({ delivered: false, failureNotified: true })
expect(mocks.exited).toHaveBeenCalledWith({
agent: 'claude',
prompt: 'fix the failing checks'
})
expect(mocks.notDelivered).not.toHaveBeenCalled()
})
it('keeps "wasn’t sent" for a prompt that never reached a running agent', async () => {
mocks.receipt.mockResolvedValue('not-delivered')
await expect(deliver()).resolves.toMatchObject({ delivered: false })
expect(mocks.notDelivered).toHaveBeenCalled()
expect(mocks.exited).not.toHaveBeenCalled()
})
})
@@ -1,6 +1,9 @@
import { createPasteReadinessTimeoutNotice } from '@/lib/launch-agent-paste-timeout-notice'
import { waitForLaunchPromptReceipt } from '@/lib/agent-launch-prompt-receipt'
import { showAgentLaunchPromptUnconfirmedNotice } from '@/lib/agent-launch-prompt-not-delivered-notice'
import {
showAgentLaunchExitedNotice,
showAgentLaunchPromptUnconfirmedNotice
} from '@/lib/agent-launch-prompt-not-delivered-notice'
import {
deliverLaunchPromptToAgentTab,
seedNativeChatLaunchDraftForAgentTab,
@@ -65,12 +68,19 @@ export function deliverNewTabLaunchPrompt(args: {
pasteDraftAfterLaunch === null
? waitForLaunchPromptReceipt({ tabId, agent, launchedAt: args.launchedAt }).then(
(receipt) => {
if (receipt !== 'unconfirmed') {
return timeoutNotice.settle(receipt === 'delivered', args.onPromptDelivered)
switch (receipt) {
case 'delivered':
case 'not-delivered':
return timeoutNotice.settle(receipt === 'delivered', args.onPromptDelivered)
case 'agent-exited':
// Why its own words: the agent did not really start, so "paste it once ready" is wrong.
showAgentLaunchExitedNotice({ agent, prompt })
return { delivered: false, failureNotified: true }
case 'unconfirmed':
// The agent may have the prompt: say so, and never invite pasting it a second time.
showAgentLaunchPromptUnconfirmedNotice({ agent, prompt })
return { delivered: false, failureNotified: true }
}
// The agent may have the prompt: say so, and never invite pasting it a second time.
showAgentLaunchPromptUnconfirmedNotice({ agent, prompt })
return { delivered: false, failureNotified: true }
}
)
: deliverLaunchPromptToAgentTab({
+3
View File
@@ -13,6 +13,9 @@ export type AiVaultSessionTitleRequest = {
agent: AiVaultSessionTitle['agent']
sessionId: string
transcriptPath?: string
/** The pane showing this session, so the host that spawned it titles a launch file's session
* with the prompt, not the pointer the transcript holds. */
paneKey?: string
}
export type AiVaultSessionTitlesArgs = {
+5
View File
@@ -47,6 +47,11 @@ export function isLaunchFilePointer(prompt: string): boolean {
)
}
/** Whether text opens like Orca's pointer, as a session title cut from one does. */
export function opensLikeLaunchFilePointer(text: string): boolean {
return text.trim().startsWith(POINTER_LEAD)
}
export function carryInLaunchFile(content: string): { prompt: string; launchFile: LaunchFile } {
const bytes = crypto.getRandomValues(new Uint8Array(16))
const placeholder = `orca-launch-file-${Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('')}`
+2 -1
View File
@@ -66,7 +66,8 @@ export const AiVaultSessionTitlesParams = z.object({
z.object({
agent: z.enum(['claude', 'codex']),
sessionId: z.string().min(1).max(512),
transcriptPath: z.string().min(1).max(32_768).optional()
transcriptPath: z.string().min(1).max(32_768).optional(),
paneKey: z.string().min(1).max(512).optional()
})
)
.max(AI_VAULT_SESSION_TITLE_REQUEST_MAX_COUNT)