diff --git a/src/components/TodoList.jsx b/src/components/TodoList.jsx
index e9028f0..8876c15 100644
--- a/src/components/TodoList.jsx
+++ b/src/components/TodoList.jsx
@@ -10,12 +10,12 @@ const TodoList = ({ todos, isResult = false }) => {
const getStatusIcon = (status) => {
switch (status) {
case 'completed':
- return
+
{todo.content}
- +
+
{rawContent}
+ {content}
+
+ );
+ }
+
+ const completed = tasks.filter(t => t.status === 'completed').length;
+ const total = tasks.length;
+
+ return (
+ +{formattedJson}); @@ -33,7 +33,7 @@ export const TextContent: React.FC= ({ if (format === 'code') { return ( - +{content}); @@ -41,7 +41,7 @@ export const TextContent: React.FC= ({ // Plain text return ( - +{content}); diff --git a/src/components/chat/tools/components/ContentRenderers/index.ts b/src/components/chat/tools/components/ContentRenderers/index.ts index a74d879..86d6be3 100644 --- a/src/components/chat/tools/components/ContentRenderers/index.ts +++ b/src/components/chat/tools/components/ContentRenderers/index.ts @@ -1,4 +1,5 @@ export { MarkdownContent } from './MarkdownContent'; export { FileListContent } from './FileListContent'; export { TodoListContent } from './TodoListContent'; +export { TaskListContent } from './TaskListContent'; export { TextContent } from './TextContent'; diff --git a/src/components/chat/tools/components/DiffViewer.tsx b/src/components/chat/tools/components/DiffViewer.tsx index b55780e..6231603 100644 --- a/src/components/chat/tools/components/DiffViewer.tsx +++ b/src/components/chat/tools/components/DiffViewer.tsx @@ -17,8 +17,7 @@ interface DiffViewerProps { } /** - * Reusable diff viewer component with consistent styling - * Replaces duplicated diff display logic in Edit, Write, and result sections + * Compact diff viewer — VS Code-style */ export const DiffViewer: React.FC= ({ oldContent, @@ -30,48 +29,48 @@ export const DiffViewer: React.FC = ({ badgeColor = 'gray' }) => { const badgeClasses = badgeColor === 'green' - ? 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400' - : 'bg-gray-100 dark:bg-gray-700/50 text-gray-500 dark:text-gray-400'; + ? 'bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400' + : 'bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400'; return ( - +{/* Header */} -+{onFileClick ? ( ) : ( - + {filePath} )} - + {badge}- {/* Diff content */} -+ {/* Diff lines */} +{createDiff(oldContent, newContent).map((diffLine, i) => ({diffLine.type === 'removed' ? '-' : '+'} {diffLine.content} diff --git a/src/components/chat/tools/components/FilePathButton.tsx b/src/components/chat/tools/components/FilePathButton.tsx index c52d0a3..aa70e08 100644 --- a/src/components/chat/tools/components/FilePathButton.tsx +++ b/src/components/chat/tools/components/FilePathButton.tsx @@ -9,8 +9,7 @@ interface FilePathButtonProps { } /** - * Reusable clickable file path component with consistent styling - * Used across Edit, Write, and Read tool displays + * Clickable file path — inline link style */ export const FilePathButton: React.FC= ({ filePath, @@ -26,7 +25,7 @@ export const FilePathButton: React.FC = ({ return ( @@ -36,7 +35,8 @@ export const FilePathButton: React.FC = ({ return ( diff --git a/src/components/chat/tools/components/OneLineDisplay.tsx b/src/components/chat/tools/components/OneLineDisplay.tsx index 25ed34f..a0707ac 100644 --- a/src/components/chat/tools/components/OneLineDisplay.tsx +++ b/src/components/chat/tools/components/OneLineDisplay.tsx @@ -3,6 +3,7 @@ import React, { useState } from 'react'; type ActionType = 'copy' | 'open-file' | 'jump-to-results' | 'none'; interface OneLineDisplayProps { + toolName: string; icon?: string; label?: string; @@ -41,15 +42,16 @@ export const OneLineDisplay: React.FC = ({ colorScheme = { primary: 'text-gray-700 dark:text-gray-300', secondary: 'text-gray-500 dark:text-gray-400', - background: 'bg-gray-50/50 dark:bg-gray-800/30', - border: 'border-blue-400 dark:border-blue-500', - icon: 'text-blue-500 dark:text-blue-400' + background: '', + border: 'border-gray-300 dark:border-gray-600', + icon: 'text-gray-500 dark:text-gray-400' }, resultId, toolResult, toolId }) => { const [copied, setCopied] = useState(false); + const isTerminal = style === 'terminal'; const handleAction = () => { if (action === 'copy' && value) { @@ -61,109 +63,118 @@ export const OneLineDisplay: React.FC = ({ } }; - const renderActionButton = () => { - if (action === 'none') return null; + const renderCopyButton = () => ( + + ); - if (action === 'copy') { - return ( - - ); - } - - if (action === 'open-file') { - return ( - - ); - } - - if (action === 'jump-to-results' && resultId) { - return ( - - Search results - - + // Terminal style: dark pill only around the command + if (isTerminal) { + return ( + +++ - - ); - } - - return null; - }; - const isTerminal = style === 'terminal'; - - return ( - --+ ); + } - {action === 'jump-to-results' && toolResult && ( + // File open style - show filename only, full path on hover + if (action === 'open-file') { + const displayName = value.split('/').pop() || value; + return ( +- {icon === 'terminal' ? ( -+- - ) : icon ? ( - - {icon} - - ) : label ? ( - {label} - ) : ( - {toolName} - )} - - • - - {action === 'open-file' ? ( - renderActionButton() - ) : ( - - {isTerminal && $} - {value} - - )} - - {secondary && ( - - ({secondary}) - - )} - - {action === 'copy' && renderActionButton()}- ++ {secondary && ( + + {secondary} + + )} +++ {action === 'copy' && renderCopyButton()} ++ $ {value} +++ {label || toolName} + / + ++ ); + } + + // Search / jump-to-results style + if (action === 'jump-to-results') { + return ( ++ {label || toolName} + / + + {value} + + {secondary && ( + + {secondary} + + )} + {toolResult && ( - Search results+ ); + } + + // Default one-line style + return ( +)} + {icon && icon !== 'terminal' && ( + {icon} + )} + {!icon && (label || toolName) && ( + {label || toolName} + )} + {(icon || label || toolName) && ( + / + )} + + {value} + + {secondary && ( + + {secondary} + + )} + {action === 'copy' && renderCopyButton()}); }; diff --git a/src/components/chat/tools/configs/toolConfigs.ts b/src/components/chat/tools/configs/toolConfigs.ts index 3231ed1..799e027 100644 --- a/src/components/chat/tools/configs/toolConfigs.ts +++ b/src/components/chat/tools/configs/toolConfigs.ts @@ -12,14 +12,19 @@ export interface ToolDisplayConfig { getValue?: (input: any) => string; getSecondary?: (input: any) => string | undefined; action?: 'copy' | 'open-file' | 'jump-to-results' | 'none'; + style?: string; + wrapText?: boolean; colorScheme?: { primary?: string; secondary?: string; + background?: string; + border?: string; + icon?: string; }; // Collapsible config title?: string | ((input: any) => string); defaultOpen?: boolean; - contentType?: 'diff' | 'markdown' | 'file-list' | 'todo-list' | 'text'; + contentType?: 'diff' | 'markdown' | 'file-list' | 'todo-list' | 'text' | 'task'; getContentProps?: (input: any, helpers?: any) => any; actionButton?: 'file-button' | 'none'; }; @@ -27,8 +32,10 @@ export interface ToolDisplayConfig { hidden?: boolean; hideOnSuccess?: boolean; type?: 'one-line' | 'collapsible' | 'special'; + title?: string | ((result: any) => string); + defaultOpen?: boolean; // Special result handlers - contentType?: 'markdown' | 'file-list' | 'todo-list' | 'text' | 'success-message'; + contentType?: 'markdown' | 'file-list' | 'todo-list' | 'text' | 'success-message' | 'task'; getMessage?: (result: any) => string; getContentProps?: (result: any) => any; }; @@ -51,14 +58,14 @@ export const TOOL_CONFIGS: Record= { colorScheme: { primary: 'text-green-400 font-mono', secondary: 'text-gray-400', - background: 'bg-gray-900 dark:bg-black', + background: '', border: 'border-green-500 dark:border-green-400', icon: 'text-green-500 dark:text-green-400' } }, result: { hideOnSuccess: true, - type: 'special' // Interactive prompts, cat -n output, etc. + type: 'special' } }, @@ -70,29 +77,35 @@ export const TOOL_CONFIGS: Record = { input: { type: 'one-line', label: 'Read', - getValue: (input) => input.file_path, + getValue: (input) => input.file_path || '', action: 'open-file', colorScheme: { - primary: 'text-gray-700 dark:text-gray-300' + primary: 'text-gray-700 dark:text-gray-300', + background: '', + border: 'border-gray-300 dark:border-gray-600', + icon: 'text-gray-500 dark:text-gray-400' } }, result: { - hidden: true // Read results not displayed + hidden: true } }, Edit: { input: { type: 'collapsible', - title: 'View edit diff for', + title: (input) => { + const filename = input.file_path?.split('/').pop() || input.file_path || 'file'; + return `${filename}`; + }, defaultOpen: false, contentType: 'diff', - actionButton: 'file-button', + actionButton: 'none', getContentProps: (input) => ({ oldContent: input.old_string, newContent: input.new_string, filePath: input.file_path, - badge: 'Diff', + badge: 'Edit', badgeColor: 'gray' }) }, @@ -104,15 +117,18 @@ export const TOOL_CONFIGS: Record = { Write: { input: { type: 'collapsible', - title: 'Creating new file', + title: (input) => { + const filename = input.file_path?.split('/').pop() || input.file_path || 'file'; + return `${filename}`; + }, defaultOpen: false, contentType: 'diff', - actionButton: 'file-button', + actionButton: 'none', getContentProps: (input) => ({ oldContent: '', newContent: input.content, filePath: input.file_path, - badge: 'New File', + badge: 'New', badgeColor: 'green' }) }, @@ -124,10 +140,13 @@ export const TOOL_CONFIGS: Record = { ApplyPatch: { input: { type: 'collapsible', - title: 'View patch diff for', + title: (input) => { + const filename = input.file_path?.split('/').pop() || input.file_path || 'file'; + return `${filename}`; + }, defaultOpen: false, contentType: 'diff', - actionButton: 'file-button', + actionButton: 'none', getContentProps: (input) => ({ oldContent: input.old_string, newContent: input.new_string, @@ -154,19 +173,25 @@ export const TOOL_CONFIGS: Record = { action: 'jump-to-results', colorScheme: { primary: 'text-gray-700 dark:text-gray-300', - secondary: 'text-gray-500 dark:text-gray-400' + secondary: 'text-gray-500 dark:text-gray-400', + background: '', + border: 'border-gray-400 dark:border-gray-500', + icon: 'text-gray-500 dark:text-gray-400' } }, result: { type: 'collapsible', + defaultOpen: false, + title: (result) => { + const toolData = result.toolUseResult || {}; + const count = toolData.numFiles || toolData.filenames?.length || 0; + return `Found ${count} ${count === 1 ? 'file' : 'files'}`; + }, contentType: 'file-list', getContentProps: (result) => { const toolData = result.toolUseResult || {}; return { - files: toolData.filenames || [], - title: toolData.filenames ? - `Found ${toolData.numFiles || toolData.filenames.length} ${(toolData.numFiles === 1 || toolData.filenames.length === 1) ? 'file' : 'files'}` - : undefined + files: toolData.filenames || [] }; } } @@ -181,19 +206,25 @@ export const TOOL_CONFIGS: Record = { action: 'jump-to-results', colorScheme: { primary: 'text-gray-700 dark:text-gray-300', - secondary: 'text-gray-500 dark:text-gray-400' + secondary: 'text-gray-500 dark:text-gray-400', + background: '', + border: 'border-gray-400 dark:border-gray-500', + icon: 'text-gray-500 dark:text-gray-400' } }, result: { type: 'collapsible', + defaultOpen: false, + title: (result) => { + const toolData = result.toolUseResult || {}; + const count = toolData.numFiles || toolData.filenames?.length || 0; + return `Found ${count} ${count === 1 ? 'file' : 'files'}`; + }, contentType: 'file-list', getContentProps: (result) => { const toolData = result.toolUseResult || {}; return { - files: toolData.filenames || [], - title: toolData.filenames ? - `Found ${toolData.numFiles || toolData.filenames.length} ${(toolData.numFiles === 1 || toolData.filenames.length === 1) ? 'file' : 'files'}` - : undefined + files: toolData.filenames || [] }; } } @@ -206,7 +237,7 @@ export const TOOL_CONFIGS: Record = { TodoWrite: { input: { type: 'collapsible', - title: 'Updating Todo List', + title: 'Updating todo list', defaultOpen: false, contentType: 'todo-list', getContentProps: (input) => ({ @@ -216,16 +247,20 @@ export const TOOL_CONFIGS: Record = { result: { type: 'collapsible', contentType: 'success-message', - getMessage: () => 'Todo list has been updated successfully' + getMessage: () => 'Todo list updated' } }, TodoRead: { input: { type: 'one-line', - label: 'Read todo list', - getValue: () => '', - action: 'none' + label: 'TodoRead', + getValue: () => 'reading list', + action: 'none', + colorScheme: { + primary: 'text-gray-500 dark:text-gray-400', + border: 'border-violet-400 dark:border-violet-500' + } }, result: { type: 'collapsible', @@ -245,6 +280,97 @@ export const TOOL_CONFIGS: Record = { } }, + // ============================================================================ + // TASK TOOLS (TaskCreate, TaskUpdate, TaskList, TaskGet) + // ============================================================================ + + TaskCreate: { + input: { + type: 'one-line', + label: 'Task', + getValue: (input) => input.subject || 'Creating task', + getSecondary: (input) => input.status || undefined, + action: 'none', + colorScheme: { + primary: 'text-gray-700 dark:text-gray-300', + border: 'border-violet-400 dark:border-violet-500', + icon: 'text-violet-500 dark:text-violet-400' + } + }, + result: { + hideOnSuccess: true + } + }, + + TaskUpdate: { + input: { + type: 'one-line', + label: 'Task', + getValue: (input) => { + const parts = []; + if (input.taskId) parts.push(`#${input.taskId}`); + if (input.status) parts.push(input.status); + if (input.subject) parts.push(`"${input.subject}"`); + return parts.join(' → ') || 'updating'; + }, + action: 'none', + colorScheme: { + primary: 'text-gray-700 dark:text-gray-300', + border: 'border-violet-400 dark:border-violet-500', + icon: 'text-violet-500 dark:text-violet-400' + } + }, + result: { + hideOnSuccess: true + } + }, + + TaskList: { + input: { + type: 'one-line', + label: 'Tasks', + getValue: () => 'listing tasks', + action: 'none', + colorScheme: { + primary: 'text-gray-500 dark:text-gray-400', + border: 'border-violet-400 dark:border-violet-500', + icon: 'text-violet-500 dark:text-violet-400' + } + }, + result: { + type: 'collapsible', + defaultOpen: true, + title: 'Task list', + contentType: 'task', + getContentProps: (result) => ({ + content: String(result.content || '') + }) + } + }, + + TaskGet: { + input: { + type: 'one-line', + label: 'Task', + getValue: (input) => input.taskId ? `#${input.taskId}` : 'fetching', + action: 'none', + colorScheme: { + primary: 'text-gray-700 dark:text-gray-300', + border: 'border-violet-400 dark:border-violet-500', + icon: 'text-violet-500 dark:text-violet-400' + } + }, + result: { + type: 'collapsible', + defaultOpen: true, + title: 'Task details', + contentType: 'task', + getContentProps: (result) => ({ + content: String(result.content || '') + }) + } + }, + // ============================================================================ // PLAN TOOLS // ============================================================================ @@ -252,7 +378,37 @@ export const TOOL_CONFIGS: Record = { exit_plan_mode: { input: { type: 'collapsible', - title: 'View implementation plan', + title: 'Implementation plan', + defaultOpen: true, + contentType: 'markdown', + getContentProps: (input) => ({ + content: input.plan?.replace(/\\n/g, '\n') || input.plan + }) + }, + result: { + type: 'collapsible', + contentType: 'markdown', + getContentProps: (result) => { + try { + let parsed = result.content; + if (typeof parsed === 'string') { + parsed = JSON.parse(parsed); + } + return { + content: parsed.plan?.replace(/\\n/g, '\n') || parsed.plan + }; + } catch (e) { + return { content: '' }; + } + } + } + }, + + // Also register as ExitPlanMode (the actual tool name used by Claude) + ExitPlanMode: { + input: { + type: 'collapsible', + title: 'Implementation plan', defaultOpen: true, contentType: 'markdown', getContentProps: (input) => ({ @@ -285,7 +441,7 @@ export const TOOL_CONFIGS: Record = { Default: { input: { type: 'collapsible', - title: 'View input parameters', + title: 'Parameters', defaultOpen: false, contentType: 'text', getContentProps: (input) => ({