write file message

This commit is contained in:
dal 2025-10-03 08:34:18 -06:00
parent edfa13e785
commit f601bf7ca1
No known key found for this signature in database
GPG Key ID: 16F4B0E1E9F61122
6 changed files with 131 additions and 2 deletions

View File

@ -2,6 +2,7 @@ import { Box, Text } from 'ink';
import React from 'react';
import type { AgentMessage } from '../services/analytics-engineer-handler';
import { ExecuteMessage } from './execute-message';
import { WriteMessage } from './write-message';
interface AgentMessageComponentProps {
message: AgentMessage;
@ -42,6 +43,10 @@ export function AgentMessageComponent({ message }: AgentMessageComponentProps) {
// For execute commands, use the ExecuteMessage component
return <ExecuteMessage message={message} />;
case 'write':
// For write operations, use the WriteMessage component
return <WriteMessage message={message} />;
default:
return null;
}

View File

@ -0,0 +1,87 @@
import { Box, Text, useInput } from 'ink';
import React, { useState } from 'react';
import path from 'node:path';
import type { AgentMessage } from '../services/analytics-engineer-handler';
interface WriteMessageProps {
message: Extract<AgentMessage, { kind: 'write' }>;
}
/**
* Component for displaying write file operations
* Shows WRITE badge, relative file path, and file content preview
* Supports expansion with Ctrl+O to show full content
*/
export function WriteMessage({ message }: WriteMessageProps) {
const [isExpanded, setIsExpanded] = useState(false);
// Handle Ctrl+O to toggle expansion
useInput((input, key) => {
if (key.ctrl && input === 'o') {
setIsExpanded((prev) => !prev);
}
});
const { args, result } = message;
// For each file, show its content
return (
<Box flexDirection="column">
{args.files.map((file, fileIdx) => {
// Get relative path from cwd
const relativePath = path.relative(process.cwd(), file.path);
// Split content into lines
const contentLines = file.content.split('\n');
// Show first 5 lines when not expanded, all lines when expanded
const displayLines = isExpanded ? contentLines : contentLines.slice(0, 5);
// Find the result for this file
const fileResult = result?.results[fileIdx];
const success = fileResult?.status === 'success';
return (
<Box key={fileIdx} flexDirection="column" marginBottom={fileIdx < args.files.length - 1 ? 1 : 0}>
{/* WRITE badge with relative file path */}
<Box flexDirection="row">
<Text bold color="white" backgroundColor="magenta">
WRITE
</Text>
<Text color="#94a3b8"> ({relativePath})</Text>
</Box>
{/* File content lines - always show with indentation */}
{contentLines.length > 0 && (
<Box flexDirection="column" paddingLeft={2}>
{displayLines.map((line, idx) => (
<Text key={idx} color="#e0e7ff">
{line}
</Text>
))}
</Box>
)}
{/* Expansion hint if content is long */}
{contentLines.length > 5 && (
<Box paddingLeft={2}>
<Text color="#64748b" dimColor>
{isExpanded ? '(Press Ctrl+O to collapse)' : `... +${contentLines.length - 5} lines (Press Ctrl+O to expand)`}
</Text>
</Box>
)}
{/* Status line with indentation */}
{fileResult && (
<Box paddingLeft={2}>
<Text color={success ? '#64748b' : 'red'} dimColor>
{success ? `Wrote ${contentLines.length} lines` : `Failed: ${fileResult.errorMessage}`}
</Text>
</Box>
)}
</Box>
);
})}
</Box>
);
}

View File

@ -36,6 +36,12 @@ export type AgentMessage =
event: 'start' | 'complete';
args: LsToolInput;
result?: LsToolOutput;
}
| {
kind: 'write';
event: 'start' | 'complete';
args: { files: { path: string; content: string }[] };
result?: { results: Array<{ status: 'success' | 'error'; filePath: string; errorMessage?: string }> };
};
export interface DocsAgentMessage {
@ -122,6 +128,18 @@ export async function runDocsAgent(params: RunDocsAgentParams) {
},
});
}
// Handle write tool events - only show complete to avoid duplicates
if (event.tool === 'writeFileTool' && event.event === 'complete') {
onMessage({
message: {
kind: 'write',
event: 'complete',
args: event.args,
result: event.result, // Type-safe: WriteFileToolOutput
},
});
}
},
});

View File

@ -64,6 +64,7 @@ export function createAnalyticsEngineerAgent(analyticsEngineerAgentOptions: Anal
const writeFileTool = createWriteFileTool({
messageId: analyticsEngineerAgentOptions.messageId,
projectDirectory: analyticsEngineerAgentOptions.folder_structure,
onToolEvent: analyticsEngineerAgentOptions.onToolEvent,
});
const grepTool = createGrepTool({
messageId: analyticsEngineerAgentOptions.messageId,

View File

@ -90,11 +90,18 @@ async function createSingleFile(
*/
export function createWriteFileToolExecute(context: WriteFileToolContext) {
return async function execute(input: WriteFileToolInput): Promise<WriteFileToolOutput> {
const { messageId, projectDirectory } = context;
const { messageId, projectDirectory, onToolEvent } = context;
const { files } = input;
console.info(`Creating ${files.length} file(s) for message ${messageId}`);
// Emit start event
onToolEvent?.({
tool: 'writeFileTool',
event: 'start',
args: input,
});
// Process all files in parallel
const fileResults = await Promise.all(
files.map((file) => createSingleFile(file.path, file.content, projectDirectory))
@ -127,6 +134,16 @@ export function createWriteFileToolExecute(context: WriteFileToolContext) {
console.error('Failed files:', errors);
}
return { results };
const output = { results };
// Emit complete event
onToolEvent?.({
tool: 'writeFileTool',
event: 'complete',
result: output,
args: input,
});
return output;
};
}

View File

@ -30,6 +30,7 @@ const WriteFileToolOutputSchema = z.object({
const WriteFileToolContextSchema = z.object({
messageId: z.string().describe('The message ID for database updates'),
projectDirectory: z.string().describe('The root directory of the project'),
onToolEvent: z.any().optional().describe('Callback for tool events'),
});
export type WriteFileToolInput = z.infer<typeof WriteFileToolInputSchema>;