mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-17 05:00:20 +00:00
Merge branch 'main' into feat/support-pi
This commit is contained in:
commit
4c6c5983c0
156 changed files with 16196 additions and 2338 deletions
750
frontend/packages/inspector/src/components/SessionCreateMenu.tsx
Normal file
750
frontend/packages/inspector/src/components/SessionCreateMenu.tsx
Normal file
|
|
@ -0,0 +1,750 @@
|
|||
import { ArrowLeft, ArrowRight, ChevronDown, ChevronRight, Pencil, Plus, X } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import type { AgentInfo, AgentModelInfo, AgentModeInfo, SkillSource } from "sandbox-agent";
|
||||
import type { McpServerEntry } from "../App";
|
||||
|
||||
export type SessionConfig = {
|
||||
model: string;
|
||||
agentMode: string;
|
||||
permissionMode: string;
|
||||
variant: string;
|
||||
};
|
||||
|
||||
const agentLabels: Record<string, string> = {
|
||||
claude: "Claude Code",
|
||||
codex: "Codex",
|
||||
opencode: "OpenCode",
|
||||
amp: "Amp",
|
||||
mock: "Mock"
|
||||
};
|
||||
|
||||
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,
|
||||
agentsError,
|
||||
modesByAgent,
|
||||
modelsByAgent,
|
||||
defaultModelByAgent,
|
||||
modesLoadingByAgent,
|
||||
modelsLoadingByAgent,
|
||||
modesErrorByAgent,
|
||||
modelsErrorByAgent,
|
||||
mcpServers,
|
||||
onMcpServersChange,
|
||||
mcpConfigError,
|
||||
skillSources,
|
||||
onSkillSourcesChange,
|
||||
onSelectAgent,
|
||||
onCreateSession,
|
||||
open,
|
||||
onClose
|
||||
}: {
|
||||
agents: AgentInfo[];
|
||||
agentsLoading: boolean;
|
||||
agentsError: string | null;
|
||||
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;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const [phase, setPhase] = useState<"agent" | "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);
|
||||
};
|
||||
|
||||
// Reset state when menu closes
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setPhase("agent");
|
||||
setSelectedAgent("");
|
||||
setAgentMode("");
|
||||
setPermissionMode("default");
|
||||
setModel("");
|
||||
setVariant("");
|
||||
setMcpExpanded(false);
|
||||
setSkillsExpanded(false);
|
||||
cancelSkillEdit();
|
||||
setAddingMcp(false);
|
||||
setEditingMcpIndex(null);
|
||||
setMcpName("");
|
||||
setMcpJson("");
|
||||
setMcpLocalError(null);
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
// Auto-select first mode when modes load for selected agent
|
||||
useEffect(() => {
|
||||
if (!selectedAgent) return;
|
||||
const modes = modesByAgent[selectedAgent];
|
||||
if (modes && modes.length > 0 && !agentMode) {
|
||||
setAgentMode(modes[0].id);
|
||||
}
|
||||
}, [modesByAgent, selectedAgent, agentMode]);
|
||||
|
||||
// Focus skill source input when adding
|
||||
useEffect(() => {
|
||||
if ((addingSkill || editingSkillIndex !== null) && skillSourceRef.current) {
|
||||
skillSourceRef.current.focus();
|
||||
}
|
||||
}, [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]);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
const handleAgentClick = (agentId: string) => {
|
||||
setSelectedAgent(agentId);
|
||||
setPhase("config");
|
||||
onSelectAgent(agentId);
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
setPhase("agent");
|
||||
setSelectedAgent("");
|
||||
setAgentMode("");
|
||||
setPermissionMode("default");
|
||||
setModel("");
|
||||
setVariant("");
|
||||
};
|
||||
|
||||
const handleCreate = () => {
|
||||
if (mcpConfigError) return;
|
||||
onCreateSession(selectedAgent, { model, agentMode, permissionMode, variant });
|
||||
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">
|
||||
{agentsLoading && <div className="sidebar-add-status">Loading agents...</div>}
|
||||
{agentsError && <div className="sidebar-add-status error">{agentsError}</div>}
|
||||
{!agentsLoading && !agentsError && agents.length === 0 && (
|
||||
<div className="sidebar-add-status">No agents available.</div>
|
||||
)}
|
||||
{!agentsLoading && !agentsError &&
|
||||
agents.map((agent) => (
|
||||
<button
|
||||
key={agent.id}
|
||||
className="sidebar-add-option"
|
||||
onClick={() => handleAgentClick(agent.id)}
|
||||
>
|
||||
<div className="agent-option-left">
|
||||
<span className="agent-option-name">{agentLabels[agent.id] ?? agent.id}</span>
|
||||
{agent.version && <span className="agent-option-version">{agent.version}</span>}
|
||||
</div>
|
||||
<div className="agent-option-badges">
|
||||
{agent.installed && <span className="agent-badge installed">Installed</span>}
|
||||
<ArrowRight size={12} className="agent-option-arrow" />
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</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;
|
||||
|
||||
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="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>
|
||||
) : (
|
||||
<input
|
||||
className="setup-input"
|
||||
value={model}
|
||||
onChange={(e) => setModel(e.target.value)}
|
||||
placeholder="Model"
|
||||
title="Model"
|
||||
/>
|
||||
)}
|
||||
</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) => (
|
||||
<option key={m.id} value={m.id}>
|
||||
{m.name || m.id}
|
||||
</option>
|
||||
))
|
||||
) : (
|
||||
<option value="">Mode</option>
|
||||
)}
|
||||
</select>
|
||||
</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 && (
|
||||
<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"
|
||||
/>
|
||||
)}
|
||||
</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)}
|
||||
>
|
||||
Create Session
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SessionCreateMenu;
|
||||
|
|
@ -1,6 +1,17 @@
|
|||
import { Plus, RefreshCw } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import type { AgentInfo, SessionInfo } from "sandbox-agent";
|
||||
import type { AgentInfo, AgentModelInfo, AgentModeInfo, SessionInfo, SkillSource } from "sandbox-agent";
|
||||
import type { McpServerEntry } from "../App";
|
||||
import SessionCreateMenu, { type SessionConfig } from "./SessionCreateMenu";
|
||||
|
||||
const agentLabels: Record<string, string> = {
|
||||
claude: "Claude Code",
|
||||
codex: "Codex",
|
||||
opencode: "OpenCode",
|
||||
amp: "Amp",
|
||||
pi: "Pi",
|
||||
mock: "Mock"
|
||||
};
|
||||
|
||||
const SessionSidebar = ({
|
||||
sessions,
|
||||
|
|
@ -8,22 +19,48 @@ const SessionSidebar = ({
|
|||
onSelectSession,
|
||||
onRefresh,
|
||||
onCreateSession,
|
||||
onSelectAgent,
|
||||
agents,
|
||||
agentsLoading,
|
||||
agentsError,
|
||||
sessionsLoading,
|
||||
sessionsError
|
||||
sessionsError,
|
||||
modesByAgent,
|
||||
modelsByAgent,
|
||||
defaultModelByAgent,
|
||||
modesLoadingByAgent,
|
||||
modelsLoadingByAgent,
|
||||
modesErrorByAgent,
|
||||
modelsErrorByAgent,
|
||||
mcpServers,
|
||||
onMcpServersChange,
|
||||
mcpConfigError,
|
||||
skillSources,
|
||||
onSkillSourcesChange
|
||||
}: {
|
||||
sessions: SessionInfo[];
|
||||
selectedSessionId: string;
|
||||
onSelectSession: (session: SessionInfo) => void;
|
||||
onRefresh: () => void;
|
||||
onCreateSession: (agentId: string) => void;
|
||||
onCreateSession: (agentId: string, config: SessionConfig) => void;
|
||||
onSelectAgent: (agentId: string) => void;
|
||||
agents: AgentInfo[];
|
||||
agentsLoading: boolean;
|
||||
agentsError: string | null;
|
||||
sessionsLoading: boolean;
|
||||
sessionsError: string | null;
|
||||
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 [showMenu, setShowMenu] = useState(false);
|
||||
const menuRef = useRef<HTMLDivElement | null>(null);
|
||||
|
|
@ -40,15 +77,6 @@ const SessionSidebar = ({
|
|||
return () => document.removeEventListener("mousedown", handler);
|
||||
}, [showMenu]);
|
||||
|
||||
const agentLabels: Record<string, string> = {
|
||||
claude: "Claude Code",
|
||||
codex: "Codex",
|
||||
opencode: "OpenCode",
|
||||
amp: "Amp",
|
||||
pi: "Pi",
|
||||
mock: "Mock"
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="session-sidebar">
|
||||
<div className="sidebar-header">
|
||||
|
|
@ -65,32 +93,27 @@ const SessionSidebar = ({
|
|||
>
|
||||
<Plus size={14} />
|
||||
</button>
|
||||
{showMenu && (
|
||||
<div className="sidebar-add-menu">
|
||||
{agentsLoading && <div className="sidebar-add-status">Loading agents...</div>}
|
||||
{agentsError && <div className="sidebar-add-status error">{agentsError}</div>}
|
||||
{!agentsLoading && !agentsError && agents.length === 0 && (
|
||||
<div className="sidebar-add-status">No agents available.</div>
|
||||
)}
|
||||
{!agentsLoading && !agentsError &&
|
||||
agents.map((agent) => (
|
||||
<button
|
||||
key={agent.id}
|
||||
className="sidebar-add-option"
|
||||
onClick={() => {
|
||||
onCreateSession(agent.id);
|
||||
setShowMenu(false);
|
||||
}}
|
||||
>
|
||||
<div className="agent-option-left">
|
||||
<span className="agent-option-name">{agentLabels[agent.id] ?? agent.id}</span>
|
||||
{agent.version && <span className="agent-badge version">v{agent.version}</span>}
|
||||
</div>
|
||||
{agent.installed && <span className="agent-badge installed">Installed</span>}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<SessionCreateMenu
|
||||
agents={agents}
|
||||
agentsLoading={agentsLoading}
|
||||
agentsError={agentsError}
|
||||
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}
|
||||
open={showMenu}
|
||||
onClose={() => setShowMenu(false)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,16 +1,15 @@
|
|||
import { MessageSquare, PauseCircle, PlayCircle, Plus, Square, Terminal } from "lucide-react";
|
||||
import { MessageSquare, Plus, Square, Terminal } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import type { AgentInfo, AgentModelInfo, AgentModeInfo, PermissionEventData, QuestionEventData } from "sandbox-agent";
|
||||
import type { AgentInfo, AgentModelInfo, AgentModeInfo, PermissionEventData, QuestionEventData, SkillSource } from "sandbox-agent";
|
||||
import type { McpServerEntry } from "../../App";
|
||||
import ApprovalsTab from "../debug/ApprovalsTab";
|
||||
import SessionCreateMenu, { type SessionConfig } from "../SessionCreateMenu";
|
||||
import ChatInput from "./ChatInput";
|
||||
import ChatMessages from "./ChatMessages";
|
||||
import ChatSetup from "./ChatSetup";
|
||||
import type { TimelineEntry } from "./types";
|
||||
|
||||
const ChatPanel = ({
|
||||
sessionId,
|
||||
polling,
|
||||
turnStreaming,
|
||||
transcriptEntries,
|
||||
sessionError,
|
||||
message,
|
||||
|
|
@ -18,35 +17,18 @@ const ChatPanel = ({
|
|||
onSendMessage,
|
||||
onKeyDown,
|
||||
onCreateSession,
|
||||
onSelectAgent,
|
||||
agents,
|
||||
agentsLoading,
|
||||
agentsError,
|
||||
messagesEndRef,
|
||||
agentId,
|
||||
agentLabel,
|
||||
agentMode,
|
||||
permissionMode,
|
||||
model,
|
||||
variant,
|
||||
modelOptions,
|
||||
defaultModel,
|
||||
modelsLoading,
|
||||
modelsError,
|
||||
variantOptions,
|
||||
defaultVariant,
|
||||
supportsVariants,
|
||||
streamMode,
|
||||
activeModes,
|
||||
currentAgentVersion,
|
||||
hasSession,
|
||||
modesLoading,
|
||||
modesError,
|
||||
onAgentModeChange,
|
||||
onPermissionModeChange,
|
||||
onModelChange,
|
||||
onVariantChange,
|
||||
onStreamModeChange,
|
||||
onToggleStream,
|
||||
sessionModel,
|
||||
sessionVariant,
|
||||
sessionPermissionMode,
|
||||
sessionMcpServerCount,
|
||||
sessionSkillSourceCount,
|
||||
onEndSession,
|
||||
eventError,
|
||||
questionRequests,
|
||||
|
|
@ -55,47 +37,40 @@ const ChatPanel = ({
|
|||
onSelectQuestionOption,
|
||||
onAnswerQuestion,
|
||||
onRejectQuestion,
|
||||
onReplyPermission
|
||||
onReplyPermission,
|
||||
modesByAgent,
|
||||
modelsByAgent,
|
||||
defaultModelByAgent,
|
||||
modesLoadingByAgent,
|
||||
modelsLoadingByAgent,
|
||||
modesErrorByAgent,
|
||||
modelsErrorByAgent,
|
||||
mcpServers,
|
||||
onMcpServersChange,
|
||||
mcpConfigError,
|
||||
skillSources,
|
||||
onSkillSourcesChange
|
||||
}: {
|
||||
sessionId: string;
|
||||
polling: boolean;
|
||||
turnStreaming: boolean;
|
||||
transcriptEntries: TimelineEntry[];
|
||||
sessionError: string | null;
|
||||
message: string;
|
||||
onMessageChange: (value: string) => void;
|
||||
onSendMessage: () => void;
|
||||
onKeyDown: (event: React.KeyboardEvent<HTMLTextAreaElement>) => void;
|
||||
onCreateSession: (agentId: string) => void;
|
||||
onCreateSession: (agentId: string, config: SessionConfig) => void;
|
||||
onSelectAgent: (agentId: string) => void;
|
||||
agents: AgentInfo[];
|
||||
agentsLoading: boolean;
|
||||
agentsError: string | null;
|
||||
messagesEndRef: React.RefObject<HTMLDivElement>;
|
||||
agentId: string;
|
||||
agentLabel: string;
|
||||
agentMode: string;
|
||||
permissionMode: string;
|
||||
model: string;
|
||||
variant: string;
|
||||
modelOptions: AgentModelInfo[];
|
||||
defaultModel: string;
|
||||
modelsLoading: boolean;
|
||||
modelsError: string | null;
|
||||
variantOptions: string[];
|
||||
defaultVariant: string;
|
||||
supportsVariants: boolean;
|
||||
streamMode: "poll" | "sse" | "turn";
|
||||
activeModes: AgentModeInfo[];
|
||||
currentAgentVersion?: string | null;
|
||||
hasSession: boolean;
|
||||
modesLoading: boolean;
|
||||
modesError: string | null;
|
||||
onAgentModeChange: (value: string) => void;
|
||||
onPermissionModeChange: (value: string) => void;
|
||||
onModelChange: (value: string) => void;
|
||||
onVariantChange: (value: string) => void;
|
||||
onStreamModeChange: (value: "poll" | "sse" | "turn") => void;
|
||||
onToggleStream: () => void;
|
||||
sessionModel?: string | null;
|
||||
sessionVariant?: string | null;
|
||||
sessionPermissionMode?: string | null;
|
||||
sessionMcpServerCount: number;
|
||||
sessionSkillSourceCount: number;
|
||||
onEndSession: () => void;
|
||||
eventError: string | null;
|
||||
questionRequests: QuestionEventData[];
|
||||
|
|
@ -105,6 +80,18 @@ const ChatPanel = ({
|
|||
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);
|
||||
|
|
@ -121,19 +108,7 @@ const ChatPanel = ({
|
|||
return () => document.removeEventListener("mousedown", handler);
|
||||
}, [showAgentMenu]);
|
||||
|
||||
const agentLabels: Record<string, string> = {
|
||||
claude: "Claude Code",
|
||||
codex: "Codex",
|
||||
opencode: "OpenCode",
|
||||
amp: "Amp",
|
||||
pi: "Pi",
|
||||
mock: "Mock"
|
||||
};
|
||||
|
||||
const hasApprovals = questionRequests.length > 0 || permissionRequests.length > 0;
|
||||
const isTurnMode = streamMode === "turn";
|
||||
const isStreaming = isTurnMode ? turnStreaming : polling;
|
||||
const turnLabel = turnStreaming ? "Streaming" : "On Send";
|
||||
|
||||
return (
|
||||
<div className="chat-panel">
|
||||
|
|
@ -142,12 +117,6 @@ const ChatPanel = ({
|
|||
<MessageSquare className="button-icon" />
|
||||
<span className="panel-title">{sessionId ? "Session" : "No Session"}</span>
|
||||
{sessionId && <span className="session-id-display">{sessionId}</span>}
|
||||
{sessionId && (
|
||||
<span className="session-agent-display">
|
||||
{agentLabel}
|
||||
{currentAgentVersion && <span className="session-agent-version">v{currentAgentVersion}</span>}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="panel-header-right">
|
||||
{sessionId && (
|
||||
|
|
@ -161,42 +130,6 @@ const ChatPanel = ({
|
|||
End
|
||||
</button>
|
||||
)}
|
||||
<div className="setup-stream">
|
||||
<select
|
||||
className="setup-select-small"
|
||||
value={streamMode}
|
||||
onChange={(e) => onStreamModeChange(e.target.value as "poll" | "sse" | "turn")}
|
||||
title="Stream Mode"
|
||||
disabled={!sessionId}
|
||||
>
|
||||
<option value="poll">Poll</option>
|
||||
<option value="sse">SSE</option>
|
||||
<option value="turn">Turn</option>
|
||||
</select>
|
||||
<button
|
||||
className={`setup-stream-btn ${isStreaming ? "active" : ""}`}
|
||||
onClick={onToggleStream}
|
||||
title={isTurnMode ? "Turn streaming starts on send" : polling ? "Stop streaming" : "Start streaming"}
|
||||
disabled={!sessionId || isTurnMode}
|
||||
>
|
||||
{isTurnMode ? (
|
||||
<>
|
||||
<PlayCircle size={14} />
|
||||
<span>{turnLabel}</span>
|
||||
</>
|
||||
) : polling ? (
|
||||
<>
|
||||
<PauseCircle size={14} />
|
||||
<span>Pause</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<PlayCircle size={14} />
|
||||
<span>Resume</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -214,32 +147,27 @@ const ChatPanel = ({
|
|||
<Plus className="button-icon" />
|
||||
Create Session
|
||||
</button>
|
||||
{showAgentMenu && (
|
||||
<div className="empty-state-menu">
|
||||
{agentsLoading && <div className="sidebar-add-status">Loading agents...</div>}
|
||||
{agentsError && <div className="sidebar-add-status error">{agentsError}</div>}
|
||||
{!agentsLoading && !agentsError && agents.length === 0 && (
|
||||
<div className="sidebar-add-status">No agents available.</div>
|
||||
)}
|
||||
{!agentsLoading && !agentsError &&
|
||||
agents.map((agent) => (
|
||||
<button
|
||||
key={agent.id}
|
||||
className="sidebar-add-option"
|
||||
onClick={() => {
|
||||
onCreateSession(agent.id);
|
||||
setShowAgentMenu(false);
|
||||
}}
|
||||
>
|
||||
<div className="agent-option-left">
|
||||
<span className="agent-option-name">{agentLabels[agent.id] ?? agent.id}</span>
|
||||
{agent.version && <span className="agent-badge version">v{agent.version}</span>}
|
||||
</div>
|
||||
{agent.installed && <span className="agent-badge installed">Installed</span>}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<SessionCreateMenu
|
||||
agents={agents}
|
||||
agentsLoading={agentsLoading}
|
||||
agentsError={agentsError}
|
||||
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}
|
||||
open={showAgentMenu}
|
||||
onClose={() => setShowAgentMenu(false)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : transcriptEntries.length === 0 && !sessionError ? (
|
||||
|
|
@ -247,7 +175,7 @@ const ChatPanel = ({
|
|||
<Terminal className="empty-state-icon" />
|
||||
<div className="empty-state-title">Ready to Chat</div>
|
||||
<p className="empty-state-text">Send a message to start a conversation with the agent.</p>
|
||||
{agentId === "mock" && (
|
||||
{agentLabel === "Mock" && (
|
||||
<div className="mock-agent-hint">
|
||||
The mock agent simulates agent responses for testing the inspector UI without requiring API credentials. Send <code>help</code> for available commands.
|
||||
</div>
|
||||
|
|
@ -284,30 +212,37 @@ const ChatPanel = ({
|
|||
onSendMessage={onSendMessage}
|
||||
onKeyDown={onKeyDown}
|
||||
placeholder={sessionId ? "Send a message..." : "Select or create a session first"}
|
||||
disabled={!sessionId || turnStreaming}
|
||||
disabled={!sessionId}
|
||||
/>
|
||||
|
||||
<ChatSetup
|
||||
agentMode={agentMode}
|
||||
permissionMode={permissionMode}
|
||||
model={model}
|
||||
variant={variant}
|
||||
modelOptions={modelOptions}
|
||||
defaultModel={defaultModel}
|
||||
modelsLoading={modelsLoading}
|
||||
modelsError={modelsError}
|
||||
variantOptions={variantOptions}
|
||||
defaultVariant={defaultVariant}
|
||||
supportsVariants={supportsVariants}
|
||||
activeModes={activeModes}
|
||||
modesLoading={modesLoading}
|
||||
modesError={modesError}
|
||||
onAgentModeChange={onAgentModeChange}
|
||||
onPermissionModeChange={onPermissionModeChange}
|
||||
onModelChange={onModelChange}
|
||||
onVariantChange={onVariantChange}
|
||||
hasSession={hasSession}
|
||||
/>
|
||||
{sessionId && (
|
||||
<div className="session-config-bar">
|
||||
<div className="session-config-field">
|
||||
<span className="session-config-label">Agent</span>
|
||||
<span className="session-config-value">{agentLabel}</span>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,178 +0,0 @@
|
|||
import type { AgentModelInfo, AgentModeInfo } from "sandbox-agent";
|
||||
|
||||
const ChatSetup = ({
|
||||
agentMode,
|
||||
permissionMode,
|
||||
model,
|
||||
variant,
|
||||
modelOptions,
|
||||
defaultModel,
|
||||
modelsLoading,
|
||||
modelsError,
|
||||
variantOptions,
|
||||
defaultVariant,
|
||||
supportsVariants,
|
||||
activeModes,
|
||||
hasSession,
|
||||
modesLoading,
|
||||
modesError,
|
||||
onAgentModeChange,
|
||||
onPermissionModeChange,
|
||||
onModelChange,
|
||||
onVariantChange
|
||||
}: {
|
||||
agentMode: string;
|
||||
permissionMode: string;
|
||||
model: string;
|
||||
variant: string;
|
||||
modelOptions: AgentModelInfo[];
|
||||
defaultModel: string;
|
||||
modelsLoading: boolean;
|
||||
modelsError: string | null;
|
||||
variantOptions: string[];
|
||||
defaultVariant: string;
|
||||
supportsVariants: boolean;
|
||||
activeModes: AgentModeInfo[];
|
||||
hasSession: boolean;
|
||||
modesLoading: boolean;
|
||||
modesError: string | null;
|
||||
onAgentModeChange: (value: string) => void;
|
||||
onPermissionModeChange: (value: string) => void;
|
||||
onModelChange: (value: string) => void;
|
||||
onVariantChange: (value: string) => void;
|
||||
}) => {
|
||||
const hasModelOptions = modelOptions.length > 0;
|
||||
const showModelSelect = hasModelOptions && !modelsError;
|
||||
const hasVariantOptions = variantOptions.length > 0;
|
||||
const showVariantSelect = supportsVariants && hasVariantOptions && !modelsError;
|
||||
const modelCustom =
|
||||
model && hasModelOptions && !modelOptions.some((entry) => entry.id === model);
|
||||
const variantCustom =
|
||||
variant && hasVariantOptions && !variantOptions.includes(variant);
|
||||
|
||||
return (
|
||||
<div className="setup-row">
|
||||
<div className="setup-field">
|
||||
<span className="setup-label">Mode</span>
|
||||
<select
|
||||
className="setup-select"
|
||||
value={agentMode}
|
||||
onChange={(e) => onAgentModeChange(e.target.value)}
|
||||
title="Mode"
|
||||
disabled={!hasSession || modesLoading || Boolean(modesError)}
|
||||
>
|
||||
{modesLoading ? (
|
||||
<option value="">Loading modes...</option>
|
||||
) : modesError ? (
|
||||
<option value="">{modesError}</option>
|
||||
) : activeModes.length > 0 ? (
|
||||
activeModes.map((mode) => (
|
||||
<option key={mode.id} value={mode.id}>
|
||||
{mode.name || mode.id}
|
||||
</option>
|
||||
))
|
||||
) : (
|
||||
<option value="">Mode</option>
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="setup-field">
|
||||
<span className="setup-label">Permission</span>
|
||||
<select
|
||||
className="setup-select"
|
||||
value={permissionMode}
|
||||
onChange={(e) => onPermissionModeChange(e.target.value)}
|
||||
title="Permission Mode"
|
||||
disabled={!hasSession}
|
||||
>
|
||||
<option value="default">Default</option>
|
||||
<option value="plan">Plan</option>
|
||||
<option value="bypass">Bypass</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="setup-field">
|
||||
<span className="setup-label">Model</span>
|
||||
{showModelSelect ? (
|
||||
<select
|
||||
className="setup-select"
|
||||
value={model}
|
||||
onChange={(e) => onModelChange(e.target.value)}
|
||||
title="Model"
|
||||
disabled={!hasSession || 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>
|
||||
) : (
|
||||
<input
|
||||
className="setup-input"
|
||||
value={model}
|
||||
onChange={(e) => onModelChange(e.target.value)}
|
||||
placeholder="Model"
|
||||
title="Model"
|
||||
disabled={!hasSession}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="setup-field">
|
||||
<span className="setup-label">Variant</span>
|
||||
{showVariantSelect ? (
|
||||
<select
|
||||
className="setup-select"
|
||||
value={variant}
|
||||
onChange={(e) => onVariantChange(e.target.value)}
|
||||
title="Variant"
|
||||
disabled={!hasSession || !supportsVariants || modelsLoading || Boolean(modelsError)}
|
||||
>
|
||||
{modelsLoading ? (
|
||||
<option value="">Loading variants...</option>
|
||||
) : modelsError ? (
|
||||
<option value="">{modelsError}</option>
|
||||
) : (
|
||||
<>
|
||||
<option value="">
|
||||
{defaultVariant ? `Default (${defaultVariant})` : "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) => onVariantChange(e.target.value)}
|
||||
placeholder={supportsVariants ? "Variant" : "Variants unsupported"}
|
||||
title="Variant"
|
||||
disabled={!hasSession || !supportsVariants}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatSetup;
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { Download, RefreshCw } from "lucide-react";
|
||||
import { Download, Loader2, RefreshCw } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import type { AgentInfo, AgentModeInfo } from "sandbox-agent";
|
||||
import FeatureCoverageBadges from "../agents/FeatureCoverageBadges";
|
||||
import { emptyFeatureCoverage } from "../../types/agents";
|
||||
|
|
@ -16,10 +17,21 @@ const AgentsTab = ({
|
|||
defaultAgents: string[];
|
||||
modesByAgent: Record<string, AgentModeInfo[]>;
|
||||
onRefresh: () => void;
|
||||
onInstall: (agentId: string, reinstall: boolean) => void;
|
||||
onInstall: (agentId: string, reinstall: boolean) => Promise<void>;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
}) => {
|
||||
const [installingAgent, setInstallingAgent] = useState<string | null>(null);
|
||||
|
||||
const handleInstall = async (agentId: string, reinstall: boolean) => {
|
||||
setInstallingAgent(agentId);
|
||||
try {
|
||||
await onInstall(agentId, reinstall);
|
||||
} finally {
|
||||
setInstallingAgent(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="inline-row" style={{ marginBottom: 16 }}>
|
||||
|
|
@ -39,42 +51,57 @@ const AgentsTab = ({
|
|||
: defaultAgents.map((id) => ({
|
||||
id,
|
||||
installed: false,
|
||||
credentialsAvailable: false,
|
||||
version: undefined,
|
||||
path: undefined,
|
||||
capabilities: emptyFeatureCoverage
|
||||
}))).map((agent) => (
|
||||
<div key={agent.id} className="card">
|
||||
<div className="card-header">
|
||||
<span className="card-title">{agent.id}</span>
|
||||
<span className={`pill ${agent.installed ? "success" : "danger"}`}>
|
||||
{agent.installed ? "Installed" : "Missing"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="card-meta">
|
||||
{agent.version ? `v${agent.version}` : "Version unknown"}
|
||||
{agent.path && <span className="mono muted" style={{ marginLeft: 8 }}>{agent.path}</span>}
|
||||
</div>
|
||||
<div className="card-meta" style={{ marginTop: 8 }}>
|
||||
Feature coverage
|
||||
</div>
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<FeatureCoverageBadges featureCoverage={agent.capabilities ?? emptyFeatureCoverage} />
|
||||
</div>
|
||||
{modesByAgent[agent.id] && modesByAgent[agent.id].length > 0 && (
|
||||
<div className="card-meta" style={{ marginTop: 8 }}>
|
||||
Modes: {modesByAgent[agent.id].map((mode) => mode.id).join(", ")}
|
||||
}))).map((agent) => {
|
||||
const isInstalling = installingAgent === agent.id;
|
||||
return (
|
||||
<div key={agent.id} className="card">
|
||||
<div className="card-header">
|
||||
<span className="card-title">{agent.id}</span>
|
||||
<div className="card-header-pills">
|
||||
<span className={`pill ${agent.installed ? "success" : "danger"}`}>
|
||||
{agent.installed ? "Installed" : "Missing"}
|
||||
</span>
|
||||
<span className={`pill ${agent.credentialsAvailable ? "success" : "warning"}`}>
|
||||
{agent.credentialsAvailable ? "Authenticated" : "No Credentials"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card-meta">
|
||||
{agent.version ?? "Version unknown"}
|
||||
{agent.path && <span className="mono muted" style={{ marginLeft: 8 }}>{agent.path}</span>}
|
||||
</div>
|
||||
<div className="card-meta" style={{ marginTop: 8 }}>
|
||||
Feature coverage
|
||||
</div>
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<FeatureCoverageBadges featureCoverage={agent.capabilities ?? emptyFeatureCoverage} />
|
||||
</div>
|
||||
{modesByAgent[agent.id] && modesByAgent[agent.id].length > 0 && (
|
||||
<div className="card-meta" style={{ marginTop: 8 }}>
|
||||
Modes: {modesByAgent[agent.id].map((mode) => mode.id).join(", ")}
|
||||
</div>
|
||||
)}
|
||||
<div className="card-actions">
|
||||
<button
|
||||
className="button secondary small"
|
||||
onClick={() => handleInstall(agent.id, agent.installed)}
|
||||
disabled={isInstalling}
|
||||
>
|
||||
{isInstalling ? (
|
||||
<Loader2 className="button-icon spinner-icon" />
|
||||
) : (
|
||||
<Download className="button-icon" />
|
||||
)}
|
||||
{isInstalling ? "Installing..." : agent.installed ? "Reinstall" : "Install"}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="card-actions">
|
||||
<button className="button secondary small" onClick={() => onInstall(agent.id, false)}>
|
||||
<Download className="button-icon" /> Install
|
||||
</button>
|
||||
<button className="button ghost small" onClick={() => onInstall(agent.id, true)}>
|
||||
Reinstall
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ const DebugPanel = ({
|
|||
defaultAgents: string[];
|
||||
modesByAgent: Record<string, AgentModeInfo[]>;
|
||||
onRefreshAgents: () => void;
|
||||
onInstallAgent: (agentId: string, reinstall: boolean) => void;
|
||||
onInstallAgent: (agentId: string, reinstall: boolean) => Promise<void>;
|
||||
agentsLoading: boolean;
|
||||
agentsError: string | null;
|
||||
}) => {
|
||||
|
|
|
|||
|
|
@ -30,6 +30,10 @@ export const getEventIcon = (type: string) => {
|
|||
return PlayCircle;
|
||||
case "session.ended":
|
||||
return PauseCircle;
|
||||
case "turn.started":
|
||||
return PlayCircle;
|
||||
case "turn.ended":
|
||||
return PauseCircle;
|
||||
case "item.started":
|
||||
return MessageSquare;
|
||||
case "item.delta":
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue