diff --git a/TOOL_CALL_IMPLEMENTATION.md b/TOOL_CALL_IMPLEMENTATION.md new file mode 100644 index 00000000..8d368cfd --- /dev/null +++ b/TOOL_CALL_IMPLEMENTATION.md @@ -0,0 +1,228 @@ +# Tool Call Rendering Implementation + +This document describes how tool calls are rendered in the OpenCode Client, following the patterns established in the TUI. + +## Overview + +Each tool type has specialized rendering logic that displays the most relevant information for that tool. This matches the TUI's approach of providing context-specific displays rather than generic input/output dumps. + +## Tool-Specific Rendering + +### 1. **read** - File Reading + +- **Title**: `Read {filename}` +- **Body**: Preview of file content (first 6 lines) from `metadata.preview` +- **Use case**: Shows what file content the assistant is reading + +### 2. **edit** - File Editing + +- **Title**: `Edit {filename}` +- **Body**: Diff/patch showing changes from `metadata.diff` +- **Special**: Shows diagnostics if available in metadata +- **Use case**: Shows what changes are being made to files + +### 3. **write** - File Writing + +- **Title**: `Write {filename}` +- **Body**: File content being written (first 10 lines) +- **Special**: Shows diagnostics if available in metadata +- **Use case**: Shows new file content being created + +### 4. **bash** - Shell Commands + +- **Title**: `Shell {description}` (or command if no description) +- **Body**: Console-style display with `$ command` and output + +``` +$ npm install vitest +added 50 packages... +``` + +- **Output from**: `metadata.output` +- **Use case**: Shows command execution and results + +### 5. **webfetch** - Web Fetching + +- **Title**: `Fetch {url}` +- **Body**: Fetched content (first 10 lines) +- **Use case**: Shows web content being retrieved + +### 6. **todowrite** - Task Planning + +- **Title**: Dynamic based on todo phase: + - All pending: "Creating plan" + - All completed: "Completing plan" + - Mixed: "Updating plan" +- **Body**: Formatted todo list: + - `- [x] Completed task` + - `- [ ] Pending task` + - `- [ ] ~~Cancelled task~~` + - `- [ ] In progress task` (highlighted) +- **Use case**: Shows the AI's task planning + +### 7. **task** - Delegated Tasks + +- **Title**: `Task[subagent_type] {description}` +- **Body**: List of delegated tool calls with icons: + +``` +⚡ bash: npm install +📖 read package.json +✏️ edit src/app.ts +``` + +- **Special**: In TUI, includes navigation hints for session tree +- **Use case**: Shows what the delegated agent is doing + +### 8. **todoread** - Plan Reading + +- **Special**: Hidden in TUI, returns empty string +- **Use case**: Internal tool, not displayed to user + +### 9. **glob** - File Pattern Matching + +- **Title**: `Glob {pattern}` +- **Use case**: Shows file search patterns + +### 10. **grep** - Content Search + +- **Title**: `Grep "{pattern}"` +- **Use case**: Shows what content is being searched + +### 11. **list** - Directory Listing + +- **Title**: `List` +- **Use case**: Shows directory operations + +### 12. **patch** - Patching Files + +- **Title**: `Patch` +- **Use case**: Shows patch operations + +### 13. **invalid** - Invalid Tool Calls + +- **Title**: Name of the actual tool attempted +- **Use case**: Shows validation errors + +### 14. **Default** - Unknown Tools + +- **Title**: Capitalized tool name +- **Body**: Output truncated to 10 lines +- **Use case**: Fallback for any new or custom tools + +## Status States + +### Pending + +- **Icon**: ⏸ (pause symbol) +- **Title**: Action text (e.g., "Writing command...", "Preparing edit...") +- **Border**: Accent color +- **Animation**: Shimmer effect on title +- **Expandable**: Shows "Waiting for permission..." message + +### Running + +- **Icon**: ⏳ (hourglass) +- **Title**: Same as completed state +- **Border**: Warning color (yellow/orange) +- **Animation**: Pulse on status icon + +### Completed + +- **Icon**: ✓ (checkmark) +- **Title**: Tool-specific title with arguments +- **Border**: Success color (green) +- **Body**: Tool-specific rendered content + +### Error + +- **Icon**: ✗ (X mark) +- **Title**: Same format but in error color +- **Border**: Error color (red) +- **Body**: Error message in highlighted box + +## Title Rendering Logic + +The title follows this pattern: + +1. **Pending state**: Show action text + + ``` + "Writing command..." + "Preparing edit..." + "Delegating..." + ``` + +2. **Completed/Running/Error**: Show specific info + + ``` + "Shell npm install" + "Edit src/app.ts" + "Read package.json" + "Task[general] Search for files" + ``` + +3. **Special cases**: + - `todowrite`: Shows plan phase + - `todoread`: Just "Plan" + - `bash`: Uses description if available, otherwise shows command + +## Metadata Usage + +Tool calls use `metadata` for rich content: + +- **read**: `metadata.preview` - file preview content +- **edit**: `metadata.diff` - patch/diff text +- **bash**: `metadata.output` - command output +- **todowrite**: `metadata.todos[]` - todo items with status +- **task**: `metadata.summary[]` - delegated tool calls +- **edit/write**: `metadata.diagnostics` - LSP diagnostics + +## Design Principles + +1. **Context-specific**: Each tool shows the most relevant information +2. **Progressive disclosure**: Collapsed by default, expand for details +3. **Visual hierarchy**: Icons, colors, and borders indicate status +4. **Truncation**: Long content is truncated (6-10 lines) to prevent overwhelming +5. **Consistency**: All tools follow same header/body/error structure + +## Component Structure + +```tsx +
+ + + {expanded && ( +
+ {/* Tool-specific body content */} + {error &&
{error}
} +
+ )} +
+``` + +## CSS Classes + +- `.tool-call` - Base container +- `.tool-call-status-{pending|running|completed|error}` - Status-specific styling +- `.tool-call-header` - Clickable header with expand/collapse +- `.tool-call-emoji` - Tool type icon +- `.tool-call-summary` - Tool title/description +- `.tool-call-details` - Expanded content area +- `.tool-call-content` - Code/output content (monospace) +- `.tool-call-todos` - Todo list container +- `.tool-call-task-summary` - Delegated task list +- `.tool-call-error-content` - Error message display + +## Future Enhancements + +1. **Syntax highlighting**: Use Shiki for code blocks in bash, read, write +2. **Diff rendering**: Better diff visualization for edit tool +3. **Copy buttons**: Quick copy for code/output +4. **File links**: Click filename to open in editor +5. **Diagnostics display**: Show LSP errors/warnings inline diff --git a/src/App.tsx b/src/App.tsx index b800b057..e7a7281c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,6 +5,7 @@ import SessionPicker from "./components/session-picker" import InstanceTabs from "./components/instance-tabs" import SessionTabs from "./components/session-tabs" import MessageStream from "./components/message-stream" +import PromptInput from "./components/prompt-input" import { hasInstances, isSelectingFolder, @@ -35,10 +36,11 @@ import { activeParentSessionId, getParentSessions, loadMessages, + sendMessage, } from "./stores/sessions" import { setupTabKeyboardShortcuts } from "./lib/keyboard" -const SessionMessages: Component<{ +const SessionView: Component<{ sessionId: string activeSessions: Map instanceId: string @@ -52,6 +54,10 @@ const SessionMessages: Component<{ } }) + async function handleSendMessage(prompt: string) { + await sendMessage(props.instanceId, props.sessionId, prompt) + } + return ( } > - {(s) => } + {(s) => ( +
+ + +
+ )}
) } @@ -213,7 +229,7 @@ const App: Component = () => { } > - - {(part) => } + + {(part) => } + + +
+ Sending... +
+
+
⚠ Message failed to send
diff --git a/src/components/message-part.tsx b/src/components/message-part.tsx index 07e77b71..303b949d 100644 --- a/src/components/message-part.tsx +++ b/src/components/message-part.tsx @@ -1,5 +1,6 @@ import { Show, Match, Switch } from "solid-js" import ToolCall from "./tool-call" +import { isItemExpanded, toggleItemExpanded } from "../stores/tool-call-state" interface MessagePartProps { part: any @@ -7,6 +8,13 @@ interface MessagePartProps { export default function MessagePart(props: MessagePartProps) { const partType = () => props.part?.type || "" + const reasoningId = () => `reasoning-${props.part?.id || ""}` + const isReasoningExpanded = () => isItemExpanded(reasoningId()) + + function handleReasoningClick(e: Event) { + e.preventDefault() + toggleItemExpanded(reasoningId()) + } return ( @@ -17,7 +25,7 @@ export default function MessagePart(props: MessagePartProps) { - + @@ -26,10 +34,15 @@ export default function MessagePart(props: MessagePartProps) {
-
- Reasoning -
{props.part.text || ""}
-
+
+
+ {isReasoningExpanded() ? "▼" : "▶"} + Reasoning +
+ +
{props.part.text || ""}
+
+
diff --git a/src/components/message-stream.tsx b/src/components/message-stream.tsx index e5710751..3e5dc638 100644 --- a/src/components/message-stream.tsx +++ b/src/components/message-stream.tsx @@ -2,8 +2,10 @@ import { For, Show, createSignal, createEffect, createMemo } from "solid-js" import type { Message } from "../types/message" import MessageItem from "./message-item" import ToolCall from "./tool-call" +import { sseManager } from "../lib/sse-manager" interface MessageStreamProps { + instanceId: string sessionId: string messages: Message[] messagesInfo?: Map @@ -21,6 +23,8 @@ export default function MessageStream(props: MessageStreamProps) { const [autoScroll, setAutoScroll] = createSignal(true) const [showScrollButton, setShowScrollButton] = createSignal(false) + const connectionStatus = () => sseManager.getStatus(props.instanceId) + function scrollToBottom() { if (containerRef) { containerRef.scrollTop = containerRef.scrollHeight @@ -81,6 +85,26 @@ export default function MessageStream(props: MessageStreamProps) { return (
+
+ + + + Connected + + + + + + Connecting... + + + + + + Disconnected + + +
@@ -107,24 +131,27 @@ export default function MessageStream(props: MessageStreamProps) {
- - {(item) => ( - -
- 🔧 - Tool Call - {item.data?.tool || "unknown"} + + {(item, index) => { + const key = item.type === "message" ? `msg-${item.data.id}` : `tool-${item.data.id}` + return ( + +
+ 🔧 + Tool Call + {item.data?.tool || "unknown"} +
+
- -
- } - > - - - )} + } + > + + + ) + }}
diff --git a/src/components/prompt-input.tsx b/src/components/prompt-input.tsx new file mode 100644 index 00000000..e9905102 --- /dev/null +++ b/src/components/prompt-input.tsx @@ -0,0 +1,79 @@ +import { createSignal, Show } from "solid-js" + +interface PromptInputProps { + instanceId: string + sessionId: string + onSend: (prompt: string) => Promise + disabled?: boolean +} + +export default function PromptInput(props: PromptInputProps) { + const [prompt, setPrompt] = createSignal("") + const [sending, setSending] = createSignal(false) + let textareaRef: HTMLTextAreaElement | undefined + + function handleKeyDown(e: KeyboardEvent) { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault() + handleSend() + } + } + + async function handleSend() { + const text = prompt().trim() + if (!text || sending() || props.disabled) return + + setSending(true) + try { + await props.onSend(text) + setPrompt("") + + if (textareaRef) { + textareaRef.style.height = "auto" + } + } catch (error) { + console.error("Failed to send message:", error) + alert("Failed to send message: " + (error instanceof Error ? error.message : String(error))) + } finally { + setSending(false) + textareaRef?.focus() + } + } + + function handleInput(e: Event) { + const target = e.target as HTMLTextAreaElement + setPrompt(target.value) + + target.style.height = "auto" + target.style.height = Math.min(target.scrollHeight, 200) + "px" + } + + const canSend = () => prompt().trim().length > 0 && !sending() && !props.disabled + + return ( +
+
+