From bd195df18f56ae2cc3592f52d007333b98f80344 Mon Sep 17 00:00:00 2001 From: dal Date: Fri, 3 Oct 2025 09:20:29 -0600 Subject: [PATCH] read file messages --- apps/cli/src/components/read-message.tsx | 97 +++++++++++++++++++ apps/cli/src/components/typed-message.tsx | 5 + .../services/analytics-engineer-handler.ts | 18 ++++ .../analytics-engineer-agent.ts | 1 + .../read-file-tool/read-file-tool-execute.ts | 37 ++++++- .../read-file-tool/read-file-tool.ts | 1 + 6 files changed, 154 insertions(+), 5 deletions(-) create mode 100644 apps/cli/src/components/read-message.tsx diff --git a/apps/cli/src/components/read-message.tsx b/apps/cli/src/components/read-message.tsx new file mode 100644 index 000000000..ceffe44a0 --- /dev/null +++ b/apps/cli/src/components/read-message.tsx @@ -0,0 +1,97 @@ +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 ReadMessageProps { + message: Extract; +} + +/** + * Component for displaying file read operations + * Shows READ badge, relative file path, and file content preview + * Supports expansion with Ctrl+O to show full content + */ +export function ReadMessage({ message }: ReadMessageProps) { + 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; + + if (!result) { + return null; + } + + // Get relative path from cwd + const relativePath = path.relative(process.cwd(), result.file_path); + + // Handle error case + if (result.status === 'error') { + return ( + + + + READ + + ({relativePath}) + + + + ↳ Error: {result.error_message} + + + + ); + } + + // Split content into lines + const contentLines = result.content?.split('\n') || []; + + // Show first 5 lines when not expanded, all lines when expanded + const displayLines = isExpanded ? contentLines : contentLines.slice(0, 5); + + return ( + + {/* READ badge with relative file path */} + + + READ + + ({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 */} + + + ↳ Read {contentLines.length} lines{result.truncated ? ' (truncated at 1000 lines)' : ''} + + + + ); +} diff --git a/apps/cli/src/components/typed-message.tsx b/apps/cli/src/components/typed-message.tsx index 90f137190..35e1425d2 100644 --- a/apps/cli/src/components/typed-message.tsx +++ b/apps/cli/src/components/typed-message.tsx @@ -4,6 +4,7 @@ import type { AgentMessage } from '../services/analytics-engineer-handler'; import { ExecuteMessage } from './execute-message'; import { WriteMessage } from './write-message'; import { EditMessage } from './edit-message'; +import { ReadMessage } from './read-message'; interface AgentMessageComponentProps { message: AgentMessage; @@ -52,6 +53,10 @@ export function AgentMessageComponent({ message }: AgentMessageComponentProps) { // For edit operations, use the EditMessage component return ; + case 'read': + // For read operations, use the ReadMessage component + return ; + default: return null; } diff --git a/apps/cli/src/services/analytics-engineer-handler.ts b/apps/cli/src/services/analytics-engineer-handler.ts index 6c09435eb..9081083b4 100644 --- a/apps/cli/src/services/analytics-engineer-handler.ts +++ b/apps/cli/src/services/analytics-engineer-handler.ts @@ -48,6 +48,12 @@ export type AgentMessage = event: 'start' | 'complete'; args: { filePath: string; oldString?: string; newString?: string; edits?: Array<{ oldString: string; newString: string }> }; result?: { success: boolean; filePath: string; diff?: string; finalDiff?: string; message?: string; errorMessage?: string }; + } + | { + kind: 'read'; + event: 'start' | 'complete'; + args: { filePath: string }; + result?: { status: 'success' | 'error'; file_path: string; content?: string; truncated?: boolean; error_message?: string }; }; export interface DocsAgentMessage { @@ -158,6 +164,18 @@ export async function runDocsAgent(params: RunDocsAgentParams) { }, }); } + + // Handle read tool events - only show complete to avoid duplicates + if (event.tool === 'readFileTool' && event.event === 'complete') { + onMessage({ + message: { + kind: 'read', + event: 'complete', + args: event.args, + result: event.result, // Type-safe: ReadFileToolOutput + }, + }); + } }, }); 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 222bf4369..cd0d67de5 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 @@ -74,6 +74,7 @@ export function createAnalyticsEngineerAgent(analyticsEngineerAgentOptions: Anal const readFileTool = createReadFileTool({ messageId: analyticsEngineerAgentOptions.messageId, projectDirectory: analyticsEngineerAgentOptions.folder_structure, + onToolEvent: analyticsEngineerAgentOptions.onToolEvent, }); const bashTool = createBashTool({ messageId: analyticsEngineerAgentOptions.messageId, diff --git a/packages/ai/src/tools/file-tools/read-file-tool/read-file-tool-execute.ts b/packages/ai/src/tools/file-tools/read-file-tool/read-file-tool-execute.ts index 97881f0c9..9aa092e10 100644 --- a/packages/ai/src/tools/file-tools/read-file-tool/read-file-tool-execute.ts +++ b/packages/ai/src/tools/file-tools/read-file-tool/read-file-tool-execute.ts @@ -34,11 +34,18 @@ function validateFilePath(filePath: string, projectDirectory: string): void { */ export function createReadFileToolExecute(context: ReadFileToolContext) { return async function execute(input: ReadFileToolInput): Promise { - const { messageId, projectDirectory } = context; + const { messageId, projectDirectory, onToolEvent } = context; const { filePath } = input; console.info(`Reading file ${filePath} for message ${messageId}`); + // Emit start event + onToolEvent?.({ + tool: 'readFileTool', + event: 'start', + args: input, + }); + try { // Convert to absolute path if relative const absolutePath = path.isAbsolute(filePath) @@ -69,21 +76,41 @@ export function createReadFileToolExecute(context: ReadFileToolContext) { console.info(`Successfully read file: ${filePath}`); - return { - status: 'success', + const output = { + status: 'success' as const, file_path: filePath, content: finalContent, truncated, }; + + // Emit complete event + onToolEvent?.({ + tool: 'readFileTool', + event: 'complete', + result: output, + args: input, + }); + + return output; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; console.error(`Error reading file ${filePath}:`, errorMessage); - return { - status: 'error', + const output = { + status: 'error' as const, file_path: filePath, error_message: errorMessage, }; + + // Emit complete event even on error + onToolEvent?.({ + tool: 'readFileTool', + event: 'complete', + result: output, + args: input, + }); + + return output; } }; } diff --git a/packages/ai/src/tools/file-tools/read-file-tool/read-file-tool.ts b/packages/ai/src/tools/file-tools/read-file-tool/read-file-tool.ts index cf9d47064..5dc9fbb6e 100644 --- a/packages/ai/src/tools/file-tools/read-file-tool/read-file-tool.ts +++ b/packages/ai/src/tools/file-tools/read-file-tool/read-file-tool.ts @@ -29,6 +29,7 @@ const ReadFileToolOutputSchema = z.discriminatedUnion('status', [ const ReadFileToolContextSchema = 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 ReadFileToolInput = z.infer;