mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-04 09:13:08 +00:00
refactor: remove direct file saving from AgentOutputModal and implement debounced file writing in auto-mode service
- Removed the saveOutput function from AgentOutputModal to streamline state management, ensuring local state updates without direct file writes. - Introduced a debounced file writing mechanism in the auto-mode service to handle incremental updates to agent output, improving performance and reliability. - Enhanced error handling during file writes to prevent execution interruptions and ensure all content is saved correctly.
This commit is contained in:
@@ -99,24 +99,6 @@ export function AgentOutputModal({
|
|||||||
loadOutput();
|
loadOutput();
|
||||||
}, [open, featureId]);
|
}, [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
|
// Listen to auto mode events and update output
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
@@ -142,7 +124,7 @@ export function AgentOutputModal({
|
|||||||
? JSON.stringify(event.input, null, 2)
|
? JSON.stringify(event.input, null, 2)
|
||||||
: "";
|
: "";
|
||||||
newContent = `\n🔧 Tool: ${toolName}\n${
|
newContent = `\n🔧 Tool: ${toolName}\n${
|
||||||
toolInput ? `Input: ${toolInput}` : ""
|
toolInput ? `Input: ${toolInput}\n` : ""
|
||||||
}`;
|
}`;
|
||||||
break;
|
break;
|
||||||
case "auto_mode_phase":
|
case "auto_mode_phase":
|
||||||
@@ -202,11 +184,8 @@ export function AgentOutputModal({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (newContent) {
|
if (newContent) {
|
||||||
setOutput((prev) => {
|
// Only update local state - server is the single source of truth for file writes
|
||||||
const updated = prev + newContent;
|
setOutput((prev) => prev + newContent);
|
||||||
saveOutput(updated);
|
|
||||||
return updated;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1264,10 +1264,49 @@ This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set.
|
|||||||
const featureDirForOutput = getFeatureDir(configProjectPath, featureId);
|
const featureDirForOutput = getFeatureDir(configProjectPath, featureId);
|
||||||
const outputPath = path.join(featureDirForOutput, "agent-output.md");
|
const outputPath = path.join(featureDirForOutput, "agent-output.md");
|
||||||
|
|
||||||
|
// Incremental file writing state
|
||||||
|
let directoryCreated = false;
|
||||||
|
let writeTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
const WRITE_DEBOUNCE_MS = 500; // Batch writes every 500ms
|
||||||
|
|
||||||
|
// Helper to write current responseText to file
|
||||||
|
const writeToFile = async (): Promise<void> => {
|
||||||
|
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) {
|
for await (const msg of stream) {
|
||||||
if (msg.type === "assistant" && msg.message?.content) {
|
if (msg.type === "assistant" && msg.message?.content) {
|
||||||
for (const block of msg.message.content) {
|
for (const block of msg.message.content) {
|
||||||
if (block.type === "text") {
|
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 || "";
|
responseText += block.text || "";
|
||||||
|
|
||||||
// Check for authentication errors in the response
|
// 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", {
|
this.emitAutoModeEvent("auto_mode_progress", {
|
||||||
featureId,
|
featureId,
|
||||||
content: block.text,
|
content: block.text,
|
||||||
});
|
});
|
||||||
} else if (block.type === "tool_use") {
|
} else if (block.type === "tool_use") {
|
||||||
|
// Emit event for real-time UI
|
||||||
this.emitAutoModeEvent("auto_mode_tool", {
|
this.emitAutoModeEvent("auto_mode_tool", {
|
||||||
featureId,
|
featureId,
|
||||||
tool: block.name,
|
tool: block.name,
|
||||||
input: block.input,
|
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") {
|
} 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");
|
throw new Error(msg.error || "Unknown error");
|
||||||
} else if (msg.type === "result" && msg.subtype === "success") {
|
} else if (msg.type === "result" && msg.subtype === "success") {
|
||||||
responseText = msg.result || responseText;
|
responseText = msg.result || responseText;
|
||||||
|
// Schedule write for final result
|
||||||
|
scheduleWrite();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save agent output
|
// Clear any pending timeout and do a final write to ensure all content is saved
|
||||||
try {
|
if (writeTimeout) {
|
||||||
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
clearTimeout(writeTimeout);
|
||||||
await fs.writeFile(outputPath, responseText);
|
|
||||||
} catch {
|
|
||||||
// May fail if directory doesn't exist
|
|
||||||
}
|
}
|
||||||
|
// Final write - ensure all accumulated content is saved
|
||||||
|
await writeToFile();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async executeFeatureWithContext(
|
private async executeFeatureWithContext(
|
||||||
|
|||||||
Reference in New Issue
Block a user