Wire @crema/agent-ui: ToolCallCard + AgentAvatar with activity
The /ai surface now renders agent-ui primitives instead of homegrown tool/typing widgets: - AgentAvatar with activity (thinking / working / waiting / speaking / idle) replaces TypingIndicator. Pulses while the model is generating, shows "waiting" while a write is held for confirmation, "working" while a confirmed write is executing, "speaking" once tokens are streaming. - ToolCallCard renders each native tool_call with typed status (pending / running / success / error). Built from the assistant message's toolCalls plus the matching tool result message. Tool messages no longer render standalone — absorbed into their parent assistant turn. - Empty assistant bubbles (no prose, only tool_calls) collapse so the ToolCallCards carry the visual weight. Wiring: add @crema/agent-ui path entry to tsconfig and @source line to app.css. Sibling lib-agent-ui must be cloned next to arcadia-admin. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,7 @@
|
||||
@source "../../lib-search-ui/src";
|
||||
@source "../../lib-feedback-ui/src";
|
||||
@source "../../lib-auth-ui/src";
|
||||
@source "../../lib-agent-ui/src";
|
||||
/* CREMA:SOURCES */
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@@ -62,7 +62,13 @@ import { addLibraryItem } from "~/lib/library"
|
||||
import { Avatar, AvatarFallback } from "~/components/ui/avatar"
|
||||
import { pageTitle } from "~/lib/page-meta"
|
||||
import { useArcadiaClient } from "@crema/arcadia-client"
|
||||
import type { ToolCall } from "@crema/llm-ui"
|
||||
import type { Message as LLMMessage, ToolCall } from "@crema/llm-ui"
|
||||
import {
|
||||
AgentAvatar,
|
||||
ToolCallCard,
|
||||
type ToolCall as AgentToolCall,
|
||||
type ToolCallStatus,
|
||||
} from "@crema/agent-ui"
|
||||
import {
|
||||
buildDenialMessages,
|
||||
classifyCalls,
|
||||
@@ -523,30 +529,60 @@ function ChatSurface({
|
||||
) : (
|
||||
<div className="flex-1 px-4 py-6 sm:px-6">
|
||||
<div className="mx-auto flex w-full max-w-3xl flex-col gap-6">
|
||||
{messages
|
||||
.filter((m) => m.role !== "system")
|
||||
.map((m, i) => (
|
||||
{messages.map((m, i) => {
|
||||
if (m.role === "system" || m.role === "tool") return null
|
||||
const calls =
|
||||
m.role === "assistant" && m.toolCalls
|
||||
? m.toolCalls.map((tc) =>
|
||||
buildAgentToolCall(tc, messages, isStreaming, !!pendingConfirm),
|
||||
)
|
||||
: []
|
||||
const isWritePending =
|
||||
pendingConfirm?.afterIndex === i ? pendingConfirm.writes : null
|
||||
return (
|
||||
<div key={i} className="contents">
|
||||
<MessageRow
|
||||
role={m.role as "user" | "assistant" | "tool"}
|
||||
role={m.role as "user" | "assistant"}
|
||||
content={m.content}
|
||||
toolCalls={m.toolCalls}
|
||||
/>
|
||||
{pendingConfirm?.afterIndex === i && (
|
||||
{calls.length > 0 && (
|
||||
<div className="self-start flex w-full max-w-[80ch] flex-col gap-2">
|
||||
{calls.map((c) => (
|
||||
<ToolCallCard key={c.id} call={c} defaultExpanded={c.status !== "success"} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{isWritePending && (
|
||||
<ConfirmCard
|
||||
calls={pendingConfirm.writes}
|
||||
calls={isWritePending}
|
||||
onConfirm={onConfirmWrites}
|
||||
onDeny={onDenyWrites}
|
||||
busy={confirmBusy}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{isStreaming && messages.at(-1)?.role !== "assistant" && (
|
||||
<div className="self-start">
|
||||
<TypingIndicator />
|
||||
</div>
|
||||
)}
|
||||
)
|
||||
})}
|
||||
{(() => {
|
||||
const activity = deriveAgentActivity({
|
||||
isStreaming,
|
||||
lastMessage: messages.at(-1),
|
||||
pendingConfirm: !!pendingConfirm,
|
||||
confirmBusy,
|
||||
})
|
||||
if (activity === "idle") return null
|
||||
return (
|
||||
<div className="self-start">
|
||||
<AgentAvatar
|
||||
name={activeAgent?.name ?? "Atlas"}
|
||||
activity={activity}
|
||||
initials={activeAgent ? agentInitials(activeAgent.name) : "AT"}
|
||||
showLabel
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
<div
|
||||
ref={endRef}
|
||||
aria-hidden="true"
|
||||
@@ -609,22 +645,82 @@ function ChatSurface({
|
||||
)
|
||||
}
|
||||
|
||||
function deriveAgentActivity({
|
||||
isStreaming,
|
||||
lastMessage,
|
||||
pendingConfirm,
|
||||
confirmBusy,
|
||||
}: {
|
||||
isStreaming: boolean
|
||||
lastMessage: LLMMessage | undefined
|
||||
pendingConfirm: boolean
|
||||
confirmBusy: boolean
|
||||
}): "idle" | "thinking" | "working" | "waiting" | "speaking" {
|
||||
if (confirmBusy) return "working"
|
||||
if (pendingConfirm) return "waiting"
|
||||
if (!isStreaming) return "idle"
|
||||
if (!lastMessage || lastMessage.role !== "assistant") return "thinking"
|
||||
if (lastMessage.content.trim().length > 0) return "speaking"
|
||||
return "thinking"
|
||||
}
|
||||
|
||||
function buildAgentToolCall(
|
||||
tc: ToolCall,
|
||||
allMessages: LLMMessage[],
|
||||
isStreaming: boolean,
|
||||
pendingConfirm: boolean,
|
||||
): AgentToolCall {
|
||||
const result = allMessages.find(
|
||||
(m) => m.role === "tool" && m.toolCallId === tc.id,
|
||||
)
|
||||
let parsedArgs: Record<string, unknown> | undefined
|
||||
try {
|
||||
parsedArgs = tc.arguments ? JSON.parse(tc.arguments) : undefined
|
||||
} catch {
|
||||
parsedArgs = undefined
|
||||
}
|
||||
if (result) {
|
||||
let parsedResult: unknown = result.content
|
||||
let errorMsg: string | undefined
|
||||
try {
|
||||
const obj = JSON.parse(result.content) as Record<string, unknown>
|
||||
if (obj && typeof obj === "object" && typeof obj.error === "string") {
|
||||
errorMsg = obj.error
|
||||
} else {
|
||||
parsedResult = obj
|
||||
}
|
||||
} catch {
|
||||
// leave as raw text
|
||||
}
|
||||
return {
|
||||
id: tc.id,
|
||||
name: tc.name,
|
||||
status: errorMsg ? "error" : "success",
|
||||
args: parsedArgs,
|
||||
result: errorMsg ? undefined : parsedResult,
|
||||
error: errorMsg,
|
||||
}
|
||||
}
|
||||
let status: ToolCallStatus = "running"
|
||||
if (pendingConfirm) status = "pending"
|
||||
else if (!isStreaming) status = "running"
|
||||
return {
|
||||
id: tc.id,
|
||||
name: tc.name,
|
||||
status,
|
||||
args: parsedArgs,
|
||||
}
|
||||
}
|
||||
|
||||
function MessageRow({
|
||||
role,
|
||||
content,
|
||||
toolCalls,
|
||||
}: {
|
||||
role: "user" | "assistant" | "tool"
|
||||
role: "user" | "assistant"
|
||||
content: string
|
||||
toolCalls?: ToolCall[]
|
||||
}) {
|
||||
if (role === "tool") {
|
||||
return (
|
||||
<div className="self-start max-w-[80ch]">
|
||||
<MessageBody content={content} isToolResult />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (role === "user") {
|
||||
return (
|
||||
<div className="self-end">
|
||||
@@ -640,6 +736,9 @@ function MessageRow({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
// Assistant messages with only tool_calls and no prose render nothing here —
|
||||
// the ToolCallCards beneath them carry the visual weight.
|
||||
if (!content.trim()) return null
|
||||
return (
|
||||
<div className="self-start max-w-[80ch]">
|
||||
<MessageBody content={content} toolCalls={toolCalls} />
|
||||
|
||||
@@ -38,6 +38,8 @@
|
||||
"@crema/feedback-ui/*": ["../lib-feedback-ui/src/*"],
|
||||
"@crema/auth-ui": ["../lib-auth-ui/src/index.tsx"],
|
||||
"@crema/auth-ui/*": ["../lib-auth-ui/src/*"],
|
||||
"@crema/agent-ui": ["../lib-agent-ui/src/index.tsx"],
|
||||
"@crema/agent-ui/*": ["../lib-agent-ui/src/*"],
|
||||
"// CREMA:PATHS": [""],
|
||||
"react": ["./node_modules/@types/react"],
|
||||
"react/*": ["./node_modules/@types/react/*"],
|
||||
|
||||
Reference in New Issue
Block a user