mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-15 05:02:11 +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
|
|
@ -1,15 +1,20 @@
|
|||
FROM node:22-alpine AS build
|
||||
WORKDIR /app
|
||||
RUN npm install -g pnpm
|
||||
RUN npm install -g pnpm@9
|
||||
|
||||
# Copy package files for all workspaces
|
||||
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||
COPY frontend/packages/inspector/package.json ./frontend/packages/inspector/
|
||||
COPY sdks/typescript/package.json ./sdks/typescript/
|
||||
COPY sdks/cli-shared/package.json ./sdks/cli-shared/
|
||||
|
||||
# Install dependencies
|
||||
RUN pnpm install --filter @sandbox-agent/inspector...
|
||||
|
||||
# Copy cli-shared source and build it
|
||||
COPY sdks/cli-shared ./sdks/cli-shared
|
||||
RUN cd sdks/cli-shared && pnpm exec tsup
|
||||
|
||||
# Copy SDK source (with pre-generated types)
|
||||
COPY sdks/typescript ./sdks/typescript
|
||||
|
||||
|
|
|
|||
|
|
@ -336,6 +336,12 @@
|
|||
color: var(--danger);
|
||||
}
|
||||
|
||||
.banner.config-note {
|
||||
background: rgba(255, 159, 10, 0.12);
|
||||
border-left: 3px solid var(--warning);
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
.banner.success {
|
||||
background: rgba(48, 209, 88, 0.1);
|
||||
border-left: 3px solid var(--success);
|
||||
|
|
@ -471,11 +477,12 @@
|
|||
position: relative;
|
||||
}
|
||||
|
||||
.sidebar-add-menu {
|
||||
.sidebar-add-menu,
|
||||
.session-create-menu {
|
||||
position: absolute;
|
||||
top: 36px;
|
||||
left: 0;
|
||||
min-width: 200px;
|
||||
min-width: 220px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-2);
|
||||
border-radius: 8px;
|
||||
|
|
@ -487,6 +494,405 @@
|
|||
z-index: 60;
|
||||
}
|
||||
|
||||
.session-create-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 6px 4px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.session-create-back {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: transparent;
|
||||
border: 1px solid var(--border-2);
|
||||
border-radius: 4px;
|
||||
color: var(--muted);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all var(--transition);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.session-create-back:hover {
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.session-create-agent-name {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.session-create-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
padding: 4px 2px;
|
||||
}
|
||||
|
||||
.session-create-form .setup-field {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.session-create-form .setup-label {
|
||||
width: 72px;
|
||||
flex-shrink: 0;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.session-create-form .setup-select,
|
||||
.session-create-form .setup-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.session-create-section {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.session-create-section-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
transition: color var(--transition);
|
||||
}
|
||||
|
||||
.session-create-section-toggle:hover {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.session-create-section-toggle .setup-label {
|
||||
width: 72px;
|
||||
flex-shrink: 0;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.session-create-section-count {
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.session-create-section-arrow {
|
||||
margin-left: auto;
|
||||
color: var(--muted-2);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.session-create-section-body {
|
||||
margin: 4px 0 6px;
|
||||
padding: 8px;
|
||||
border: 1px solid var(--border-2);
|
||||
border-radius: 4px;
|
||||
background: var(--surface-2);
|
||||
}
|
||||
|
||||
.session-create-textarea {
|
||||
width: 100%;
|
||||
background: var(--surface-2);
|
||||
border: 1px solid var(--border-2);
|
||||
border-radius: 4px;
|
||||
padding: 6px 8px;
|
||||
font-size: 10px;
|
||||
color: var(--text);
|
||||
outline: none;
|
||||
resize: vertical;
|
||||
min-height: 60px;
|
||||
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Consolas, monospace;
|
||||
transition: border-color var(--transition);
|
||||
}
|
||||
|
||||
.session-create-textarea:focus {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.session-create-textarea::placeholder {
|
||||
color: var(--muted-2);
|
||||
}
|
||||
|
||||
.session-create-inline-error {
|
||||
font-size: 10px;
|
||||
color: var(--danger);
|
||||
margin-top: 4px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.session-create-skill-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.session-create-skill-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 3px 4px 3px 8px;
|
||||
background: var(--surface-2);
|
||||
border: 1px solid var(--border-2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.session-create-skill-path {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 10px;
|
||||
color: var(--text-secondary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.session-create-skill-remove {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
color: var(--muted);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
transition: all var(--transition);
|
||||
}
|
||||
|
||||
.session-create-skill-remove:hover {
|
||||
color: var(--danger);
|
||||
background: rgba(255, 59, 48, 0.12);
|
||||
}
|
||||
|
||||
.session-create-skill-add-row {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.session-create-skill-input {
|
||||
width: 100%;
|
||||
background: var(--surface-2);
|
||||
border: 1px solid var(--accent);
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
font-size: 10px;
|
||||
color: var(--text);
|
||||
outline: none;
|
||||
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Consolas, monospace;
|
||||
}
|
||||
|
||||
.session-create-skill-input::placeholder {
|
||||
color: var(--muted-2);
|
||||
}
|
||||
|
||||
.session-create-skill-type-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 1px 5px;
|
||||
border-radius: 3px;
|
||||
font-size: 9px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
background: rgba(255, 79, 0, 0.15);
|
||||
color: var(--accent);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.session-create-skill-type-row {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.session-create-skill-type-select {
|
||||
width: 80px;
|
||||
flex-shrink: 0;
|
||||
background: var(--surface-2);
|
||||
border: 1px solid var(--accent);
|
||||
border-radius: 4px;
|
||||
padding: 4px 6px;
|
||||
font-size: 10px;
|
||||
color: var(--text);
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.session-create-mcp-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.session-create-mcp-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 3px 4px 3px 8px;
|
||||
background: var(--surface-2);
|
||||
border: 1px solid var(--border-2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.session-create-mcp-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.session-create-mcp-name {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.session-create-mcp-type {
|
||||
font-size: 9px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
color: var(--muted);
|
||||
background: var(--surface);
|
||||
padding: 1px 4px;
|
||||
border-radius: 3px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.session-create-mcp-summary {
|
||||
font-size: 10px;
|
||||
color: var(--muted);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.session-create-mcp-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.session-create-mcp-edit {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.session-create-mcp-name-input {
|
||||
width: 100%;
|
||||
background: var(--surface-2);
|
||||
border: 1px solid var(--accent);
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
font-size: 11px;
|
||||
color: var(--text);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.session-create-mcp-name-input:disabled {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.session-create-mcp-name-input::placeholder {
|
||||
color: var(--muted-2);
|
||||
}
|
||||
|
||||
.session-create-mcp-edit-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.session-create-mcp-save,
|
||||
.session-create-mcp-cancel {
|
||||
flex: 1;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background var(--transition);
|
||||
}
|
||||
|
||||
.session-create-mcp-save {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.session-create-mcp-save:hover {
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
.session-create-mcp-cancel {
|
||||
background: var(--border-2);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.session-create-mcp-cancel:hover {
|
||||
background: var(--muted-2);
|
||||
}
|
||||
|
||||
.session-create-add-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
padding: 4px 8px;
|
||||
background: transparent;
|
||||
border: 1px dashed var(--border-2);
|
||||
border-radius: 4px;
|
||||
color: var(--muted);
|
||||
font-size: 10px;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition);
|
||||
}
|
||||
|
||||
.session-create-add-btn:hover {
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.session-create-actions {
|
||||
padding: 4px 2px 2px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.session-create-actions .button.primary {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Empty state variant of session-create-menu */
|
||||
.empty-state-menu-wrapper .session-create-menu {
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.sidebar-add-option {
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
|
|
@ -515,12 +921,40 @@
|
|||
.agent-option-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.agent-option-name {
|
||||
white-space: nowrap;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.agent-option-version {
|
||||
font-size: 10px;
|
||||
color: var(--muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sidebar-add-option:hover .agent-option-version {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.agent-option-badges {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.agent-option-arrow {
|
||||
color: var(--muted-2);
|
||||
transition: color var(--transition);
|
||||
}
|
||||
|
||||
.sidebar-add-option:hover .agent-option-arrow {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.agent-badge {
|
||||
|
|
@ -535,9 +969,6 @@
|
|||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.agent-badge.version {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.sidebar-add-status {
|
||||
padding: 6px 8px;
|
||||
|
|
@ -1043,6 +1474,36 @@
|
|||
height: 16px;
|
||||
}
|
||||
|
||||
/* Session Config Bar */
|
||||
.session-config-bar {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 20px;
|
||||
padding: 10px 16px 12px;
|
||||
border-top: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.session-config-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.session-config-label {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.session-config-value {
|
||||
font-size: 12px;
|
||||
color: #8e8e93;
|
||||
}
|
||||
|
||||
/* Setup Row */
|
||||
.setup-row {
|
||||
display: flex;
|
||||
|
|
@ -1207,6 +1668,29 @@
|
|||
color: #fff;
|
||||
}
|
||||
|
||||
.setup-config-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.setup-config-btn {
|
||||
border: 1px solid var(--border-2);
|
||||
border-radius: 4px;
|
||||
background: var(--surface);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.setup-config-btn:hover {
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.setup-config-btn.error {
|
||||
color: var(--danger);
|
||||
border-color: rgba(255, 59, 48, 0.4);
|
||||
}
|
||||
|
||||
.setup-version {
|
||||
font-size: 10px;
|
||||
color: var(--muted);
|
||||
|
|
@ -1311,6 +1795,15 @@
|
|||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.config-textarea {
|
||||
min-height: 130px;
|
||||
}
|
||||
|
||||
.config-inline-error {
|
||||
margin-top: 8px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -1319,6 +1812,16 @@
|
|||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.card-header-pills {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.spinner-icon {
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
|
|
|
|||
|
|
@ -3,11 +3,13 @@ import {
|
|||
SandboxAgentError,
|
||||
SandboxAgent,
|
||||
type AgentInfo,
|
||||
type CreateSessionRequest,
|
||||
type AgentModelInfo,
|
||||
type AgentModeInfo,
|
||||
type PermissionEventData,
|
||||
type QuestionEventData,
|
||||
type SessionInfo,
|
||||
type SkillSource,
|
||||
type UniversalEvent,
|
||||
type UniversalItem
|
||||
} from "sandbox-agent";
|
||||
|
|
@ -32,6 +34,41 @@ type ItemDeltaEventData = {
|
|||
delta: string;
|
||||
};
|
||||
|
||||
export type McpServerEntry = {
|
||||
name: string;
|
||||
configJson: string;
|
||||
error: string | null;
|
||||
};
|
||||
|
||||
type ParsedMcpConfig = {
|
||||
value: NonNullable<CreateSessionRequest["mcp"]>;
|
||||
count: number;
|
||||
error: string | null;
|
||||
};
|
||||
|
||||
const buildMcpConfig = (entries: McpServerEntry[]): ParsedMcpConfig => {
|
||||
if (entries.length === 0) {
|
||||
return { value: {}, count: 0, error: null };
|
||||
}
|
||||
const firstError = entries.find((e) => e.error);
|
||||
if (firstError) {
|
||||
return { value: {}, count: entries.length, error: `${firstError.name}: ${firstError.error}` };
|
||||
}
|
||||
const value: NonNullable<CreateSessionRequest["mcp"]> = {};
|
||||
for (const entry of entries) {
|
||||
try {
|
||||
value[entry.name] = JSON.parse(entry.configJson);
|
||||
} catch {
|
||||
return { value: {}, count: entries.length, error: `${entry.name}: Invalid JSON` };
|
||||
}
|
||||
}
|
||||
return { value, count: entries.length, error: null };
|
||||
};
|
||||
|
||||
const buildSkillsConfig = (sources: SkillSource[]): NonNullable<CreateSessionRequest["skills"]> => {
|
||||
return { sources };
|
||||
};
|
||||
|
||||
const buildStubItem = (itemId: string, nativeItemId?: string | null): UniversalItem => {
|
||||
return {
|
||||
item_id: itemId,
|
||||
|
|
@ -53,6 +90,23 @@ const getCurrentOriginEndpoint = () => {
|
|||
return window.location.origin;
|
||||
};
|
||||
|
||||
const getSessionIdFromPath = (): string => {
|
||||
const basePath = import.meta.env.BASE_URL;
|
||||
const path = window.location.pathname;
|
||||
const relative = path.startsWith(basePath) ? path.slice(basePath.length) : path;
|
||||
const match = relative.match(/^sessions\/(.+)/);
|
||||
return match ? match[1] : "";
|
||||
};
|
||||
|
||||
const updateSessionPath = (id: string) => {
|
||||
const basePath = import.meta.env.BASE_URL;
|
||||
const params = window.location.search;
|
||||
const newPath = id ? `${basePath}sessions/${id}${params}` : `${basePath}${params}`;
|
||||
if (window.location.pathname + window.location.search !== newPath) {
|
||||
window.history.replaceState(null, "", newPath);
|
||||
}
|
||||
};
|
||||
|
||||
const getInitialConnection = () => {
|
||||
if (typeof window === "undefined") {
|
||||
return { endpoint: "http://127.0.0.1:2468", token: "", headers: {} as Record<string, string>, hasUrlParam: false };
|
||||
|
|
@ -103,11 +157,7 @@ export default function App() {
|
|||
const [modelsErrorByAgent, setModelsErrorByAgent] = useState<Record<string, string | null>>({});
|
||||
|
||||
const [agentId, setAgentId] = useState("claude");
|
||||
const [agentMode, setAgentMode] = useState("");
|
||||
const [permissionMode, setPermissionMode] = useState("default");
|
||||
const [model, setModel] = useState("");
|
||||
const [variant, setVariant] = useState("");
|
||||
const [sessionId, setSessionId] = useState("");
|
||||
const [sessionId, setSessionId] = useState(getSessionIdFromPath());
|
||||
const [sessionError, setSessionError] = useState<string | null>(null);
|
||||
|
||||
const [message, setMessage] = useState("");
|
||||
|
|
@ -115,6 +165,8 @@ export default function App() {
|
|||
const [offset, setOffset] = useState(0);
|
||||
const offsetRef = useRef(0);
|
||||
const [eventsLoading, setEventsLoading] = useState(false);
|
||||
const [mcpServers, setMcpServers] = useState<McpServerEntry[]>([]);
|
||||
const [skillSources, setSkillSources] = useState<SkillSource[]>([]);
|
||||
|
||||
const [polling, setPolling] = useState(false);
|
||||
const pollTimerRef = useRef<number | null>(null);
|
||||
|
|
@ -377,50 +429,52 @@ export default function App() {
|
|||
stopSse();
|
||||
stopTurnStream();
|
||||
setSessionId(session.sessionId);
|
||||
updateSessionPath(session.sessionId);
|
||||
setAgentId(session.agent);
|
||||
setAgentMode(session.agentMode);
|
||||
setPermissionMode(session.permissionMode);
|
||||
setModel(session.model ?? "");
|
||||
setVariant(session.variant ?? "");
|
||||
setEvents([]);
|
||||
setOffset(0);
|
||||
offsetRef.current = 0;
|
||||
setSessionError(null);
|
||||
};
|
||||
|
||||
const createNewSession = async (nextAgentId?: string) => {
|
||||
const createNewSession = async (
|
||||
nextAgentId: string,
|
||||
config: { model: string; agentMode: string; permissionMode: string; variant: string }
|
||||
) => {
|
||||
stopPolling();
|
||||
stopSse();
|
||||
stopTurnStream();
|
||||
const selectedAgent = nextAgentId ?? agentId;
|
||||
if (nextAgentId) {
|
||||
setAgentId(nextAgentId);
|
||||
setAgentId(nextAgentId);
|
||||
if (parsedMcpConfig.error) {
|
||||
setSessionError(parsedMcpConfig.error);
|
||||
return;
|
||||
}
|
||||
const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
|
||||
let id = "session-";
|
||||
for (let i = 0; i < 8; i++) {
|
||||
id += chars[Math.floor(Math.random() * chars.length)];
|
||||
}
|
||||
setSessionId(id);
|
||||
setEvents([]);
|
||||
setOffset(0);
|
||||
offsetRef.current = 0;
|
||||
setSessionError(null);
|
||||
|
||||
try {
|
||||
const body: {
|
||||
agent: string;
|
||||
agentMode?: string;
|
||||
permissionMode?: string;
|
||||
model?: string;
|
||||
variant?: string;
|
||||
} = { agent: selectedAgent };
|
||||
if (agentMode) body.agentMode = agentMode;
|
||||
if (permissionMode) body.permissionMode = permissionMode;
|
||||
if (model) body.model = model;
|
||||
if (variant) body.variant = variant;
|
||||
const body: CreateSessionRequest = { agent: nextAgentId };
|
||||
if (config.agentMode) body.agentMode = config.agentMode;
|
||||
if (config.permissionMode) body.permissionMode = config.permissionMode;
|
||||
if (config.model) body.model = config.model;
|
||||
if (config.variant) body.variant = config.variant;
|
||||
if (parsedMcpConfig.count > 0) {
|
||||
body.mcp = parsedMcpConfig.value;
|
||||
}
|
||||
if (parsedSkillsConfig.sources.length > 0) {
|
||||
body.skills = parsedSkillsConfig;
|
||||
}
|
||||
|
||||
await getClient().createSession(id, body);
|
||||
setSessionId(id);
|
||||
updateSessionPath(id);
|
||||
setEvents([]);
|
||||
setOffset(0);
|
||||
offsetRef.current = 0;
|
||||
await fetchSessions();
|
||||
} catch (error) {
|
||||
setSessionError(getErrorMessage(error, "Unable to create session"));
|
||||
|
|
@ -762,6 +816,30 @@ export default function App() {
|
|||
});
|
||||
break;
|
||||
}
|
||||
case "turn.started": {
|
||||
entries.push({
|
||||
id: event.event_id,
|
||||
kind: "meta",
|
||||
time: event.time,
|
||||
meta: {
|
||||
title: "Turn started",
|
||||
severity: "info"
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "turn.ended": {
|
||||
entries.push({
|
||||
id: event.event_id,
|
||||
kind: "meta",
|
||||
time: event.time,
|
||||
meta: {
|
||||
title: "Turn ended",
|
||||
severity: "info"
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
|
@ -852,38 +930,10 @@ export default function App() {
|
|||
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
}, [transcriptEntries]);
|
||||
|
||||
useEffect(() => {
|
||||
if (connected && agentId && !modesByAgent[agentId]) {
|
||||
loadModes(agentId);
|
||||
}
|
||||
}, [connected, agentId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (connected && agentId && !modelsByAgent[agentId]) {
|
||||
loadModels(agentId);
|
||||
}
|
||||
}, [connected, agentId]);
|
||||
|
||||
useEffect(() => {
|
||||
const modes = modesByAgent[agentId];
|
||||
if (modes && modes.length > 0 && !agentMode) {
|
||||
setAgentMode(modes[0].id);
|
||||
}
|
||||
}, [modesByAgent, agentId]);
|
||||
|
||||
const currentAgent = agents.find((agent) => agent.id === agentId);
|
||||
const activeModes = modesByAgent[agentId] ?? [];
|
||||
const modesLoading = modesLoadingByAgent[agentId] ?? false;
|
||||
const modesError = modesErrorByAgent[agentId] ?? null;
|
||||
const modelOptions = modelsByAgent[agentId] ?? [];
|
||||
const modelsLoading = modelsLoadingByAgent[agentId] ?? false;
|
||||
const modelsError = modelsErrorByAgent[agentId] ?? null;
|
||||
const defaultModel = defaultModelByAgent[agentId] ?? "";
|
||||
const selectedModelId = model || defaultModel;
|
||||
const selectedModel = modelOptions.find((entry) => entry.id === selectedModelId);
|
||||
const variantOptions = selectedModel?.variants ?? [];
|
||||
const defaultVariant = selectedModel?.defaultVariant ?? "";
|
||||
const supportsVariants = Boolean(currentAgent?.capabilities?.variants);
|
||||
const currentSessionInfo = sessions.find((s) => s.sessionId === sessionId);
|
||||
const parsedMcpConfig = useMemo(() => buildMcpConfig(mcpServers), [mcpServers]);
|
||||
const parsedSkillsConfig = useMemo(() => buildSkillsConfig(skillSources), [skillSources]);
|
||||
const agentDisplayNames: Record<string, string> = {
|
||||
claude: "Claude Code",
|
||||
codex: "Codex",
|
||||
|
|
@ -894,6 +944,15 @@ export default function App() {
|
|||
};
|
||||
const agentLabel = agentDisplayNames[agentId] ?? agentId;
|
||||
|
||||
const handleSelectAgent = useCallback((targetAgentId: string) => {
|
||||
if (connected && !modesByAgent[targetAgentId]) {
|
||||
loadModes(targetAgentId);
|
||||
}
|
||||
if (connected && !modelsByAgent[targetAgentId]) {
|
||||
loadModels(targetAgentId);
|
||||
}
|
||||
}, [connected, modesByAgent, modelsByAgent]);
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (event.key === "Enter" && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
|
|
@ -957,17 +1016,28 @@ export default function App() {
|
|||
onSelectSession={selectSession}
|
||||
onRefresh={fetchSessions}
|
||||
onCreateSession={createNewSession}
|
||||
onSelectAgent={handleSelectAgent}
|
||||
agents={agents.length ? agents : defaultAgents.map((id) => ({ id, installed: false, capabilities: {} }) as AgentInfo)}
|
||||
agentsLoading={agentsLoading}
|
||||
agentsError={agentsError}
|
||||
sessionsLoading={sessionsLoading}
|
||||
sessionsError={sessionsError}
|
||||
modesByAgent={modesByAgent}
|
||||
modelsByAgent={modelsByAgent}
|
||||
defaultModelByAgent={defaultModelByAgent}
|
||||
modesLoadingByAgent={modesLoadingByAgent}
|
||||
modelsLoadingByAgent={modelsLoadingByAgent}
|
||||
modesErrorByAgent={modesErrorByAgent}
|
||||
modelsErrorByAgent={modelsErrorByAgent}
|
||||
mcpServers={mcpServers}
|
||||
onMcpServersChange={setMcpServers}
|
||||
mcpConfigError={parsedMcpConfig.error}
|
||||
skillSources={skillSources}
|
||||
onSkillSourcesChange={setSkillSources}
|
||||
/>
|
||||
|
||||
<ChatPanel
|
||||
sessionId={sessionId}
|
||||
polling={polling}
|
||||
turnStreaming={turnStreaming}
|
||||
transcriptEntries={transcriptEntries}
|
||||
sessionError={sessionError}
|
||||
message={message}
|
||||
|
|
@ -975,36 +1045,19 @@ export default function App() {
|
|||
onSendMessage={sendMessage}
|
||||
onKeyDown={handleKeyDown}
|
||||
onCreateSession={createNewSession}
|
||||
onSelectAgent={handleSelectAgent}
|
||||
agents={agents.length ? agents : defaultAgents.map((id) => ({ id, installed: false, capabilities: {} }) as AgentInfo)}
|
||||
agentsLoading={agentsLoading}
|
||||
agentsError={agentsError}
|
||||
messagesEndRef={messagesEndRef}
|
||||
agentId={agentId}
|
||||
agentLabel={agentLabel}
|
||||
agentMode={agentMode}
|
||||
permissionMode={permissionMode}
|
||||
model={model}
|
||||
variant={variant}
|
||||
modelOptions={modelOptions}
|
||||
defaultModel={defaultModel}
|
||||
modelsLoading={modelsLoading}
|
||||
modelsError={modelsError}
|
||||
variantOptions={variantOptions}
|
||||
defaultVariant={defaultVariant}
|
||||
supportsVariants={supportsVariants}
|
||||
streamMode={streamMode}
|
||||
activeModes={activeModes}
|
||||
currentAgentVersion={currentAgent?.version ?? null}
|
||||
modesLoading={modesLoading}
|
||||
modesError={modesError}
|
||||
onAgentModeChange={setAgentMode}
|
||||
onPermissionModeChange={setPermissionMode}
|
||||
onModelChange={setModel}
|
||||
onVariantChange={setVariant}
|
||||
onStreamModeChange={setStreamMode}
|
||||
onToggleStream={toggleStream}
|
||||
sessionModel={currentSessionInfo?.model ?? null}
|
||||
sessionVariant={currentSessionInfo?.variant ?? null}
|
||||
sessionPermissionMode={currentSessionInfo?.permissionMode ?? null}
|
||||
sessionMcpServerCount={currentSessionInfo?.mcp ? Object.keys(currentSessionInfo.mcp).length : 0}
|
||||
sessionSkillSourceCount={currentSessionInfo?.skills?.sources?.length ?? 0}
|
||||
onEndSession={endSession}
|
||||
hasSession={Boolean(sessionId)}
|
||||
eventError={eventError}
|
||||
questionRequests={questionRequests}
|
||||
permissionRequests={permissionRequests}
|
||||
|
|
@ -1013,6 +1066,18 @@ export default function App() {
|
|||
onAnswerQuestion={answerQuestion}
|
||||
onRejectQuestion={rejectQuestion}
|
||||
onReplyPermission={replyPermission}
|
||||
modesByAgent={modesByAgent}
|
||||
modelsByAgent={modelsByAgent}
|
||||
defaultModelByAgent={defaultModelByAgent}
|
||||
modesLoadingByAgent={modesLoadingByAgent}
|
||||
modelsLoadingByAgent={modelsLoadingByAgent}
|
||||
modesErrorByAgent={modesErrorByAgent}
|
||||
modelsErrorByAgent={modelsErrorByAgent}
|
||||
mcpServers={mcpServers}
|
||||
onMcpServersChange={setMcpServers}
|
||||
mcpConfigError={parsedMcpConfig.error}
|
||||
skillSources={skillSources}
|
||||
onSkillSourcesChange={setSkillSources}
|
||||
/>
|
||||
|
||||
<DebugPanel
|
||||
|
|
|
|||
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":
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
FROM node:22-alpine AS build
|
||||
WORKDIR /app
|
||||
RUN npm install -g pnpm
|
||||
RUN npm install -g pnpm@9
|
||||
|
||||
# Copy website package
|
||||
COPY frontend/packages/website/package.json ./
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue