mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-13 00:14:33 +00:00
feat(ui): add tool call details with arguments, results, and thinking display (#92)
- Enhance tool call cards with expandable arguments/result sections and inline summary - Display thinking/reasoning blocks in chat messages (uses existing ThinkingBlock component) - Reconstruct tool details from session history for persistent display - Emit tool result content and truncated arguments in agent events - Fix sanitize to always return cleaned content instead of empty string Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
This commit is contained in:
1 parent
4721cf26bd
commit
a321af8b04
9 files changed
+162
-132
No files matched your search
+21
-5
@@ -333,6 +333,7 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error)
|
||||
iteration := 0
|
||||
totalToolCalls := 0
|
||||
var finalContent string
|
||||
var finalThinking string
|
||||
var asyncToolCalls []string // track async spawn tool names for fallback
|
||||
var mediaResults []MediaResult // media files from tool MEDIA: results
|
||||
var deliverables []string // actual content from tool outputs (for team task results)
|
||||
@@ -532,6 +533,7 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error)
|
||||
}
|
||||
}
|
||||
finalContent = resp.Content
|
||||
finalThinking = resp.Thinking
|
||||
break
|
||||
}
|
||||
|
||||
@@ -594,7 +596,7 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error)
|
||||
Type: protocol.AgentEventToolCall,
|
||||
AgentID: l.id,
|
||||
RunID: req.RunID,
|
||||
Payload: map[string]interface{}{"name": tc.Name, "id": tc.ID, "arguments": tc.Arguments},
|
||||
Payload: map[string]interface{}{"name": tc.Name, "id": tc.ID, "arguments": truncateToolArgs(tc.Arguments, 500)},
|
||||
})
|
||||
|
||||
argsJSON, _ := json.Marshal(tc.Arguments)
|
||||
@@ -640,6 +642,7 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error)
|
||||
"id": tc.ID,
|
||||
"is_error": result.IsError,
|
||||
"arguments": tc.Arguments,
|
||||
"result": truncateStr(result.ForLLM, 1000),
|
||||
}
|
||||
if result.IsError && result.ForLLM != "" {
|
||||
toolResultPayload["content"] = result.ForLLM
|
||||
@@ -707,7 +710,7 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error)
|
||||
Type: protocol.AgentEventToolCall,
|
||||
AgentID: l.id,
|
||||
RunID: req.RunID,
|
||||
Payload: map[string]interface{}{"name": tc.Name, "id": tc.ID, "arguments": tc.Arguments},
|
||||
Payload: map[string]interface{}{"name": tc.Name, "id": tc.ID, "arguments": truncateToolArgs(tc.Arguments, 500)},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -780,6 +783,7 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error)
|
||||
"id": r.tc.ID,
|
||||
"is_error": r.result.IsError,
|
||||
"arguments": r.tc.Arguments,
|
||||
"result": truncateStr(r.result.ForLLM, 1000),
|
||||
}
|
||||
if r.result.IsError && r.result.ForLLM != "" {
|
||||
parToolResultPayload["content"] = r.result.ForLLM
|
||||
@@ -862,8 +866,9 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error)
|
||||
}
|
||||
|
||||
pendingMsgs = append(pendingMsgs, providers.Message{
|
||||
Role: "assistant",
|
||||
Content: finalContent,
|
||||
Role: "assistant",
|
||||
Content: finalContent,
|
||||
Thinking: finalThinking,
|
||||
})
|
||||
|
||||
// Flush all buffered messages to session atomically.
|
||||
@@ -937,4 +942,15 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error)
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
// truncateToolArgs returns a copy of arguments with string values truncated to maxLen.
|
||||
func truncateToolArgs(args map[string]interface{}, maxLen int) map[string]interface{} {
|
||||
out := make(map[string]interface{}, len(args))
|
||||
for k, v := range args {
|
||||
if s, ok := v.(string); ok && len(s) > maxLen {
|
||||
out[k] = truncateStr(s, maxLen)
|
||||
} else {
|
||||
out[k] = v
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -106,17 +106,15 @@ func stripGarbledToolXML(content string) string {
|
||||
cleaned := garbledToolXMLPattern.ReplaceAllString(content, "")
|
||||
cleaned = strings.TrimSpace(cleaned)
|
||||
|
||||
if cleaned != "" && hasIndicator {
|
||||
slog.Warn("stripped garbled tool call XML, preserving remaining content",
|
||||
"original_len", len(content),
|
||||
"remaining_len", len(cleaned),
|
||||
)
|
||||
return cleaned
|
||||
}
|
||||
|
||||
if cleaned == "" {
|
||||
slog.Warn("stripped entire response as garbled tool XML", "original_len", len(content))
|
||||
return ""
|
||||
}
|
||||
|
||||
slog.Warn("stripped garbled tool call XML from response",
|
||||
"original_len", len(content),
|
||||
"remaining_len", len(cleaned),
|
||||
)
|
||||
return cleaned
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import { useState } from "react";
|
||||
import { Bot, User, ChevronRight, Check, AlertTriangle, Zap } from "lucide-react";
|
||||
import { Bot, User } from "lucide-react";
|
||||
import { MessageContent } from "./message-content";
|
||||
import { ThinkingBlock } from "./thinking-block";
|
||||
import { ToolCallCard } from "./tool-call-card";
|
||||
import type { ChatMessage } from "@/types/chat";
|
||||
import type { ToolCall } from "@/types/session";
|
||||
|
||||
interface MessageBubbleProps {
|
||||
message: ChatMessage;
|
||||
toolCallErrors?: Map<string, string>;
|
||||
}
|
||||
|
||||
export function MessageBubble({ message, toolCallErrors }: MessageBubbleProps) {
|
||||
export function MessageBubble({ message }: MessageBubbleProps) {
|
||||
const isUser = message.role === "user";
|
||||
const isTool = message.role === "tool";
|
||||
|
||||
@@ -17,25 +16,17 @@ export function MessageBubble({ message, toolCallErrors }: MessageBubbleProps) {
|
||||
return null; // Tool messages are shown inline with assistant messages
|
||||
}
|
||||
|
||||
const isAssistant = message.role === "assistant";
|
||||
const hasThinking = isAssistant && !!message.thinking;
|
||||
const hasToolDetails = isAssistant && message.toolDetails && message.toolDetails.length > 0;
|
||||
const hasToolCalls = isAssistant && message.tool_calls && message.tool_calls.length > 0;
|
||||
const hasContent = !!message.content?.trim();
|
||||
const hasToolCalls = message.tool_calls && message.tool_calls.length > 0;
|
||||
|
||||
// Skip assistant messages with neither text nor tool calls
|
||||
if (!isUser && !hasContent && !hasToolCalls) {
|
||||
// Skip assistant messages with no content at all
|
||||
if (isAssistant && !hasContent && !hasToolCalls && !hasToolDetails) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Tool-call-only assistant message: render compact tool call list
|
||||
if (!isUser && !hasContent && hasToolCalls) {
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
{message.tool_calls!.map((tc) => (
|
||||
<ToolCallItem key={tc.id} toolCall={tc} isError={toolCallErrors?.has(tc.id)} errorContent={toolCallErrors?.get(tc.id)} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`flex gap-3 ${isUser ? "flex-row-reverse" : ""}`}>
|
||||
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full border bg-background">
|
||||
@@ -53,10 +44,15 @@ export function MessageBubble({ message, toolCallErrors }: MessageBubbleProps) {
|
||||
: "bg-card text-card-foreground border border-border shadow-sm"
|
||||
}`}
|
||||
>
|
||||
{hasToolCalls && (
|
||||
<div className="mb-2 space-y-1">
|
||||
{message.tool_calls!.map((tc) => (
|
||||
<ToolCallItem key={tc.id} toolCall={tc} compact isError={toolCallErrors?.has(tc.id)} errorContent={toolCallErrors?.get(tc.id)} />
|
||||
{hasThinking && (
|
||||
<div className="mb-2">
|
||||
<ThinkingBlock text={message.thinking!} />
|
||||
</div>
|
||||
)}
|
||||
{hasToolDetails && (
|
||||
<div className="mb-2">
|
||||
{message.toolDetails!.map((entry) => (
|
||||
<ToolCallCard key={entry.toolCallId} entry={entry} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@@ -73,53 +69,3 @@ export function MessageBubble({ message, toolCallErrors }: MessageBubbleProps) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ToolCallItem({ toolCall, compact, isError, errorContent }: { toolCall: ToolCall; compact?: boolean; isError?: boolean; errorContent?: string }) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const hasArgs = toolCall.arguments && Object.keys(toolCall.arguments).length > 0;
|
||||
const canExpand = hasArgs || (isError && !!errorContent);
|
||||
const iconSize = compact ? "h-3 w-3" : "h-3.5 w-3.5";
|
||||
const isSkill = toolCall.name === "use_skill";
|
||||
|
||||
const StatusIcon = isError
|
||||
? <AlertTriangle className={`${iconSize} shrink-0 text-red-500`} />
|
||||
: isSkill
|
||||
? <Zap className={`${iconSize} shrink-0 text-amber-500`} />
|
||||
: <Check className={`${iconSize} shrink-0 text-green-500`} />;
|
||||
|
||||
return (
|
||||
<div className={compact ? "" : "rounded-md border bg-muted/50 overflow-hidden"}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => canExpand && setExpanded(!expanded)}
|
||||
className={`flex items-center gap-1.5 w-full text-left ${
|
||||
compact
|
||||
? "text-xs text-muted-foreground py-0.5"
|
||||
: "px-3 py-1.5 text-sm hover:bg-muted/80 transition-colors"
|
||||
} ${canExpand ? "cursor-pointer" : "cursor-default"}`}
|
||||
>
|
||||
{StatusIcon}
|
||||
<span className={`font-mono truncate ${compact ? "" : "text-xs"}`}>
|
||||
{isSkill
|
||||
? `skill: ${(toolCall.arguments?.name as string) || "unknown"}`
|
||||
: toolCall.name}
|
||||
</span>
|
||||
{canExpand && (
|
||||
<ChevronRight className={`h-3 w-3 shrink-0 ml-auto text-muted-foreground transition-transform ${expanded ? "rotate-90" : ""}`} />
|
||||
)}
|
||||
</button>
|
||||
{expanded && canExpand && (
|
||||
<div className={`text-[11px] overflow-auto max-h-48 ${
|
||||
compact ? "pl-4.5 pb-1" : "px-3 pb-2 border-t bg-muted/30"
|
||||
}`}>
|
||||
{isError && errorContent && (
|
||||
<pre className="text-red-500 whitespace-pre-wrap">{errorContent}</pre>
|
||||
)}
|
||||
{hasArgs && (
|
||||
<pre className="text-muted-foreground">{JSON.stringify(toolCall.arguments, null, 2)}</pre>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,50 +1,75 @@
|
||||
import { useState } from "react";
|
||||
import { Wrench, Check, AlertTriangle, Loader2, ChevronRight, Zap } from "lucide-react";
|
||||
import { Wrench, Check, AlertTriangle, Loader2, ChevronDown, ChevronRight, Zap } from "lucide-react";
|
||||
import type { ToolStreamEntry } from "@/types/chat";
|
||||
|
||||
const isSkillTool = (name: string) => name === "use_skill";
|
||||
|
||||
/** Build a short summary string from tool arguments for inline display. */
|
||||
function buildToolSummary(entry: ToolStreamEntry): string | null {
|
||||
if (!entry.arguments) return null;
|
||||
const args = entry.arguments;
|
||||
const key = args.path ?? args.command ?? args.query ?? args.url ?? args.name;
|
||||
if (typeof key === "string") return key.length > 60 ? key.slice(0, 57) + "..." : key;
|
||||
return null;
|
||||
}
|
||||
|
||||
interface ToolCallCardProps {
|
||||
entry: ToolStreamEntry;
|
||||
}
|
||||
|
||||
export function ToolCallCard({ entry }: ToolCallCardProps) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const hasArgs = entry.arguments && Object.keys(entry.arguments).length > 0;
|
||||
const hasDetails = entry.arguments || entry.result;
|
||||
const hasError = entry.phase === "error" && !!entry.errorContent;
|
||||
const canExpand = hasArgs || hasError;
|
||||
const canExpand = hasDetails || hasError;
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const summary = buildToolSummary(entry);
|
||||
|
||||
return (
|
||||
<div className="my-1 rounded-md border bg-muted/50 overflow-hidden">
|
||||
<div className="my-1 rounded-md border bg-muted/50 text-sm">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => canExpand && setExpanded(!expanded)}
|
||||
className={`flex items-start gap-2 w-full text-left px-3 py-2 text-sm ${
|
||||
canExpand ? "cursor-pointer hover:bg-muted/80 transition-colors" : "cursor-default"
|
||||
}`}
|
||||
className="flex w-full items-center gap-2 px-3 py-2 text-left"
|
||||
onClick={() => canExpand && setExpanded((v) => !v)}
|
||||
disabled={!canExpand}
|
||||
>
|
||||
<ToolIcon phase={entry.phase} isSkill={isSkillTool(entry.name)} />
|
||||
<span className="font-mono text-xs break-all">
|
||||
<span className="font-medium shrink-0">
|
||||
{isSkillTool(entry.name)
|
||||
? `skill: ${(entry.arguments?.name as string) || "unknown"}`
|
||||
: entry.name}
|
||||
</span>
|
||||
<PhaseLabel phase={entry.phase} isSkill={isSkillTool(entry.name)} />
|
||||
{canExpand && (
|
||||
<ChevronRight
|
||||
className={`h-3 w-3 shrink-0 text-muted-foreground transition-transform ${
|
||||
expanded ? "rotate-90" : ""
|
||||
}`}
|
||||
/>
|
||||
{summary && (
|
||||
<span className="truncate text-xs text-muted-foreground">{summary}</span>
|
||||
)}
|
||||
<span className="ml-auto flex items-center gap-2 shrink-0">
|
||||
<PhaseLabel phase={entry.phase} isSkill={isSkillTool(entry.name)} />
|
||||
{canExpand && (
|
||||
expanded
|
||||
? <ChevronDown className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
: <ChevronRight className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
{expanded && canExpand && (
|
||||
<div className="px-3 pb-2 border-t bg-muted/30 text-[11px] overflow-auto max-h-48">
|
||||
<div className="border-t border-muted px-3 py-2 space-y-2">
|
||||
{hasError && (
|
||||
<pre className="text-red-500 whitespace-pre-wrap">{entry.errorContent}</pre>
|
||||
)}
|
||||
{hasArgs && (
|
||||
<pre className="text-muted-foreground whitespace-pre-wrap break-words">{JSON.stringify(entry.arguments, null, 2)}</pre>
|
||||
{entry.arguments && Object.keys(entry.arguments).length > 0 && (
|
||||
<div>
|
||||
<div className="text-[10px] font-semibold uppercase text-muted-foreground mb-1">Arguments</div>
|
||||
<pre className="whitespace-pre-wrap text-xs font-mono bg-background rounded p-2 max-h-48 overflow-y-auto">
|
||||
{JSON.stringify(entry.arguments, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
{entry.result && (
|
||||
<div>
|
||||
<div className="text-[10px] font-semibold uppercase text-muted-foreground mb-1">Result</div>
|
||||
<pre className="whitespace-pre-wrap text-xs font-mono bg-background rounded p-2 max-h-48 overflow-y-auto">
|
||||
{entry.result}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@@ -87,7 +112,7 @@ function PhaseLabel({ phase, isSkill }: { phase: ToolStreamEntry["phase"]; isSki
|
||||
error: "text-red-500",
|
||||
};
|
||||
return (
|
||||
<span className={`ml-auto text-xs ${colors[phase] ?? "text-muted-foreground"}`}>
|
||||
<span className={`text-xs ${colors[phase] ?? "text-muted-foreground"}`}>
|
||||
{labels[phase] ?? phase}
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useMemo } from "react";
|
||||
import { Circle } from "lucide-react";
|
||||
import { MessageBubble } from "@/components/chat/message-bubble";
|
||||
import { StreamingText } from "@/components/chat/streaming-text";
|
||||
@@ -33,19 +32,6 @@ export function ChatThread({
|
||||
scrollTrigger,
|
||||
);
|
||||
|
||||
// Build map of tool_call_id → error content for tool results that indicate errors
|
||||
const toolCallErrors = useMemo(() => {
|
||||
const errors = new Map<string, string>();
|
||||
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 (
|
||||
@@ -72,7 +58,7 @@ export function ChatThread({
|
||||
>
|
||||
<div className="mx-auto max-w-3xl space-y-4">
|
||||
{messages.map((msg, i) => (
|
||||
<MessageBubble key={`${msg.role}-${i}`} message={msg} toolCallErrors={toolCallErrors} />
|
||||
<MessageBubble key={`${msg.role}-${i}`} message={msg} />
|
||||
))}
|
||||
|
||||
{/* Tool stream during active run */}
|
||||
|
||||
@@ -59,10 +59,37 @@ export function useChatMessages(sessionKey: string, agentId: string) {
|
||||
agentId,
|
||||
sessionKey,
|
||||
});
|
||||
const msgs: ChatMessage[] = (res.messages ?? []).map((m: Message, i: number) => ({
|
||||
...m,
|
||||
timestamp: Date.now() - (res.messages!.length - i) * 1000,
|
||||
}));
|
||||
const allMsgs = res.messages ?? [];
|
||||
// Build a map of tool_call_id -> tool message for result lookup
|
||||
const toolResultMap = new Map<string, Message>();
|
||||
for (const m of allMsgs) {
|
||||
if (m.role === "tool" && m.tool_call_id) {
|
||||
toolResultMap.set(m.tool_call_id, m);
|
||||
}
|
||||
}
|
||||
const msgs: ChatMessage[] = allMsgs.map((m: Message, i: number) => {
|
||||
const chatMsg: ChatMessage = {
|
||||
...m,
|
||||
timestamp: Date.now() - (allMsgs.length - i) * 1000,
|
||||
};
|
||||
// Reconstruct toolDetails for assistant messages with tool_calls
|
||||
if (m.role === "assistant" && m.tool_calls && m.tool_calls.length > 0) {
|
||||
chatMsg.toolDetails = m.tool_calls.map((tc) => {
|
||||
const toolMsg = toolResultMap.get(tc.id);
|
||||
return {
|
||||
toolCallId: tc.id,
|
||||
runId: "",
|
||||
name: tc.name,
|
||||
phase: (toolMsg ? "completed" : "calling") as ToolStreamEntry["phase"],
|
||||
startedAt: 0,
|
||||
updatedAt: 0,
|
||||
arguments: tc.arguments,
|
||||
result: toolMsg?.content,
|
||||
};
|
||||
});
|
||||
}
|
||||
return chatMsg;
|
||||
});
|
||||
setMessages(msgs);
|
||||
} catch {
|
||||
// will retry
|
||||
@@ -153,6 +180,7 @@ export function useChatMessages(sessionKey: string, agentId: string) {
|
||||
...t,
|
||||
phase: isError ? ("error" as const) : ("completed" as const),
|
||||
errorContent: isError ? event.payload?.content : undefined,
|
||||
result: event.payload?.result,
|
||||
updatedAt: now,
|
||||
}
|
||||
: t,
|
||||
|
||||
@@ -10,8 +10,8 @@ import { useDebouncedCallback } from "@/hooks/use-debounced-callback";
|
||||
import { Events } from "@/api/protocol";
|
||||
import { parseSessionKey } from "@/lib/session-key";
|
||||
import { formatDate, formatTokens } from "@/lib/format";
|
||||
import type { SessionInfo, SessionPreview } from "@/types/session";
|
||||
import type { ChatMessage, AgentEventPayload } from "@/types/chat";
|
||||
import type { SessionInfo, SessionPreview, Message } from "@/types/session";
|
||||
import type { ChatMessage, AgentEventPayload, ToolStreamEntry } from "@/types/chat";
|
||||
|
||||
/** Check if a message is an internal system message (subagent results, cron, etc.) */
|
||||
function isSystemMessage(msg: ChatMessage): boolean {
|
||||
@@ -58,11 +58,38 @@ export function SessionDetailPage({
|
||||
onPreview(session.key)
|
||||
.then((preview) => {
|
||||
if (preview) {
|
||||
const allMsgs = preview.messages;
|
||||
// Build a map of tool_call_id -> tool message for result lookup
|
||||
const toolResultMap = new Map<string, Message>();
|
||||
for (const m of allMsgs) {
|
||||
if (m.role === "tool" && m.tool_call_id) {
|
||||
toolResultMap.set(m.tool_call_id, m);
|
||||
}
|
||||
}
|
||||
setMessages(
|
||||
preview.messages.map((m, i) => ({
|
||||
...m,
|
||||
timestamp: Date.now() - (preview.messages.length - i) * 1000,
|
||||
})),
|
||||
allMsgs.map((m, i) => {
|
||||
const chatMsg: ChatMessage = {
|
||||
...m,
|
||||
timestamp: Date.now() - (allMsgs.length - i) * 1000,
|
||||
};
|
||||
// Reconstruct toolDetails for assistant messages with tool_calls
|
||||
if (m.role === "assistant" && m.tool_calls && m.tool_calls.length > 0) {
|
||||
chatMsg.toolDetails = m.tool_calls.map((tc) => {
|
||||
const toolMsg = toolResultMap.get(tc.id);
|
||||
return {
|
||||
toolCallId: tc.id,
|
||||
runId: "",
|
||||
name: tc.name,
|
||||
phase: (toolMsg ? "completed" : "calling") as ToolStreamEntry["phase"],
|
||||
startedAt: 0,
|
||||
updatedAt: 0,
|
||||
arguments: tc.arguments,
|
||||
result: toolMsg?.content,
|
||||
};
|
||||
});
|
||||
}
|
||||
return chatMsg;
|
||||
}),
|
||||
);
|
||||
setSummary(preview.summary ?? null);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { Message } from "./session";
|
||||
export interface ChatMessage extends Message {
|
||||
timestamp?: number;
|
||||
isStreaming?: boolean;
|
||||
toolDetails?: ToolStreamEntry[];
|
||||
}
|
||||
|
||||
/** Agent event payload from WS event "agent" */
|
||||
@@ -21,6 +22,7 @@ export interface AgentEventPayload {
|
||||
is_error?: boolean;
|
||||
error?: string;
|
||||
arguments?: Record<string, unknown>;
|
||||
result?: string;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -31,6 +33,7 @@ export interface ToolStreamEntry {
|
||||
name: string;
|
||||
phase: "calling" | "completed" | "error";
|
||||
arguments?: Record<string, unknown>;
|
||||
result?: string;
|
||||
errorContent?: string;
|
||||
startedAt: number;
|
||||
updatedAt: number;
|
||||
|
||||
@@ -26,6 +26,7 @@ export interface SessionPreview {
|
||||
export interface Message {
|
||||
role: "user" | "assistant" | "tool";
|
||||
content: string;
|
||||
thinking?: string;
|
||||
tool_calls?: ToolCall[];
|
||||
tool_call_id?: string;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user