Files
NomadArch/tasks/done/010-tool-call-rendering.md
Gemini AI 157449a9ad restore: recover deleted documentation, CI/CD, and infrastructure files
Restored from origin/main (b4663fb):
- .github/ workflows and issue templates
- .gitignore (proper exclusions)
- .opencode/agent/web_developer.md
- AGENTS.md, BUILD.md, PROGRESS.md
- dev-docs/ (9 architecture/implementation docs)
- docs/screenshots/ (4 UI screenshots)
- images/ (CodeNomad icons)
- package-lock.json (dependency lockfile)
- tasks/ (25+ project task files)

Also restored original source files that were modified:
- packages/ui/src/App.tsx
- packages/ui/src/lib/logger.ts
- packages/ui/src/stores/instances.ts
- packages/server/src/server/routes/workspaces.ts
- packages/server/src/workspaces/manager.ts
- packages/server/src/workspaces/runtime.ts
- packages/server/package.json

Kept new additions:
- Install-*.bat/.sh (enhanced installers)
- Launch-*.bat/.sh (new launchers)
- README.md (SEO optimized with GLM 4.7)
2025-12-23 13:03:48 +04:00

604 lines
15 KiB
Markdown

# Task 010: Tool Call Rendering - Display Tool Executions Inline
## Status: TODO
## Objective
Implement interactive tool call rendering that displays tool executions inline within assistant messages. Users should be able to expand/collapse tool calls to see input, output, and execution status.
## Prerequisites
- Task 007 (Message display) complete
- Task 008 (SSE integration) complete
- Task 009 (Prompt input) complete
- Messages streaming from API
- Tool call data available in message parts
## Context
When OpenCode executes tools (bash commands, file edits, etc.), these should be visible to the user in the message stream. Tool calls need:
- Collapsed state showing summary (tool name + brief description)
- Expanded state showing full input/output
- Status indicators (pending, running, success, error)
- Click to toggle expand/collapse
- Syntax highlighting for code in input/output
This provides transparency into what OpenCode is doing and helps users understand the assistant's actions.
## Implementation Steps
### Step 1: Define Tool Call Types
Create or update `src/types/message.ts`:
```typescript
export interface ToolCallPart {
type: "tool_call"
id: string
tool: string
input: any
output?: any
status: "pending" | "running" | "success" | "error"
error?: string
}
export interface MessagePart {
type: "text" | "tool_call"
text?: string
id?: string
tool?: string
input?: any
output?: any
status?: "pending" | "running" | "success" | "error"
error?: string
}
```
### Step 2: Create Tool Call Component
Create `src/components/tool-call.tsx`:
```typescript
import { createSignal, Show, Switch, Match } from "solid-js"
import type { ToolCallPart } from "../types/message"
interface ToolCallProps {
part: ToolCallPart
}
export default function ToolCall(props: ToolCallProps) {
const [expanded, setExpanded] = createSignal(false)
function toggleExpanded() {
setExpanded(!expanded())
}
function getToolIcon(tool: string): string {
switch (tool) {
case "bash":
return "⚡"
case "edit":
return "✏️"
case "read":
return "📖"
case "write":
return "📝"
case "glob":
return "🔍"
case "grep":
return "🔎"
default:
return "🔧"
}
}
function getStatusIcon(status: string): string {
switch (status) {
case "pending":
return "⏳"
case "running":
return "⟳"
case "success":
return "✓"
case "error":
return "✗"
default:
return ""
}
}
function getToolSummary(part: ToolCallPart): string {
const { tool, input } = part
switch (tool) {
case "bash":
return input?.command || "Execute command"
case "edit":
return `Edit ${input?.filePath || "file"}`
case "read":
return `Read ${input?.filePath || "file"}`
case "write":
return `Write ${input?.filePath || "file"}`
case "glob":
return `Find ${input?.pattern || "files"}`
case "grep":
return `Search for "${input?.pattern || "pattern"}"`
default:
return tool
}
}
function formatJson(obj: any): string {
if (typeof obj === "string") return obj
return JSON.stringify(obj, null, 2)
}
return (
<div
class="tool-call"
classList={{
"tool-call-expanded": expanded(),
"tool-call-error": props.part.status === "error",
"tool-call-success": props.part.status === "success",
"tool-call-running": props.part.status === "running",
}}
onClick={toggleExpanded}
>
<div class="tool-call-header">
<span class="tool-call-expand-icon">{expanded() ? "▼" : "▶"}</span>
<span class="tool-call-icon">{getToolIcon(props.part.tool)}</span>
<span class="tool-call-tool">{props.part.tool}:</span>
<span class="tool-call-summary">{getToolSummary(props.part)}</span>
<span class="tool-call-status">{getStatusIcon(props.part.status)}</span>
</div>
<Show when={expanded()}>
<div class="tool-call-body" onClick={(e) => e.stopPropagation()}>
<Show when={props.part.input}>
<div class="tool-call-section">
<div class="tool-call-section-title">Input:</div>
<pre class="tool-call-content">
<code>{formatJson(props.part.input)}</code>
</pre>
</div>
</Show>
<Show when={props.part.output !== undefined}>
<div class="tool-call-section">
<div class="tool-call-section-title">Output:</div>
<pre class="tool-call-content">
<code>{formatJson(props.part.output)}</code>
</pre>
</div>
</Show>
<Show when={props.part.error}>
<div class="tool-call-section tool-call-error-section">
<div class="tool-call-section-title">Error:</div>
<pre class="tool-call-content tool-call-error-content">
<code>{props.part.error}</code>
</pre>
</div>
</Show>
<Show when={props.part.status === "running"}>
<div class="tool-call-running-indicator">
<span class="spinner-small" />
<span>Executing...</span>
</div>
</Show>
</div>
</Show>
</div>
)
}
```
### Step 3: Update Message Item to Render Tool Calls
Update `src/components/message-item.tsx`:
```typescript
import { For, Show, Switch, Match } from "solid-js"
import type { Message, MessagePart } from "../types/message"
import ToolCall from "./tool-call"
interface MessageItemProps {
message: Message
}
export default function MessageItem(props: MessageItemProps) {
const isUser = () => props.message.type === "user"
return (
<div
class="message-item"
classList={{
"message-user": isUser(),
"message-assistant": !isUser(),
}}
>
<div class="message-header">
<span class="message-author">{isUser() ? "You" : "Assistant"}</span>
<span class="message-timestamp">
{new Date(props.message.timestamp).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})}
</span>
</div>
<div class="message-content">
<For each={props.message.parts}>
{(part) => (
<Switch>
<Match when={part.type === "text"}>
<div class="message-text">{part.text}</div>
</Match>
<Match when={part.type === "tool_call"}>
<ToolCall part={part as any} />
</Match>
</Switch>
)}
</For>
</div>
<Show when={props.message.status === "error"}>
<div class="message-error">Failed to send message</div>
</Show>
<Show when={props.message.status === "sending"}>
<div class="message-sending">
<span class="generating-spinner"></span> Sending...
</div>
</Show>
</div>
)
}
```
### Step 4: Add Tool Call Styling
Add to `src/index.css`:
```css
/* Tool Call Styles */
.tool-call {
margin: 8px 0;
border: 1px solid var(--border-color);
border-radius: 6px;
background-color: var(--secondary-bg);
overflow: hidden;
cursor: pointer;
transition:
border-color 150ms ease,
background-color 150ms ease;
}
.tool-call:hover {
border-color: var(--accent-color);
}
.tool-call-expanded {
cursor: default;
}
.tool-call-success {
border-left: 3px solid #10b981;
}
.tool-call-error {
border-left: 3px solid #ef4444;
}
.tool-call-running {
border-left: 3px solid var(--accent-color);
}
.tool-call-header {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
font-size: 13px;
}
.tool-call-expand-icon {
font-size: 10px;
color: var(--text-muted);
transition: transform 150ms ease;
}
.tool-call-expanded .tool-call-expand-icon {
transform: rotate(0deg);
}
.tool-call-icon {
font-size: 14px;
}
.tool-call-tool {
font-weight: 600;
color: var(--text);
}
.tool-call-summary {
flex: 1;
color: var(--text-muted);
font-family: monospace;
font-size: 12px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.tool-call-status {
font-size: 14px;
margin-left: auto;
}
.tool-call-body {
border-top: 1px solid var(--border-color);
padding: 12px;
background-color: var(--background);
}
.tool-call-section {
margin-bottom: 12px;
}
.tool-call-section:last-child {
margin-bottom: 0;
}
.tool-call-section-title {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
color: var(--text-muted);
margin-bottom: 6px;
letter-spacing: 0.5px;
}
.tool-call-content {
background-color: var(--secondary-bg);
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 8px 12px;
font-family: monospace;
font-size: 12px;
line-height: 1.5;
overflow-x: auto;
margin: 0;
}
.tool-call-content code {
font-family: inherit;
background: none;
padding: 0;
}
.tool-call-error-section {
background-color: rgba(239, 68, 68, 0.05);
border-radius: 4px;
padding: 8px;
}
.tool-call-error-content {
background-color: rgba(239, 68, 68, 0.1);
border-color: rgba(239, 68, 68, 0.3);
color: #dc2626;
}
.tool-call-running-indicator {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background-color: var(--secondary-bg);
border-radius: 4px;
font-size: 13px;
color: var(--text-muted);
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* Dark mode adjustments */
@media (prefers-color-scheme: dark) {
.tool-call {
background-color: rgba(255, 255, 255, 0.03);
}
.tool-call-body {
background-color: rgba(0, 0, 0, 0.2);
}
.tool-call-content {
background-color: rgba(0, 0, 0, 0.3);
}
}
```
### Step 5: Update SSE Handler to Parse Tool Calls
Update `src/lib/sse-manager.ts` to correctly parse tool call parts from SSE events:
```typescript
function handleMessageUpdate(event: MessageUpdateEvent, instanceId: string) {
// When a message part arrives via SSE, check if it's a tool call
const part = event.part
if (part.type === "tool_call") {
// Parse tool call data
const toolCallPart: ToolCallPart = {
type: "tool_call",
id: part.id || `tool-${Date.now()}`,
tool: part.tool || "unknown",
input: part.input,
output: part.output,
status: part.status || "pending",
error: part.error,
}
// Add or update in messages
updateMessagePart(instanceId, event.sessionId, event.messageId, toolCallPart)
}
}
```
### Step 6: Handle Tool Call Updates
Ensure that tool calls can update their status as they execute:
```typescript
// In sessions store
function updateMessagePart(instanceId: string, sessionId: string, messageId: string, part: MessagePart) {
setSessions((prev) => {
const next = new Map(prev)
const instanceSessions = new Map(prev.get(instanceId))
const session = instanceSessions.get(sessionId)
if (session) {
const messages = session.messages.map((msg) => {
if (msg.id === messageId) {
// Find existing part by ID and update, or append
const partIndex = msg.parts.findIndex((p) => p.type === "tool_call" && p.id === part.id)
if (partIndex !== -1) {
const updatedParts = [...msg.parts]
updatedParts[partIndex] = part
return { ...msg, parts: updatedParts }
} else {
return { ...msg, parts: [...msg.parts, part] }
}
}
return msg
})
instanceSessions.set(sessionId, { ...session, messages })
}
next.set(instanceId, instanceSessions)
return next
})
}
```
## Testing Checklist
### Visual Rendering
- [ ] Tool calls render in collapsed state by default
- [ ] Tool icon displays correctly for each tool type
- [ ] Tool summary shows meaningful description
- [ ] Status icon displays correctly (pending, running, success, error)
- [ ] Styling is consistent with design
### Expand/Collapse
- [ ] Click tool call header - expands to show details
- [ ] Click again - collapses back to summary
- [ ] Expand icon rotates correctly
- [ ] Clicking inside expanded body doesn't collapse
- [ ] Multiple tool calls can be expanded independently
### Content Display
- [ ] Input section shows tool input data
- [ ] Output section shows tool output data
- [ ] JSON is formatted with proper indentation
- [ ] Code/text is displayed in monospace font
- [ ] Long output is scrollable horizontally
### Status Indicators
- [ ] Pending status shows waiting icon (⏳)
- [ ] Running status shows spinner and "Executing..."
- [ ] Success status shows checkmark (✓)
- [ ] Error status shows X (✗) and error message
- [ ] Border color changes based on status
### Real-time Updates
- [ ] Tool calls appear as SSE events arrive
- [ ] Status updates from pending → running → success
- [ ] Output appears when tool completes
- [ ] Error state shows if tool fails
- [ ] UI updates smoothly without flashing
### Different Tool Types
- [ ] Bash commands display correctly
- [ ] File edits show file path and changes
- [ ] File reads show file path
- [ ] Glob/grep show patterns
- [ ] Unknown tools have fallback icon
### Error Handling
- [ ] Tool errors display error message
- [ ] Error section has red styling
- [ ] Error state is clearly visible
- [ ] Can expand to see full error details
## Acceptance Criteria
- [ ] Tool calls render inline in assistant messages
- [ ] Default collapsed state shows summary
- [ ] Click to expand shows full input/output
- [ ] Status indicators work correctly
- [ ] Real-time updates via SSE work
- [ ] Multiple tool calls in one message work
- [ ] Error states are clear and helpful
- [ ] Styling matches design specifications
- [ ] No performance issues with many tool calls
- [ ] No console errors during normal operation
## Performance Considerations
**Per MVP principles - keep it simple:**
- Render all tool calls - no virtualization
- No lazy loading of tool content
- Simple JSON.stringify for formatting
- Direct DOM updates via SolidJS reactivity
- Add optimizations only if problems arise
## Future Enhancements (Post-MVP)
- Syntax highlighting for code in input/output (using Shiki)
- Diff view for file edits
- Copy button for tool output
- Link to file in file operations
- Collapsible sections within tool calls
- Tool execution time display
- Retry failed tools
- Export tool output
## References
- [User Interface - Tool Call Rendering](../docs/user-interface.md#3-messages-area)
- [Technical Implementation - Tool Call Rendering](../docs/technical-implementation.md#message-rendering)
- [Build Roadmap - Phase 2](../docs/build-roadmap.md#phase-2-core-chat-interface-week-2)
## Estimated Time
3-4 hours
## Notes
- Focus on clear visual hierarchy - collapsed view should be scannable
- Status indicators help users understand what's happening
- Errors should be prominent but not alarming
- Tool calls are a key differentiator - make them shine
- Test with real OpenCode responses to ensure data format matches
- Consider adding debug logging to verify SSE data structure