feat: acp http adapter

This commit is contained in:
Nathan Flurry 2026-02-10 16:05:56 -08:00
parent 2ba630c180
commit b4c8564cb2
217 changed files with 18785 additions and 17400 deletions

View file

@ -3,7 +3,7 @@
## Inspector Architecture
- Inspector source is `frontend/packages/inspector/`.
- `/ui/` must use ACP over HTTP (`/v2/rpc`) for session/prompt traffic.
- `/ui/` must use ACP over HTTP (`/v1/rpc`) for session/prompt traffic.
- Primary flow:
- `initialize`
- `session/new`

View file

@ -110,6 +110,25 @@
color: var(--muted);
}
.header-link {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 5px 10px;
border: 1px solid var(--border-2);
border-radius: var(--radius-sm);
font-size: 11px;
font-weight: 600;
color: var(--muted);
text-decoration: none;
transition: color var(--transition), border-color var(--transition);
}
.header-link:hover {
color: var(--accent);
border-color: var(--accent);
}
.status-indicator.disconnected {
display: flex;
align-items: center;
@ -404,7 +423,7 @@
flex-direction: column;
border-right: 1px solid var(--border);
background: var(--surface-2);
overflow: visible;
min-height: 0;
}
.sidebar-header {
@ -555,6 +574,20 @@
min-width: 0;
}
.setup-custom-back {
flex-shrink: 0;
background: none;
border: none;
color: var(--accent);
font-size: 10px;
cursor: pointer;
padding: 2px 4px;
}
.setup-custom-back:hover {
text-decoration: underline;
}
.session-create-section {
overflow: hidden;
}
@ -1055,6 +1088,23 @@
color: var(--danger);
}
.session-persistence-note {
padding: 8px 10px 10px;
border-top: 1px solid var(--border);
font-size: 10px;
line-height: 1.45;
color: var(--muted);
}
.session-persistence-note a {
color: var(--accent);
text-decoration: none;
}
.session-persistence-note a:hover {
text-decoration: underline;
}
/* Chat Panel */
.chat-panel {
display: flex;
@ -1322,6 +1372,64 @@
margin-top: 8px;
}
.toast-stack {
position: fixed;
right: 16px;
bottom: 16px;
z-index: 1000;
display: flex;
flex-direction: column;
gap: 8px;
width: min(420px, calc(100vw - 24px));
pointer-events: none;
}
.toast {
pointer-events: auto;
display: flex;
align-items: flex-start;
gap: 10px;
padding: 10px 12px;
border-radius: var(--radius-sm);
border: 1px solid rgba(255, 59, 48, 0.35);
background: rgba(28, 8, 8, 0.95);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.35);
}
.toast-content {
min-width: 0;
}
.toast-title {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.4px;
color: var(--danger);
margin-bottom: 4px;
}
.toast-message {
color: var(--text-secondary);
font-size: 12px;
line-height: 1.4;
word-break: break-word;
}
.toast-close {
background: transparent;
border: none;
color: var(--muted);
font-size: 12px;
line-height: 1;
cursor: pointer;
padding: 2px;
margin-top: 1px;
}
.toast-close:hover {
color: var(--text);
}
.cursor {
display: inline-block;
width: 2px;
@ -1932,84 +2040,82 @@
letter-spacing: 0.3px;
}
.event-type.session,
.event-type.session-started,
.event-type.session-ended {
/* ACP event categories: connection */
.event-type.connection,
.event-type.session {
color: var(--success);
}
.event-type.item,
.event-type.item-started,
.event-type.item-completed {
color: var(--accent);
}
.event-type.item-delta {
color: var(--cyan);
}
.event-type.error,
.event-type.agent-unparsed {
color: var(--danger);
}
.event-type.question,
.event-type.question-requested,
.event-type.question-resolved {
color: var(--warning);
}
.event-type.permission,
.event-type.permission-requested,
.event-type.permission-resolved {
color: var(--purple);
}
.event-icon.session,
.event-icon.session-started,
.event-icon.session-ended {
.event-icon.connection,
.event-icon.session {
color: var(--success);
border-color: rgba(48, 209, 88, 0.35);
background: rgba(48, 209, 88, 0.12);
}
.event-icon.item,
.event-icon.item-started,
.event-icon.item-completed {
/* ACP event categories: prompt / tool */
.event-type.prompt,
.event-type.tool {
color: var(--accent);
}
.event-icon.prompt,
.event-icon.tool {
color: var(--accent);
border-color: rgba(255, 79, 0, 0.35);
background: rgba(255, 79, 0, 0.12);
}
.event-icon.item-delta {
/* ACP event categories: update / terminal (streaming, realtime) */
.event-type.update,
.event-type.terminal {
color: var(--cyan);
}
.event-icon.update,
.event-icon.terminal {
color: var(--cyan);
border-color: rgba(100, 210, 255, 0.35);
background: rgba(100, 210, 255, 0.12);
}
.event-icon.error,
.event-icon.agent-unparsed {
/* ACP event categories: cancel */
.event-type.cancel {
color: var(--danger);
}
.event-icon.cancel {
color: var(--danger);
border-color: rgba(255, 59, 48, 0.35);
background: rgba(255, 59, 48, 0.12);
}
.event-icon.question,
.event-icon.question-requested,
.event-icon.question-resolved {
/* ACP event categories: filesystem */
.event-type.filesystem {
color: var(--warning);
}
.event-icon.filesystem {
color: var(--warning);
border-color: rgba(255, 159, 10, 0.35);
background: rgba(255, 159, 10, 0.12);
}
.event-icon.permission,
.event-icon.permission-requested,
.event-icon.permission-resolved {
/* ACP event categories: config / permission */
.event-type.config,
.event-type.permission {
color: var(--purple);
}
.event-icon.config,
.event-icon.permission {
color: var(--purple);
border-color: rgba(191, 90, 242, 0.35);
background: rgba(191, 90, 242, 0.12);
}
/* ACP event categories: response (fallback) */
.event-type.response {
color: var(--muted);
}
.event-icon.response {
color: var(--muted);
}
.event-time {
font-size: 10px;
color: var(--muted);
@ -2247,6 +2353,13 @@
.header-title {
display: none;
}
.toast-stack {
left: 12px;
right: 12px;
bottom: 12px;
width: auto;
}
}
</style>
</head>

View file

@ -6,19 +6,23 @@
"type": "module",
"scripts": {
"dev": "vite",
"build": "pnpm --filter sandbox-agent build && vite build",
"build": "SKIP_OPENAPI_GEN=1 pnpm --filter @sandbox-agent/persist-indexeddb build && vite build",
"preview": "vite preview",
"typecheck": "tsc --noEmit"
"typecheck": "SKIP_OPENAPI_GEN=1 pnpm --filter @sandbox-agent/persist-indexeddb build && tsc --noEmit",
"test": "SKIP_OPENAPI_GEN=1 pnpm --filter @sandbox-agent/persist-indexeddb build && vitest run"
},
"devDependencies": {
"sandbox-agent": "workspace:*",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.1",
"fake-indexeddb": "^6.2.4",
"typescript": "^5.7.3",
"vite": "^5.4.7"
"vite": "^5.4.7",
"vitest": "^3.0.0"
},
"dependencies": {
"@sandbox-agent/persist-indexeddb": "workspace:*",
"lucide-react": "^0.469.0",
"react": "^18.3.1",
"react-dom": "^18.3.1"

View file

@ -1,4 +1,4 @@
import { AlertTriangle, Zap } from "lucide-react";
import { AlertTriangle, BookOpen, Zap } from "lucide-react";
import { isHttpsToHttpConnection, isLocalNetworkTarget } from "../lib/permissions";
const logoUrl = `${import.meta.env.BASE_URL}logos/sandboxagent.svg`;
@ -11,7 +11,9 @@ const ConnectScreen = ({
onEndpointChange,
onTokenChange,
onConnect,
reportUrl
reportUrl,
docsUrl,
discordUrl,
}: {
endpoint: string;
token: string;
@ -21,6 +23,8 @@ const ConnectScreen = ({
onTokenChange: (value: string) => void;
onConnect: () => void;
reportUrl?: string;
docsUrl?: string;
discordUrl?: string;
}) => {
return (
<div className="app">
@ -28,11 +32,26 @@ const ConnectScreen = ({
<div className="header-left">
<img src={logoUrl} alt="Sandbox Agent" className="logo-text" style={{ height: '20px', width: 'auto' }} />
</div>
{reportUrl && (
{(docsUrl || discordUrl || reportUrl) && (
<div className="header-right">
<a className="button ghost small" href={reportUrl} target="_blank" rel="noreferrer">
Report Bug
</a>
{docsUrl && (
<a className="header-link" href={docsUrl} target="_blank" rel="noreferrer">
<BookOpen size={12} />
Docs
</a>
)}
{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>
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>
Issues
</a>
)}
</div>
)}
</header>

View file

@ -1,15 +1,17 @@
import { ArrowLeft, ArrowRight, ChevronDown, ChevronRight, Pencil, Plus, X } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import type { McpServerEntry } from "../App";
import type { AgentInfo, AgentModelInfo, AgentModeInfo, SkillSource } from "../types/legacyApi";
import { ArrowLeft, ArrowRight } from "lucide-react";
import { useEffect, useState } from "react";
import type { AgentInfo } from "sandbox-agent";
type AgentModeInfo = { id: string; name: string; description: string };
type AgentModelInfo = { id: string; name?: string };
export type SessionConfig = {
model: string;
agentMode: string;
permissionMode: string;
variant: string;
model: string;
};
const CUSTOM_MODEL_VALUE = "__custom__";
const agentLabels: Record<string, string> = {
claude: "Claude Code",
codex: "Codex",
@ -17,59 +19,6 @@ const agentLabels: Record<string, string> = {
amp: "Amp"
};
const validateServerJson = (json: string): string | null => {
const trimmed = json.trim();
if (!trimmed) return "Config is required";
try {
const parsed = JSON.parse(trimmed);
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
return "Must be a JSON object";
}
if (!parsed.type) return 'Missing "type" field';
if (parsed.type !== "local" && parsed.type !== "remote") {
return 'Type must be "local" or "remote"';
}
if (parsed.type === "local" && !parsed.command) return 'Local server requires "command"';
if (parsed.type === "remote" && !parsed.url) return 'Remote server requires "url"';
return null;
} catch {
return "Invalid JSON";
}
};
const getServerType = (configJson: string): string | null => {
try {
const parsed = JSON.parse(configJson);
return parsed?.type ?? null;
} catch {
return null;
}
};
const getServerSummary = (configJson: string): string => {
try {
const parsed = JSON.parse(configJson);
if (parsed?.type === "local") {
const cmd = Array.isArray(parsed.command) ? parsed.command.join(" ") : parsed.command;
return cmd ?? "local";
}
if (parsed?.type === "remote") {
return parsed.url ?? "remote";
}
return parsed?.type ?? "";
} catch {
return "";
}
};
const skillSourceSummary = (source: SkillSource): string => {
let summary = source.source;
if (source.skills && source.skills.length > 0) {
summary += ` [${source.skills.join(", ")}]`;
}
return summary;
};
const SessionCreateMenu = ({
agents,
agentsLoading,
@ -77,17 +26,8 @@ const SessionCreateMenu = ({
modesByAgent,
modelsByAgent,
defaultModelByAgent,
modesLoadingByAgent,
modelsLoadingByAgent,
modesErrorByAgent,
modelsErrorByAgent,
mcpServers,
onMcpServersChange,
mcpConfigError,
skillSources,
onSkillSourcesChange,
onSelectAgent,
onCreateSession,
onSelectAgent,
open,
onClose
}: {
@ -97,60 +37,18 @@ const SessionCreateMenu = ({
modesByAgent: Record<string, AgentModeInfo[]>;
modelsByAgent: Record<string, AgentModelInfo[]>;
defaultModelByAgent: Record<string, string>;
modesLoadingByAgent: Record<string, boolean>;
modelsLoadingByAgent: Record<string, boolean>;
modesErrorByAgent: Record<string, string | null>;
modelsErrorByAgent: Record<string, string | null>;
mcpServers: McpServerEntry[];
onMcpServersChange: (servers: McpServerEntry[]) => void;
mcpConfigError: string | null;
skillSources: SkillSource[];
onSkillSourcesChange: (sources: SkillSource[]) => void;
onSelectAgent: (agentId: string) => void;
onCreateSession: (agentId: string, config: SessionConfig) => void;
onSelectAgent: (agentId: string) => Promise<void>;
open: boolean;
onClose: () => void;
}) => {
const [phase, setPhase] = useState<"agent" | "config">("agent");
const [phase, setPhase] = useState<"agent" | "config" | "loading-config">("agent");
const [selectedAgent, setSelectedAgent] = useState("");
const [agentMode, setAgentMode] = useState("");
const [permissionMode, setPermissionMode] = useState("default");
const [model, setModel] = useState("");
const [variant, setVariant] = useState("");
const [mcpExpanded, setMcpExpanded] = useState(false);
const [skillsExpanded, setSkillsExpanded] = useState(false);
// Skill add/edit state
const [addingSkill, setAddingSkill] = useState(false);
const [editingSkillIndex, setEditingSkillIndex] = useState<number | null>(null);
const [skillType, setSkillType] = useState<"github" | "local" | "git">("github");
const [skillSource, setSkillSource] = useState("");
const [skillFilter, setSkillFilter] = useState("");
const [skillRef, setSkillRef] = useState("");
const [skillSubpath, setSkillSubpath] = useState("");
const [skillLocalError, setSkillLocalError] = useState<string | null>(null);
const skillSourceRef = useRef<HTMLInputElement>(null);
// MCP add/edit state
const [addingMcp, setAddingMcp] = useState(false);
const [editingMcpIndex, setEditingMcpIndex] = useState<number | null>(null);
const [mcpName, setMcpName] = useState("");
const [mcpJson, setMcpJson] = useState("");
const [mcpLocalError, setMcpLocalError] = useState<string | null>(null);
const mcpNameRef = useRef<HTMLInputElement>(null);
const mcpJsonRef = useRef<HTMLTextAreaElement>(null);
const cancelSkillEdit = () => {
setAddingSkill(false);
setEditingSkillIndex(null);
setSkillType("github");
setSkillSource("");
setSkillFilter("");
setSkillRef("");
setSkillSubpath("");
setSkillLocalError(null);
};
const [selectedModel, setSelectedModel] = useState("");
const [customModel, setCustomModel] = useState("");
const [isCustomModel, setIsCustomModel] = useState(false);
const [configLoadDone, setConfigLoadDone] = useState(false);
// Reset state when menu closes
useEffect(() => {
@ -158,20 +56,21 @@ const SessionCreateMenu = ({
setPhase("agent");
setSelectedAgent("");
setAgentMode("");
setPermissionMode("default");
setModel("");
setVariant("");
setMcpExpanded(false);
setSkillsExpanded(false);
cancelSkillEdit();
setAddingMcp(false);
setEditingMcpIndex(null);
setMcpName("");
setMcpJson("");
setMcpLocalError(null);
setSelectedModel("");
setCustomModel("");
setIsCustomModel(false);
setConfigLoadDone(false);
}
}, [open]);
// Transition to config phase after load completes — deferred via useEffect
// so parent props (modelsByAgent) have settled before we render the config form
useEffect(() => {
if (phase === "loading-config" && configLoadDone) {
setPhase("config");
}
}, [phase, configLoadDone]);
// Auto-select first mode when modes load for selected agent
useEffect(() => {
if (!selectedAgent) return;
@ -181,174 +80,60 @@ const SessionCreateMenu = ({
}
}, [modesByAgent, selectedAgent, agentMode]);
// Focus skill source input when adding
// Auto-select default model when agent is selected
useEffect(() => {
if ((addingSkill || editingSkillIndex !== null) && skillSourceRef.current) {
skillSourceRef.current.focus();
if (!selectedAgent) return;
if (selectedModel) return;
const defaultModel = defaultModelByAgent[selectedAgent];
if (defaultModel) {
setSelectedModel(defaultModel);
} else {
const models = modelsByAgent[selectedAgent];
if (models && models.length > 0) {
setSelectedModel(models[0].id);
}
}
}, [addingSkill, editingSkillIndex]);
// Focus MCP name input when adding
useEffect(() => {
if (addingMcp && mcpNameRef.current) {
mcpNameRef.current.focus();
}
}, [addingMcp]);
// Focus MCP json textarea when editing
useEffect(() => {
if (editingMcpIndex !== null && mcpJsonRef.current) {
mcpJsonRef.current.focus();
}
}, [editingMcpIndex]);
}, [modelsByAgent, defaultModelByAgent, selectedAgent, selectedModel]);
if (!open) return null;
const handleAgentClick = (agentId: string) => {
setSelectedAgent(agentId);
setPhase("config");
onSelectAgent(agentId);
setPhase("loading-config");
setConfigLoadDone(false);
onSelectAgent(agentId).finally(() => {
setConfigLoadDone(true);
});
};
const handleBack = () => {
setPhase("agent");
setSelectedAgent("");
setAgentMode("");
setPermissionMode("default");
setModel("");
setVariant("");
setSelectedModel("");
setCustomModel("");
setIsCustomModel(false);
setConfigLoadDone(false);
};
const handleModelSelectChange = (value: string) => {
if (value === CUSTOM_MODEL_VALUE) {
setIsCustomModel(true);
setSelectedModel("");
} else {
setIsCustomModel(false);
setCustomModel("");
setSelectedModel(value);
}
};
const resolvedModel = isCustomModel ? customModel : selectedModel;
const handleCreate = () => {
if (mcpConfigError) return;
onCreateSession(selectedAgent, { model, agentMode, permissionMode, variant });
onCreateSession(selectedAgent, { agentMode, model: resolvedModel });
onClose();
};
// Skill source helpers
const startAddSkill = () => {
setAddingSkill(true);
setEditingSkillIndex(null);
setSkillType("github");
setSkillSource("rivet-dev/skills");
setSkillFilter("sandbox-agent");
setSkillRef("");
setSkillSubpath("");
setSkillLocalError(null);
};
const startEditSkill = (index: number) => {
const entry = skillSources[index];
setEditingSkillIndex(index);
setAddingSkill(false);
setSkillType(entry.type as "github" | "local" | "git");
setSkillSource(entry.source);
setSkillFilter(entry.skills?.join(", ") ?? "");
setSkillRef(entry.ref ?? "");
setSkillSubpath(entry.subpath ?? "");
setSkillLocalError(null);
};
const commitSkill = () => {
const src = skillSource.trim();
if (!src) {
setSkillLocalError("Source is required");
return;
}
const entry: SkillSource = {
type: skillType,
source: src,
};
const filterList = skillFilter.trim()
? skillFilter.split(",").map((s) => s.trim()).filter(Boolean)
: undefined;
if (filterList && filterList.length > 0) entry.skills = filterList;
if (skillRef.trim()) entry.ref = skillRef.trim();
if (skillSubpath.trim()) entry.subpath = skillSubpath.trim();
if (editingSkillIndex !== null) {
const updated = [...skillSources];
updated[editingSkillIndex] = entry;
onSkillSourcesChange(updated);
} else {
onSkillSourcesChange([...skillSources, entry]);
}
cancelSkillEdit();
};
const removeSkill = (index: number) => {
onSkillSourcesChange(skillSources.filter((_, i) => i !== index));
if (editingSkillIndex === index) {
cancelSkillEdit();
}
};
const isEditingSkill = addingSkill || editingSkillIndex !== null;
const startAddMcp = () => {
setAddingMcp(true);
setEditingMcpIndex(null);
setMcpName("everything");
setMcpJson('{\n "type": "local",\n "command": "npx",\n "args": ["@modelcontextprotocol/server-everything"]\n}');
setMcpLocalError(null);
};
const startEditMcp = (index: number) => {
const entry = mcpServers[index];
setEditingMcpIndex(index);
setAddingMcp(false);
setMcpName(entry.name);
setMcpJson(entry.configJson);
setMcpLocalError(entry.error);
};
const cancelMcpEdit = () => {
setAddingMcp(false);
setEditingMcpIndex(null);
setMcpName("");
setMcpJson("");
setMcpLocalError(null);
};
const commitMcp = () => {
const name = mcpName.trim();
if (!name) {
setMcpLocalError("Server name is required");
return;
}
const error = validateServerJson(mcpJson);
if (error) {
setMcpLocalError(error);
return;
}
// Check for duplicate names (except when editing the same entry)
const duplicate = mcpServers.findIndex((e) => e.name === name);
if (duplicate !== -1 && duplicate !== editingMcpIndex) {
setMcpLocalError(`Server "${name}" already exists`);
return;
}
const entry: McpServerEntry = { name, configJson: mcpJson.trim(), error: null };
if (editingMcpIndex !== null) {
const updated = [...mcpServers];
updated[editingMcpIndex] = entry;
onMcpServersChange(updated);
} else {
onMcpServersChange([...mcpServers, entry]);
}
cancelMcpEdit();
};
const removeMcp = (index: number) => {
onMcpServersChange(mcpServers.filter((_, i) => i !== index));
if (editingMcpIndex === index) {
cancelMcpEdit();
}
};
const isEditingMcp = addingMcp || editingMcpIndex !== null;
if (phase === "agent") {
return (
<div className="session-create-menu">
@ -378,30 +163,25 @@ const SessionCreateMenu = ({
);
}
const agentLabel = agentLabels[selectedAgent] ?? selectedAgent;
if (phase === "loading-config") {
return (
<div className="session-create-menu">
<div className="session-create-header">
<button className="session-create-back" onClick={handleBack} title="Back to agents">
<ArrowLeft size={14} />
</button>
<span className="session-create-agent-name">{agentLabel}</span>
</div>
<div className="sidebar-add-status">Loading config...</div>
</div>
);
}
// Phase 2: config form
const activeModes = modesByAgent[selectedAgent] ?? [];
const modesLoading = modesLoadingByAgent[selectedAgent] ?? false;
const modesError = modesErrorByAgent[selectedAgent] ?? null;
const modelOptions = modelsByAgent[selectedAgent] ?? [];
const modelsLoading = modelsLoadingByAgent[selectedAgent] ?? false;
const modelsError = modelsErrorByAgent[selectedAgent] ?? null;
const defaultModel = defaultModelByAgent[selectedAgent] ?? "";
const selectedModelId = model || defaultModel;
const selectedModelObj = modelOptions.find((entry) => entry.id === selectedModelId);
const variantOptions = selectedModelObj?.variants ?? [];
const showModelSelect = modelsLoading || Boolean(modelsError) || modelOptions.length > 0;
const hasModelOptions = modelOptions.length > 0;
const modelCustom =
model && hasModelOptions && !modelOptions.some((entry) => entry.id === model);
const supportsVariants =
modelsLoading ||
Boolean(modelsError) ||
modelOptions.some((entry) => (entry.variants?.length ?? 0) > 0);
const showVariantSelect =
supportsVariants && (modelsLoading || Boolean(modelsError) || variantOptions.length > 0);
const hasVariantOptions = variantOptions.length > 0;
const variantCustom = variant && hasVariantOptions && !variantOptions.includes(variant);
const agentLabel = agentLabels[selectedAgent] ?? selectedAgent;
const activeModels = modelsByAgent[selectedAgent] ?? [];
return (
<div className="session-create-menu">
@ -415,330 +195,69 @@ const SessionCreateMenu = ({
<div className="session-create-form">
<div className="setup-field">
<span className="setup-label">Model</span>
{showModelSelect ? (
<select
className="setup-select"
value={model}
onChange={(e) => { setModel(e.target.value); setVariant(""); }}
title="Model"
disabled={modelsLoading || Boolean(modelsError)}
>
{modelsLoading ? (
<option value="">Loading models...</option>
) : modelsError ? (
<option value="">{modelsError}</option>
) : (
<>
<option value="">
{defaultModel ? `Default (${defaultModel})` : "Default"}
</option>
{modelCustom && <option value={model}>{model} (custom)</option>}
{modelOptions.map((entry) => (
<option key={entry.id} value={entry.id}>
{entry.name ?? entry.id}
</option>
))}
</>
)}
</select>
) : (
{isCustomModel ? (
<input
className="setup-input"
value={model}
onChange={(e) => setModel(e.target.value)}
placeholder="Model"
title="Model"
type="text"
value={customModel}
onChange={(e) => setCustomModel(e.target.value)}
placeholder="Enter model name..."
autoFocus
/>
)}
</div>
<div className="setup-field">
<span className="setup-label">Mode</span>
<select
className="setup-select"
value={agentMode}
onChange={(e) => setAgentMode(e.target.value)}
title="Mode"
disabled={modesLoading || Boolean(modesError)}
>
{modesLoading ? (
<option value="">Loading modes...</option>
) : modesError ? (
<option value="">{modesError}</option>
) : activeModes.length > 0 ? (
activeModes.map((m) => (
) : (
<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}
</option>
))
) : (
<option value="">Mode</option>
)}
</select>
))}
<option value={CUSTOM_MODEL_VALUE}>Custom...</option>
</select>
)}
{isCustomModel && (
<button
className="setup-custom-back"
onClick={() => {
setIsCustomModel(false);
setCustomModel("");
const defaultModel = defaultModelByAgent[selectedAgent];
setSelectedModel(
defaultModel || (activeModels.length > 0 ? activeModels[0].id : "")
);
}}
title="Back to model list"
type="button"
>
List
</button>
)}
</div>
<div className="setup-field">
<span className="setup-label">Permission</span>
<select
className="setup-select"
value={permissionMode}
onChange={(e) => setPermissionMode(e.target.value)}
title="Permission Mode"
>
<option value="default">Default</option>
<option value="plan">Plan</option>
<option value="bypass">Bypass</option>
</select>
</div>
{supportsVariants && (
{activeModes.length > 0 && (
<div className="setup-field">
<span className="setup-label">Variant</span>
{showVariantSelect ? (
<select
className="setup-select"
value={variant}
onChange={(e) => setVariant(e.target.value)}
title="Variant"
disabled={modelsLoading || Boolean(modelsError)}
>
{modelsLoading ? (
<option value="">Loading variants...</option>
) : modelsError ? (
<option value="">{modelsError}</option>
) : (
<>
<option value="">Default</option>
{variantCustom && <option value={variant}>{variant} (custom)</option>}
{variantOptions.map((entry) => (
<option key={entry} value={entry}>
{entry}
</option>
))}
</>
)}
</select>
) : (
<input
className="setup-input"
value={variant}
onChange={(e) => setVariant(e.target.value)}
placeholder="Variant"
title="Variant"
/>
)}
<span className="setup-label">Mode</span>
<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}
</option>
))}
</select>
</div>
)}
{/* MCP Servers - collapsible */}
<div className="session-create-section">
<button
type="button"
className="session-create-section-toggle"
onClick={() => setMcpExpanded(!mcpExpanded)}
>
<span className="setup-label">MCP</span>
<span className="session-create-section-count">{mcpServers.length} server{mcpServers.length !== 1 ? "s" : ""}</span>
{mcpExpanded ? <ChevronDown size={12} className="session-create-section-arrow" /> : <ChevronRight size={12} className="session-create-section-arrow" />}
</button>
{mcpExpanded && (
<div className="session-create-section-body">
{mcpServers.length > 0 && !isEditingMcp && (
<div className="session-create-mcp-list">
{mcpServers.map((entry, index) => (
<div key={entry.name} className="session-create-mcp-item">
<div className="session-create-mcp-info">
<span className="session-create-mcp-name">{entry.name}</span>
{getServerType(entry.configJson) && (
<span className="session-create-mcp-type">{getServerType(entry.configJson)}</span>
)}
<span className="session-create-mcp-summary mono">{getServerSummary(entry.configJson)}</span>
</div>
<div className="session-create-mcp-actions">
<button
type="button"
className="session-create-skill-remove"
onClick={() => startEditMcp(index)}
title="Edit server"
>
<Pencil size={10} />
</button>
<button
type="button"
className="session-create-skill-remove"
onClick={() => removeMcp(index)}
title="Remove server"
>
<X size={12} />
</button>
</div>
</div>
))}
</div>
)}
{isEditingMcp ? (
<div className="session-create-mcp-edit">
<input
ref={mcpNameRef}
className="session-create-mcp-name-input"
value={mcpName}
onChange={(e) => { setMcpName(e.target.value); setMcpLocalError(null); }}
placeholder="server-name"
disabled={editingMcpIndex !== null}
/>
<textarea
ref={mcpJsonRef}
className="session-create-textarea mono"
value={mcpJson}
onChange={(e) => { setMcpJson(e.target.value); setMcpLocalError(null); }}
placeholder='{"type":"local","command":"node","args":["./server.js"]}'
rows={4}
/>
{mcpLocalError && (
<div className="session-create-inline-error">{mcpLocalError}</div>
)}
<div className="session-create-mcp-edit-actions">
<button type="button" className="session-create-mcp-save" onClick={commitMcp}>
{editingMcpIndex !== null ? "Save" : "Add"}
</button>
<button type="button" className="session-create-mcp-cancel" onClick={cancelMcpEdit}>
Cancel
</button>
</div>
</div>
) : (
<button
type="button"
className="session-create-add-btn"
onClick={startAddMcp}
>
<Plus size={12} />
Add server
</button>
)}
{mcpConfigError && !isEditingMcp && (
<div className="session-create-inline-error">{mcpConfigError}</div>
)}
</div>
)}
</div>
{/* Skills - collapsible with source-based list */}
<div className="session-create-section">
<button
type="button"
className="session-create-section-toggle"
onClick={() => setSkillsExpanded(!skillsExpanded)}
>
<span className="setup-label">Skills</span>
<span className="session-create-section-count">{skillSources.length} source{skillSources.length !== 1 ? "s" : ""}</span>
{skillsExpanded ? <ChevronDown size={12} className="session-create-section-arrow" /> : <ChevronRight size={12} className="session-create-section-arrow" />}
</button>
{skillsExpanded && (
<div className="session-create-section-body">
{skillSources.length > 0 && !isEditingSkill && (
<div className="session-create-skill-list">
{skillSources.map((entry, index) => (
<div key={`${entry.type}-${entry.source}-${index}`} className="session-create-skill-item">
<span className="session-create-skill-type-badge">{entry.type}</span>
<span className="session-create-skill-path mono">{skillSourceSummary(entry)}</span>
<div className="session-create-mcp-actions">
<button
type="button"
className="session-create-skill-remove"
onClick={() => startEditSkill(index)}
title="Edit source"
>
<Pencil size={10} />
</button>
<button
type="button"
className="session-create-skill-remove"
onClick={() => removeSkill(index)}
title="Remove source"
>
<X size={12} />
</button>
</div>
</div>
))}
</div>
)}
{isEditingSkill ? (
<div className="session-create-mcp-edit">
<div className="session-create-skill-type-row">
<select
className="session-create-skill-type-select"
value={skillType}
onChange={(e) => { setSkillType(e.target.value as "github" | "local" | "git"); setSkillLocalError(null); }}
>
<option value="github">github</option>
<option value="local">local</option>
<option value="git">git</option>
</select>
<input
ref={skillSourceRef}
className="session-create-skill-input mono"
value={skillSource}
onChange={(e) => { setSkillSource(e.target.value); setSkillLocalError(null); }}
placeholder={skillType === "github" ? "owner/repo" : skillType === "local" ? "/path/to/skill" : "https://git.example.com/repo.git"}
/>
</div>
<input
className="session-create-skill-input mono"
value={skillFilter}
onChange={(e) => setSkillFilter(e.target.value)}
placeholder="Filter skills (comma-separated, optional)"
/>
{skillType !== "local" && (
<div className="session-create-skill-type-row">
<input
className="session-create-skill-input mono"
value={skillRef}
onChange={(e) => setSkillRef(e.target.value)}
placeholder="Branch/tag (optional)"
/>
<input
className="session-create-skill-input mono"
value={skillSubpath}
onChange={(e) => setSkillSubpath(e.target.value)}
placeholder="Subpath (optional)"
/>
</div>
)}
{skillLocalError && (
<div className="session-create-inline-error">{skillLocalError}</div>
)}
<div className="session-create-mcp-edit-actions">
<button type="button" className="session-create-mcp-save" onClick={commitSkill}>
{editingSkillIndex !== null ? "Save" : "Add"}
</button>
<button type="button" className="session-create-mcp-cancel" onClick={cancelSkillEdit}>
Cancel
</button>
</div>
</div>
) : (
<button
type="button"
className="session-create-add-btn"
onClick={startAddSkill}
>
<Plus size={12} />
Add source
</button>
)}
</div>
)}
</div>
</div>
<div className="session-create-actions">
<button
className="button primary"
onClick={handleCreate}
disabled={Boolean(mcpConfigError)}
>
<button className="button primary" onClick={handleCreate}>
Create Session
</button>
</div>

View file

@ -1,23 +1,22 @@
import { getAvatarLabel, getMessageClass } from "./messageUtils";
import renderContentPart from "./renderContentPart";
import type { TimelineEntry } from "./types";
import { formatJson } from "../../utils/format";
const ChatMessages = ({
entries,
sessionError,
eventError,
messagesEndRef
}: {
entries: TimelineEntry[];
sessionError: string | null;
eventError: string | null;
messagesEndRef: React.RefObject<HTMLDivElement>;
}) => {
return (
<div className="messages">
{entries.map((entry) => {
const messageClass = getMessageClass(entry);
if (entry.kind === "meta") {
const messageClass = entry.meta?.severity === "error" ? "error" : "system";
return (
<div key={entry.id} className={`message ${messageClass}`}>
<div className="avatar">{getAvatarLabel(messageClass)}</div>
@ -31,53 +30,73 @@ const ChatMessages = ({
);
}
const item = entry.item;
if (!item) return null;
const hasParts = (item.content ?? []).length > 0;
const isInProgress = item.status === "in_progress";
const isFailed = item.status === "failed";
const messageClass = getMessageClass(item);
const statusValue = item.status ?? "";
const statusLabel =
statusValue && statusValue !== "completed" ? statusValue.replace("_", " ") : "";
const kindLabel = item.kind.replace("_", " ");
return (
<div key={entry.id} className={`message ${messageClass} ${isFailed ? "error" : ""}`}>
<div className="avatar">{getAvatarLabel(isFailed ? "error" : messageClass)}</div>
<div className="message-content">
{(item.kind !== "message" || item.status !== "completed") && (
if (entry.kind === "reasoning") {
return (
<div key={entry.id} className="message assistant">
<div className="avatar">AI</div>
<div className="message-content">
<div className="message-meta">
<span>{kindLabel}</span>
{statusLabel && (
<span className={`pill ${item.status === "failed" ? "danger" : "accent"}`}>
{statusLabel}
<span>reasoning - {entry.reasoning?.visibility ?? "public"}</span>
</div>
<div className="part-body muted">{entry.reasoning?.text ?? ""}</div>
</div>
</div>
);
}
if (entry.kind === "tool") {
const isComplete = entry.toolStatus === "completed" || entry.toolStatus === "failed";
const isFailed = entry.toolStatus === "failed";
return (
<div key={entry.id} className={`message tool ${isFailed ? "error" : ""}`}>
<div className="avatar">{getAvatarLabel(isFailed ? "error" : "tool")}</div>
<div className="message-content">
<div className="message-meta">
<span>tool call - {entry.toolName}</span>
{entry.toolStatus && entry.toolStatus !== "completed" && (
<span className={`pill ${isFailed ? "danger" : "accent"}`}>
{entry.toolStatus.replace("_", " ")}
</span>
)}
</div>
)}
{hasParts ? (
(item.content ?? []).map(renderContentPart)
) : entry.deltaText ? (
<span>
{entry.deltaText}
{isInProgress && <span className="cursor" />}
</span>
) : isInProgress ? (
{entry.toolInput && <pre className="code-block">{entry.toolInput}</pre>}
{isComplete && entry.toolOutput && (
<div className="part">
<div className="part-title">result</div>
<pre className="code-block">{entry.toolOutput}</pre>
</div>
)}
{!isComplete && !entry.toolInput && (
<span className="thinking-indicator">
<span className="thinking-dot" />
<span className="thinking-dot" />
<span className="thinking-dot" />
</span>
)}
</div>
</div>
);
}
// Message (user or assistant)
return (
<div key={entry.id} className={`message ${messageClass}`}>
<div className="avatar">{getAvatarLabel(messageClass)}</div>
<div className="message-content">
{entry.text ? (
<div className="part-body">{entry.text}</div>
) : (
<span className="thinking-indicator">
<span className="thinking-dot" />
<span className="thinking-dot" />
<span className="thinking-dot" />
</span>
) : (
<span className="muted">No content yet.</span>
)}
</div>
</div>
);
})}
{sessionError && <div className="message-error">{sessionError}</div>}
{eventError && <div className="message-error">{eventError}</div>}
<div ref={messagesEndRef} />
</div>
);

View file

@ -1,15 +1,9 @@
import { MessageSquare, Plus, Square, Terminal } from "lucide-react";
import { CheckSquare, MessageSquare, Plus, Square, Terminal } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import type { McpServerEntry } from "../../App";
import type {
AgentInfo,
AgentModelInfo,
AgentModeInfo,
PermissionEventData,
QuestionEventData,
SkillSource
} from "../../types/legacyApi";
import ApprovalsTab from "../debug/ApprovalsTab";
import type { AgentInfo } from "sandbox-agent";
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";
@ -31,32 +25,11 @@ const ChatPanel = ({
messagesEndRef,
agentLabel,
currentAgentVersion,
sessionModel,
sessionVariant,
sessionPermissionMode,
sessionMcpServerCount,
sessionSkillSourceCount,
sessionEnded,
onEndSession,
eventError,
questionRequests,
permissionRequests,
questionSelections,
onSelectQuestionOption,
onAnswerQuestion,
onRejectQuestion,
onReplyPermission,
modesByAgent,
modelsByAgent,
defaultModelByAgent,
modesLoadingByAgent,
modelsLoadingByAgent,
modesErrorByAgent,
modelsErrorByAgent,
mcpServers,
onMcpServersChange,
mcpConfigError,
skillSources,
onSkillSourcesChange
}: {
sessionId: string;
transcriptEntries: TimelineEntry[];
@ -66,39 +39,18 @@ const ChatPanel = ({
onSendMessage: () => void;
onKeyDown: (event: React.KeyboardEvent<HTMLTextAreaElement>) => void;
onCreateSession: (agentId: string, config: SessionConfig) => void;
onSelectAgent: (agentId: string) => void;
onSelectAgent: (agentId: string) => Promise<void>;
agents: AgentInfo[];
agentsLoading: boolean;
agentsError: string | null;
messagesEndRef: React.RefObject<HTMLDivElement>;
agentLabel: string;
currentAgentVersion?: string | null;
sessionModel?: string | null;
sessionVariant?: string | null;
sessionPermissionMode?: string | null;
sessionMcpServerCount: number;
sessionSkillSourceCount: number;
sessionEnded: boolean;
onEndSession: () => void;
eventError: string | null;
questionRequests: QuestionEventData[];
permissionRequests: PermissionEventData[];
questionSelections: Record<string, string[][]>;
onSelectQuestionOption: (requestId: string, optionLabel: string) => void;
onAnswerQuestion: (request: QuestionEventData) => void;
onRejectQuestion: (requestId: string) => void;
onReplyPermission: (requestId: string, reply: "once" | "always" | "reject") => void;
modesByAgent: Record<string, AgentModeInfo[]>;
modelsByAgent: Record<string, AgentModelInfo[]>;
defaultModelByAgent: Record<string, string>;
modesLoadingByAgent: Record<string, boolean>;
modelsLoadingByAgent: Record<string, boolean>;
modesErrorByAgent: Record<string, string | null>;
modelsErrorByAgent: Record<string, string | null>;
mcpServers: McpServerEntry[];
onMcpServersChange: (servers: McpServerEntry[]) => void;
mcpConfigError: string | null;
skillSources: SkillSource[];
onSkillSourcesChange: (sources: SkillSource[]) => void;
}) => {
const [showAgentMenu, setShowAgentMenu] = useState(false);
const menuRef = useRef<HTMLDivElement | null>(null);
@ -115,8 +67,6 @@ const ChatPanel = ({
return () => document.removeEventListener("mousedown", handler);
}, [showAgentMenu]);
const hasApprovals = questionRequests.length > 0 || permissionRequests.length > 0;
return (
<div className="chat-panel">
<div className="panel-header">
@ -127,15 +77,22 @@ const ChatPanel = ({
</div>
<div className="panel-header-right">
{sessionId && (
<button
type="button"
className="button ghost small"
onClick={onEndSession}
title="End session"
>
<Square size={12} />
End
</button>
sessionEnded ? (
<span className="button ghost small" style={{ opacity: 0.5, cursor: "default" }} title="Session ended">
<CheckSquare size={12} />
Ended
</span>
) : (
<button
type="button"
className="button ghost small"
onClick={onEndSession}
title="End session"
>
<Square size={12} />
End
</button>
)
)}
</div>
</div>
@ -161,17 +118,8 @@ const ChatPanel = ({
modesByAgent={modesByAgent}
modelsByAgent={modelsByAgent}
defaultModelByAgent={defaultModelByAgent}
modesLoadingByAgent={modesLoadingByAgent}
modelsLoadingByAgent={modelsLoadingByAgent}
modesErrorByAgent={modesErrorByAgent}
modelsErrorByAgent={modelsErrorByAgent}
mcpServers={mcpServers}
onMcpServersChange={onMcpServersChange}
mcpConfigError={mcpConfigError}
skillSources={skillSources}
onSkillSourcesChange={onSkillSourcesChange}
onSelectAgent={onSelectAgent}
onCreateSession={onCreateSession}
onSelectAgent={onSelectAgent}
open={showAgentMenu}
onClose={() => setShowAgentMenu(false)}
/>
@ -187,27 +135,11 @@ const ChatPanel = ({
<ChatMessages
entries={transcriptEntries}
sessionError={sessionError}
eventError={eventError}
messagesEndRef={messagesEndRef}
/>
)}
</div>
{hasApprovals && (
<div className="approvals-inline">
<div className="approvals-inline-header">Approvals</div>
<ApprovalsTab
questionRequests={questionRequests}
permissionRequests={permissionRequests}
questionSelections={questionSelections}
onSelectQuestionOption={onSelectQuestionOption}
onAnswerQuestion={onAnswerQuestion}
onRejectQuestion={onRejectQuestion}
onReplyPermission={onReplyPermission}
/>
</div>
)}
<ChatInput
message={message}
onMessageChange={onMessageChange}
@ -223,26 +155,12 @@ const ChatPanel = ({
<span className="session-config-label">Agent</span>
<span className="session-config-value">{agentLabel}</span>
</div>
<div className="session-config-field">
<span className="session-config-label">Model</span>
<span className="session-config-value">{sessionModel || "-"}</span>
</div>
<div className="session-config-field">
<span className="session-config-label">Variant</span>
<span className="session-config-value">{sessionVariant || "-"}</span>
</div>
<div className="session-config-field">
<span className="session-config-label">Permission</span>
<span className="session-config-value">{sessionPermissionMode || "-"}</span>
</div>
<div className="session-config-field">
<span className="session-config-label">MCP Servers</span>
<span className="session-config-value">{sessionMcpServerCount}</span>
</div>
<div className="session-config-field">
<span className="session-config-label">Skills</span>
<span className="session-config-value">{sessionSkillSourceCount}</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>

View file

@ -1,11 +1,10 @@
import type { UniversalItem } from "../../types/legacyApi";
import type { TimelineEntry } from "./types";
export const getMessageClass = (item: UniversalItem) => {
if (item.kind === "tool_call" || item.kind === "tool_result") return "tool";
if (item.kind === "system" || item.kind === "status") return "system";
if (item.role === "user") return "user";
if (item.role === "tool") return "tool";
if (item.role === "system") return "system";
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";
};

View file

@ -1,93 +0,0 @@
import type { ContentPart } from "../../types/legacyApi";
import { formatJson } from "../../utils/format";
const renderContentPart = (part: ContentPart, index: number) => {
const partType = (part as { type?: string }).type ?? "unknown";
const key = `${partType}-${index}`;
switch (partType) {
case "text":
return (
<div key={key} className="part">
<div className="part-body">{(part as { text: string }).text}</div>
</div>
);
case "json":
return (
<div key={key} className="part">
<div className="part-title">json</div>
<pre className="code-block">{formatJson((part as { json: unknown }).json)}</pre>
</div>
);
case "tool_call": {
const { name, arguments: args, call_id } = part as {
name: string;
arguments: string;
call_id: string;
};
return (
<div key={key} className="part">
<div className="part-title">
tool call - {name}
{call_id ? ` - ${call_id}` : ""}
</div>
{args ? <pre className="code-block">{args}</pre> : <div className="muted">No arguments</div>}
</div>
);
}
case "tool_result": {
const { call_id, output } = part as { call_id: string; output: string };
return (
<div key={key} className="part">
<div className="part-title">tool result - {call_id}</div>
{output ? <pre className="code-block">{output}</pre> : <div className="muted">No output</div>}
</div>
);
}
case "file_ref": {
const { path, action, diff } = part as { path: string; action: string; diff?: string | null };
return (
<div key={key} className="part">
<div className="part-title">file - {action}</div>
<div className="part-body mono">{path}</div>
{diff && <pre className="code-block">{diff}</pre>}
</div>
);
}
case "reasoning": {
const { text, visibility } = part as { text: string; visibility: string };
return (
<div key={key} className="part">
<div className="part-title">reasoning - {visibility}</div>
<div className="part-body muted">{text}</div>
</div>
);
}
case "image": {
const { path, mime } = part as { path: string; mime?: string | null };
return (
<div key={key} className="part">
<div className="part-title">image {mime ? `- ${mime}` : ""}</div>
<div className="part-body mono">{path}</div>
</div>
);
}
case "status": {
const { label, detail } = part as { label: string; detail?: string | null };
return (
<div key={key} className="part">
<div className="part-title">status - {label}</div>
{detail && <div className="part-body">{detail}</div>}
</div>
);
}
default:
return (
<div key={key} className="part">
<div className="part-title">unknown</div>
<pre className="code-block">{formatJson(part)}</pre>
</div>
);
}
};
export default renderContentPart;

View file

@ -1,14 +1,17 @@
import type { UniversalItem } from "../../types/legacyApi";
export type TimelineEntry = {
id: string;
kind: "item" | "meta";
kind: "message" | "tool" | "meta" | "reasoning";
time: string;
item?: UniversalItem;
deltaText?: string;
meta?: {
title: string;
detail?: string;
severity?: "info" | "error";
};
// 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" };
};

View file

@ -1,6 +1,8 @@
import { Download, Loader2, RefreshCw } from "lucide-react";
import { useState } from "react";
import type { AgentInfo, AgentModeInfo } from "../../types/legacyApi";
import type { AgentInfo } from "sandbox-agent";
type AgentModeInfo = { id: string; name: string; description: string };
import FeatureCoverageBadges from "../agents/FeatureCoverageBadges";
import { emptyFeatureCoverage } from "../../types/agents";
@ -52,9 +54,9 @@ const AgentsTab = ({
id,
installed: false,
credentialsAvailable: false,
version: undefined,
path: undefined,
capabilities: emptyFeatureCoverage
version: undefined as string | undefined,
path: undefined as string | undefined,
capabilities: emptyFeatureCoverage as AgentInfo["capabilities"],
}))).map((agent) => {
const isInstalling = installingAgent === agent.id;
return (

View file

@ -1,105 +0,0 @@
import { HelpCircle, Shield } from "lucide-react";
import type { PermissionEventData, QuestionEventData } from "../../types/legacyApi";
import { formatJson } from "../../utils/format";
const ApprovalsTab = ({
questionRequests,
permissionRequests,
questionSelections,
onSelectQuestionOption,
onAnswerQuestion,
onRejectQuestion,
onReplyPermission
}: {
questionRequests: QuestionEventData[];
permissionRequests: PermissionEventData[];
questionSelections: Record<string, string[][]>;
onSelectQuestionOption: (requestId: string, optionLabel: string) => void;
onAnswerQuestion: (request: QuestionEventData) => void;
onRejectQuestion: (requestId: string) => void;
onReplyPermission: (requestId: string, reply: "once" | "always" | "reject") => void;
}) => {
return (
<>
{questionRequests.length === 0 && permissionRequests.length === 0 ? (
<div className="card-meta">No pending approvals.</div>
) : (
<>
{questionRequests.map((request) => {
const selections = questionSelections[request.question_id] ?? [];
const selected = selections[0] ?? [];
const answered = selected.length > 0;
return (
<div key={request.question_id} className="card">
<div className="card-header">
<span className="card-title">
<HelpCircle className="button-icon" style={{ marginRight: 6 }} />
Question
</span>
<span className="pill accent">Pending</span>
</div>
<div style={{ marginTop: 12 }}>
<div style={{ fontSize: 12, marginBottom: 8 }}>{request.prompt}</div>
<div className="option-list">
{request.options.map((option) => {
const isSelected = selected.includes(option);
return (
<label key={option} className="option-item">
<input
type="radio"
checked={isSelected}
onChange={() => onSelectQuestionOption(request.question_id, option)}
/>
<span>{option}</span>
</label>
);
})}
</div>
</div>
<div className="card-actions">
<button className="button success small" disabled={!answered} onClick={() => onAnswerQuestion(request)}>
Reply
</button>
<button className="button danger small" onClick={() => onRejectQuestion(request.question_id)}>
Reject
</button>
</div>
</div>
);
})}
{permissionRequests.map((request) => (
<div key={request.permission_id} className="card">
<div className="card-header">
<span className="card-title">
<Shield className="button-icon" style={{ marginRight: 6 }} />
Permission
</span>
<span className="pill accent">Pending</span>
</div>
<div className="card-meta" style={{ marginTop: 8 }}>
{request.action}
</div>
{request.metadata !== null && request.metadata !== undefined && (
<pre className="code-block">{formatJson(request.metadata)}</pre>
)}
<div className="card-actions">
<button className="button success small" onClick={() => onReplyPermission(request.permission_id, "once")}>
Allow Once
</button>
<button className="button secondary small" onClick={() => onReplyPermission(request.permission_id, "always")}>
Always
</button>
<button className="button danger small" onClick={() => onReplyPermission(request.permission_id, "reject")}>
Reject
</button>
</div>
</div>
))}
</>
)}
</>
);
};
export default ApprovalsTab;

View file

@ -1,19 +1,21 @@
import { Cloud, PlayCircle, Terminal } from "lucide-react";
import type { AgentInfo, AgentModeInfo, UniversalEvent } from "../../types/legacyApi";
import { Cloud, PlayCircle, Server, Terminal, Wrench } from "lucide-react";
import type { AgentInfo, SandboxAgent, SessionEvent } from "sandbox-agent";
type AgentModeInfo = { id: string; name: string; description: string };
import AgentsTab from "./AgentsTab";
import EventsTab from "./EventsTab";
import McpTab from "./McpTab";
import SkillsTab from "./SkillsTab";
import RequestLogTab from "./RequestLogTab";
import type { RequestLog } from "../../types/requestLog";
export type DebugTab = "log" | "events" | "agents";
export type DebugTab = "log" | "events" | "agents" | "mcp" | "skills";
const DebugPanel = ({
debugTab,
onDebugTabChange,
events,
offset,
onResetEvents,
eventsError,
requestLog,
copiedLogId,
onClearRequestLog,
@ -24,14 +26,13 @@ const DebugPanel = ({
onRefreshAgents,
onInstallAgent,
agentsLoading,
agentsError
agentsError,
getClient,
}: {
debugTab: DebugTab;
onDebugTabChange: (tab: DebugTab) => void;
events: UniversalEvent[];
offset: number;
events: SessionEvent[];
onResetEvents: () => void;
eventsError: string | null;
requestLog: RequestLog[];
copiedLogId: number | null;
onClearRequestLog: () => void;
@ -43,6 +44,7 @@ const DebugPanel = ({
onInstallAgent: (agentId: string, reinstall: boolean) => Promise<void>;
agentsLoading: boolean;
agentsError: string | null;
getClient: () => SandboxAgent;
}) => {
return (
<div className="debug-panel">
@ -60,6 +62,14 @@ const DebugPanel = ({
<Cloud className="button-icon" style={{ marginRight: 4, width: 12, height: 12 }} />
Agents
</button>
<button className={`debug-tab ${debugTab === "mcp" ? "active" : ""}`} onClick={() => onDebugTabChange("mcp")}>
<Server className="button-icon" style={{ marginRight: 4, width: 12, height: 12 }} />
MCP
</button>
<button className={`debug-tab ${debugTab === "skills" ? "active" : ""}`} onClick={() => onDebugTabChange("skills")}>
<Wrench className="button-icon" style={{ marginRight: 4, width: 12, height: 12 }} />
Skills
</button>
</div>
<div className="debug-content">
@ -75,9 +85,7 @@ const DebugPanel = ({
{debugTab === "events" && (
<EventsTab
events={events}
offset={offset}
onClear={onResetEvents}
error={eventsError}
/>
)}
@ -92,6 +100,14 @@ const DebugPanel = ({
error={agentsError}
/>
)}
{debugTab === "mcp" && (
<McpTab getClient={getClient} />
)}
{debugTab === "skills" && (
<SkillsTab getClient={getClient} />
)}
</div>
</div>
);

View file

@ -1,19 +1,119 @@
import { ChevronDown, ChevronRight } from "lucide-react";
import {
Ban,
Bot,
Brain,
ChevronDown,
ChevronRight,
Circle,
CircleX,
Command,
CornerDownLeft,
FilePen,
FileText,
FolderOpen,
Hourglass,
KeyRound,
ListChecks,
MessageSquare,
Plug,
Radio,
ScrollText,
Settings,
ShieldCheck,
SquarePlus,
SquareTerminal,
ToggleLeft,
Trash2,
Unplug,
Wrench,
type LucideIcon,
} from "lucide-react";
import { useEffect, useState } from "react";
import type { UniversalEvent } from "../../types/legacyApi";
import type { SessionEvent } from "sandbox-agent";
import { formatJson, formatTime } from "../../utils/format";
import { getEventCategory, getEventClass, getEventIcon, getEventKey, getEventType } from "./eventUtils";
type EventIconInfo = { Icon: LucideIcon; category: string };
function getEventIcon(method: string, payload: Record<string, unknown>): EventIconInfo {
if (method === "session/update") {
const params = payload.params as Record<string, unknown> | undefined;
const update = params?.update as Record<string, unknown> | undefined;
const updateType = update?.sessionUpdate as string | undefined;
switch (updateType) {
case "user_message_chunk":
return { Icon: MessageSquare, category: "prompt" };
case "agent_message_chunk":
return { Icon: Bot, category: "update" };
case "agent_thought_chunk":
return { Icon: Brain, category: "update" };
case "tool_call":
case "tool_call_update":
return { Icon: Wrench, category: "tool" };
case "plan":
return { Icon: ListChecks, category: "config" };
case "available_commands_update":
return { Icon: Command, category: "config" };
case "current_mode_update":
return { Icon: ToggleLeft, category: "config" };
case "config_option_update":
return { Icon: Settings, category: "config" };
default:
return { Icon: Radio, category: "update" };
}
}
switch (method) {
case "initialize":
return { Icon: Plug, category: "connection" };
case "authenticate":
return { Icon: KeyRound, category: "connection" };
case "session/new":
return { Icon: SquarePlus, category: "session" };
case "session/load":
return { Icon: FolderOpen, category: "session" };
case "session/prompt":
return { Icon: MessageSquare, category: "prompt" };
case "session/cancel":
return { Icon: Ban, category: "cancel" };
case "session/set_mode":
return { Icon: ToggleLeft, category: "config" };
case "session/set_config_option":
return { Icon: Settings, category: "config" };
case "session/request_permission":
return { Icon: ShieldCheck, category: "permission" };
case "fs/read_text_file":
return { Icon: FileText, category: "filesystem" };
case "fs/write_text_file":
return { Icon: FilePen, category: "filesystem" };
case "terminal/create":
return { Icon: SquareTerminal, category: "terminal" };
case "terminal/kill":
return { Icon: CircleX, category: "terminal" };
case "terminal/output":
return { Icon: ScrollText, category: "terminal" };
case "terminal/release":
return { Icon: Trash2, category: "terminal" };
case "terminal/wait_for_exit":
return { Icon: Hourglass, category: "terminal" };
case "_sandboxagent/session/detach":
return { Icon: Unplug, category: "session" };
case "(response)":
return { Icon: CornerDownLeft, category: "response" };
default:
if (method.startsWith("_sandboxagent/")) {
return { Icon: Radio, category: "connection" };
}
return { Icon: Circle, category: "response" };
}
}
const EventsTab = ({
events,
offset,
onClear,
error
}: {
events: UniversalEvent[];
offset: number;
events: SessionEvent[];
onClear: () => void;
error: string | null;
}) => {
const [collapsedEvents, setCollapsedEvents] = useState<Record<string, boolean>>({});
const [copied, setCopied] = useState(false);
@ -55,10 +155,15 @@ const EventsTab = ({
}
}, [events.length]);
const getMethod = (event: SessionEvent): string => {
const payload = event.payload as Record<string, unknown>;
return typeof payload.method === "string" ? payload.method : "(response)";
};
return (
<>
<div className="inline-row" style={{ marginBottom: 12, justifyContent: "space-between" }}>
<span className="card-meta">Offset: {offset}</span>
<span className="card-meta">{events.length} events</span>
<div className="inline-row">
<button
type="button"
@ -75,26 +180,26 @@ const EventsTab = ({
</div>
</div>
{error && <div className="banner error">{error}</div>}
{events.length === 0 ? (
<div className="card-meta">
No events yet. Start streaming to receive events.
No events yet. Create a session and send a message.
</div>
) : (
<div className="event-list">
{[...events].reverse().map((event) => {
const type = getEventType(event);
const category = getEventCategory(type);
const eventClass = `${category} ${getEventClass(type)}`;
const eventKey = getEventKey(event);
const eventKey = event.id;
const isCollapsed = collapsedEvents[eventKey] ?? true;
const toggleCollapsed = () =>
setCollapsedEvents((prev) => ({
...prev,
[eventKey]: !(prev[eventKey] ?? true)
}));
const Icon = getEventIcon(type);
const method = getMethod(event);
const payload = event.payload as Record<string, unknown>;
const { Icon, category } = getEventIcon(method, payload);
const time = formatTime(new Date(event.createdAt).toISOString());
const senderClass = event.sender === "client" ? "client" : "agent";
return (
<div key={eventKey} className={`event-item ${isCollapsed ? "collapsed" : "expanded"}`}>
<button
@ -103,24 +208,26 @@ const EventsTab = ({
onClick={toggleCollapsed}
title={isCollapsed ? "Expand payload" : "Collapse payload"}
>
<span className={`event-icon ${eventClass}`}>
<span className={`event-icon ${category}`}>
<Icon size={14} />
</span>
<div className="event-summary-main">
<div className="event-title-row">
<span className={`event-type ${eventClass}`}>{type}</span>
<span className="event-time">{formatTime(event.time)}</span>
<span className={`event-type ${category}`}>{method}</span>
<span className={`pill ${senderClass === "client" ? "accent" : "success"}`}>
{event.sender}
</span>
<span className="event-time">{time}</span>
</div>
<div className="event-id">
Event #{event.event_id || event.sequence} - seq {event.sequence} - {event.source}
{event.synthetic ? " (synthetic)" : ""}
{event.id}
</div>
</div>
<span className="event-chevron">
{isCollapsed ? <ChevronRight size={16} /> : <ChevronDown size={16} />}
</span>
</button>
{!isCollapsed && <pre className="code-block event-payload">{formatJson(event.data)}</pre>}
{!isCollapsed && <pre className="code-block event-payload">{formatJson(event.payload)}</pre>}
</div>
);
})}

View file

@ -0,0 +1,210 @@
import { FolderOpen, Loader2, Plus, Trash2 } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import type { SandboxAgent } from "sandbox-agent";
import { formatJson } from "../../utils/format";
type McpEntry = {
name: string;
config: Record<string, unknown>;
};
const McpTab = ({
getClient,
}: {
getClient: () => SandboxAgent;
}) => {
const [directory, setDirectory] = useState("/");
const [entries, setEntries] = useState<McpEntry[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// Add/edit form state
const [editing, setEditing] = useState(false);
const [editName, setEditName] = useState("");
const [editJson, setEditJson] = useState("");
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()) {
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([]);
} finally {
setLoading(false);
}
}, [getClient]);
useEffect(() => {
loadAll(directory);
}, [directory, loadAll]);
const startAdd = () => {
setEditing(true);
setEditName("");
setEditJson('{\n "type": "local",\n "command": "npx",\n "args": ["@modelcontextprotocol/server-everything"]\n}');
setEditError(null);
};
const cancelEdit = () => {
setEditing(false);
setEditName("");
setEditJson("");
setEditError(null);
};
const save = async () => {
const name = editName.trim();
if (!name) {
setEditError("Name is required");
return;
}
let parsed: Record<string, unknown>;
try {
parsed = JSON.parse(editJson.trim());
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
setEditError("Must be a JSON object");
return;
}
} catch {
setEditError("Invalid JSON");
return;
}
setSaving(true);
setEditError(null);
try {
await getClient().setMcpConfig(
{ directory, mcpName: name },
parsed as Parameters<SandboxAgent["setMcpConfig"]>[1],
);
cancelEdit();
await loadAll(directory);
} catch (err) {
setEditError(err instanceof Error ? err.message : "Failed to save");
} finally {
setSaving(false);
}
};
const remove = async (name: string) => {
try {
await getClient().deleteMcpConfig({ directory, mcpName: name });
await loadAll(directory);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to delete");
}
};
return (
<>
<div className="inline-row" style={{ marginBottom: 12, justifyContent: "space-between" }}>
<span className="card-meta">MCP Server Configuration</span>
<div className="inline-row">
{!editing && (
<button className="button secondary small" onClick={startAdd}>
<Plus className="button-icon" style={{ width: 12, height: 12 }} />
Add
</button>
)}
</div>
</div>
<div className="inline-row" style={{ marginBottom: 12, gap: 6 }}>
<FolderOpen size={14} className="muted" style={{ flexShrink: 0 }} />
<input
className="setup-input mono"
value={directory}
onChange={(e) => setDirectory(e.target.value)}
placeholder="/"
style={{ flex: 1, fontSize: 11 }}
/>
</div>
{error && <div className="banner error">{error}</div>}
{loading && <div className="card-meta">Loading...</div>}
{editing && (
<div className="card" style={{ marginBottom: 12 }}>
<div className="card-header">
<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); }}
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); }}
rows={6}
style={{ width: "100%", boxSizing: "border-box", fontFamily: "monospace", fontSize: 11 }}
/>
{editError && <div className="banner error" style={{ marginTop: 4 }}>{editError}</div>}
</div>
<div className="card-actions">
<button className="button primary small" onClick={save} disabled={saving}>
{saving ? <Loader2 className="button-icon spinner-icon" /> : null}
Save
</button>
<button className="button ghost small" onClick={cancelEdit}>
Cancel
</button>
</div>
</div>
)}
{entries.length === 0 && !editing && !loading && (
<div className="card-meta">
No MCP servers configured in this directory.
</div>
)}
{entries.map((entry) => (
<div key={entry.name} className="card" style={{ marginBottom: 8 }}>
<div className="card-header">
<span className="card-title">{entry.name}</span>
<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" }}
>
<Trash2 size={12} />
</button>
</div>
</div>
<pre className="code-block" style={{ marginTop: 4, fontSize: 10 }}>
{formatJson(entry.config)}
</pre>
</div>
))}
</>
);
};
export default McpTab;

View file

@ -44,21 +44,21 @@ const RequestLogTab = ({
type="button"
onClick={() => hasDetails && toggleExpanded(entry.id)}
title={hasDetails ? (isExpanded ? "Collapse" : "Expand") : undefined}
style={{ cursor: hasDetails ? "pointer" : "default" }}
style={{ cursor: hasDetails ? "pointer" : "default", gridTemplateColumns: "1fr auto auto auto" }}
>
<div className="event-summary-main" style={{ flex: 1 }}>
<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-status ${entry.status && entry.status < 400 ? "ok" : "error"}`}>
{entry.status || "ERR"}
</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="copy-button"
onClick={(e) => {

View file

@ -0,0 +1,263 @@
import { FolderOpen, Loader2, Plus, Trash2 } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import type { SandboxAgent } from "sandbox-agent";
import { formatJson } from "../../utils/format";
type SkillEntry = {
name: string;
config: { sources: Array<{ source: string; type: string; ref?: string | null; subpath?: string | null; skills?: string[] | null }> };
};
const SkillsTab = ({
getClient,
}: {
getClient: () => SandboxAgent;
}) => {
const [directory, setDirectory] = useState("/");
const [entries, setEntries] = useState<SkillEntry[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// Add form state
const [editing, setEditing] = useState(false);
const [editName, setEditName] = useState("");
const [editSource, setEditSource] = useState("");
const [editType, setEditType] = useState("github");
const [editRef, setEditRef] = useState("");
const [editSubpath, setEditSubpath] = useState("");
const [editSkills, setEditSkills] = useState("");
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()) {
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([]);
} finally {
setLoading(false);
}
}, [getClient]);
useEffect(() => {
loadAll(directory);
}, [directory, loadAll]);
const startAdd = () => {
setEditing(true);
setEditName("");
setEditSource("rivet-dev/skills");
setEditType("github");
setEditRef("");
setEditSubpath("");
setEditSkills("sandbox-agent");
setEditError(null);
};
const cancelEdit = () => {
setEditing(false);
setEditName("");
setEditSource("");
setEditType("github");
setEditRef("");
setEditSubpath("");
setEditSkills("");
setEditError(null);
};
const save = async () => {
const name = editName.trim();
if (!name) {
setEditError("Name is required");
return;
}
const source = editSource.trim();
if (!source) {
setEditError("Source is required");
return;
}
const skillEntry: SkillEntry["config"]["sources"][0] = {
source,
type: editType,
};
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)
: null;
if (skillsList && skillsList.length > 0) skillEntry.skills = skillsList;
const config = { sources: [skillEntry] };
setSaving(true);
setEditError(null);
try {
await getClient().setSkillsConfig(
{ directory, skillName: name },
config,
);
cancelEdit();
await loadAll(directory);
} catch (err) {
setEditError(err instanceof Error ? err.message : "Failed to save");
} finally {
setSaving(false);
}
};
const remove = async (name: string) => {
try {
await getClient().deleteSkillsConfig({ directory, skillName: name });
await loadAll(directory);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to delete");
}
};
return (
<>
<div className="inline-row" style={{ marginBottom: 12, justifyContent: "space-between" }}>
<span className="card-meta">Skills Configuration</span>
<div className="inline-row">
{!editing && (
<button className="button secondary small" onClick={startAdd}>
<Plus className="button-icon" style={{ width: 12, height: 12 }} />
Add
</button>
)}
</div>
</div>
<div className="inline-row" style={{ marginBottom: 12, gap: 6 }}>
<FolderOpen size={14} className="muted" style={{ flexShrink: 0 }} />
<input
className="setup-input mono"
value={directory}
onChange={(e) => setDirectory(e.target.value)}
placeholder="/"
style={{ flex: 1, fontSize: 11 }}
/>
</div>
{error && <div className="banner error">{error}</div>}
{loading && <div className="card-meta">Loading...</div>}
{editing && (
<div className="card" style={{ marginBottom: 12 }}>
<div className="card-header">
<span className="card-title">Add Skill Source</span>
</div>
<div style={{ marginTop: 8 }}>
<input
className="setup-input"
value={editName}
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 }}
>
<option value="github">github</option>
<option value="local">local</option>
<option value="git">git</option>
</select>
<input
className="setup-input mono"
value={editSource}
onChange={(e) => { setEditSource(e.target.value); setEditError(null); }}
placeholder={editType === "github" ? "owner/repo" : editType === "local" ? "/path/to/skill" : "https://..."}
style={{ flex: 1 }}
/>
</div>
<input
className="setup-input"
value={editSkills}
onChange={(e) => setEditSkills(e.target.value)}
placeholder="Skills filter (comma-separated, optional)"
style={{ marginBottom: 6, width: "100%", boxSizing: "border-box" }}
/>
{editType !== "local" && (
<div className="inline-row" style={{ gap: 4 }}>
<input
className="setup-input mono"
value={editRef}
onChange={(e) => setEditRef(e.target.value)}
placeholder="Branch/tag (optional)"
style={{ flex: 1 }}
/>
<input
className="setup-input mono"
value={editSubpath}
onChange={(e) => setEditSubpath(e.target.value)}
placeholder="Subpath (optional)"
style={{ flex: 1 }}
/>
</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}>
{saving ? <Loader2 className="button-icon spinner-icon" /> : null}
Save
</button>
<button className="button ghost small" onClick={cancelEdit}>
Cancel
</button>
</div>
</div>
)}
{entries.length === 0 && !editing && !loading && (
<div className="card-meta">
No skills configured in this directory.
</div>
)}
{entries.map((entry) => (
<div key={entry.name} className="card" style={{ marginBottom: 8 }}>
<div className="card-header">
<span className="card-title">{entry.name}</span>
<div className="card-header-pills">
<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" }}
>
<Trash2 size={12} />
</button>
</div>
</div>
<pre className="code-block" style={{ marginTop: 4, fontSize: 10 }}>
{formatJson(entry.config)}
</pre>
</div>
))}
</>
);
};
export default SkillsTab;

View file

@ -1,110 +0,0 @@
import {
Activity,
AlertTriangle,
Brain,
CheckCircle,
FileDiff,
HelpCircle,
Info,
MessageSquare,
PauseCircle,
PlayCircle,
Shield,
Terminal,
Wrench,
Zap
} from "lucide-react";
import type { UniversalEvent } from "../../types/legacyApi";
export const getEventType = (event: UniversalEvent) => event.type;
export const getEventKey = (event: UniversalEvent) =>
event.event_id ? `id:${event.event_id}` : `seq:${event.sequence}`;
export const getEventCategory = (type: string) => type.split(".")[0] ?? type;
export const getEventClass = (type: string) => type.replace(/\./g, "-");
export const getEventIcon = (type: string) => {
switch (type) {
// ACP session update events
case "acp.agent_message_chunk":
return MessageSquare;
case "acp.user_message_chunk":
return MessageSquare;
case "acp.agent_thought_chunk":
return Brain;
case "acp.tool_call":
return Wrench;
case "acp.tool_call_update":
return Activity;
case "acp.plan":
return FileDiff;
case "acp.session_info_update":
return Info;
case "acp.usage_update":
return Info;
case "acp.current_mode_update":
return Info;
case "acp.config_option_update":
return Info;
case "acp.available_commands_update":
return Terminal;
// Inspector lifecycle events
case "inspector.turn_started":
return PlayCircle;
case "inspector.turn_ended":
return PauseCircle;
case "inspector.user_message":
return MessageSquare;
// Session lifecycle (inspector-emitted)
case "session.started":
return PlayCircle;
case "session.ended":
return PauseCircle;
// Legacy synthetic events
case "turn.started":
return PlayCircle;
case "turn.ended":
return PauseCircle;
case "item.started":
return MessageSquare;
case "item.delta":
return Activity;
case "item.completed":
return CheckCircle;
// Approval events
case "question.requested":
return HelpCircle;
case "question.resolved":
return CheckCircle;
case "permission.requested":
return Shield;
case "permission.resolved":
return CheckCircle;
// Error events
case "error":
return AlertTriangle;
case "agent.unparsed":
return Brain;
default:
if (type.startsWith("acp.")) return Zap;
if (type.startsWith("inspector.")) return Info;
if (type.startsWith("item.")) return MessageSquare;
if (type.startsWith("session.")) return PlayCircle;
if (type.startsWith("error")) return AlertTriangle;
if (type.startsWith("agent.")) return Brain;
if (type.startsWith("question.")) return HelpCircle;
if (type.startsWith("permission.")) return Shield;
if (type.startsWith("file.")) return FileDiff;
if (type.startsWith("command.")) return Terminal;
if (type.startsWith("tool.")) return Wrench;
return Zap;
}
};

View file

@ -1,790 +0,0 @@
import {
SandboxAgent,
type PermissionOption,
type RequestPermissionRequest,
type RequestPermissionResponse,
type SandboxAgentAcpClient,
type SandboxAgentConnectOptions,
type SessionNotification,
} from "sandbox-agent";
import type {
AgentInfo,
AgentModelInfo,
AgentModeInfo,
AgentModelsResponse,
AgentModesResponse,
CreateSessionRequest,
EventsQuery,
EventsResponse,
MessageRequest,
PermissionEventData,
PermissionReplyRequest,
QuestionEventData,
QuestionReplyRequest,
SessionInfo,
SessionListResponse,
TurnStreamQuery,
UniversalEvent,
} from "../types/legacyApi";
type PendingPermission = {
request: RequestPermissionRequest;
resolve: (response: RequestPermissionResponse) => void;
autoEndTurnOnResolve?: boolean;
};
type PendingQuestion = {
prompt: string;
options: string[];
autoEndTurnOnResolve?: boolean;
};
type RuntimeSession = {
aliasSessionId: string;
realSessionId: string;
agent: string;
connection: SandboxAgentAcpClient;
events: UniversalEvent[];
nextSequence: number;
listeners: Set<(event: UniversalEvent) => void>;
info: SessionInfo;
pendingPermissions: Map<string, PendingPermission>;
pendingQuestions: Map<string, PendingQuestion>;
};
const TDOO_PERMISSION_MODE =
"TDOO: ACP permission mode preconfiguration is not implemented in inspector compatibility.";
const TDOO_VARIANT =
"TDOO: ACP session variants are not implemented in inspector compatibility.";
const TDOO_SKILLS =
"TDOO: ACP skills source configuration is not implemented in inspector compatibility.";
const TDOO_MODE_DISCOVERY =
"TDOO: ACP mode discovery before session creation is not implemented; returning cached/empty modes.";
const TDOO_MODEL_DISCOVERY =
"TDOO: ACP model discovery before session creation is not implemented; returning cached/empty models.";
export class InspectorLegacyClient {
private readonly base: SandboxAgent;
private readonly sessions = new Map<string, RuntimeSession>();
private readonly aliasByRealSessionId = new Map<string, string>();
private readonly modeCache = new Map<string, AgentModeInfo[]>();
private readonly modelCache = new Map<string, AgentModelsResponse>();
private permissionCounter = 0;
private constructor(base: SandboxAgent) {
this.base = base;
}
static async connect(options: SandboxAgentConnectOptions): Promise<InspectorLegacyClient> {
const base = await SandboxAgent.connect(options);
return new InspectorLegacyClient(base);
}
async getHealth() {
return this.base.getHealth();
}
async listAgents(): Promise<{ agents: AgentInfo[] }> {
const response = await this.base.listAgents();
return {
agents: response.agents.map((agent) => {
const installed =
agent.agent_process_installed &&
(!agent.native_required || agent.native_installed);
return {
id: agent.id,
installed,
credentialsAvailable: true,
version: agent.agent_process_version ?? agent.native_version ?? null,
path: null,
capabilities: {
unstable_methods: agent.capabilities.unstable_methods,
},
native_required: agent.native_required,
native_installed: agent.native_installed,
native_version: agent.native_version,
agent_process_installed: agent.agent_process_installed,
agent_process_source: agent.agent_process_source,
agent_process_version: agent.agent_process_version,
};
}),
};
}
async installAgent(agent: string, request: { reinstall?: boolean } = {}) {
return this.base.installAgent(agent, request);
}
async getAgentModes(agentId: string): Promise<AgentModesResponse> {
const modes = this.modeCache.get(agentId);
if (modes) {
return { modes };
}
console.warn(TDOO_MODE_DISCOVERY);
return { modes: [] };
}
async getAgentModels(agentId: string): Promise<AgentModelsResponse> {
const models = this.modelCache.get(agentId);
if (models) {
return models;
}
console.warn(TDOO_MODEL_DISCOVERY);
return { models: [], defaultModel: null };
}
async createSession(aliasSessionId: string, request: CreateSessionRequest): Promise<void> {
await this.terminateSession(aliasSessionId).catch(() => {
// Ignore if it doesn't exist yet.
});
const acp = await this.base.createAcpClient({
agent: request.agent,
client: {
sessionUpdate: async (notification) => {
this.handleSessionUpdate(notification);
},
requestPermission: async (permissionRequest) => {
return this.handlePermissionRequest(permissionRequest);
},
},
});
await acp.initialize();
const created = await acp.newSession({
cwd: "/",
mcpServers: convertMcpConfig(request.mcp ?? {}),
});
if (created.modes?.availableModes) {
this.modeCache.set(
request.agent,
created.modes.availableModes.map((mode) => ({
id: mode.id,
name: mode.name,
description: mode.description ?? undefined,
})),
);
}
if (created.models?.availableModels) {
this.modelCache.set(request.agent, {
models: created.models.availableModels.map((model) => ({
id: model.modelId,
name: model.name,
description: model.description ?? undefined,
})),
defaultModel: created.models.currentModelId ?? null,
});
}
const runtime: RuntimeSession = {
aliasSessionId,
realSessionId: created.sessionId,
agent: request.agent,
connection: acp,
events: [],
nextSequence: 1,
listeners: new Set(),
info: {
sessionId: aliasSessionId,
agent: request.agent,
eventCount: 0,
ended: false,
model: request.model ?? null,
variant: request.variant ?? null,
permissionMode: request.permissionMode ?? null,
mcp: request.mcp,
skills: request.skills,
},
pendingPermissions: new Map(),
pendingQuestions: new Map(),
};
this.sessions.set(aliasSessionId, runtime);
this.aliasByRealSessionId.set(created.sessionId, aliasSessionId);
if (request.agentMode) {
try {
await acp.setSessionMode({ sessionId: created.sessionId, modeId: request.agentMode });
} catch {
this.emitError(aliasSessionId, `TDOO: Unable to apply mode \"${request.agentMode}\" via ACP.`);
}
}
if (request.model) {
try {
await acp.unstableSetSessionModel({
sessionId: created.sessionId,
modelId: request.model,
});
} catch {
this.emitError(aliasSessionId, `TDOO: Unable to apply model \"${request.model}\" via ACP.`);
}
}
if (request.permissionMode) {
this.emitError(aliasSessionId, TDOO_PERMISSION_MODE);
}
if (request.variant) {
this.emitError(aliasSessionId, TDOO_VARIANT);
}
if (request.skills?.sources && request.skills.sources.length > 0) {
this.emitError(aliasSessionId, TDOO_SKILLS);
}
this.emitEvent(aliasSessionId, "session.started", {
session_id: aliasSessionId,
agent: request.agent,
});
}
async listSessions(): Promise<SessionListResponse> {
const sessions = Array.from(this.sessions.values()).map((session) => {
return {
...session.info,
eventCount: session.events.length,
};
});
return { sessions };
}
async postMessage(sessionId: string, request: MessageRequest): Promise<void> {
const runtime = this.requireActiveSession(sessionId);
const message = request.message.trim();
if (!message) {
return;
}
this.emitEvent(sessionId, "inspector.turn_started", {
session_id: sessionId,
});
this.emitEvent(sessionId, "inspector.user_message", {
session_id: sessionId,
text: message,
});
try {
await runtime.connection.prompt({
sessionId: runtime.realSessionId,
prompt: [{ type: "text", text: message }],
});
} catch (error) {
const detail = error instanceof Error ? error.message : "prompt failed";
this.emitError(sessionId, detail);
throw error;
} finally {
this.emitEvent(sessionId, "inspector.turn_ended", {
session_id: sessionId,
});
}
}
async getEvents(sessionId: string, query: EventsQuery = {}): Promise<EventsResponse> {
const runtime = this.requireSession(sessionId);
const offset = query.offset ?? 0;
const limit = query.limit ?? 200;
const events = runtime.events.filter((event) => event.sequence > offset).slice(0, limit);
return { events };
}
async *streamEvents(
sessionId: string,
query: EventsQuery = {},
signal?: AbortSignal,
): AsyncIterable<UniversalEvent> {
const runtime = this.requireSession(sessionId);
let cursor = query.offset ?? 0;
for (const event of runtime.events) {
if (event.sequence <= cursor) {
continue;
}
cursor = event.sequence;
yield event;
}
const queue: UniversalEvent[] = [];
let wake: (() => void) | null = null;
const listener = (event: UniversalEvent) => {
if (event.sequence <= cursor) {
return;
}
queue.push(event);
if (wake) {
wake();
wake = null;
}
};
runtime.listeners.add(listener);
try {
while (!signal?.aborted) {
if (queue.length === 0) {
await waitForSignalOrEvent(signal, () => {
wake = () => {};
return new Promise<void>((resolve) => {
wake = resolve;
});
});
continue;
}
const next = queue.shift();
if (!next) {
continue;
}
cursor = next.sequence;
yield next;
}
} finally {
runtime.listeners.delete(listener);
}
}
async *streamTurn(
sessionId: string,
request: MessageRequest,
_query?: TurnStreamQuery,
signal?: AbortSignal,
): AsyncIterable<UniversalEvent> {
if (signal?.aborted) {
return;
}
const runtime = this.requireActiveSession(sessionId);
let cursor = runtime.nextSequence - 1;
const queue: UniversalEvent[] = [];
let wake: (() => void) | null = null;
let promptDone = false;
let promptError: unknown = null;
const notify = () => {
if (wake) {
wake();
wake = null;
}
};
const listener = (event: UniversalEvent) => {
if (event.sequence <= cursor) {
return;
}
queue.push(event);
notify();
};
runtime.listeners.add(listener);
const promptPromise = this.postMessage(sessionId, request)
.catch((error) => {
promptError = error;
})
.finally(() => {
promptDone = true;
notify();
});
try {
while (!signal?.aborted) {
if (queue.length === 0) {
if (promptDone) {
break;
}
await waitForSignalOrEvent(signal, () => {
wake = () => {};
return new Promise<void>((resolve) => {
wake = resolve;
});
});
continue;
}
const next = queue.shift();
if (!next) {
continue;
}
cursor = next.sequence;
yield next;
}
} finally {
runtime.listeners.delete(listener);
}
await promptPromise;
if (promptError) {
throw promptError;
}
}
async replyQuestion(
sessionId: string,
questionId: string,
request: QuestionReplyRequest,
): Promise<void> {
const runtime = this.requireSession(sessionId);
const pending = runtime.pendingQuestions.get(questionId);
if (!pending) {
throw new Error("TDOO: Question request no longer pending.");
}
runtime.pendingQuestions.delete(questionId);
const response = request.answers?.[0]?.[0] ?? null;
const resolved: QuestionEventData & { response?: string | null } = {
question_id: questionId,
status: "resolved",
prompt: pending.prompt,
options: pending.options,
response,
};
this.emitEvent(sessionId, "question.resolved", resolved);
if (pending.autoEndTurnOnResolve) {
this.emitEvent(sessionId, "turn.ended", { session_id: sessionId });
}
}
async rejectQuestion(sessionId: string, questionId: string): Promise<void> {
const runtime = this.requireSession(sessionId);
const pending = runtime.pendingQuestions.get(questionId);
if (!pending) {
throw new Error("TDOO: Question request no longer pending.");
}
runtime.pendingQuestions.delete(questionId);
const resolved: QuestionEventData & { response?: string | null } = {
question_id: questionId,
status: "resolved",
prompt: pending.prompt,
options: pending.options,
response: null,
};
this.emitEvent(sessionId, "question.resolved", resolved);
if (pending.autoEndTurnOnResolve) {
this.emitEvent(sessionId, "turn.ended", { session_id: sessionId });
}
}
async replyPermission(
sessionId: string,
permissionId: string,
request: PermissionReplyRequest,
): Promise<void> {
const runtime = this.requireSession(sessionId);
const pending = runtime.pendingPermissions.get(permissionId);
if (!pending) {
throw new Error("TDOO: Permission request no longer pending.");
}
const optionId = selectPermissionOption(pending.request.options, request.reply);
const response: RequestPermissionResponse = optionId
? {
outcome: {
outcome: "selected",
optionId,
},
}
: {
outcome: {
outcome: "cancelled",
},
};
pending.resolve(response);
runtime.pendingPermissions.delete(permissionId);
const action = pending.request.toolCall.title ?? pending.request.toolCall.kind ?? "permission";
const resolved: PermissionEventData = {
permission_id: permissionId,
status: "resolved",
action,
metadata: {
reply: request.reply,
},
};
this.emitEvent(sessionId, "permission.resolved", resolved);
if (pending.autoEndTurnOnResolve) {
this.emitEvent(sessionId, "turn.ended", { session_id: sessionId });
}
}
async terminateSession(sessionId: string): Promise<void> {
const runtime = this.sessions.get(sessionId);
if (!runtime) {
return;
}
this.emitEvent(sessionId, "session.ended", {
reason: "terminated_by_user",
terminated_by: "inspector",
});
runtime.info.ended = true;
for (const pending of runtime.pendingPermissions.values()) {
pending.resolve({
outcome: {
outcome: "cancelled",
},
});
}
runtime.pendingPermissions.clear();
runtime.pendingQuestions.clear();
try {
await runtime.connection.close();
} catch {
// Best-effort close.
}
this.aliasByRealSessionId.delete(runtime.realSessionId);
}
async dispose(): Promise<void> {
for (const sessionId of Array.from(this.sessions.keys())) {
await this.terminateSession(sessionId);
}
await this.base.dispose();
}
private handleSessionUpdate(notification: SessionNotification): void {
const aliasSessionId = this.aliasByRealSessionId.get(notification.sessionId);
if (!aliasSessionId) {
return;
}
const runtime = this.sessions.get(aliasSessionId);
if (!runtime || runtime.info.ended) {
return;
}
const update = notification.update;
// Still handle session_info_update for sidebar metadata
if (update.sessionUpdate === "session_info_update") {
runtime.info.title = update.title ?? runtime.info.title;
runtime.info.updatedAt = update.updatedAt ?? runtime.info.updatedAt;
}
// Emit the raw notification as the event data, using the ACP discriminator as the type
this.emitEvent(aliasSessionId, `acp.${update.sessionUpdate}`, notification);
}
private async handlePermissionRequest(
request: RequestPermissionRequest,
): Promise<RequestPermissionResponse> {
const aliasSessionId = this.aliasByRealSessionId.get(request.sessionId);
if (!aliasSessionId) {
return {
outcome: {
outcome: "cancelled",
},
};
}
const runtime = this.sessions.get(aliasSessionId);
if (!runtime || runtime.info.ended) {
return {
outcome: {
outcome: "cancelled",
},
};
}
this.permissionCounter += 1;
const permissionId = `permission-${this.permissionCounter}`;
const action = request.toolCall.title ?? request.toolCall.kind ?? "permission";
const pendingEvent: PermissionEventData = {
permission_id: permissionId,
status: "requested",
action,
metadata: request,
};
this.emitEvent(aliasSessionId, "permission.requested", pendingEvent);
return await new Promise<RequestPermissionResponse>((resolve) => {
runtime.pendingPermissions.set(permissionId, { request, resolve });
});
}
private emitError(sessionId: string, message: string): void {
this.emitEvent(sessionId, "error", {
message,
});
}
private emitEvent(sessionId: string, type: string, data: unknown): void {
const runtime = this.sessions.get(sessionId);
if (!runtime) {
return;
}
const event: UniversalEvent = {
event_id: `${sessionId}-${runtime.nextSequence}`,
sequence: runtime.nextSequence,
type,
source: "inspector.acp",
time: new Date().toISOString(),
synthetic: true,
data,
};
runtime.nextSequence += 1;
runtime.events.push(event);
runtime.info.eventCount = runtime.events.length;
for (const listener of runtime.listeners) {
listener(event);
}
}
private requireSession(sessionId: string): RuntimeSession {
const runtime = this.sessions.get(sessionId);
if (!runtime) {
throw new Error(`Session not found: ${sessionId}`);
}
return runtime;
}
private requireActiveSession(sessionId: string): RuntimeSession {
const runtime = this.requireSession(sessionId);
if (runtime.info.ended) {
throw new Error(`Session ended: ${sessionId}`);
}
return runtime;
}
}
const convertMcpConfig = (mcp: Record<string, unknown>) => {
return Object.entries(mcp)
.map(([name, config]) => {
if (!config || typeof config !== "object") {
return null;
}
const value = config as Record<string, unknown>;
const type = value.type;
if (type === "local") {
const commandValue = value.command;
const argsValue = value.args;
let command = "";
let args: string[] = [];
if (Array.isArray(commandValue) && commandValue.length > 0) {
command = String(commandValue[0] ?? "");
args = commandValue.slice(1).map((part) => String(part));
} else if (typeof commandValue === "string") {
command = commandValue;
}
if (Array.isArray(argsValue)) {
args = argsValue.map((part) => String(part));
}
const envObject =
value.env && typeof value.env === "object" ? (value.env as Record<string, unknown>) : {};
const env = Object.entries(envObject).map(([envName, envValue]) => ({
name: envName,
value: String(envValue),
}));
return {
name,
command,
args,
env,
};
}
if (type === "remote") {
const headersObject =
value.headers && typeof value.headers === "object"
? (value.headers as Record<string, unknown>)
: {};
const headers = Object.entries(headersObject).map(([headerName, headerValue]) => ({
name: headerName,
value: String(headerValue),
}));
return {
type: "http" as const,
name,
url: String(value.url ?? ""),
headers,
};
}
return null;
})
.filter((entry): entry is NonNullable<typeof entry> => entry !== null);
};
const selectPermissionOption = (
options: PermissionOption[],
reply: PermissionReplyRequest["reply"],
): string | null => {
const pick = (...kinds: PermissionOption["kind"][]) => {
return options.find((option) => kinds.includes(option.kind))?.optionId ?? null;
};
if (reply === "always") {
return pick("allow_always", "allow_once");
}
if (reply === "once") {
return pick("allow_once", "allow_always");
}
return pick("reject_once", "reject_always");
};
const waitForSignalOrEvent = async (
signal: AbortSignal | undefined,
createWaitPromise: () => Promise<void>,
) => {
if (signal?.aborted) {
return;
}
await new Promise<void>((resolve) => {
let done = false;
const finish = () => {
if (done) {
return;
}
done = true;
if (signal) {
signal.removeEventListener("abort", onAbort);
}
resolve();
};
const onAbort = () => finish();
if (signal) {
signal.addEventListener("abort", onAbort, { once: true });
}
createWaitPromise().then(finish).catch(finish);
});
};

View file

@ -1,145 +0,0 @@
export type SkillSourceType = "github" | "local" | "git";
export type SkillSource = {
type: SkillSourceType;
source: string;
skills?: string[];
ref?: string;
subpath?: string;
};
export type CreateSessionRequest = {
agent: string;
agentMode?: string;
permissionMode?: string;
model?: string;
variant?: string;
mcp?: Record<string, unknown>;
skills?: {
sources: SkillSource[];
};
};
export type AgentModeInfo = {
id: string;
name?: string;
description?: string;
};
export type AgentModelInfo = {
id: string;
name?: string;
description?: string;
variants?: string[];
};
export type AgentInfo = {
id: string;
installed: boolean;
credentialsAvailable: boolean;
version?: string | null;
path?: string | null;
capabilities: Record<string, boolean | undefined>;
native_required?: boolean;
native_installed?: boolean;
native_version?: string | null;
agent_process_installed?: boolean;
agent_process_source?: string | null;
agent_process_version?: string | null;
};
export type ContentPart = {
type?: string;
[key: string]: unknown;
};
export type UniversalItem = {
item_id: string;
native_item_id?: string | null;
parent_id?: string | null;
kind: string;
role?: string | null;
content?: ContentPart[];
status?: string | null;
[key: string]: unknown;
};
export type UniversalEvent = {
event_id: string;
sequence: number;
type: string;
source: string;
time: string;
synthetic?: boolean;
data: unknown;
[key: string]: unknown;
};
export type PermissionEventData = {
permission_id: string;
status: "requested" | "resolved";
action: string;
metadata?: unknown;
};
export type QuestionEventData = {
question_id: string;
status: "requested" | "resolved";
prompt: string;
options: string[];
};
export type SessionInfo = {
sessionId: string;
agent: string;
eventCount: number;
ended?: boolean;
model?: string | null;
variant?: string | null;
permissionMode?: string | null;
mcp?: Record<string, unknown>;
skills?: {
sources?: SkillSource[];
};
title?: string | null;
updatedAt?: string | null;
};
export type EventsQuery = {
offset?: number;
limit?: number;
includeRaw?: boolean;
};
export type EventsResponse = {
events: UniversalEvent[];
};
export type SessionListResponse = {
sessions: SessionInfo[];
};
export type AgentModesResponse = {
modes: AgentModeInfo[];
};
export type AgentModelsResponse = {
models: AgentModelInfo[];
defaultModel?: string | null;
};
export type MessageRequest = {
message: string;
};
export type TurnStreamQuery = {
includeRaw?: boolean;
};
export type PermissionReplyRequest = {
reply: "once" | "always" | "reject";
};
export type QuestionReplyRequest = {
answers: string[][];
};

View file

@ -7,7 +7,7 @@ export default defineConfig(({ command }) => ({
server: {
port: 5173,
proxy: {
"/v2": {
"/v1": {
target: "http://localhost:2468",
changeOrigin: true,
},

View file

@ -2,7 +2,7 @@
<path fill="currentColor" d="m126.103 130-15.679 15.678v-6.703h-6.335v15.31h6.335v-8.607l4.48 4.48 15.678-15.679-4.479-4.479Zm-4.592 13.726h21.117v6.335h-21.117v-6.335ZM96.7 165.899h9.244L94 153.955l4.48-4.48 12.041 12.041-4.383 4.383h9.04v6.335H96.701v-6.335Zm16.967 17.172 12.597-12.597v9.679h6.335v-19.006h-6.335v9.136l-4.384-4.384-12.692 12.692 4.479 4.48Z"/>
<path fill="currentColor" d="m126.263 154.541 14.185 14.185 4.48-4.479-14.185-14.186-4.48 4.48Z"/>
<path fill="currentColor" fill-rule="evenodd" d="M168.882 176.693h9.245c5.23.08 9.161-.212 11.956-1.414 2.834-1.243 4.831-3.308 5.988-6.194 1.198-2.887 1.797-7.056 1.797-12.508 0-5.052-.499-9.243-1.497-12.089-.998-2.847-2.695-4.952-5.091-6.315-2.355-1.403-5.709-2.325-10.061-2.766-1.996-.2-3.932-.3-5.809-.3a152.116 152.116 0 0 0-4.093-.01c-.784.005-1.598.009-2.435.01v41.586Zm18.266-7.187c-1.437.922-3.433 1.463-5.989 1.623-.758.08-1.577.1-2.455.06-.838-.04-1.897-.1-3.174-.18v-29.887c1.677 0 3.513.1 5.51.3 2.675.321 4.731.963 6.168 1.925 1.437.962 2.455 2.445 3.054 4.45.599 1.964.899 4.73.899 8.298 0 3.769-.32 6.716-.959 8.84-.598 2.085-1.617 3.608-3.054 4.571Zm16.454 6.675c1.518.561 3.654.841 6.408.841 2.436 0 4.292-.3 5.57-.902 1.278-.641 2.455-1.804 3.533-3.487h.479v4.091h5.869v-21.29c0-2.927-.379-5.172-1.137-6.735-.759-1.604-2.057-2.726-3.893-3.368-1.797-.681-4.352-1.022-7.666-1.022-3.274 0-5.809.301-7.606.902-1.756.561-2.994 1.543-3.713 2.947-.718 1.363-1.2 3.332-1.2 5.893h6.65c0-1.684.399-2.826 1.198-3.428.838-.601 2.395-.902 4.671-.902 1.717 0 3.055.181 4.013.541.958.361 1.637.942 2.036 1.744s.599 1.945.599 3.428v.842h-5.091c-3.872 0-6.867.341-8.983 1.022-2.116.642-3.613 1.784-4.492 3.428-.878 1.603-1.317 3.869-1.317 6.795 0 2.486.299 4.41.898 5.773.639 1.363 1.697 2.325 3.174 2.887Zm12.517-5.593c-1.118.601-2.735.902-4.851.902-2.076 0-3.493-.321-4.252-.962-.759-.642-1.138-1.824-1.138-3.548 0-1.403.22-2.466.659-3.187.439-.762 1.158-1.303 2.156-1.624 1.038-.321 2.495-.501 4.372-.541l6.288-.06c-.08 2.606-.359 4.61-.839 6.013-.479 1.403-1.277 2.406-2.395 3.007Zm71.836 6.434c-3.753 0-6.647-.481-8.684-1.443-2.036-.962-3.473-2.606-4.311-4.931-.839-2.325-1.258-5.632-1.258-9.922 0-4.33.419-7.657 1.258-9.983.838-2.325 2.275-3.968 4.311-4.931 2.037-1.002 4.931-1.503 8.684-1.503 3.753 0 6.628.501 8.624 1.503 2.036.963 3.474 2.606 4.312 4.931.878 2.326 1.318 5.653 1.318 9.983 0 4.29-.44 7.597-1.318 9.922-.838 2.285-2.276 3.929-4.312 4.931-2.036.962-4.911 1.443-8.624 1.443Zm0-5.532c2.076 0 3.653-.321 4.731-.962 1.118-.642 1.917-1.724 2.396-3.247.479-1.524.719-3.709.719-6.555 0-2.887-.24-5.092-.719-6.615-.479-1.564-1.278-2.666-2.396-3.307-1.078-.682-2.655-1.023-4.731-1.023s-3.673.341-4.791 1.023c-1.118.641-1.916 1.743-2.395 3.307-.479 1.523-.719 3.728-.719 6.615 0 2.846.24 5.031.719 6.555.479 1.523 1.277 2.605 2.395 3.247 1.118.641 2.715.962 4.791.962Z" clip-rule="evenodd"/>
<path fill="currentColor" d="M304.407 144.606h6.408v4.935h.479c.998-2.045 2.116-3.428 3.353-4.15 1.278-.721 3.115-1.082 5.51-1.082 2.875 0 5.091.381 6.648 1.143 1.557.721 2.675 1.944 3.353 3.668.679 1.724 1.019 4.189 1.019 7.396v20.208h-6.408v-20.328c0-1.644-.18-2.926-.539-3.849-.36-.922-.979-1.563-1.857-1.924s-2.136-.541-3.773-.541c-2.036 0-3.613.301-4.731.902-1.078.601-1.857 1.624-2.336 3.067-.479 1.443-.718 3.488-.718 6.134v16.539h-6.408v-32.118Z"/>
<path fill="currentColor" fill-rule="evenodd" d="M344.149 177.022c-2.755 0-4.891-.28-6.408-.841-1.477-.562-2.535-1.524-3.174-2.887-.599-1.363-.899-3.287-.899-5.773 0-2.926.44-5.192 1.318-6.795.878-1.644 2.376-2.786 4.492-3.428 2.116-.681 5.11-1.022 8.983-1.022h5.09v-.842c0-1.483-.199-2.626-.599-3.428-.399-.802-1.078-1.383-2.036-1.744-.958-.36-2.295-.541-4.012-.541-2.276 0-3.833.301-4.672.902-.798.602-1.197 1.744-1.197 3.428h-6.517c0-2.804.348-4.53 1.067-5.893.719-1.404 1.956-2.386 3.713-2.947 1.797-.601 4.332-.902 7.606-.902 3.314 0 5.869.341 7.665 1.022 1.837.642 3.135 1.764 3.893 3.368.759 1.563 1.138 3.808 1.138 6.735v21.29h-5.869v-4.091h-.479c-1.078 1.683-2.256 2.846-3.534 3.487-1.277.602-3.134.902-5.569.902Zm1.258-5.532c2.116 0 3.733-.301 4.85-.902 1.118-.601 1.917-1.604 2.396-3.007.479-1.403.759-3.407.838-6.013l-6.288.06c-1.876.04-3.334.22-4.372.541-.998.321-1.716.862-2.156 1.624-.439.721-.658 1.784-.658 3.187 0 1.724.379 2.906 1.137 3.548.759.641 2.176.962 4.253.962Z" clip-rule="evenodd"/>
<path fill="currentColor" d="M304.407 144.606h6.408v4.935h.479c.998-2.045 2.116-3.428 3.353-4.15 1.278-.721 3.115-1.082 5.51-1.082 2.875 0 5.091.381 6.648 1.143 1.557.721 2.675 1.944 3.353 3.668.679 1.724 1.019 4.189 1.019 7.396v 20.208h-6.408v-20.328c0-1.644-.18-2.926-.539-3.849-.36-.922-.979-1.563-1.857-1.924s-2.136-.541-3.773-.541c-2.036 0-3.613.301-4.731.902-1.078.601-1.857 1.624-2.336 3.067-.479 1.443-.718 3.488-.718 6.134v16.539h-6.408v-32.118Z"/>
<path fill="currentColor" fill-rule="evenodd" d="M344.149 177.022c-2.755 0-4.891-.28-6.408-.841-1.477-.562-2.535-1.524-3.174-2.887-.599-1.363-.899-3.287-.899-5.773 0-2.926.44-5.192 1.318-6.795.878-1.644 2.376-2.786 4.492-3.428 2.116-.681 5.11-1.022 8.983-1.022h5.09v-.842c0-1.483-.199-2.626-.599-3.428-.399-.802-1.078-1.383-2.036-1.744-.958-.36-2.295-.541-4.012-.541-2.276 0-3.833.301-4.672.902-.798.602-1.197 1.744-1.197 3.428h-6.517c0-2.804.348-4.53 1.067-5.893.719-1.404 1.956-2.386 3.713-2.947 1.797-.601 4.332-.902 7.606-.902 3.314 0 5.869.341 7.665 1.022 1.837.642 3.135 1.764 3.893 3.368.759 1.563 1.138 3.808 1.138 6.735v 21.29h-5.869v-4.091h-.479c-1.078 1.683-2.256 2.846-3.534 3.487-1.277.602-3.134.902-5.569.902Zm1.258-5.532c2.116 0 3.733-.301 4.85-.902 1.118-.601 1.917-1.604 2.396-3.007.479-1.403.759-3.407.838-6.013l-6.288.06c-1.876.04-3.334.22-4.372.541-.998.321-1.716.862-2.156 1.624-.439.721-.658 1.784-.658 3.187 0 1.724.379 2.906 1.137 3.548.759.641 2.176.962 4.253.962Z" clip-rule="evenodd"/>
<path fill="currentColor" d="M253.349 144.54h-6.169l-6.058 26.414h-2.205l-5.816-26.414h-6.058l5.147 26.192a7.27 7.27 0 0 0 7.134 5.869h1.168l-1.309 4.111c-.065.168-.125.336-.185.503-.526 1.46-1.018 2.825-3.867 2.825h-3.969v5.211l7.355.076c2.218-.038 5.005-1.652 7.077-8.615l7.755-36.172Zm15.285 32.264c-2.741 0-4.818-.363-6.229-1.089-1.37-.725-2.298-1.814-2.781-3.265-.484-1.492-.726-3.528-.726-6.108v-16.449h-3.744l1.09-5.322h2.896v-6.773h5.866v6.773h7.31v5.322h-7.31V166.1c0 1.33.121 2.358.363 3.084.282.686.826 1.21 1.632 1.573.807.362 2.016.544 3.629.544h1.686v5.503h-3.682Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Before After
Before After

View file

@ -1,6 +1,6 @@
<svg width="676" height="232" viewBox="0 0 676 232" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M545.35 202V30H608.499C627.501 30 642.162 34.0952 652.482 42.2857C662.802 50.3124 667.962 61.3695 667.962 75.4571C667.962 85.7771 665.259 94.0495 659.853 100.274C654.611 106.335 647.485 110.594 638.476 113.051C645.356 114.198 651.499 116.328 656.905 119.44C662.31 122.552 666.569 126.893 669.682 132.463C672.958 137.869 674.596 144.667 674.596 152.857C674.596 168.091 669.108 180.131 658.133 188.977C647.322 197.659 631.514 202 610.71 202H545.35ZM573.607 178.166H610.956C622.259 178.166 630.859 175.954 636.756 171.531C642.653 166.945 645.602 160.638 645.602 152.611C645.602 144.257 642.571 137.787 636.51 133.2C630.449 128.613 621.931 126.32 610.956 126.32H573.607V178.166ZM573.607 102.977H609.236C619.064 102.977 626.6 100.766 631.842 96.3429C637.084 91.92 639.705 85.8591 639.705 78.16C639.705 70.461 637.084 64.4819 631.842 60.2229C626.6 55.8 619.064 53.5886 609.236 53.5886H573.607V102.977Z" fill="currentColor"/>
<path d="M405.588 202V179.851L472.534 122.86C477.843 118.381 482.571 113.984 486.719 109.67C490.867 105.357 494.102 100.877 496.425 96.2316C498.748 91.4202 499.909 86.277 499.909 80.8019C499.909 70.8472 496.84 63.2153 490.701 57.9061C484.728 52.597 476.764 49.9424 466.81 49.9424C456.855 49.9424 448.808 53.0947 442.67 59.3993C436.531 65.704 433.461 74.2484 433.461 85.0326V88.019H405.837V83.5394C405.837 72.4234 408.326 62.6346 413.303 54.1731C418.281 45.5457 425.332 38.7434 434.457 33.766C443.748 28.7887 454.532 26.3 466.81 26.3C479.917 26.3 490.95 28.5398 499.909 33.0195C509.034 37.4991 515.919 43.7207 520.565 51.6845C525.376 59.6482 527.782 68.9392 527.782 79.5576C527.782 88.1849 526.206 95.8998 523.054 102.702C519.901 109.339 515.505 115.643 509.864 121.616C504.389 127.423 498.084 133.313 490.95 139.286L444.66 177.86H529.026V202H405.588Z" fill="currentColor"/>
<path d="M274 202V30H386.292V55.0629H302.257V102.731H371.549V127.057H302.257V176.937H389.24V202H274Z" fill="currentColor"/>
<path d="M405.588 202V179.851L472.534 122.86C477.843 118.381 482.571 113.984 486.719 109.67C490.867 105.357 494.102 100.877 496.425 96.2316C498.748 91.4202 499.909 86.277 499.909 80.8019C499.909 70.8472 496.84 63.2153 490.701 57.9061C484.728 52.597 476.764 49.9424 466.81 49.9424C456.855 49.9424 448.808 53.0947 442.67 59.3993C436.531 65.704 433.461 74.2484 433.461 85.0326V88.019H405.837V83.5394C405.837 72.4234 408.326 62.6346 413.303 54.1731C418.281 45.5457 425.332 38.7434 434.457 33.766C443.748 28.7887 454.532 26.3 466.81 26.3C479.917 26.3 490.95 28.5398 499.909 33.0195C509.034 37.4991 515.919 43.7207 520.565 51.6845C525.376 59.6482 527.782 68.9392 527.782 79.5576C527.782 88.1849 526.206 95.8998 523.054 102.702C519.901 109.339 515.505 115.643 509.864 121.616C504.389 127.423 498.084 133.313 490.95 139.286L444.66 177.86H529.026V 202H405.588Z" fill="currentColor"/>
<path d="M274 202V30H386.292V55.0629H302.257V102.731H371.549V127.057H302.257V176.937H389.24V 202H274Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M188.212 157.998C186.672 157.998 185.71 159.665 186.48 160.998L202.585 188.894C203.476 190.437 202.056 192.287 200.335 191.826L151.491 178.737C149.357 178.165 147.163 179.432 146.592 181.566L133.504 230.411C133.042 232.132 130.731 232.436 129.84 230.893L113.732 202.992C112.962 201.659 111.037 201.659 110.268 202.992L94.1595 230.893C93.2686 232.436 90.9568 232.132 90.4956 230.411L77.4075 181.566C76.8357 179.432 74.6423 178.165 72.5085 178.737L23.664 191.826C21.9429 192.287 20.5234 190.437 21.4143 188.894L37.5192 160.998C38.289 159.665 37.3267 157.998 35.7871 157.998L3.57893 157.998C1.79713 157.998 0.904821 155.844 2.16476 154.584L37.9218 118.827C39.484 117.265 39.484 114.733 37.9218 113.171L2.16478 77.4133C0.904844 76.1533 1.7972 73.999 3.57902 73.9991L35.7837 73.9995C37.3233 73.9995 38.2856 72.3328 37.5158 70.9995L21.4143 43.11C20.5234 41.5669 21.9429 39.717 23.664 40.1781L72.5085 53.2665C74.6423 53.8383 76.8357 52.572 77.4075 50.4381L90.4956 1.59292C90.9568 -0.128187 93.2686 -0.432531 94.1595 1.11058L110.267 29.0111C111.037 30.3445 112.962 30.3445 113.732 29.0111L129.84 1.11058C130.73 -0.432532 133.042 -0.128189 133.503 1.59292L146.592 50.4381C147.163 52.572 149.357 53.8383 151.491 53.2665L200.335 40.1781C202.056 39.717 203.476 41.5669 202.585 43.11L186.483 70.9995C185.713 72.3328 186.676 73.9995 188.215 73.9995L220.421 73.9991C222.203 73.999 223.095 76.1533 221.835 77.4133L186.078 113.171C184.516 114.733 184.516 117.265 186.078 118.827L221.835 154.584C223.095 155.844 222.203 157.998 220.421 157.998L188.212 157.998ZM175.919 81.3306C177.366 79.8837 175.963 77.4549 173.987 77.9845L130.491 89.6396C128.357 90.2114 126.164 88.9451 125.592 86.8112L113.931 43.293C113.402 41.3166 110.597 41.3166 110.068 43.293L98.4069 86.8112C97.8351 88.9451 95.6418 90.2114 93.5079 89.6396L50.0136 77.9849C48.0371 77.4553 46.6348 79.8841 48.0817 81.331L79.9216 113.171C81.4837 114.733 81.4837 117.266 79.9216 118.828L48.0742 150.675C46.6273 152.122 48.0296 154.55 50.0061 154.021L93.5079 142.364C95.6418 141.792 97.8351 143.059 98.4069 145.192L110.068 188.711C110.597 190.687 113.402 190.687 113.931 188.711L125.592 145.192C126.164 143.059 128.357 141.792 130.491 142.364L173.994 154.021C175.971 154.551 177.373 152.122 175.926 150.675L144.079 118.828C142.516 117.266 142.516 114.733 144.079 113.171L175.919 81.3306Z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Before After
Before After

View file

@ -1,2 +1,2 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#10A37F" width="800px" height="800px" viewBox="0 0 24 24" role="img" xmlns="http://www.w3.org/2000/svg"><title>OpenAI icon</title><path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z"/></svg>
<svg fill="#10A37F" width="800px" height="800px" viewBox="0 0 24 24" role="img" xmlns="http://www.w3.org/2000/svg"><title>OpenAI icon</title><path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v 2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v 2.9994l-2.5974 1.4997-2.6067-1.4997Z"/></svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Before After
Before After

View file

@ -1,9 +1,9 @@
<svg width="204" height="68" viewBox="0 0 204 68" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="3" y="3" width="62" height="62" rx="17.55" stroke="white" stroke-width="6"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M28.9979 19.7671C28.9979 19.3436 28.6541 19 28.2308 19H22.4809C20.5597 19 19 20.5597 19 22.4807V45.5125C19 47.4336 20.5597 48.9935 22.4809 48.9935H28.2308C28.6541 48.9935 28.9979 48.6496 28.9979 48.2263V19.7671ZM45.6293 38.7439C44.7861 37.231 42.8732 36.0028 41.3604 36.0028H32.5577C32.0922 36.0028 31.9249 36.3807 32.1843 36.8462L37.4298 46.2586C38.273 47.7717 40.1858 49 41.6987 49H50.5012C50.9667 49 51.1342 48.6221 50.8745 48.1563L45.6293 38.7439ZM45.9963 25.9983C45.9963 22.1359 42.8604 19 38.9977 19H32.8227C32.3682 19 31.9994 19.3688 31.9994 19.8233V32.1734C31.9994 32.6278 32.3682 32.9969 32.8227 32.9969H38.9977C42.8604 32.9969 45.9963 29.861 45.9963 25.9983Z" fill="white"/>
<path d="M197.177 18.564C197.485 18.564 197.691 18.7698 197.691 19.0784V25.8685C197.691 26.1257 197.845 26.28 198.103 26.28H202.372C202.681 26.28 202.886 26.4858 202.886 26.7944V31.2697C202.886 31.5783 202.681 31.7841 202.372 31.7841H198.103C197.845 31.7841 197.691 31.9384 197.691 32.1956V51.4856C197.691 51.7942 197.485 52 197.177 52H191.415C191.107 52 190.901 51.7942 190.901 51.4856V32.1956C190.901 31.9384 190.747 31.7841 190.489 31.7841H185.808C185.5 31.7841 185.294 31.5783 185.294 31.2697V26.7944C185.294 26.4858 185.5 26.28 185.808 26.28H190.489C190.747 26.28 190.901 26.1257 190.901 25.8685V19.0784C190.901 18.7698 191.107 18.564 191.415 18.564H197.177Z" fill="white"/>
<path d="M197.177 18.564C197.485 18.564 197.691 18.7698 197.691 19.0784V 25.8685C197.691 26.1257 197.845 26.28 198.103 26.28H202.372C202.681 26.28 202.886 26.4858 202.886 26.7944V31.2697C202.886 31.5783 202.681 31.7841 202.372 31.7841H198.103C197.845 31.7841 197.691 31.9384 197.691 32.1956V51.4856C197.691 51.7942 197.485 52 197.177 52H191.415C191.107 52 190.901 51.7942 190.901 51.4856V32.1956C190.901 31.9384 190.747 31.7841 190.489 31.7841H185.808C185.5 31.7841 185.294 31.5783 185.294 31.2697V 26.7944C185.294 26.4858 185.5 26.28 185.808 26.28H190.489C190.747 26.28 190.901 26.1257 190.901 25.8685V19.0784C190.901 18.7698 191.107 18.564 191.415 18.564H197.177Z" fill="white"/>
<path d="M172.602 52.6173C165.143 52.6173 159.021 46.4959 159.021 38.8828C159.021 31.3211 164.628 25.457 172.036 25.457C178.311 25.457 183.558 30.0866 184.021 35.6421C184.073 35.8993 183.918 36.105 183.713 36.2594L168.281 45.2614C168.023 45.4157 167.972 45.6729 168.229 45.8786C169.567 47.0103 171.213 47.2675 172.602 47.2675C175.791 47.2675 177.386 45.7758 178.723 43.5638C178.877 43.3066 179.083 43.2038 179.34 43.2552L183.713 44.0268C184.021 44.0782 184.227 44.2326 184.176 44.4383C183.25 48.1934 179.186 52.6173 172.602 52.6173ZM165.503 40.1174L165.606 40.426C165.709 40.7346 165.966 40.7861 166.172 40.6318L176.717 34.1503C176.923 33.996 177.025 33.8417 176.871 33.5845C175.945 32.0413 174.042 31.0639 171.676 31.3211C168.229 31.7326 164.114 35.8993 165.503 40.1174Z" fill="white"/>
<path d="M132.755 26.8973C132.601 26.5372 132.806 26.28 133.166 26.28H139.288C139.545 26.28 139.751 26.3829 139.854 26.6915L145.718 42.2264C145.821 42.5865 146.129 42.5865 146.232 42.2264L152.148 26.6915C152.251 26.3829 152.456 26.28 152.714 26.28H158.835C159.195 26.28 159.401 26.5372 159.247 26.8973L149.113 51.5885C149.01 51.8971 148.804 52 148.547 52H143.403C143.146 52 142.94 51.8971 142.837 51.5885L132.755 26.8973Z" fill="white"/>
<path d="M123.506 52C123.198 52 122.992 51.7942 122.992 51.4856V26.7944C122.992 26.4858 123.198 26.28 123.506 26.28H129.267C129.576 26.28 129.782 26.4858 129.782 26.7944V51.4856C129.782 51.7942 129.576 52 129.267 52H123.506ZM122.375 19.7986C122.375 17.5352 124.175 15.7348 126.387 15.7348C128.65 15.7348 130.399 17.5352 130.399 19.7986C130.399 22.0105 128.599 23.8109 126.387 23.8109C124.175 23.8109 122.375 22.0105 122.375 19.7986Z" fill="white"/>
<path d="M123.506 52C123.198 52 122.992 51.7942 122.992 51.4856V 26.7944C122.992 26.4858 123.198 26.28 123.506 26.28H129.267C129.576 26.28 129.782 26.4858 129.782 26.7944V51.4856C129.782 51.7942 129.576 52 129.267 52H123.506ZM122.375 19.7986C122.375 17.5352 124.175 15.7348 126.387 15.7348C128.65 15.7348 130.399 17.5352 130.399 19.7986C130.399 22.0105 128.599 23.8109 126.387 23.8109C124.175 23.8109 122.375 22.0105 122.375 19.7986Z" fill="white"/>
<path d="M105.23 15.992C112.895 15.992 118.296 20.8274 118.296 28.4405C118.296 33.1215 116.393 36.568 112.74 38.5742C112.483 38.7285 112.483 38.9342 112.74 39.14C116.855 41.9692 118.604 47.2675 118.759 51.4856C118.759 51.7942 118.553 52 118.244 52H112.997C112.689 52 112.483 51.8457 112.483 51.4856C112.277 47.6276 109.602 41.3519 102.864 40.426C102.349 40.426 101.784 40.3746 101.218 40.3746C100.961 40.3231 100.806 40.4774 100.806 40.7346V51.4856C100.806 51.7942 100.6 52 100.292 52H94.5305C94.2219 52 94.0161 51.7942 94.0161 51.4856V16.5064C94.0161 16.1978 94.2219 15.992 94.5305 15.992H105.23ZM100.806 33.996C100.806 34.2532 100.961 34.4075 101.218 34.4075H105.076C114.283 34.4075 114.283 22.2162 105.539 22.2162H101.218C100.961 22.2162 100.806 22.3706 100.806 22.6278V33.996Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 4 KiB

After

Width:  |  Height:  |  Size: 4 KiB

Before After
Before After

View file

@ -18,7 +18,7 @@ const faqs = [
{
question: 'How is session data persisted?',
answer:
"This SDK does not handle persisting session data. In v2, traffic is ACP JSON-RPC over <code>/v2/rpc</code>; persist envelopes in your own storage if you need replay or auditing.",
"This SDK does not handle persisting session data. Events stream in a universal JSON schema that you can persist anywhere. Consider using Postgres or <a href='https://rivet.gg' target='_blank' rel='noopener noreferrer' class='text-orange-400 hover:underline'>Rivet Actors</a> for data persistence.",
},
{
question: 'Can I run this locally or does it require a sandbox provider?',

View file

@ -86,7 +86,7 @@ function SdkCodeHighlighted() {
);
}
const sandboxCommand = `curl -fsSL https://releases.rivet.dev/sandbox-agent/latest/install.sh | sh`;
const sandboxCommand = `curl -fsSL https://releases.rivet.dev/sandbox-agent/0.2.x/install.sh | sh`;
const sourceCommands = `git clone https://github.com/rivet-dev/sandbox-agent
cd sandbox-agent
@ -172,7 +172,7 @@ export function GetStarted() {
<span className="text-zinc-300">curl -fsSL \</span>
{"\n"}
<span className="text-zinc-300">{" "}</span>
<span className="text-green-400">https://releases.rivet.dev/sandbox-agent/latest/install.sh</span>
<span className="text-green-400">https://releases.rivet.dev/sandbox-agent/0.2.x/install.sh</span>
<span className="text-zinc-300"> | </span>
<span className="text-blue-400">sh</span>
</code>

View file

@ -106,7 +106,7 @@ function UniversalAPIDiagram() {
<g opacity={isActive ? 1 : 0.4}>
{p.logo === 'openai' ? (
<svg x="43" y="10" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z" fill="#ffffff" />
<path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v 2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v 2.9994l-2.5974 1.4997-2.6067-1.4997Z" fill="#ffffff" />
</svg>
) : p.logo === 'opencode' ? (
<svg x="43" y="10" width="19" height="24" viewBox="0 0 32 40" fill="none">