/** * Last Output Detector - Find the last visible output in an AI Group * * Uses a state machine approach to find the last meaningful output * for display in the chat UI. */ import { toDate } from './aiGroupHelpers'; import type { SemanticStep } from '../types/data'; import type { AIGroupLastOutput } from '../types/groups'; /** * Find the last visible output in the AI Group. * * Strategy: * 1. If isOngoing is true, return 'ongoing' type (session still in progress) * 2. Check for ExitPlanMode tool_call as special 'plan_exit' type * 3. Iterate through steps in reverse order * 4. Find the last 'output' step with outputText * 5. If no output found, find the last 'tool_result' step * 6. If no tool_result found, find the last 'interruption' step * 7. Return null if none exists * * Special case: ExitPlanMode * When the last tool_call is ExitPlanMode, return 'plan_exit' type with the plan content. * The preamble text (if any) is captured from the preceding output step. * * @param steps - Semantic steps from the AI Group * @param isOngoing - Whether this AI group is still in progress * @returns The last output or null */ export function findLastOutput( steps: SemanticStep[], isOngoing: boolean = false ): AIGroupLastOutput | null { // Check for interruption first - interruption takes precedence over ongoing status // This ensures user interruptions are always visible even if session appears ongoing for (let i = steps.length - 1; i >= 0; i--) { const step = steps[i]; if (step.type === 'interruption') { return { type: 'interruption', timestamp: step.startTime, }; } } // If session is ongoing (and no interruption), return 'ongoing' type if (isOngoing) { return { type: 'ongoing', timestamp: steps.length > 0 ? toDate(steps[steps.length - 1].startTime) : new Date(), }; } // Check for ExitPlanMode as the last significant activity // ExitPlanMode is a special ending tool that signals plan completion let lastExitPlanModeStep: SemanticStep | null = null; let lastOutputBeforeExitPlanMode: SemanticStep | null = null; for (let i = steps.length - 1; i >= 0; i--) { const step = steps[i]; if (step.type === 'tool_call' && step.content.toolName === 'ExitPlanMode') { lastExitPlanModeStep = step; // Look for the preceding output step (preamble text) for (let j = i - 1; j >= 0; j--) { if (steps[j].type === 'output' && steps[j].content.outputText) { lastOutputBeforeExitPlanMode = steps[j]; break; } } break; } } // If ExitPlanMode is found, check if it's the "last" activity // (no other output or tool_result comes after it) if (lastExitPlanModeStep) { const exitPlanModeIndex = steps.indexOf(lastExitPlanModeStep); let hasLaterEnding = false; for (let i = exitPlanModeIndex + 1; i < steps.length; i++) { const step = steps[i]; if (step.type === 'output' && step.content.outputText) { hasLaterEnding = true; break; } if (step.type === 'tool_result' && step.content.toolResultContent) { hasLaterEnding = true; break; } } if (!hasLaterEnding) { // ExitPlanMode is the last significant activity - return plan_exit const toolInput = lastExitPlanModeStep.content.toolInput as | Record | undefined; const planContent = toolInput?.plan as string | undefined; return { type: 'plan_exit', planContent: planContent ?? '', planPreamble: lastOutputBeforeExitPlanMode?.content.outputText, timestamp: lastExitPlanModeStep.startTime, }; } } // First pass: look for last 'output' step with outputText for (let i = steps.length - 1; i >= 0; i--) { const step = steps[i]; if (step.type === 'output' && step.content.outputText) { return { type: 'text', text: step.content.outputText, timestamp: step.startTime, }; } } // Second pass: look for last 'tool_result' step for (let i = steps.length - 1; i >= 0; i--) { const step = steps[i]; if (step.type === 'tool_result' && step.content.toolResultContent) { return { type: 'tool_result', toolName: step.content.toolName, toolResult: step.content.toolResultContent, isError: step.content.isError ?? false, timestamp: step.startTime, }; } } // Third pass: look for last 'interruption' step for (let i = steps.length - 1; i >= 0; i--) { const step = steps[i]; if (step.type === 'interruption' && step.content.interruptionText) { return { type: 'interruption', interruptionMessage: step.content.interruptionText, timestamp: step.startTime, }; } } return null; }