mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-16 19:04:40 +00:00
feat(inspector): improve session UI, skills dropdown, and visual polish
- Add delete button on ended sessions (visible on hover) - Darken ended sessions with opacity and "ended" pill badge - Sort ended sessions to bottom of list - Add token usage pill in chat header - Disable input when session ended - Add Official Skills dropdown with SDK and Rivet presets - Format session IDs shorter with full ID on hover - Add arrow icon to "Configure persistence" link - Add agent logo SVGs Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
ed30b5f068
commit
700f7dbae0
20 changed files with 1861 additions and 317 deletions
|
|
@ -1,6 +1,7 @@
|
|||
import { CheckSquare, MessageSquare, Plus, Square, Terminal } from "lucide-react";
|
||||
import { AlertTriangle, Archive, CheckSquare, MessageSquare, Plus, Square, Terminal } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import type { AgentInfo } from "sandbox-agent";
|
||||
import { formatShortId } from "../../utils/format";
|
||||
|
||||
type AgentModeInfo = { id: string; name: string; description: string };
|
||||
type AgentModelInfo = { id: string; name?: string };
|
||||
|
|
@ -24,12 +25,20 @@ const ChatPanel = ({
|
|||
agentsError,
|
||||
messagesEndRef,
|
||||
agentLabel,
|
||||
modelLabel,
|
||||
currentAgentVersion,
|
||||
sessionEnded,
|
||||
sessionArchived,
|
||||
onEndSession,
|
||||
onArchiveSession,
|
||||
onUnarchiveSession,
|
||||
modesByAgent,
|
||||
modelsByAgent,
|
||||
defaultModelByAgent,
|
||||
onEventClick,
|
||||
isThinking,
|
||||
agentId,
|
||||
tokenUsage,
|
||||
}: {
|
||||
sessionId: string;
|
||||
transcriptEntries: TimelineEntry[];
|
||||
|
|
@ -38,21 +47,30 @@ const ChatPanel = ({
|
|||
onMessageChange: (value: string) => void;
|
||||
onSendMessage: () => void;
|
||||
onKeyDown: (event: React.KeyboardEvent<HTMLTextAreaElement>) => void;
|
||||
onCreateSession: (agentId: string, config: SessionConfig) => void;
|
||||
onCreateSession: (agentId: string, config: SessionConfig) => Promise<void>;
|
||||
onSelectAgent: (agentId: string) => Promise<void>;
|
||||
agents: AgentInfo[];
|
||||
agentsLoading: boolean;
|
||||
agentsError: string | null;
|
||||
messagesEndRef: React.RefObject<HTMLDivElement>;
|
||||
agentLabel: string;
|
||||
modelLabel?: string | null;
|
||||
currentAgentVersion?: string | null;
|
||||
sessionEnded: boolean;
|
||||
sessionArchived: boolean;
|
||||
onEndSession: () => void;
|
||||
onArchiveSession: () => void;
|
||||
onUnarchiveSession: () => void;
|
||||
modesByAgent: Record<string, AgentModeInfo[]>;
|
||||
modelsByAgent: Record<string, AgentModelInfo[]>;
|
||||
defaultModelByAgent: Record<string, string>;
|
||||
onEventClick?: (eventId: string) => void;
|
||||
isThinking?: boolean;
|
||||
agentId?: string;
|
||||
tokenUsage?: { used: number; size: number; cost?: number } | null;
|
||||
}) => {
|
||||
const [showAgentMenu, setShowAgentMenu] = useState(false);
|
||||
const [copiedSessionId, setCopiedSessionId] = useState(false);
|
||||
const menuRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -67,21 +85,92 @@ const ChatPanel = ({
|
|||
return () => document.removeEventListener("mousedown", handler);
|
||||
}, [showAgentMenu]);
|
||||
|
||||
const copySessionId = async () => {
|
||||
if (!sessionId) return;
|
||||
const onSuccess = () => {
|
||||
setCopiedSessionId(true);
|
||||
window.setTimeout(() => setCopiedSessionId(false), 1200);
|
||||
};
|
||||
try {
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
await navigator.clipboard.writeText(sessionId);
|
||||
onSuccess();
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// Fallback below for older/insecure contexts.
|
||||
}
|
||||
|
||||
const textarea = document.createElement("textarea");
|
||||
textarea.value = sessionId;
|
||||
textarea.style.position = "fixed";
|
||||
textarea.style.opacity = "0";
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
try {
|
||||
document.execCommand("copy");
|
||||
onSuccess();
|
||||
} finally {
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
};
|
||||
|
||||
const handleArchiveSession = () => {
|
||||
if (!sessionId) return;
|
||||
onArchiveSession();
|
||||
};
|
||||
|
||||
const handleUnarchiveSession = () => {
|
||||
if (!sessionId) return;
|
||||
onUnarchiveSession();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="chat-panel">
|
||||
<div className="panel-header">
|
||||
<div className="panel-header-left">
|
||||
<MessageSquare className="button-icon" />
|
||||
<span className="panel-title">{sessionId ? "Session" : "No Session"}</span>
|
||||
{sessionId && <span className="session-id-display">{sessionId}</span>}
|
||||
<span className="panel-title">{sessionId ? agentLabel : "No Session"}</span>
|
||||
{sessionId && modelLabel && (
|
||||
<span className="header-meta-pill" title={modelLabel}>
|
||||
{modelLabel}
|
||||
</span>
|
||||
)}
|
||||
{sessionId && currentAgentVersion && (
|
||||
<span className="header-meta-pill">v{currentAgentVersion}</span>
|
||||
)}
|
||||
{sessionId && (
|
||||
<button
|
||||
type="button"
|
||||
className="session-id-display"
|
||||
title={copiedSessionId ? "Copied" : `${sessionId} (click to copy)`}
|
||||
onClick={() => void copySessionId()}
|
||||
>
|
||||
{copiedSessionId ? "Copied" : formatShortId(sessionId)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="panel-header-right">
|
||||
{sessionId && tokenUsage && (
|
||||
<span className="token-pill">{tokenUsage.used.toLocaleString()} tokens</span>
|
||||
)}
|
||||
{sessionId && (
|
||||
sessionEnded ? (
|
||||
<span className="button ghost small" style={{ opacity: 0.5, cursor: "default" }} title="Session ended">
|
||||
<CheckSquare size={12} />
|
||||
Ended
|
||||
</span>
|
||||
<>
|
||||
<span className="button ghost small session-ended-status" title="Session ended">
|
||||
<CheckSquare size={12} />
|
||||
Ended
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="button ghost small"
|
||||
onClick={sessionArchived ? handleUnarchiveSession : handleArchiveSession}
|
||||
title={sessionArchived ? "Unarchive session" : "Archive session"}
|
||||
>
|
||||
<Archive size={12} />
|
||||
{sessionArchived ? "Unarchive" : "Archive"}
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -97,12 +186,18 @@ const ChatPanel = ({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{sessionError && (
|
||||
<div className="error-banner">
|
||||
<AlertTriangle size={14} />
|
||||
<span>{sessionError}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="messages-container">
|
||||
{!sessionId ? (
|
||||
<div className="empty-state">
|
||||
<MessageSquare className="empty-state-icon" />
|
||||
<div className="empty-state-title">No Session Selected</div>
|
||||
<p className="empty-state-text">Create a new session to start chatting with an agent.</p>
|
||||
<p className="empty-state-text no-session-subtext">Create a new session to start chatting with an agent.</p>
|
||||
<div className="empty-state-menu-wrapper" ref={menuRef}>
|
||||
<button
|
||||
className="button primary"
|
||||
|
|
@ -135,7 +230,11 @@ const ChatPanel = ({
|
|||
<ChatMessages
|
||||
entries={transcriptEntries}
|
||||
sessionError={sessionError}
|
||||
eventError={null}
|
||||
messagesEndRef={messagesEndRef}
|
||||
onEventClick={onEventClick}
|
||||
isThinking={isThinking}
|
||||
agentId={agentId}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -145,24 +244,9 @@ const ChatPanel = ({
|
|||
onMessageChange={onMessageChange}
|
||||
onSendMessage={onSendMessage}
|
||||
onKeyDown={onKeyDown}
|
||||
placeholder={sessionId ? "Send a message..." : "Select or create a session first"}
|
||||
disabled={!sessionId}
|
||||
placeholder={sessionEnded ? "Session ended" : sessionId ? "Send a message..." : "Select or create a session first"}
|
||||
disabled={!sessionId || sessionEnded}
|
||||
/>
|
||||
|
||||
{sessionId && (
|
||||
<div className="session-config-bar">
|
||||
<div className="session-config-field">
|
||||
<span className="session-config-label">Agent</span>
|
||||
<span className="session-config-value">{agentLabel}</span>
|
||||
</div>
|
||||
{currentAgentVersion && (
|
||||
<div className="session-config-field">
|
||||
<span className="session-config-label">Version</span>
|
||||
<span className="session-config-value">{currentAgentVersion}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue