diff --git a/web/src/components/Chat/tools/BashToolBlock.tsx b/web/src/components/Chat/tools/BashToolBlock.tsx index 1eae474..bcd61df 100644 --- a/web/src/components/Chat/tools/BashToolBlock.tsx +++ b/web/src/components/Chat/tools/BashToolBlock.tsx @@ -1,53 +1,44 @@ -import { useState } from 'react'; -import { ChevronRight, ChevronDown, Terminal, Loader2 } from 'lucide-react'; +import { Terminal, Loader2 } from 'lucide-react'; import type { ToolCallBlockData } from '../../../types/chat'; +import { CollapsibleToolBlock } from './CollapsibleToolBlock'; export function BashToolBlock({ block }: { block: ToolCallBlockData }) { - const [expanded, setExpanded] = useState(false); const isRunning = block.status === 'running'; const command = String(block.input.command || ''); const truncatedCmd = command.length > 80 ? command.slice(0, 80) + '...' : command; return ( -
- - - {expanded && ( -
- {/* Full command */} - {command.length > 80 && ( -
-
{command}
-
- )} + )} - {/* Output */} - {block.result !== undefined && ( -
-              {block.result}
-            
- )} + {/* Output */} + {block.result !== undefined && ( +
+          {block.result}
+        
+ )} - {isRunning && block.result === undefined && ( -
- Running... -
- )} + {isRunning && block.result === undefined && ( +
+ Running...
)} -
+ ); } diff --git a/web/src/components/Chat/tools/CollapsibleToolBlock.tsx b/web/src/components/Chat/tools/CollapsibleToolBlock.tsx new file mode 100644 index 0000000..333274f --- /dev/null +++ b/web/src/components/Chat/tools/CollapsibleToolBlock.tsx @@ -0,0 +1,98 @@ +import { useState, type ReactNode } from 'react'; +import { ChevronRight, ChevronDown, Loader2 } from 'lucide-react'; +import type { LucideIcon } from 'lucide-react'; + +interface CollapsibleToolBlockProps { + /** Whether the tool is currently running. */ + isRunning: boolean; + /** Whether the tool resulted in an error. */ + isError?: boolean; + /** Primary icon shown in collapsed state. */ + icon: LucideIcon; + /** CSS class for the icon (color). Falls back to text-[#888]. */ + iconClassName?: string; + /** Short label next to the icon (e.g. "List Tasks"). */ + label: string; + /** CSS class for the label text. Falls back to text-[#ccc]. */ + labelClassName?: string; + /** Extra elements rendered between label and chevron. */ + headerExtra?: ReactNode; + /** Border + background theme. Defaults to neutral gray. */ + theme?: 'default' | 'amber' | 'purple' | 'cyan'; + /** Start expanded? Defaults to false. */ + defaultExpanded?: boolean; + /** Content shown when expanded. */ + children: ReactNode; +} + +const THEMES = { + default: { + border: 'border-[#2a2a2a]', + bg: 'bg-[#141414]', + hover: 'hover:bg-[#1a1a1a]', + divider: 'border-[#2a2a2a]', + spinner: 'text-[#6366f1]', + }, + amber: { + border: 'border-amber-500/20', + bg: 'bg-[#141411]', + hover: 'hover:bg-[#1a1a18]', + divider: 'border-amber-500/10', + spinner: 'text-amber-400', + }, + purple: { + border: 'border-purple-500/20', + bg: 'bg-[#141418]', + hover: 'hover:bg-[#1a1a20]', + divider: 'border-purple-500/10', + spinner: 'text-purple-400', + }, + cyan: { + border: 'border-cyan-500/20', + bg: 'bg-[#141416]', + hover: 'hover:bg-[#1a1a1e]', + divider: 'border-cyan-500/10', + spinner: 'text-cyan-400', + }, +} as const; + +export function CollapsibleToolBlock({ + isRunning, + isError, + icon: Icon, + iconClassName = 'text-[#888]', + label, + labelClassName = 'text-[#ccc]', + headerExtra, + theme = 'default', + defaultExpanded = false, + children, +}: CollapsibleToolBlockProps) { + const [expanded, setExpanded] = useState(defaultExpanded); + const t = THEMES[theme]; + + return ( +
+ + + {expanded && ( +
+ {children} +
+ )} +
+ ); +} diff --git a/web/src/components/Chat/tools/EditToolBlock.tsx b/web/src/components/Chat/tools/EditToolBlock.tsx index 167a107..72a80b6 100644 --- a/web/src/components/Chat/tools/EditToolBlock.tsx +++ b/web/src/components/Chat/tools/EditToolBlock.tsx @@ -1,9 +1,8 @@ -import { useState } from 'react'; -import { ChevronRight, ChevronDown, FileEdit, Loader2 } from 'lucide-react'; +import { FileEdit, Loader2 } from 'lucide-react'; import type { ToolCallBlockData } from '../../../types/chat'; +import { CollapsibleToolBlock } from './CollapsibleToolBlock'; export function EditToolBlock({ block }: { block: ToolCallBlockData }) { - const [expanded, setExpanded] = useState(false); const isRunning = block.status === 'running'; const filePath = String(block.input.file_path || ''); const oldString = String(block.input.old_string || ''); @@ -13,52 +12,43 @@ export function EditToolBlock({ block }: { block: ToolCallBlockData }) { const newLines = newString.split('\n'); return ( -
- - - {expanded && ( -
- {/* Diff view */} -
- {oldLines.map((line, i) => ( -
- -{line} -
- ))} - {newLines.map((line, i) => ( -
- +{line} -
- ))} + } + > + {/* Diff view */} +
+ {oldLines.map((line, i) => ( +
+ -{line} +
+ ))} + {newLines.map((line, i) => ( +
+ +{line}
+ ))} +
- {/* Error */} - {block.isError && block.result && ( -
-
{block.result}
-
- )} + {/* Error */} + {block.isError && block.result && ( +
+
{block.result}
+
+ )} - {isRunning && block.result === undefined && ( -
- Applying edit... -
- )} + {isRunning && block.result === undefined && ( +
+ Applying edit...
)} -
+ ); } diff --git a/web/src/components/Chat/tools/FileToolBlock.tsx b/web/src/components/Chat/tools/FileToolBlock.tsx index 5cf3697..f285e28 100644 --- a/web/src/components/Chat/tools/FileToolBlock.tsx +++ b/web/src/components/Chat/tools/FileToolBlock.tsx @@ -1,9 +1,8 @@ -import { useState } from 'react'; -import { ChevronRight, ChevronDown, FileText, FilePlus, Loader2 } from 'lucide-react'; +import { FileText, FilePlus, Loader2 } from 'lucide-react'; import type { ToolCallBlockData } from '../../../types/chat'; +import { CollapsibleToolBlock } from './CollapsibleToolBlock'; export function FileToolBlock({ block }: { block: ToolCallBlockData }) { - const [expanded, setExpanded] = useState(false); const isRunning = block.status === 'running'; const filePath = String(block.input.file_path || block.input.path || ''); const isWrite = block.tool === 'Write'; @@ -13,40 +12,31 @@ export function FileToolBlock({ block }: { block: ToolCallBlockData }) { const lineCount = block.result ? block.result.split('\n').length : null; return ( -
- - - {expanded && ( -
- {block.result !== undefined && ( -
-              {block.result}
-            
- )} + } + > + {block.result !== undefined && ( +
+          {block.result}
+        
+ )} - {isRunning && block.result === undefined && ( -
- {isWrite ? 'Writing...' : 'Reading...'} -
- )} + {isRunning && block.result === undefined && ( +
+ {isWrite ? 'Writing...' : 'Reading...'}
)} -
+ ); } diff --git a/web/src/components/Chat/tools/MemoryToolBlock.tsx b/web/src/components/Chat/tools/MemoryToolBlock.tsx index 080c930..d0699b5 100644 --- a/web/src/components/Chat/tools/MemoryToolBlock.tsx +++ b/web/src/components/Chat/tools/MemoryToolBlock.tsx @@ -1,22 +1,7 @@ -import { useState } from 'react'; -import { ChevronRight, ChevronDown, Brain, BookOpen, Search, Loader2 } from 'lucide-react'; +import { Brain, BookOpen, Search, Loader2 } from 'lucide-react'; import type { ToolCallBlockData } from '../../../types/chat'; - -/** Extract readable text from MCP content blocks or plain text. */ -function extractText(result: string): string { - try { - const parsed = JSON.parse(result); - if (Array.isArray(parsed)) { - return parsed - .filter((b: any) => b.type === 'text') - .map((b: any) => b.text) - .join('\n'); - } - } catch { - // Not JSON — return as-is - } - return result; -} +import { extractText } from '../../../utils/extractResultText'; +import { CollapsibleToolBlock } from './CollapsibleToolBlock'; interface MemoryItem { type: string; // event, profile, knowledge, behavior @@ -45,7 +30,6 @@ const TYPE_COLORS: Record = { }; export function MemoryToolBlock({ block }: { block: ToolCallBlockData }) { - const [expanded, setExpanded] = useState(false); const isRunning = block.status === 'running'; const isRecall = block.tool.includes('recall'); @@ -75,80 +59,72 @@ export function MemoryToolBlock({ block }: { block: ToolCallBlockData }) { const count = countMatch ? countMatch[1] : memoryItems.length > 0 ? String(memoryItems.length) : null; return ( -
- - - {expanded && ( -
- {/* Memorize: show what was memorized */} - {isMemorize && query && ( -
-
- - Memorized -
-

{String(query)}

- {block.input.memory_type ? ( - - {String(block.input.memory_type)} - - ) : null} -
- )} + )} - {/* Recall / History: show parsed memory items */} - {(isRecall || isHistory) && memoryItems.length > 0 ? ( -
- {memoryItems.map((item, i) => ( -
- - {item.type} - - {item.text} -
- ))} + {/* Recall / History: show parsed memory items */} + {(isRecall || isHistory) && memoryItems.length > 0 ? ( +
+ {memoryItems.map((item, i) => ( +
+ + {item.type} + + {item.text}
- ) : resultText && !isMemorize ? ( -
-              {resultText}
-            
- ) : null} + ))} +
+ ) : resultText && !isMemorize ? ( +
+          {resultText}
+        
+ ) : null} - {/* Success/error feedback for memorize */} - {isMemorize && resultText && !block.isError && ( -
- Saved to memory -
- )} - {block.isError && resultText && ( -
-              {resultText}
-            
- )} + {/* Success/error feedback for memorize */} + {isMemorize && resultText && !block.isError && ( +
+ Saved to memory +
+ )} + {block.isError && resultText && ( +
+          {resultText}
+        
+ )} - {isRunning && block.result === undefined && ( -
- {isRecall || isHistory ? 'Searching...' : 'Saving...'} -
- )} + {isRunning && block.result === undefined && ( +
+ {isRecall || isHistory ? 'Searching...' : 'Saving...'}
)} -
+ ); } diff --git a/web/src/components/Chat/tools/NotificationToolBlock.tsx b/web/src/components/Chat/tools/NotificationToolBlock.tsx index 20e79b1..e6fb025 100644 --- a/web/src/components/Chat/tools/NotificationToolBlock.tsx +++ b/web/src/components/Chat/tools/NotificationToolBlock.tsx @@ -1,23 +1,9 @@ -import { useState } from 'react'; -import { Bell, HelpCircle, Loader2, ChevronRight, ChevronDown } from 'lucide-react'; +import { Bell, HelpCircle, Loader2 } from 'lucide-react'; import type { ToolCallBlockData } from '../../../types/chat'; - -/** Extract readable text from MCP content blocks. */ -function extractText(result: string): string { - try { - const parsed = JSON.parse(result); - if (Array.isArray(parsed)) { - return parsed - .filter((b: any) => b.type === 'text') - .map((b: any) => b.text) - .join('\n'); - } - } catch { /* not JSON */ } - return result; -} +import { extractText } from '../../../utils/extractResultText'; +import { CollapsibleToolBlock } from './CollapsibleToolBlock'; export function NotificationToolBlock({ block }: { block: ToolCallBlockData }) { - const [expanded, setExpanded] = useState(false); const isRunning = block.status === 'running'; const isNotify = block.tool === 'notify'; const isAsk = block.tool === 'ask_user'; @@ -30,23 +16,20 @@ export function NotificationToolBlock({ block }: { block: ToolCallBlockData }) { const body = String(block.input.body || ''); const Icon = isAsk ? HelpCircle : Bell; - const iconColor = block.isError ? 'text-red-400' : isAsk ? 'text-blue-400' : 'text-amber-400'; + const iconColor = isAsk ? 'text-blue-400' : 'text-amber-400'; const label = isNotify ? 'Notify' : 'Ask User'; const resultText = block.result ? extractText(block.result) : ''; const isSent = resultText.includes('sent') || resultText.includes('Sent'); return ( -
- - - {expanded && ( -
-
- {title &&

{title}

} - {body &&

{body}

} + } + > +
+ {title &&

{title}

} + {body &&

{body}

} - {/* Options for questions */} - {isAsk && options.length > 0 && ( -
- {options.map(opt => ( - - {opt} - - ))} -
- )} + {/* Options for questions */} + {isAsk && options.length > 0 && ( +
+ {options.map(opt => ( + + {opt} + + ))}
+ )} +
- {/* Result */} - {resultText && ( -
-
-                {resultText}
-              
-
- )} + {/* Result */} + {resultText && ( +
+
+            {resultText}
+          
+
+ )} - {isRunning && block.result === undefined && ( -
- Sending... -
- )} + {isRunning && block.result === undefined && ( +
+ Sending...
)} -
+ ); } diff --git a/web/src/components/Chat/tools/PlanToolBlock.tsx b/web/src/components/Chat/tools/PlanToolBlock.tsx index 7d58dad..d8d8924 100644 --- a/web/src/components/Chat/tools/PlanToolBlock.tsx +++ b/web/src/components/Chat/tools/PlanToolBlock.tsx @@ -1,22 +1,10 @@ -import { useState } from 'react'; -import { ChevronRight, ChevronDown, Lightbulb, ListTodo, FileText, Check, X, MessageSquare, Loader2, ExternalLink } from 'lucide-react'; +import { Lightbulb, ListTodo, FileText, Check, X, MessageSquare, Loader2, ExternalLink } from 'lucide-react'; import { useNavigate } from 'react-router-dom'; import { MarkdownContent } from '../MarkdownContent'; import type { ToolCallBlockData } from '../../../types/chat'; - -/** Extract readable text from MCP content blocks. */ -function extractText(result: string): string { - try { - const parsed = JSON.parse(result); - if (Array.isArray(parsed)) { - return parsed - .filter((b: any) => b.type === 'text') - .map((b: any) => b.text) - .join('\n'); - } - } catch { /* not JSON */ } - return result; -} +import { extractText } from '../../../utils/extractResultText'; +import { PLAN_STATUS_COLORS as STATUS_COLORS } from '../../../constants/statusStyles'; +import { CollapsibleToolBlock } from './CollapsibleToolBlock'; interface ParsedPlan { status: string; @@ -44,14 +32,6 @@ function parsePlanList(text: string): ParsedPlan[] { return items; } -const STATUS_COLORS: Record = { - pending: 'bg-yellow-500/15 text-yellow-400', - approved: 'bg-green-500/15 text-green-400', - implementing: 'bg-blue-500/15 text-blue-400', - declined: 'bg-red-500/15 text-red-400', - superseded: 'bg-[#333] text-[#888]', -}; - type PlanTool = 'plan_propose' | 'plan_list' | 'plan_read' | 'plan_approve' | 'plan_decline' | 'plan_revise'; const TOOL_CONFIG: Record = { @@ -64,7 +44,6 @@ const TOOL_CONFIG: Record - - - {expanded && ( -
- - {/* ── plan_propose ── */} - {toolName === 'plan_propose' && ( -
- {block.input.content ? ( -
- {String(block.input.content).slice(0, 500)} - {String(block.input.content).length > 500 ? '...' : null} -
- ) : null} - {proposedPlanId && !block.isError && ( - - )} - {proposedPlanId && !block.isError && ( -
- Plan proposed — awaiting review -
- )} + {summary} : undefined + } + > + {/* ── plan_propose ── */} + {toolName === 'plan_propose' && ( +
+ {block.input.content ? ( +
+ {String(block.input.content).slice(0, 500)} + {String(block.input.content).length > 500 ? '...' : null}
+ ) : null} + {proposedPlanId && !block.isError && ( + )} - - {/* ── plan_list ── */} - {toolName === 'plan_list' && (planList.length > 0 ? ( -
- {planList.map((p, i) => ( -
navigate(`/plans/${p.planId}`)} - > - - {p.status} - - {p.taskTitle} - v{p.version} -
- ))} -
- ) : resultText ? ( -
-              {resultText}
-            
- ) : null)} - - {/* ── plan_read ── */} - {toolName === 'plan_read' && resultText && !block.isError && ( -
- {/* Header metadata */} - {readHeader && ( -
{readHeader}
- )} - {/* Plan content */} - {readContent && ( -
- -
- )} - {planId && ( - - )} + {proposedPlanId && !block.isError && ( +
+ Plan proposed — awaiting review
)} +
+ )} - {/* ── plan_approve ── */} - {toolName === 'plan_approve' && resultText && !block.isError && ( -
-
- - Plan approved -
- {implSessionId && ( - - )} - {planId && ( - - )} + {/* ── plan_list ── */} + {toolName === 'plan_list' && (planList.length > 0 ? ( +
+ {planList.map((p, i) => ( +
navigate(`/plans/${p.planId}`)} + > + + {p.status} + + {p.taskTitle} + v{p.version}
+ ))} +
+ ) : resultText ? ( +
+          {resultText}
+        
+ ) : null)} + + {/* ── plan_read ── */} + {toolName === 'plan_read' && resultText && !block.isError && ( +
+ {/* Header metadata */} + {readHeader && ( +
{readHeader}
)} - - {/* ── plan_decline ── */} - {toolName === 'plan_decline' && resultText && !block.isError && ( -
-
- - Plan declined -
- {feedback && ( -
-
-

{feedback}

-
- )} + {/* Plan content */} + {readContent && ( +
+
)} + {planId && ( + + )} +
+ )} - {/* ── plan_revise ── */} - {toolName === 'plan_revise' && resultText && !block.isError && ( -
-
- - Revision requested -
- {feedback && ( -
-
-

{feedback}

-
- )} -
+ {/* ── plan_approve ── */} + {toolName === 'plan_approve' && resultText && !block.isError && ( +
+
+ + Plan approved +
+ {implSessionId && ( + )} + {planId && ( + + )} +
+ )} - {/* ── Error fallback ── */} - {block.isError && resultText && ( -
-              {resultText}
-            
+ {/* ── plan_decline ── */} + {toolName === 'plan_decline' && resultText && !block.isError && ( +
+
+ + Plan declined +
+ {feedback && ( +
+
+

{feedback}

+
)} +
+ )} - {/* ── Running spinner ── */} - {isRunning && block.result === undefined && ( -
- {config.runningLabel} + {/* ── plan_revise ── */} + {toolName === 'plan_revise' && resultText && !block.isError && ( +
+
+ + Revision requested +
+ {feedback && ( +
+
+

{feedback}

)}
)} -
+ + {/* ── Error fallback ── */} + {block.isError && resultText && ( +
+          {resultText}
+        
+ )} + + {/* ── Running spinner ── */} + {isRunning && block.result === undefined && ( +
+ {config.runningLabel} +
+ )} + ); } diff --git a/web/src/components/Chat/tools/SkillToolBlock.tsx b/web/src/components/Chat/tools/SkillToolBlock.tsx index 834646e..1078ad5 100644 --- a/web/src/components/Chat/tools/SkillToolBlock.tsx +++ b/web/src/components/Chat/tools/SkillToolBlock.tsx @@ -1,20 +1,7 @@ -import { useState } from 'react'; -import { ChevronRight, ChevronDown, Sparkles, BookOpen, Play, List, Plus, Pencil, Loader2 } from 'lucide-react'; +import { Sparkles, BookOpen, Play, List, Plus, Pencil, Loader2 } from 'lucide-react'; import type { ToolCallBlockData } from '../../../types/chat'; - -/** Extract readable text from MCP content blocks. */ -function extractText(result: string): string { - try { - const parsed = JSON.parse(result); - if (Array.isArray(parsed)) { - return parsed - .filter((b: any) => b.type === 'text') - .map((b: any) => b.text) - .join('\n'); - } - } catch { /* not JSON */ } - return result; -} +import { extractText } from '../../../utils/extractResultText'; +import { CollapsibleToolBlock } from './CollapsibleToolBlock'; /** Parse skill list from result text. */ function parseSkillList(text: string): Array<{ name: string; id: string; description: string }> { @@ -29,7 +16,6 @@ function parseSkillList(text: string): Array<{ name: string; id: string; descrip } export function SkillToolBlock({ block }: { block: ToolCallBlockData }) { - const [expanded, setExpanded] = useState(false); const isRunning = block.status === 'running'; const toolName = block.tool.split('__').pop() || block.tool; @@ -68,167 +54,152 @@ export function SkillToolBlock({ block }: { block: ToolCallBlockData }) { const contentLines = resultText ? resultText.split('\n').length : 0; return ( -
- - - {expanded && ( -
- {/* Skill list */} - {skillList.length > 0 && ( -
- {skillList.map((s) => ( -
- -
-
- {s.name} - {s.id} -
-

{s.description}

-
+ } + > + {/* Skill list */} + {skillList.length > 0 && ( +
+ {skillList.map((s) => ( +
+ +
+
+ {s.name} + {s.id}
- ))} +

{s.description}

+
- )} + ))} +
+ )} - {/* Loaded skill content */} - {isGet && resultText && !block.isError && !skillList.length && ( -
- {loadedSkillTitle && ( -
- - {loadedSkillTitle} -
- )} -
-                {resultText}
-              
+ {/* Loaded skill content */} + {isGet && resultText && !block.isError && !skillList.length && ( +
+ {loadedSkillTitle && ( +
+ + {loadedSkillTitle}
)} +
+            {resultText}
+          
+
+ )} - {/* Script output */} - {isRun && resultText && !block.isError && ( -
-
Output
-
-                {resultText}
-              
-
- )} + {/* Script output */} + {isRun && resultText && !block.isError && ( +
+
Output
+
+            {resultText}
+          
+
+ )} - {/* Reference content */} - {isRef && resultText && !block.isError && ( -
- {refPath && ( -
- {skillName}/{refPath} -
- )} -
-                {resultText}
-              
+ {/* Reference content */} + {isRef && resultText && !block.isError && ( +
+ {refPath && ( +
+ {skillName}/{refPath}
)} +
+            {resultText}
+          
+
+ )} - {/* Create skill */} - {isCreate && ( -
- {skillName && ( -
- - {skillName} -
- )} - {skillDescription && ( -

{skillDescription.slice(0, 300)}

- )} - {String(block.input.content || '') && ( -
-                  {String(block.input.content).slice(0, 500)}{String(block.input.content).length > 500 ? '...' : ''}
-                
- )} - {resultText && !block.isError && ( -
- {resultText} -
- )} + {/* Create skill */} + {isCreate && ( +
+ {skillName && ( +
+ + {skillName}
)} - - {/* Update skill */} - {isUpdate && ( -
- {String(block.input.content || '') && ( -
-                  {String(block.input.content).slice(0, 800)}{String(block.input.content).length > 800 ? '\n...' : ''}
-                
- )} - {resultText && !block.isError && ( -
- {resultText} -
- )} -
+ {skillDescription && ( +

{skillDescription.slice(0, 300)}

)} - - {/* Fallback for unknown skill tools */} - {!isList && !isGet && !isRun && !isRef && !isCreate && !isUpdate && resultText && !block.isError && ( -
-              {resultText}
+          {String(block.input.content || '') && (
+            
+              {String(block.input.content).slice(0, 500)}{String(block.input.content).length > 500 ? '...' : ''}
             
)} + {resultText && !block.isError && ( +
+ {resultText} +
+ )} +
+ )} - {/* Error */} - {block.isError && resultText && ( -
-              {resultText}
+      {/* Update skill */}
+      {isUpdate && (
+        
+ {String(block.input.content || '') && ( +
+              {String(block.input.content).slice(0, 800)}{String(block.input.content).length > 800 ? '\n...' : ''}
             
)} - - {/* Running */} - {isRunning && block.result === undefined && ( -
- - {isGet ? 'Loading skill...' : isRun ? 'Running script...' : 'Working...'} + {resultText && !block.isError && ( +
+ {resultText}
)}
)} -
+ + {/* Fallback for unknown skill tools */} + {!isList && !isGet && !isRun && !isRef && !isCreate && !isUpdate && resultText && !block.isError && ( +
+          {resultText}
+        
+ )} + + {/* Error */} + {block.isError && resultText && ( +
+          {resultText}
+        
+ )} + + {/* Running */} + {isRunning && block.result === undefined && ( +
+ + {isGet ? 'Loading skill...' : isRun ? 'Running script...' : 'Working...'} +
+ )} + ); } diff --git a/web/src/components/Chat/tools/SourceToolBlock.tsx b/web/src/components/Chat/tools/SourceToolBlock.tsx index 9b5b31f..b3629ab 100644 --- a/web/src/components/Chat/tools/SourceToolBlock.tsx +++ b/web/src/components/Chat/tools/SourceToolBlock.tsx @@ -1,22 +1,7 @@ -import { useState } from 'react'; -import { ChevronRight, ChevronDown, Inbox, Radio, BookOpen, Loader2, Mail, Github, MessageCircle } from 'lucide-react'; +import { Inbox, Radio, BookOpen, Loader2, Mail, Github, MessageCircle } from 'lucide-react'; import type { ToolCallBlockData } from '../../../types/chat'; - -/** Extract readable text from MCP content blocks or plain text. */ -function extractText(result: string): string { - try { - const parsed = JSON.parse(result); - if (Array.isArray(parsed)) { - return parsed - .filter((b: any) => b.type === 'text') - .map((b: any) => b.text) - .join('\n'); - } - } catch { - // Not JSON — return as-is - } - return result; -} +import { extractText } from '../../../utils/extractResultText'; +import { CollapsibleToolBlock } from './CollapsibleToolBlock'; function sourceIcon(source: string) { const type = source.split(':')[0]; @@ -106,7 +91,6 @@ function parseSourceMessages(text: string): { messages: SourceMessage[]; message } export function SourceToolBlock({ block }: { block: ToolCallBlockData }) { - const [expanded, setExpanded] = useState(false); const isRunning = block.status === 'running'; const isList = block.tool.includes('list_sources'); @@ -148,107 +132,99 @@ export function SourceToolBlock({ block }: { block: ToolCallBlockData }) { } return ( -
- - - {expanded && ( -
- {/* list_sources: structured source list */} - {isList && sourceEntries.length > 0 && ( -
- {sourceEntries.map((entry, i) => ( -
- {sourceIcon(entry.name)} - {entry.name} - {entry.messageCount && {entry.messageCount} msgs} - {entry.unread && parseInt(entry.unread) > 0 && ( - {entry.unread} unread - )} - {entry.unread === '0' && ( - 0 unread - )} -
- ))} -
- )} - - {/* poll/read: message list */} - {(isPoll || isRead) && parsedMessages.length > 0 && ( -
- {parsedMessages.map((msg, i) => ( -
-
- {sourceIcon(msg.source)} - {msg.summary} - {msg.relativeTime} -
-
- {msg.type} - seq:{msg.seq} - {msg.time && {msg.time}} -
- {msg.content && ( -
-                      {msg.content.length > 500 ? msg.content.slice(0, 500) + '...' : msg.content}
-                    
- )} -
- ))} + } + > + {/* list_sources: structured source list */} + {isList && sourceEntries.length > 0 && ( +
+ {sourceEntries.map((entry, i) => ( +
+ {sourceIcon(entry.name)} + {entry.name} + {entry.messageCount && {entry.messageCount} msgs} + {entry.unread && parseInt(entry.unread) > 0 && ( + {entry.unread} unread + )} + {entry.unread === '0' && ( + 0 unread + )}
- )} + ))} +
+ )} - {/* No messages state */} - {isNoMessages && !isList && ( -
- No new messages -
- )} - - {/* Fallback: raw text for unparsed results */} - {!isList && parsedMessages.length === 0 && !isNoMessages && resultText && ( -
-              {resultText}
-            
- )} - - {/* list_sources fallback */} - {isList && sourceEntries.length === 0 && resultText && ( -
-              {resultText}
-            
- )} - - {/* Error */} - {block.isError && resultText && ( -
-              {resultText}
-            
- )} - - {/* Running state */} - {isRunning && block.result === undefined && ( -
- {isPoll ? 'Polling...' : isList ? 'Loading sources...' : 'Browsing...'} + {/* poll/read: message list */} + {(isPoll || isRead) && parsedMessages.length > 0 && ( +
+ {parsedMessages.map((msg, i) => ( +
+
+ {sourceIcon(msg.source)} + {msg.summary} + {msg.relativeTime} +
+
+ {msg.type} + seq:{msg.seq} + {msg.time && {msg.time}} +
+ {msg.content && ( +
+                  {msg.content.length > 500 ? msg.content.slice(0, 500) + '...' : msg.content}
+                
+ )}
- )} + ))} +
+ )} + + {/* No messages state */} + {isNoMessages && !isList && ( +
+ No new messages +
+ )} + + {/* Fallback: raw text for unparsed results */} + {!isList && parsedMessages.length === 0 && !isNoMessages && resultText && ( +
+          {resultText}
+        
+ )} + + {/* list_sources fallback */} + {isList && sourceEntries.length === 0 && resultText && ( +
+          {resultText}
+        
+ )} + + {/* Error */} + {block.isError && resultText && ( +
+          {resultText}
+        
+ )} + + {/* Running state */} + {isRunning && block.result === undefined && ( +
+ {isPoll ? 'Polling...' : isList ? 'Loading sources...' : 'Browsing...'}
)} -
+ ); } diff --git a/web/src/components/Chat/tools/TaskToolBlock.tsx b/web/src/components/Chat/tools/TaskToolBlock.tsx index d5aee3a..201528d 100644 --- a/web/src/components/Chat/tools/TaskToolBlock.tsx +++ b/web/src/components/Chat/tools/TaskToolBlock.tsx @@ -1,29 +1,8 @@ -import { useState } from 'react'; -import { ChevronRight, ChevronDown, CheckSquare, ListTodo, Plus, CheckCircle, Pencil, FileText, Loader2 } from 'lucide-react'; +import { CheckSquare, ListTodo, Plus, CheckCircle, Pencil, FileText, Loader2 } from 'lucide-react'; import type { ToolCallBlockData } from '../../../types/chat'; - -const STATUS_COLORS: Record = { - pending: 'bg-yellow-500/15 text-yellow-400', - 'in-progress': 'bg-blue-500/15 text-blue-400', - 'in_progress': 'bg-blue-500/15 text-blue-400', - done: 'bg-green-500/15 text-green-400', - completed: 'bg-green-500/15 text-green-400', - deferred: 'bg-[#333] text-[#888]', -}; - -/** Extract readable text from MCP content blocks. */ -function extractText(result: string): string { - try { - const parsed = JSON.parse(result); - if (Array.isArray(parsed)) { - return parsed - .filter((b: any) => b.type === 'text') - .map((b: any) => b.text) - .join('\n'); - } - } catch { /* not JSON */ } - return result; -} +import { extractText } from '../../../utils/extractResultText'; +import { TASK_STATUS_COLORS as STATUS_COLORS } from '../../../constants/statusStyles'; +import { CollapsibleToolBlock } from './CollapsibleToolBlock'; interface ParsedTask { title: string; @@ -60,7 +39,6 @@ function parseTaskList(text: string): ParsedTask[] { } export function TaskToolBlock({ block }: { block: ToolCallBlockData }) { - const [expanded, setExpanded] = useState(false); const isRunning = block.status === 'running'; const toolName = block.tool.split('__').pop() || block.tool; @@ -86,16 +64,13 @@ export function TaskToolBlock({ block }: { block: ToolCallBlockData }) { const taskList = isList ? parseTaskList(resultText) : []; return ( -
- - - {expanded && ( -
- {/* Create: show what was created */} - {isCreate && title && ( -
-
- - {title} -
- {block.input.content ? ( -

{String(block.input.content).slice(0, 200)}

- ) : null} - {block.input.deadline ? ( -

Deadline: {String(block.input.deadline)}

- ) : null} -
- )} - - {/* Done/Update: show status change */} - {(isDone || isUpdate) && ( -
- {block.input.task_id ? String(block.input.task_id) : title} - {block.input.note ?

{String(block.input.note)}

: null} -
- )} + )} - {/* Task list */} - {taskList.length > 0 ? ( -
- {taskList.map((t, i) => ( -
- - {t.status} - - {t.title} - {t.deadline && {t.deadline}} -
- ))} -
- ) : resultText && !isCreate && !isDone && !isUpdate ? ( -
-              {resultText}
-            
- ) : null} + {/* Done/Update: show status change */} + {(isDone || isUpdate) && ( +
+ {block.input.task_id ? String(block.input.task_id) : title} + {block.input.note ?

{String(block.input.note)}

: null} +
+ )} - {/* Success message */} - {(isCreate || isDone) && resultText && !block.isError && !taskList.length && ( -
- {isDone ? 'Task completed' : 'Task created'} + {/* Task list */} + {taskList.length > 0 ? ( +
+ {taskList.map((t, i) => ( +
+ + {t.status} + + {t.title} + {t.deadline && {t.deadline}}
- )} + ))} +
+ ) : resultText && !isCreate && !isDone && !isUpdate ? ( +
+          {resultText}
+        
+ ) : null} + + {/* Success message */} + {(isCreate || isDone) && resultText && !block.isError && !taskList.length && ( +
+ {isDone ? 'Task completed' : 'Task created'} +
+ )} - {block.isError && resultText && ( -
-              {resultText}
-            
- )} + {block.isError && resultText && ( +
+          {resultText}
+        
+ )} - {isRunning && block.result === undefined && ( -
- Working... -
- )} + {isRunning && block.result === undefined && ( +
+ Working...
)} -
+ ); } diff --git a/web/src/components/Tasks/TaskCard.tsx b/web/src/components/Tasks/TaskCard.tsx index c3e4797..a947131 100644 --- a/web/src/components/Tasks/TaskCard.tsx +++ b/web/src/components/Tasks/TaskCard.tsx @@ -1,13 +1,7 @@ import { useNavigate } from 'react-router-dom'; import { Calendar, ExternalLink } from 'lucide-react'; import type { Task } from '../../stores/taskStore'; - -const STATUS_STYLES: Record = { - pending: 'bg-yellow-400/10 text-yellow-400 border-yellow-400/20', - in_progress: 'bg-blue-400/10 text-blue-400 border-blue-400/20', - done: 'bg-emerald-400/10 text-emerald-400 border-emerald-400/20', - deferred: 'bg-[#333]/50 text-[#888] border-[#333]', -}; +import { TASK_STATUS_STYLES as STATUS_STYLES } from '../../constants/statusStyles'; export function TaskCard({ task, onStatusChange }: { task: Task; diff --git a/web/src/components/Tasks/TaskList.tsx b/web/src/components/Tasks/TaskList.tsx index fa632fe..18052e5 100644 --- a/web/src/components/Tasks/TaskList.tsx +++ b/web/src/components/Tasks/TaskList.tsx @@ -10,12 +10,7 @@ interface Task { created_at: string; } -const STATUS_COLORS: Record = { - pending: 'text-yellow-400', - in_progress: 'text-blue-400', - done: 'text-green-400', - deferred: 'text-[#888]', -}; +import { TASK_STATUS_TEXT_COLORS as STATUS_COLORS } from '../../constants/statusStyles'; export function TaskList() { const [tasks, setTasks] = useState([]); diff --git a/web/src/constants/statusStyles.ts b/web/src/constants/statusStyles.ts new file mode 100644 index 0000000..23894df --- /dev/null +++ b/web/src/constants/statusStyles.ts @@ -0,0 +1,58 @@ +/** Compact badge style — no border, used inside tool blocks. */ +export const TASK_STATUS_COLORS: Record = { + pending: 'bg-yellow-500/15 text-yellow-400', + 'in-progress': 'bg-blue-500/15 text-blue-400', + 'in_progress': 'bg-blue-500/15 text-blue-400', + done: 'bg-green-500/15 text-green-400', + completed: 'bg-green-500/15 text-green-400', + deferred: 'bg-[#333] text-[#888]', +}; + +/** Bordered pill style — used on pages and cards. */ +export const TASK_STATUS_STYLES: Record = { + pending: 'bg-yellow-400/10 text-yellow-400 border-yellow-400/20', + in_progress: 'bg-blue-400/10 text-blue-400 border-blue-400/20', + done: 'bg-emerald-400/10 text-emerald-400 border-emerald-400/20', + deferred: 'bg-[#333]/50 text-[#888] border-[#333]', +}; + +/** Compact badge style for plan statuses in tool blocks. */ +export const PLAN_STATUS_COLORS: Record = { + pending: 'bg-yellow-500/15 text-yellow-400', + approved: 'bg-green-500/15 text-green-400', + implementing: 'bg-blue-500/15 text-blue-400', + declined: 'bg-red-500/15 text-red-400', + superseded: 'bg-[#333] text-[#888]', +}; + +/** Bordered pill style for plan statuses on pages. */ +export const PLAN_STATUS_STYLES: Record = { + pending: 'bg-yellow-400/10 text-yellow-400 border-yellow-400/20', + approved: 'bg-emerald-400/10 text-emerald-400 border-emerald-400/20', + implementing: 'bg-blue-400/10 text-blue-400 border-blue-400/20', + declined: 'bg-red-400/10 text-red-400 border-red-400/20', + superseded: 'bg-[#333]/50 text-[#888] border-[#333]', + failed: 'bg-red-400/10 text-red-400 border-red-400/20', +}; + +/** Plan type styles (skill-create, skill-update). */ +export const PLAN_TYPE_STYLES: Record = { + 'skill-create': { label: 'Skill', className: 'bg-purple-400/10 text-purple-400 border-purple-400/20' }, + 'skill-update': { label: 'Skill Update', className: 'bg-purple-400/10 text-purple-300 border-purple-400/20' }, +}; + +/** Bordered pill style for notification statuses. */ +export const NOTIFICATION_STATUS_STYLES: Record = { + pending: 'bg-yellow-400/10 text-yellow-400 border-yellow-400/20', + answered: 'bg-emerald-400/10 text-emerald-400 border-emerald-400/20', + expired: 'bg-[#333]/50 text-[#888] border-[#333]', + dismissed: 'bg-[#333]/50 text-[#666] border-[#333]', +}; + +/** Text-only status colors for inline use (e.g. task lists). */ +export const TASK_STATUS_TEXT_COLORS: Record = { + pending: 'text-yellow-400', + in_progress: 'text-blue-400', + done: 'text-green-400', + deferred: 'text-[#888]', +}; diff --git a/web/src/pages/NotificationsPage.tsx b/web/src/pages/NotificationsPage.tsx index e9f12c8..5386003 100644 --- a/web/src/pages/NotificationsPage.tsx +++ b/web/src/pages/NotificationsPage.tsx @@ -2,13 +2,7 @@ import { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { Bell, X, CheckCheck, EyeOff } from 'lucide-react'; import { useNotificationStore, type Notification } from '../stores/notificationStore'; - -const STATUS_STYLES: Record = { - pending: 'bg-yellow-400/10 text-yellow-400 border-yellow-400/20', - answered: 'bg-emerald-400/10 text-emerald-400 border-emerald-400/20', - expired: 'bg-[#333]/50 text-[#888] border-[#333]', - dismissed: 'bg-[#333]/50 text-[#666] border-[#333]', -}; +import { NOTIFICATION_STATUS_STYLES as STATUS_STYLES } from '../constants/statusStyles'; const PRIORITY_DOTS: Record = { urgent: 'bg-red-500', diff --git a/web/src/pages/PlanDetailPage.tsx b/web/src/pages/PlanDetailPage.tsx index c02d7d8..68ea96d 100644 --- a/web/src/pages/PlanDetailPage.tsx +++ b/web/src/pages/PlanDetailPage.tsx @@ -4,20 +4,7 @@ import { ArrowLeft, Check, X, MessageSquare, ExternalLink, Users, ChevronDown } import { usePlanStore } from '../stores/planStore'; import { MarkdownContent } from '../components/Chat/MarkdownContent'; import { api } from '../api/client'; - -const STATUS_STYLES: Record = { - pending: 'bg-yellow-400/10 text-yellow-400 border-yellow-400/20', - approved: 'bg-emerald-400/10 text-emerald-400 border-emerald-400/20', - implementing: 'bg-blue-400/10 text-blue-400 border-blue-400/20', - declined: 'bg-red-400/10 text-red-400 border-red-400/20', - superseded: 'bg-[#333]/50 text-[#888] border-[#333]', - failed: 'bg-red-400/10 text-red-400 border-red-400/20', -}; - -const TYPE_STYLES: Record = { - 'skill-create': { label: 'Skill', className: 'bg-purple-400/10 text-purple-400 border-purple-400/20' }, - 'skill-update': { label: 'Skill Update', className: 'bg-purple-400/10 text-purple-300 border-purple-400/20' }, -}; +import { PLAN_STATUS_STYLES as STATUS_STYLES, PLAN_TYPE_STYLES as TYPE_STYLES } from '../constants/statusStyles'; interface HoaStatus { enabled: boolean; diff --git a/web/src/pages/PlansPage.tsx b/web/src/pages/PlansPage.tsx index f265977..64a4495 100644 --- a/web/src/pages/PlansPage.tsx +++ b/web/src/pages/PlansPage.tsx @@ -2,20 +2,7 @@ import { useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { Lightbulb } from 'lucide-react'; import { usePlanStore, type Plan } from '../stores/planStore'; - -const STATUS_STYLES: Record = { - pending: 'bg-yellow-400/10 text-yellow-400 border-yellow-400/20', - approved: 'bg-emerald-400/10 text-emerald-400 border-emerald-400/20', - implementing: 'bg-blue-400/10 text-blue-400 border-blue-400/20', - declined: 'bg-red-400/10 text-red-400 border-red-400/20', - superseded: 'bg-[#333]/50 text-[#888] border-[#333]', - failed: 'bg-red-400/10 text-red-400 border-red-400/20', -}; - -const TYPE_STYLES: Record = { - 'skill-create': { label: 'Skill', className: 'bg-purple-400/10 text-purple-400 border-purple-400/20' }, - 'skill-update': { label: 'Skill Update', className: 'bg-purple-400/10 text-purple-300 border-purple-400/20' }, -}; +import { PLAN_STATUS_STYLES as STATUS_STYLES, PLAN_TYPE_STYLES as TYPE_STYLES } from '../constants/statusStyles'; const FILTERS = [ { label: 'All', value: '' }, diff --git a/web/src/pages/TaskDetailPage.tsx b/web/src/pages/TaskDetailPage.tsx index b883500..b3a89fb 100644 --- a/web/src/pages/TaskDetailPage.tsx +++ b/web/src/pages/TaskDetailPage.tsx @@ -3,13 +3,7 @@ import { useParams, useNavigate } from 'react-router-dom'; import { ArrowLeft, Edit3, Eye, Save, Calendar, ExternalLink } from 'lucide-react'; import { useTaskStore } from '../stores/taskStore'; import { MarkdownContent } from '../components/Chat/MarkdownContent'; - -const STATUS_STYLES: Record = { - pending: 'bg-yellow-400/10 text-yellow-400 border-yellow-400/20', - in_progress: 'bg-blue-400/10 text-blue-400 border-blue-400/20', - done: 'bg-emerald-400/10 text-emerald-400 border-emerald-400/20', - deferred: 'bg-[#333]/50 text-[#888] border-[#333]', -}; +import { TASK_STATUS_STYLES as STATUS_STYLES } from '../constants/statusStyles'; export function TaskDetailPage() { const { taskId } = useParams<{ taskId: string }>(); diff --git a/web/src/utils/extractResultText.ts b/web/src/utils/extractResultText.ts index 0af2314..2d82244 100644 --- a/web/src/utils/extractResultText.ts +++ b/web/src/utils/extractResultText.ts @@ -1,4 +1,18 @@ -/** Extract readable text from MCP content blocks (used by SubagentToolBlock and chatStore). */ +/** Extract readable text from MCP content blocks. */ +export function extractText(result: string): string { + try { + const parsed = JSON.parse(result); + if (Array.isArray(parsed)) { + return parsed + .filter((b: any) => b.type === 'text') + .map((b: any) => b.text) + .join('\n'); + } + } catch { /* not JSON */ } + return result; +} + +/** Extract readable text from MCP content blocks, filtering agent metadata (used by SubagentToolBlock and chatStore). */ export function extractResultText(result: string): string { try { const parsed = JSON.parse(result);