mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-18 00:02:48 +00:00
Configure lefthook formatter checks (#231)
* Add lefthook formatter checks * Fix SDK mode hydration * Stabilize SDK mode integration test
This commit is contained in:
parent
0471214d65
commit
d2346bafb3
282 changed files with 5840 additions and 8399 deletions
|
|
@ -30,7 +30,7 @@ const ConnectScreen = ({
|
|||
<div className="app">
|
||||
<header className="header">
|
||||
<div className="header-left">
|
||||
<img src={logoUrl} alt="Sandbox Agent" className="logo-text" style={{ height: '20px', width: 'auto' }} />
|
||||
<img src={logoUrl} alt="Sandbox Agent" className="logo-text" style={{ height: "20px", width: "auto" }} />
|
||||
</div>
|
||||
{(docsUrl || discordUrl || reportUrl) && (
|
||||
<div className="header-right">
|
||||
|
|
@ -42,13 +42,17 @@ const ConnectScreen = ({
|
|||
)}
|
||||
{discordUrl && (
|
||||
<a className="header-link" href={discordUrl} target="_blank" rel="noreferrer">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.095 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.095 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/></svg>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.095 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.095 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z" />
|
||||
</svg>
|
||||
Discord
|
||||
</a>
|
||||
)}
|
||||
{reportUrl && (
|
||||
<a className="header-link" href={reportUrl} target="_blank" rel="noreferrer">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
|
||||
</svg>
|
||||
Issues
|
||||
</a>
|
||||
)}
|
||||
|
|
@ -59,7 +63,7 @@ const ConnectScreen = ({
|
|||
<main className="landing">
|
||||
<div className="landing-container">
|
||||
<div className="landing-hero">
|
||||
<img src={logoUrl} alt="Sandbox Agent" style={{ height: '32px', width: 'auto', marginBottom: '20px' }} />
|
||||
<img src={logoUrl} alt="Sandbox Agent" style={{ height: "32px", width: "auto", marginBottom: "20px" }} />
|
||||
</div>
|
||||
|
||||
<div className="connect-card">
|
||||
|
|
@ -67,17 +71,15 @@ const ConnectScreen = ({
|
|||
|
||||
{connectError && <div className="banner error">{connectError}</div>}
|
||||
|
||||
{isHttpsToHttpConnection(window.location.href, endpoint) &&
|
||||
isLocalNetworkTarget(endpoint) && (
|
||||
<div className="banner warning">
|
||||
<AlertTriangle size={16} />
|
||||
<span>
|
||||
Connecting from HTTPS to a local HTTP server requires{" "}
|
||||
<strong>local network access</strong> permission. Your browser may prompt you to
|
||||
allow this connection.
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{isHttpsToHttpConnection(window.location.href, endpoint) && isLocalNetworkTarget(endpoint) && (
|
||||
<div className="banner warning">
|
||||
<AlertTriangle size={16} />
|
||||
<span>
|
||||
Connecting from HTTPS to a local HTTP server requires <strong>local network access</strong> permission. Your browser may prompt you to allow
|
||||
this connection.
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<label className="field">
|
||||
<span className="label">Endpoint</span>
|
||||
|
|
@ -92,13 +94,7 @@ const ConnectScreen = ({
|
|||
|
||||
<label className="field">
|
||||
<span className="label">Token (optional)</span>
|
||||
<input
|
||||
className="input"
|
||||
type="password"
|
||||
placeholder="Bearer token"
|
||||
value={token}
|
||||
onChange={(event) => onTokenChange(event.target.value)}
|
||||
/>
|
||||
<input className="input" type="password" placeholder="Bearer token" value={token} onChange={(event) => onTokenChange(event.target.value)} />
|
||||
</label>
|
||||
|
||||
<button className="button primary" onClick={onConnect} disabled={connecting}>
|
||||
|
|
@ -116,7 +112,11 @@ const ConnectScreen = ({
|
|||
</button>
|
||||
|
||||
<p className="hint">
|
||||
Having trouble connecting? See the <a href="https://sandboxagent.dev/docs/cors" target="_blank" rel="noreferrer">CORS documentation</a>.
|
||||
Having trouble connecting? See the{" "}
|
||||
<a href="https://sandboxagent.dev/docs/cors" target="_blank" rel="noreferrer">
|
||||
CORS documentation
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ const agentLabels: Record<string, string> = {
|
|||
opencode: "OpenCode",
|
||||
amp: "Amp",
|
||||
pi: "Pi",
|
||||
cursor: "Cursor"
|
||||
cursor: "Cursor",
|
||||
};
|
||||
|
||||
const agentLogos: Record<string, string> = {
|
||||
|
|
@ -39,7 +39,7 @@ const SessionCreateMenu = ({
|
|||
onCreateSession,
|
||||
onSelectAgent,
|
||||
open,
|
||||
onClose
|
||||
onClose,
|
||||
}: {
|
||||
agents: AgentInfo[];
|
||||
agentsLoading: boolean;
|
||||
|
|
@ -157,54 +157,45 @@ const SessionCreateMenu = ({
|
|||
<div className="session-create-menu">
|
||||
{agentsLoading && <div className="sidebar-add-status">Loading agents...</div>}
|
||||
{agentsError && <div className="sidebar-add-status error">{agentsError}</div>}
|
||||
{!agentsLoading && !agentsError && agents.length === 0 && (
|
||||
<div className="sidebar-add-status">No agents available.</div>
|
||||
)}
|
||||
{!agentsLoading && !agentsError && (() => {
|
||||
const codingAgents = agents.filter((a) => a.id !== "mock");
|
||||
const mockAgent = agents.find((a) => a.id === "mock");
|
||||
return (
|
||||
<>
|
||||
{codingAgents.map((agent) => (
|
||||
<button
|
||||
key={agent.id}
|
||||
className="sidebar-add-option"
|
||||
onClick={() => handleAgentClick(agent.id)}
|
||||
>
|
||||
<div className="agent-option-left">
|
||||
{agentLogos[agent.id] && (
|
||||
<img src={agentLogos[agent.id]} alt="" className="agent-option-logo" />
|
||||
)}
|
||||
<span className="agent-option-name">{agentLabels[agent.id] ?? agent.id}</span>
|
||||
{agent.version && <span className="agent-option-version">{agent.version}</span>}
|
||||
</div>
|
||||
<div className="agent-option-badges">
|
||||
{agent.installed && <span className="agent-badge installed">Installed</span>}
|
||||
<ArrowRight size={12} className="agent-option-arrow" />
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
{mockAgent && (
|
||||
<>
|
||||
<div className="agent-divider" />
|
||||
<button
|
||||
className="sidebar-add-option"
|
||||
onClick={() => handleAgentClick(mockAgent.id)}
|
||||
>
|
||||
{!agentsLoading && !agentsError && agents.length === 0 && <div className="sidebar-add-status">No agents available.</div>}
|
||||
{!agentsLoading &&
|
||||
!agentsError &&
|
||||
(() => {
|
||||
const codingAgents = agents.filter((a) => a.id !== "mock");
|
||||
const mockAgent = agents.find((a) => a.id === "mock");
|
||||
return (
|
||||
<>
|
||||
{codingAgents.map((agent) => (
|
||||
<button key={agent.id} className="sidebar-add-option" onClick={() => handleAgentClick(agent.id)}>
|
||||
<div className="agent-option-left">
|
||||
<span className="agent-option-name">{agentLabels[mockAgent.id] ?? mockAgent.id}</span>
|
||||
{mockAgent.version && <span className="agent-option-version">{mockAgent.version}</span>}
|
||||
{agentLogos[agent.id] && <img src={agentLogos[agent.id]} alt="" className="agent-option-logo" />}
|
||||
<span className="agent-option-name">{agentLabels[agent.id] ?? agent.id}</span>
|
||||
{agent.version && <span className="agent-option-version">{agent.version}</span>}
|
||||
</div>
|
||||
<div className="agent-option-badges">
|
||||
{mockAgent.installed && <span className="agent-badge installed">Installed</span>}
|
||||
{agent.installed && <span className="agent-badge installed">Installed</span>}
|
||||
<ArrowRight size={12} className="agent-option-arrow" />
|
||||
</div>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
))}
|
||||
{mockAgent && (
|
||||
<>
|
||||
<div className="agent-divider" />
|
||||
<button className="sidebar-add-option" onClick={() => handleAgentClick(mockAgent.id)}>
|
||||
<div className="agent-option-left">
|
||||
<span className="agent-option-name">{agentLabels[mockAgent.id] ?? mockAgent.id}</span>
|
||||
{mockAgent.version && <span className="agent-option-version">{mockAgent.version}</span>}
|
||||
</div>
|
||||
<div className="agent-option-badges">
|
||||
{mockAgent.installed && <span className="agent-badge installed">Installed</span>}
|
||||
<ArrowRight size={12} className="agent-option-arrow" />
|
||||
</div>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -237,12 +228,7 @@ const SessionCreateMenu = ({
|
|||
autoFocus
|
||||
/>
|
||||
) : (
|
||||
<select
|
||||
className="setup-select"
|
||||
value={selectedModel}
|
||||
onChange={(e) => handleModelSelectChange(e.target.value)}
|
||||
title="Model"
|
||||
>
|
||||
<select className="setup-select" value={selectedModel} onChange={(e) => handleModelSelectChange(e.target.value)} title="Model">
|
||||
{activeModels.map((m) => (
|
||||
<option key={m.id} value={m.id}>
|
||||
{m.name || m.id}
|
||||
|
|
@ -258,9 +244,7 @@ const SessionCreateMenu = ({
|
|||
setIsCustomModel(false);
|
||||
setCustomModel("");
|
||||
const defaultModel = defaultModelByAgent[selectedAgent];
|
||||
setSelectedModel(
|
||||
defaultModel || (activeModels.length > 0 ? activeModels[0].id : "")
|
||||
);
|
||||
setSelectedModel(defaultModel || (activeModels.length > 0 ? activeModels[0].id : ""));
|
||||
}}
|
||||
title="Back to model list"
|
||||
type="button"
|
||||
|
|
@ -272,12 +256,7 @@ const SessionCreateMenu = ({
|
|||
{activeModes.length > 0 && (
|
||||
<div className="setup-field">
|
||||
<span className="setup-label">Mode</span>
|
||||
<select
|
||||
className="setup-select"
|
||||
value={agentMode}
|
||||
onChange={(e) => setAgentMode(e.target.value)}
|
||||
title="Mode"
|
||||
>
|
||||
<select className="setup-select" value={agentMode} onChange={(e) => setAgentMode(e.target.value)} title="Mode">
|
||||
{activeModes.map((m) => (
|
||||
<option key={m.id} value={m.id}>
|
||||
{m.name || m.id}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ const agentLabels: Record<string, string> = {
|
|||
opencode: "OpenCode",
|
||||
amp: "Amp",
|
||||
pi: "Pi",
|
||||
cursor: "Cursor"
|
||||
cursor: "Cursor",
|
||||
};
|
||||
const persistenceDocsUrl = "https://sandboxagent.dev/docs/session-persistence";
|
||||
const MIN_REFRESH_SPIN_MS = 350;
|
||||
|
|
@ -64,9 +64,7 @@ const SessionSidebar = ({
|
|||
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;
|
||||
const orderedVisibleSessions = showArchived ? [...visibleSessions].sort((a, b) => Number(a.ended) - Number(b.ended)) : visibleSessions;
|
||||
|
||||
useEffect(() => {
|
||||
if (!showMenu) return;
|
||||
|
|
@ -114,27 +112,14 @@ const SessionSidebar = ({
|
|||
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" />
|
||||
)}
|
||||
{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}
|
||||
>
|
||||
<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
|
||||
className="sidebar-add-btn"
|
||||
onClick={() => setShowMenu((value) => !value)}
|
||||
title="New session"
|
||||
>
|
||||
<button className="sidebar-add-btn" onClick={() => setShowMenu((value) => !value)} title="New session">
|
||||
<Plus size={14} />
|
||||
</button>
|
||||
<SessionCreateMenu
|
||||
|
|
@ -164,31 +149,27 @@ const SessionSidebar = ({
|
|||
<>
|
||||
{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
|
||||
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. These sessions are only from your browser; your SDK sessions are separate. Adding inspector support for SDK soon.{" "}
|
||||
Sessions are persisted in your browser using IndexedDB. These sessions are only from your browser; your SDK sessions are separate. Adding inspector
|
||||
support for SDK soon.{" "}
|
||||
<a href={persistenceDocsUrl} target="_blank" rel="noreferrer" style={{ display: "inline-flex", alignItems: "center", gap: 2 }}>
|
||||
Configure persistence
|
||||
<ArrowUpRight size={10} />
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import {
|
|||
Plug,
|
||||
Shield,
|
||||
Terminal,
|
||||
Wrench
|
||||
Wrench,
|
||||
} from "lucide-react";
|
||||
import type { FeatureCoverageView } from "../../types/agents";
|
||||
|
||||
|
|
@ -39,7 +39,7 @@ const badges = [
|
|||
{ key: "mcpTools", label: "MCP", icon: Plug },
|
||||
{ key: "streamingDeltas", label: "Deltas", icon: Activity },
|
||||
{ key: "itemStarted", label: "Item Start", icon: CircleDot },
|
||||
{ key: "variants", label: "Variants", icon: Layers }
|
||||
{ key: "variants", label: "Variants", icon: Layers },
|
||||
] as const;
|
||||
|
||||
type BadgeItem = (typeof badges)[number];
|
||||
|
|
|
|||
|
|
@ -156,9 +156,7 @@ const ChatPanel = ({
|
|||
{modelLabel}
|
||||
</span>
|
||||
)}
|
||||
{sessionId && currentAgentVersion && (
|
||||
<span className="header-meta-pill">v{currentAgentVersion}</span>
|
||||
)}
|
||||
{sessionId && currentAgentVersion && <span className="header-meta-pill">v{currentAgentVersion}</span>}
|
||||
{sessionId && (
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -171,11 +169,9 @@ const ChatPanel = ({
|
|||
)}
|
||||
</div>
|
||||
<div className="panel-header-right">
|
||||
{sessionId && tokenUsage && (
|
||||
<span className="token-pill">{tokenUsage.used.toLocaleString()} tokens</span>
|
||||
)}
|
||||
{sessionId && (
|
||||
sessionEnded ? (
|
||||
{sessionId && tokenUsage && <span className="token-pill">{tokenUsage.used.toLocaleString()} tokens</span>}
|
||||
{sessionId &&
|
||||
(sessionEnded ? (
|
||||
<>
|
||||
<span className="button ghost small session-ended-status" title="Session ended">
|
||||
<CheckSquare size={12} />
|
||||
|
|
@ -192,17 +188,11 @@ const ChatPanel = ({
|
|||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="button ghost small"
|
||||
onClick={onEndSession}
|
||||
title="End session"
|
||||
>
|
||||
<button type="button" className="button ghost small" onClick={onEndSession} title="End session">
|
||||
<Square size={12} />
|
||||
End
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -219,10 +209,7 @@ const ChatPanel = ({
|
|||
<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>
|
||||
<div className="empty-state-menu-wrapper" ref={menuRef}>
|
||||
<button
|
||||
className="button primary"
|
||||
onClick={() => setShowAgentMenu((value) => !value)}
|
||||
>
|
||||
<button className="button primary" onClick={() => setShowAgentMenu((value) => !value)}>
|
||||
<Plus className="button-icon" />
|
||||
Create Session
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ const InspectorConversation = ({
|
|||
renderPermissionIcon: () => <Shield size={14} />,
|
||||
renderPermissionOptionContent: ({ option, label, selected }) => (
|
||||
<>
|
||||
{selected ? (option.kind.startsWith("allow") ? <Check size={12} /> : <X size={12} />) : null}
|
||||
{selected ? option.kind.startsWith("allow") ? <Check size={12} /> : <X size={12} /> : null}
|
||||
{label}
|
||||
</>
|
||||
),
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ const AgentsTab = ({
|
|||
onRefresh,
|
||||
onInstall,
|
||||
loading,
|
||||
error
|
||||
error,
|
||||
}: {
|
||||
agents: AgentInfo[];
|
||||
defaultAgents: string[];
|
||||
|
|
@ -60,9 +60,7 @@ const AgentsTab = ({
|
|||
</div>
|
||||
|
||||
{error && <div className="banner error">{error}</div>}
|
||||
{!loading && agents.length === 0 && (
|
||||
<div className="card-meta">No agents reported. Click refresh to check.</div>
|
||||
)}
|
||||
{!loading && agents.length === 0 && <div className="card-meta">No agents reported. Click refresh to check.</div>}
|
||||
|
||||
{(agents.length
|
||||
? agents
|
||||
|
|
@ -73,16 +71,15 @@ const AgentsTab = ({
|
|||
version: undefined as string | undefined,
|
||||
path: undefined as string | undefined,
|
||||
capabilities: emptyFeatureCoverage as AgentInfo["capabilities"],
|
||||
}))).map((agent) => {
|
||||
}))
|
||||
).map((agent) => {
|
||||
const isInstalling = installingAgent === agent.id;
|
||||
return (
|
||||
<div key={agent.id} className="card">
|
||||
<div className="card-header">
|
||||
<span className="card-title">{agent.id}</span>
|
||||
<div className="card-header-pills">
|
||||
<span className={`pill ${agent.installed ? "success" : "danger"}`}>
|
||||
{agent.installed ? "Installed" : "Missing"}
|
||||
</span>
|
||||
<span className={`pill ${agent.installed ? "success" : "danger"}`}>{agent.installed ? "Installed" : "Missing"}</span>
|
||||
<span className={`pill ${agent.credentialsAvailable ? "success" : "warning"}`}>
|
||||
{agent.credentialsAvailable ? "Authenticated" : "No Credentials"}
|
||||
</span>
|
||||
|
|
@ -90,7 +87,11 @@ const AgentsTab = ({
|
|||
</div>
|
||||
<div className="card-meta">
|
||||
{agent.version ?? "Version unknown"}
|
||||
{agent.path && <span className="mono muted" style={{ marginLeft: 8 }}>{agent.path}</span>}
|
||||
{agent.path && (
|
||||
<span className="mono muted" style={{ marginLeft: 8 }}>
|
||||
{agent.path}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="card-meta" style={{ marginTop: 8 }}>
|
||||
Feature coverage
|
||||
|
|
@ -104,16 +105,8 @@ const AgentsTab = ({
|
|||
</div>
|
||||
)}
|
||||
<div className="card-actions">
|
||||
<button
|
||||
className="button secondary small"
|
||||
onClick={() => handleInstall(agent.id, agent.installed)}
|
||||
disabled={isInstalling}
|
||||
>
|
||||
{isInstalling ? (
|
||||
<Loader2 className="button-icon spinner-icon" />
|
||||
) : (
|
||||
<Download className="button-icon" />
|
||||
)}
|
||||
<button className="button secondary small" onClick={() => handleInstall(agent.id, agent.installed)} disabled={isInstalling}>
|
||||
{isInstalling ? <Loader2 className="button-icon spinner-icon" /> : <Download className="button-icon" />}
|
||||
{isInstalling ? "Installing..." : agent.installed ? "Reinstall" : "Install"}
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -59,11 +59,7 @@ const DebugPanel = ({
|
|||
return (
|
||||
<div className={`debug-panel ${collapsed ? "collapsed" : ""}`}>
|
||||
<div className="debug-tabs">
|
||||
<button
|
||||
className="debug-collapse-btn"
|
||||
onClick={onToggleCollapse}
|
||||
title={collapsed ? "Expand panel" : "Collapse panel"}
|
||||
>
|
||||
<button className="debug-collapse-btn" onClick={onToggleCollapse} title={collapsed ? "Expand panel" : "Collapse panel"}>
|
||||
{collapsed ? <ChevronLeft size={14} /> : <ChevronRight size={14} />}
|
||||
</button>
|
||||
<button className={`debug-tab ${debugTab === "events" ? "active" : ""}`} onClick={() => onDebugTabChange("events")}>
|
||||
|
|
@ -98,22 +94,10 @@ const DebugPanel = ({
|
|||
</div>
|
||||
|
||||
<div className="debug-content">
|
||||
{debugTab === "log" && (
|
||||
<RequestLogTab
|
||||
requestLog={requestLog}
|
||||
copiedLogId={copiedLogId}
|
||||
onClear={onClearRequestLog}
|
||||
onCopy={onCopyRequestLog}
|
||||
/>
|
||||
)}
|
||||
{debugTab === "log" && <RequestLogTab requestLog={requestLog} copiedLogId={copiedLogId} onClear={onClearRequestLog} onCopy={onCopyRequestLog} />}
|
||||
|
||||
{debugTab === "events" && (
|
||||
<EventsTab
|
||||
events={events}
|
||||
onClear={onResetEvents}
|
||||
highlightedEventId={highlightedEventId}
|
||||
onClearHighlight={onClearHighlight}
|
||||
/>
|
||||
<EventsTab events={events} onClear={onResetEvents} highlightedEventId={highlightedEventId} onClearHighlight={onClearHighlight} />
|
||||
)}
|
||||
|
||||
{debugTab === "agents" && (
|
||||
|
|
@ -128,21 +112,13 @@ const DebugPanel = ({
|
|||
/>
|
||||
)}
|
||||
|
||||
{debugTab === "mcp" && (
|
||||
<McpTab getClient={getClient} />
|
||||
)}
|
||||
{debugTab === "mcp" && <McpTab getClient={getClient} />}
|
||||
|
||||
{debugTab === "processes" && (
|
||||
<ProcessesTab getClient={getClient} />
|
||||
)}
|
||||
{debugTab === "processes" && <ProcessesTab getClient={getClient} />}
|
||||
|
||||
{debugTab === "run-process" && (
|
||||
<ProcessRunTab getClient={getClient} />
|
||||
)}
|
||||
{debugTab === "run-process" && <ProcessRunTab getClient={getClient} />}
|
||||
|
||||
{debugTab === "skills" && (
|
||||
<SkillsTab getClient={getClient} />
|
||||
)}
|
||||
{debugTab === "skills" && <SkillsTab getClient={getClient} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -125,12 +125,15 @@ const EventsTab = ({
|
|||
const handleCopy = () => {
|
||||
const text = JSON.stringify(events, null, 2);
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}).catch(() => {
|
||||
fallbackCopy(text);
|
||||
});
|
||||
navigator.clipboard
|
||||
.writeText(text)
|
||||
.then(() => {
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
})
|
||||
.catch(() => {
|
||||
fallbackCopy(text);
|
||||
});
|
||||
} else {
|
||||
fallbackCopy(text);
|
||||
}
|
||||
|
|
@ -188,13 +191,7 @@ const EventsTab = ({
|
|||
<div className="inline-row" style={{ marginBottom: 12, justifyContent: "space-between" }}>
|
||||
<span className="card-meta">{events.length} events</span>
|
||||
<div className="inline-row">
|
||||
<button
|
||||
type="button"
|
||||
className="button ghost small"
|
||||
onClick={handleCopy}
|
||||
disabled={events.length === 0}
|
||||
title="Copy all events as JSON"
|
||||
>
|
||||
<button type="button" className="button ghost small" onClick={handleCopy} disabled={events.length === 0} title="Copy all events as JSON">
|
||||
{copied ? "Copied" : "Copy JSON"}
|
||||
</button>
|
||||
<button className="button ghost small" onClick={onClear}>
|
||||
|
|
@ -204,9 +201,7 @@ const EventsTab = ({
|
|||
</div>
|
||||
|
||||
{events.length === 0 ? (
|
||||
<div className="card-meta">
|
||||
No events yet. Create a session and send a message.
|
||||
</div>
|
||||
<div className="card-meta">No events yet. Create a session and send a message.</div>
|
||||
) : (
|
||||
<div className="event-list">
|
||||
{[...events].reverse().map((event) => {
|
||||
|
|
@ -215,7 +210,7 @@ const EventsTab = ({
|
|||
const toggleCollapsed = () =>
|
||||
setCollapsedEvents((prev) => ({
|
||||
...prev,
|
||||
[eventKey]: !(prev[eventKey] ?? true)
|
||||
[eventKey]: !(prev[eventKey] ?? true),
|
||||
}));
|
||||
const method = getMethod(event);
|
||||
const payload = event.payload as Record<string, unknown>;
|
||||
|
|
@ -231,30 +226,21 @@ const EventsTab = ({
|
|||
id={`event-${event.id}`}
|
||||
className={`event-item ${isCollapsed ? "collapsed" : "expanded"} ${isHighlighted ? "highlighted" : ""}`}
|
||||
>
|
||||
<button
|
||||
className="event-summary"
|
||||
type="button"
|
||||
onClick={toggleCollapsed}
|
||||
title={isCollapsed ? "Expand payload" : "Collapse payload"}
|
||||
>
|
||||
<button className="event-summary" type="button" onClick={toggleCollapsed} title={isCollapsed ? "Expand payload" : "Collapse payload"}>
|
||||
<span className={`event-icon ${category}`}>
|
||||
<Icon size={14} />
|
||||
</span>
|
||||
<div className="event-summary-main">
|
||||
<div className="event-title-row">
|
||||
<span className={`event-type ${category}`}>{method}</span>
|
||||
<span className={`pill ${senderClass === "client" ? "accent" : "success"}`}>
|
||||
{event.sender}
|
||||
</span>
|
||||
<span className={`pill ${senderClass === "client" ? "accent" : "success"}`}>{event.sender}</span>
|
||||
<span className="event-time">{time}</span>
|
||||
</div>
|
||||
<div className="event-id" title={event.id}>
|
||||
{formatShortId(event.id)}
|
||||
</div>
|
||||
</div>
|
||||
<span className="event-chevron">
|
||||
{isCollapsed ? <ChevronRight size={16} /> : <ChevronDown size={16} />}
|
||||
</span>
|
||||
<span className="event-chevron">{isCollapsed ? <ChevronRight size={16} /> : <ChevronDown size={16} />}</span>
|
||||
</button>
|
||||
{!isCollapsed && <pre className="code-block event-payload">{formatJson(event.payload)}</pre>}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -10,11 +10,7 @@ type McpEntry = {
|
|||
|
||||
const MCP_DIRECTORY_STORAGE_KEY = "sandbox-agent-inspector-mcp-directory";
|
||||
|
||||
const McpTab = ({
|
||||
getClient,
|
||||
}: {
|
||||
getClient: () => SandboxAgent;
|
||||
}) => {
|
||||
const McpTab = ({ getClient }: { getClient: () => SandboxAgent }) => {
|
||||
const [directory, setDirectory] = useState(() => {
|
||||
if (typeof window === "undefined") return "/";
|
||||
try {
|
||||
|
|
@ -35,28 +31,29 @@ const McpTab = ({
|
|||
const [editError, setEditError] = useState<string | null>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
const loadAll = useCallback(async (dir: string) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const configPath = `${dir === "/" ? "" : dir}/.sandbox-agent/config/mcp.json`;
|
||||
const bytes = await getClient().readFsFile({ path: configPath });
|
||||
const text = new TextDecoder().decode(bytes);
|
||||
if (!text.trim()) {
|
||||
const loadAll = useCallback(
|
||||
async (dir: string) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const configPath = `${dir === "/" ? "" : dir}/.sandbox-agent/config/mcp.json`;
|
||||
const bytes = await getClient().readFsFile({ path: configPath });
|
||||
const text = new TextDecoder().decode(bytes);
|
||||
if (!text.trim()) {
|
||||
setEntries([]);
|
||||
return;
|
||||
}
|
||||
const map = JSON.parse(text) as Record<string, Record<string, unknown>>;
|
||||
setEntries(Object.entries(map).map(([name, config]) => ({ name, config })));
|
||||
} catch {
|
||||
// File doesn't exist yet or is empty — that's fine
|
||||
setEntries([]);
|
||||
return;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
const map = JSON.parse(text) as Record<string, Record<string, unknown>>;
|
||||
setEntries(
|
||||
Object.entries(map).map(([name, config]) => ({ name, config })),
|
||||
);
|
||||
} catch {
|
||||
// File doesn't exist yet or is empty — that's fine
|
||||
setEntries([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [getClient]);
|
||||
},
|
||||
[getClient],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
loadAll(directory);
|
||||
|
|
@ -106,10 +103,7 @@ const McpTab = ({
|
|||
setSaving(true);
|
||||
setEditError(null);
|
||||
try {
|
||||
await getClient().setMcpConfig(
|
||||
{ directory, mcpName: name },
|
||||
parsed as Parameters<SandboxAgent["setMcpConfig"]>[1],
|
||||
);
|
||||
await getClient().setMcpConfig({ directory, mcpName: name }, parsed as Parameters<SandboxAgent["setMcpConfig"]>[1]);
|
||||
cancelEdit();
|
||||
await loadAll(directory);
|
||||
} catch (err) {
|
||||
|
|
@ -159,26 +153,34 @@ const McpTab = ({
|
|||
{editing && (
|
||||
<div className="card" style={{ marginBottom: 12 }}>
|
||||
<div className="card-header">
|
||||
<span className="card-title">
|
||||
{editName ? `Edit: ${editName}` : "Add MCP Server"}
|
||||
</span>
|
||||
<span className="card-title">{editName ? `Edit: ${editName}` : "Add MCP Server"}</span>
|
||||
</div>
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<input
|
||||
className="setup-input"
|
||||
value={editName}
|
||||
onChange={(e) => { setEditName(e.target.value); setEditError(null); }}
|
||||
onChange={(e) => {
|
||||
setEditName(e.target.value);
|
||||
setEditError(null);
|
||||
}}
|
||||
placeholder="server-name"
|
||||
style={{ marginBottom: 8, width: "100%", boxSizing: "border-box" }}
|
||||
/>
|
||||
<textarea
|
||||
className="setup-input mono"
|
||||
value={editJson}
|
||||
onChange={(e) => { setEditJson(e.target.value); setEditError(null); }}
|
||||
onChange={(e) => {
|
||||
setEditJson(e.target.value);
|
||||
setEditError(null);
|
||||
}}
|
||||
rows={6}
|
||||
style={{ width: "100%", boxSizing: "border-box", fontFamily: "monospace", fontSize: 11, resize: "vertical" }}
|
||||
/>
|
||||
{editError && <div className="banner error" style={{ marginTop: 4 }}>{editError}</div>}
|
||||
{editError && (
|
||||
<div className="banner error" style={{ marginTop: 4 }}>
|
||||
{editError}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="card-actions">
|
||||
<button className="button primary small" onClick={save} disabled={saving}>
|
||||
|
|
@ -192,11 +194,7 @@ const McpTab = ({
|
|||
</div>
|
||||
)}
|
||||
|
||||
{entries.length === 0 && !editing && !loading && (
|
||||
<div className="card-meta">
|
||||
No MCP servers configured in this directory.
|
||||
</div>
|
||||
)}
|
||||
{entries.length === 0 && !editing && !loading && <div className="card-meta">No MCP servers configured in this directory.</div>}
|
||||
|
||||
{entries.map((entry) => {
|
||||
const isCollapsed = collapsedServers[entry.name] ?? true;
|
||||
|
|
@ -215,15 +213,8 @@ const McpTab = ({
|
|||
<span className="card-title">{entry.name}</span>
|
||||
</div>
|
||||
<div className="card-header-pills">
|
||||
<span className="pill accent">
|
||||
{(entry.config as { type?: string }).type ?? "unknown"}
|
||||
</span>
|
||||
<button
|
||||
className="button ghost small"
|
||||
onClick={() => remove(entry.name)}
|
||||
title="Remove"
|
||||
style={{ padding: "2px 4px" }}
|
||||
>
|
||||
<span className="pill accent">{(entry.config as { type?: string }).type ?? "unknown"}</span>
|
||||
<button className="button ghost small" onClick={() => remove(entry.name)} title="Remove" style={{ padding: "2px 4px" }}>
|
||||
<Trash2 size={12} />
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,13 +3,13 @@ import { useState } from "react";
|
|||
import { SandboxAgentError } from "sandbox-agent";
|
||||
import type { ProcessRunResponse, SandboxAgent } from "sandbox-agent";
|
||||
|
||||
const parseArgs = (value: string): string[] => value.split("\n").map((part) => part.trim()).filter(Boolean);
|
||||
const parseArgs = (value: string): string[] =>
|
||||
value
|
||||
.split("\n")
|
||||
.map((part) => part.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
const ProcessRunTab = ({
|
||||
getClient,
|
||||
}: {
|
||||
getClient: () => SandboxAgent;
|
||||
}) => {
|
||||
const ProcessRunTab = ({ getClient }: { getClient: () => SandboxAgent }) => {
|
||||
const [command, setCommand] = useState("");
|
||||
const [argsText, setArgsText] = useState("");
|
||||
const [cwd, setCwd] = useState("");
|
||||
|
|
@ -91,11 +91,7 @@ const ProcessRunTab = ({
|
|||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="process-advanced-toggle"
|
||||
onClick={() => setShowAdvanced((prev) => !prev)}
|
||||
type="button"
|
||||
>
|
||||
<button className="process-advanced-toggle" onClick={() => setShowAdvanced((prev) => !prev)} type="button">
|
||||
{showAdvanced ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
|
||||
Advanced
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -26,7 +26,11 @@ const formatDateTime = (value: number | null | undefined): string => {
|
|||
return new Date(value).toLocaleString();
|
||||
};
|
||||
|
||||
const parseArgs = (value: string): string[] => value.split("\n").map((part) => part.trim()).filter(Boolean);
|
||||
const parseArgs = (value: string): string[] =>
|
||||
value
|
||||
.split("\n")
|
||||
.map((part) => part.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
const formatCommandSummary = (process: Pick<ProcessInfo, "command" | "args">): string => {
|
||||
return [process.command, ...process.args].join(" ").trim();
|
||||
|
|
@ -36,11 +40,7 @@ const canOpenTerminal = (process: ProcessInfo | null | undefined): boolean => {
|
|||
return Boolean(process && process.status === "running" && process.interactive && process.tty);
|
||||
};
|
||||
|
||||
const ProcessesTab = ({
|
||||
getClient,
|
||||
}: {
|
||||
getClient: () => SandboxAgent;
|
||||
}) => {
|
||||
const ProcessesTab = ({ getClient }: { getClient: () => SandboxAgent }) => {
|
||||
const [processes, setProcesses] = useState<ProcessInfo[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
|
@ -62,63 +62,64 @@ const ProcessesTab = ({
|
|||
const [terminalOpen, setTerminalOpen] = useState(false);
|
||||
const [actingProcessId, setActingProcessId] = useState<string | null>(null);
|
||||
|
||||
const loadProcesses = useCallback(async (mode: "initial" | "refresh" = "initial") => {
|
||||
if (mode === "initial") {
|
||||
setLoading(true);
|
||||
} else {
|
||||
setRefreshing(true);
|
||||
}
|
||||
setError(null);
|
||||
try {
|
||||
const response = await getClient().listProcesses();
|
||||
setProcesses(response.processes);
|
||||
setSelectedProcessId((current) => {
|
||||
if (!current) {
|
||||
return response.processes[0]?.id ?? null;
|
||||
}
|
||||
return response.processes.some((listedProcess) => listedProcess.id === current)
|
||||
? current
|
||||
: response.processes[0]?.id ?? null;
|
||||
});
|
||||
} catch (loadError) {
|
||||
setError(extractErrorMessage(loadError, "Unable to load processes."));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
}
|
||||
}, [getClient]);
|
||||
const loadProcesses = useCallback(
|
||||
async (mode: "initial" | "refresh" = "initial") => {
|
||||
if (mode === "initial") {
|
||||
setLoading(true);
|
||||
} else {
|
||||
setRefreshing(true);
|
||||
}
|
||||
setError(null);
|
||||
try {
|
||||
const response = await getClient().listProcesses();
|
||||
setProcesses(response.processes);
|
||||
setSelectedProcessId((current) => {
|
||||
if (!current) {
|
||||
return response.processes[0]?.id ?? null;
|
||||
}
|
||||
return response.processes.some((listedProcess) => listedProcess.id === current) ? current : (response.processes[0]?.id ?? null);
|
||||
});
|
||||
} catch (loadError) {
|
||||
setError(extractErrorMessage(loadError, "Unable to load processes."));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
}
|
||||
},
|
||||
[getClient],
|
||||
);
|
||||
|
||||
const loadSelectedLogs = useCallback(async (process: ProcessInfo | null) => {
|
||||
if (!process) {
|
||||
setLogsText("");
|
||||
const loadSelectedLogs = useCallback(
|
||||
async (process: ProcessInfo | null) => {
|
||||
if (!process) {
|
||||
setLogsText("");
|
||||
setLogsError(null);
|
||||
return;
|
||||
}
|
||||
setLogsLoading(true);
|
||||
setLogsError(null);
|
||||
return;
|
||||
}
|
||||
setLogsLoading(true);
|
||||
setLogsError(null);
|
||||
try {
|
||||
const response = await getClient().getProcessLogs(process.id, {
|
||||
stream: process.tty ? "pty" : "combined",
|
||||
tail: 200,
|
||||
});
|
||||
const text = response.entries.map((logEntry) => decodeBase64Utf8(logEntry.data)).join("");
|
||||
setLogsText(text);
|
||||
} catch (loadError) {
|
||||
setLogsError(extractErrorMessage(loadError, "Unable to load process logs."));
|
||||
setLogsText("");
|
||||
} finally {
|
||||
setLogsLoading(false);
|
||||
}
|
||||
}, [getClient]);
|
||||
try {
|
||||
const response = await getClient().getProcessLogs(process.id, {
|
||||
stream: process.tty ? "pty" : "combined",
|
||||
tail: 200,
|
||||
});
|
||||
const text = response.entries.map((logEntry) => decodeBase64Utf8(logEntry.data)).join("");
|
||||
setLogsText(text);
|
||||
} catch (loadError) {
|
||||
setLogsError(extractErrorMessage(loadError, "Unable to load process logs."));
|
||||
setLogsText("");
|
||||
} finally {
|
||||
setLogsLoading(false);
|
||||
}
|
||||
},
|
||||
[getClient],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
void loadProcesses();
|
||||
}, [loadProcesses]);
|
||||
|
||||
const selectedProcess = useMemo(
|
||||
() => processes.find((process) => process.id === selectedProcessId) ?? null,
|
||||
[processes, selectedProcessId]
|
||||
);
|
||||
const selectedProcess = useMemo(() => processes.find((process) => process.id === selectedProcessId) ?? null, [processes, selectedProcessId]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadSelectedLogs(selectedProcess);
|
||||
|
|
@ -187,11 +188,7 @@ const ProcessesTab = ({
|
|||
<div className="processes-container">
|
||||
{/* Create form */}
|
||||
<div className="processes-section">
|
||||
<button
|
||||
className="processes-section-toggle"
|
||||
onClick={() => setShowCreateForm((prev) => !prev)}
|
||||
type="button"
|
||||
>
|
||||
<button className="processes-section-toggle" onClick={() => setShowCreateForm((prev) => !prev)} type="button">
|
||||
{showCreateForm ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
|
||||
<span>Create Process</span>
|
||||
</button>
|
||||
|
|
@ -311,7 +308,9 @@ const ProcessesTab = ({
|
|||
<span className={`process-status-dot ${process.status}`} />
|
||||
<span className="process-list-item-cmd mono">{formatCommandSummary(process)}</span>
|
||||
{process.interactive && process.tty && (
|
||||
<span className="pill neutral" style={{ fontSize: 9 }}>tty</span>
|
||||
<span className="pill neutral" style={{ fontSize: 9 }}>
|
||||
tty
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="process-list-item-meta">
|
||||
|
|
@ -336,7 +335,10 @@ const ProcessesTab = ({
|
|||
<>
|
||||
<button
|
||||
className="button secondary small"
|
||||
onClick={(e) => { e.stopPropagation(); void handleAction(process.id, "stop"); }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
void handleAction(process.id, "stop");
|
||||
}}
|
||||
disabled={Boolean(actingProcessId)}
|
||||
>
|
||||
{isStopping ? <Loader2 className="button-icon spinner-icon" size={12} /> : null}
|
||||
|
|
@ -344,7 +346,10 @@ const ProcessesTab = ({
|
|||
</button>
|
||||
<button
|
||||
className="button secondary small"
|
||||
onClick={(e) => { e.stopPropagation(); void handleAction(process.id, "kill"); }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
void handleAction(process.id, "kill");
|
||||
}}
|
||||
disabled={Boolean(actingProcessId)}
|
||||
>
|
||||
{isKilling ? <Loader2 className="button-icon spinner-icon" size={12} /> : <Skull className="button-icon" size={12} />}
|
||||
|
|
@ -355,7 +360,10 @@ const ProcessesTab = ({
|
|||
{process.status === "exited" ? (
|
||||
<button
|
||||
className="button secondary small"
|
||||
onClick={(e) => { e.stopPropagation(); void handleAction(process.id, "delete"); }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
void handleAction(process.id, "delete");
|
||||
}}
|
||||
disabled={Boolean(actingProcessId)}
|
||||
>
|
||||
{isDeleting ? <Loader2 className="button-icon spinner-icon" size={12} /> : <Trash2 className="button-icon" size={12} />}
|
||||
|
|
@ -385,30 +393,21 @@ const ProcessesTab = ({
|
|||
<span>Created: {formatDateTime(selectedProcess.createdAtMs)}</span>
|
||||
{selectedProcess.exitedAtMs ? <span>Exited: {formatDateTime(selectedProcess.exitedAtMs)}</span> : null}
|
||||
{selectedProcess.exitCode != null ? <span>Exit code: {selectedProcess.exitCode}</span> : null}
|
||||
<span className="mono" style={{ opacity: 0.6 }}>{selectedProcess.id}</span>
|
||||
<span className="mono" style={{ opacity: 0.6 }}>
|
||||
{selectedProcess.id}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Terminal */}
|
||||
{terminalOpen && canOpenTerminal(selectedProcess) ? (
|
||||
<ProcessTerminal
|
||||
client={getClient()}
|
||||
processId={selectedProcess.id}
|
||||
style={{ marginTop: 4 }}
|
||||
onExit={handleTerminalExit}
|
||||
/>
|
||||
<ProcessTerminal client={getClient()} processId={selectedProcess.id} style={{ marginTop: 4 }} onExit={handleTerminalExit} />
|
||||
) : canOpenTerminal(selectedProcess) ? (
|
||||
<button
|
||||
className="button secondary small"
|
||||
onClick={() => setTerminalOpen(true)}
|
||||
style={{ marginTop: 8 }}
|
||||
>
|
||||
<button className="button secondary small" onClick={() => setTerminalOpen(true)} style={{ marginTop: 8 }}>
|
||||
<SquareTerminal className="button-icon" size={12} />
|
||||
Open Terminal
|
||||
</button>
|
||||
) : selectedProcess.interactive && selectedProcess.tty ? (
|
||||
<div className="process-terminal-empty">
|
||||
Terminal available while process is running.
|
||||
</div>
|
||||
<div className="process-terminal-empty">Terminal available while process is running.</div>
|
||||
) : null}
|
||||
|
||||
{/* Logs */}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ const RequestLogTab = ({
|
|||
requestLog,
|
||||
copiedLogId,
|
||||
onClear,
|
||||
onCopy
|
||||
onCopy,
|
||||
}: {
|
||||
requestLog: RequestLog[];
|
||||
copiedLogId: number | null;
|
||||
|
|
@ -49,16 +49,16 @@ const RequestLogTab = ({
|
|||
<div className="event-summary-main">
|
||||
<div className="event-title-row">
|
||||
<span className="log-method">{entry.method}</span>
|
||||
<span className="log-url text-truncate" style={{ flex: 1 }}>{entry.url}</span>
|
||||
<span className="log-url text-truncate" style={{ flex: 1 }}>
|
||||
{entry.url}
|
||||
</span>
|
||||
</div>
|
||||
<div className="event-id">
|
||||
{entry.time}
|
||||
{entry.error && ` - ${entry.error}`}
|
||||
</div>
|
||||
</div>
|
||||
<span className={`log-status ${entry.status && entry.status < 400 ? "ok" : "error"}`}>
|
||||
{entry.status || "ERR"}
|
||||
</span>
|
||||
<span className={`log-status ${entry.status && entry.status < 400 ? "ok" : "error"}`}>{entry.status || "ERR"}</span>
|
||||
<span
|
||||
className="copy-button"
|
||||
onClick={(e) => {
|
||||
|
|
@ -77,18 +77,18 @@ const RequestLogTab = ({
|
|||
<Clipboard size={14} />
|
||||
{copiedLogId === entry.id ? "Copied" : "curl"}
|
||||
</span>
|
||||
{hasDetails && (
|
||||
<span className="event-chevron">
|
||||
{isExpanded ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
|
||||
</span>
|
||||
)}
|
||||
{hasDetails && <span className="event-chevron">{isExpanded ? <ChevronDown size={16} /> : <ChevronRight size={16} />}</span>}
|
||||
</button>
|
||||
{isExpanded && (
|
||||
<div className="event-payload" style={{ padding: "8px 12px" }}>
|
||||
{entry.headers && Object.keys(entry.headers).length > 0 && (
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<div className="part-title">Request Headers</div>
|
||||
<pre className="code-block">{Object.entries(entry.headers).map(([k, v]) => `${k}: ${v}`).join("\n")}</pre>
|
||||
<pre className="code-block">
|
||||
{Object.entries(entry.headers)
|
||||
.map(([k, v]) => `${k}: ${v}`)
|
||||
.join("\n")}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
{entry.body && (
|
||||
|
|
|
|||
|
|
@ -10,11 +10,7 @@ type SkillEntry = {
|
|||
|
||||
const SKILLS_DIRECTORY_STORAGE_KEY = "sandbox-agent-inspector-skills-directory";
|
||||
|
||||
const SkillsTab = ({
|
||||
getClient,
|
||||
}: {
|
||||
getClient: () => SandboxAgent;
|
||||
}) => {
|
||||
const SkillsTab = ({ getClient }: { getClient: () => SandboxAgent }) => {
|
||||
const officialSkills = [
|
||||
{
|
||||
name: "Sandbox Agent SDK",
|
||||
|
|
@ -27,13 +23,7 @@ const SkillsTab = ({
|
|||
skillId: "rivet",
|
||||
source: "rivet-dev/skills",
|
||||
summary: "Open-source platform for building, deploying, and scaling AI agents.",
|
||||
features: [
|
||||
"Session Persistence",
|
||||
"Resumable Sessions",
|
||||
"Multi-Agent Support",
|
||||
"Realtime Events",
|
||||
"Tool Call Visibility",
|
||||
],
|
||||
features: ["Session Persistence", "Resumable Sessions", "Multi-Agent Support", "Realtime Events", "Tool Call Visibility"],
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -63,28 +53,29 @@ const SkillsTab = ({
|
|||
const [editError, setEditError] = useState<string | null>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
const loadAll = useCallback(async (dir: string) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const configPath = `${dir === "/" ? "" : dir}/.sandbox-agent/config/skills.json`;
|
||||
const bytes = await getClient().readFsFile({ path: configPath });
|
||||
const text = new TextDecoder().decode(bytes);
|
||||
if (!text.trim()) {
|
||||
const loadAll = useCallback(
|
||||
async (dir: string) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const configPath = `${dir === "/" ? "" : dir}/.sandbox-agent/config/skills.json`;
|
||||
const bytes = await getClient().readFsFile({ path: configPath });
|
||||
const text = new TextDecoder().decode(bytes);
|
||||
if (!text.trim()) {
|
||||
setEntries([]);
|
||||
return;
|
||||
}
|
||||
const map = JSON.parse(text) as Record<string, SkillEntry["config"]>;
|
||||
setEntries(Object.entries(map).map(([name, config]) => ({ name, config })));
|
||||
} catch {
|
||||
// File doesn't exist yet or is empty — that's fine
|
||||
setEntries([]);
|
||||
return;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
const map = JSON.parse(text) as Record<string, SkillEntry["config"]>;
|
||||
setEntries(
|
||||
Object.entries(map).map(([name, config]) => ({ name, config })),
|
||||
);
|
||||
} catch {
|
||||
// File doesn't exist yet or is empty — that's fine
|
||||
setEntries([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [getClient]);
|
||||
},
|
||||
[getClient],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
loadAll(directory);
|
||||
|
|
@ -139,7 +130,10 @@ const SkillsTab = ({
|
|||
if (editRef.trim()) skillEntry.ref = editRef.trim();
|
||||
if (editSubpath.trim()) skillEntry.subpath = editSubpath.trim();
|
||||
const skillsList = editSkills.trim()
|
||||
? editSkills.split(",").map((s) => s.trim()).filter(Boolean)
|
||||
? editSkills
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
: null;
|
||||
if (skillsList && skillsList.length > 0) skillEntry.skills = skillsList;
|
||||
|
||||
|
|
@ -148,10 +142,7 @@ const SkillsTab = ({
|
|||
setSaving(true);
|
||||
setEditError(null);
|
||||
try {
|
||||
await getClient().setSkillsConfig(
|
||||
{ directory, skillName: name },
|
||||
config,
|
||||
);
|
||||
await getClient().setSkillsConfig({ directory, skillName: name }, config);
|
||||
cancelEdit();
|
||||
await loadAll(directory);
|
||||
} catch (err) {
|
||||
|
|
@ -197,7 +188,7 @@ const SkillsTab = ({
|
|||
}
|
||||
};
|
||||
|
||||
const applySkillPreset = (skill: typeof officialSkills[0]) => {
|
||||
const applySkillPreset = (skill: (typeof officialSkills)[0]) => {
|
||||
setEditing(true);
|
||||
setEditName(skill.skillId);
|
||||
setEditSource(skill.source);
|
||||
|
|
@ -222,12 +213,12 @@ const SkillsTab = ({
|
|||
<div className="inline-row" style={{ marginBottom: 12, justifyContent: "space-between" }}>
|
||||
<span className="card-meta">Skills Configuration</span>
|
||||
<div className="inline-row" style={{ gap: 6 }}>
|
||||
<button
|
||||
className="button secondary small"
|
||||
onClick={() => setShowSdkSkills((prev) => !prev)}
|
||||
title="Toggle official skills list"
|
||||
>
|
||||
{showSdkSkills ? <ChevronDown className="button-icon" style={{ width: 12, height: 12 }} /> : <ChevronRight className="button-icon" style={{ width: 12, height: 12 }} />}
|
||||
<button className="button secondary small" onClick={() => setShowSdkSkills((prev) => !prev)} title="Toggle official skills list">
|
||||
{showSdkSkills ? (
|
||||
<ChevronDown className="button-icon" style={{ width: 12, height: 12 }} />
|
||||
) : (
|
||||
<ChevronRight className="button-icon" style={{ width: 12, height: 12 }} />
|
||||
)}
|
||||
Official Skills
|
||||
</button>
|
||||
{!editing && (
|
||||
|
|
@ -261,7 +252,9 @@ const SkillsTab = ({
|
|||
{copiedId === `skill-input-${skill.skillId}` ? "Filled" : "Use"}
|
||||
</button>
|
||||
</div>
|
||||
<div className="card-meta" style={{ fontSize: 10, marginBottom: skill.features ? 6 : 0 }}>{skill.summary}</div>
|
||||
<div className="card-meta" style={{ fontSize: 10, marginBottom: skill.features ? 6 : 0 }}>
|
||||
{skill.summary}
|
||||
</div>
|
||||
{skill.features && (
|
||||
<div style={{ display: "flex", flexWrap: "wrap", gap: 4 }}>
|
||||
{skill.features.map((feature) => (
|
||||
|
|
@ -299,17 +292,15 @@ const SkillsTab = ({
|
|||
<input
|
||||
className="setup-input"
|
||||
value={editName}
|
||||
onChange={(e) => { setEditName(e.target.value); setEditError(null); }}
|
||||
onChange={(e) => {
|
||||
setEditName(e.target.value);
|
||||
setEditError(null);
|
||||
}}
|
||||
placeholder="skill-name"
|
||||
style={{ marginBottom: 6, width: "100%", boxSizing: "border-box" }}
|
||||
/>
|
||||
<div className="inline-row" style={{ marginBottom: 6, gap: 4 }}>
|
||||
<select
|
||||
className="setup-select"
|
||||
value={editType}
|
||||
onChange={(e) => setEditType(e.target.value)}
|
||||
style={{ width: 90 }}
|
||||
>
|
||||
<select className="setup-select" value={editType} onChange={(e) => setEditType(e.target.value)} style={{ width: 90 }}>
|
||||
<option value="github">github</option>
|
||||
<option value="local">local</option>
|
||||
<option value="git">git</option>
|
||||
|
|
@ -317,7 +308,10 @@ const SkillsTab = ({
|
|||
<input
|
||||
className="setup-input mono"
|
||||
value={editSource}
|
||||
onChange={(e) => { setEditSource(e.target.value); setEditError(null); }}
|
||||
onChange={(e) => {
|
||||
setEditSource(e.target.value);
|
||||
setEditError(null);
|
||||
}}
|
||||
placeholder={editType === "github" ? "owner/repo" : editType === "local" ? "/path/to/skill" : "https://..."}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
|
|
@ -347,7 +341,11 @@ const SkillsTab = ({
|
|||
/>
|
||||
</div>
|
||||
)}
|
||||
{editError && <div className="banner error" style={{ marginTop: 4 }}>{editError}</div>}
|
||||
{editError && (
|
||||
<div className="banner error" style={{ marginTop: 4 }}>
|
||||
{editError}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="card-actions">
|
||||
<button className="button primary small" onClick={save} disabled={saving}>
|
||||
|
|
@ -361,11 +359,7 @@ const SkillsTab = ({
|
|||
</div>
|
||||
)}
|
||||
|
||||
{entries.length === 0 && !editing && !loading && (
|
||||
<div className="card-meta">
|
||||
No skills configured in this directory.
|
||||
</div>
|
||||
)}
|
||||
{entries.length === 0 && !editing && !loading && <div className="card-meta">No skills configured in this directory.</div>}
|
||||
|
||||
{entries.map((entry) => {
|
||||
const isCollapsed = collapsedSkills[entry.name] ?? true;
|
||||
|
|
@ -387,12 +381,7 @@ const SkillsTab = ({
|
|||
<span className="pill accent">
|
||||
{entry.config.sources.length} source{entry.config.sources.length !== 1 ? "s" : ""}
|
||||
</span>
|
||||
<button
|
||||
className="button ghost small"
|
||||
onClick={() => remove(entry.name)}
|
||||
title="Remove"
|
||||
style={{ padding: "2px 4px" }}
|
||||
>
|
||||
<button className="button ghost small" onClick={() => remove(entry.name)} title="Remove" style={{ padding: "2px 4px" }}>
|
||||
<Trash2 size={12} />
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue