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 ; + return ; case 'in_progress': - return ; + return ; case 'pending': default: - return ; + return ; } }; @@ -44,38 +44,38 @@ const TodoList = ({ todos, isResult = false }) => { }; return ( -
+
{isResult && ( -
+
Todo List ({todos.length} {todos.length === 1 ? 'item' : 'items'})
)} - + {todos.map((todo, index) => (
{getStatusIcon(todo.status)}
- +
-
-

+

+

{todo.content}

- +
{todo.priority} {todo.status.replace('_', ' ')} @@ -88,4 +88,4 @@ const TodoList = ({ todos, isResult = false }) => { ); }; -export default TodoList; \ No newline at end of file +export default TodoList; diff --git a/src/components/chat/messages/MessageComponent.tsx b/src/components/chat/messages/MessageComponent.tsx index 294fdff..ccc432a 100644 --- a/src/components/chat/messages/MessageComponent.tsx +++ b/src/components/chat/messages/MessageComponent.tsx @@ -11,7 +11,7 @@ import { Markdown } from '../markdown/Markdown'; import { formatUsageLimitText } from '../utils/chatFormatting'; import { getClaudePermissionSuggestion } from '../utils/chatPermissions'; import type { Project } from '../../../types/app'; -import { ToolRenderer, shouldHideToolResult } from '../tools'; +import { ToolRenderer, shouldHideToolResult, FileListContent, TaskListContent } from '../tools'; type DiffLine = { type: string; @@ -181,38 +181,29 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile return (
- {/* Decorative gradient overlay */} -
- -
-
+ - - {message.toolResult.isError ? ( - - ) : ( - - )} - -
- + {message.toolResult.isError ? ( + + ) : ( + + )} + + - {message.toolResult.isError ? 'Tool Error' : 'Tool Result'} + {message.toolResult.isError ? 'Error' : 'Result'}
@@ -291,57 +282,26 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile } } - // Special handling for Grep/Glob results with structured data + // Grep/Glob results - compact comma-separated file list if ((message.toolName === 'Grep' || message.toolName === 'Glob') && message.toolResult?.toolUseResult) { const toolData = message.toolResult.toolUseResult; - - // Handle files_with_matches mode or any tool result with filenames array if (toolData.filenames && Array.isArray(toolData.filenames) && toolData.filenames.length > 0) { + const count = toolData.numFiles || toolData.filenames.length; return ( -
-
- - Found {toolData.numFiles || toolData.filenames.length} {(toolData.numFiles === 1 || toolData.filenames.length === 1) ? 'file' : 'files'} - -
-
- {toolData.filenames.map((filePath, index) => { - const fileName = filePath.split('/').pop(); - const dirPath = filePath.substring(0, filePath.lastIndexOf('/')); - - return ( -
{ - if (onFileOpen) { - onFileOpen(filePath); - } - }} - className="group flex items-center gap-2 px-2 py-1.5 rounded hover:bg-green-100/50 dark:hover:bg-green-800/20 cursor-pointer transition-colors" - > - - - -
-
- {fileName} -
-
- {dirPath} -
-
- - - -
- ); - })} -
-
+ ); } } + // Task tool results - proper task list rendering + if (message.toolName === 'TaskList' || message.toolName === 'TaskGet') { + return ; + } + // Special handling for interactive prompts if (content.includes('Do you want to proceed?') && message.toolName === 'Bash') { const lines = content.split('\n'); @@ -797,9 +757,11 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
)} -
- {new Date(message.timestamp).toLocaleTimeString()} -
+ {!isGrouped && ( +
+ {new Date(message.timestamp).toLocaleTimeString()} +
+ )}
)} diff --git a/src/components/chat/tools/ToolRenderer.tsx b/src/components/chat/tools/ToolRenderer.tsx index 3aa5a5f..10ea393 100644 --- a/src/components/chat/tools/ToolRenderer.tsx +++ b/src/components/chat/tools/ToolRenderer.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { getToolConfig } from './configs/toolConfigs'; -import { OneLineDisplay, CollapsibleDisplay, FilePathButton, DiffViewer, MarkdownContent, FileListContent, TodoListContent, TextContent } from './components'; +import { OneLineDisplay, CollapsibleDisplay, FilePathButton, DiffViewer, MarkdownContent, FileListContent, TodoListContent, TaskListContent, TextContent } from './components'; import type { Project } from '../../../types/app'; type DiffLine = { @@ -24,6 +24,16 @@ interface ToolRendererProps { rawToolInput?: string; } +function getToolCategory(toolName: string): string { + if (['Edit', 'Write', 'ApplyPatch'].includes(toolName)) return 'edit'; + if (['Grep', 'Glob'].includes(toolName)) return 'search'; + if (toolName === 'Bash') return 'bash'; + if (['TodoWrite', 'TodoRead'].includes(toolName)) return 'todo'; + if (['TaskCreate', 'TaskUpdate', 'TaskList', 'TaskGet'].includes(toolName)) return 'task'; + if (toolName === 'exit_plan_mode' || toolName === 'ExitPlanMode') return 'plan'; + return 'default'; +} + /** * Main tool renderer router * Routes to OneLineDisplay or CollapsibleDisplay based on tool config @@ -43,7 +53,7 @@ export const ToolRenderer: React.FC = ({ rawToolInput }) => { const config = getToolConfig(toolName); - const displayConfig = mode === 'input' ? config.input : config.result; + const displayConfig: any = mode === 'input' ? config.input : config.result; if (!displayConfig) return null; @@ -87,7 +97,7 @@ export const ToolRenderer: React.FC = ({ if (displayConfig.type === 'collapsible') { const title = typeof displayConfig.title === 'function' ? displayConfig.title(parsedData) - : displayConfig.title || 'View details'; + : displayConfig.title || 'Details'; const defaultOpen = displayConfig.defaultOpen !== undefined ? displayConfig.defaultOpen @@ -143,6 +153,14 @@ export const ToolRenderer: React.FC = ({ ); break; + case 'task': + contentComponent = ( + + ); + break; + case 'text': contentComponent = ( = ({ case 'success-message': const message = displayConfig.getMessage?.(parsedData) || 'Success'; contentComponent = ( -
- {message} +
+ + + + {message}
); break; default: contentComponent = ( -
Unknown content type: {displayConfig.contentType}
+
Unknown content type: {displayConfig.contentType}
); } @@ -223,11 +244,16 @@ export const ToolRenderer: React.FC = ({ actionButton = ( handleFileClick(e)} /> ); } + // For edit tools, make the title (filename) clickable to open the file + const handleTitleClick = (toolName === 'Edit' || toolName === 'Write' || toolName === 'ApplyPatch') && contentProps.filePath && onFileOpen + ? () => onFileOpen(contentProps.filePath) + : undefined; + return ( = ({ title={title} defaultOpen={defaultOpen} action={actionButton} + onTitleClick={handleTitleClick} contentType={displayConfig.contentType || 'text'} contentProps={{ DiffViewer: contentComponent, MarkdownComponent: contentComponent, FileListComponent: contentComponent, TodoListComponent: contentComponent, + TaskComponent: contentComponent, TextComponent: contentComponent }} showRawParameters={mode === 'input' && showRawParameters} rawContent={rawToolInput} onShowSettings={onShowSettings} + toolCategory={getToolCategory(toolName)} /> ); } diff --git a/src/components/chat/tools/components/CollapsibleDisplay.tsx b/src/components/chat/tools/components/CollapsibleDisplay.tsx index a92dffe..93930bf 100644 --- a/src/components/chat/tools/components/CollapsibleDisplay.tsx +++ b/src/components/chat/tools/components/CollapsibleDisplay.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { CollapsibleSection } from './CollapsibleSection'; -type ContentType = 'diff' | 'markdown' | 'file-list' | 'todo-list' | 'text'; +type ContentType = 'diff' | 'markdown' | 'file-list' | 'todo-list' | 'text' | 'task' | 'success-message'; interface CollapsibleDisplayProps { toolName: string; @@ -9,111 +9,85 @@ interface CollapsibleDisplayProps { title: string; defaultOpen?: boolean; action?: React.ReactNode; + onTitleClick?: () => void; contentType: ContentType; contentProps: any; showRawParameters?: boolean; rawContent?: string; className?: string; onShowSettings?: () => void; + toolCategory?: string; } +const borderColorMap: Record = { + edit: 'border-l-amber-500 dark:border-l-amber-400', + search: 'border-l-gray-400 dark:border-l-gray-500', + bash: 'border-l-green-500 dark:border-l-green-400', + todo: 'border-l-violet-500 dark:border-l-violet-400', + task: 'border-l-violet-500 dark:border-l-violet-400', + plan: 'border-l-indigo-500 dark:border-l-indigo-400', + default: 'border-l-gray-300 dark:border-l-gray-600', +}; + export const CollapsibleDisplay: React.FC = ({ toolName, - toolId, title, defaultOpen = false, action, + onTitleClick, contentType, contentProps, showRawParameters = false, rawContent, className = '', - onShowSettings + toolCategory }) => { const renderContent = () => { switch (contentType) { case 'diff': return contentProps.DiffViewer; - case 'markdown': return contentProps.MarkdownComponent; - case 'file-list': return contentProps.FileListComponent; - case 'todo-list': return contentProps.TodoListComponent; - + case 'task': + return contentProps.TaskComponent; case 'text': return contentProps.TextComponent; - default: - return
Unknown content type: {contentType}
; + return
Unknown content type: {contentType}
; } }; + + const borderColor = borderColorMap[toolCategory || 'default']; + return ( -
-
- -
-
-
- - - - -
-
-
- - {toolName} - - {toolId && ( - - {toolId} - - )} -
-
- {onShowSettings && ( - - )} -
- +
{renderContent()} {showRawParameters && rawContent && ( -
- +
+ - + - View raw parameters + raw params -
+            
               {rawContent}
             
diff --git a/src/components/chat/tools/components/CollapsibleSection.tsx b/src/components/chat/tools/components/CollapsibleSection.tsx index c5d2bfd..f83135c 100644 --- a/src/components/chat/tools/components/CollapsibleSection.tsx +++ b/src/components/chat/tools/components/CollapsibleSection.tsx @@ -2,40 +2,58 @@ import React from 'react'; interface CollapsibleSectionProps { title: string; + toolName?: string; open?: boolean; action?: React.ReactNode; + onTitleClick?: () => void; children: React.ReactNode; className?: string; } /** * Reusable collapsible section with consistent styling - * Replaces repeated details/summary patterns throughout MessageComponent */ export const CollapsibleSection: React.FC = ({ title, + toolName, open = false, action, + onTitleClick, children, className = '' }) => { return ( -
- +
+ - + - - {title} - - {action} + {toolName && ( + {toolName} + )} + {toolName && ( + / + )} + {onTitleClick ? ( + + ) : ( + + {title} + + )} + {action && {action}} -
+
{children}
diff --git a/src/components/chat/tools/components/ContentRenderers/FileListContent.tsx b/src/components/chat/tools/components/ContentRenderers/FileListContent.tsx index 4135554..695b4f2 100644 --- a/src/components/chat/tools/components/ContentRenderers/FileListContent.tsx +++ b/src/components/chat/tools/components/ContentRenderers/FileListContent.tsx @@ -12,7 +12,7 @@ interface FileListContentProps { } /** - * Renders a list of files with click handlers + * Renders a compact comma-separated list of clickable file names * Used by: Grep/Glob results */ export const FileListContent: React.FC = ({ @@ -20,54 +20,34 @@ export const FileListContent: React.FC = ({ onFileClick, title }) => { - const fileCount = files.length; - return (
{title && ( -
- - {title || `Found ${fileCount} ${fileCount === 1 ? 'file' : 'files'}`} - +
+ {title}
)} -
+
{files.map((file, index) => { const filePath = typeof file === 'string' ? file : file.path; const fileName = filePath.split('/').pop() || filePath; - const dirPath = filePath.substring(0, filePath.lastIndexOf('/')); const handleClick = typeof file === 'string' ? () => onFileClick?.(file) : file.onClick; return ( -
- {/* File icon */} - - - - - {/* File path */} -
-
- {fileName} -
- {dirPath && ( -
- {dirPath} -
- )} -
- - {/* Chevron on hover */} - - - -
+ + + {index < files.length - 1 && ( + , + )} + ); })}
diff --git a/src/components/chat/tools/components/ContentRenderers/MarkdownContent.tsx b/src/components/chat/tools/components/ContentRenderers/MarkdownContent.tsx index 2d6995c..de94af7 100644 --- a/src/components/chat/tools/components/ContentRenderers/MarkdownContent.tsx +++ b/src/components/chat/tools/components/ContentRenderers/MarkdownContent.tsx @@ -12,7 +12,7 @@ interface MarkdownContentProps { */ export const MarkdownContent: React.FC = ({ content, - className = 'mt-3 prose prose-sm max-w-none dark:prose-invert' + className = 'mt-1 prose prose-sm max-w-none dark:prose-invert' }) => { return ( diff --git a/src/components/chat/tools/components/ContentRenderers/TaskListContent.tsx b/src/components/chat/tools/components/ContentRenderers/TaskListContent.tsx new file mode 100644 index 0000000..5ae3f71 --- /dev/null +++ b/src/components/chat/tools/components/ContentRenderers/TaskListContent.tsx @@ -0,0 +1,125 @@ +import React from 'react'; + +interface TaskItem { + id: string; + subject: string; + status: 'pending' | 'in_progress' | 'completed'; + owner?: string; + blockedBy?: string[]; +} + +interface TaskListContentProps { + content: string; +} + +function parseTaskContent(content: string): TaskItem[] { + const tasks: TaskItem[] = []; + const lines = content.split('\n'); + + for (const line of lines) { + // Match patterns like: #15. [in_progress] Subject here + // or: - #15 [in_progress] Subject (owner: agent) + // or: #15. Subject here (status: in_progress) + const match = line.match(/#(\d+)\.?\s*(?:\[(\w+)\]\s*)?(.+?)(?:\s*\((?:owner:\s*\w+)?\))?$/); + if (match) { + const [, id, status, subject] = match; + const blockedMatch = line.match(/blockedBy:\s*\[([^\]]*)\]/); + tasks.push({ + id, + subject: subject.trim(), + status: (status as TaskItem['status']) || 'pending', + blockedBy: blockedMatch ? blockedMatch[1].split(',').map(s => s.trim()).filter(Boolean) : undefined + }); + } + } + + return tasks; +} + +const statusConfig = { + completed: { + icon: ( + + + + ), + textClass: 'line-through text-gray-400 dark:text-gray-500', + badgeClass: 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300 border-green-200 dark:border-green-800' + }, + in_progress: { + icon: ( + + + + ), + textClass: 'text-gray-900 dark:text-gray-100', + badgeClass: 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 border-blue-200 dark:border-blue-800' + }, + pending: { + icon: ( + + + + ), + textClass: 'text-gray-700 dark:text-gray-300', + badgeClass: 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 border-gray-200 dark:border-gray-700' + } +}; + +/** + * Renders task list results with proper status icons and compact layout + * Parses text content from TaskList/TaskGet results + */ +export const TaskListContent: React.FC = ({ content }) => { + const tasks = parseTaskContent(content); + + // If we couldn't parse any tasks, fall back to text display + if (tasks.length === 0) { + return ( +
+        {content}
+      
+ ); + } + + const completed = tasks.filter(t => t.status === 'completed').length; + const total = tasks.length; + + return ( +
+
+ + {completed}/{total} completed + +
+
0 ? (completed / total) * 100 : 0}%` }} + /> +
+
+
+ {tasks.map((task) => { + const config = statusConfig[task.status] || statusConfig.pending; + return ( +
+ {config.icon} + + #{task.id} + + + {task.subject} + + + {task.status.replace('_', ' ')} + +
+ ); + })} +
+
+ ); +}; diff --git a/src/components/chat/tools/components/ContentRenderers/TextContent.tsx b/src/components/chat/tools/components/ContentRenderers/TextContent.tsx index 24aa543..811165a 100644 --- a/src/components/chat/tools/components/ContentRenderers/TextContent.tsx +++ b/src/components/chat/tools/components/ContentRenderers/TextContent.tsx @@ -25,7 +25,7 @@ export const TextContent: React.FC = ({ } 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 ( -
-
-
- {icon === 'terminal' ? ( - - - - ) : icon ? ( - - {icon} - - ) : label ? ( - {label} - ) : ( - {toolName} - )} - - - - {action === 'open-file' ? ( - renderActionButton() - ) : ( - - {isTerminal && $} - {value} - - )} - - {secondary && ( - - ({secondary}) - - )} - - {action === 'copy' && renderActionButton()}
+
+
+ + $ {value} + +
+ {action === 'copy' && renderCopyButton()} +
+ {secondary && ( + + {secondary} + + )} +
+ ); + } - {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 ( +
+ {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) => ({