Show in-progress tool calls
This commit is contained in:
117
web/src/App.tsx
117
web/src/App.tsx
@@ -435,7 +435,7 @@ type ToolLogMetadata = {
|
||||
kind: "tool_call";
|
||||
toolCallId?: string;
|
||||
toolName?: string;
|
||||
status?: "completed" | "failed";
|
||||
status?: "initiated" | "completed" | "failed";
|
||||
summary?: string;
|
||||
args?: Record<string, unknown>;
|
||||
startedAt?: string;
|
||||
@@ -461,28 +461,48 @@ function isDisplayableMessage(message: Message) {
|
||||
}
|
||||
|
||||
function buildOptimisticToolMessage(event: ToolCallEvent): Message {
|
||||
const metadata: ToolLogMetadata = {
|
||||
kind: "tool_call",
|
||||
toolCallId: event.toolCallId,
|
||||
toolName: event.name,
|
||||
status: event.status,
|
||||
summary: event.summary,
|
||||
args: event.args,
|
||||
startedAt: event.startedAt,
|
||||
error: event.error ?? null,
|
||||
resultPreview: event.resultPreview ?? null,
|
||||
};
|
||||
|
||||
if (event.completedAt) metadata.completedAt = event.completedAt;
|
||||
if (typeof event.durationMs === "number") metadata.durationMs = event.durationMs;
|
||||
|
||||
return {
|
||||
id: `temp-tool-${event.toolCallId}`,
|
||||
createdAt: event.completedAt ?? new Date().toISOString(),
|
||||
createdAt: event.completedAt ?? event.startedAt ?? new Date().toISOString(),
|
||||
role: "tool",
|
||||
content: event.summary,
|
||||
name: event.name,
|
||||
metadata: {
|
||||
kind: "tool_call",
|
||||
toolCallId: event.toolCallId,
|
||||
toolName: event.name,
|
||||
status: event.status,
|
||||
summary: event.summary,
|
||||
args: event.args,
|
||||
startedAt: event.startedAt,
|
||||
completedAt: event.completedAt,
|
||||
durationMs: event.durationMs,
|
||||
error: event.error ?? null,
|
||||
resultPreview: event.resultPreview ?? null,
|
||||
} satisfies ToolLogMetadata,
|
||||
metadata,
|
||||
};
|
||||
}
|
||||
|
||||
function upsertOptimisticToolMessage(messages: Message[], event: ToolCallEvent, assistantMessagePrefix: string) {
|
||||
const toolMessage = buildOptimisticToolMessage(event);
|
||||
const existingIndex = messages.findIndex(
|
||||
(message) => asToolLogMetadata(message.metadata)?.toolCallId === event.toolCallId || message.id === `temp-tool-${event.toolCallId}`
|
||||
);
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
return messages.map((message, index) => (index === existingIndex ? { ...toolMessage, id: message.id } : message));
|
||||
}
|
||||
|
||||
const assistantIndex = messages.findIndex(
|
||||
(message, index, all) => index === all.length - 1 && message.id.startsWith(assistantMessagePrefix)
|
||||
);
|
||||
if (assistantIndex < 0) return messages.concat(toolMessage);
|
||||
return [...messages.slice(0, assistantIndex), toolMessage, ...messages.slice(assistantIndex)];
|
||||
}
|
||||
|
||||
type ModelComboboxProps = {
|
||||
options: string[];
|
||||
value: string;
|
||||
@@ -2093,33 +2113,10 @@ export default function App() {
|
||||
setPendingChatStates((current) => {
|
||||
const pendingState = current[chatId];
|
||||
if (!pendingState) return current;
|
||||
if (
|
||||
pendingState.messages.some(
|
||||
(message) =>
|
||||
asToolLogMetadata(message.metadata)?.toolCallId === payload.toolCallId || message.id === `temp-tool-${payload.toolCallId}`
|
||||
)
|
||||
) {
|
||||
return current;
|
||||
}
|
||||
|
||||
const toolMessage = buildOptimisticToolMessage(payload);
|
||||
const assistantIndex = pendingState.messages.findIndex(
|
||||
(message, index, all) => index === all.length - 1 && message.id.startsWith("temp-assistant-")
|
||||
);
|
||||
if (assistantIndex < 0) {
|
||||
return {
|
||||
...current,
|
||||
[chatId]: { messages: pendingState.messages.concat(toolMessage) },
|
||||
};
|
||||
}
|
||||
return {
|
||||
...current,
|
||||
[chatId]: {
|
||||
messages: [
|
||||
...pendingState.messages.slice(0, assistantIndex),
|
||||
toolMessage,
|
||||
...pendingState.messages.slice(assistantIndex),
|
||||
],
|
||||
messages: upsertOptimisticToolMessage(pendingState.messages, payload, "temp-assistant-"),
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -2359,30 +2356,10 @@ export default function App() {
|
||||
setPendingChatStates((current) => {
|
||||
const pendingState = current[chatId];
|
||||
if (!pendingState) return current;
|
||||
if (
|
||||
pendingState.messages.some(
|
||||
(message) =>
|
||||
asToolLogMetadata(message.metadata)?.toolCallId === payload.toolCallId || message.id === `temp-tool-${payload.toolCallId}`
|
||||
)
|
||||
) {
|
||||
return current;
|
||||
}
|
||||
|
||||
const toolMessage = buildOptimisticToolMessage(payload);
|
||||
const assistantIndex = pendingState.messages.findIndex(
|
||||
(message, index, all) => index === all.length - 1 && message.id.startsWith("temp-assistant-")
|
||||
);
|
||||
if (assistantIndex < 0) {
|
||||
return { ...current, [chatId]: { messages: pendingState.messages.concat(toolMessage) } };
|
||||
}
|
||||
return {
|
||||
...current,
|
||||
[chatId]: {
|
||||
messages: [
|
||||
...pendingState.messages.slice(0, assistantIndex),
|
||||
toolMessage,
|
||||
...pendingState.messages.slice(assistantIndex),
|
||||
],
|
||||
messages: upsertOptimisticToolMessage(pendingState.messages, payload, "temp-assistant-"),
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -2649,25 +2626,7 @@ export default function App() {
|
||||
{
|
||||
onToolCall: (payload) => {
|
||||
setQuickQuestionMessages((current) => {
|
||||
if (
|
||||
current.some(
|
||||
(message) =>
|
||||
asToolLogMetadata(message.metadata)?.toolCallId === payload.toolCallId || message.id === `temp-tool-${payload.toolCallId}`
|
||||
)
|
||||
) {
|
||||
return current;
|
||||
}
|
||||
|
||||
const toolMessage = buildOptimisticToolMessage(payload);
|
||||
const assistantIndex = current.findIndex(
|
||||
(message, index, all) => index === all.length - 1 && message.id.startsWith("temp-assistant-quick-")
|
||||
);
|
||||
if (assistantIndex < 0) return current.concat(toolMessage);
|
||||
return [
|
||||
...current.slice(0, assistantIndex),
|
||||
toolMessage,
|
||||
...current.slice(assistantIndex),
|
||||
];
|
||||
return upsertOptimisticToolMessage(current, payload, "temp-assistant-quick-");
|
||||
});
|
||||
},
|
||||
onDelta: (payload) => {
|
||||
|
||||
@@ -14,7 +14,7 @@ type ToolLogMetadata = {
|
||||
kind: "tool_call";
|
||||
toolCallId?: string;
|
||||
toolName?: string;
|
||||
status?: "completed" | "failed";
|
||||
status?: "initiated" | "completed" | "failed";
|
||||
summary?: string;
|
||||
args?: Record<string, unknown>;
|
||||
startedAt?: string;
|
||||
@@ -71,9 +71,17 @@ function formatToolTimestamp(...values: Array<string | null | undefined>) {
|
||||
return new Intl.DateTimeFormat(undefined, { hour: "numeric", minute: "2-digit" }).format(new Date(value));
|
||||
}
|
||||
|
||||
function getToolDetailLabel(message: Message, metadata: ToolLogMetadata, isFailed: boolean) {
|
||||
type ToolCallVisualState = "initiated" | "completed" | "failed";
|
||||
|
||||
function getToolVisualState(metadata: ToolLogMetadata): ToolCallVisualState {
|
||||
if (metadata.status === "failed") return "failed";
|
||||
if (metadata.status === "initiated") return "initiated";
|
||||
return "completed";
|
||||
}
|
||||
|
||||
function getToolDetailLabel(message: Message, metadata: ToolLogMetadata, state: ToolCallVisualState) {
|
||||
return [
|
||||
isFailed ? "Failed" : "Completed",
|
||||
state === "failed" ? "Failed" : state === "initiated" ? "Running" : "Completed",
|
||||
formatDuration(metadata.durationMs),
|
||||
formatToolTimestamp(message.createdAt, metadata.completedAt, metadata.startedAt),
|
||||
]
|
||||
@@ -93,10 +101,12 @@ export function ChatMessagesPanel({ messages, isLoading, isSending }: Props) {
|
||||
if (message.role === "tool" && toolLogMetadata) {
|
||||
const iconKind = getToolIconName(toolLogMetadata.toolName ?? message.name);
|
||||
const Icon = iconKind === "search" ? Globe2 : iconKind === "fetch" ? Link2 : Wrench;
|
||||
const isFailed = toolLogMetadata.status === "failed";
|
||||
const toolState = getToolVisualState(toolLogMetadata);
|
||||
const isFailed = toolState === "failed";
|
||||
const isInitiated = toolState === "initiated";
|
||||
const toolSummary = getToolSummary(message, toolLogMetadata);
|
||||
const toolLabel = getToolLabel(message, toolLogMetadata);
|
||||
const toolDetailLabel = getToolDetailLabel(message, toolLogMetadata, isFailed);
|
||||
const toolDetailLabel = getToolDetailLabel(message, toolLogMetadata, toolState);
|
||||
return (
|
||||
<div key={message.id} className="flex justify-start">
|
||||
<div
|
||||
@@ -104,6 +114,8 @@ export function ChatMessagesPanel({ messages, isLoading, isSending }: Props) {
|
||||
"inline-flex max-w-[85%] min-w-0 items-start gap-3 overflow-hidden rounded-xl border px-3 py-2.5 shadow-[inset_0_1px_0_hsl(180_100%_88%_/_0.06)]",
|
||||
isFailed
|
||||
? "border-rose-400/34 bg-[linear-gradient(90deg,hsl(350_72%_44%_/_0.18),hsl(342_66%_9%_/_0.72))]"
|
||||
: isInitiated
|
||||
? "border-amber-300/34 bg-[linear-gradient(90deg,hsl(43_74%_30%_/_0.34),hsl(260_48%_13%_/_0.74))]"
|
||||
: "border-cyan-400/34 bg-[linear-gradient(90deg,hsl(184_89%_21%_/_0.70),hsl(208_66%_12%_/_0.78))]"
|
||||
)}
|
||||
title={`${toolSummary}\n${toolLabel} • ${toolDetailLabel}`}
|
||||
@@ -111,7 +123,11 @@ export function ChatMessagesPanel({ messages, isLoading, isSending }: Props) {
|
||||
<span
|
||||
className={cn(
|
||||
"mt-0.5 flex h-[30px] w-[30px] shrink-0 items-center justify-center rounded-lg border",
|
||||
isFailed ? "border-rose-400/34 bg-rose-400/13 text-rose-300" : "border-cyan-300/34 bg-cyan-300/13 text-cyan-300"
|
||||
isFailed
|
||||
? "border-rose-400/34 bg-rose-400/13 text-rose-300"
|
||||
: isInitiated
|
||||
? "border-amber-300/34 bg-amber-300/13 text-amber-200"
|
||||
: "border-cyan-300/34 bg-cyan-300/13 text-cyan-300"
|
||||
)}
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
@@ -121,7 +137,7 @@ export function ChatMessagesPanel({ messages, isLoading, isSending }: Props) {
|
||||
{toolSummary}
|
||||
</span>
|
||||
<span className="flex min-w-0 items-center gap-1.5 text-[11px] leading-4">
|
||||
<span className={cn("min-w-0 truncate font-semibold", isFailed ? "text-rose-300/85" : "text-cyan-200/90")}>
|
||||
<span className={cn("min-w-0 truncate font-semibold", isFailed ? "text-rose-300/85" : isInitiated ? "text-amber-200/90" : "text-cyan-200/90")}>
|
||||
{toolLabel}
|
||||
</span>
|
||||
<span className="min-w-0 truncate text-violet-200/64">{toolDetailLabel}</span>
|
||||
|
||||
@@ -45,12 +45,12 @@ export type Message = {
|
||||
export type ToolCallEvent = {
|
||||
toolCallId: string;
|
||||
name: string;
|
||||
status: "completed" | "failed";
|
||||
status: "initiated" | "completed" | "failed";
|
||||
summary: string;
|
||||
args: Record<string, unknown>;
|
||||
startedAt: string;
|
||||
completedAt: string;
|
||||
durationMs: number;
|
||||
completedAt?: string;
|
||||
durationMs?: number;
|
||||
error?: string;
|
||||
resultPreview?: string;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user