feat(chat): stream paseo tool calls

This commit is contained in:
Miniputer
2026-07-21 17:06:12 +05:30
parent 59777e60b8
commit 677a355397
8 changed files with 211 additions and 7 deletions

View File

@@ -7,13 +7,14 @@ import { getMessageText, isMessageStreaming } from "@/lib/chat/transforms";
import type { ChatMessageProps } from "@/lib/chat/types";
import { AssistantIdentity } from "./assistant-identity";
import { ChatToolCall } from "./chat-tool-call";
export const ChatMessage = ({ message }: ChatMessageProps) => {
const isUser = message.role === "user";
const isStreaming = isMessageStreaming(message);
const text = getMessageText(message);
if (!text && !isStreaming) {
if (message.parts.length === 0 && !isStreaming) {
return null;
}
@@ -63,6 +64,11 @@ export const ChatMessage = ({ message }: ChatMessageProps) => {
: "w-full max-w-none overflow-visible leading-7 text-foreground/95"
}
>
{message.parts.map((part) =>
part.type === "dynamic-tool" ? (
<ChatToolCall key={part.toolCallId} part={part} />
) : null
)}
{content}
</BubbleContent>
</Bubble>

View File

@@ -0,0 +1,41 @@
import { Check, LoaderCircle, Terminal, TriangleAlert } from "lucide-react";
import type { ChatToolCallProps } from "@/lib/chat/types";
export const ChatToolCall = ({ part }: ChatToolCallProps) => {
let detail =
typeof part.input === "string"
? part.input
: JSON.stringify(part.input, null, 2);
let Icon = LoaderCircle;
let status = "Running";
if (part.state === "output-available") {
detail =
typeof part.output === "string"
? part.output
: JSON.stringify(part.output, null, 2);
Icon = Check;
status = "Completed";
} else if (part.state === "output-error") {
detail = part.errorText;
Icon = TriangleAlert;
status = "Failed";
}
const isRunning = part.state === "input-available";
return (
<details className="group/tool overflow-hidden rounded-xl border border-border/60 bg-card/45 text-xs">
<summary className="flex cursor-pointer list-none items-center gap-2 px-3 py-2 text-muted-foreground marker:hidden">
<Terminal className="size-3.5 shrink-0" />
<span className="min-w-0 flex-1 truncate font-medium text-foreground/85">
{part.toolName}
</span>
<span>{status}</span>
<Icon className={`size-3.5 ${isRunning ? "animate-spin" : ""}`} />
</summary>
<pre className="max-h-72 overflow-auto border-t border-border/50 bg-background/60 px-3 py-2 font-mono text-[11px] leading-5 whitespace-pre-wrap text-muted-foreground">
{detail}
</pre>
</details>
);
};

View File

@@ -6,7 +6,7 @@ export const CHAT_AGENT = {
name: "zopu",
} as const;
export const MODEL_LABEL = "MiniMax M3";
export const MODEL_LABEL = "GLM-5.2";
export const SUGGESTIONS = [
["Think it through", "Help me make a difficult decision"],

View File

@@ -24,11 +24,15 @@ export const getMessageText = (message: FlueConversationMessage): string =>
);
export const isMessageStreaming = (message: FlueConversationMessage): boolean =>
message.parts.some(
(part) =>
message.parts.some((part) => {
if (part.type === "dynamic-tool") {
return part.state === "input-available";
}
return (
(part.type === "text" || part.type === "reasoning") &&
part.state === "streaming"
);
);
});
export const getStatusDotClass = (status: AgentStatus): string => {
if (status === "error") {

View File

@@ -1,4 +1,8 @@
import type { AgentStatus, FlueConversationMessage } from "@flue/react";
import type {
AgentStatus,
FlueConversationMessage,
FlueConversationPart,
} from "@flue/react";
export interface ChatAgentState {
error?: Error;
@@ -29,4 +33,8 @@ export interface ChatMessageProps {
message: FlueConversationMessage;
}
export interface ChatToolCallProps {
part: Extract<FlueConversationPart, { type: "dynamic-tool" }>;
}
export type AssistantResponseState = "thinking" | "writing";