Merge pull request #83 from multica-ai/feat/issue-82-render-tool-call-styles
feat: tool call rendering and error handling
This commit is contained in:
commit
bef2e901cb
14 changed files with 705 additions and 184 deletions
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||
import { useMessagesStore } from '@multica/store'
|
||||
import type { ContentBlock } from '@multica/sdk'
|
||||
|
||||
interface UseLocalChatOptions {
|
||||
agentId: string
|
||||
|
|
@ -45,7 +46,15 @@ export function useLocalChat({ agentId }: UseLocalChatOptions): UseLocalChatRetu
|
|||
try {
|
||||
const result = await window.electronAPI.localChat.getHistory(agentId)
|
||||
if (result.messages && result.messages.length > 0) {
|
||||
useMessagesStore.getState().loadMessages(result.messages)
|
||||
// Normalize: IPC may return content as string, store expects ContentBlock[]
|
||||
useMessagesStore.getState().loadMessages(
|
||||
result.messages.map((m: Record<string, unknown>) => ({
|
||||
...m,
|
||||
content: typeof m.content === 'string'
|
||||
? (m.content ? [{ type: 'text' as const, text: m.content }] : [])
|
||||
: (m.content ?? []),
|
||||
})) as import('@multica/store').Message[]
|
||||
)
|
||||
}
|
||||
} catch {
|
||||
// History load is best-effort
|
||||
|
|
@ -74,18 +83,17 @@ export function useLocalChat({ agentId }: UseLocalChatOptions): UseLocalChatRetu
|
|||
if (agentEvent.type === 'message_start') {
|
||||
currentStreamRef.current = streamId
|
||||
store.startStream(streamId, agentId)
|
||||
// Extract initial text if any
|
||||
const text = extractTextFromAgentEvent(agentEvent)
|
||||
if (text) store.appendStream(streamId, text)
|
||||
const content = extractContentFromAgentEvent(agentEvent)
|
||||
if (content.length) store.appendStream(streamId, content)
|
||||
} else if (agentEvent.type === 'message_update') {
|
||||
const text = extractTextFromAgentEvent(agentEvent)
|
||||
if (text && currentStreamRef.current) {
|
||||
store.appendStream(currentStreamRef.current, text)
|
||||
const content = extractContentFromAgentEvent(agentEvent)
|
||||
if (content.length && currentStreamRef.current) {
|
||||
store.appendStream(currentStreamRef.current, content)
|
||||
}
|
||||
} else if (agentEvent.type === 'message_end') {
|
||||
const text = extractTextFromAgentEvent(agentEvent)
|
||||
const content = extractContentFromAgentEvent(agentEvent)
|
||||
if (currentStreamRef.current) {
|
||||
store.endStream(currentStreamRef.current, text)
|
||||
store.endStream(currentStreamRef.current, content)
|
||||
currentStreamRef.current = null
|
||||
}
|
||||
setIsLoading(false)
|
||||
|
|
@ -131,14 +139,9 @@ export function useLocalChat({ agentId }: UseLocalChatOptions): UseLocalChatRetu
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract text content from AgentEvent message.
|
||||
* Same logic as @multica/sdk extractTextFromEvent.
|
||||
*/
|
||||
function extractTextFromAgentEvent(event: { message?: { content?: Array<{ type: string; text?: string }> } }): string {
|
||||
if (!event.message?.content) return ''
|
||||
return event.message.content
|
||||
.filter((c): c is { type: 'text'; text: string } => c.type === 'text' && !!c.text)
|
||||
.map((c) => c.text)
|
||||
.join('')
|
||||
/** Extract content blocks from AgentEvent message */
|
||||
function extractContentFromAgentEvent(event: { message?: { content?: unknown } }): ContentBlock[] {
|
||||
if (!event.message?.content) return []
|
||||
const content = event.message.content
|
||||
return Array.isArray(content) ? content as ContentBlock[] : []
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@
|
|||
"uuid": "^13.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mariozechner/pi-agent-core": "^0.50.3",
|
||||
"@mariozechner/pi-ai": "^0.50.3",
|
||||
"@types/uuid": "^11.0.0",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,9 +33,11 @@ export {
|
|||
export {
|
||||
StreamAction,
|
||||
type StreamPayload,
|
||||
type StreamEvent,
|
||||
type StreamMessageEvent,
|
||||
type StreamToolEvent,
|
||||
extractTextFromEvent,
|
||||
type AgentEvent,
|
||||
type ContentBlock,
|
||||
type TextContent,
|
||||
type ThinkingContent,
|
||||
type ToolCall,
|
||||
type ImageContent,
|
||||
extractThinkingFromEvent,
|
||||
} from "./stream";
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
/** RPC Actions - 请求/响应模式 */
|
||||
|
||||
import type { Message } from "@mariozechner/pi-ai";
|
||||
|
||||
export const RequestAction = "request" as const;
|
||||
export const ResponseAction = "response" as const;
|
||||
|
||||
|
|
@ -65,34 +67,11 @@ export interface GetAgentMessagesParams {
|
|||
limit?: number;
|
||||
}
|
||||
|
||||
/** Content block types from the agent engine */
|
||||
export interface TextContentBlock {
|
||||
type: "text";
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface ThinkingContentBlock {
|
||||
type: "thinking";
|
||||
thinking: string;
|
||||
}
|
||||
|
||||
export interface ToolCallBlock {
|
||||
type: "tool_use";
|
||||
id: string;
|
||||
name: string;
|
||||
input: unknown;
|
||||
}
|
||||
|
||||
export interface ImageContentBlock {
|
||||
type: "image";
|
||||
url: string;
|
||||
}
|
||||
|
||||
/** Agent message returned by getAgentMessages (mirrors pi-ai Message) */
|
||||
export type AgentMessageItem =
|
||||
| { role: "user"; content: string | (TextContentBlock | ImageContentBlock)[]; timestamp: number }
|
||||
| { role: "assistant"; content: (TextContentBlock | ThinkingContentBlock | ToolCallBlock)[]; timestamp: number }
|
||||
| { role: "tool_result"; toolCallId: string; content: (TextContentBlock | ImageContentBlock)[]; isError: boolean; timestamp: number }
|
||||
/**
|
||||
* Agent message returned by getAgentMessages.
|
||||
* This is pi-ai's Message type — the backend returns it as-is from SessionManager.loadMessages().
|
||||
*/
|
||||
export type AgentMessageItem = Message;
|
||||
|
||||
/** getAgentMessages - response payload */
|
||||
export interface GetAgentMessagesResult {
|
||||
|
|
|
|||
|
|
@ -1,59 +1,51 @@
|
|||
/** Stream Action - 流式消息传输 */
|
||||
/** Stream Action */
|
||||
|
||||
export const StreamAction = "stream" as const;
|
||||
|
||||
// --- Content block types (re-exported from pi-ai, the single source of truth) ---
|
||||
|
||||
import type {
|
||||
TextContent,
|
||||
ThinkingContent,
|
||||
ToolCall,
|
||||
ImageContent,
|
||||
} from "@mariozechner/pi-ai";
|
||||
import type { AgentEvent } from "@mariozechner/pi-agent-core";
|
||||
|
||||
export type { TextContent, ThinkingContent, ToolCall, ImageContent };
|
||||
export type { AgentEvent };
|
||||
|
||||
/**
|
||||
* AgentEvent types forwarded by the Hub to frontend clients.
|
||||
* These mirror the subset of AgentEvent from @mariozechner/pi-agent-core
|
||||
* that the Hub forwards (filtered at the Hub layer).
|
||||
* Convenience union of all content block types across message roles.
|
||||
*
|
||||
* NOTE: This is a deliberate simplification. The backend uses narrower unions
|
||||
* per role (e.g. AssistantMessage.content excludes ImageContent, UserMessage
|
||||
* excludes ThinkingContent/ToolCall). We accept the wider union on the frontend
|
||||
* for simpler handling — the backend already guarantees correctness.
|
||||
*/
|
||||
export interface StreamMessageEvent {
|
||||
type: "message_start" | "message_update" | "message_end";
|
||||
message: {
|
||||
id?: string;
|
||||
role: string;
|
||||
content?: Array<{ type: string; text?: string; thinking?: string }>;
|
||||
};
|
||||
assistantMessageEvent?: unknown;
|
||||
}
|
||||
export type ContentBlock = TextContent | ThinkingContent | ToolCall | ImageContent;
|
||||
|
||||
export interface StreamToolEvent {
|
||||
type: "tool_execution_start" | "tool_execution_end";
|
||||
toolCallId: string;
|
||||
toolName: string;
|
||||
args?: unknown;
|
||||
result?: unknown;
|
||||
isError?: boolean;
|
||||
}
|
||||
// --- Stream event types ---
|
||||
|
||||
export type StreamEvent = StreamMessageEvent | StreamToolEvent;
|
||||
|
||||
/** 流消息 payload — wraps a raw AgentEvent with stream/agent identifiers */
|
||||
/**
|
||||
* Hub forwards AgentEvent from pi-agent-core as-is.
|
||||
* StreamPayload wraps it with routing metadata.
|
||||
*/
|
||||
export interface StreamPayload {
|
||||
/** 流 ID,关联同一个流的所有消息 */
|
||||
streamId: string;
|
||||
/** 所属 agent ID */
|
||||
agentId: string;
|
||||
/** Raw agent event from the engine */
|
||||
event: StreamEvent;
|
||||
event: AgentEvent;
|
||||
}
|
||||
|
||||
/** Extract plain text from an AgentMessage content array */
|
||||
export function extractTextFromEvent(event: StreamMessageEvent): string {
|
||||
const content = event.message?.content;
|
||||
/** Extract thinking/reasoning content from an AgentEvent that carries a message */
|
||||
export function extractThinkingFromEvent(event: AgentEvent): string {
|
||||
if (!("message" in event)) return "";
|
||||
const msg = event.message;
|
||||
if (!msg || !("content" in msg)) return "";
|
||||
const content = msg.content;
|
||||
if (!Array.isArray(content)) return "";
|
||||
return content
|
||||
.filter((c) => c.type === "text")
|
||||
.map((c) => c.text ?? "")
|
||||
.join("");
|
||||
}
|
||||
|
||||
/** Extract thinking/reasoning content from an AgentMessage content array */
|
||||
export function extractThinkingFromEvent(event: StreamMessageEvent): string {
|
||||
const content = event.message?.content;
|
||||
if (!Array.isArray(content)) return "";
|
||||
return content
|
||||
.filter((c) => c.type === "thinking")
|
||||
.filter((c): c is ThinkingContent => c.type === "thinking")
|
||||
.map((c) => c.thinking ?? "")
|
||||
.join("");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,13 +19,13 @@ import { v7 as uuidv7 } from "uuid"
|
|||
import {
|
||||
GatewayClient,
|
||||
StreamAction,
|
||||
extractTextFromEvent,
|
||||
type ConnectionState,
|
||||
type SendErrorResponse,
|
||||
type StreamPayload,
|
||||
type StreamMessageEvent,
|
||||
type AgentEvent,
|
||||
type GetAgentMessagesResult,
|
||||
type ContentBlock,
|
||||
} from "@multica/sdk"
|
||||
import { useMessagesStore } from "./messages"
|
||||
import { useMessagesStore, type Message } from "./messages"
|
||||
import { clearConnection, type ConnectionInfo } from "./connection"
|
||||
|
||||
interface ConnectionStoreState {
|
||||
|
|
@ -34,7 +34,7 @@ interface ConnectionStoreState {
|
|||
hubId: string | null
|
||||
agentId: string | null
|
||||
connectionState: ConnectionState
|
||||
lastError: SendErrorResponse | null
|
||||
lastError: { code: string; message: string } | null
|
||||
}
|
||||
|
||||
interface ConnectionStoreActions {
|
||||
|
|
@ -104,34 +104,61 @@ function createClient(
|
|||
switch (event.type) {
|
||||
case "message_start": {
|
||||
store.startStream(payload.streamId, payload.agentId)
|
||||
const text = extractTextFromEvent(event as StreamMessageEvent)
|
||||
if (text) store.appendStream(payload.streamId, text)
|
||||
const content = extractContent(event)
|
||||
if (content.length) store.appendStream(payload.streamId, content)
|
||||
break
|
||||
}
|
||||
case "message_update": {
|
||||
const text = extractTextFromEvent(event as StreamMessageEvent)
|
||||
store.appendStream(payload.streamId, text)
|
||||
const content = extractContent(event)
|
||||
store.appendStream(payload.streamId, content)
|
||||
break
|
||||
}
|
||||
case "message_end": {
|
||||
const text = extractTextFromEvent(event as StreamMessageEvent)
|
||||
store.endStream(payload.streamId, text)
|
||||
const content = extractContent(event)
|
||||
const stopReason = "message" in event
|
||||
? (event.message as { stopReason?: string })?.stopReason
|
||||
: undefined
|
||||
store.endStream(payload.streamId, content, stopReason)
|
||||
break
|
||||
}
|
||||
case "tool_execution_start":
|
||||
case "tool_execution_end":
|
||||
case "tool_execution_start": {
|
||||
store.startToolExecution(
|
||||
payload.agentId,
|
||||
event.toolCallId,
|
||||
event.toolName,
|
||||
event.args,
|
||||
)
|
||||
break
|
||||
}
|
||||
case "tool_execution_end": {
|
||||
store.endToolExecution(
|
||||
event.toolCallId,
|
||||
event.result,
|
||||
event.isError,
|
||||
)
|
||||
break
|
||||
}
|
||||
case "tool_execution_update":
|
||||
// Partial results — not rendered yet, ignored for now
|
||||
break
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Handle error messages from Hub (e.g. UNAUTHORIZED)
|
||||
if (msg.action === "error") {
|
||||
const payload = msg.payload as { code: string; message: string }
|
||||
set({ lastError: { code: payload.code, message: payload.message } })
|
||||
return
|
||||
}
|
||||
|
||||
// Handle direct (non-streaming) messages
|
||||
const payload = msg.payload as { agentId?: string; content?: string }
|
||||
if (payload?.agentId && payload?.content) {
|
||||
useMessagesStore.getState().addAssistantMessage(payload.content, payload.agentId)
|
||||
}
|
||||
})
|
||||
.onSendError((error) => set({ lastError: error }))
|
||||
.onSendError((error) => set({ lastError: { code: error.code, message: error.error } }))
|
||||
}
|
||||
|
||||
/** Fetch message history from Hub via RPC after connection is established */
|
||||
|
|
@ -140,20 +167,55 @@ async function fetchHistory(state: ConnectionStoreState): Promise<void> {
|
|||
if (!client || !hubId || !agentId) return
|
||||
|
||||
try {
|
||||
const result = await client.request<{
|
||||
messages: Array<{ role: string; content: unknown }>
|
||||
total: number
|
||||
}>(hubId, "getAgentMessages", { agentId, limit: 200 })
|
||||
const result = await client.request<GetAgentMessagesResult>(
|
||||
hubId, "getAgentMessages", { agentId, limit: 200 },
|
||||
)
|
||||
|
||||
const messages = result.messages
|
||||
.filter((m) => m.role === "user" || m.role === "assistant")
|
||||
.map((m) => ({
|
||||
id: uuidv7(),
|
||||
role: m.role as "user" | "assistant",
|
||||
content: extractText(m.content),
|
||||
agentId: agentId,
|
||||
}))
|
||||
.filter((m) => m.content.length > 0)
|
||||
// Build a lookup map: toolCallId → { name, arguments } from assistant ToolCall blocks
|
||||
const toolCallArgsMap = new Map<string, { name: string; args: Record<string, unknown> }>()
|
||||
for (const m of result.messages) {
|
||||
if (m.role === "assistant") {
|
||||
for (const block of m.content) {
|
||||
if (block.type === "toolCall") {
|
||||
toolCallArgsMap.set(block.id, { name: block.name, args: block.arguments })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mirror the backend message array directly
|
||||
const messages: Message[] = []
|
||||
for (const m of result.messages) {
|
||||
if (m.role === "user") {
|
||||
messages.push({
|
||||
id: uuidv7(),
|
||||
role: "user",
|
||||
content: toContentBlocks(m.content),
|
||||
agentId,
|
||||
})
|
||||
} else if (m.role === "assistant") {
|
||||
messages.push({
|
||||
id: uuidv7(),
|
||||
role: "assistant",
|
||||
content: toContentBlocks(m.content),
|
||||
agentId,
|
||||
stopReason: m.stopReason,
|
||||
})
|
||||
} else if (m.role === "toolResult") {
|
||||
const callInfo = toolCallArgsMap.get(m.toolCallId)
|
||||
messages.push({
|
||||
id: uuidv7(),
|
||||
role: "toolResult",
|
||||
content: toContentBlocks(m.content),
|
||||
agentId,
|
||||
toolCallId: m.toolCallId,
|
||||
toolName: m.toolName,
|
||||
toolArgs: callInfo?.args,
|
||||
toolStatus: m.isError ? "error" : "success",
|
||||
isError: m.isError,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (messages.length > 0) {
|
||||
useMessagesStore.getState().loadMessages(messages)
|
||||
|
|
@ -163,14 +225,22 @@ async function fetchHistory(state: ConnectionStoreState): Promise<void> {
|
|||
}
|
||||
}
|
||||
|
||||
/** Extract plain text from AgentMessage content (string or content block array) */
|
||||
function extractText(content: unknown): string {
|
||||
if (typeof content === "string") return content
|
||||
if (!Array.isArray(content)) return ""
|
||||
return content
|
||||
.filter((c: { type?: string }) => c.type === "text")
|
||||
.map((c: { text?: string }) => c.text ?? "")
|
||||
.join("")
|
||||
/** Convert raw backend content (string or block array) to ContentBlock[] */
|
||||
function toContentBlocks(content: string | ContentBlock[]): ContentBlock[] {
|
||||
if (typeof content === "string") {
|
||||
return content ? [{ type: "text", text: content }] : []
|
||||
}
|
||||
if (Array.isArray(content)) return content
|
||||
return []
|
||||
}
|
||||
|
||||
/** Extract content blocks from an AgentEvent that carries a message */
|
||||
function extractContent(event: AgentEvent): ContentBlock[] {
|
||||
if (!("message" in event)) return []
|
||||
const msg = event.message
|
||||
if (!msg || !("content" in msg)) return []
|
||||
const content = msg.content
|
||||
return Array.isArray(content) ? content as ContentBlock[] : []
|
||||
}
|
||||
|
||||
export const useConnectionStore = create<ConnectionStore>()(
|
||||
|
|
|
|||
|
|
@ -2,6 +2,6 @@ export { useConnectionStore } from "./connection-store"
|
|||
export type { ConnectionStore } from "./connection-store"
|
||||
export { useAutoConnect } from "./use-auto-connect"
|
||||
export { useMessagesStore } from "./messages"
|
||||
export type { Message, MessagesStore, SendContext } from "./messages"
|
||||
export type { Message, MessagesStore, SendContext, ToolStatus } from "./messages"
|
||||
export { parseConnectionCode, saveConnection, loadConnection, clearConnection } from "./connection"
|
||||
export type { ConnectionInfo } from "./connection"
|
||||
|
|
|
|||
|
|
@ -1,28 +1,33 @@
|
|||
/**
|
||||
* Messages Store - manages chat messages and streaming state for the current Agent
|
||||
* Messages Store - manages chat messages and streaming state
|
||||
*
|
||||
* Responsibilities:
|
||||
* 1. Store current Agent's chat messages (replaced on Agent switch, not accumulated)
|
||||
* 2. Manage streaming state (intermediate state while AI replies arrive in chunks)
|
||||
* 3. Provide sendMessage() as the single entry point for sending messages
|
||||
* Data model mirrors the backend (pi-ai / pi-agent-core) exactly:
|
||||
* - UserMessage: { role: "user", content: ContentBlock[] }
|
||||
* - AssistantMessage: { role: "assistant", content: ContentBlock[] }
|
||||
* - ToolResultMessage: { role: "toolResult", toolCallId, toolName, content, isError }
|
||||
*
|
||||
* Send flow:
|
||||
* user input → sendMessage(text)
|
||||
* → addUserMessage() immediately adds to local state (optimistic update)
|
||||
* → ConnectionStore.send() sends to Gateway → Hub → Agent
|
||||
*
|
||||
* Receive flow (driven by ConnectionStore's onMessage callback):
|
||||
* Streaming: startStream → appendStream (repeated) → endStream
|
||||
* Non-streaming: addAssistantMessage (one-shot)
|
||||
* Streaming simply updates the content of the current assistant message in-place.
|
||||
* Tool execution events (start/end) create / update toolResult messages.
|
||||
*/
|
||||
import { create } from "zustand"
|
||||
import { v7 as uuidv7 } from "uuid"
|
||||
import type { ContentBlock } from "@multica/sdk"
|
||||
|
||||
export type ToolStatus = "running" | "success" | "error" | "interrupted"
|
||||
|
||||
export interface Message {
|
||||
id: string
|
||||
role: "user" | "assistant"
|
||||
content: string
|
||||
role: "user" | "assistant" | "toolResult"
|
||||
content: ContentBlock[]
|
||||
agentId: string
|
||||
// AssistantMessage metadata
|
||||
stopReason?: string
|
||||
// ToolResult fields (only when role === "toolResult")
|
||||
toolCallId?: string
|
||||
toolName?: string
|
||||
toolArgs?: Record<string, unknown>
|
||||
toolStatus?: ToolStatus
|
||||
isError?: boolean
|
||||
}
|
||||
|
||||
/** Parameters needed to route a message through the gateway */
|
||||
|
|
@ -41,13 +46,16 @@ interface MessagesActions {
|
|||
sendMessage: (text: string, ctx: SendContext) => void
|
||||
addUserMessage: (content: string, agentId: string) => void
|
||||
addAssistantMessage: (content: string, agentId: string) => void
|
||||
updateMessage: (id: string, content: string) => void
|
||||
// Replace all messages (for Agent switch or loading history)
|
||||
updateMessage: (id: string, content: ContentBlock[]) => void
|
||||
loadMessages: (msgs: Message[]) => void
|
||||
clearMessages: () => void
|
||||
// Streaming
|
||||
startStream: (streamId: string, agentId: string) => void
|
||||
appendStream: (streamId: string, content: string) => void
|
||||
endStream: (streamId: string, content: string) => void
|
||||
appendStream: (streamId: string, content: ContentBlock[]) => void
|
||||
endStream: (streamId: string, content: ContentBlock[], stopReason?: string) => void
|
||||
// Tool execution lifecycle
|
||||
startToolExecution: (agentId: string, toolCallId: string, toolName: string, args?: unknown) => void
|
||||
endToolExecution: (toolCallId: string, result?: unknown, isError?: boolean) => void
|
||||
}
|
||||
|
||||
export type MessagesStore = MessagesState & MessagesActions
|
||||
|
|
@ -56,7 +64,6 @@ export const useMessagesStore = create<MessagesStore>()((set, get) => ({
|
|||
messages: [],
|
||||
streamingIds: new Set<string>(),
|
||||
|
||||
// Single entry point for sending: optimistic local add, then send via WebSocket
|
||||
sendMessage: (text, ctx) => {
|
||||
get().addUserMessage(text, ctx.agentId)
|
||||
ctx.send(ctx.hubId, "message", { agentId: ctx.agentId, content: text })
|
||||
|
|
@ -64,13 +71,23 @@ export const useMessagesStore = create<MessagesStore>()((set, get) => ({
|
|||
|
||||
addUserMessage: (content, agentId) => {
|
||||
set((s) => ({
|
||||
messages: [...s.messages, { id: uuidv7(), role: "user", content, agentId }],
|
||||
messages: [...s.messages, {
|
||||
id: uuidv7(),
|
||||
role: "user",
|
||||
content: [{ type: "text" as const, text: content }],
|
||||
agentId,
|
||||
}],
|
||||
}))
|
||||
},
|
||||
|
||||
addAssistantMessage: (content, agentId) => {
|
||||
set((s) => ({
|
||||
messages: [...s.messages, { id: uuidv7(), role: "assistant", content, agentId }],
|
||||
messages: [...s.messages, {
|
||||
id: uuidv7(),
|
||||
role: "assistant",
|
||||
content: [{ type: "text" as const, text: content }],
|
||||
agentId,
|
||||
}],
|
||||
}))
|
||||
},
|
||||
|
||||
|
|
@ -80,7 +97,6 @@ export const useMessagesStore = create<MessagesStore>()((set, get) => ({
|
|||
}))
|
||||
},
|
||||
|
||||
// Replace all messages (for Agent switch or loading history)
|
||||
loadMessages: (msgs) => {
|
||||
set({ messages: msgs })
|
||||
},
|
||||
|
|
@ -89,35 +105,79 @@ export const useMessagesStore = create<MessagesStore>()((set, get) => ({
|
|||
set({ messages: [], streamingIds: new Set() })
|
||||
},
|
||||
|
||||
// === The following three methods are called by ConnectionStore's onMessage callback ===
|
||||
// Stream start: create an empty placeholder message and mark as streaming
|
||||
// --- Streaming: build assistant message incrementally ---
|
||||
|
||||
startStream: (streamId, agentId) => {
|
||||
set((s) => {
|
||||
const ids = new Set(s.streamingIds)
|
||||
ids.add(streamId)
|
||||
return {
|
||||
messages: [...s.messages, { id: streamId, role: "assistant" as const, content: "", agentId }],
|
||||
messages: [...s.messages, { id: streamId, role: "assistant" as const, content: [], agentId }],
|
||||
streamingIds: ids,
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// Stream update: replace message content (each update carries the full accumulated text)
|
||||
// Replace the entire content array with the latest partial snapshot
|
||||
appendStream: (streamId, content) => {
|
||||
set((s) => ({
|
||||
messages: s.messages.map((m) => (m.id === streamId ? { ...m, content } : m)),
|
||||
}))
|
||||
},
|
||||
|
||||
// Stream end: write final content, remove streaming marker
|
||||
endStream: (streamId, content) => {
|
||||
endStream: (streamId, content, stopReason) => {
|
||||
set((s) => {
|
||||
const ids = new Set(s.streamingIds)
|
||||
ids.delete(streamId)
|
||||
// Find the agentId of the stream being ended to scope tool interruption
|
||||
const streamMsg = s.messages.find((m) => m.id === streamId)
|
||||
const streamAgentId = streamMsg?.agentId
|
||||
return {
|
||||
messages: s.messages.map((m) => (m.id === streamId ? { ...m, content } : m)),
|
||||
messages: s.messages.map((m) => {
|
||||
if (m.id === streamId) return { ...m, content, stopReason }
|
||||
// Interrupt running tool executions belonging to the same agent
|
||||
if (m.role === "toolResult" && m.toolStatus === "running" && m.agentId === streamAgentId) {
|
||||
return { ...m, toolStatus: "interrupted" as ToolStatus }
|
||||
}
|
||||
return m
|
||||
}),
|
||||
streamingIds: ids,
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// --- Tool execution: create / update toolResult messages ---
|
||||
|
||||
startToolExecution: (agentId, toolCallId, toolName, args) => {
|
||||
set((s) => ({
|
||||
messages: [...s.messages, {
|
||||
id: uuidv7(),
|
||||
role: "toolResult" as const,
|
||||
content: [],
|
||||
agentId,
|
||||
toolCallId,
|
||||
toolName,
|
||||
toolArgs: args as Record<string, unknown> | undefined,
|
||||
toolStatus: "running" as ToolStatus,
|
||||
isError: false,
|
||||
}],
|
||||
}))
|
||||
},
|
||||
|
||||
endToolExecution: (toolCallId, result, isError) => {
|
||||
set((s) => ({
|
||||
messages: s.messages.map((m) =>
|
||||
m.role === "toolResult" && m.toolCallId === toolCallId
|
||||
? {
|
||||
...m,
|
||||
toolStatus: (isError ? "error" : "success") as ToolStatus,
|
||||
isError: isError ?? false,
|
||||
content: result != null
|
||||
? [{ type: "text" as const, text: typeof result === "string" ? result : JSON.stringify(result) }]
|
||||
: [],
|
||||
}
|
||||
: m
|
||||
),
|
||||
}))
|
||||
},
|
||||
}))
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ export function Chat() {
|
|||
const agentId = useConnectionStore((s) => s.agentId)
|
||||
const gwState = useConnectionStore((s) => s.connectionState)
|
||||
const hubId = useConnectionStore((s) => s.hubId)
|
||||
const lastError = useConnectionStore((s) => s.lastError)
|
||||
|
||||
const messages = useMessagesStore((s) => s.messages)
|
||||
const streamingIds = useMessagesStore((s) => s.streamingIds)
|
||||
|
|
@ -65,6 +66,23 @@ export function Chat() {
|
|||
)}
|
||||
</main>
|
||||
|
||||
{/* Error banner */}
|
||||
{lastError && (
|
||||
<div className="px-4 py-2 max-w-4xl mx-auto w-full" role="alert" aria-live="polite">
|
||||
<div className="rounded-md bg-destructive/10 text-destructive text-sm px-3 py-2 flex items-center justify-between">
|
||||
<span>{lastError.message} ({lastError.code})</span>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Dismiss error"
|
||||
onClick={() => useConnectionStore.setState({ lastError: null })}
|
||||
className="text-destructive/60 hover:text-destructive ml-2 text-xs focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 rounded outline-none"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="w-full p-2 pt-1 max-w-4xl mx-auto">
|
||||
<ChatInput
|
||||
|
|
|
|||
|
|
@ -1,44 +1,102 @@
|
|||
"use client";
|
||||
|
||||
import { memo, useMemo } from "react";
|
||||
import { MemoizedMarkdown } from "@multica/ui/components/markdown";
|
||||
import { StreamingMarkdown } from "@multica/ui/components/markdown/StreamingMarkdown";
|
||||
import { cn } from "@multica/ui/lib/utils";
|
||||
import { ToolCallItem } from "@multica/ui/components/tool-call-item";
|
||||
import { cn, getTextContent } from "@multica/ui/lib/utils";
|
||||
import type { Message } from "@multica/store";
|
||||
import type { ContentBlock, ToolCall } from "@multica/sdk";
|
||||
|
||||
/** Extract toolCall blocks from content */
|
||||
function getToolCalls(blocks: ContentBlock[]): ToolCall[] {
|
||||
return blocks.filter((b): b is ToolCall => b.type === "toolCall")
|
||||
}
|
||||
|
||||
/** Build a synthetic "running" toolResult Message from a ToolCall block */
|
||||
function toRunningMessage(tc: ToolCall, agentId: string): Message {
|
||||
return {
|
||||
id: tc.id,
|
||||
role: "toolResult",
|
||||
content: [],
|
||||
agentId,
|
||||
toolCallId: tc.id,
|
||||
toolName: tc.name,
|
||||
toolArgs: tc.arguments,
|
||||
toolStatus: "running",
|
||||
}
|
||||
}
|
||||
|
||||
interface MessageListProps {
|
||||
messages: Message[]
|
||||
streamingIds: Set<string>
|
||||
}
|
||||
|
||||
export function MessageList({ messages, streamingIds }: MessageListProps) {
|
||||
export const MessageList = memo(function MessageList({ messages, streamingIds }: MessageListProps) {
|
||||
// Build a set of toolCallIds that already have a toolResult message,
|
||||
// so we don't render duplicate items from the assistant's toolCall blocks
|
||||
const resolvedToolCallIds = useMemo(() => {
|
||||
const ids = new Set<string>()
|
||||
for (const msg of messages) {
|
||||
if (msg.role === "toolResult" && msg.toolCallId) {
|
||||
ids.add(msg.toolCallId)
|
||||
}
|
||||
}
|
||||
return ids
|
||||
}, [messages])
|
||||
|
||||
return (
|
||||
<div className="relative px-4 py-6 space-y-6 max-w-4xl mx-auto">
|
||||
<div className="relative px-4 py-6 max-w-4xl mx-auto">
|
||||
{messages.map((msg) => {
|
||||
// ToolResult messages → render as tool execution item
|
||||
if (msg.role === "toolResult") {
|
||||
return <ToolCallItem key={msg.id} message={msg} />
|
||||
}
|
||||
|
||||
const text = getTextContent(msg.content)
|
||||
const toolCalls = msg.role === "assistant" ? getToolCalls(msg.content) : []
|
||||
const isStreaming = streamingIds.has(msg.id)
|
||||
|
||||
// Find toolCall blocks that don't have a toolResult message yet —
|
||||
// these are tools the LLM decided to call but haven't started executing
|
||||
const unresolvedToolCalls = toolCalls.filter((tc) => !resolvedToolCallIds.has(tc.id))
|
||||
|
||||
// Skip completely empty messages (no text, no unresolved tools, not streaming)
|
||||
if (!text && unresolvedToolCalls.length === 0 && !isStreaming) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
key={msg.id}
|
||||
className={cn(
|
||||
"flex",
|
||||
msg.role === "user" ? "justify-end" : "justify-start"
|
||||
<div key={msg.id}>
|
||||
{/* Render text content (if any) */}
|
||||
{(text || isStreaming) && (
|
||||
<div
|
||||
className={cn(
|
||||
"flex",
|
||||
msg.role === "user" ? "justify-end" : "justify-start"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
msg.role === "user" ? "bg-muted rounded-md max-w-[60%] py-1 px-2.5 my-2" : "w-full py-1 px-2.5 my-1"
|
||||
)}
|
||||
>
|
||||
{isStreaming ? (
|
||||
<StreamingMarkdown content={text} isStreaming={true} mode="minimal" />
|
||||
) : (
|
||||
<MemoizedMarkdown mode="minimal" id={msg.id}>
|
||||
{text}
|
||||
</MemoizedMarkdown>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
msg.role === "user" ? "bg-muted rounded-md max-w-[60%] p-1 px-2.5" : "w-full p-1 px-2.5"
|
||||
)}
|
||||
>
|
||||
{isStreaming ? (
|
||||
<StreamingMarkdown content={msg.content} isStreaming={true} mode="minimal" />
|
||||
) : (
|
||||
<MemoizedMarkdown mode="minimal" id={msg.id}>
|
||||
{msg.content}
|
||||
</MemoizedMarkdown>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Render unresolved toolCall blocks as "running" tool items */}
|
||||
{unresolvedToolCalls.map((tc) => (
|
||||
<ToolCallItem key={tc.id} message={toRunningMessage(tc, msg.agentId)} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
|
|
|||
224
packages/ui/src/components/tool-call-item.tsx
Normal file
224
packages/ui/src/components/tool-call-item.tsx
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
"use client"
|
||||
|
||||
import { memo, useState } from "react"
|
||||
import { HugeiconsIcon } from "@hugeicons/react"
|
||||
import {
|
||||
File01Icon,
|
||||
FloppyDiskIcon,
|
||||
FileEditIcon,
|
||||
CommandLineIcon,
|
||||
Search01Icon,
|
||||
FolderOpenIcon,
|
||||
GlobeIcon,
|
||||
DatabaseIcon,
|
||||
GitBranchIcon,
|
||||
ArrowRight01Icon,
|
||||
} from "@hugeicons/core-free-icons"
|
||||
import { cn, getTextContent } from "@multica/ui/lib/utils"
|
||||
import type { Message } from "@multica/store"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tool display config
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const TOOL_DISPLAY: Record<string, { label: string; icon: typeof File01Icon }> = {
|
||||
read: { label: "Read", icon: File01Icon },
|
||||
write: { label: "Write", icon: FloppyDiskIcon },
|
||||
edit: { label: "Edit", icon: FileEditIcon },
|
||||
exec: { label: "Exec", icon: CommandLineIcon },
|
||||
bash: { label: "Exec", icon: CommandLineIcon },
|
||||
process: { label: "Process", icon: CommandLineIcon },
|
||||
grep: { label: "Grep", icon: Search01Icon },
|
||||
find: { label: "Find", icon: Search01Icon },
|
||||
ls: { label: "ListDir", icon: FolderOpenIcon },
|
||||
glob: { label: "Glob", icon: Search01Icon },
|
||||
web_search: { label: "WebSearch", icon: GlobeIcon },
|
||||
web_fetch: { label: "WebFetch", icon: GlobeIcon },
|
||||
memory_get: { label: "MemoryGet", icon: DatabaseIcon },
|
||||
memory_set: { label: "MemorySet", icon: DatabaseIcon },
|
||||
memory_delete: { label: "MemoryDelete", icon: DatabaseIcon },
|
||||
memory_list: { label: "MemoryList", icon: DatabaseIcon },
|
||||
sessions_spawn: { label: "SpawnSession", icon: GitBranchIcon },
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Extract a short basename from a file path */
|
||||
function basename(path: string): string {
|
||||
return path.split("/").pop() ?? path
|
||||
}
|
||||
|
||||
/** Smart subtitle based on tool type and args */
|
||||
function getSubtitle(toolName: string, args?: Record<string, unknown>): string {
|
||||
if (!args) return ""
|
||||
switch (toolName) {
|
||||
case "read":
|
||||
case "write":
|
||||
case "edit":
|
||||
return args.path ? basename(String(args.path)) : ""
|
||||
case "exec":
|
||||
case "bash":
|
||||
case "process": {
|
||||
const cmd = String(args.command ?? args.cmd ?? "")
|
||||
return cmd.length > 60 ? cmd.slice(0, 57) + "…" : cmd
|
||||
}
|
||||
case "grep":
|
||||
case "find":
|
||||
return args.pattern ? String(args.pattern) : ""
|
||||
case "glob":
|
||||
return args.pattern ? String(args.pattern) : ""
|
||||
case "web_search":
|
||||
return args.query ? String(args.query) : ""
|
||||
case "web_fetch":
|
||||
try { return new URL(String(args.url)).hostname } catch { return String(args.url ?? "") }
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
/** Running-state label per tool */
|
||||
const RUNNING_LABELS: Record<string, string> = {
|
||||
read: "reading…",
|
||||
write: "writing…",
|
||||
edit: "editing…",
|
||||
exec: "running…",
|
||||
bash: "running…",
|
||||
process: "running…",
|
||||
grep: "searching…",
|
||||
find: "searching…",
|
||||
glob: "searching…",
|
||||
web_search: "searching…",
|
||||
web_fetch: "fetching…",
|
||||
}
|
||||
|
||||
/** Stats derived from tool result content */
|
||||
function getStats(toolName: string, toolStatus: string, resultText: string): string {
|
||||
if (toolStatus === "running") return RUNNING_LABELS[toolName] ?? "working…"
|
||||
if (toolStatus === "error" || toolStatus === "interrupted" || !resultText) return ""
|
||||
|
||||
switch (toolName) {
|
||||
case "read": {
|
||||
const lines = resultText.split("\n").length
|
||||
return `${lines} lines`
|
||||
}
|
||||
case "grep": {
|
||||
const matches = resultText.split("\n").filter((l) => l.trim()).length
|
||||
return matches > 0 ? `${matches} matches` : ""
|
||||
}
|
||||
case "glob":
|
||||
case "find": {
|
||||
const files = resultText.split("\n").filter((l) => l.trim()).length
|
||||
return files > 0 ? `${files} files` : ""
|
||||
}
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Component
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const ToolCallItem = memo(function ToolCallItem({ message }: { message: Message }) {
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const { toolName = "", toolStatus = "running", toolArgs, content } = message
|
||||
|
||||
const display = TOOL_DISPLAY[toolName] ?? { label: toolName, icon: CommandLineIcon }
|
||||
const isFinished = toolStatus !== "running"
|
||||
const resultText = getTextContent(content)
|
||||
const hasDetails = isFinished && !!resultText
|
||||
const subtitle = getSubtitle(toolName, toolArgs)
|
||||
const stats = getStats(toolName, toolStatus, resultText)
|
||||
|
||||
return (
|
||||
<div className="py-0.5 px-2.5 text-sm text-muted-foreground">
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`${display.label}${subtitle ? ` ${subtitle}` : ""} — ${toolStatus}`}
|
||||
aria-expanded={hasDetails ? expanded : undefined}
|
||||
onClick={() => hasDetails && setExpanded(!expanded)}
|
||||
className={cn(
|
||||
"group flex w-full items-center gap-1.5 rounded px-1.5 py-0.5",
|
||||
"text-left transition-[color,background-color]",
|
||||
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 outline-none",
|
||||
hasDetails && "hover:bg-muted/30 cursor-pointer",
|
||||
!hasDetails && "cursor-default",
|
||||
)}
|
||||
>
|
||||
{/* Status dot */}
|
||||
<span
|
||||
className={cn(
|
||||
"size-1.5 rounded-full shrink-0",
|
||||
toolStatus === "running" && "bg-[var(--tool-running)] motion-safe:animate-[glow-pulse_2s_ease-in-out_infinite]",
|
||||
toolStatus === "success" && "bg-[var(--tool-success)]",
|
||||
toolStatus === "error" && "bg-[var(--tool-error)]",
|
||||
toolStatus === "interrupted" && "bg-[var(--tool-error)]",
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Tool icon */}
|
||||
<HugeiconsIcon
|
||||
icon={display.icon}
|
||||
strokeWidth={2}
|
||||
className={cn("size-3.5 shrink-0", toolStatus === "error" && "text-[var(--tool-error)]")}
|
||||
/>
|
||||
|
||||
{/* Tool label */}
|
||||
<span className={cn(
|
||||
"font-medium shrink-0",
|
||||
toolStatus === "error" && "text-[var(--tool-error)]",
|
||||
toolStatus === "interrupted" && "text-[var(--tool-error)]",
|
||||
)}>
|
||||
{display.label}
|
||||
</span>
|
||||
|
||||
{/* Smart subtitle */}
|
||||
{subtitle && (
|
||||
<span className="text-muted-foreground/60 truncate min-w-0">
|
||||
{subtitle}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Right-aligned stats */}
|
||||
{stats && (
|
||||
<span className={cn(
|
||||
"ml-auto text-xs text-muted-foreground/60 shrink-0",
|
||||
"font-[tabular-nums]",
|
||||
toolStatus === "running" && "motion-safe:animate-pulse",
|
||||
)}>
|
||||
{stats}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Chevron — visible on hover when expandable */}
|
||||
{hasDetails && (
|
||||
<HugeiconsIcon
|
||||
icon={ArrowRight01Icon}
|
||||
strokeWidth={2}
|
||||
className={cn(
|
||||
"size-3 text-muted-foreground/40 shrink-0",
|
||||
"transition-[transform,opacity] duration-150",
|
||||
!stats && "ml-auto",
|
||||
"opacity-0 group-hover:opacity-100",
|
||||
expanded && "rotate-90 opacity-100",
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Expanded result */}
|
||||
{expanded && resultText && (
|
||||
<div
|
||||
role="region"
|
||||
aria-label={`${display.label} result`}
|
||||
tabIndex={0}
|
||||
className="mt-1 ml-7 text-xs bg-muted rounded p-2 max-h-48 overflow-y-auto whitespace-pre-wrap break-all"
|
||||
>
|
||||
{resultText}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
|
@ -1,6 +1,15 @@
|
|||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import type { ContentBlock } from "@multica/sdk"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
||||
/** Extract concatenated plain text from a ContentBlock array */
|
||||
export function getTextContent(blocks: ContentBlock[]): string {
|
||||
return blocks
|
||||
.filter((b): b is { type: "text"; text: string } => b.type === "text")
|
||||
.map((b) => b.text)
|
||||
.join("")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -86,6 +86,9 @@
|
|||
--scrollbar-thumb: oklch(0.82 0.003 286);
|
||||
--scrollbar-thumb-hover: oklch(0.705 0.015 286.067);
|
||||
--scrollbar-track: transparent;
|
||||
--tool-running: oklch(0.6 0.18 250);
|
||||
--tool-success: oklch(0.72 0.12 145);
|
||||
--tool-error: oklch(0.65 0.2 25);
|
||||
}
|
||||
|
||||
.dark {
|
||||
|
|
@ -123,6 +126,9 @@
|
|||
--scrollbar-thumb: oklch(1 0 0 / 15%);
|
||||
--scrollbar-thumb-hover: oklch(1 0 0 / 30%);
|
||||
--scrollbar-track: transparent;
|
||||
--tool-running: oklch(0.65 0.2 250);
|
||||
--tool-success: oklch(0.65 0.15 145);
|
||||
--tool-error: oklch(0.7 0.2 22);
|
||||
}
|
||||
|
||||
/* Shiki dual themes: CSS-only light/dark switching via CSS variables */
|
||||
|
|
@ -213,3 +219,9 @@
|
|||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
/* Tool status: running glow pulse */
|
||||
@keyframes glow-pulse {
|
||||
0%, 100% { box-shadow: 0 0 0 0 var(--tool-running); }
|
||||
50% { box-shadow: 0 0 0 3px oklch(0.6 0.2 250 / 0); }
|
||||
}
|
||||
|
|
|
|||
96
pnpm-lock.yaml
generated
96
pnpm-lock.yaml
generated
|
|
@ -427,6 +427,12 @@ importers:
|
|||
specifier: ^13.0.0
|
||||
version: 13.0.0
|
||||
devDependencies:
|
||||
'@mariozechner/pi-agent-core':
|
||||
specifier: ^0.50.3
|
||||
version: 0.50.3(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@4.3.6))(ws@8.18.3)(zod@4.3.6)
|
||||
'@mariozechner/pi-ai':
|
||||
specifier: ^0.50.3
|
||||
version: 0.50.3(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@4.3.6))(ws@8.18.3)(zod@4.3.6)
|
||||
'@types/uuid':
|
||||
specifier: ^11.0.0
|
||||
version: 11.0.0
|
||||
|
|
@ -9094,6 +9100,12 @@ snapshots:
|
|||
optionalDependencies:
|
||||
zod: 3.25.76
|
||||
|
||||
'@anthropic-ai/sdk@0.71.2(zod@4.3.6)':
|
||||
dependencies:
|
||||
json-schema-to-ts: 3.1.1
|
||||
optionalDependencies:
|
||||
zod: 4.3.6
|
||||
|
||||
'@aws-crypto/crc32@5.2.0':
|
||||
dependencies:
|
||||
'@aws-crypto/util': 5.2.0
|
||||
|
|
@ -10876,6 +10888,17 @@ snapshots:
|
|||
- supports-color
|
||||
- utf-8-validate
|
||||
|
||||
'@google/genai@1.34.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@4.3.6))':
|
||||
dependencies:
|
||||
google-auth-library: 10.5.0
|
||||
ws: 8.18.3
|
||||
optionalDependencies:
|
||||
'@modelcontextprotocol/sdk': 1.25.3(hono@4.11.7)(zod@4.3.6)
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
|
||||
'@hono/node-server@1.19.9(hono@4.11.7)':
|
||||
dependencies:
|
||||
hono: 4.11.7
|
||||
|
|
@ -11218,6 +11241,19 @@ snapshots:
|
|||
- ws
|
||||
- zod
|
||||
|
||||
'@mariozechner/pi-agent-core@0.50.3(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@4.3.6))(ws@8.18.3)(zod@4.3.6)':
|
||||
dependencies:
|
||||
'@mariozechner/pi-ai': 0.50.3(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@4.3.6))(ws@8.18.3)(zod@4.3.6)
|
||||
'@mariozechner/pi-tui': 0.50.3
|
||||
transitivePeerDependencies:
|
||||
- '@modelcontextprotocol/sdk'
|
||||
- aws-crt
|
||||
- bufferutil
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
- ws
|
||||
- zod
|
||||
|
||||
'@mariozechner/pi-ai@0.50.3(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@3.25.76))(ws@8.18.3)(zod@3.25.76)':
|
||||
dependencies:
|
||||
'@anthropic-ai/sdk': 0.71.2(zod@3.25.76)
|
||||
|
|
@ -11242,6 +11278,30 @@ snapshots:
|
|||
- ws
|
||||
- zod
|
||||
|
||||
'@mariozechner/pi-ai@0.50.3(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@4.3.6))(ws@8.18.3)(zod@4.3.6)':
|
||||
dependencies:
|
||||
'@anthropic-ai/sdk': 0.71.2(zod@4.3.6)
|
||||
'@aws-sdk/client-bedrock-runtime': 3.978.0
|
||||
'@google/genai': 1.34.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@4.3.6))
|
||||
'@mistralai/mistralai': 1.10.0
|
||||
'@sinclair/typebox': 0.34.48
|
||||
ajv: 8.17.1
|
||||
ajv-formats: 3.0.1(ajv@8.17.1)
|
||||
chalk: 5.6.2
|
||||
openai: 6.10.0(ws@8.18.3)(zod@4.3.6)
|
||||
partial-json: 0.1.7
|
||||
proxy-agent: 6.5.0
|
||||
undici: 7.19.2
|
||||
zod-to-json-schema: 3.25.1(zod@4.3.6)
|
||||
transitivePeerDependencies:
|
||||
- '@modelcontextprotocol/sdk'
|
||||
- aws-crt
|
||||
- bufferutil
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
- ws
|
||||
- zod
|
||||
|
||||
'@mariozechner/pi-coding-agent@0.50.3(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@3.25.76))(ws@8.18.3)(zod@3.25.76)':
|
||||
dependencies:
|
||||
'@mariozechner/clipboard': 0.3.0
|
||||
|
|
@ -11305,6 +11365,29 @@ snapshots:
|
|||
- hono
|
||||
- supports-color
|
||||
|
||||
'@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@4.3.6)':
|
||||
dependencies:
|
||||
'@hono/node-server': 1.19.9(hono@4.11.7)
|
||||
ajv: 8.17.1
|
||||
ajv-formats: 3.0.1(ajv@8.17.1)
|
||||
content-type: 1.0.5
|
||||
cors: 2.8.6
|
||||
cross-spawn: 7.0.6
|
||||
eventsource: 3.0.7
|
||||
eventsource-parser: 3.0.6
|
||||
express: 5.2.1
|
||||
express-rate-limit: 7.5.1(express@5.2.1)
|
||||
jose: 6.1.3
|
||||
json-schema-typed: 8.0.2
|
||||
pkce-challenge: 5.0.1
|
||||
raw-body: 3.0.2
|
||||
zod: 4.3.6
|
||||
zod-to-json-schema: 3.25.1(zod@4.3.6)
|
||||
transitivePeerDependencies:
|
||||
- hono
|
||||
- supports-color
|
||||
optional: true
|
||||
|
||||
'@mozilla/readability@0.6.0': {}
|
||||
|
||||
'@mswjs/interceptors@0.40.0':
|
||||
|
|
@ -14376,7 +14459,7 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)):
|
||||
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)):
|
||||
dependencies:
|
||||
debug: 3.2.7
|
||||
optionalDependencies:
|
||||
|
|
@ -14407,7 +14490,7 @@ snapshots:
|
|||
doctrine: 2.1.0
|
||||
eslint: 9.39.2(jiti@2.6.1)
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))
|
||||
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1))
|
||||
hasown: 2.0.2
|
||||
is-core-module: 2.16.1
|
||||
is-glob: 4.0.3
|
||||
|
|
@ -17124,6 +17207,11 @@ snapshots:
|
|||
ws: 8.18.3
|
||||
zod: 3.25.76
|
||||
|
||||
openai@6.10.0(ws@8.18.3)(zod@4.3.6):
|
||||
optionalDependencies:
|
||||
ws: 8.18.3
|
||||
zod: 4.3.6
|
||||
|
||||
optionator@0.9.4:
|
||||
dependencies:
|
||||
deep-is: 0.1.4
|
||||
|
|
@ -19414,6 +19502,10 @@ snapshots:
|
|||
dependencies:
|
||||
zod: 3.25.76
|
||||
|
||||
zod-to-json-schema@3.25.1(zod@4.3.6):
|
||||
dependencies:
|
||||
zod: 4.3.6
|
||||
|
||||
zod-validation-error@4.0.2(zod@4.3.6):
|
||||
dependencies:
|
||||
zod: 4.3.6
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue