mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-15 07:04:48 +00:00
feat: add sessions list API and frontend sidebar
This commit is contained in:
parent
ab2c1c2b62
commit
365cf262f3
4 changed files with 256 additions and 4 deletions
|
|
@ -23,6 +23,7 @@ Universal schema guidance:
|
|||
- `sandbox-agent agents list` ↔ `GET /v1/agents`
|
||||
- `sandbox-agent agents install` ↔ `POST /v1/agents/{agent}/install`
|
||||
- `sandbox-agent agents modes` ↔ `GET /v1/agents/{agent}/modes`
|
||||
- `sandbox-agent sessions list` ↔ `GET /v1/sessions`
|
||||
- `sandbox-agent sessions create` ↔ `POST /v1/sessions/{sessionId}`
|
||||
- `sandbox-agent sessions send-message` ↔ `POST /v1/sessions/{sessionId}/messages`
|
||||
- `sandbox-agent sessions events` / `get-messages` ↔ `GET /v1/sessions/{sessionId}/events`
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ use axum::routing::{get, post};
|
|||
use axum::Json;
|
||||
use axum::Router;
|
||||
use futures::{stream, StreamExt};
|
||||
use tower_http::trace::TraceLayer;
|
||||
use reqwest::Client;
|
||||
use sandbox_agent_error::{AgentError, ErrorType, ProblemDetails, SandboxError};
|
||||
use sandbox_agent_universal_agent_schema::{
|
||||
|
|
@ -80,6 +81,7 @@ pub fn build_router(state: AppState) -> Router {
|
|||
.route("/agents", get(list_agents))
|
||||
.route("/agents/:agent/install", post(install_agent))
|
||||
.route("/agents/:agent/modes", get(get_agent_modes))
|
||||
.route("/sessions", get(list_sessions))
|
||||
.route("/sessions/:session_id", post(create_session))
|
||||
.route("/sessions/:session_id/messages", post(post_message))
|
||||
.route("/sessions/:session_id/events", get(get_events))
|
||||
|
|
@ -102,7 +104,9 @@ pub fn build_router(state: AppState) -> Router {
|
|||
v1_router = v1_router.layer(axum::middleware::from_fn_with_state(shared, require_token));
|
||||
}
|
||||
|
||||
Router::new().nest("/v1", v1_router)
|
||||
Router::new()
|
||||
.nest("/v1", v1_router)
|
||||
.layer(TraceLayer::new_for_http())
|
||||
}
|
||||
|
||||
#[derive(OpenApi)]
|
||||
|
|
@ -112,6 +116,7 @@ pub fn build_router(state: AppState) -> Router {
|
|||
install_agent,
|
||||
get_agent_modes,
|
||||
list_agents,
|
||||
list_sessions,
|
||||
create_session,
|
||||
post_message,
|
||||
get_events,
|
||||
|
|
@ -127,6 +132,8 @@ pub fn build_router(state: AppState) -> Router {
|
|||
AgentModesResponse,
|
||||
AgentInfo,
|
||||
AgentListResponse,
|
||||
SessionInfo,
|
||||
SessionListResponse,
|
||||
HealthResponse,
|
||||
CreateSessionRequest,
|
||||
CreateSessionResponse,
|
||||
|
|
@ -1477,6 +1484,19 @@ async fn list_agents(
|
|||
Ok(Json(AgentListResponse { agents }))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/sessions",
|
||||
responses((status = 200, body = SessionListResponse)),
|
||||
tag = "sessions"
|
||||
)]
|
||||
async fn list_sessions(
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> Result<Json<SessionListResponse>, ApiError> {
|
||||
let sessions = state.session_manager.list_sessions().await;
|
||||
Ok(Json(SessionListResponse { sessions }))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/v1/sessions/{session_id}",
|
||||
|
|
|
|||
|
|
@ -372,11 +372,139 @@
|
|||
/* Main Layout (Connected) */
|
||||
.main-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-columns: 200px 1fr 1fr;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Session Sidebar */
|
||||
.session-sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-right: 1px solid var(--border);
|
||||
background: var(--surface-2);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
height: 41px;
|
||||
padding: 0 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sidebar-title {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.sidebar-add-btn {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: var(--accent);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background var(--transition);
|
||||
}
|
||||
|
||||
.sidebar-add-btn:hover {
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
.session-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.session-item {
|
||||
display: block;
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 10px;
|
||||
margin-bottom: 4px;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
transition: all var(--transition);
|
||||
}
|
||||
|
||||
.session-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.session-item:hover {
|
||||
background: var(--surface);
|
||||
border-color: var(--border-2);
|
||||
}
|
||||
|
||||
.session-item.active {
|
||||
background: rgba(255, 79, 0, 0.1);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.session-item-id {
|
||||
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Consolas, monospace;
|
||||
font-size: 11px;
|
||||
color: var(--text);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.session-item-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 10px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.session-item-agent {
|
||||
font-weight: 600;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.session-item-events {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.session-item-ended {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.sidebar-empty {
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.sidebar-refresh {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
padding: 8px;
|
||||
border-top: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Chat Panel */
|
||||
.chat-panel {
|
||||
display: flex;
|
||||
|
|
@ -1123,11 +1251,21 @@
|
|||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 1200px) {
|
||||
.main-layout {
|
||||
grid-template-columns: 180px 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.main-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.session-sidebar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.chat-panel {
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--border);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
MessageSquare,
|
||||
PauseCircle,
|
||||
PlayCircle,
|
||||
Plus,
|
||||
RefreshCw,
|
||||
Send,
|
||||
Shield,
|
||||
|
|
@ -23,6 +24,18 @@ type AgentInfo = {
|
|||
path?: string;
|
||||
};
|
||||
|
||||
type SessionInfo = {
|
||||
sessionId: string;
|
||||
agent: string;
|
||||
agentMode: string;
|
||||
permissionMode: string;
|
||||
model?: string;
|
||||
variant?: string;
|
||||
agentSessionId?: string;
|
||||
ended: boolean;
|
||||
eventCount: number;
|
||||
};
|
||||
|
||||
type AgentMode = {
|
||||
id: string;
|
||||
name: string;
|
||||
|
|
@ -183,6 +196,7 @@ export default function App() {
|
|||
|
||||
const [agents, setAgents] = useState<AgentInfo[]>([]);
|
||||
const [modesByAgent, setModesByAgent] = useState<Record<string, AgentMode[]>>({});
|
||||
const [sessions, setSessions] = useState<SessionInfo[]>([]);
|
||||
|
||||
const [agentId, setAgentId] = useState("claude");
|
||||
const [agentMode, setAgentMode] = useState("");
|
||||
|
|
@ -289,6 +303,7 @@ export default function App() {
|
|||
await apiFetch(`${API_PREFIX}/health`);
|
||||
setConnected(true);
|
||||
await refreshAgents();
|
||||
await fetchSessions();
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unable to connect";
|
||||
setConnectError(message);
|
||||
|
|
@ -325,6 +340,16 @@ export default function App() {
|
|||
}
|
||||
};
|
||||
|
||||
const fetchSessions = async () => {
|
||||
try {
|
||||
const data = await apiFetch(`${API_PREFIX}/sessions`);
|
||||
const sessionList = (data as { sessions?: SessionInfo[] })?.sessions ?? [];
|
||||
setSessions(sessionList);
|
||||
} catch {
|
||||
// Silently fail - sessions list is supplementary
|
||||
}
|
||||
};
|
||||
|
||||
const installAgent = async (targetId: string, reinstall: boolean) => {
|
||||
try {
|
||||
await apiFetch(`${API_PREFIX}/agents/${targetId}/install`, {
|
||||
|
|
@ -379,11 +404,35 @@ export default function App() {
|
|||
method: "POST",
|
||||
body
|
||||
});
|
||||
await fetchSessions();
|
||||
} catch (error) {
|
||||
setSessionError(error instanceof Error ? error.message : "Unable to create session");
|
||||
}
|
||||
};
|
||||
|
||||
const selectSession = (session: SessionInfo) => {
|
||||
setSessionId(session.sessionId);
|
||||
setAgentId(session.agent);
|
||||
setAgentMode(session.agentMode);
|
||||
setPermissionMode(session.permissionMode);
|
||||
setModel(session.model ?? "");
|
||||
setVariant(session.variant ?? "");
|
||||
// Reset events and offset when switching sessions
|
||||
setEvents([]);
|
||||
setOffset(0);
|
||||
offsetRef.current = 0;
|
||||
setSessionError(null);
|
||||
};
|
||||
|
||||
const generateSessionId = () => {
|
||||
const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
|
||||
let id = "session-";
|
||||
for (let i = 0; i < 8; i++) {
|
||||
id += chars[Math.floor(Math.random() * chars.length)];
|
||||
}
|
||||
setSessionId(id);
|
||||
};
|
||||
|
||||
const appendEvents = useCallback((incoming: UniversalEvent[]) => {
|
||||
if (!incoming.length) return;
|
||||
setEvents((prev) => [...prev, ...incoming]);
|
||||
|
|
@ -719,7 +768,51 @@ export default function App() {
|
|||
</header>
|
||||
|
||||
<main className="main-layout">
|
||||
{/* Chat Panel - Left */}
|
||||
{/* Session Sidebar */}
|
||||
<div className="session-sidebar">
|
||||
<div className="sidebar-header">
|
||||
<span className="sidebar-title">Sessions</span>
|
||||
<button
|
||||
className="sidebar-add-btn"
|
||||
onClick={generateSessionId}
|
||||
title="New session ID"
|
||||
>
|
||||
<Plus size={14} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="session-list">
|
||||
{sessions.length === 0 ? (
|
||||
<div className="sidebar-empty">
|
||||
No sessions yet.<br />
|
||||
Create one to get started.
|
||||
</div>
|
||||
) : (
|
||||
sessions.map((session) => (
|
||||
<button
|
||||
key={session.sessionId}
|
||||
className={`session-item ${session.sessionId === sessionId ? "active" : ""}`}
|
||||
onClick={() => selectSession(session)}
|
||||
>
|
||||
<div className="session-item-id">{session.sessionId}</div>
|
||||
<div className="session-item-meta">
|
||||
<span className="session-item-agent">{session.agent}</span>
|
||||
<span className="session-item-events">{session.eventCount} events</span>
|
||||
{session.ended && <span className="session-item-ended">ended</span>}
|
||||
</div>
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="sidebar-refresh">
|
||||
<button className="button ghost small" onClick={fetchSessions}>
|
||||
<RefreshCw size={12} /> Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Chat Panel */}
|
||||
<div className="chat-panel">
|
||||
<div className="panel-header">
|
||||
<div className="panel-header-left">
|
||||
|
|
@ -1123,7 +1216,7 @@ export default function App() {
|
|||
<div className="card-meta">No agents reported. Click refresh to check.</div>
|
||||
)}
|
||||
|
||||
{(agents.length ? agents : defaultAgents.map((id) => ({ id, installed: false }))).map((agent) => (
|
||||
{(agents.length ? agents : defaultAgents.map((id) => ({ id, installed: false, version: undefined, path: undefined }))).map((agent) => (
|
||||
<div key={agent.id} className="card">
|
||||
<div className="card-header">
|
||||
<span className="card-title">{agent.id}</span>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue