Implement comprehensive tool call rendering with state persistence

- Implement tool-specific rendering for all 14 tool types (read, edit, write, bash, webfetch, todowrite, task, etc.)
- Each tool shows contextually relevant information (file previews, diffs, command output, todo lists)
- Add metadata-driven content display using preview, diff, output, and todos from tool state
- Implement status-based rendering (pending, running, completed, error) with animations
- Create global state store for expandable items (tool calls and reasoning sections)
- Fix state persistence: expanded tool calls and reasoning sections remain expanded when new messages arrive
- Fix scroll position preservation during live message updates
- Fix reasoning toggle loop by replacing native details element with custom expandable
- Add comprehensive documentation in TOOL_CALL_IMPLEMENTATION.md
- Reduce font sizes for better readability in expanded tool content
- Add proper keying to For loops to prevent component recreation
- Match TUI patterns for tool names, actions, and content formatting
This commit is contained in:
Shantur Rathore
2025-10-23 01:18:25 +01:00
parent fa77b4e82e
commit d7f619486e
15 changed files with 3059 additions and 106 deletions

View File

@@ -1,6 +1,7 @@
import { createSignal } from "solid-js"
import type { Instance } from "../types/instance"
import { sdkManager } from "../lib/sdk-manager"
import { sseManager } from "../lib/sse-manager"
import { fetchSessions, fetchAgents, fetchProviders } from "./sessions"
import { showSessionPicker } from "./ui"
@@ -66,6 +67,8 @@ async function createInstance(folder: string): Promise<string> {
setActiveInstanceId(tempId)
sseManager.connect(tempId, port)
try {
await fetchSessions(tempId)
await fetchAgents(tempId)
@@ -90,6 +93,8 @@ async function stopInstance(id: string) {
const instance = instances().get(id)
if (!instance) return
sseManager.disconnect(id)
if (instance.port) {
sdkManager.destroyClient(instance.port)
}

View File

@@ -2,6 +2,7 @@ import { createSignal } from "solid-js"
import type { Session, Agent, Provider } from "../types/session"
import type { Message } from "../types/message"
import { instances } from "./instances"
import { sseManager } from "../lib/sse-manager"
const [sessions, setSessions] = createSignal<Map<string, Map<string, Session>>>(new Map())
const [activeSessionId, setActiveSessionId] = createSignal<Map<string, string>>(new Map())
@@ -394,6 +395,209 @@ async function loadMessages(instanceId: string, sessionId: string): Promise<void
}
}
function handleMessageUpdate(instanceId: string, event: any): void {
const instanceSessions = sessions().get(instanceId)
if (!instanceSessions) return
if (event.type === "message.part.updated") {
const part = event.properties?.part
if (!part) return
const session = instanceSessions.get(part.sessionID)
if (!session) return
let message = session.messages.find((m) => m.id === part.messageID)
if (!message) {
message = {
id: part.messageID,
sessionId: part.sessionID,
type: "assistant",
parts: [part],
timestamp: Date.now(),
status: "streaming",
}
session.messages.push(message)
} else {
const partIndex = message.parts.findIndex((p: any) => p.id === part.id)
if (partIndex === -1) {
message.parts.push(part)
} else {
message.parts[partIndex] = part
}
}
setSessions((prev) => {
const next = new Map(prev)
const instanceSessions = new Map(prev.get(instanceId))
instanceSessions.set(part.sessionID, { ...session })
next.set(instanceId, instanceSessions)
return next
})
} else if (event.type === "message.updated") {
const info = event.properties?.info
if (!info) return
const session = instanceSessions.get(info.sessionID)
if (!session) return
let message = session.messages.find((m) => m.id === info.id)
if (!message) {
message = {
id: info.id,
sessionId: info.sessionID,
type: info.role === "user" ? "user" : "assistant",
parts: [],
timestamp: info.time?.created || Date.now(),
status: "complete",
}
session.messages.push(message)
} else {
// Update existing message - replace temp message with real one
message.id = info.id
message.status = "complete"
}
setSessions((prev) => {
const next = new Map(prev)
const instanceSessions = new Map(prev.get(instanceId))
const updatedSession = instanceSessions.get(info.sessionID)
if (updatedSession) {
const messagesInfo = new Map(updatedSession.messagesInfo)
messagesInfo.set(info.id, info)
instanceSessions.set(info.sessionID, { ...updatedSession, messagesInfo })
}
next.set(instanceId, instanceSessions)
return next
})
}
}
function handleSessionUpdate(instanceId: string, event: any): void {
const info = event.properties?.info
if (!info) return
const instanceSessions = sessions().get(instanceId)
if (!instanceSessions) return
const existingSession = instanceSessions.get(info.id)
if (!existingSession) {
const newSession: Session = {
id: info.id,
instanceId,
title: info.title || "Untitled",
parentId: info.parentID || null,
agent: info.agent || "",
model: {
providerId: info.model?.providerID || "",
modelId: info.model?.modelID || "",
},
time: {
created: info.time?.created || Date.now(),
updated: info.time?.updated || Date.now(),
},
messages: [],
messagesInfo: new Map(),
}
setSessions((prev) => {
const next = new Map(prev)
const instanceSessions = new Map(prev.get(instanceId))
instanceSessions.set(newSession.id, newSession)
next.set(instanceId, instanceSessions)
return next
})
console.log(`[SSE] New session created: ${info.id}`, newSession)
} else {
const updatedSession = {
...existingSession,
title: info.title || existingSession.title,
agent: info.agent || existingSession.agent,
model: info.model
? {
providerId: info.model.providerID || existingSession.model.providerId,
modelId: info.model.modelID || existingSession.model.modelId,
}
: existingSession.model,
time: {
...existingSession.time,
updated: info.time?.updated || Date.now(),
},
}
setSessions((prev) => {
const next = new Map(prev)
const instanceSessions = new Map(prev.get(instanceId))
instanceSessions.set(existingSession.id, updatedSession)
next.set(instanceId, instanceSessions)
return next
})
}
}
async function sendMessage(
instanceId: string,
sessionId: string,
prompt: string,
attachments: string[] = [],
): Promise<void> {
const instance = instances().get(instanceId)
if (!instance || !instance.client) {
throw new Error("Instance not ready")
}
const instanceSessions = sessions().get(instanceId)
const session = instanceSessions?.get(sessionId)
if (!session) {
throw new Error("Session not found")
}
const requestBody = {
parts: [
{
type: "text" as const,
text: prompt,
},
],
...(session.agent && { agent: session.agent }),
...(session.model.providerId &&
session.model.modelId && {
model: {
providerID: session.model.providerId,
modelID: session.model.modelId,
},
}),
}
console.log("[sendMessage] Sending prompt:", {
sessionId,
requestBody,
})
try {
const response = await instance.client.session.prompt({
path: { id: sessionId },
body: requestBody,
})
console.log("[sendMessage] Response:", response)
if (response.error) {
console.error("[sendMessage] Server returned error:", response.error)
throw new Error(JSON.stringify(response.error) || "Failed to send message")
}
} catch (error) {
console.error("[sendMessage] Failed to send prompt:", error)
throw error
}
}
sseManager.onMessageUpdate = handleMessageUpdate
sseManager.onSessionUpdate = handleSessionUpdate
export {
sessions,
activeSessionId,
@@ -407,6 +611,7 @@ export {
fetchAgents,
fetchProviders,
loadMessages,
sendMessage,
setActiveSession,
setActiveParentSession,
clearActiveParentSession,

View File

@@ -0,0 +1,36 @@
import { createSignal } from "solid-js"
const [expandedItems, setExpandedItems] = createSignal<Set<string>>(new Set())
export function isItemExpanded(itemId: string): boolean {
return expandedItems().has(itemId)
}
export function toggleItemExpanded(itemId: string): void {
setExpandedItems((prev) => {
const next = new Set(prev)
if (next.has(itemId)) {
next.delete(itemId)
} else {
next.add(itemId)
}
return next
})
}
export function setItemExpanded(itemId: string, expanded: boolean): void {
setExpandedItems((prev) => {
const next = new Set(prev)
if (expanded) {
next.add(itemId)
} else {
next.delete(itemId)
}
return next
})
}
// Backward compatibility aliases
export const isToolCallExpanded = isItemExpanded
export const toggleToolCallExpanded = toggleItemExpanded
export const setToolCallExpanded = setItemExpanded