starting to piece together ui for tasks

This commit is contained in:
dal 2025-10-03 08:09:09 -06:00
parent a59d4df920
commit edfa13e785
No known key found for this signature in database
GPG Key ID: 16F4B0E1E9F61122
14 changed files with 503 additions and 199 deletions

View File

@ -10,34 +10,20 @@ import {
ChatVersionTagline,
VimStatus,
} from '../components/chat-layout';
import { Diff } from '../components/diff';
import { SettingsForm } from '../components/settings-form';
import { type MessageType, TypedMessage } from '../components/typed-message';
import { AgentMessageComponent } from '../components/typed-message';
import type { DocsAgentMessage } from '../services/analytics-engineer-handler';
import { getSetting } from '../utils/settings';
import type { SlashCommand } from '../utils/slash-commands';
import type { VimMode } from '../utils/vim-mode';
type AppMode = 'Planning' | 'Auto-accept' | 'None';
interface Message {
id: number;
type: 'user' | 'assistant';
content: string;
messageType?: MessageType;
metadata?: string;
diffLines?: Array<{
lineNumber: number;
content: string;
type: 'add' | 'remove' | 'context';
}>;
fileName?: string;
}
export function Main() {
const { exit } = useApp();
const [input, setInput] = useState('');
const [history, setHistory] = useState<ChatHistoryEntry[]>([]);
const [messages, setMessages] = useState<Message[]>([]);
const [messages, setMessages] = useState<DocsAgentMessage[]>([]);
const historyCounter = useRef(0);
const messageCounter = useRef(0);
const [vimEnabled, setVimEnabled] = useState(() => getSetting('vimMode'));
@ -66,7 +52,43 @@ export function Main() {
}
});
const getMockResponse = (userInput: string): Message[] => {
const handleSubmit = useCallback(async () => {
const trimmed = input.trim();
if (!trimmed) {
setInput('');
return;
}
const userMessage: DocsAgentMessage = {
id: ++messageCounter.current,
message: {
kind: 'user',
content: trimmed,
},
};
setMessages((prev) => [...prev, userMessage]);
setInput('');
// Import and run the docs agent
const { runDocsAgent } = await import('../services/analytics-engineer-handler');
// Run agent - callbacks will handle message display
await runDocsAgent({
userMessage: trimmed,
onMessage: (agentMessage) => {
// Assign unique ID to each message
const messageWithId = {
id: ++messageCounter.current,
message: agentMessage.message,
};
setMessages((prev) => [...prev, messageWithId]);
},
});
}, [input]);
// REMOVED: Old getMockResponse function
const _oldGetMockResponse = (userInput: string) => {
const responses: Message[] = [];
if (userInput.toLowerCase().includes('plan')) {
@ -225,47 +247,9 @@ export function Main() {
});
}
return responses;
return []; // Old mock code removed
};
const handleSubmit = useCallback(async () => {
const trimmed = input.trim();
if (!trimmed) {
setInput('');
return;
}
messageCounter.current += 1;
const userMessage: Message = {
id: messageCounter.current,
type: 'user',
content: trimmed,
};
setMessages((prev) => [...prev, userMessage]);
setInput('');
// Import and run the docs agent
const { runDocsAgent } = await import('../services/analytics-engineer-handler');
await runDocsAgent({
userMessage: trimmed,
onMessage: (agentMessage) => {
messageCounter.current += 1;
setMessages((prev) => [
...prev,
{
id: messageCounter.current,
type: agentMessage.type,
content: agentMessage.content,
messageType: agentMessage.messageType,
metadata: agentMessage.metadata,
},
]);
},
});
}, [input]);
const handleCommandExecute = useCallback(
(command: SlashCommand) => {
switch (command.action) {
@ -309,35 +293,9 @@ export function Main() {
// Memoize message list to prevent re-renders from cursor blinking
const messageList = useMemo(() => {
return messages.map((message) => {
if (message.type === 'user') {
return (
<Box key={message.id} marginBottom={1}>
<Text color="#a855f7" bold>
{' '}
</Text>
<Text color="#e0e7ff">{message.content}</Text>
</Box>
);
} else if (message.messageType) {
return (
<Box key={message.id} flexDirection="column">
<TypedMessage
type={message.messageType}
content={message.content}
metadata={message.metadata}
/>
{message.diffLines && <Diff lines={message.diffLines} fileName={message.fileName} />}
</Box>
);
} else {
return (
<Box key={message.id} marginBottom={1}>
<Text color="#e0e7ff">{message.content}</Text>
</Box>
);
}
});
return messages.map((msg) => (
<AgentMessageComponent key={msg.id} message={msg.message} />
));
}, [messages]);
return (

View File

@ -0,0 +1,120 @@
import { Box, Text, useInput } from 'ink';
import React, { useState } from 'react';
import type { AgentMessage } from '../services/analytics-engineer-handler';
interface ExecuteMessageProps {
message: Extract<AgentMessage, { kind: 'bash' | 'grep' | 'ls' }>;
}
/**
* Component for displaying bash, grep, and ls command execution
* Shows EXECUTE badge, command description, and output logs
* Supports expansion with Ctrl+O to show full output
*/
export function ExecuteMessage({ message }: ExecuteMessageProps) {
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;
// Get command description and output based on tool type
let description = '';
let output = '';
let exitCode: number | undefined;
let success = true;
if (message.kind === 'bash') {
description = args.description || args.command;
if (result) {
output = result.stdout || result.stderr || '';
exitCode = result.exitCode;
success = result.success;
}
} else if (message.kind === 'grep') {
description = `Search for "${args.pattern}"${args.glob ? ` in ${args.glob}` : ''}`;
if (result) {
output = result.matches
.map((m) => `${m.path}:${m.lineNum}: ${m.lineText}`)
.join('\n');
success = result.totalMatches > 0;
}
} else if (message.kind === 'ls') {
description = `List directory ${args.path || '.'}`;
if (result) {
output = result.output;
success = result.success;
}
}
// Split output into lines for display
const outputLines = output.split('\n').filter(Boolean);
// Show last 5 lines when not expanded, all lines when expanded
const displayLines = isExpanded ? outputLines : outputLines.slice(-5);
return (
<Box flexDirection="column">
{/* EXECUTE badge with actual command in parentheses */}
<Box flexDirection="row">
<Text bold color="white" backgroundColor="orange">
EXECUTE
</Text>
<Text color="#94a3b8"> ({args.command})</Text>
</Box>
{/* Output lines - always show with indentation */}
{outputLines.length > 0 && (
<Box flexDirection="column" paddingLeft={2}>
{displayLines.map((line, idx) => (
<Text key={idx} color="#e0e7ff">
{line}
</Text>
))}
</Box>
)}
{/* Exit code/status line with indentation */}
{message.kind === 'bash' && exitCode !== undefined && (
<Box paddingLeft={2}>
<Text color={success ? '#64748b' : 'red'} dimColor>
Exit code: {exitCode}. Output: {outputLines.length} lines.
</Text>
</Box>
)}
{message.kind === 'grep' && result && (
<Box paddingLeft={2}>
<Text color={result.totalMatches > 0 ? 'green' : 'yellow'}>
Found {result.totalMatches} match{result.totalMatches !== 1 ? 'es' : ''}
{result.truncated ? ' (truncated)' : ''}
</Text>
</Box>
)}
{message.kind === 'ls' && result && (
<Box paddingLeft={2}>
<Text color={result.success ? 'green' : 'red'}>
Listed {result.count} file{result.count !== 1 ? 's' : ''}
{result.truncated ? ' (truncated)' : ''}
{result.errorMessage ? `: ${result.errorMessage}` : ''}
</Text>
</Box>
)}
{/* Expansion hint if output is long */}
{outputLines.length > 5 && (
<Box paddingLeft={2}>
<Text color="#64748b" dimColor>
{isExpanded ? '(Press Ctrl+O to collapse)' : `... (${outputLines.length - 5} more lines, press Ctrl+O to expand)`}
</Text>
</Box>
)}
</Box>
);
}

View File

@ -1,35 +1,48 @@
import { Box, Text } from 'ink';
import React from 'react';
import type { AgentMessage } from '../services/analytics-engineer-handler';
import { ExecuteMessage } from './execute-message';
export type MessageType = 'PLAN' | 'EXECUTE' | 'WRITE' | 'EDIT';
interface TypedMessageProps {
type: MessageType;
content: string;
metadata?: string;
interface AgentMessageComponentProps {
message: AgentMessage;
}
const typeStyles: Record<MessageType, { bg: string; fg: string; label: string }> = {
PLAN: { bg: '#fb923c', fg: '#000000', label: 'PLAN' },
EXECUTE: { bg: '#fb923c', fg: '#000000', label: 'EXECUTE' },
WRITE: { bg: '#3b82f6', fg: '#ffffff', label: 'WRITE' },
EDIT: { bg: '#22c55e', fg: '#ffffff', label: 'EDIT' },
};
export function AgentMessageComponent({ message }: AgentMessageComponentProps) {
switch (message.kind) {
case 'user':
return (
<Box marginBottom={1}>
<Text color="#a855f7" bold>
{' '}
</Text>
<Text color="#e0e7ff">{message.content}</Text>
</Box>
);
export function TypedMessage({ type, content, metadata }: TypedMessageProps) {
const style = typeStyles[type];
case 'text-delta':
return (
<Box marginBottom={1}>
<Text color="#e0e7ff">{message.content}</Text>
</Box>
);
return (
<Box flexDirection="column" marginBottom={1}>
<Box gap={1}>
<Text backgroundColor={style.bg} color={style.fg} bold>
{` ${style.label} `}
</Text>
{metadata && <Text dimColor>{metadata}</Text>}
</Box>
<Box marginLeft={2} marginTop={0}>
<Text>{content}</Text>
</Box>
</Box>
);
case 'idle':
// For idle tool, just show the final response as plain text
return (
<Box marginBottom={1}>
<Text color="#e0e7ff">
{message.args?.final_response || 'Task completed'}
</Text>
</Box>
);
case 'bash':
case 'grep':
case 'ls':
// For execute commands, use the ExecuteMessage component
return <ExecuteMessage message={message} />;
default:
return null;
}
}

View File

@ -1,15 +1,45 @@
import { randomUUID } from 'node:crypto';
import { createProxyModel } from '@buster/ai/llm/providers/proxy-model';
import type { ModelMessage } from '@buster/ai';
import type {
BashToolInput,
BashToolOutput,
GrepToolInput,
GrepToolOutput,
IdleInput,
LsToolInput,
LsToolOutput,
ModelMessage,
ToolEvent,
} from '@buster/ai';
import { getProxyConfig } from '../utils/ai-proxy';
import { createAnalyticsEngineerAgent } from '@buster/ai/agents/analytics-engineer-agent/analytics-engineer-agent';
// Discriminated union for all possible message types - uses tool types directly
export type AgentMessage =
| { kind: 'user'; content: string }
| { kind: 'text-delta'; content: string }
| { kind: 'idle'; args: IdleInput }
| {
kind: 'bash';
event: 'start' | 'complete';
args: BashToolInput;
result?: BashToolOutput;
}
| {
kind: 'grep';
event: 'start' | 'complete';
args: GrepToolInput;
result?: GrepToolOutput;
}
| {
kind: 'ls';
event: 'start' | 'complete';
args: LsToolInput;
result?: LsToolOutput;
};
export interface DocsAgentMessage {
id: number;
type: 'user' | 'assistant';
content: string;
messageType?: 'PLAN' | 'EDIT' | 'EXECUTE' | 'WRITE' | 'WEB_SEARCH';
metadata?: string;
message: AgentMessage;
}
export interface RunDocsAgentParams {
@ -21,11 +51,9 @@ export interface RunDocsAgentParams {
* Runs the docs agent in the CLI without sandbox
* The agent runs locally but uses the proxy model to route LLM calls through the server
*/
export async function runDocsAgent(params: RunDocsAgentParams): Promise<void> {
export async function runDocsAgent(params: RunDocsAgentParams) {
const { userMessage, onMessage } = params;
let messageId = 1;
// Get proxy configuration
const proxyConfig = await getProxyConfig();
@ -36,16 +64,65 @@ export async function runDocsAgent(params: RunDocsAgentParams): Promise<void> {
modelId: 'anthropic/claude-4-sonnet-20250514',
});
// Create the docs agent with proxy model
// Create the docs agent with proxy model and typed event callback
// Tools are handled locally, only model calls go through proxy
const analyticsEngineerAgent = createAnalyticsEngineerAgent({
folder_structure: 'CLI mode - limited file access',
folder_structure: process.cwd(), // Use current working directory for CLI mode
userId: 'cli-user',
chatId: randomUUID(),
dataSourceId: '',
organizationId: 'cli',
messageId: randomUUID(),
model: proxyModel,
// Handle typed tool events - TypeScript knows exact shape based on discriminants
onToolEvent: (event: ToolEvent) => {
// Type narrowing: TypeScript knows event.args and event.result types!
if (event.tool === 'idleTool' && event.event === 'complete') {
// event.args is IdleInput, event.result is IdleOutput - fully typed!
onMessage({
message: {
kind: 'idle',
args: event.args, // Type-safe: IdleInput
},
});
}
// Handle bash tool events - only show complete to avoid duplicates
if (event.tool === 'bashTool' && event.event === 'complete') {
onMessage({
message: {
kind: 'bash',
event: 'complete',
args: event.args,
result: event.result, // Type-safe: BashToolOutput
},
});
}
// Handle grep tool events - only show complete to avoid duplicates
if (event.tool === 'grepTool' && event.event === 'complete') {
onMessage({
message: {
kind: 'grep',
event: 'complete',
args: event.args,
result: event.result, // Type-safe: GrepToolOutput
},
});
}
// Handle ls tool events - only show complete to avoid duplicates
if (event.tool === 'lsTool' && event.event === 'complete') {
onMessage({
message: {
kind: 'ls',
event: 'complete',
args: event.args,
result: event.result, // Type-safe: LsToolOutput
},
});
}
},
});
const messages: ModelMessage[] = [
@ -55,65 +132,13 @@ export async function runDocsAgent(params: RunDocsAgentParams): Promise<void> {
},
];
try {
// Execute the docs agent
const result = await analyticsEngineerAgent.stream({ messages });
// Start the stream - this triggers the agent to run
const stream = await analyticsEngineerAgent.stream({ messages });
// Stream the response
for await (const part of result.fullStream) {
// Handle different stream part types
if (part.type === 'text-delta') {
onMessage({
id: messageId++,
type: 'assistant',
content: part.delta,
});
} else if (part.type === 'tool-call') {
// Map tool calls to message types
let messageType: DocsAgentMessage['messageType'];
let content = '';
const metadata = '';
switch (part.toolName) {
case 'sequentialThinking':
messageType = 'PLAN';
content = 'Planning next steps...';
break;
case 'bashExecute':
messageType = 'EXECUTE';
content = 'Executing command...';
break;
case 'webSearch':
messageType = 'WEB_SEARCH';
content = 'Searching the web...';
break;
case 'grepSearch':
messageType = 'EXECUTE';
content = 'Searching files...';
break;
case 'idleTool':
messageType = 'EXECUTE';
content = 'Entering idle state...';
break;
default:
content = `Using tool: ${part.toolName}`;
}
onMessage({
id: messageId++,
type: 'assistant',
content,
messageType,
metadata,
});
}
// Ignore other stream part types (start, finish, etc.)
}
} catch (error) {
onMessage({
id: messageId++,
type: 'assistant',
content: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
});
// Consume the stream to trigger tool execution
// Tools will call callbacks directly when they execute
for await (const _part of stream.fullStream) {
// Stream parts are consumed but tools handle their own display via callbacks
// In the future we could handle text-delta here if needed
}
}

View File

@ -12,6 +12,7 @@ import { createGrepTool } from '../../tools/file-tools/grep-tool/grep-tool';
import { createReadFileTool } from '../../tools/file-tools/read-file-tool/read-file-tool';
import { type AgentContext, repairToolCall } from '../../utils/tool-call-repair';
import { getDocsAgentSystemPrompt as getAnalyticsEngineerAgentSystemPrompt } from './get-analytics-engineer-agent-system-prompt';
import type { ToolEventCallback } from './tool-events';
export const ANALYST_ENGINEER_AGENT_NAME = 'analyticsEngineerAgent';
@ -42,7 +43,9 @@ const AnalyticsEngineerAgentStreamOptionsSchema = z.object({
messages: z.array(z.custom<ModelMessage>()).describe('The messages to send to the docs agent'),
});
export type AnalyticsEngineerAgentOptions = z.infer<typeof AnalyticsEngineerAgentOptionsSchema>;
export type AnalyticsEngineerAgentOptions = z.infer<typeof AnalyticsEngineerAgentOptionsSchema> & {
onToolEvent?: ToolEventCallback;
};
export type AnalyticsEngineerAgentStreamOptions = z.infer<typeof AnalyticsEngineerAgentStreamOptionsSchema>;
// Extended type for passing to tools (includes sandbox)
@ -55,7 +58,9 @@ export function createAnalyticsEngineerAgent(analyticsEngineerAgentOptions: Anal
providerOptions: DEFAULT_ANTHROPIC_OPTIONS,
} as ModelMessage;
const idleTool = createIdleTool();
const idleTool = createIdleTool({
onToolEvent: analyticsEngineerAgentOptions.onToolEvent,
});
const writeFileTool = createWriteFileTool({
messageId: analyticsEngineerAgentOptions.messageId,
projectDirectory: analyticsEngineerAgentOptions.folder_structure,
@ -63,6 +68,7 @@ export function createAnalyticsEngineerAgent(analyticsEngineerAgentOptions: Anal
const grepTool = createGrepTool({
messageId: analyticsEngineerAgentOptions.messageId,
projectDirectory: analyticsEngineerAgentOptions.folder_structure,
onToolEvent: analyticsEngineerAgentOptions.onToolEvent,
});
const readFileTool = createReadFileTool({
messageId: analyticsEngineerAgentOptions.messageId,
@ -71,6 +77,7 @@ export function createAnalyticsEngineerAgent(analyticsEngineerAgentOptions: Anal
const bashTool = createBashTool({
messageId: analyticsEngineerAgentOptions.messageId,
projectDirectory: analyticsEngineerAgentOptions.folder_structure,
onToolEvent: analyticsEngineerAgentOptions.onToolEvent,
});
const editFileTool = createEditFileTool({
messageId: analyticsEngineerAgentOptions.messageId,
@ -83,6 +90,7 @@ export function createAnalyticsEngineerAgent(analyticsEngineerAgentOptions: Anal
const lsTool = createLsTool({
messageId: analyticsEngineerAgentOptions.messageId,
projectDirectory: analyticsEngineerAgentOptions.folder_structure,
onToolEvent: analyticsEngineerAgentOptions.onToolEvent,
});
// Create planning tools with simple context

View File

@ -0,0 +1,53 @@
import type { IdleInput, IdleOutput } from '../../tools/communication-tools/idle-tool/idle-tool';
import type { BashToolInput, BashToolOutput } from '../../tools/file-tools/bash-tool/bash-tool';
import type {
EditFileToolInput,
EditFileToolOutput,
} from '../../tools/file-tools/edit-file-tool/edit-file-tool';
import type { GrepToolInput, GrepToolOutput } from '../../tools/file-tools/grep-tool/grep-tool';
import type { LsToolInput, LsToolOutput } from '../../tools/file-tools/ls-tool/ls-tool';
import type {
ReadFileToolInput,
ReadFileToolOutput,
} from '../../tools/file-tools/read-file-tool/read-file-tool';
import type {
WriteFileToolInput,
WriteFileToolOutput,
} from '../../tools/file-tools/write-file-tool/write-file-tool';
/**
* Discriminated union of all possible tool events in the analytics engineer agent
* This provides full type safety from tools -> CLI display
*/
export type ToolEvent =
// Idle tool events
| { tool: 'idleTool'; event: 'start'; args: IdleInput }
| { tool: 'idleTool'; event: 'complete'; result: IdleOutput; args: IdleInput }
// Bash tool events
| { tool: 'bashTool'; event: 'start'; args: BashToolInput }
| { tool: 'bashTool'; event: 'complete'; result: BashToolOutput; args: BashToolInput }
// Grep tool events
| { tool: 'grepTool'; event: 'start'; args: GrepToolInput }
| { tool: 'grepTool'; event: 'complete'; result: GrepToolOutput; args: GrepToolInput }
// Read file tool events
| { tool: 'readFileTool'; event: 'start'; args: ReadFileToolInput }
| { tool: 'readFileTool'; event: 'complete'; result: ReadFileToolOutput; args: ReadFileToolInput }
// Write file tool events
| { tool: 'writeFileTool'; event: 'start'; args: WriteFileToolInput }
| {
tool: 'writeFileTool';
event: 'complete';
result: WriteFileToolOutput;
args: WriteFileToolInput;
}
// Edit file tool events
| { tool: 'editFileTool'; event: 'start'; args: EditFileToolInput }
| { tool: 'editFileTool'; event: 'complete'; result: EditFileToolOutput; args: EditFileToolInput }
// Ls tool events
| { tool: 'lsTool'; event: 'start'; args: LsToolInput }
| { tool: 'lsTool'; event: 'complete'; result: LsToolOutput; args: LsToolInput };
/**
* Callback type for tool events - single typed callback for all tools
*/
export type ToolEventCallback = (event: ToolEvent) => void;

View File

@ -4,3 +4,36 @@ export * from './workflows';
export * from './utils';
export * from './embeddings';
export * from './tasks';
// Export tool types for CLI usage
export type {
IdleInput,
IdleOutput,
} from './tools/communication-tools/idle-tool/idle-tool';
export type {
BashToolInput,
BashToolOutput,
} from './tools/file-tools/bash-tool/bash-tool';
export type {
GrepToolInput,
GrepToolOutput,
} from './tools/file-tools/grep-tool/grep-tool';
export type {
ReadFileToolInput,
ReadFileToolOutput,
} from './tools/file-tools/read-file-tool/read-file-tool';
export type {
WriteFileToolInput,
WriteFileToolOutput,
} from './tools/file-tools/write-file-tool/write-file-tool';
export type {
EditFileToolInput,
EditFileToolOutput,
} from './tools/file-tools/edit-file-tool/edit-file-tool';
export type {
LsToolInput,
LsToolOutput,
} from './tools/file-tools/ls-tool/ls-tool';
// Export typed tool events for type-safe tool callbacks
export type { ToolEvent, ToolEventCallback } from './agents/analytics-engineer-agent/tool-events';

View File

@ -19,6 +19,7 @@ const IdleOutputSchema = z.object({
// Optional context for consistency with other tools
const IdleContextSchema = z.object({
messageId: z.string().optional().describe('The message ID for tracking tool execution.'),
onToolEvent: z.any().optional(),
});
export type IdleInput = z.infer<typeof IdleInputSchema>;
@ -29,18 +30,28 @@ async function processIdle(): Promise<IdleOutput> {
return { success: true };
}
function createIdleExecute() {
function createIdleExecute<TAgentContext extends IdleContext = IdleContext>(context?: TAgentContext) {
return wrapTraced(
async (): Promise<IdleOutput> => {
return await processIdle();
async (args: IdleInput) => {
const result: IdleOutput = await processIdle();
// Emit typed tool event when idle tool completes
context?.onToolEvent?.({
tool: 'idleTool',
event: 'complete',
result,
args,
});
return result;
},
{ name: 'idle-tool' }
);
}
// Factory: simple tool without streaming lifecycle
export function createIdleTool() {
const execute = createIdleExecute();
export function createIdleTool<TAgentContext extends IdleContext = IdleContext>(context?: TAgentContext) {
const execute = createIdleExecute(context);
return tool({
description:

View File

@ -22,7 +22,8 @@ async function executeCommand(
const timeoutId = setTimeout(() => controller.abort(), timeout);
// Execute command using Bun.spawn
const proc = Bun.spawn(['bash', '-c', command], {
// Use full path to bash for reliability
const proc = Bun.spawn(['/bin/bash', '-c', command], {
cwd: projectDirectory,
stdout: 'pipe',
stderr: 'pipe',
@ -113,11 +114,18 @@ async function executeCommand(
*/
export function createBashToolExecute(context: BashToolContext) {
return async function execute(input: BashToolInput): Promise<BashToolOutput> {
const { messageId, projectDirectory } = context;
const { messageId, projectDirectory, onToolEvent } = context;
const { command, timeout } = input;
console.info(`Executing bash command for message ${messageId}: ${command}`);
// Emit start event
onToolEvent?.({
tool: 'bashTool',
event: 'start',
args: input,
});
const commandTimeout = Math.min(timeout ?? DEFAULT_TIMEOUT, MAX_TIMEOUT);
const result = await executeCommand(command, commandTimeout, projectDirectory);
@ -128,6 +136,14 @@ export function createBashToolExecute(context: BashToolContext) {
console.error(`Command failed: ${command}`, result.error);
}
// Emit complete event
onToolEvent?.({
tool: 'bashTool',
event: 'complete',
result,
args: input,
});
return result;
};
}

View File

@ -28,6 +28,7 @@ export const BashToolOutputSchema = z.object({
const BashToolContextSchema = 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(),
});
export type BashToolInput = z.infer<typeof BashToolInputSchema>;

View File

@ -91,7 +91,7 @@ async function executeRipgrep(
*/
export function createGrepSearchToolExecute(context: GrepToolContext) {
return async function execute(input: GrepToolInput): Promise<GrepToolOutput> {
const { messageId, projectDirectory } = context;
const { messageId, projectDirectory, onToolEvent } = context;
const { pattern, path, glob } = input;
if (!pattern) {
@ -102,6 +102,13 @@ export function createGrepSearchToolExecute(context: GrepToolContext) {
console.info(`Searching for pattern "${pattern}" in ${searchPath} for message ${messageId}`);
// Emit start event
onToolEvent?.({
tool: 'grepTool',
event: 'start',
args: input,
});
try {
// Execute ripgrep
const matches = await executeRipgrep(pattern, searchPath, glob);
@ -117,23 +124,43 @@ export function createGrepSearchToolExecute(context: GrepToolContext) {
`Search complete: ${finalMatches.length} matches found${truncated ? ' (truncated)' : ''}`
);
return {
const result = {
pattern,
matches: finalMatches,
totalMatches: finalMatches.length,
truncated,
};
// Emit complete event
onToolEvent?.({
tool: 'grepTool',
event: 'complete',
result,
args: input,
});
return result;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
console.error(`Grep search failed:`, errorMessage);
// Return empty results on error
return {
const result = {
pattern,
matches: [],
totalMatches: 0,
truncated: false,
};
// Emit complete event even on error
onToolEvent?.({
tool: 'grepTool',
event: 'complete',
result,
args: input,
});
return result;
}
};
}

View File

@ -34,6 +34,7 @@ const GrepToolOutputSchema = z.object({
const GrepSearchContextSchema = 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(),
});
export type GrepToolInput = z.infer<typeof GrepToolInputSchema>;

View File

@ -159,7 +159,7 @@ function renderDir(
*/
export function createLsToolExecute(context: LsToolContext) {
return async function execute(input: LsToolInput): Promise<LsToolOutput> {
const { messageId, projectDirectory } = context;
const { messageId, projectDirectory, onToolEvent } = context;
const searchPath = path.resolve(
projectDirectory,
input.path || projectDirectory
@ -167,11 +167,18 @@ export function createLsToolExecute(context: LsToolContext) {
console.info(`Listing directory ${searchPath} for message ${messageId}`);
// Emit start event
onToolEvent?.({
tool: 'lsTool',
event: 'start',
args: input,
});
try {
// Validate the path exists and is a directory
const stats = await stat(searchPath);
if (!stats.isDirectory()) {
return {
const result = {
success: false,
path: searchPath,
output: '',
@ -179,6 +186,16 @@ export function createLsToolExecute(context: LsToolContext) {
truncated: false,
errorMessage: `Path is not a directory: ${searchPath}`,
};
// Emit complete event
onToolEvent?.({
tool: 'lsTool',
event: 'complete',
result,
args: input,
});
return result;
}
// Build ignore patterns
@ -225,19 +242,29 @@ export function createLsToolExecute(context: LsToolContext) {
`Listed ${files.length} file(s) in ${searchPath}${files.length >= LIMIT ? ' (truncated)' : ''}`
);
return {
const result = {
success: true,
path: searchPath,
output,
count: files.length,
truncated: files.length >= LIMIT,
};
// Emit complete event
onToolEvent?.({
tool: 'lsTool',
event: 'complete',
result,
args: input,
});
return result;
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : 'Unknown error';
console.error(`Error listing directory ${searchPath}:`, errorMessage);
return {
const result = {
success: false,
path: searchPath,
output: '',
@ -245,6 +272,16 @@ export function createLsToolExecute(context: LsToolContext) {
truncated: false,
errorMessage,
};
// Emit complete event even on error
onToolEvent?.({
tool: 'lsTool',
event: 'complete',
result,
args: input,
});
return result;
}
};
}

View File

@ -28,6 +28,7 @@ export const LsToolOutputSchema = z.object({
export const LsToolContextSchema = 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(),
});
export type LsToolInput = z.infer<typeof LsToolInputSchema>;