diff --git a/src/shared/agent-hook-listener.test.ts b/src/shared/agent-hook-listener.test.ts index 124aae1de74..9057c37a1d9 100644 --- a/src/shared/agent-hook-listener.test.ts +++ b/src/shared/agent-hook-listener.test.ts @@ -176,6 +176,41 @@ describe('shared agent-hook-listener', () => { }) }) + it('reads Antigravity user requests from the transcript', () => { + const tmpDir = mkdtempSync(join(tmpdir(), 'orca-antigravity-prompt-')) + const transcriptPath = join(tmpDir, 'transcript.jsonl') + try { + writeFileSync( + transcriptPath, + `${JSON.stringify({ + source: 'USER_EXPLICIT', + type: 'USER_INPUT', + content: + '\nFix the failing test\n\n\nignored\n' + })}\n` + ) + + const started = normalizeHookPayload( + state, + 'antigravity', + { + paneKey: PANE_KEY, + hook_event_name: 'PreInvocation', + payload: { transcriptPath } + }, + 'production' + ) + + expect(started?.payload).toMatchObject({ + state: 'working', + prompt: 'Fix the failing test', + agentType: 'antigravity' + }) + } finally { + rmSync(tmpDir, { recursive: true, force: true }) + } + }) + it('maps Antigravity feedback tools to waiting state', () => { const question = normalizeHookPayload( state, @@ -285,6 +320,7 @@ describe('shared agent-hook-listener', () => { expect(done?.payload).toMatchObject({ state: 'done', + prompt: 'hi', agentType: 'antigravity', lastAssistantMessage: 'Antigravity is wired up.' }) @@ -293,7 +329,7 @@ describe('shared agent-hook-listener', () => { } }) - it('keeps Antigravity working when Stop reports the agent is not fully idle', () => { + it('normalizes Antigravity Stop to done even when fullyIdle is false', () => { const event = normalizeHookPayload( state, 'antigravity', @@ -306,11 +342,42 @@ describe('shared agent-hook-listener', () => { ) expect(event?.payload).toMatchObject({ - state: 'working', + state: 'done', agentType: 'antigravity' }) }) + it('ignores late Antigravity tool hooks after a completed Stop for the same transcript', () => { + const transcriptPath = '/tmp/antigravity-transcript.jsonl' + const done = normalizeHookPayload( + state, + 'antigravity', + { + paneKey: PANE_KEY, + hook_event_name: 'Stop', + payload: { transcriptPath, fullyIdle: true } + }, + 'production' + ) + expect(done?.payload.state).toBe('done') + + const lateTool = normalizeHookPayload( + state, + 'antigravity', + { + paneKey: PANE_KEY, + hook_event_name: 'PostToolUse', + payload: { + transcriptPath, + toolCall: { name: 'run_command', args: { CommandLine: 'pwd' } } + } + }, + 'production' + ) + + expect(lateTool).toBeNull() + }) + it('treats Antigravity Stop transcripts as pending result text', () => { expect( hasPendingAgentResultText('antigravity', { diff --git a/src/shared/agent-hook-listener.ts b/src/shared/agent-hook-listener.ts index e9a69ae7b61..0b233322dab 100644 --- a/src/shared/agent-hook-listener.ts +++ b/src/shared/agent-hook-listener.ts @@ -58,6 +58,7 @@ export type HookListenerState = { lastPromptByPaneKey: Map lastToolByPaneKey: Map lastStatusByPaneKey: Map + antigravityCompletedTranscriptByPaneKey: Map } export function createHookListenerState(): HookListenerState { @@ -66,7 +67,8 @@ export function createHookListenerState(): HookListenerState { warnedEnvs: new Set(), lastPromptByPaneKey: new Map(), lastToolByPaneKey: new Map(), - lastStatusByPaneKey: new Map() + lastStatusByPaneKey: new Map(), + antigravityCompletedTranscriptByPaneKey: new Map() } } @@ -74,17 +76,20 @@ export function clearPaneCacheState(state: HookListenerState, paneKey: string): state.lastPromptByPaneKey.delete(paneKey) state.lastToolByPaneKey.delete(paneKey) state.lastStatusByPaneKey.delete(paneKey) + state.antigravityCompletedTranscriptByPaneKey.delete(paneKey) } function clearPaneTurnCacheState(state: HookListenerState, paneKey: string): void { state.lastPromptByPaneKey.delete(paneKey) state.lastToolByPaneKey.delete(paneKey) + state.antigravityCompletedTranscriptByPaneKey.delete(paneKey) } export function clearAllListenerCaches(state: HookListenerState): void { state.lastPromptByPaneKey.clear() state.lastToolByPaneKey.clear() state.lastStatusByPaneKey.clear() + state.antigravityCompletedTranscriptByPaneKey.clear() state.warnedVersions.clear() state.warnedEnvs.clear() } @@ -558,6 +563,34 @@ function extractAssistantContentText(content: unknown): string | undefined { return undefined } +function extractAntigravityUserRequest(content: string): string | undefined { + const request = content.match(/\s*([\s\S]*?)\s*<\/USER_REQUEST>/) + const text = request ? request[1] : content + const trimmed = text.trim() + return trimmed.length > 0 ? trimmed : undefined +} + +function extractUserPromptTextFromLine(line: string): string | undefined { + let entry: unknown + try { + entry = JSON.parse(line) + } catch { + return undefined + } + if (typeof entry !== 'object' || entry === null) { + return undefined + } + const record = entry as Record + if ( + (record.source === 'USER_EXPLICIT' || record.source === 'USER') && + (record.type === 'USER_INPUT' || record.type === 'REQUEST') && + typeof record.content === 'string' + ) { + return extractAntigravityUserRequest(record.content) + } + return undefined +} + function readLastAssistantFromTranscript(transcriptPath: unknown): string | undefined { if (typeof transcriptPath !== 'string' || transcriptPath.length === 0) { return undefined @@ -565,6 +598,13 @@ function readLastAssistantFromTranscript(transcriptPath: unknown): string | unde return readLastAssistantFromTranscriptOnce(transcriptPath) } +function readLastUserPromptFromTranscript(transcriptPath: unknown): string | undefined { + if (typeof transcriptPath !== 'string' || transcriptPath.length === 0) { + return undefined + } + return readLastTextFromTranscriptOnce(transcriptPath, extractUserPromptTextFromLine) +} + function parseHookBodyPayloadRecord(body: unknown): Record | null { if (typeof body !== 'object' || body === null) { return null @@ -667,6 +707,13 @@ export function hasPendingAgentResultText(source: AgentHookSource, body: unknown } function readLastAssistantFromTranscriptOnce(transcriptPath: string): string | undefined { + return readLastTextFromTranscriptOnce(transcriptPath, extractAssistantTextFromLine) +} + +function readLastTextFromTranscriptOnce( + transcriptPath: string, + extractLineText: (line: string) => string | undefined +): string | undefined { try { const stats = statSync(transcriptPath) const size = stats.size @@ -716,7 +763,7 @@ function readLastAssistantFromTranscriptOnce(transcriptPath: string): string | u if (line.length === 0) { continue } - const extracted = extractAssistantTextFromLine(line) + const extracted = extractLineText(line) if (extracted !== undefined) { return extracted } @@ -1530,14 +1577,25 @@ function normalizeAntigravityEvent( paneKey: string, hookPayload: Record ): ParsedAgentStatusPayload | null { + const transcriptPath = readFirstString(hookPayload, ['transcriptPath', 'transcript_path']) + if (eventName === 'PreInvocation') { + state.antigravityCompletedTranscriptByPaneKey.delete(paneKey) + } else if ( + transcriptPath && + eventName !== 'Stop' && + state.antigravityCompletedTranscriptByPaneKey.get(paneKey) === transcriptPath + ) { + // Why: agy can emit a bookkeeping PostToolUse after Stop; ignore it so a + // finished row does not turn back into a yellow spinner. + return null + } + const toolName = readAntigravityToolCall(hookPayload).toolName const stateName = eventName === 'PreToolUse' && isAntigravityFeedbackTool(toolName) ? 'waiting' : eventName === 'Stop' - ? hookPayload.fullyIdle === false - ? 'working' - : 'done' + ? 'done' : eventName === 'PreInvocation' || eventName === 'PostInvocation' || eventName === 'PreToolUse' || @@ -1549,6 +1607,7 @@ function normalizeAntigravityEvent( return null } + const effectivePrompt = promptText || readLastUserPromptFromTranscript(transcriptPath) || '' const snapshot = resolveToolState( state, paneKey, @@ -1556,10 +1615,10 @@ function normalizeAntigravityEvent( { resetOnNewTurn: isNewTurnEvent('antigravity', eventName) } ) - return parseAgentStatusPayload( + const payload = parseAgentStatusPayload( JSON.stringify({ state: stateName, - prompt: resolvePrompt(state, paneKey, promptText, { + prompt: resolvePrompt(state, paneKey, effectivePrompt, { resetOnNewTurn: isNewTurnEvent('antigravity', eventName) }), agentType: 'antigravity', @@ -1568,6 +1627,10 @@ function normalizeAntigravityEvent( lastAssistantMessage: snapshot.lastAssistantMessage }) ) + if (eventName === 'Stop' && transcriptPath) { + state.antigravityCompletedTranscriptByPaneKey.set(paneKey, transcriptPath) + } + return payload } function normalizeCodexEvent(