diff --git a/apps/app/src/components/views/board-view/dialogs/agent-output-modal.tsx b/apps/app/src/components/views/board-view/dialogs/agent-output-modal.tsx index 0dd6429c..d92cabcc 100644 --- a/apps/app/src/components/views/board-view/dialogs/agent-output-modal.tsx +++ b/apps/app/src/components/views/board-view/dialogs/agent-output-modal.tsx @@ -99,24 +99,6 @@ export function AgentOutputModal({ loadOutput(); }, [open, featureId]); - // Save output to file - const saveOutput = async (newContent: string) => { - if (!projectPathRef.current) return; - - const api = getElectronAPI(); - if (!api) return; - - try { - // Use features API - agent output is stored in features/{id}/agent-output.md - // We need to write it directly since there's no updateAgentOutput method - // The context-manager handles this on the backend, but for frontend edits we write directly - const outputPath = `${projectPathRef.current}/.automaker/features/${featureId}/agent-output.md`; - await api.writeFile(outputPath, newContent); - } catch (error) { - console.error("Failed to save output:", error); - } - }; - // Listen to auto mode events and update output useEffect(() => { if (!open) return; @@ -142,7 +124,7 @@ export function AgentOutputModal({ ? JSON.stringify(event.input, null, 2) : ""; newContent = `\n🔧 Tool: ${toolName}\n${ - toolInput ? `Input: ${toolInput}` : "" + toolInput ? `Input: ${toolInput}\n` : "" }`; break; case "auto_mode_phase": @@ -202,11 +184,8 @@ export function AgentOutputModal({ } if (newContent) { - setOutput((prev) => { - const updated = prev + newContent; - saveOutput(updated); - return updated; - }); + // Only update local state - server is the single source of truth for file writes + setOutput((prev) => prev + newContent); } }); diff --git a/apps/server/src/services/auto-mode-service.ts b/apps/server/src/services/auto-mode-service.ts index 7d779da8..c40c91ff 100644 --- a/apps/server/src/services/auto-mode-service.ts +++ b/apps/server/src/services/auto-mode-service.ts @@ -1264,10 +1264,49 @@ This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set. const featureDirForOutput = getFeatureDir(configProjectPath, featureId); const outputPath = path.join(featureDirForOutput, "agent-output.md"); + // Incremental file writing state + let directoryCreated = false; + let writeTimeout: ReturnType | null = null; + const WRITE_DEBOUNCE_MS = 500; // Batch writes every 500ms + + // Helper to write current responseText to file + const writeToFile = async (): Promise => { + try { + if (!directoryCreated) { + await fs.mkdir(path.dirname(outputPath), { recursive: true }); + directoryCreated = true; + } + await fs.writeFile(outputPath, responseText); + } catch (error) { + // Log but don't crash - file write errors shouldn't stop execution + console.error(`[AutoMode] Failed to write agent output for ${featureId}:`, error); + } + }; + + // Debounced write - schedules a write after WRITE_DEBOUNCE_MS + const scheduleWrite = (): void => { + if (writeTimeout) { + clearTimeout(writeTimeout); + } + writeTimeout = setTimeout(() => { + writeToFile().catch((err) => { + console.error(`[AutoMode] Debounced write error:`, err); + }); + }, WRITE_DEBOUNCE_MS); + }; + for await (const msg of stream) { if (msg.type === "assistant" && msg.message?.content) { for (const block of msg.message.content) { if (block.type === "text") { + // Add separator before new text if we already have content and it doesn't end with newlines + if (responseText.length > 0 && !responseText.endsWith('\n\n')) { + if (responseText.endsWith('\n')) { + responseText += '\n'; + } else { + responseText += '\n\n'; + } + } responseText += block.text || ""; // Check for authentication errors in the response @@ -1283,16 +1322,30 @@ This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set. ); } + // Schedule incremental file write (debounced) + scheduleWrite(); + this.emitAutoModeEvent("auto_mode_progress", { featureId, content: block.text, }); } else if (block.type === "tool_use") { + // Emit event for real-time UI this.emitAutoModeEvent("auto_mode_tool", { featureId, tool: block.name, input: block.input, }); + + // Also add to file output for persistence + if (responseText.length > 0 && !responseText.endsWith('\n')) { + responseText += '\n'; + } + responseText += `\n🔧 Tool: ${block.name}\n`; + if (block.input) { + responseText += `Input: ${JSON.stringify(block.input, null, 2)}\n`; + } + scheduleWrite(); } } } else if (msg.type === "error") { @@ -1300,16 +1353,17 @@ This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set. throw new Error(msg.error || "Unknown error"); } else if (msg.type === "result" && msg.subtype === "success") { responseText = msg.result || responseText; + // Schedule write for final result + scheduleWrite(); } } - // Save agent output - try { - await fs.mkdir(path.dirname(outputPath), { recursive: true }); - await fs.writeFile(outputPath, responseText); - } catch { - // May fail if directory doesn't exist + // Clear any pending timeout and do a final write to ensure all content is saved + if (writeTimeout) { + clearTimeout(writeTimeout); } + // Final write - ensure all accumulated content is saved + await writeToFile(); } private async executeFeatureWithContext(