mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-16 21:03:46 +00:00
chore: recover lisbon workspace state
This commit is contained in:
parent
5d65013aa5
commit
053417e85e
272 changed files with 1884 additions and 43241 deletions
|
|
@ -8,7 +8,7 @@
|
|||
<!-- Preconnect to font providers -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
|
|
@ -51,8 +51,7 @@
|
|||
|
||||
body {
|
||||
color: var(--text);
|
||||
font-family: 'Manrope', system-ui, sans-serif;
|
||||
font-weight: 500;
|
||||
font-family: 'Open Sans', system-ui, sans-serif;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
|
|
@ -1348,6 +1347,13 @@
|
|||
padding: 16px;
|
||||
}
|
||||
|
||||
.chat-conversation {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.messages-container:has(.empty-state) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type { TranscriptEntry } from "@sandbox-agent/react";
|
||||
import { BookOpen } from "lucide-react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
|
|
@ -23,7 +24,6 @@ type AgentModeInfo = { id: string; name: string; description: string };
|
|||
type AgentModelInfo = { id: string; name?: string };
|
||||
import { IndexedDbSessionPersistDriver } from "@sandbox-agent/persist-indexeddb";
|
||||
import ChatPanel from "./components/chat/ChatPanel";
|
||||
import type { TimelineEntry } from "./components/chat/types";
|
||||
import ConnectScreen from "./components/ConnectScreen";
|
||||
import DebugPanel, { type DebugTab } from "./components/debug/DebugPanel";
|
||||
import SessionSidebar from "./components/SessionSidebar";
|
||||
|
|
@ -921,7 +921,7 @@ export default function App() {
|
|||
|
||||
// Build transcript entries from raw SessionEvents
|
||||
const transcriptEntries = useMemo(() => {
|
||||
const entries: TimelineEntry[] = [];
|
||||
const entries: TranscriptEntry[] = [];
|
||||
|
||||
// Accumulators for streaming chunks
|
||||
let assistantAccumId: string | null = null;
|
||||
|
|
@ -954,7 +954,7 @@ export default function App() {
|
|||
};
|
||||
|
||||
// Track tool calls by ID for updates
|
||||
const toolEntryMap = new Map<string, TimelineEntry>();
|
||||
const toolEntryMap = new Map<string, TranscriptEntry>();
|
||||
|
||||
for (const event of events) {
|
||||
const payload = event.payload as Record<string, unknown>;
|
||||
|
|
@ -1068,7 +1068,7 @@ export default function App() {
|
|||
if (update.title) existing.toolName = update.title as string;
|
||||
existing.time = time;
|
||||
} else {
|
||||
const entry: TimelineEntry = {
|
||||
const entry: TranscriptEntry = {
|
||||
id: `tool-${toolCallId}`,
|
||||
eventId: event.id,
|
||||
kind: "tool",
|
||||
|
|
|
|||
|
|
@ -1,37 +0,0 @@
|
|||
import { Send } from "lucide-react";
|
||||
|
||||
const ChatInput = ({
|
||||
message,
|
||||
onMessageChange,
|
||||
onSendMessage,
|
||||
onKeyDown,
|
||||
placeholder,
|
||||
disabled
|
||||
}: {
|
||||
message: string;
|
||||
onMessageChange: (value: string) => void;
|
||||
onSendMessage: () => void;
|
||||
onKeyDown: (event: React.KeyboardEvent<HTMLTextAreaElement>) => void;
|
||||
placeholder: string;
|
||||
disabled: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<div className="input-container">
|
||||
<div className="input-wrapper">
|
||||
<textarea
|
||||
value={message}
|
||||
onChange={(event) => onMessageChange(event.target.value)}
|
||||
onKeyDown={onKeyDown}
|
||||
placeholder={placeholder}
|
||||
rows={1}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<button className="send-button" onClick={onSendMessage} disabled={disabled || !message.trim()}>
|
||||
<Send />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatInput;
|
||||
|
|
@ -1,292 +0,0 @@
|
|||
import { useState } from "react";
|
||||
import { getMessageClass } from "./messageUtils";
|
||||
import type { TimelineEntry } from "./types";
|
||||
import { AlertTriangle, ChevronRight, ChevronDown, Wrench, Brain, Info, ExternalLink, PlayCircle } from "lucide-react";
|
||||
import MarkdownText from "./MarkdownText";
|
||||
|
||||
const ToolItem = ({
|
||||
entry,
|
||||
isLast,
|
||||
onEventClick
|
||||
}: {
|
||||
entry: TimelineEntry;
|
||||
isLast: boolean;
|
||||
onEventClick?: (eventId: string) => void;
|
||||
}) => {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
const isTool = entry.kind === "tool";
|
||||
const isReasoning = entry.kind === "reasoning";
|
||||
const isMeta = entry.kind === "meta";
|
||||
|
||||
const isComplete = isTool && (entry.toolStatus === "completed" || entry.toolStatus === "failed");
|
||||
const isFailed = isTool && entry.toolStatus === "failed";
|
||||
const isInProgress = isTool && entry.toolStatus === "in_progress";
|
||||
|
||||
let label = "";
|
||||
let icon = <Info size={12} />;
|
||||
|
||||
if (isTool) {
|
||||
const statusLabel = entry.toolStatus && entry.toolStatus !== "completed"
|
||||
? ` (${entry.toolStatus.replace("_", " ")})`
|
||||
: "";
|
||||
label = `${entry.toolName ?? "tool"}${statusLabel}`;
|
||||
icon = <Wrench size={12} />;
|
||||
} else if (isReasoning) {
|
||||
label = `Reasoning${entry.reasoning?.visibility ? ` (${entry.reasoning.visibility})` : ""}`;
|
||||
icon = <Brain size={12} />;
|
||||
} else if (isMeta) {
|
||||
label = entry.meta?.title ?? "Status";
|
||||
icon = entry.meta?.severity === "error" ? <AlertTriangle size={12} /> : <Info size={12} />;
|
||||
}
|
||||
|
||||
const hasContent = isTool
|
||||
? Boolean(entry.toolInput || entry.toolOutput)
|
||||
: isReasoning
|
||||
? Boolean(entry.reasoning?.text?.trim())
|
||||
: Boolean(entry.meta?.detail?.trim());
|
||||
const canOpenEvent = Boolean(
|
||||
entry.eventId &&
|
||||
onEventClick &&
|
||||
!(isMeta && entry.meta?.title === "Available commands update"),
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={`tool-item ${isLast ? "last" : ""} ${isFailed ? "failed" : ""}`}>
|
||||
<div className="tool-item-connector">
|
||||
<div className="tool-item-dot" />
|
||||
{!isLast && <div className="tool-item-line" />}
|
||||
</div>
|
||||
<div className="tool-item-content">
|
||||
<button
|
||||
className={`tool-item-header ${expanded ? "expanded" : ""}`}
|
||||
onClick={() => hasContent && setExpanded(!expanded)}
|
||||
disabled={!hasContent}
|
||||
>
|
||||
<span className="tool-item-icon">{icon}</span>
|
||||
<span className="tool-item-label">{label}</span>
|
||||
{isInProgress && (
|
||||
<span className="tool-item-spinner">
|
||||
<span className="thinking-dot" />
|
||||
<span className="thinking-dot" />
|
||||
<span className="thinking-dot" />
|
||||
</span>
|
||||
)}
|
||||
{canOpenEvent && (
|
||||
<span
|
||||
className="tool-item-link"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onEventClick?.(entry.eventId!);
|
||||
}}
|
||||
title="View in Events"
|
||||
>
|
||||
<ExternalLink size={10} />
|
||||
</span>
|
||||
)}
|
||||
{hasContent && (
|
||||
<span className="tool-item-chevron">
|
||||
{expanded ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
{expanded && hasContent && (
|
||||
<div className="tool-item-body">
|
||||
{isTool && entry.toolInput && (
|
||||
<div className="tool-section">
|
||||
<div className="tool-section-title">Input</div>
|
||||
<pre className="tool-code">{entry.toolInput}</pre>
|
||||
</div>
|
||||
)}
|
||||
{isTool && isComplete && entry.toolOutput && (
|
||||
<div className="tool-section">
|
||||
<div className="tool-section-title">Output</div>
|
||||
<pre className="tool-code">{entry.toolOutput}</pre>
|
||||
</div>
|
||||
)}
|
||||
{isReasoning && entry.reasoning?.text && (
|
||||
<div className="tool-section">
|
||||
<pre className="tool-code muted">{entry.reasoning.text}</pre>
|
||||
</div>
|
||||
)}
|
||||
{isMeta && entry.meta?.detail && (
|
||||
<div className="tool-section">
|
||||
<pre className="tool-code">{entry.meta.detail}</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ToolGroup = ({ entries, onEventClick }: { entries: TimelineEntry[]; onEventClick?: (eventId: string) => void }) => {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
// If only one item, render it directly without macro wrapper
|
||||
if (entries.length === 1) {
|
||||
return (
|
||||
<div className="tool-group-single">
|
||||
<ToolItem entry={entries[0]} isLast={true} onEventClick={onEventClick} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const totalCount = entries.length;
|
||||
const summary = `${totalCount} Event${totalCount > 1 ? "s" : ""}`;
|
||||
|
||||
// Check if any are in progress
|
||||
const hasInProgress = entries.some(e => e.kind === "tool" && e.toolStatus === "in_progress");
|
||||
const hasFailed = entries.some(e => e.kind === "tool" && e.toolStatus === "failed");
|
||||
|
||||
return (
|
||||
<div className={`tool-group-container ${hasFailed ? "failed" : ""}`}>
|
||||
<button
|
||||
className={`tool-group-header ${expanded ? "expanded" : ""}`}
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
>
|
||||
<span className="tool-group-icon">
|
||||
<PlayCircle size={14} />
|
||||
</span>
|
||||
<span className="tool-group-label">{summary}</span>
|
||||
<span className="tool-group-chevron">
|
||||
{expanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
||||
</span>
|
||||
</button>
|
||||
{expanded && (
|
||||
<div className="tool-group">
|
||||
{entries.map((entry, idx) => (
|
||||
<ToolItem
|
||||
key={entry.id}
|
||||
entry={entry}
|
||||
isLast={idx === entries.length - 1}
|
||||
onEventClick={onEventClick}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const agentLogos: Record<string, string> = {
|
||||
claude: `${import.meta.env.BASE_URL}logos/claude.svg`,
|
||||
codex: `${import.meta.env.BASE_URL}logos/openai.svg`,
|
||||
opencode: `${import.meta.env.BASE_URL}logos/opencode.svg`,
|
||||
amp: `${import.meta.env.BASE_URL}logos/amp.svg`,
|
||||
pi: `${import.meta.env.BASE_URL}logos/pi.svg`,
|
||||
};
|
||||
|
||||
const ChatMessages = ({
|
||||
entries,
|
||||
sessionError,
|
||||
eventError,
|
||||
messagesEndRef,
|
||||
onEventClick,
|
||||
isThinking,
|
||||
agentId
|
||||
}: {
|
||||
entries: TimelineEntry[];
|
||||
sessionError: string | null;
|
||||
eventError?: string | null;
|
||||
messagesEndRef: React.RefObject<HTMLDivElement>;
|
||||
onEventClick?: (eventId: string) => void;
|
||||
isThinking?: boolean;
|
||||
agentId?: string;
|
||||
}) => {
|
||||
// Group consecutive tool/reasoning/meta entries together
|
||||
const groupedEntries: Array<{ type: "message" | "tool-group" | "divider"; entries: TimelineEntry[] }> = [];
|
||||
|
||||
let currentToolGroup: TimelineEntry[] = [];
|
||||
|
||||
const flushToolGroup = () => {
|
||||
if (currentToolGroup.length > 0) {
|
||||
groupedEntries.push({ type: "tool-group", entries: currentToolGroup });
|
||||
currentToolGroup = [];
|
||||
}
|
||||
};
|
||||
|
||||
for (const entry of entries) {
|
||||
const isStatusDivider = entry.kind === "meta" &&
|
||||
["Session Started", "Turn Started", "Turn Ended"].includes(entry.meta?.title ?? "");
|
||||
|
||||
if (isStatusDivider) {
|
||||
flushToolGroup();
|
||||
groupedEntries.push({ type: "divider", entries: [entry] });
|
||||
} else if (entry.kind === "tool" || entry.kind === "reasoning" || (entry.kind === "meta" && entry.meta?.detail)) {
|
||||
currentToolGroup.push(entry);
|
||||
} else if (entry.kind === "meta" && !entry.meta?.detail) {
|
||||
// Simple meta without detail - add to tool group as single item
|
||||
currentToolGroup.push(entry);
|
||||
} else {
|
||||
// Regular message
|
||||
flushToolGroup();
|
||||
groupedEntries.push({ type: "message", entries: [entry] });
|
||||
}
|
||||
}
|
||||
flushToolGroup();
|
||||
|
||||
return (
|
||||
<div className="messages">
|
||||
{groupedEntries.map((group, idx) => {
|
||||
if (group.type === "divider") {
|
||||
const entry = group.entries[0];
|
||||
const title = entry.meta?.title ?? "Status";
|
||||
return (
|
||||
<div key={entry.id} className="status-divider">
|
||||
<div className="status-divider-line" />
|
||||
<span className="status-divider-text">{title}</span>
|
||||
<div className="status-divider-line" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (group.type === "tool-group") {
|
||||
return <ToolGroup key={`group-${idx}`} entries={group.entries} onEventClick={onEventClick} />;
|
||||
}
|
||||
|
||||
// Regular message
|
||||
const entry = group.entries[0];
|
||||
const messageClass = getMessageClass(entry);
|
||||
|
||||
return (
|
||||
<div key={entry.id} className={`message ${messageClass} no-avatar`}>
|
||||
<div className="message-content">
|
||||
{entry.text ? (
|
||||
<MarkdownText text={entry.text} />
|
||||
) : (
|
||||
<span className="thinking-indicator">
|
||||
<span className="thinking-dot" />
|
||||
<span className="thinking-dot" />
|
||||
<span className="thinking-dot" />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{sessionError && <div className="message-error">{sessionError}</div>}
|
||||
{eventError && <div className="message-error">{eventError}</div>}
|
||||
{isThinking && (
|
||||
<div className="thinking-row">
|
||||
<div className="thinking-avatar">
|
||||
{agentId && agentLogos[agentId] ? (
|
||||
<img src={agentLogos[agentId]} alt="" className="thinking-avatar-img" />
|
||||
) : (
|
||||
<span className="ai-label">AI</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="thinking-indicator">
|
||||
<span className="thinking-dot" />
|
||||
<span className="thinking-dot" />
|
||||
<span className="thinking-dot" />
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatMessages;
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import type { TranscriptEntry } from "@sandbox-agent/react";
|
||||
import { AlertTriangle, Archive, CheckSquare, MessageSquare, Plus, Square, Terminal } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import type { AgentInfo } from "sandbox-agent";
|
||||
|
|
@ -6,9 +7,7 @@ import { formatShortId } from "../../utils/format";
|
|||
type AgentModeInfo = { id: string; name: string; description: string };
|
||||
type AgentModelInfo = { id: string; name?: string };
|
||||
import SessionCreateMenu, { type SessionConfig } from "../SessionCreateMenu";
|
||||
import ChatInput from "./ChatInput";
|
||||
import ChatMessages from "./ChatMessages";
|
||||
import type { TimelineEntry } from "./types";
|
||||
import InspectorConversation from "./InspectorConversation";
|
||||
|
||||
const HistoryLoadingSkeleton = () => (
|
||||
<div className="chat-loading-skeleton" aria-hidden>
|
||||
|
|
@ -59,7 +58,7 @@ const ChatPanel = ({
|
|||
tokenUsage,
|
||||
}: {
|
||||
sessionId: string;
|
||||
transcriptEntries: TimelineEntry[];
|
||||
transcriptEntries: TranscriptEntry[];
|
||||
isLoadingHistory?: boolean;
|
||||
sessionError: string | null;
|
||||
message: string;
|
||||
|
|
@ -212,8 +211,8 @@ const ChatPanel = ({
|
|||
</div>
|
||||
)}
|
||||
|
||||
<div className="messages-container">
|
||||
{!sessionId ? (
|
||||
{!sessionId ? (
|
||||
<div className="messages-container">
|
||||
<div className="empty-state">
|
||||
<div className="empty-state-title">No Session Selected</div>
|
||||
<p className="empty-state-text no-session-subtext">Create a new session to start chatting with an agent.</p>
|
||||
|
|
@ -239,37 +238,35 @@ const ChatPanel = ({
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : transcriptEntries.length === 0 && !sessionError ? (
|
||||
isLoadingHistory ? (
|
||||
<HistoryLoadingSkeleton />
|
||||
) : (
|
||||
<div className="empty-state">
|
||||
<Terminal className="empty-state-icon" />
|
||||
<div className="empty-state-title">Ready to Chat</div>
|
||||
<p className="empty-state-text">Send a message to start a conversation with the agent.</p>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<ChatMessages
|
||||
entries={transcriptEntries}
|
||||
sessionError={sessionError}
|
||||
eventError={null}
|
||||
messagesEndRef={messagesEndRef}
|
||||
onEventClick={onEventClick}
|
||||
isThinking={isThinking}
|
||||
agentId={agentId}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ChatInput
|
||||
message={message}
|
||||
onMessageChange={onMessageChange}
|
||||
onSendMessage={onSendMessage}
|
||||
onKeyDown={onKeyDown}
|
||||
placeholder={sessionEnded ? "Session ended" : sessionId ? "Send a message..." : "Select or create a session first"}
|
||||
disabled={!sessionId || sessionEnded}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<InspectorConversation
|
||||
entries={transcriptEntries}
|
||||
sessionError={sessionError}
|
||||
eventError={null}
|
||||
messagesEndRef={messagesEndRef}
|
||||
onEventClick={onEventClick}
|
||||
isThinking={isThinking}
|
||||
agentId={agentId}
|
||||
emptyState={
|
||||
isLoadingHistory ? (
|
||||
<HistoryLoadingSkeleton />
|
||||
) : (
|
||||
<div className="empty-state">
|
||||
<Terminal className="empty-state-icon" />
|
||||
<div className="empty-state-title">Ready to Chat</div>
|
||||
<p className="empty-state-text">Send a message to start a conversation with the agent.</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
message={message}
|
||||
onMessageChange={onMessageChange}
|
||||
onSendMessage={onSendMessage}
|
||||
onKeyDown={onKeyDown}
|
||||
placeholder={sessionEnded ? "Session ended" : "Send a message..."}
|
||||
disabled={sessionEnded}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,165 @@
|
|||
import {
|
||||
AgentConversation,
|
||||
type AgentConversationClassNames,
|
||||
type AgentTranscriptClassNames,
|
||||
type ChatComposerClassNames,
|
||||
type TranscriptEntry,
|
||||
} from "@sandbox-agent/react";
|
||||
import { AlertTriangle, Brain, ChevronDown, ChevronRight, ExternalLink, Info, PlayCircle, Send, Wrench } from "lucide-react";
|
||||
import type { ReactNode } from "react";
|
||||
import MarkdownText from "./MarkdownText";
|
||||
|
||||
const agentLogos: Record<string, string> = {
|
||||
claude: `${import.meta.env.BASE_URL}logos/claude.svg`,
|
||||
codex: `${import.meta.env.BASE_URL}logos/openai.svg`,
|
||||
opencode: `${import.meta.env.BASE_URL}logos/opencode.svg`,
|
||||
amp: `${import.meta.env.BASE_URL}logos/amp.svg`,
|
||||
pi: `${import.meta.env.BASE_URL}logos/pi.svg`,
|
||||
};
|
||||
|
||||
const transcriptClassNames: Partial<AgentTranscriptClassNames> = {
|
||||
root: "messages",
|
||||
divider: "status-divider",
|
||||
dividerLine: "status-divider-line",
|
||||
dividerText: "status-divider-text",
|
||||
message: "message",
|
||||
messageContent: "message-content",
|
||||
error: "message-error",
|
||||
toolGroupSingle: "tool-group-single",
|
||||
toolGroupContainer: "tool-group-container",
|
||||
toolGroupHeader: "tool-group-header",
|
||||
toolGroupIcon: "tool-group-icon",
|
||||
toolGroupLabel: "tool-group-label",
|
||||
toolGroupChevron: "tool-group-chevron",
|
||||
toolGroupBody: "tool-group",
|
||||
toolItem: "tool-item",
|
||||
toolItemConnector: "tool-item-connector",
|
||||
toolItemDot: "tool-item-dot",
|
||||
toolItemLine: "tool-item-line",
|
||||
toolItemContent: "tool-item-content",
|
||||
toolItemHeader: "tool-item-header",
|
||||
toolItemIcon: "tool-item-icon",
|
||||
toolItemLabel: "tool-item-label",
|
||||
toolItemSpinner: "tool-item-spinner",
|
||||
toolItemLink: "tool-item-link",
|
||||
toolItemChevron: "tool-item-chevron",
|
||||
toolItemBody: "tool-item-body",
|
||||
toolSection: "tool-section",
|
||||
toolSectionTitle: "tool-section-title",
|
||||
toolCode: "tool-code",
|
||||
toolCodeMuted: "muted",
|
||||
thinkingRow: "thinking-row",
|
||||
thinkingIndicator: "thinking-indicator",
|
||||
};
|
||||
|
||||
const conversationClassNames: Partial<AgentConversationClassNames> = {
|
||||
root: "chat-conversation",
|
||||
transcript: "messages-container",
|
||||
};
|
||||
|
||||
const composerClassNames: Partial<ChatComposerClassNames> = {
|
||||
root: "input-container",
|
||||
form: "input-wrapper",
|
||||
submit: "send-button",
|
||||
};
|
||||
|
||||
const ThinkingDots = () => (
|
||||
<>
|
||||
<span className="thinking-dot" />
|
||||
<span className="thinking-dot" />
|
||||
<span className="thinking-dot" />
|
||||
</>
|
||||
);
|
||||
|
||||
export interface InspectorConversationProps {
|
||||
entries: TranscriptEntry[];
|
||||
sessionError: string | null;
|
||||
eventError?: string | null;
|
||||
messagesEndRef: React.RefObject<HTMLDivElement>;
|
||||
onEventClick?: (eventId: string) => void;
|
||||
isThinking?: boolean;
|
||||
agentId?: string;
|
||||
emptyState?: ReactNode;
|
||||
message: string;
|
||||
onMessageChange: (value: string) => void;
|
||||
onSendMessage: () => void;
|
||||
onKeyDown: (event: React.KeyboardEvent<HTMLTextAreaElement>) => void;
|
||||
placeholder: string;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
const InspectorConversation = ({
|
||||
entries,
|
||||
sessionError,
|
||||
eventError,
|
||||
messagesEndRef,
|
||||
onEventClick,
|
||||
isThinking,
|
||||
agentId,
|
||||
emptyState,
|
||||
message,
|
||||
onMessageChange,
|
||||
onSendMessage,
|
||||
onKeyDown,
|
||||
placeholder,
|
||||
disabled,
|
||||
}: InspectorConversationProps) => {
|
||||
return (
|
||||
<AgentConversation
|
||||
entries={entries}
|
||||
classNames={conversationClassNames}
|
||||
emptyState={emptyState}
|
||||
transcriptClassNames={transcriptClassNames}
|
||||
transcriptProps={{
|
||||
endRef: messagesEndRef,
|
||||
sessionError,
|
||||
eventError,
|
||||
onEventClick,
|
||||
isThinking,
|
||||
agentId,
|
||||
canOpenEvent: (entry) => !(entry.kind === "meta" && entry.meta?.title === "Available commands update"),
|
||||
renderMessageText: (entry) => <MarkdownText text={entry.text ?? ""} />,
|
||||
renderInlinePendingIndicator: () => <ThinkingDots />,
|
||||
renderToolItemIcon: (entry) => {
|
||||
if (entry.kind === "tool") {
|
||||
return <Wrench size={12} />;
|
||||
}
|
||||
if (entry.kind === "reasoning") {
|
||||
return <Brain size={12} />;
|
||||
}
|
||||
return entry.meta?.severity === "error" ? <AlertTriangle size={12} /> : <Info size={12} />;
|
||||
},
|
||||
renderToolGroupIcon: () => <PlayCircle size={14} />,
|
||||
renderChevron: (expanded) => (expanded ? <ChevronDown size={12} /> : <ChevronRight size={12} />),
|
||||
renderEventLinkContent: () => <ExternalLink size={10} />,
|
||||
renderThinkingState: ({ agentId: activeAgentId }) => (
|
||||
<div className="thinking-row">
|
||||
<div className="thinking-avatar">
|
||||
{activeAgentId && agentLogos[activeAgentId] ? (
|
||||
<img src={agentLogos[activeAgentId]} alt="" className="thinking-avatar-img" />
|
||||
) : (
|
||||
<span className="ai-label">AI</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="thinking-indicator">
|
||||
<ThinkingDots />
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
composerClassNames={composerClassNames}
|
||||
composerProps={{
|
||||
message,
|
||||
onMessageChange,
|
||||
onSubmit: onSendMessage,
|
||||
onKeyDown,
|
||||
placeholder,
|
||||
disabled,
|
||||
submitLabel: "Send",
|
||||
renderSubmitContent: () => <Send />,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default InspectorConversation;
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
import type { TimelineEntry } from "./types";
|
||||
import { Settings, AlertTriangle } from "lucide-react";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
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): ReactNode => {
|
||||
if (messageClass === "user") return null;
|
||||
if (messageClass === "tool") return "T";
|
||||
if (messageClass === "system") return <Settings size={14} />;
|
||||
if (messageClass === "error") return <AlertTriangle size={14} />;
|
||||
return "AI";
|
||||
};
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
export type TimelineEntry = {
|
||||
id: string;
|
||||
eventId?: string; // Links back to the original event for navigation
|
||||
kind: "message" | "tool" | "meta" | "reasoning";
|
||||
time: string;
|
||||
// For messages:
|
||||
role?: "user" | "assistant";
|
||||
text?: string;
|
||||
// For tool calls:
|
||||
toolName?: string;
|
||||
toolInput?: string;
|
||||
toolOutput?: string;
|
||||
toolStatus?: string;
|
||||
// For reasoning:
|
||||
reasoning?: { text: string; visibility?: string };
|
||||
// For meta:
|
||||
meta?: { title: string; detail?: string; severity?: "info" | "error" };
|
||||
};
|
||||
|
|
@ -46,9 +46,13 @@ const structuredData = {
|
|||
<!-- Preconnect to font providers -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link rel="preconnect" href="https://api.fontshare.com" crossorigin />
|
||||
|
||||
<!-- Manrope + JetBrains Mono (from Google Fonts) -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Manrope:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
|
||||
<!-- Satoshi for headings (from Fontshare) -->
|
||||
<link href="https://api.fontshare.com/v2/css?f[]=satoshi@700,900&display=swap" rel="stylesheet" />
|
||||
|
||||
<!-- Open Sans + JetBrains Mono (from Google Fonts) -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Open+Sans:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
||||
|
||||
<title>{title}</title>
|
||||
|
||||
|
|
|
|||
|
|
@ -35,8 +35,7 @@
|
|||
|
||||
body {
|
||||
@apply bg-black text-white antialiased;
|
||||
font-family: 'Manrope', system-ui, sans-serif;
|
||||
font-weight: 500;
|
||||
font-family: 'Open Sans', system-ui, sans-serif;
|
||||
}
|
||||
|
||||
/* Text selection - matches rivet.dev */
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ export default {
|
|||
'code-comment': '#737373',
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['Manrope', 'system-ui', 'sans-serif'],
|
||||
heading: ['Manrope', 'system-ui', 'sans-serif'],
|
||||
sans: ['Open Sans', 'system-ui', 'sans-serif'],
|
||||
heading: ['Satoshi', 'Open Sans', 'system-ui', 'sans-serif'],
|
||||
mono: ['JetBrains Mono', 'monospace'],
|
||||
},
|
||||
animation: {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue