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(