diff --git a/src/components/ToolWidgets.new.tsx b/src/components/ToolWidgets.new.tsx new file mode 100644 index 0000000..01ad1c9 --- /dev/null +++ b/src/components/ToolWidgets.new.tsx @@ -0,0 +1,4 @@ +// This file re-exports all widgets from the widgets directory +// It maintains backward compatibility with the original ToolWidgets.tsx + +export * from './widgets'; \ No newline at end of file diff --git a/src/components/widgets/BashWidget.tsx b/src/components/widgets/BashWidget.tsx new file mode 100644 index 0000000..02c2551 --- /dev/null +++ b/src/components/widgets/BashWidget.tsx @@ -0,0 +1,71 @@ +import React from "react"; +import { Terminal, ChevronRight } from "lucide-react"; +import { cn } from "@/lib/utils"; + +interface BashWidgetProps { + command: string; + description?: string; + result?: any; +} + +export const BashWidget: React.FC = ({ command, description, result }) => { + // Extract result content if available + let resultContent = ''; + let isError = false; + + if (result) { + isError = result.is_error || false; + if (typeof result.content === 'string') { + resultContent = result.content; + } else if (result.content && typeof result.content === 'object') { + if (result.content.text) { + resultContent = result.content.text; + } else if (Array.isArray(result.content)) { + resultContent = result.content + .map((c: any) => (typeof c === 'string' ? c : c.text || JSON.stringify(c))) + .join('\n'); + } else { + resultContent = JSON.stringify(result.content, null, 2); + } + } + } + + return ( +
+
+ + Terminal + {description && ( + <> + + {description} + + )} + {/* Show loading indicator when no result yet */} + {!result && ( +
+
+ Running... +
+ )} +
+
+ + $ {command} + + + {/* Show result if available */} + {result && ( +
+ {resultContent || (isError ? "Command failed" : "Command completed")} +
+ )} +
+
+ ); +}; \ No newline at end of file diff --git a/src/components/widgets/LSWidget.tsx b/src/components/widgets/LSWidget.tsx new file mode 100644 index 0000000..f19d747 --- /dev/null +++ b/src/components/widgets/LSWidget.tsx @@ -0,0 +1,229 @@ +import React, { useState } from "react"; +import { FolderOpen, Folder, FileCode, FileText, Terminal, ChevronRight } from "lucide-react"; +import { cn } from "@/lib/utils"; + +interface LSWidgetProps { + path: string; + result?: any; +} + +export const LSWidget: React.FC = ({ path, result }) => { + // If we have a result, show it using the LSResultWidget + if (result) { + let resultContent = ''; + if (typeof result.content === 'string') { + resultContent = result.content; + } else if (result.content && typeof result.content === 'object') { + if (result.content.text) { + resultContent = result.content.text; + } else if (Array.isArray(result.content)) { + resultContent = result.content + .map((c: any) => (typeof c === 'string' ? c : c.text || JSON.stringify(c))) + .join('\n'); + } else { + resultContent = JSON.stringify(result.content, null, 2); + } + } + + return ( +
+
+ + Directory contents for: + + {path} + +
+ {resultContent && } +
+ ); + } + + return ( +
+ + Listing directory: + + {path} + + {!result && ( +
+
+ Loading... +
+ )} +
+ ); +}; + +interface LSResultWidgetProps { + content: string; +} + +export const LSResultWidget: React.FC = ({ content }) => { + const [expandedDirs, setExpandedDirs] = useState>(new Set()); + + // Parse the directory tree structure + const parseDirectoryTree = (rawContent: string) => { + const lines = rawContent.split('\n'); + const entries: Array<{ + path: string; + name: string; + type: 'file' | 'directory'; + level: number; + }> = []; + + let currentPath: string[] = []; + + for (const line of lines) { + // Skip NOTE section and everything after it + if (line.startsWith('NOTE:')) { + break; + } + + // Skip empty lines + if (!line.trim()) continue; + + // Calculate indentation level + const indent = line.match(/^(\s*)/)?.[1] || ''; + const level = Math.floor(indent.length / 2); + + // Extract the entry name + const entryMatch = line.match(/^\s*-\s+(.+?)(\/$)?$/); + if (!entryMatch) continue; + + const fullName = entryMatch[1]; + const isDirectory = line.trim().endsWith('/'); + const name = isDirectory ? fullName : fullName; + + // Update current path based on level + currentPath = currentPath.slice(0, level); + currentPath.push(name); + + entries.push({ + path: currentPath.join('/'), + name, + type: isDirectory ? 'directory' : 'file', + level, + }); + } + + return entries; + }; + + const entries = parseDirectoryTree(content); + + const toggleDirectory = (path: string) => { + setExpandedDirs(prev => { + const next = new Set(prev); + if (next.has(path)) { + next.delete(path); + } else { + next.add(path); + } + return next; + }); + }; + + // Group entries by parent for collapsible display + const getChildren = (parentPath: string, parentLevel: number) => { + return entries.filter(e => { + if (e.level !== parentLevel + 1) return false; + const parentParts = parentPath.split('/').filter(Boolean); + const entryParts = e.path.split('/').filter(Boolean); + + // Check if this entry is a direct child of the parent + if (entryParts.length !== parentParts.length + 1) return false; + + // Check if all parent parts match + for (let i = 0; i < parentParts.length; i++) { + if (parentParts[i] !== entryParts[i]) return false; + } + + return true; + }); + }; + + const renderEntry = (entry: typeof entries[0], isRoot = false) => { + const hasChildren = entry.type === 'directory' && + entries.some(e => e.path.startsWith(entry.path + '/') && e.level === entry.level + 1); + const isExpanded = expandedDirs.has(entry.path) || isRoot; + + const getIcon = () => { + if (entry.type === 'directory') { + return isExpanded ? + : + ; + } + + // File type icons based on extension + const ext = entry.name.split('.').pop()?.toLowerCase(); + switch (ext) { + case 'rs': + return ; + case 'toml': + case 'yaml': + case 'yml': + case 'json': + return ; + case 'md': + return ; + case 'js': + case 'jsx': + case 'ts': + case 'tsx': + return ; + case 'py': + return ; + case 'go': + return ; + case 'sh': + case 'bash': + return ; + default: + return ; + } + }; + + return ( +
+
entry.type === 'directory' && hasChildren && toggleDirectory(entry.path)} + > + {entry.type === 'directory' && hasChildren && ( + + )} + {(!hasChildren || entry.type !== 'directory') && ( +
+ )} + {getIcon()} + {entry.name} +
+ + {entry.type === 'directory' && hasChildren && isExpanded && ( +
+ {getChildren(entry.path, entry.level).map(child => renderEntry(child))} +
+ )} +
+ ); + }; + + // Get root entries + const rootEntries = entries.filter(e => e.level === 0); + + return ( +
+
+ {rootEntries.map(entry => renderEntry(entry, true))} +
+
+ ); +}; \ No newline at end of file diff --git a/src/components/widgets/TodoWidget.tsx b/src/components/widgets/TodoWidget.tsx new file mode 100644 index 0000000..938e103 --- /dev/null +++ b/src/components/widgets/TodoWidget.tsx @@ -0,0 +1,63 @@ +import React from "react"; +import { CheckCircle2, Circle, Clock, FileEdit } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; +import { cn } from "@/lib/utils"; + +interface TodoWidgetProps { + todos: any[]; + result?: any; +} + +export const TodoWidget: React.FC = ({ todos, result: _result }) => { + const statusIcons = { + completed: , + in_progress: , + pending: + }; + + const priorityColors = { + high: "bg-red-500/10 text-red-500 border-red-500/20", + medium: "bg-yellow-500/10 text-yellow-500 border-yellow-500/20", + low: "bg-green-500/10 text-green-500 border-green-500/20" + }; + + return ( +
+
+ + Todo List +
+
+ {todos.map((todo, idx) => ( +
+
+ {statusIcons[todo.status as keyof typeof statusIcons] || statusIcons.pending} +
+
+

+ {todo.content} +

+ {todo.priority && ( + + {todo.priority} + + )} +
+
+ ))} +
+
+ ); +}; \ No newline at end of file diff --git a/src/components/widgets/index.ts b/src/components/widgets/index.ts new file mode 100644 index 0000000..bcdddd2 --- /dev/null +++ b/src/components/widgets/index.ts @@ -0,0 +1,27 @@ +// Re-export all widgets from their individual files +export { TodoWidget } from './TodoWidget'; +export { LSWidget } from './LSWidget'; +export { BashWidget } from './BashWidget'; + +// TODO: Add these widgets as they are implemented +// export { LSResultWidget } from './LSWidget'; +// export { ReadWidget } from './ReadWidget'; +// export { ReadResultWidget } from './ReadResultWidget'; +// export { GlobWidget } from './GlobWidget'; +// export { WriteWidget } from './WriteWidget'; +// export { GrepWidget } from './GrepWidget'; +// export { EditWidget } from './EditWidget'; +// export { EditResultWidget } from './EditResultWidget'; +// export { MCPWidget } from './MCPWidget'; +// export { CommandWidget } from './CommandWidget'; +// export { CommandOutputWidget } from './CommandOutputWidget'; +// export { SummaryWidget } from './SummaryWidget'; +// export { MultiEditWidget } from './MultiEditWidget'; +// export { MultiEditResultWidget } from './MultiEditResultWidget'; +// export { SystemReminderWidget } from './SystemReminderWidget'; +// export { SystemInitializedWidget } from './SystemInitializedWidget'; +// export { TaskWidget } from './TaskWidget'; +// export { WebSearchWidget } from './WebSearchWidget'; +// export { ThinkingWidget } from './ThinkingWidget'; +// export { WebFetchWidget } from './WebFetchWidget'; +// export { TodoReadWidget } from './TodoReadWidget'; \ No newline at end of file