diff --git a/frontend/packages/inspector/index.html b/frontend/packages/inspector/index.html index b7b284d..259bce8 100644 --- a/frontend/packages/inspector/index.html +++ b/frontend/packages/inspector/index.html @@ -5,31 +5,36 @@ Sandbox Agent + + + + diff --git a/frontend/packages/inspector/src/components/chat/ChatMessages.tsx b/frontend/packages/inspector/src/components/chat/ChatMessages.tsx index 3c7c2cc..75f1ee1 100644 --- a/frontend/packages/inspector/src/components/chat/ChatMessages.tsx +++ b/frontend/packages/inspector/src/components/chat/ChatMessages.tsx @@ -1,102 +1,150 @@ +import { useState } from "react"; import { getAvatarLabel, getMessageClass } from "./messageUtils"; +import renderContentPart from "./renderContentPart"; import type { TimelineEntry } from "./types"; -import { formatJson } from "../../utils/format"; +import { AlertTriangle, Settings, ChevronRight, ChevronDown } from "lucide-react"; + +const CollapsibleMessage = ({ + id, + icon, + label, + children, + className = "" +}: { + id: string; + icon: React.ReactNode; + label: string; + children: React.ReactNode; + className?: string; +}) => { + const [expanded, setExpanded] = useState(false); + + return ( +
+ + {expanded &&
{children}
} +
+ ); +}; const ChatMessages = ({ entries, sessionError, + eventError, messagesEndRef }: { entries: TimelineEntry[]; sessionError: string | null; + eventError: string | null; messagesEndRef: React.RefObject; }) => { return (
{entries.map((entry) => { - const messageClass = getMessageClass(entry); - if (entry.kind === "meta") { - return ( -
-
{getAvatarLabel(messageClass)}
-
-
- {entry.meta?.title ?? "Status"} -
- {entry.meta?.detail &&
{entry.meta.detail}
} + const isError = entry.meta?.severity === "error"; + const title = entry.meta?.title ?? "Status"; + const isStatusDivider = ["Session Started", "Turn Started", "Turn Ended"].includes(title); + + if (isStatusDivider) { + return ( +
+
+ + + {title} + +
-
+ ); + } + + // Other status messages - collapsible + return ( + : } + label={title} + className={isError ? "error" : "system"} + > + {entry.meta?.detail &&
{entry.meta.detail}
} +
); } - if (entry.kind === "reasoning") { + const item = entry.item; + if (!item) return null; + const hasParts = (item.content ?? []).length > 0; + const isInProgress = item.status === "in_progress"; + const isFailed = item.status === "failed"; + const messageClass = getMessageClass(item); + const statusLabel = item.status !== "completed" ? item.status.replace("_", " ") : ""; + const kindLabel = item.kind.replace("_", " "); + const isTool = messageClass === "tool"; + + // Tool results - collapsible + if (isTool) { return ( -
-
AI
-
-
- reasoning - {entry.reasoning?.visibility ?? "public"} -
-
{entry.reasoning?.text ?? ""}
-
-
+ T} + label={`${kindLabel}${statusLabel ? ` (${statusLabel})` : ""}`} + className="tool" + > + {hasParts ? ( + (item.content ?? []).map(renderContentPart) + ) : entry.deltaText ? ( + {entry.deltaText} + ) : ( + No content. + )} + ); } - if (entry.kind === "tool") { - const isComplete = entry.toolStatus === "completed" || entry.toolStatus === "failed"; - const isFailed = entry.toolStatus === "failed"; - return ( -
-
{getAvatarLabel(isFailed ? "error" : "tool")}
-
+ return ( +
+ {!isFailed &&
{getAvatarLabel(messageClass)}
} +
+ {(item.kind !== "message" || item.status !== "completed") && (
- tool call - {entry.toolName} - {entry.toolStatus && entry.toolStatus !== "completed" && ( - - {entry.toolStatus.replace("_", " ")} + {isFailed && } + {kindLabel} + {statusLabel && ( + + {statusLabel} )}
- {entry.toolInput &&
{entry.toolInput}
} - {isComplete && entry.toolOutput && ( -
-
result
-
{entry.toolOutput}
-
- )} - {!isComplete && !entry.toolInput && ( - - - - - - )} -
-
- ); - } - - // Message (user or assistant) - return ( -
-
{getAvatarLabel(messageClass)}
-
- {entry.text ? ( -
{entry.text}
- ) : ( + )} + {hasParts ? ( + (item.content ?? []).map(renderContentPart) + ) : entry.deltaText ? ( + + {entry.deltaText} + {isInProgress && } + + ) : isInProgress ? ( + ) : ( + No content yet. )}
); })} {sessionError &&
{sessionError}
} + {eventError &&
{eventError}
}
); diff --git a/frontend/packages/inspector/src/components/chat/messageUtils.ts b/frontend/packages/inspector/src/components/chat/messageUtils.ts deleted file mode 100644 index 6abdb7a..0000000 --- a/frontend/packages/inspector/src/components/chat/messageUtils.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { TimelineEntry } from "./types"; - -export const getMessageClass = (entry: TimelineEntry) => { - if (entry.kind === "tool") return "tool"; - if (entry.kind === "meta") return entry.meta?.severity === "error" ? "error" : "system"; - if (entry.kind === "reasoning") return "assistant"; - if (entry.role === "user") return "user"; - return "assistant"; -}; - -export const getAvatarLabel = (messageClass: string) => { - if (messageClass === "user") return "U"; - if (messageClass === "tool") return "T"; - if (messageClass === "system") return "S"; - if (messageClass === "error") return "!"; - return "AI"; -}; diff --git a/frontend/packages/inspector/src/components/chat/messageUtils.tsx b/frontend/packages/inspector/src/components/chat/messageUtils.tsx new file mode 100644 index 0000000..fba8962 --- /dev/null +++ b/frontend/packages/inspector/src/components/chat/messageUtils.tsx @@ -0,0 +1,20 @@ +import type { UniversalItem } from "sandbox-agent"; +import { Settings, AlertTriangle, User } from "lucide-react"; +import type { ReactNode } from "react"; + +export const getMessageClass = (item: UniversalItem) => { + if (item.kind === "tool_call" || item.kind === "tool_result") return "tool"; + if (item.kind === "system" || item.kind === "status") return "system"; + if (item.role === "user") return "user"; + if (item.role === "tool") return "tool"; + if (item.role === "system") return "system"; + return "assistant"; +}; + +export const getAvatarLabel = (messageClass: string): ReactNode => { + if (messageClass === "user") return ; + if (messageClass === "tool") return "T"; + if (messageClass === "system") return ; + if (messageClass === "error") return ; + return "AI"; +};