diff --git a/apps/cli/src/components/typed-message.tsx b/apps/cli/src/components/typed-message.tsx index f7871195a..2c8802220 100644 --- a/apps/cli/src/components/typed-message.tsx +++ b/apps/cli/src/components/typed-message.tsx @@ -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 ; + case 'write': + // For write operations, use the WriteMessage component + return ; + default: return null; } diff --git a/apps/cli/src/components/write-message.tsx b/apps/cli/src/components/write-message.tsx new file mode 100644 index 000000000..bcfca311d --- /dev/null +++ b/apps/cli/src/components/write-message.tsx @@ -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; +} + +/** + * 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 ( + + {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 ( + + {/* WRITE badge with relative file path */} + + + WRITE + + ({relativePath}) + + + {/* File content lines - always show with indentation */} + {contentLines.length > 0 && ( + + {displayLines.map((line, idx) => ( + + {line} + + ))} + + )} + + {/* Expansion hint if content is long */} + {contentLines.length > 5 && ( + + + {isExpanded ? '(Press Ctrl+O to collapse)' : `... +${contentLines.length - 5} lines (Press Ctrl+O to expand)`} + + + )} + + {/* Status line with indentation */} + {fileResult && ( + + + ↳ {success ? `Wrote ${contentLines.length} lines` : `Failed: ${fileResult.errorMessage}`} + + + )} + + ); + })} + + ); +} diff --git a/apps/cli/src/services/analytics-engineer-handler.ts b/apps/cli/src/services/analytics-engineer-handler.ts index 0f49778f5..354221138 100644 --- a/apps/cli/src/services/analytics-engineer-handler.ts +++ b/apps/cli/src/services/analytics-engineer-handler.ts @@ -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 + }, + }); + } }, }); diff --git a/packages/ai/src/agents/analytics-engineer-agent/analytics-engineer-agent.ts b/packages/ai/src/agents/analytics-engineer-agent/analytics-engineer-agent.ts index 5fa6d66ac..5cfbe045e 100644 --- a/packages/ai/src/agents/analytics-engineer-agent/analytics-engineer-agent.ts +++ b/packages/ai/src/agents/analytics-engineer-agent/analytics-engineer-agent.ts @@ -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, diff --git a/packages/ai/src/tools/file-tools/write-file-tool/write-file-tool-execute.ts b/packages/ai/src/tools/file-tools/write-file-tool/write-file-tool-execute.ts index 9c213f052..37444eb0b 100644 --- a/packages/ai/src/tools/file-tools/write-file-tool/write-file-tool-execute.ts +++ b/packages/ai/src/tools/file-tools/write-file-tool/write-file-tool-execute.ts @@ -90,11 +90,18 @@ async function createSingleFile( */ export function createWriteFileToolExecute(context: WriteFileToolContext) { return async function execute(input: WriteFileToolInput): Promise { - 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; }; } diff --git a/packages/ai/src/tools/file-tools/write-file-tool/write-file-tool.ts b/packages/ai/src/tools/file-tools/write-file-tool/write-file-tool.ts index 71a90a22a..96748902b 100644 --- a/packages/ai/src/tools/file-tools/write-file-tool/write-file-tool.ts +++ b/packages/ai/src/tools/file-tools/write-file-tool/write-file-tool.ts @@ -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;