diff --git a/src/components/chat/tools/README.md b/src/components/chat/tools/README.md index b6b9387..206d90f 100644 --- a/src/components/chat/tools/README.md +++ b/src/components/chat/tools/README.md @@ -2,7 +2,9 @@ ## Overview -This folder contains a **config-driven architecture** for rendering tool executions in the chat interface. Instead of scattered conditional logic, all tool rendering is centralized through configurations and reusable display components. +Config-driven architecture for rendering tool executions in chat. All tool display behavior is defined in `toolConfigs.ts` — no scattered conditionals. Two base display patterns: **OneLineDisplay** for compact tools, **CollapsibleDisplay** for tools with expandable content. + +Non-error tool results route through `ToolRenderer` with `mode="result"` (single source of truth). Error results are handled inline in `MessageComponent` with a red error box. --- @@ -10,407 +12,213 @@ This folder contains a **config-driven architecture** for rendering tool executi ``` tools/ -├── components/ # Reusable display components -│ ├── OneLineDisplay.tsx # Simple one-line tool displays -│ ├── CollapsibleDisplay.tsx # Expandable tool displays -│ ├── ContentRenderers/ # Content-specific renderers -│ │ ├── DiffViewer.tsx # File diff viewer -│ │ ├── MarkdownContent.tsx # Markdown renderer -│ │ ├── FileListContent.tsx # File list for search results -│ │ ├── TodoListContent.tsx # Todo list renderer -│ │ └── TextContent.tsx # Plain text/JSON/code -│ ├── FilePathButton.tsx # Clickable file paths -│ └── CollapsibleSection.tsx # Collapsible wrapper -├── configs/ # Tool configurations -│ ├── types.ts # TypeScript interfaces -│ └── toolConfigs.ts # All tool configs (10 tools) -├── ToolRenderer.tsx # Main router component -└── README.md # This file +├── components/ +│ ├── OneLineDisplay.tsx # Compact one-line tool display +│ ├── CollapsibleDisplay.tsx # Expandable tool display (uses children pattern) +│ ├── CollapsibleSection.tsx #
/ wrapper +│ ├── ContentRenderers/ +│ │ ├── DiffViewer.tsx # File diff viewer (memoized) +│ │ ├── MarkdownContent.tsx # Markdown renderer +│ │ ├── FileListContent.tsx # Comma-separated clickable file list +│ │ ├── TodoListContent.tsx # Todo items with status badges +│ │ ├── TaskListContent.tsx # Task tracker with progress bar +│ │ └── TextContent.tsx # Plain text / JSON / code +├── configs/ +│ └── toolConfigs.ts # All tool configs + ToolDisplayConfig type +├── ToolRenderer.tsx # Main router (React.memo wrapped) +└── README.md ``` --- -## Core Concepts +## Display Patterns -### 1. Display Components +### OneLineDisplay -All tools use one of two base display patterns: +Used by: Bash, Read, Grep, Glob, TodoRead, TaskCreate, TaskUpdate, TaskGet -#### **OneLineDisplay** - For simple tools -Used by: Bash, Read, Grep, Glob, TodoRead +Renders as a single line with `border-l-2` accent. Supports multiple rendering modes based on `action`: + +- **terminal** (`style: 'terminal'`) — Dark pill around command text, green `$` prompt +- **open-file** — Shows filename only (truncated from full path), clickable to open +- **jump-to-results** — Shows pattern with anchor link to result section +- **copy** — Shows value with hover copy button +- **none** — Plain display ```tsx ...} // Action callback - colorScheme={{ // Optional color customization - primary: "text-...", - secondary: "text-..." + toolName="Read" + icon="terminal" // Optional icon or style keyword + label="Read" // Tool label + value="/path/to/file.ts" // Main display value + secondary="description" // Optional secondary text (italic) + action="open-file" // Action type + onAction={() => ...} // Click handler + colorScheme={{ // Per-tool colors + primary: 'text-...', + border: 'border-...', + icon: 'text-...' }} + resultId="tool-result-x" // For jump-to-results anchor + toolResult={...} // For conditional jump arrow + toolId="x" // Tool use ID /> ``` -#### **CollapsibleDisplay** - For complex tools -Used by: Edit, Write, Plan, TodoWrite, Grep/Glob results +### CollapsibleDisplay + +Used by: Edit, Write, ApplyPatch, Grep/Glob results, TodoWrite, TaskList/TaskGet results, ExitPlanMode, Default + +Wraps `CollapsibleSection` (`
`/``) with a `border-l-2` accent colored by tool category. Accepts **children** directly (not contentProps). ```tsx } // Optional action button - contentType="diff" // Type of content to render - contentProps={{...}} // Props for content renderer - showRawParameters={true} // Show raw JSON? - rawContent="..." // Raw JSON content -/> + toolName="Edit" + toolId="123" + title="filename.ts" // Section title (can be clickable) + defaultOpen={false} + onTitleClick={() => ...} // Makes title a clickable link (for edit tools) + showRawParameters={true} // Show raw JSON toggle + rawContent="..." // Raw JSON string + toolCategory="edit" // Drives border color +> + // Content as children + ``` -### 2. Content Renderers - -Different content types are handled by specialized renderers: - -- **diff** → `DiffViewer` - Shows before/after file changes -- **markdown** → `MarkdownContent` - Renders markdown with styling -- **file-list** → `FileListContent` - Clickable file list -- **todo-list** → `TodoListContent` - Todo items with status -- **text** → `TextContent` - Plain text, JSON, or code - -### 3. Configuration-Driven - -Every tool is defined by a config object. No code changes needed to add/modify tools! +**Tool category colors** (via `border-l-2`): +| Category | Tools | Color | +|----------|-------|-------| +| `edit` | Edit, Write, ApplyPatch | amber | +| `bash` | Bash | green | +| `search` | Grep, Glob | gray | +| `todo` | TodoWrite, TodoRead | violet | +| `task` | TaskCreate/Update/List/Get | violet | +| `plan` | ExitPlanMode | indigo | +| `default` | everything else | neutral gray | --- -## How to Add a New Tool +## Content Renderers -### Example: Adding a "Format" tool +Specialized components for different content types, rendered as children of `CollapsibleDisplay`: + +| contentType | Component | Used by | +|---|---|---| +| `diff` | `DiffViewer` | Edit, Write, ApplyPatch | +| `markdown` | `MarkdownContent` | ExitPlanMode | +| `file-list` | `FileListContent` | Grep/Glob results | +| `todo-list` | `TodoListContent` | TodoWrite, TodoRead | +| `task` | `TaskListContent` | TaskList, TaskGet results | +| `text` | `TextContent` | Default fallback | +| `success-message` | inline SVG | TodoWrite result | + +--- + +## Adding a New Tool **Step 1:** Add config to `configs/toolConfigs.ts` ```typescript -Format: { +MyTool: { input: { type: 'one-line', // or 'collapsible' - label: 'Format', - getValue: (input) => input.file_path, + label: 'MyTool', + getValue: (input) => input.some_field, action: 'open-file', colorScheme: { - primary: 'text-purple-600 dark:text-purple-400' + primary: 'text-purple-600 dark:text-purple-400', + border: 'border-purple-400 dark:border-purple-500' } }, result: { - hideOnSuccess: true // Hide successful results + hideOnSuccess: true // Only show errors } } ``` -**Step 2:** That's it! No other files to touch. +**Step 2:** If the tool needs a category color, add it to `getToolCategory()` in `ToolRenderer.tsx`. -The ToolRenderer automatically: -- Parses the tool input -- Selects the right display component -- Passes the correct props -- Handles callbacks (file opening, copy, etc.) +**That's it.** The ToolRenderer auto-routes based on config. --- ## Configuration Reference -### Input Configuration +### ToolDisplayConfig ```typescript -input: { - // Display type (required) - type: 'one-line' | 'collapsible' | 'hidden' - - // One-line specific - icon?: string // Icon to display (e.g., "$", "✓") - label?: string // Text label (e.g., "Read", "Grep") - getValue?: (input) => string // Extract main value from input - getSecondary?: (input) => string // Extract secondary text (description) - action?: 'copy' | 'open-file' | 'jump-to-results' | 'none' - colorScheme?: { - primary?: string // Tailwind classes for main text - secondary?: string // Tailwind classes for secondary text - } - - // Collapsible specific - title?: string | ((input) => string) // Section title - defaultOpen?: boolean // Auto-expand? - contentType?: 'diff' | 'markdown' | 'file-list' | 'todo-list' | 'text' - getContentProps?: (input, helpers) => any // Extract props for content renderer - actionButton?: 'file-button' | 'none' // Show file path button? -} -``` - -### Result Configuration - -```typescript -result?: { - hidden?: boolean // Never show results - hideOnSuccess?: boolean // Only show errors - type?: 'one-line' | 'collapsible' | 'special' - contentType?: 'markdown' | 'file-list' | 'todo-list' | 'text' | 'success-message' - getMessage?: (result) => string // For success messages - getContentProps?: (result) => any // Extract content props -} -``` - ---- - -## Real-World Examples - -### Simple One-Line Tool (Bash) - -```typescript -Bash: { +interface ToolDisplayConfig { input: { - type: 'one-line', - icon: '$', - getValue: (input) => input.command, - getSecondary: (input) => input.description, - action: 'copy' - }, - result: { - hideOnSuccess: true - } -} -``` + type: 'one-line' | 'collapsible' | 'hidden'; -**Renders:** -``` -$ npm install (Install dependencies) [Copy Button] -``` + // One-line + icon?: string; + label?: string; + getValue?: (input) => string; + getSecondary?: (input) => string | undefined; + action?: 'copy' | 'open-file' | 'jump-to-results' | 'none'; + style?: string; // 'terminal' for Bash + wrapText?: boolean; + colorScheme?: { + primary?: string; + secondary?: string; + background?: string; + border?: string; + icon?: string; + }; -### Collapsible Diff Tool (Edit) + // Collapsible + title?: string | ((input) => string); + defaultOpen?: boolean; + contentType?: 'diff' | 'markdown' | 'file-list' | 'todo-list' | 'text' | 'task'; + getContentProps?: (input, helpers?) => any; + actionButton?: 'none'; + }; -```typescript -Edit: { - input: { - type: 'collapsible', - title: 'View edit diff for', - contentType: 'diff', - actionButton: 'file-button', - getContentProps: (input) => ({ - oldContent: input.old_string, - newContent: input.new_string, - filePath: input.file_path - }) - }, - result: { - hideOnSuccess: true - } -} -``` - -**Renders:** -``` -▶ View edit diff for [file.ts] - ┌─────────────────────────┐ - │ - old line │ - │ + new line │ - └─────────────────────────┘ -``` - -### Search Results (Grep) - -```typescript -Grep: { - input: { - type: 'one-line', - label: 'Grep', - getValue: (input) => input.pattern, - getSecondary: (input) => input.path ? `in ${input.path}` : undefined, - action: 'jump-to-results' - }, - result: { - type: 'collapsible', - contentType: 'file-list', - getContentProps: (result) => ({ - files: result.toolUseResult?.filenames || [], - title: `Found ${count} files` - }) - } -} -``` - -**Renders:** -``` -Input: Grep "TODO" in src/ [Search results ↓] - -Result: Found 5 files - 📄 app.ts (src/) - 📄 utils.ts (src/utils/) - ... -``` - ---- - -## Advanced Features - -### Dynamic Content Props with Helpers - -For tools that need API calls or complex logic: - -```typescript -getContentProps: (input, helpers) => { - const { selectedProject, createDiff, onFileOpen } = helpers; - - // Use helpers for complex operations - return { - filePath: input.file_path, - onFileClick: () => onFileOpen(input.file_path) + result?: { + hidden?: boolean; // Never show + hideOnSuccess?: boolean; // Only show errors + type?: 'one-line' | 'collapsible' | 'special'; + title?: string | ((result) => string); + defaultOpen?: boolean; + contentType?: 'markdown' | 'file-list' | 'todo-list' | 'text' | 'success-message' | 'task'; + getMessage?: (result) => string; + getContentProps?: (result) => any; }; } ``` -### File Opening Logic +--- -The ToolRenderer handles file opening automatically for Edit/Write tools: +## All Configured Tools -1. Fetches current file via API -2. Reverse-applies edits (for Edit) -3. Opens file in diff view -4. Falls back gracefully on errors - -No config needed - handled by `actionButton: 'file-button'`. - -### Custom Color Schemes - -Override default colors per tool: - -```typescript -colorScheme: { - primary: 'text-purple-600 dark:text-purple-400', - secondary: 'text-purple-400 dark:text-purple-500' -} -``` +| Tool | Input | Result | Notes | +|------|-------|--------|-------| +| Bash | terminal one-line | hide success | Dark command pill, green accent | +| Read | one-line (open-file) | hidden | Shows filename, clicks to open | +| Edit | collapsible (diff) | hide success | Amber border, clickable filename | +| Write | collapsible (diff) | hide success | "New" badge on diff | +| ApplyPatch | collapsible (diff) | hide success | "Patch" badge on diff | +| Grep | one-line (jump) | collapsible file-list | Collapsed by default | +| Glob | one-line (jump) | collapsible file-list | Collapsed by default | +| TodoWrite | collapsible (todo-list) | success message | | +| TodoRead | one-line | collapsible todo-list | | +| TaskCreate | one-line | hide success | Shows task subject | +| TaskUpdate | one-line | hide success | Shows `#id → status` | +| TaskList | one-line | collapsible task | Progress bar, status icons | +| TaskGet | one-line | collapsible task | Task details with status | +| ExitPlanMode | collapsible (markdown) | collapsible markdown | Also registered as `exit_plan_mode` | +| Default | collapsible (code) | collapsible text | Fallback for unknown tools | --- -## Component Props Reference - -### ToolRenderer (Main Entry Point) - -```typescript - ...} - createDiff={(old, new) => [...]} - // Context - selectedProject={...} - // Display options - autoExpandTools={false} - showRawParameters={false} - rawToolInput="..." -/> -``` - -### OneLineDisplay - -```typescript -interface OneLineDisplayProps { - icon?: string; - label?: string; - value: string; // Required - secondary?: string; - action?: ActionType; - onAction?: () => void; - colorScheme?: { primary, secondary }; - resultId?: string; // For jump-to-results -} -``` - -### CollapsibleDisplay - -```typescript -interface CollapsibleDisplayProps { - title: string; // Required - defaultOpen?: boolean; - action?: React.ReactNode; - contentType: ContentType; // Required - contentProps: any; // Required - showRawParameters?: boolean; - rawContent?: string; - className?: string; -} -``` - ---- - -## Testing Your Tool - -### 1. Add Config -Add your tool to `toolConfigs.ts` - -### 2. Test Input Rendering -Trigger the tool and verify: -- ✅ Correct display component used (one-line vs collapsible) -- ✅ Values extracted correctly from input -- ✅ Actions work (copy, file open, jump) -- ✅ Colors and styling correct - -### 3. Test Result Rendering -Check tool results: -- ✅ Results hidden when appropriate -- ✅ Error results always shown -- ✅ Content rendered correctly -- ✅ Interactive elements work - -### 4. Test Edge Cases -- Empty inputs -- Missing fields -- Parse errors -- API failures - ---- - - - -## Performance Notes - -### Config Loading -- Configs are loaded once at module import -- No runtime overhead for config lookups -- Tree-shaking removes unused configs in production - -### Component Rendering -- Display components are memoized where appropriate -- Content renderers only render when props change -- Collapsible sections lazy-load content - -### API Calls -- File opening uses async/await with error handling -- Failures gracefully fall back to simple file open -- API module dynamically imported to reduce bundle size - ---- - - - -## Quick Reference - -### All Configured Tools - -| Tool | Type | Display | Action | Result | -|------|------|---------|--------|--------| -| Bash | one-line | $ command | copy | hide success | -| Read | one-line | Read file.ts | open-file | hidden | -| Edit | collapsible | diff viewer | file-button | hide success | -| Write | collapsible | diff viewer | file-button | hide success | -| ApplyPatch | collapsible | diff viewer | file-button | hide success | -| Grep | one-line | pattern | jump | file-list | -| Glob | one-line | pattern | jump | file-list | -| TodoWrite | collapsible | todo-list | none | success msg | -| TodoRead | one-line | Read todo | none | todo-list | -| exit_plan_mode | collapsible | markdown | none | markdown | -| Default | collapsible | text/code | none | text | +## Performance +- **ToolRenderer** is wrapped with `React.memo` — skips re-render when props haven't changed +- **parsedData** is memoized with `useMemo` — JSON parsing only runs when input changes +- **DiffViewer** memoizes `createDiff()` — expensive diff computation cached +- **MessageComponent** caches `localStorage` reads and timestamp formatting via `useMemo` +- Tool results route through `ToolRenderer` (no duplicate rendering paths) +- `CollapsibleDisplay` uses children pattern (no wasteful contentProps indirection) +- Configs are static module-level objects — zero runtime overhead for lookups diff --git a/src/components/chat/tools/components/FilePathButton.tsx b/src/components/chat/tools/components/FilePathButton.tsx deleted file mode 100644 index aa70e08..0000000 --- a/src/components/chat/tools/components/FilePathButton.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import React from 'react'; - -interface FilePathButtonProps { - filePath: string; - onClick: () => void; - variant?: 'button' | 'link'; - showFullPath?: boolean; - className?: string; -} - -/** - * Clickable file path — inline link style - */ -export const FilePathButton: React.FC = ({ - filePath, - onClick, - variant = 'button', - showFullPath = false, - className = '' -}) => { - const filename = filePath.split('/').pop() || filePath; - const displayText = showFullPath ? filePath : filename; - - if (variant === 'link') { - return ( - - ); - } - - return ( - - ); -}; diff --git a/src/components/chat/tools/components/index.ts b/src/components/chat/tools/components/index.ts index fa6a07a..fce0c5e 100644 --- a/src/components/chat/tools/components/index.ts +++ b/src/components/chat/tools/components/index.ts @@ -1,5 +1,4 @@ export { CollapsibleSection } from './CollapsibleSection'; -export { FilePathButton } from './FilePathButton'; export { DiffViewer } from './DiffViewer'; export { OneLineDisplay } from './OneLineDisplay'; export { CollapsibleDisplay } from './CollapsibleDisplay'; diff --git a/src/components/chat/tools/configs/index.ts b/src/components/chat/tools/configs/index.ts deleted file mode 100644 index 021d41d..0000000 --- a/src/components/chat/tools/configs/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './types'; -export * from './toolConfigs'; diff --git a/src/components/chat/tools/configs/types.ts b/src/components/chat/tools/configs/types.ts deleted file mode 100644 index a8edd8e..0000000 --- a/src/components/chat/tools/configs/types.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { ToolResult } from '../../types'; - -export type ToolCategory = 'command' | 'file-operation' | 'search' | 'todo' | 'plan' | 'default'; - -export interface ToolConfig { - /** Tool display name */ - displayName: string; - - /** Tool category for grouping and styling */ - category: ToolCategory; - - /** Icon identifier (can be emoji or icon name) */ - icon?: string; - - /** Whether to show minimized display by default */ - minimized?: boolean; - - /** Name of the renderer component to use */ - renderer: string; - - /** Whether to hide successful results (show only errors) */ - hideSuccessfulResult?: boolean; - - /** Whether this tool requires file system access */ - requiresFileAccess?: boolean; - - /** Whether this tool supports copy to clipboard */ - supportsCopy?: boolean; - - /** Custom function to determine if result should be hidden */ - shouldHideResult?: (result: ToolResult | null) => boolean; - - /** Color scheme for the tool display */ - colorScheme?: { - primary: string; - secondary: string; - }; -} - -export type ToolConfigRegistry = Record;