mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-17 12:04:15 +00:00
feat(inspector): improve session UI, skills dropdown, and visual polish (#179)
- 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
This commit is contained in:
parent
1c381c552a
commit
e134012955
22 changed files with 2283 additions and 395 deletions
|
|
@ -1,6 +1,7 @@
|
|||
import { Plus, RefreshCw } from "lucide-react";
|
||||
import { Archive, ArrowLeft, ArrowUpRight, Plus, RefreshCw } 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 };
|
||||
|
|
@ -10,6 +11,7 @@ type SessionListItem = {
|
|||
sessionId: string;
|
||||
agent: string;
|
||||
ended: boolean;
|
||||
archived: boolean;
|
||||
};
|
||||
|
||||
const agentLabels: Record<string, string> = {
|
||||
|
|
@ -21,6 +23,7 @@ const agentLabels: Record<string, string> = {
|
|||
cursor: "Cursor"
|
||||
};
|
||||
const persistenceDocsUrl = "https://sandboxagent.dev/docs/session-persistence";
|
||||
const MIN_REFRESH_SPIN_MS = 350;
|
||||
|
||||
const SessionSidebar = ({
|
||||
sessions,
|
||||
|
|
@ -42,7 +45,7 @@ const SessionSidebar = ({
|
|||
selectedSessionId: string;
|
||||
onSelectSession: (session: SessionListItem) => void;
|
||||
onRefresh: () => void;
|
||||
onCreateSession: (agentId: string, config: SessionConfig) => void;
|
||||
onCreateSession: (agentId: string, config: SessionConfig) => Promise<void>;
|
||||
onSelectAgent: (agentId: string) => Promise<void>;
|
||||
agents: AgentInfo[];
|
||||
agentsLoading: boolean;
|
||||
|
|
@ -54,7 +57,16 @@ const SessionSidebar = ({
|
|||
defaultModelByAgent: Record<string, string>;
|
||||
}) => {
|
||||
const [showMenu, setShowMenu] = useState(false);
|
||||
const [showArchived, setShowArchived] = useState(false);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const menuRef = useRef<HTMLDivElement | null>(null);
|
||||
const archivedCount = sessions.filter((session) => session.archived).length;
|
||||
const activeSessions = sessions.filter((session) => !session.archived);
|
||||
const archivedSessions = sessions.filter((session) => session.archived);
|
||||
const visibleSessions = showArchived ? archivedSessions : activeSessions;
|
||||
const orderedVisibleSessions = showArchived
|
||||
? [...visibleSessions].sort((a, b) => Number(a.ended) - Number(b.ended))
|
||||
: visibleSessions;
|
||||
|
||||
useEffect(() => {
|
||||
if (!showMenu) return;
|
||||
|
|
@ -68,13 +80,54 @@ const SessionSidebar = ({
|
|||
return () => document.removeEventListener("mousedown", handler);
|
||||
}, [showMenu]);
|
||||
|
||||
useEffect(() => {
|
||||
// Prevent getting stuck in archived view when there are no archived sessions.
|
||||
if (!showArchived) return;
|
||||
if (archivedSessions.length === 0) {
|
||||
setShowArchived(false);
|
||||
}
|
||||
}, [showArchived, archivedSessions.length]);
|
||||
|
||||
const handleRefresh = async () => {
|
||||
if (refreshing) return;
|
||||
const startedAt = Date.now();
|
||||
setRefreshing(true);
|
||||
try {
|
||||
await Promise.resolve(onRefresh());
|
||||
} finally {
|
||||
const elapsedMs = Date.now() - startedAt;
|
||||
if (elapsedMs < MIN_REFRESH_SPIN_MS) {
|
||||
await new Promise((resolve) => window.setTimeout(resolve, MIN_REFRESH_SPIN_MS - elapsedMs));
|
||||
}
|
||||
setRefreshing(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="session-sidebar">
|
||||
<div className="sidebar-header">
|
||||
<span className="sidebar-title">Sessions</span>
|
||||
<div className="sidebar-header-actions">
|
||||
<button className="sidebar-icon-btn" onClick={onRefresh} title="Refresh sessions">
|
||||
<RefreshCw size={14} />
|
||||
{archivedCount > 0 && (
|
||||
<button
|
||||
className={`button secondary small ${showArchived ? "active" : ""}`}
|
||||
onClick={() => setShowArchived((value) => !value)}
|
||||
title={showArchived ? "Hide archived sessions" : `Show archived sessions (${archivedCount})`}
|
||||
>
|
||||
{showArchived ? (
|
||||
<ArrowLeft size={12} className="button-icon" />
|
||||
) : (
|
||||
<Archive size={12} className="button-icon" />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="button secondary small"
|
||||
onClick={() => void handleRefresh()}
|
||||
title="Refresh sessions"
|
||||
disabled={sessionsLoading || refreshing}
|
||||
>
|
||||
<RefreshCw size={12} className={`button-icon ${sessionsLoading || refreshing ? "spinner-icon" : ""}`} />
|
||||
</button>
|
||||
<div className="sidebar-add-menu-wrapper" ref={menuRef}>
|
||||
<button
|
||||
|
|
@ -105,30 +158,41 @@ const SessionSidebar = ({
|
|||
<div className="sidebar-empty">Loading sessions...</div>
|
||||
) : sessionsError ? (
|
||||
<div className="sidebar-empty error">{sessionsError}</div>
|
||||
) : sessions.length === 0 ? (
|
||||
<div className="sidebar-empty">No sessions yet.</div>
|
||||
) : visibleSessions.length === 0 ? (
|
||||
<div className="sidebar-empty">{showArchived ? "No archived sessions." : "No sessions yet."}</div>
|
||||
) : (
|
||||
sessions.map((session) => (
|
||||
<button
|
||||
key={session.sessionId}
|
||||
className={`session-item ${session.sessionId === selectedSessionId ? "active" : ""}`}
|
||||
onClick={() => onSelectSession(session)}
|
||||
>
|
||||
<div className="session-item-id">{session.sessionId}</div>
|
||||
<div className="session-item-meta">
|
||||
<span className="session-item-agent">{agentLabels[session.agent] ?? session.agent}</span>
|
||||
{session.ended && <span className="session-item-ended">ended</span>}
|
||||
</div>
|
||||
</button>
|
||||
))
|
||||
<>
|
||||
{showArchived && <div className="sidebar-empty">Archived Sessions</div>}
|
||||
{orderedVisibleSessions.map((session) => (
|
||||
<div
|
||||
key={session.sessionId}
|
||||
className={`session-item ${session.sessionId === selectedSessionId ? "active" : ""} ${session.ended ? "ended" : ""} ${session.archived ? "ended" : ""}`}
|
||||
>
|
||||
<button
|
||||
className="session-item-content"
|
||||
onClick={() => onSelectSession(session)}
|
||||
>
|
||||
<div className="session-item-id" title={session.sessionId}>
|
||||
{formatShortId(session.sessionId)}
|
||||
</div>
|
||||
<div className="session-item-meta">
|
||||
<span className="session-item-agent">
|
||||
{agentLabels[session.agent] ?? session.agent}
|
||||
</span>
|
||||
{(session.archived || session.ended) && <span className="session-item-ended">ended</span>}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="session-persistence-note">
|
||||
Sessions are persisted in your browser using IndexedDB.{" "}
|
||||
<a href={persistenceDocsUrl} target="_blank" rel="noreferrer">
|
||||
<a href={persistenceDocsUrl} target="_blank" rel="noreferrer" style={{ display: "inline-flex", alignItems: "center", gap: 2 }}>
|
||||
Configure persistence
|
||||
<ArrowUpRight size={10} />
|
||||
</a>
|
||||
.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue