mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-02-14 20:57:32 +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) => {
|
const getStatusIcon = (status) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'completed':
|
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':
|
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':
|
case 'pending':
|
||||||
default:
|
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 (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-1.5">
|
||||||
{isResult && (
|
{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'})
|
Todo List ({todos.length} {todos.length === 1 ? 'item' : 'items'})
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{todos.map((todo, index) => (
|
{todos.map((todo, index) => (
|
||||||
<div
|
<div
|
||||||
key={todo.id || `todo-${index}`}
|
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">
|
<div className="flex-shrink-0 mt-0.5">
|
||||||
{getStatusIcon(todo.status)}
|
{getStatusIcon(todo.status)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-start justify-between gap-2 mb-2">
|
<div className="flex items-start justify-between gap-2 mb-0.5">
|
||||||
<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'}`}>
|
<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}
|
{todo.content}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="flex gap-1 flex-shrink-0">
|
<div className="flex gap-1 flex-shrink-0">
|
||||||
<Badge
|
<Badge
|
||||||
variant="outline"
|
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}
|
{todo.priority}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge
|
<Badge
|
||||||
variant="outline"
|
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('_', ' ')}
|
{todo.status.replace('_', ' ')}
|
||||||
</Badge>
|
</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 { formatUsageLimitText } from '../utils/chatFormatting';
|
||||||
import { getClaudePermissionSuggestion } from '../utils/chatPermissions';
|
import { getClaudePermissionSuggestion } from '../utils/chatPermissions';
|
||||||
import type { Project } from '../../../types/app';
|
import type { Project } from '../../../types/app';
|
||||||
import { ToolRenderer, shouldHideToolResult } from '../tools';
|
import { ToolRenderer, shouldHideToolResult, FileListContent, TaskListContent } from '../tools';
|
||||||
|
|
||||||
type DiffLine = {
|
type DiffLine = {
|
||||||
type: string;
|
type: string;
|
||||||
@@ -181,38 +181,29 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
id={`tool-result-${message.toolId}`}
|
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
|
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-red-50/50 dark:bg-red-950/10 border-red-200/60 dark:border-red-800/40'
|
||||||
: '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-green-50/50 dark:bg-green-950/10 border-green-200/60 dark:border-green-800/40'
|
||||||
}`}>
|
}`}>
|
||||||
{/* Decorative gradient overlay */}
|
<div className="relative flex items-center gap-1.5 mb-2">
|
||||||
<div className={`absolute inset-0 rounded-lg opacity-50 ${
|
<svg className={`w-4 h-4 ${
|
||||||
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 ${
|
|
||||||
message.toolResult.isError
|
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'
|
? 'text-red-500 dark:text-red-400'
|
||||||
: 'bg-gradient-to-br from-green-500 to-emerald-600 dark:from-green-400 dark:to-emerald-500 shadow-green-500/20'
|
: 'text-green-500 dark:text-green-400'
|
||||||
}`}>
|
}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<svg className="w-3.5 h-3.5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
{message.toolResult.isError ? (
|
||||||
{message.toolResult.isError ? (
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M6 18L18 6M6 6l12 12" />
|
) : (
|
||||||
) : (
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M5 13l4 4L19 7" />
|
)}
|
||||||
)}
|
</svg>
|
||||||
</svg>
|
<span className={`text-xs font-medium ${
|
||||||
</div>
|
|
||||||
<span className={`text-sm font-semibold ${
|
|
||||||
message.toolResult.isError
|
message.toolResult.isError
|
||||||
? 'text-red-800 dark:text-red-200'
|
? 'text-red-700 dark:text-red-300'
|
||||||
: 'text-green-800 dark:text-green-200'
|
: 'text-green-700 dark:text-green-300'
|
||||||
}`}>
|
}`}>
|
||||||
{message.toolResult.isError ? 'Tool Error' : 'Tool Result'}
|
{message.toolResult.isError ? 'Error' : 'Result'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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) {
|
if ((message.toolName === 'Grep' || message.toolName === 'Glob') && message.toolResult?.toolUseResult) {
|
||||||
const toolData = 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) {
|
if (toolData.filenames && Array.isArray(toolData.filenames) && toolData.filenames.length > 0) {
|
||||||
|
const count = toolData.numFiles || toolData.filenames.length;
|
||||||
return (
|
return (
|
||||||
<div>
|
<FileListContent
|
||||||
<div className="flex items-center gap-2 mb-3">
|
files={toolData.filenames}
|
||||||
<span className="font-medium">
|
onFileClick={onFileOpen}
|
||||||
Found {toolData.numFiles || toolData.filenames.length} {(toolData.numFiles === 1 || toolData.filenames.length === 1) ? 'file' : 'files'}
|
title={`Found ${count} ${count === 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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Task tool results - proper task list rendering
|
||||||
|
if (message.toolName === 'TaskList' || message.toolName === 'TaskGet') {
|
||||||
|
return <TaskListContent content={content} />;
|
||||||
|
}
|
||||||
|
|
||||||
// Special handling for interactive prompts
|
// Special handling for interactive prompts
|
||||||
if (content.includes('Do you want to proceed?') && message.toolName === 'Bash') {
|
if (content.includes('Do you want to proceed?') && message.toolName === 'Bash') {
|
||||||
const lines = content.split('\n');
|
const lines = content.split('\n');
|
||||||
@@ -797,9 +757,11 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={`text-xs text-gray-500 dark:text-gray-400 mt-1 ${isGrouped ? 'opacity-0 group-hover:opacity-100' : ''}`}>
|
{!isGrouped && (
|
||||||
{new Date(message.timestamp).toLocaleTimeString()}
|
<div className="text-[11px] text-gray-400 dark:text-gray-500 mt-1">
|
||||||
</div>
|
{new Date(message.timestamp).toLocaleTimeString()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { getToolConfig } from './configs/toolConfigs';
|
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';
|
import type { Project } from '../../../types/app';
|
||||||
|
|
||||||
type DiffLine = {
|
type DiffLine = {
|
||||||
@@ -24,6 +24,16 @@ interface ToolRendererProps {
|
|||||||
rawToolInput?: string;
|
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
|
* Main tool renderer router
|
||||||
* Routes to OneLineDisplay or CollapsibleDisplay based on tool config
|
* Routes to OneLineDisplay or CollapsibleDisplay based on tool config
|
||||||
@@ -43,7 +53,7 @@ export const ToolRenderer: React.FC<ToolRendererProps> = ({
|
|||||||
rawToolInput
|
rawToolInput
|
||||||
}) => {
|
}) => {
|
||||||
const config = getToolConfig(toolName);
|
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;
|
if (!displayConfig) return null;
|
||||||
|
|
||||||
@@ -87,7 +97,7 @@ export const ToolRenderer: React.FC<ToolRendererProps> = ({
|
|||||||
if (displayConfig.type === 'collapsible') {
|
if (displayConfig.type === 'collapsible') {
|
||||||
const title = typeof displayConfig.title === 'function'
|
const title = typeof displayConfig.title === 'function'
|
||||||
? displayConfig.title(parsedData)
|
? displayConfig.title(parsedData)
|
||||||
: displayConfig.title || 'View details';
|
: displayConfig.title || 'Details';
|
||||||
|
|
||||||
const defaultOpen = displayConfig.defaultOpen !== undefined
|
const defaultOpen = displayConfig.defaultOpen !== undefined
|
||||||
? displayConfig.defaultOpen
|
? displayConfig.defaultOpen
|
||||||
@@ -143,6 +153,14 @@ export const ToolRenderer: React.FC<ToolRendererProps> = ({
|
|||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'task':
|
||||||
|
contentComponent = (
|
||||||
|
<TaskListContent
|
||||||
|
content={contentProps.content || ''}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
case 'text':
|
case 'text':
|
||||||
contentComponent = (
|
contentComponent = (
|
||||||
<TextContent
|
<TextContent
|
||||||
@@ -155,15 +173,18 @@ export const ToolRenderer: React.FC<ToolRendererProps> = ({
|
|||||||
case 'success-message':
|
case 'success-message':
|
||||||
const message = displayConfig.getMessage?.(parsedData) || 'Success';
|
const message = displayConfig.getMessage?.(parsedData) || 'Success';
|
||||||
contentComponent = (
|
contentComponent = (
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="flex items-center gap-1.5 text-xs text-green-600 dark:text-green-400">
|
||||||
<span className="font-medium">{message}</span>
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
contentComponent = (
|
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 = (
|
actionButton = (
|
||||||
<FilePathButton
|
<FilePathButton
|
||||||
filePath={contentProps.filePath}
|
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 (
|
return (
|
||||||
<CollapsibleDisplay
|
<CollapsibleDisplay
|
||||||
toolName={toolName}
|
toolName={toolName}
|
||||||
@@ -235,17 +261,20 @@ export const ToolRenderer: React.FC<ToolRendererProps> = ({
|
|||||||
title={title}
|
title={title}
|
||||||
defaultOpen={defaultOpen}
|
defaultOpen={defaultOpen}
|
||||||
action={actionButton}
|
action={actionButton}
|
||||||
|
onTitleClick={handleTitleClick}
|
||||||
contentType={displayConfig.contentType || 'text'}
|
contentType={displayConfig.contentType || 'text'}
|
||||||
contentProps={{
|
contentProps={{
|
||||||
DiffViewer: contentComponent,
|
DiffViewer: contentComponent,
|
||||||
MarkdownComponent: contentComponent,
|
MarkdownComponent: contentComponent,
|
||||||
FileListComponent: contentComponent,
|
FileListComponent: contentComponent,
|
||||||
TodoListComponent: contentComponent,
|
TodoListComponent: contentComponent,
|
||||||
|
TaskComponent: contentComponent,
|
||||||
TextComponent: contentComponent
|
TextComponent: contentComponent
|
||||||
}}
|
}}
|
||||||
showRawParameters={mode === 'input' && showRawParameters}
|
showRawParameters={mode === 'input' && showRawParameters}
|
||||||
rawContent={rawToolInput}
|
rawContent={rawToolInput}
|
||||||
onShowSettings={onShowSettings}
|
onShowSettings={onShowSettings}
|
||||||
|
toolCategory={getToolCategory(toolName)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { CollapsibleSection } from './CollapsibleSection';
|
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 {
|
interface CollapsibleDisplayProps {
|
||||||
toolName: string;
|
toolName: string;
|
||||||
@@ -9,111 +9,85 @@ interface CollapsibleDisplayProps {
|
|||||||
title: string;
|
title: string;
|
||||||
defaultOpen?: boolean;
|
defaultOpen?: boolean;
|
||||||
action?: React.ReactNode;
|
action?: React.ReactNode;
|
||||||
|
onTitleClick?: () => void;
|
||||||
contentType: ContentType;
|
contentType: ContentType;
|
||||||
contentProps: any;
|
contentProps: any;
|
||||||
showRawParameters?: boolean;
|
showRawParameters?: boolean;
|
||||||
rawContent?: string;
|
rawContent?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
onShowSettings?: () => void;
|
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> = ({
|
export const CollapsibleDisplay: React.FC<CollapsibleDisplayProps> = ({
|
||||||
toolName,
|
toolName,
|
||||||
toolId,
|
|
||||||
title,
|
title,
|
||||||
defaultOpen = false,
|
defaultOpen = false,
|
||||||
action,
|
action,
|
||||||
|
onTitleClick,
|
||||||
contentType,
|
contentType,
|
||||||
contentProps,
|
contentProps,
|
||||||
showRawParameters = false,
|
showRawParameters = false,
|
||||||
rawContent,
|
rawContent,
|
||||||
className = '',
|
className = '',
|
||||||
onShowSettings
|
toolCategory
|
||||||
}) => {
|
}) => {
|
||||||
const renderContent = () => {
|
const renderContent = () => {
|
||||||
switch (contentType) {
|
switch (contentType) {
|
||||||
case 'diff':
|
case 'diff':
|
||||||
return contentProps.DiffViewer;
|
return contentProps.DiffViewer;
|
||||||
|
|
||||||
case 'markdown':
|
case 'markdown':
|
||||||
return contentProps.MarkdownComponent;
|
return contentProps.MarkdownComponent;
|
||||||
|
|
||||||
case 'file-list':
|
case 'file-list':
|
||||||
return contentProps.FileListComponent;
|
return contentProps.FileListComponent;
|
||||||
|
|
||||||
case 'todo-list':
|
case 'todo-list':
|
||||||
return contentProps.TodoListComponent;
|
return contentProps.TodoListComponent;
|
||||||
|
case 'task':
|
||||||
|
return contentProps.TaskComponent;
|
||||||
case 'text':
|
case 'text':
|
||||||
return contentProps.TextComponent;
|
return contentProps.TextComponent;
|
||||||
|
|
||||||
default:
|
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 (
|
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={`border-l-2 ${borderColor} pl-3 py-0.5 my-1 ${className}`}>
|
||||||
<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>
|
|
||||||
|
|
||||||
<CollapsibleSection
|
<CollapsibleSection
|
||||||
title={title}
|
title={title}
|
||||||
|
toolName={toolName}
|
||||||
open={defaultOpen}
|
open={defaultOpen}
|
||||||
action={action}
|
action={action}
|
||||||
className={className}
|
onTitleClick={onTitleClick}
|
||||||
>
|
>
|
||||||
{renderContent()}
|
{renderContent()}
|
||||||
|
|
||||||
{showRawParameters && rawContent && (
|
{showRawParameters && rawContent && (
|
||||||
<details className="relative mt-3 pl-6 group/raw" open={defaultOpen}>
|
<details className="relative mt-2 group/raw">
|
||||||
<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">
|
<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
|
<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"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
viewBox="0 0 24 24"
|
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>
|
</svg>
|
||||||
View raw parameters
|
raw params
|
||||||
</summary>
|
</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}
|
{rawContent}
|
||||||
</pre>
|
</pre>
|
||||||
</details>
|
</details>
|
||||||
|
|||||||
@@ -2,40 +2,58 @@ import React from 'react';
|
|||||||
|
|
||||||
interface CollapsibleSectionProps {
|
interface CollapsibleSectionProps {
|
||||||
title: string;
|
title: string;
|
||||||
|
toolName?: string;
|
||||||
open?: boolean;
|
open?: boolean;
|
||||||
action?: React.ReactNode;
|
action?: React.ReactNode;
|
||||||
|
onTitleClick?: () => void;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reusable collapsible section with consistent styling
|
* Reusable collapsible section with consistent styling
|
||||||
* Replaces repeated details/summary patterns throughout MessageComponent
|
|
||||||
*/
|
*/
|
||||||
export const CollapsibleSection: React.FC<CollapsibleSectionProps> = ({
|
export const CollapsibleSection: React.FC<CollapsibleSectionProps> = ({
|
||||||
title,
|
title,
|
||||||
|
toolName,
|
||||||
open = false,
|
open = false,
|
||||||
action,
|
action,
|
||||||
|
onTitleClick,
|
||||||
children,
|
children,
|
||||||
className = ''
|
className = ''
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<details className={`relative mt-3 group/details ${className}`} open={open}>
|
<details className={`relative 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">
|
<summary className="flex items-center gap-1.5 text-xs cursor-pointer py-0.5 select-none">
|
||||||
<svg
|
<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"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
viewBox="0 0 24 24"
|
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>
|
</svg>
|
||||||
<span className="flex items-center gap-2 flex-1">
|
{toolName && (
|
||||||
{title}
|
<span className="font-medium text-gray-500 dark:text-gray-400 flex-shrink-0">{toolName}</span>
|
||||||
</span>
|
)}
|
||||||
{action}
|
{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>
|
</summary>
|
||||||
<div className="mt-3 pl-6">
|
<div className="mt-1.5 pl-[18px]">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</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
|
* Used by: Grep/Glob results
|
||||||
*/
|
*/
|
||||||
export const FileListContent: React.FC<FileListContentProps> = ({
|
export const FileListContent: React.FC<FileListContentProps> = ({
|
||||||
@@ -20,54 +20,34 @@ export const FileListContent: React.FC<FileListContentProps> = ({
|
|||||||
onFileClick,
|
onFileClick,
|
||||||
title
|
title
|
||||||
}) => {
|
}) => {
|
||||||
const fileCount = files.length;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{title && (
|
{title && (
|
||||||
<div className="flex items-center gap-2 mb-3">
|
<div className="text-[11px] text-gray-500 dark:text-gray-400 mb-1">
|
||||||
<span className="font-medium">
|
{title}
|
||||||
{title || `Found ${fileCount} ${fileCount === 1 ? 'file' : 'files'}`}
|
|
||||||
</span>
|
|
||||||
</div>
|
</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) => {
|
{files.map((file, index) => {
|
||||||
const filePath = typeof file === 'string' ? file : file.path;
|
const filePath = typeof file === 'string' ? file : file.path;
|
||||||
const fileName = filePath.split('/').pop() || filePath;
|
const fileName = filePath.split('/').pop() || filePath;
|
||||||
const dirPath = filePath.substring(0, filePath.lastIndexOf('/'));
|
|
||||||
const handleClick = typeof file === 'string'
|
const handleClick = typeof file === 'string'
|
||||||
? () => onFileClick?.(file)
|
? () => onFileClick?.(file)
|
||||||
: file.onClick;
|
: file.onClick;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<span key={index} className="inline-flex items-center">
|
||||||
key={index}
|
<button
|
||||||
onClick={handleClick}
|
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"
|
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}
|
||||||
{/* 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">
|
{fileName}
|
||||||
<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" />
|
</button>
|
||||||
</svg>
|
{index < files.length - 1 && (
|
||||||
|
<span className="text-gray-300 dark:text-gray-600 text-[10px] ml-1">,</span>
|
||||||
{/* File path */}
|
)}
|
||||||
<div className="flex-1 min-w-0">
|
</span>
|
||||||
<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>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ interface MarkdownContentProps {
|
|||||||
*/
|
*/
|
||||||
export const MarkdownContent: React.FC<MarkdownContentProps> = ({
|
export const MarkdownContent: React.FC<MarkdownContentProps> = ({
|
||||||
content,
|
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 (
|
return (
|
||||||
<Markdown className={className}>
|
<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 (
|
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}
|
{formattedJson}
|
||||||
</pre>
|
</pre>
|
||||||
);
|
);
|
||||||
@@ -33,7 +33,7 @@ export const TextContent: React.FC<TextContentProps> = ({
|
|||||||
|
|
||||||
if (format === 'code') {
|
if (format === 'code') {
|
||||||
return (
|
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}
|
{content}
|
||||||
</pre>
|
</pre>
|
||||||
);
|
);
|
||||||
@@ -41,7 +41,7 @@ export const TextContent: React.FC<TextContentProps> = ({
|
|||||||
|
|
||||||
// Plain text
|
// Plain text
|
||||||
return (
|
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}
|
{content}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export { MarkdownContent } from './MarkdownContent';
|
export { MarkdownContent } from './MarkdownContent';
|
||||||
export { FileListContent } from './FileListContent';
|
export { FileListContent } from './FileListContent';
|
||||||
export { TodoListContent } from './TodoListContent';
|
export { TodoListContent } from './TodoListContent';
|
||||||
|
export { TaskListContent } from './TaskListContent';
|
||||||
export { TextContent } from './TextContent';
|
export { TextContent } from './TextContent';
|
||||||
|
|||||||
@@ -17,8 +17,7 @@ interface DiffViewerProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reusable diff viewer component with consistent styling
|
* Compact diff viewer — VS Code-style
|
||||||
* Replaces duplicated diff display logic in Edit, Write, and result sections
|
|
||||||
*/
|
*/
|
||||||
export const DiffViewer: React.FC<DiffViewerProps> = ({
|
export const DiffViewer: React.FC<DiffViewerProps> = ({
|
||||||
oldContent,
|
oldContent,
|
||||||
@@ -30,48 +29,48 @@ export const DiffViewer: React.FC<DiffViewerProps> = ({
|
|||||||
badgeColor = 'gray'
|
badgeColor = 'gray'
|
||||||
}) => {
|
}) => {
|
||||||
const badgeClasses = badgeColor === 'green'
|
const badgeClasses = badgeColor === 'green'
|
||||||
? 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400'
|
? 'bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400'
|
||||||
: 'bg-gray-100 dark:bg-gray-700/50 text-gray-500 dark:text-gray-400';
|
: 'bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400';
|
||||||
|
|
||||||
return (
|
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 */}
|
{/* 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 ? (
|
{onFileClick ? (
|
||||||
<button
|
<button
|
||||||
onClick={onFileClick}
|
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}
|
{filePath}
|
||||||
</button>
|
</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}
|
{filePath}
|
||||||
</span>
|
</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}
|
{badge}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Diff content */}
|
{/* Diff lines */}
|
||||||
<div className="text-xs font-mono">
|
<div className="text-[11px] font-mono leading-[18px]">
|
||||||
{createDiff(oldContent, newContent).map((diffLine, i) => (
|
{createDiff(oldContent, newContent).map((diffLine, i) => (
|
||||||
<div key={i} className="flex">
|
<div key={i} className="flex">
|
||||||
<span
|
<span
|
||||||
className={`w-8 text-center border-r ${
|
className={`w-6 text-center select-none flex-shrink-0 ${
|
||||||
diffLine.type === 'removed'
|
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-red-50 dark:bg-red-950/30 text-red-400 dark:text-red-500'
|
||||||
: 'bg-green-50 dark:bg-green-900/20 text-green-600 dark:text-green-400 border-green-200 dark:border-green-800'
|
: 'bg-green-50 dark:bg-green-950/30 text-green-400 dark:text-green-500'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{diffLine.type === 'removed' ? '-' : '+'}
|
{diffLine.type === 'removed' ? '-' : '+'}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className={`px-2 py-0.5 flex-1 whitespace-pre-wrap ${
|
className={`px-2 flex-1 whitespace-pre-wrap ${
|
||||||
diffLine.type === 'removed'
|
diffLine.type === 'removed'
|
||||||
? 'bg-red-50 dark:bg-red-900/20 text-red-800 dark:text-red-200'
|
? 'bg-red-50/50 dark:bg-red-950/20 text-red-800 dark:text-red-200'
|
||||||
: 'bg-green-50 dark:bg-green-900/20 text-green-800 dark:text-green-200'
|
: 'bg-green-50/50 dark:bg-green-950/20 text-green-800 dark:text-green-200'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{diffLine.content}
|
{diffLine.content}
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ interface FilePathButtonProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reusable clickable file path component with consistent styling
|
* Clickable file path — inline link style
|
||||||
* Used across Edit, Write, and Read tool displays
|
|
||||||
*/
|
*/
|
||||||
export const FilePathButton: React.FC<FilePathButtonProps> = ({
|
export const FilePathButton: React.FC<FilePathButtonProps> = ({
|
||||||
filePath,
|
filePath,
|
||||||
@@ -26,7 +25,7 @@ export const FilePathButton: React.FC<FilePathButtonProps> = ({
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={onClick}
|
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}
|
{displayText}
|
||||||
</button>
|
</button>
|
||||||
@@ -36,7 +35,8 @@ export const FilePathButton: React.FC<FilePathButtonProps> = ({
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={onClick}
|
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}
|
{displayText}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import React, { useState } from 'react';
|
|||||||
type ActionType = 'copy' | 'open-file' | 'jump-to-results' | 'none';
|
type ActionType = 'copy' | 'open-file' | 'jump-to-results' | 'none';
|
||||||
|
|
||||||
interface OneLineDisplayProps {
|
interface OneLineDisplayProps {
|
||||||
|
|
||||||
toolName: string;
|
toolName: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
@@ -41,15 +42,16 @@ export const OneLineDisplay: React.FC<OneLineDisplayProps> = ({
|
|||||||
colorScheme = {
|
colorScheme = {
|
||||||
primary: 'text-gray-700 dark:text-gray-300',
|
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: 'bg-gray-50/50 dark:bg-gray-800/30',
|
background: '',
|
||||||
border: 'border-blue-400 dark:border-blue-500',
|
border: 'border-gray-300 dark:border-gray-600',
|
||||||
icon: 'text-blue-500 dark:text-blue-400'
|
icon: 'text-gray-500 dark:text-gray-400'
|
||||||
},
|
},
|
||||||
resultId,
|
resultId,
|
||||||
toolResult,
|
toolResult,
|
||||||
toolId
|
toolId
|
||||||
}) => {
|
}) => {
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
const isTerminal = style === 'terminal';
|
||||||
|
|
||||||
const handleAction = () => {
|
const handleAction = () => {
|
||||||
if (action === 'copy' && value) {
|
if (action === 'copy' && value) {
|
||||||
@@ -61,109 +63,118 @@ export const OneLineDisplay: React.FC<OneLineDisplayProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderActionButton = () => {
|
const renderCopyButton = () => (
|
||||||
if (action === 'none') return null;
|
<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') {
|
// Terminal style: dark pill only around the command
|
||||||
return (
|
if (isTerminal) {
|
||||||
<button
|
return (
|
||||||
onClick={handleAction}
|
<div className="group flex items-start gap-2 my-1">
|
||||||
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 transition-colors ml-1"
|
<div className="flex items-center gap-1.5 flex-shrink-0 pt-0.5">
|
||||||
title="Copy to clipboard"
|
<svg className="w-3 h-3 text-green-500 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
aria-label="Copy to clipboard"
|
<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" />
|
||||||
>
|
|
||||||
{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" />
|
|
||||||
</svg>
|
</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>
|
||||||
|
<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
|
<a
|
||||||
href={`#tool-result-${toolId}`}
|
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">
|
<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" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,14 +12,19 @@ export interface ToolDisplayConfig {
|
|||||||
getValue?: (input: any) => string;
|
getValue?: (input: any) => string;
|
||||||
getSecondary?: (input: any) => string | undefined;
|
getSecondary?: (input: any) => string | undefined;
|
||||||
action?: 'copy' | 'open-file' | 'jump-to-results' | 'none';
|
action?: 'copy' | 'open-file' | 'jump-to-results' | 'none';
|
||||||
|
style?: string;
|
||||||
|
wrapText?: boolean;
|
||||||
colorScheme?: {
|
colorScheme?: {
|
||||||
primary?: string;
|
primary?: string;
|
||||||
secondary?: string;
|
secondary?: string;
|
||||||
|
background?: string;
|
||||||
|
border?: string;
|
||||||
|
icon?: string;
|
||||||
};
|
};
|
||||||
// Collapsible config
|
// Collapsible config
|
||||||
title?: string | ((input: any) => string);
|
title?: string | ((input: any) => string);
|
||||||
defaultOpen?: boolean;
|
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;
|
getContentProps?: (input: any, helpers?: any) => any;
|
||||||
actionButton?: 'file-button' | 'none';
|
actionButton?: 'file-button' | 'none';
|
||||||
};
|
};
|
||||||
@@ -27,8 +32,10 @@ export interface ToolDisplayConfig {
|
|||||||
hidden?: boolean;
|
hidden?: boolean;
|
||||||
hideOnSuccess?: boolean;
|
hideOnSuccess?: boolean;
|
||||||
type?: 'one-line' | 'collapsible' | 'special';
|
type?: 'one-line' | 'collapsible' | 'special';
|
||||||
|
title?: string | ((result: any) => string);
|
||||||
|
defaultOpen?: boolean;
|
||||||
// Special result handlers
|
// 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;
|
getMessage?: (result: any) => string;
|
||||||
getContentProps?: (result: any) => any;
|
getContentProps?: (result: any) => any;
|
||||||
};
|
};
|
||||||
@@ -51,14 +58,14 @@ export const TOOL_CONFIGS: Record<string, ToolDisplayConfig> = {
|
|||||||
colorScheme: {
|
colorScheme: {
|
||||||
primary: 'text-green-400 font-mono',
|
primary: 'text-green-400 font-mono',
|
||||||
secondary: 'text-gray-400',
|
secondary: 'text-gray-400',
|
||||||
background: 'bg-gray-900 dark:bg-black',
|
background: '',
|
||||||
border: 'border-green-500 dark:border-green-400',
|
border: 'border-green-500 dark:border-green-400',
|
||||||
icon: 'text-green-500 dark:text-green-400'
|
icon: 'text-green-500 dark:text-green-400'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
result: {
|
result: {
|
||||||
hideOnSuccess: true,
|
hideOnSuccess: true,
|
||||||
type: 'special' // Interactive prompts, cat -n output, etc.
|
type: 'special'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -70,29 +77,35 @@ export const TOOL_CONFIGS: Record<string, ToolDisplayConfig> = {
|
|||||||
input: {
|
input: {
|
||||||
type: 'one-line',
|
type: 'one-line',
|
||||||
label: 'Read',
|
label: 'Read',
|
||||||
getValue: (input) => input.file_path,
|
getValue: (input) => input.file_path || '',
|
||||||
action: 'open-file',
|
action: 'open-file',
|
||||||
colorScheme: {
|
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: {
|
result: {
|
||||||
hidden: true // Read results not displayed
|
hidden: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
Edit: {
|
Edit: {
|
||||||
input: {
|
input: {
|
||||||
type: 'collapsible',
|
type: 'collapsible',
|
||||||
title: 'View edit diff for',
|
title: (input) => {
|
||||||
|
const filename = input.file_path?.split('/').pop() || input.file_path || 'file';
|
||||||
|
return `${filename}`;
|
||||||
|
},
|
||||||
defaultOpen: false,
|
defaultOpen: false,
|
||||||
contentType: 'diff',
|
contentType: 'diff',
|
||||||
actionButton: 'file-button',
|
actionButton: 'none',
|
||||||
getContentProps: (input) => ({
|
getContentProps: (input) => ({
|
||||||
oldContent: input.old_string,
|
oldContent: input.old_string,
|
||||||
newContent: input.new_string,
|
newContent: input.new_string,
|
||||||
filePath: input.file_path,
|
filePath: input.file_path,
|
||||||
badge: 'Diff',
|
badge: 'Edit',
|
||||||
badgeColor: 'gray'
|
badgeColor: 'gray'
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -104,15 +117,18 @@ export const TOOL_CONFIGS: Record<string, ToolDisplayConfig> = {
|
|||||||
Write: {
|
Write: {
|
||||||
input: {
|
input: {
|
||||||
type: 'collapsible',
|
type: 'collapsible',
|
||||||
title: 'Creating new file',
|
title: (input) => {
|
||||||
|
const filename = input.file_path?.split('/').pop() || input.file_path || 'file';
|
||||||
|
return `${filename}`;
|
||||||
|
},
|
||||||
defaultOpen: false,
|
defaultOpen: false,
|
||||||
contentType: 'diff',
|
contentType: 'diff',
|
||||||
actionButton: 'file-button',
|
actionButton: 'none',
|
||||||
getContentProps: (input) => ({
|
getContentProps: (input) => ({
|
||||||
oldContent: '',
|
oldContent: '',
|
||||||
newContent: input.content,
|
newContent: input.content,
|
||||||
filePath: input.file_path,
|
filePath: input.file_path,
|
||||||
badge: 'New File',
|
badge: 'New',
|
||||||
badgeColor: 'green'
|
badgeColor: 'green'
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -124,10 +140,13 @@ export const TOOL_CONFIGS: Record<string, ToolDisplayConfig> = {
|
|||||||
ApplyPatch: {
|
ApplyPatch: {
|
||||||
input: {
|
input: {
|
||||||
type: 'collapsible',
|
type: 'collapsible',
|
||||||
title: 'View patch diff for',
|
title: (input) => {
|
||||||
|
const filename = input.file_path?.split('/').pop() || input.file_path || 'file';
|
||||||
|
return `${filename}`;
|
||||||
|
},
|
||||||
defaultOpen: false,
|
defaultOpen: false,
|
||||||
contentType: 'diff',
|
contentType: 'diff',
|
||||||
actionButton: 'file-button',
|
actionButton: 'none',
|
||||||
getContentProps: (input) => ({
|
getContentProps: (input) => ({
|
||||||
oldContent: input.old_string,
|
oldContent: input.old_string,
|
||||||
newContent: input.new_string,
|
newContent: input.new_string,
|
||||||
@@ -154,19 +173,25 @@ export const TOOL_CONFIGS: Record<string, ToolDisplayConfig> = {
|
|||||||
action: 'jump-to-results',
|
action: 'jump-to-results',
|
||||||
colorScheme: {
|
colorScheme: {
|
||||||
primary: 'text-gray-700 dark:text-gray-300',
|
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: {
|
result: {
|
||||||
type: 'collapsible',
|
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',
|
contentType: 'file-list',
|
||||||
getContentProps: (result) => {
|
getContentProps: (result) => {
|
||||||
const toolData = result.toolUseResult || {};
|
const toolData = result.toolUseResult || {};
|
||||||
return {
|
return {
|
||||||
files: toolData.filenames || [],
|
files: toolData.filenames || []
|
||||||
title: toolData.filenames ?
|
|
||||||
`Found ${toolData.numFiles || toolData.filenames.length} ${(toolData.numFiles === 1 || toolData.filenames.length === 1) ? 'file' : 'files'}`
|
|
||||||
: undefined
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -181,19 +206,25 @@ export const TOOL_CONFIGS: Record<string, ToolDisplayConfig> = {
|
|||||||
action: 'jump-to-results',
|
action: 'jump-to-results',
|
||||||
colorScheme: {
|
colorScheme: {
|
||||||
primary: 'text-gray-700 dark:text-gray-300',
|
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: {
|
result: {
|
||||||
type: 'collapsible',
|
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',
|
contentType: 'file-list',
|
||||||
getContentProps: (result) => {
|
getContentProps: (result) => {
|
||||||
const toolData = result.toolUseResult || {};
|
const toolData = result.toolUseResult || {};
|
||||||
return {
|
return {
|
||||||
files: toolData.filenames || [],
|
files: toolData.filenames || []
|
||||||
title: toolData.filenames ?
|
|
||||||
`Found ${toolData.numFiles || toolData.filenames.length} ${(toolData.numFiles === 1 || toolData.filenames.length === 1) ? 'file' : 'files'}`
|
|
||||||
: undefined
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -206,7 +237,7 @@ export const TOOL_CONFIGS: Record<string, ToolDisplayConfig> = {
|
|||||||
TodoWrite: {
|
TodoWrite: {
|
||||||
input: {
|
input: {
|
||||||
type: 'collapsible',
|
type: 'collapsible',
|
||||||
title: 'Updating Todo List',
|
title: 'Updating todo list',
|
||||||
defaultOpen: false,
|
defaultOpen: false,
|
||||||
contentType: 'todo-list',
|
contentType: 'todo-list',
|
||||||
getContentProps: (input) => ({
|
getContentProps: (input) => ({
|
||||||
@@ -216,16 +247,20 @@ export const TOOL_CONFIGS: Record<string, ToolDisplayConfig> = {
|
|||||||
result: {
|
result: {
|
||||||
type: 'collapsible',
|
type: 'collapsible',
|
||||||
contentType: 'success-message',
|
contentType: 'success-message',
|
||||||
getMessage: () => 'Todo list has been updated successfully'
|
getMessage: () => 'Todo list updated'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
TodoRead: {
|
TodoRead: {
|
||||||
input: {
|
input: {
|
||||||
type: 'one-line',
|
type: 'one-line',
|
||||||
label: 'Read todo list',
|
label: 'TodoRead',
|
||||||
getValue: () => '',
|
getValue: () => 'reading list',
|
||||||
action: 'none'
|
action: 'none',
|
||||||
|
colorScheme: {
|
||||||
|
primary: 'text-gray-500 dark:text-gray-400',
|
||||||
|
border: 'border-violet-400 dark:border-violet-500'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
result: {
|
result: {
|
||||||
type: 'collapsible',
|
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
|
// PLAN TOOLS
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -252,7 +378,37 @@ export const TOOL_CONFIGS: Record<string, ToolDisplayConfig> = {
|
|||||||
exit_plan_mode: {
|
exit_plan_mode: {
|
||||||
input: {
|
input: {
|
||||||
type: 'collapsible',
|
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,
|
defaultOpen: true,
|
||||||
contentType: 'markdown',
|
contentType: 'markdown',
|
||||||
getContentProps: (input) => ({
|
getContentProps: (input) => ({
|
||||||
@@ -285,7 +441,7 @@ export const TOOL_CONFIGS: Record<string, ToolDisplayConfig> = {
|
|||||||
Default: {
|
Default: {
|
||||||
input: {
|
input: {
|
||||||
type: 'collapsible',
|
type: 'collapsible',
|
||||||
title: 'View input parameters',
|
title: 'Parameters',
|
||||||
defaultOpen: false,
|
defaultOpen: false,
|
||||||
contentType: 'text',
|
contentType: 'text',
|
||||||
getContentProps: (input) => ({
|
getContentProps: (input) => ({
|
||||||
|
|||||||
Reference in New Issue
Block a user