diff --git a/src/renderer/src/components/automations/automation-run-output-snapshot.test.ts b/src/renderer/src/components/automations/automation-run-output-snapshot.test.ts index cadcbfd636c..6c830f5581d 100644 --- a/src/renderer/src/components/automations/automation-run-output-snapshot.test.ts +++ b/src/renderer/src/components/automations/automation-run-output-snapshot.test.ts @@ -1,5 +1,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { createAutomationRunOutputSnapshotBuffer } from './automation-run-output-snapshot' +import { + createAutomationRunOutputSnapshotBuffer, + createAutomationRunOutputSnapshotFromText, + selectAutomationRunOutputSnapshot +} from './automation-run-output-snapshot' describe('automation run output snapshot buffer', () => { beforeEach(() => { @@ -68,4 +72,26 @@ describe('automation run output snapshot buffer', () => { content: 'LoadingDone' }) }) + + it('creates a saved snapshot from agent transcript text', () => { + expect(createAutomationRunOutputSnapshotFromText('\nFinal summary.\n')).toEqual({ + format: 'plain_text', + content: 'Final summary.', + capturedAt: new Date('2026-05-16T12:00:00Z').getTime(), + truncated: false + }) + }) + + it('prefers agent transcript text over raw terminal redraw output', () => { + const rawTerminalSnapshot = createAutomationRunOutputSnapshotFromText( + 'q;\u2834 orca q\u2022Working q q' + ) + + expect(selectAutomationRunOutputSnapshot('Posted to #releases.', rawTerminalSnapshot)).toEqual({ + format: 'plain_text', + content: 'Posted to #releases.', + capturedAt: new Date('2026-05-16T12:00:00Z').getTime(), + truncated: false + }) + }) }) diff --git a/src/renderer/src/components/automations/automation-run-output-snapshot.ts b/src/renderer/src/components/automations/automation-run-output-snapshot.ts index 41ea8bd138f..89ede7f961c 100644 --- a/src/renderer/src/components/automations/automation-run-output-snapshot.ts +++ b/src/renderer/src/components/automations/automation-run-output-snapshot.ts @@ -17,6 +17,31 @@ export type AutomationRunOutputSnapshotBuffer = { snapshot: () => AutomationRunOutputSnapshot | null } +export function createAutomationRunOutputSnapshotFromText( + content: string, + truncated = false +): AutomationRunOutputSnapshot | null { + const trimmed = content.trim() + if (!trimmed) { + return null + } + return { + format: 'plain_text', + content: trimmed, + capturedAt: Date.now(), + truncated + } +} + +export function selectAutomationRunOutputSnapshot( + assistantMessage: string | null | undefined, + terminalSnapshot: AutomationRunOutputSnapshot | null +): AutomationRunOutputSnapshot | null { + // Why: raw hidden-PTY captures include full-screen TUI redraws; hook + // transcript text is the user-facing automation result when available. + return createAutomationRunOutputSnapshotFromText(assistantMessage ?? '') ?? terminalSnapshot +} + function stripTerminalControls(value: string): string { return value .replace(OSC_SEQUENCE_PATTERN, '') @@ -52,15 +77,7 @@ export function createAutomationRunOutputSnapshotBuffer(): AutomationRunOutputSn }, snapshot() { const content = stripTerminalControls(chunks.join('')).trim() - if (!content) { - return null - } - return { - format: 'plain_text', - content, - capturedAt: Date.now(), - truncated - } + return createAutomationRunOutputSnapshotFromText(content, truncated) } } } diff --git a/src/renderer/src/hooks/useAutomationDispatchEvents.ts b/src/renderer/src/hooks/useAutomationDispatchEvents.ts index b16a7ab3ad0..18a260cdb3b 100644 --- a/src/renderer/src/hooks/useAutomationDispatchEvents.ts +++ b/src/renderer/src/hooks/useAutomationDispatchEvents.ts @@ -3,7 +3,10 @@ import { launchAgentBackgroundSession } from '@/lib/launch-agent-background-sess import { useAppStore } from '@/store' import type { AutomationDispatchResult } from '../../../shared/automations-types' import { parsePaneKey } from '../../../shared/stable-pane-id' -import { createAutomationRunOutputSnapshotBuffer } from '@/components/automations/automation-run-output-snapshot' +import { + createAutomationRunOutputSnapshotBuffer, + selectAutomationRunOutputSnapshot +} from '@/components/automations/automation-run-output-snapshot' const AUTOMATIONS_CHANGED_EVENT = 'orca:automations-changed' @@ -122,6 +125,9 @@ export function useAutomationDispatchEvents(): void { dispatchWorkspaceDisplayName = worktree.displayName const outputSnapshotBuffer = createAutomationRunOutputSnapshotBuffer() + let latestAssistantMessage: string | null = null + const getOutputSnapshot = () => + selectAutomationRunOutputSnapshot(latestAssistantMessage, outputSnapshotBuffer.snapshot()) let dispatchMarked = false let pendingExitCode: number | null = null let pendingDone = false @@ -138,7 +144,7 @@ export function useAutomationDispatchEvents(): void { status: 'completed', workspaceId: worktree.id, workspaceDisplayName: worktree.displayName, - outputSnapshot: outputSnapshotBuffer.snapshot(), + outputSnapshot: getOutputSnapshot(), error: null }) } @@ -149,7 +155,7 @@ export function useAutomationDispatchEvents(): void { status: code === 0 ? 'completed' : 'dispatch_failed', workspaceId: worktree.id, workspaceDisplayName: worktree.displayName, - outputSnapshot: outputSnapshotBuffer.snapshot(), + outputSnapshot: getOutputSnapshot(), error: code === 0 ? null : `Automation process exited with code ${code}.` }) } @@ -169,6 +175,7 @@ export function useAutomationDispatchEvents(): void { for (const [paneKey, entry] of Object.entries(agentStatusByPaneKey)) { const parsed = parsePaneKey(paneKey) if (parsed?.tabId === tabId && entry.state === 'done') { + latestAssistantMessage = entry.lastAssistantMessage?.trim() || latestAssistantMessage handleAgentDone() return } @@ -189,6 +196,7 @@ export function useAutomationDispatchEvents(): void { outputSnapshotBuffer.append(chunk) }, onAgentStatus: (payload) => { + latestAssistantMessage = payload.lastAssistantMessage?.trim() || latestAssistantMessage if (payload.state !== 'done') { return }