diff --git a/src/client/src/controllers/sessionController.ts b/src/client/src/controllers/sessionController.ts index 52c1dc0..add920e 100644 --- a/src/client/src/controllers/sessionController.ts +++ b/src/client/src/controllers/sessionController.ts @@ -1,6 +1,7 @@ import { api, type CommandResult, type SessionActivity, type SessionInfo, type SessionStatus } from "../api"; import { appendText, normalizeMessages, textMessage } from "../chatMessages"; import { GlobalSessionSocket, SessionSocket, type SessionUiEvent } from "../sessionSocket"; +import type { ChatLine, ChatPart } from "../components/shared"; import type { GetState, SetState, UpdateUrl } from "./types"; export class SessionController { @@ -139,9 +140,9 @@ export class SessionController { if (event.type === "assistant.delta") { this.setState({ messages: appendText(messages, "assistant", event.text) }); } else if (event.type === "tool.start") { - this.setState({ messages: [...messages, { role: "tool", parts: [{ type: "toolCall", toolName: event.toolName, summary: "" }] }] }); + this.setState({ messages: appendPart(messages, "assistant", { type: "toolCall", toolName: event.toolName, summary: event.summary }) }); } else if (event.type === "tool.end") { - this.setState({ messages: [...messages, textMessage("tool", `${event.isError ? "✖" : "✓"} ${event.toolName}`)] }); + this.setState({ messages: [...messages, { role: "tool", parts: [{ type: "toolResult", toolName: event.toolName, text: event.text, isError: event.isError }] }] }); } else if (event.type === "status.update") { this.applyStatus(event.status); } else if (event.type === "activity.update") { @@ -153,3 +154,9 @@ export class SessionController { } } } + +function appendPart(messages: ChatLine[], role: ChatLine["role"], part: ChatPart): ChatLine[] { + const last = messages.at(-1); + if (last?.role === role) return [...messages.slice(0, -1), { ...last, parts: [...last.parts, part] }]; + return [...messages, { role, parts: [part] }]; +} diff --git a/src/client/src/sessionSocket.ts b/src/client/src/sessionSocket.ts index f03bf25..ea194b0 100644 --- a/src/client/src/sessionSocket.ts +++ b/src/client/src/sessionSocket.ts @@ -2,8 +2,8 @@ import { globalSessionEvents, sessionEvents, type SessionActivity, type SessionS export type SessionUiEvent = | { type: "assistant.delta"; text: string } - | { type: "tool.start"; toolName: string } - | { type: "tool.end"; toolName: string; isError: boolean } + | { type: "tool.start"; toolName: string; summary: string } + | { type: "tool.end"; toolName: string; text: string; isError: boolean } | { type: "status.update"; status: SessionStatus } | { type: "activity.update"; activity: SessionActivity } | { type: "command.output"; level: "info" | "success" | "error"; message: string } diff --git a/src/server/sessions/piSessionService.ts b/src/server/sessions/piSessionService.ts index c39c446..1cbe6b0 100644 --- a/src/server/sessions/piSessionService.ts +++ b/src/server/sessions/piSessionService.ts @@ -245,13 +245,42 @@ function toClientEvent(event: any): unknown { return { type: "assistant.delta", text: event.assistantMessageEvent.delta }; } if (event.type === "tool_execution_start") { - return { type: "tool.start", toolName: event.toolName, toolCallId: event.toolCallId }; + return { type: "tool.start", toolName: event.toolName, toolCallId: event.toolCallId, summary: summarizeToolArgs(event.args) }; } if (event.type === "tool_execution_end") { - return { type: "tool.end", toolName: event.toolName, toolCallId: event.toolCallId, isError: event.isError }; + return { type: "tool.end", toolName: event.toolName, toolCallId: event.toolCallId, text: stringifyToolResult(event.result), isError: event.isError }; } if (event.type === "agent_start") return { type: "agent.start" }; if (event.type === "agent_end") return { type: "agent.end" }; if (event.type === "message_end") return { type: "message.end" }; return { type: "pi.event", eventType: event.type }; } + +function summarizeToolArgs(args: any): string { + if (!args || typeof args !== "object") return args == null ? "" : String(args); + if (typeof args.command === "string") return args.command; + if (typeof args.path === "string") return args.path; + if (typeof args.oldText === "string" && typeof args.newText === "string") return "edit text replacement"; + if (Array.isArray(args.edits)) return `${args.edits.length} edit${args.edits.length === 1 ? "" : "s"}`; + const entries = Object.entries(args).filter(([, value]) => value != null).slice(0, 3); + return entries.map(([key, value]) => `${key}: ${shortToolValue(value)}`).join(" · "); +} + +function shortToolValue(value: unknown): string { + if (typeof value === "string") return value.length > 80 ? `${value.slice(0, 77)}…` : value; + if (typeof value === "number" || typeof value === "boolean") return String(value); + if (Array.isArray(value)) return `${value.length} item${value.length === 1 ? "" : "s"}`; + if (typeof value === "object" && value) return "object"; + return ""; +} + +function stringifyToolResult(result: unknown): string { + if (typeof result === "string") return result; + if (Array.isArray(result)) return result.map(stringifyToolResult).filter(Boolean).join("\n"); + if (result && typeof result === "object") { + const text = (result as any).text ?? (result as any).content ?? (result as any).output; + if (typeof text === "string") return text; + return JSON.stringify(result, null, 2); + } + return result == null ? "" : String(result); +}