(messages: Message[])
| 98 | * Uses incremental accumulation - only processes new messages since last render. |
| 99 | */ |
| 100 | export function useTurnDiffs(messages: Message[]): TurnDiff[] { |
| 101 | const cache = useRef<TurnDiffCache>({ |
| 102 | completedTurns: [], |
| 103 | currentTurn: null, |
| 104 | lastProcessedIndex: 0, |
| 105 | lastTurnIndex: 0, |
| 106 | }) |
| 107 | |
| 108 | return useMemo(() => { |
| 109 | const c = cache.current |
| 110 | |
| 111 | // Reset if messages shrunk (user rewound conversation) |
| 112 | if (messages.length < c.lastProcessedIndex) { |
| 113 | c.completedTurns = [] |
| 114 | c.currentTurn = null |
| 115 | c.lastProcessedIndex = 0 |
| 116 | c.lastTurnIndex = 0 |
| 117 | } |
| 118 | |
| 119 | // Process only new messages |
| 120 | for (let i = c.lastProcessedIndex; i < messages.length; i++) { |
| 121 | const message = messages[i] |
| 122 | if (!message || message.type !== 'user') continue |
| 123 | |
| 124 | // Check if this is a user prompt (not a tool result) |
| 125 | const isToolResult = |
| 126 | message.toolUseResult || |
| 127 | (Array.isArray(message.message.content) && |
| 128 | message.message.content[0]?.type === 'tool_result') |
| 129 | |
| 130 | if (!isToolResult && !message.isMeta) { |
| 131 | // Start a new turn on user prompt |
| 132 | if (c.currentTurn && c.currentTurn.files.size > 0) { |
| 133 | computeTurnStats(c.currentTurn) |
| 134 | c.completedTurns.push(c.currentTurn) |
| 135 | } |
| 136 | |
| 137 | c.lastTurnIndex++ |
| 138 | c.currentTurn = { |
| 139 | turnIndex: c.lastTurnIndex, |
| 140 | userPromptPreview: getUserPromptPreview(message), |
| 141 | timestamp: message.timestamp, |
| 142 | files: new Map(), |
| 143 | stats: { filesChanged: 0, linesAdded: 0, linesRemoved: 0 }, |
| 144 | } |
| 145 | } else if (c.currentTurn && message.toolUseResult) { |
| 146 | // Collect file edits from tool results |
| 147 | const result = message.toolUseResult |
| 148 | if (isFileEditResult(result)) { |
| 149 | const { filePath, structuredPatch } = result |
| 150 | const isNewFile = 'type' in result && result.type === 'create' |
| 151 | |
| 152 | // Get or create file entry |
| 153 | let fileEntry = c.currentTurn.files.get(filePath) |
| 154 | if (!fileEntry) { |
| 155 | fileEntry = { |
| 156 | filePath, |
| 157 | hunks: [], |
no test coverage detected