diff --git a/ui/web/src/pages/chat/chat-thread.tsx b/ui/web/src/pages/chat/chat-thread.tsx
index d0c58cfe..e6ef9e29 100644
--- a/ui/web/src/pages/chat/chat-thread.tsx
+++ b/ui/web/src/pages/chat/chat-thread.tsx
@@ -1,3 +1,4 @@
+import { useMemo } from "react";
import { Circle } from "lucide-react";
import { MessageBubble } from "@/components/chat/message-bubble";
import { StreamingText } from "@/components/chat/streaming-text";
@@ -28,6 +29,19 @@ export function ChatThread({
[messages.length, streamText, thinkingText, toolStream.length],
);
+ // Build map of tool_call_id → error content for tool results that indicate errors
+ const toolCallErrors = useMemo(() => {
+ const errors = new Map
();
+ for (const msg of messages) {
+ if (msg.role !== "tool" || !msg.tool_call_id || !msg.content) continue;
+ const c = msg.content.trimStart();
+ if (c.startsWith("Error") || c.startsWith("error:") || c.includes("failed:") || c.includes("Failed:")) {
+ errors.set(msg.tool_call_id, msg.content);
+ }
+ }
+ return errors;
+ }, [messages]);
+
// Show spinner while loading history for a different session
if (loading) {
return (
@@ -54,7 +68,7 @@ export function ChatThread({
>
{messages.map((msg, i) => (
-
+
))}
{/* Tool stream during active run */}
diff --git a/ui/web/src/pages/chat/hooks/use-chat-messages.ts b/ui/web/src/pages/chat/hooks/use-chat-messages.ts
index df78b360..cd3ba07b 100644
--- a/ui/web/src/pages/chat/hooks/use-chat-messages.ts
+++ b/ui/web/src/pages/chat/hooks/use-chat-messages.ts
@@ -26,6 +26,7 @@ export function useChatMessages(sessionKey: string, agentId: string) {
const expectingRunRef = useRef(false);
const streamRef = useRef("");
const thinkingRef = useRef("");
+ const toolStreamRef = useRef([]);
const agentIdRef = useRef(agentId);
agentIdRef.current = agentId;
@@ -44,6 +45,7 @@ export function useChatMessages(sessionKey: string, agentId: string) {
expectingRunRef.current = false;
streamRef.current = "";
thinkingRef.current = "";
+ toolStreamRef.current = [];
}
// Load history (no loading spinner — the empty state placeholder is shown instead)
@@ -87,6 +89,13 @@ export function useChatMessages(sessionKey: string, agentId: string) {
const event = payload as AgentEventPayload;
if (!event) return;
+ // Announce run.completed: reload history when a subagent/delegate announce finishes
+ // for the current agent (these runs aren't tracked by runIdRef).
+ if (event.type === "run.completed" && event.runKind === "announce" && event.agentId === agentIdRef.current) {
+ loadHistory();
+ return;
+ }
+
// Capture run.started when we are expecting a run for this agent
if (event.type === "run.started") {
if (expectingRunRef.current && event.agentId === agentIdRef.current) {
@@ -98,6 +107,7 @@ export function useChatMessages(sessionKey: string, agentId: string) {
setToolStream([]);
streamRef.current = "";
thinkingRef.current = "";
+ toolStreamRef.current = [];
}
return;
}
@@ -123,20 +133,24 @@ export function useChatMessages(sessionKey: string, agentId: string) {
toolCallId: event.payload?.id ?? "",
runId: event.runId,
name: event.payload?.name ?? "tool",
+ arguments: event.payload?.arguments,
phase: "calling",
startedAt: Date.now(),
updatedAt: Date.now(),
};
- setToolStream((prev) => [...prev, entry]);
+ toolStreamRef.current = [...toolStreamRef.current, entry];
+ setToolStream(toolStreamRef.current);
break;
}
case "tool.result": {
+ const isError = event.payload?.is_error;
setToolStream((prev) =>
prev.map((t) =>
t.toolCallId === event.payload?.id
? {
...t,
- phase: event.payload?.is_error ? "error" : "completed",
+ phase: isError ? "error" : "completed",
+ errorContent: isError ? event.payload?.content : undefined,
updatedAt: Date.now(),
}
: t,
@@ -147,12 +161,28 @@ export function useChatMessages(sessionKey: string, agentId: string) {
case "run.completed": {
setIsRunning(false);
runIdRef.current = null;
- loadHistory();
+
+ // If we have streamed text and no tool calls, promote it directly
+ // to avoid a flash caused by loadHistory() replacing the DOM.
+ // When tools were used, reload to get structured tool_calls data.
+ const hadTools = toolStreamRef.current.length > 0;
+ const streamed = streamRef.current;
+
setStreamText(null);
setThinkingText(null);
setToolStream([]);
streamRef.current = "";
thinkingRef.current = "";
+ toolStreamRef.current = [];
+
+ if (streamed && !hadTools) {
+ setMessages((prev) => [
+ ...prev,
+ { role: "assistant", content: streamed, timestamp: Date.now() },
+ ]);
+ } else {
+ loadHistory();
+ }
break;
}
case "run.failed": {
diff --git a/ui/web/src/types/chat.ts b/ui/web/src/types/chat.ts
index d775bd7d..28684fbb 100644
--- a/ui/web/src/types/chat.ts
+++ b/ui/web/src/types/chat.ts
@@ -13,12 +13,14 @@ export interface AgentEventPayload {
type: string; // "run.started" | "run.completed" | "run.failed" | "chunk" | "tool.call" | "tool.result"
agentId: string;
runId: string;
+ runKind?: string; // "delegation" | "announce" — omitted for user-initiated runs
payload?: {
content?: string;
name?: string;
id?: string;
is_error?: boolean;
error?: string;
+ arguments?: Record;
};
}
@@ -28,6 +30,8 @@ export interface ToolStreamEntry {
runId: string;
name: string;
phase: "calling" | "completed" | "error";
+ arguments?: Record;
+ errorContent?: string;
startedAt: number;
updatedAt: number;
}
diff --git a/ui/web/src/types/session.ts b/ui/web/src/types/session.ts
index ca179223..0bcc140a 100644
--- a/ui/web/src/types/session.ts
+++ b/ui/web/src/types/session.ts
@@ -23,8 +23,8 @@ export interface SessionPreview {
export interface Message {
role: "user" | "assistant" | "tool";
content: string;
- toolCalls?: ToolCall[];
- toolCallId?: string;
+ tool_calls?: ToolCall[];
+ tool_call_id?: string;
}
export interface ToolCall {