feat(maestro): web client (Vite + TS + xterm.js)
NAP-218. Browser UI for the prototype: catalog tree derived from `metadata.label`, live updates over `WS /api/events`, search across session name+label, spawn modal (with optional git worktree), and an attach view that opens the daemon's per-session WS for xterm.js. Framework choice: vanilla TypeScript. The interactivity surface is small enough that pulling in a framework wouldn't pay back; xterm.js does the heavy lifting. Easy to swap later without protocol impact. Hash-based routing (`#/`, `#/attach/<id>`) keeps the registry's static surface dumb — no SPA fallback needed; concurrent attaches work by opening the same hash URL in another tab. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
import type {
|
||||
ApiError,
|
||||
ConnectInfo,
|
||||
CreateSessionRequest,
|
||||
DaemonInfo,
|
||||
Session,
|
||||
Uuid,
|
||||
} from "./types.js";
|
||||
|
||||
async function jsonOrError<T>(res: Response): Promise<T> {
|
||||
if (res.ok) {
|
||||
if (res.status === 204) return undefined as unknown as T;
|
||||
return (await res.json()) as T;
|
||||
}
|
||||
let detail = "";
|
||||
try {
|
||||
const body = (await res.json()) as ApiError;
|
||||
detail = `${body.error.code}: ${body.error.message}`;
|
||||
} catch {
|
||||
detail = await res.text();
|
||||
}
|
||||
throw new Error(`${res.status} ${res.statusText}${detail ? ` — ${detail}` : ""}`);
|
||||
}
|
||||
|
||||
export async function listDaemons(): Promise<DaemonInfo[]> {
|
||||
return jsonOrError(await fetch("/api/daemons", { credentials: "same-origin" }));
|
||||
}
|
||||
|
||||
export async function listSessions(): Promise<Session[]> {
|
||||
return jsonOrError(await fetch("/api/sessions", { credentials: "same-origin" }));
|
||||
}
|
||||
|
||||
export async function createSession(req: CreateSessionRequest): Promise<Session> {
|
||||
return jsonOrError(
|
||||
await fetch("/api/sessions", {
|
||||
method: "POST",
|
||||
credentials: "same-origin",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify(req),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export async function killSession(id: Uuid): Promise<void> {
|
||||
await jsonOrError(
|
||||
await fetch(`/api/sessions/${encodeURIComponent(id)}`, {
|
||||
method: "DELETE",
|
||||
credentials: "same-origin",
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export async function getConnectInfo(id: Uuid): Promise<ConnectInfo> {
|
||||
return jsonOrError(
|
||||
await fetch(`/api/sessions/${encodeURIComponent(id)}/connect-info`, {
|
||||
credentials: "same-origin",
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function eventsWsUrl(): string {
|
||||
const proto = location.protocol === "https:" ? "wss:" : "ws:";
|
||||
return `${proto}//${location.host}/api/events`;
|
||||
}
|
||||
Reference in New Issue
Block a user