mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-02-14 12:47:33 +00:00
refactor(design): change the design of tools and introduce todo list and task list.
This commit is contained in:
@@ -10,12 +10,12 @@ const TodoList = ({ todos, isResult = false }) => {
|
||||
const getStatusIcon = (status) => {
|
||||
switch (status) {
|
||||
case 'completed':
|
||||
return <CheckCircle2 className="w-4 h-4 text-green-500 dark:text-green-400" />;
|
||||
return <CheckCircle2 className="w-3.5 h-3.5 text-green-500 dark:text-green-400" />;
|
||||
case 'in_progress':
|
||||
return <Clock className="w-4 h-4 text-blue-500 dark:text-blue-400" />;
|
||||
return <Clock className="w-3.5 h-3.5 text-blue-500 dark:text-blue-400" />;
|
||||
case 'pending':
|
||||
default:
|
||||
return <Circle className="w-4 h-4 text-gray-400 dark:text-gray-500" />;
|
||||
return <Circle className="w-3.5 h-3.5 text-gray-400 dark:text-gray-500" />;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -44,38 +44,38 @@ const TodoList = ({ todos, isResult = false }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-1.5">
|
||||
{isResult && (
|
||||
<div className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
|
||||
<div className="text-xs font-medium text-gray-600 dark:text-gray-400 mb-1.5">
|
||||
Todo List ({todos.length} {todos.length === 1 ? 'item' : 'items'})
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{todos.map((todo, index) => (
|
||||
<div
|
||||
key={todo.id || `todo-${index}`}
|
||||
className="flex items-start gap-3 p-3 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm hover:shadow-md dark:shadow-gray-900/50 transition-shadow"
|
||||
className="flex items-start gap-2 p-2 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded transition-colors"
|
||||
>
|
||||
<div className="flex-shrink-0 mt-0.5">
|
||||
{getStatusIcon(todo.status)}
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-start justify-between gap-2 mb-2">
|
||||
<p className={`text-sm font-medium ${todo.status === 'completed' ? 'line-through text-gray-500 dark:text-gray-400' : 'text-gray-900 dark:text-gray-100'}`}>
|
||||
<div className="flex items-start justify-between gap-2 mb-0.5">
|
||||
<p className={`text-xs font-medium ${todo.status === 'completed' ? 'line-through text-gray-500 dark:text-gray-400' : 'text-gray-900 dark:text-gray-100'}`}>
|
||||
{todo.content}
|
||||
</p>
|
||||
|
||||
|
||||
<div className="flex gap-1 flex-shrink-0">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={`text-xs px-2 py-0.5 ${getPriorityColor(todo.priority)}`}
|
||||
className={`text-[10px] px-1.5 py-px ${getPriorityColor(todo.priority)}`}
|
||||
>
|
||||
{todo.priority}
|
||||
</Badge>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={`text-xs px-2 py-0.5 ${getStatusColor(todo.status)}`}
|
||||
className={`text-[10px] px-1.5 py-px ${getStatusColor(todo.status)}`}
|
||||
>
|
||||
{todo.status.replace('_', ' ')}
|
||||
</Badge>
|
||||
@@ -88,4 +88,4 @@ const TodoList = ({ todos, isResult = false }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default TodoList;
|
||||
export default TodoList;
|
||||
|
||||
@@ -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 (
|
||||
<div
|
||||
id={`tool-result-${message.toolId}`}
|
||||
className={`relative mt-4 p-4 rounded-lg border backdrop-blur-sm scroll-mt-4 ${
|
||||
className={`relative mt-2 p-3 rounded border scroll-mt-4 ${
|
||||
message.toolResult.isError
|
||||
? 'bg-gradient-to-br from-red-50 to-rose-50 dark:from-red-950/20 dark:to-rose-950/20 border-red-200/60 dark:border-red-800/60'
|
||||
: 'bg-gradient-to-br from-green-50 to-emerald-50 dark:from-green-950/20 dark:to-emerald-950/20 border-green-200/60 dark:border-green-800/60'
|
||||
? 'bg-red-50/50 dark:bg-red-950/10 border-red-200/60 dark:border-red-800/40'
|
||||
: 'bg-green-50/50 dark:bg-green-950/10 border-green-200/60 dark:border-green-800/40'
|
||||
}`}>
|
||||
{/* Decorative gradient overlay */}
|
||||
<div className={`absolute inset-0 rounded-lg opacity-50 ${
|
||||
message.toolResult.isError
|
||||
? 'bg-gradient-to-br from-red-500/5 to-rose-500/5 dark:from-red-400/5 dark:to-rose-400/5'
|
||||
: 'bg-gradient-to-br from-green-500/5 to-emerald-500/5 dark:from-green-400/5 dark:to-emerald-400/5'
|
||||
}`}></div>
|
||||
|
||||
<div className="relative flex items-center gap-2.5 mb-3">
|
||||
<div className={`w-6 h-6 rounded-lg flex items-center justify-center shadow-md ${
|
||||
<div className="relative flex items-center gap-1.5 mb-2">
|
||||
<svg className={`w-4 h-4 ${
|
||||
message.toolResult.isError
|
||||
? 'bg-gradient-to-br from-red-500 to-rose-600 dark:from-red-400 dark:to-rose-500 shadow-red-500/20'
|
||||
: 'bg-gradient-to-br from-green-500 to-emerald-600 dark:from-green-400 dark:to-emerald-500 shadow-green-500/20'
|
||||
}`}>
|
||||
<svg className="w-3.5 h-3.5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
{message.toolResult.isError ? (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M6 18L18 6M6 6l12 12" />
|
||||
) : (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M5 13l4 4L19 7" />
|
||||
)}
|
||||
</svg>
|
||||
</div>
|
||||
<span className={`text-sm font-semibold ${
|
||||
? 'text-red-500 dark:text-red-400'
|
||||
: 'text-green-500 dark:text-green-400'
|
||||
}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
{message.toolResult.isError ? (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
) : (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
)}
|
||||
</svg>
|
||||
<span className={`text-xs font-medium ${
|
||||
message.toolResult.isError
|
||||
? 'text-red-800 dark:text-red-200'
|
||||
: 'text-green-800 dark:text-green-200'
|
||||
? 'text-red-700 dark:text-red-300'
|
||||
: 'text-green-700 dark:text-green-300'
|
||||
}`}>
|
||||
{message.toolResult.isError ? 'Tool Error' : 'Tool Result'}
|
||||
{message.toolResult.isError ? 'Error' : 'Result'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -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 (
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<span className="font-medium">
|
||||
Found {toolData.numFiles || toolData.filenames.length} {(toolData.numFiles === 1 || toolData.filenames.length === 1) ? 'file' : 'files'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-1 max-h-96 overflow-y-auto">
|
||||
{toolData.filenames.map((filePath, index) => {
|
||||
const fileName = filePath.split('/').pop();
|
||||
const dirPath = filePath.substring(0, filePath.lastIndexOf('/'));
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
onClick={() => {
|
||||
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"
|
||||
>
|
||||
<svg className="w-4 h-4 text-green-600 dark:text-green-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-mono text-sm font-medium text-green-800 dark:text-green-200 truncate group-hover:text-green-900 dark:group-hover:text-green-100">
|
||||
{fileName}
|
||||
</div>
|
||||
<div className="font-mono text-xs text-green-600/70 dark:text-green-400/70 truncate">
|
||||
{dirPath}
|
||||
</div>
|
||||
</div>
|
||||
<svg className="w-4 h-4 text-green-600 dark:text-green-400 opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<FileListContent
|
||||
files={toolData.filenames}
|
||||
onFileClick={onFileOpen}
|
||||
title={`Found ${count} ${count === 1 ? 'file' : 'files'}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Task tool results - proper task list rendering
|
||||
if (message.toolName === 'TaskList' || message.toolName === 'TaskGet') {
|
||||
return <TaskListContent content={content} />;
|
||||
}
|
||||
|
||||
// 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
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={`text-xs text-gray-500 dark:text-gray-400 mt-1 ${isGrouped ? 'opacity-0 group-hover:opacity-100' : ''}`}>
|
||||
{new Date(message.timestamp).toLocaleTimeString()}
|
||||
</div>
|
||||
{!isGrouped && (
|
||||
<div className="text-[11px] text-gray-400 dark:text-gray-500 mt-1">
|
||||
{new Date(message.timestamp).toLocaleTimeString()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -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<ToolRendererProps> = ({
|
||||
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<ToolRendererProps> = ({
|
||||
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<ToolRendererProps> = ({
|
||||
);
|
||||
break;
|
||||
|
||||
case 'task':
|
||||
contentComponent = (
|
||||
<TaskListContent
|
||||
content={contentProps.content || ''}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
|
||||
case 'text':
|
||||
contentComponent = (
|
||||
<TextContent
|
||||
@@ -155,15 +173,18 @@ export const ToolRenderer: React.FC<ToolRendererProps> = ({
|
||||
case 'success-message':
|
||||
const message = displayConfig.getMessage?.(parsedData) || 'Success';
|
||||
contentComponent = (
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="font-medium">{message}</span>
|
||||
<div className="flex items-center gap-1.5 text-xs text-green-600 dark:text-green-400">
|
||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
{message}
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
contentComponent = (
|
||||
<div className="text-gray-500">Unknown content type: {displayConfig.contentType}</div>
|
||||
<div className="text-gray-500 text-xs">Unknown content type: {displayConfig.contentType}</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -223,11 +244,16 @@ export const ToolRenderer: React.FC<ToolRendererProps> = ({
|
||||
actionButton = (
|
||||
<FilePathButton
|
||||
filePath={contentProps.filePath}
|
||||
onClick={handleFileClick}
|
||||
onClick={(e?: any) => 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 (
|
||||
<CollapsibleDisplay
|
||||
toolName={toolName}
|
||||
@@ -235,17 +261,20 @@ export const ToolRenderer: React.FC<ToolRendererProps> = ({
|
||||
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)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<string, string> = {
|
||||
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<CollapsibleDisplayProps> = ({
|
||||
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 <div className="text-gray-500">Unknown content type: {contentType}</div>;
|
||||
return <div className="text-xs text-gray-500">Unknown content type: {contentType}</div>;
|
||||
}
|
||||
};
|
||||
|
||||
const borderColor = borderColorMap[toolCategory || 'default'];
|
||||
|
||||
return (
|
||||
<div className="group relative bg-gradient-to-br from-blue-50/50 to-indigo-50/50 dark:from-blue-950/20 dark:to-indigo-950/20 border border-blue-100/30 dark:border-blue-800/30 rounded-lg p-3 mb-2">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-blue-500/3 to-indigo-500/3 dark:from-blue-400/3 dark:to-indigo-400/3 rounded-lg opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||
|
||||
<div className="relative flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative w-8 h-8 bg-gradient-to-br from-blue-500 to-indigo-600 dark:from-blue-400 dark:to-indigo-500 rounded-lg flex items-center justify-center shadow-lg shadow-blue-500/20 dark:shadow-blue-400/20">
|
||||
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
<div className="absolute inset-0 rounded-lg bg-blue-500 dark:bg-blue-400 animate-pulse opacity-20"></div>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-semibold text-gray-900 dark:text-white text-sm">
|
||||
{toolName}
|
||||
</span>
|
||||
{toolId && (
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400 font-mono">
|
||||
{toolId}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{onShowSettings && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onShowSettings();
|
||||
}}
|
||||
className="p-2 rounded-lg hover:bg-white/60 dark:hover:bg-gray-800/60 transition-all duration-200 group/btn backdrop-blur-sm"
|
||||
title="Settings"
|
||||
>
|
||||
<svg className="w-4 h-4 text-gray-600 dark:text-gray-400 group-hover/btn:text-blue-600 dark:group-hover/btn:text-blue-400 transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={`border-l-2 ${borderColor} pl-3 py-0.5 my-1 ${className}`}>
|
||||
<CollapsibleSection
|
||||
title={title}
|
||||
toolName={toolName}
|
||||
open={defaultOpen}
|
||||
action={action}
|
||||
className={className}
|
||||
onTitleClick={onTitleClick}
|
||||
>
|
||||
{renderContent()}
|
||||
|
||||
{showRawParameters && rawContent && (
|
||||
<details className="relative mt-3 pl-6 group/raw" open={defaultOpen}>
|
||||
<summary className="flex items-center gap-2 text-xs font-medium text-gray-600 dark:text-gray-400 cursor-pointer hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200 p-2 rounded-lg hover:bg-white/50 dark:hover:bg-gray-800/50">
|
||||
<details className="relative mt-2 group/raw">
|
||||
<summary className="flex items-center gap-1.5 text-[11px] text-gray-400 dark:text-gray-500 cursor-pointer hover:text-gray-600 dark:hover:text-gray-300 py-0.5">
|
||||
<svg
|
||||
className="w-3 h-3 transition-transform duration-200 group-open/raw:rotate-180"
|
||||
className="w-2.5 h-2.5 transition-transform duration-150 group-open/raw:rotate-90"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
View raw parameters
|
||||
raw params
|
||||
</summary>
|
||||
<pre className="mt-2 text-xs bg-gray-50 dark:bg-gray-800/50 border border-gray-200/60 dark:border-gray-700/60 p-3 rounded-lg whitespace-pre-wrap break-words overflow-hidden text-gray-700 dark:text-gray-300 font-mono">
|
||||
<pre className="mt-1 text-[11px] bg-gray-50 dark:bg-gray-900/50 border border-gray-200/40 dark:border-gray-700/40 p-2 rounded whitespace-pre-wrap break-words overflow-hidden text-gray-600 dark:text-gray-400 font-mono">
|
||||
{rawContent}
|
||||
</pre>
|
||||
</details>
|
||||
|
||||
@@ -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<CollapsibleSectionProps> = ({
|
||||
title,
|
||||
toolName,
|
||||
open = false,
|
||||
action,
|
||||
onTitleClick,
|
||||
children,
|
||||
className = ''
|
||||
}) => {
|
||||
return (
|
||||
<details className={`relative mt-3 group/details ${className}`} open={open}>
|
||||
<summary className="flex items-center gap-2 text-sm font-medium text-gray-700 dark:text-gray-300 cursor-pointer hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200 p-2.5 rounded-lg hover:bg-white/50 dark:hover:bg-gray-800/50">
|
||||
<details className={`relative group/details ${className}`} open={open}>
|
||||
<summary className="flex items-center gap-1.5 text-xs cursor-pointer py-0.5 select-none">
|
||||
<svg
|
||||
className="w-4 h-4 transition-transform duration-200 group-open/details:rotate-180"
|
||||
className="w-3 h-3 text-gray-400 dark:text-gray-500 transition-transform duration-150 group-open/details:rotate-90 flex-shrink-0"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
<span className="flex items-center gap-2 flex-1">
|
||||
{title}
|
||||
</span>
|
||||
{action}
|
||||
{toolName && (
|
||||
<span className="font-medium text-gray-500 dark:text-gray-400 flex-shrink-0">{toolName}</span>
|
||||
)}
|
||||
{toolName && (
|
||||
<span className="text-gray-300 dark:text-gray-600 text-[10px] flex-shrink-0">/</span>
|
||||
)}
|
||||
{onTitleClick ? (
|
||||
<button
|
||||
onClick={(e) => { e.preventDefault(); e.stopPropagation(); onTitleClick(); }}
|
||||
className="text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 font-mono hover:underline truncate flex-1 text-left transition-colors"
|
||||
>
|
||||
{title}
|
||||
</button>
|
||||
) : (
|
||||
<span className="text-gray-600 dark:text-gray-400 truncate flex-1">
|
||||
{title}
|
||||
</span>
|
||||
)}
|
||||
{action && <span className="flex-shrink-0 ml-1">{action}</span>}
|
||||
</summary>
|
||||
<div className="mt-3 pl-6">
|
||||
<div className="mt-1.5 pl-[18px]">
|
||||
{children}
|
||||
</div>
|
||||
</details>
|
||||
|
||||
@@ -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<FileListContentProps> = ({
|
||||
@@ -20,54 +20,34 @@ export const FileListContent: React.FC<FileListContentProps> = ({
|
||||
onFileClick,
|
||||
title
|
||||
}) => {
|
||||
const fileCount = files.length;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{title && (
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<span className="font-medium">
|
||||
{title || `Found ${fileCount} ${fileCount === 1 ? 'file' : 'files'}`}
|
||||
</span>
|
||||
<div className="text-[11px] text-gray-500 dark:text-gray-400 mb-1">
|
||||
{title}
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-1 max-h-96 overflow-y-auto">
|
||||
<div className="flex flex-wrap gap-x-1 gap-y-0.5 max-h-48 overflow-y-auto">
|
||||
{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 (
|
||||
<div
|
||||
key={index}
|
||||
onClick={handleClick}
|
||||
className="group flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-blue-50 dark:hover:bg-blue-900/20 cursor-pointer transition-colors"
|
||||
>
|
||||
{/* File icon */}
|
||||
<svg className="w-4 h-4 text-gray-400 dark:text-gray-500 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
|
||||
{/* File path */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-gray-100 truncate">
|
||||
{fileName}
|
||||
</div>
|
||||
{dirPath && (
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 truncate">
|
||||
{dirPath}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Chevron on hover */}
|
||||
<svg className="w-4 h-4 text-gray-400 opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<span key={index} className="inline-flex items-center">
|
||||
<button
|
||||
onClick={handleClick}
|
||||
className="text-[11px] font-mono text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 hover:underline transition-colors"
|
||||
title={filePath}
|
||||
>
|
||||
{fileName}
|
||||
</button>
|
||||
{index < files.length - 1 && (
|
||||
<span className="text-gray-300 dark:text-gray-600 text-[10px] ml-1">,</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,7 @@ interface MarkdownContentProps {
|
||||
*/
|
||||
export const MarkdownContent: React.FC<MarkdownContentProps> = ({
|
||||
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 (
|
||||
<Markdown className={className}>
|
||||
|
||||
@@ -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: (
|
||||
<svg className="w-3.5 h-3.5 text-green-500 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
),
|
||||
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: (
|
||||
<svg className="w-3.5 h-3.5 text-blue-500 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
),
|
||||
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: (
|
||||
<svg className="w-3.5 h-3.5 text-gray-400 dark:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<circle cx="12" cy="12" r="9" strokeWidth={2} />
|
||||
</svg>
|
||||
),
|
||||
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<TaskListContentProps> = ({ content }) => {
|
||||
const tasks = parseTaskContent(content);
|
||||
|
||||
// If we couldn't parse any tasks, fall back to text display
|
||||
if (tasks.length === 0) {
|
||||
return (
|
||||
<pre className="text-[11px] font-mono text-gray-600 dark:text-gray-400 whitespace-pre-wrap">
|
||||
{content}
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
|
||||
const completed = tasks.filter(t => t.status === 'completed').length;
|
||||
const total = tasks.length;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1.5">
|
||||
<span className="text-[11px] text-gray-500 dark:text-gray-400">
|
||||
{completed}/{total} completed
|
||||
</span>
|
||||
<div className="flex-1 h-1 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-green-500 dark:bg-green-400 rounded-full transition-all"
|
||||
style={{ width: `${total > 0 ? (completed / total) * 100 : 0}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-px">
|
||||
{tasks.map((task) => {
|
||||
const config = statusConfig[task.status] || statusConfig.pending;
|
||||
return (
|
||||
<div
|
||||
key={task.id}
|
||||
className="flex items-center gap-1.5 py-0.5 group"
|
||||
>
|
||||
<span className="flex-shrink-0">{config.icon}</span>
|
||||
<span className="text-[11px] font-mono text-gray-400 dark:text-gray-500 flex-shrink-0">
|
||||
#{task.id}
|
||||
</span>
|
||||
<span className={`text-xs truncate flex-1 ${config.textClass}`}>
|
||||
{task.subject}
|
||||
</span>
|
||||
<span className={`text-[10px] px-1 py-px rounded border flex-shrink-0 ${config.badgeClass}`}>
|
||||
{task.status.replace('_', ' ')}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -25,7 +25,7 @@ export const TextContent: React.FC<TextContentProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<pre className={`mt-2 text-xs bg-gray-900 dark:bg-gray-950 text-gray-100 p-4 rounded-lg overflow-x-auto font-mono ${className}`}>
|
||||
<pre className={`mt-1 text-xs bg-gray-900 dark:bg-gray-950 text-gray-100 p-2.5 rounded overflow-x-auto font-mono ${className}`}>
|
||||
{formattedJson}
|
||||
</pre>
|
||||
);
|
||||
@@ -33,7 +33,7 @@ export const TextContent: React.FC<TextContentProps> = ({
|
||||
|
||||
if (format === 'code') {
|
||||
return (
|
||||
<pre className={`mt-2 text-xs bg-gray-50 dark:bg-gray-800/50 border border-gray-200/60 dark:border-gray-700/60 p-3 rounded-lg whitespace-pre-wrap break-words overflow-hidden text-gray-700 dark:text-gray-300 font-mono ${className}`}>
|
||||
<pre className={`mt-1 text-xs bg-gray-50 dark:bg-gray-800/50 border border-gray-200/50 dark:border-gray-700/50 p-2 rounded whitespace-pre-wrap break-words overflow-hidden text-gray-700 dark:text-gray-300 font-mono ${className}`}>
|
||||
{content}
|
||||
</pre>
|
||||
);
|
||||
@@ -41,7 +41,7 @@ export const TextContent: React.FC<TextContentProps> = ({
|
||||
|
||||
// Plain text
|
||||
return (
|
||||
<div className={`mt-2 text-sm text-gray-700 dark:text-gray-300 whitespace-pre-wrap ${className}`}>
|
||||
<div className={`mt-1 text-sm text-gray-700 dark:text-gray-300 whitespace-pre-wrap ${className}`}>
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export { MarkdownContent } from './MarkdownContent';
|
||||
export { FileListContent } from './FileListContent';
|
||||
export { TodoListContent } from './TodoListContent';
|
||||
export { TaskListContent } from './TaskListContent';
|
||||
export { TextContent } from './TextContent';
|
||||
|
||||
@@ -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<DiffViewerProps> = ({
|
||||
oldContent,
|
||||
@@ -30,48 +29,48 @@ export const DiffViewer: React.FC<DiffViewerProps> = ({
|
||||
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 (
|
||||
<div className="bg-white dark:bg-gray-900/50 border border-gray-200/60 dark:border-gray-700/60 rounded-lg overflow-hidden shadow-sm">
|
||||
<div className="border border-gray-200/60 dark:border-gray-700/50 rounded overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-4 py-2.5 bg-gradient-to-r from-gray-50 to-gray-100/50 dark:from-gray-800/80 dark:to-gray-800/40 border-b border-gray-200/60 dark:border-gray-700/60 backdrop-blur-sm">
|
||||
<div className="flex items-center justify-between px-2.5 py-1 bg-gray-50/80 dark:bg-gray-800/40 border-b border-gray-200/60 dark:border-gray-700/50">
|
||||
{onFileClick ? (
|
||||
<button
|
||||
onClick={onFileClick}
|
||||
className="text-xs font-mono text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 truncate cursor-pointer font-medium transition-colors"
|
||||
className="text-[11px] font-mono text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 truncate cursor-pointer transition-colors"
|
||||
>
|
||||
{filePath}
|
||||
</button>
|
||||
) : (
|
||||
<span className="text-xs font-mono text-gray-700 dark:text-gray-300 truncate">
|
||||
<span className="text-[11px] font-mono text-gray-600 dark:text-gray-400 truncate">
|
||||
{filePath}
|
||||
</span>
|
||||
)}
|
||||
<span className={`text-xs font-medium px-2 py-0.5 rounded ${badgeClasses}`}>
|
||||
<span className={`text-[10px] font-medium px-1.5 py-px rounded ${badgeClasses} flex-shrink-0 ml-2`}>
|
||||
{badge}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Diff content */}
|
||||
<div className="text-xs font-mono">
|
||||
{/* Diff lines */}
|
||||
<div className="text-[11px] font-mono leading-[18px]">
|
||||
{createDiff(oldContent, newContent).map((diffLine, i) => (
|
||||
<div key={i} className="flex">
|
||||
<span
|
||||
className={`w-8 text-center border-r ${
|
||||
className={`w-6 text-center select-none flex-shrink-0 ${
|
||||
diffLine.type === 'removed'
|
||||
? 'bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 border-red-200 dark:border-red-800'
|
||||
: 'bg-green-50 dark:bg-green-900/20 text-green-600 dark:text-green-400 border-green-200 dark:border-green-800'
|
||||
? 'bg-red-50 dark:bg-red-950/30 text-red-400 dark:text-red-500'
|
||||
: 'bg-green-50 dark:bg-green-950/30 text-green-400 dark:text-green-500'
|
||||
}`}
|
||||
>
|
||||
{diffLine.type === 'removed' ? '-' : '+'}
|
||||
</span>
|
||||
<span
|
||||
className={`px-2 py-0.5 flex-1 whitespace-pre-wrap ${
|
||||
className={`px-2 flex-1 whitespace-pre-wrap ${
|
||||
diffLine.type === 'removed'
|
||||
? 'bg-red-50 dark:bg-red-900/20 text-red-800 dark:text-red-200'
|
||||
: 'bg-green-50 dark:bg-green-900/20 text-green-800 dark:text-green-200'
|
||||
? 'bg-red-50/50 dark:bg-red-950/20 text-red-800 dark:text-red-200'
|
||||
: 'bg-green-50/50 dark:bg-green-950/20 text-green-800 dark:text-green-200'
|
||||
}`}
|
||||
>
|
||||
{diffLine.content}
|
||||
|
||||
@@ -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<FilePathButtonProps> = ({
|
||||
filePath,
|
||||
@@ -26,7 +25,7 @@ export const FilePathButton: React.FC<FilePathButtonProps> = ({
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={`text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 underline font-mono transition-colors ${className}`}
|
||||
className={`text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 font-mono text-xs hover:underline transition-colors ${className}`}
|
||||
>
|
||||
{displayText}
|
||||
</button>
|
||||
@@ -36,7 +35,8 @@ export const FilePathButton: React.FC<FilePathButtonProps> = ({
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={`px-2.5 py-1 rounded-md bg-white/60 dark:bg-gray-800/60 text-blue-600 dark:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/30 font-mono text-xs font-medium transition-all duration-200 shadow-sm ${className}`}
|
||||
className={`text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 font-mono text-xs hover:underline transition-colors ${className}`}
|
||||
title={filePath}
|
||||
>
|
||||
{displayText}
|
||||
</button>
|
||||
|
||||
@@ -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<OneLineDisplayProps> = ({
|
||||
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<OneLineDisplayProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const renderActionButton = () => {
|
||||
if (action === 'none') return null;
|
||||
const renderCopyButton = () => (
|
||||
<button
|
||||
onClick={handleAction}
|
||||
className="opacity-0 group-hover:opacity-100 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 transition-all ml-1 flex-shrink-0"
|
||||
title="Copy to clipboard"
|
||||
aria-label="Copy to clipboard"
|
||||
>
|
||||
{copied ? (
|
||||
<svg className="w-3 h-3 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
|
||||
if (action === 'copy') {
|
||||
return (
|
||||
<button
|
||||
onClick={handleAction}
|
||||
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 transition-colors ml-1"
|
||||
title="Copy to clipboard"
|
||||
aria-label="Copy to clipboard"
|
||||
>
|
||||
{copied ? (
|
||||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
if (action === 'open-file') {
|
||||
return (
|
||||
<button
|
||||
onClick={handleAction}
|
||||
className="text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 underline font-mono transition-colors"
|
||||
>
|
||||
{value.split('/').pop()}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
if (action === 'jump-to-results' && resultId) {
|
||||
return (
|
||||
<a
|
||||
href={`#${resultId}`}
|
||||
className="flex-shrink-0 text-xs text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 font-medium transition-colors flex items-center gap-1"
|
||||
>
|
||||
<span>Search results</span>
|
||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
// Terminal style: dark pill only around the command
|
||||
if (isTerminal) {
|
||||
return (
|
||||
<div className="group flex items-start gap-2 my-1">
|
||||
<div className="flex items-center gap-1.5 flex-shrink-0 pt-0.5">
|
||||
<svg className="w-3 h-3 text-green-500 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
const isTerminal = style === 'terminal';
|
||||
|
||||
return (
|
||||
<div className={`group relative ${colorScheme.background} border-l-2 ${colorScheme.border} pl-3 py-1 my-0.5`}>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-2 text-xs text-gray-600 dark:text-gray-400 flex-1 min-w-0">
|
||||
{icon === 'terminal' ? (
|
||||
<svg className={`w-3.5 h-3.5 ${colorScheme.icon} flex-shrink-0`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
) : icon ? (
|
||||
<span className={`${colorScheme.icon} flex-shrink-0`}>
|
||||
{icon}
|
||||
</span>
|
||||
) : label ? (
|
||||
<span className="font-medium flex-shrink-0">{label}</span>
|
||||
) : (
|
||||
<span className="font-medium flex-shrink-0">{toolName}</span>
|
||||
)}
|
||||
|
||||
<span className="text-gray-400 dark:text-gray-500 flex-shrink-0">•</span>
|
||||
|
||||
{action === 'open-file' ? (
|
||||
renderActionButton()
|
||||
) : (
|
||||
<span className={`font-mono ${wrapText ? 'whitespace-pre-wrap break-all' : 'truncate'} flex-1 min-w-0 ${colorScheme.primary}`}>
|
||||
{isTerminal && <span className="text-green-500 dark:text-green-400 mr-1">$</span>}
|
||||
{value}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{secondary && (
|
||||
<span className={`text-xs ${colorScheme.secondary} italic ml-2`}>
|
||||
({secondary})
|
||||
</span>
|
||||
)}
|
||||
|
||||
{action === 'copy' && renderActionButton()}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0 flex items-start gap-2">
|
||||
<div className="bg-gray-900 dark:bg-black rounded px-2.5 py-1 flex-1 min-w-0">
|
||||
<code className={`text-xs text-green-400 font-mono ${wrapText ? 'whitespace-pre-wrap break-all' : 'block truncate'}`}>
|
||||
<span className="text-green-600 dark:text-green-500 select-none">$ </span>{value}
|
||||
</code>
|
||||
</div>
|
||||
{action === 'copy' && renderCopyButton()}
|
||||
</div>
|
||||
{secondary && (
|
||||
<span className="text-[11px] text-gray-400 dark:text-gray-500 italic flex-shrink-0 pt-1">
|
||||
{secondary}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
{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 (
|
||||
<div className={`group flex items-center gap-1.5 border-l-2 ${colorScheme.border} pl-3 py-0.5 my-0.5`}>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400 flex-shrink-0">{label || toolName}</span>
|
||||
<span className="text-gray-300 dark:text-gray-600 text-[10px]">/</span>
|
||||
<button
|
||||
onClick={handleAction}
|
||||
className="text-xs text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 font-mono hover:underline transition-colors truncate"
|
||||
title={value}
|
||||
>
|
||||
{displayName}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Search / jump-to-results style
|
||||
if (action === 'jump-to-results') {
|
||||
return (
|
||||
<div className={`group flex items-center gap-1.5 border-l-2 ${colorScheme.border} pl-3 py-0.5 my-0.5`}>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400 flex-shrink-0">{label || toolName}</span>
|
||||
<span className="text-gray-300 dark:text-gray-600 text-[10px]">/</span>
|
||||
<span className={`text-xs font-mono truncate flex-1 min-w-0 ${colorScheme.primary}`}>
|
||||
{value}
|
||||
</span>
|
||||
{secondary && (
|
||||
<span className="text-[11px] text-gray-400 dark:text-gray-500 italic flex-shrink-0">
|
||||
{secondary}
|
||||
</span>
|
||||
)}
|
||||
{toolResult && (
|
||||
<a
|
||||
href={`#tool-result-${toolId}`}
|
||||
className="flex-shrink-0 text-xs text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 font-medium transition-colors flex items-center gap-1"
|
||||
className="flex-shrink-0 text-[11px] text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 transition-colors flex items-center gap-0.5"
|
||||
>
|
||||
<span>Search results</span>
|
||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Default one-line style
|
||||
return (
|
||||
<div className={`group flex items-center gap-1.5 ${colorScheme.background || ''} border-l-2 ${colorScheme.border} pl-3 py-0.5 my-0.5`}>
|
||||
{icon && icon !== 'terminal' && (
|
||||
<span className={`${colorScheme.icon} flex-shrink-0 text-xs`}>{icon}</span>
|
||||
)}
|
||||
{!icon && (label || toolName) && (
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400 flex-shrink-0">{label || toolName}</span>
|
||||
)}
|
||||
{(icon || label || toolName) && (
|
||||
<span className="text-gray-300 dark:text-gray-600 text-[10px]">/</span>
|
||||
)}
|
||||
<span className={`text-xs font-mono ${wrapText ? 'whitespace-pre-wrap break-all' : 'truncate'} flex-1 min-w-0 ${colorScheme.primary}`}>
|
||||
{value}
|
||||
</span>
|
||||
{secondary && (
|
||||
<span className={`text-[11px] ${colorScheme.secondary} italic flex-shrink-0`}>
|
||||
{secondary}
|
||||
</span>
|
||||
)}
|
||||
{action === 'copy' && renderCopyButton()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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<string, ToolDisplayConfig> = {
|
||||
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<string, ToolDisplayConfig> = {
|
||||
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<string, ToolDisplayConfig> = {
|
||||
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<string, ToolDisplayConfig> = {
|
||||
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<string, ToolDisplayConfig> = {
|
||||
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<string, ToolDisplayConfig> = {
|
||||
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<string, ToolDisplayConfig> = {
|
||||
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<string, ToolDisplayConfig> = {
|
||||
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<string, ToolDisplayConfig> = {
|
||||
}
|
||||
},
|
||||
|
||||
// ============================================================================
|
||||
// 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<string, ToolDisplayConfig> = {
|
||||
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<string, ToolDisplayConfig> = {
|
||||
Default: {
|
||||
input: {
|
||||
type: 'collapsible',
|
||||
title: 'View input parameters',
|
||||
title: 'Parameters',
|
||||
defaultOpen: false,
|
||||
contentType: 'text',
|
||||
getContentProps: (input) => ({
|
||||
|
||||
Reference in New Issue
Block a user