import "./styles.css"; import { createApiClient } from "./api.js"; import { type ApprovalDecisionAction, type DoctorCheck, type RunSummary, approvalDecisionActions, compactPath, formatDateTime, highestDoctorStatus, screenFromHash, screens, stateTone, } from "./view-model.js"; interface TemplateSummary { id: string; name: string; version: number; hash: string; definition: { phases?: unknown[]; roles?: unknown[] }; } interface PersonaSummary { id: string; name: string; version: number; hash: string; definition: { backend?: string; capabilities?: unknown[]; maxRiskLevel?: string }; } interface RunStatus { run: RunSummary & { worktreeRoot: string; finalReportPath: string | null; }; phases: Array<{ id: string; phaseKey: string; state: string; attempts: number }>; approvals: Array<{ id: string; gateKey: string; state: string; phaseId: string | null }>; eventsTail: Array<{ seq: string; type: string; ts: string; payload: unknown }>; } interface SessionSummary { id: string; roleId: string; backend: string; cwd: string; state: string; expectedArtifactPath: string | null; expectedSchema: string | null; lastPromptAt: string | null; recoveryAttempts: number; tmuxSession: string | null; tmuxWindow: string | null; } const api = createApiClient(import.meta.env.VITE_API_BASE); const appRoot = document.querySelector("#app"); if (appRoot === null) { throw new Error("Missing #app root"); } const app = appRoot; let activeRunId: string | undefined; let activeRunStatus: RunStatus | undefined; let activeRunSessions: SessionSummary[] = []; let connectedRunId: string | undefined; let doctorChecks: DoctorCheck[] = []; let eventSource: EventSource | undefined; let liveEvents: string[] = []; const approvalErrors = new Map(); const approvalTokens = new Map(); window.addEventListener("hashchange", () => { void render(); }); void render(); async function render(): Promise { const screen = screenFromHash(window.location.hash); app.innerHTML = shell(screen, "Loading"); try { doctorChecks = await loadDoctorChecks(); if (screen === "dashboard") { await renderDashboard(); } else if (screen === "run-detail") { await renderRunDetailScreen(); } else if (screen === "approvals") { await renderApprovalsScreen(); } else if (screen === "sessions") { await renderSessionsScreen(); } else if (screen === "templates") { await renderTemplates(); } else if (screen === "personas") { await renderPersonas(); } else { await renderNewRun(); } } catch (error) { app.innerHTML = shell(screen, errorBanner(error)); } } async function renderDashboard(): Promise { const { runs, sessions, status } = await loadActiveRunData(); app.innerHTML = shell( "dashboard", ` ${doctorPanel()}

Runs

${runsTable(runs)}

Run Detail

${activeRunId === undefined ? "" : `${escapeHtml(activeRunId)}`}
${status === undefined ? empty("No run selected") : runDetail(status, sessions)}
`, ); bindDashboardActions(); connectRunStream(activeRunId); } async function renderRunDetailScreen(): Promise { const { runs, sessions, status } = await loadActiveRunData(); app.innerHTML = shell( "run-detail", `

Runs

${runsTable(runs)}

Run Detail

${activeRunId === undefined ? "" : `${escapeHtml(activeRunId)}`}
${status === undefined ? empty("No run selected") : runDetail(status, sessions)}
`, ); bindDashboardActions(); connectRunStream(activeRunId); } async function renderApprovalsScreen(): Promise { const { runs, status } = await loadActiveRunData(); app.innerHTML = shell( "approvals", `

Runs

${runsTable(runs)}

Approvals

${activeRunId === undefined ? "" : `${escapeHtml(activeRunId)}`}
${ status === undefined || status.approvals.length === 0 ? empty("No approvals") : `${status.approvals.map(approvalRow).join("")}
` }
`, ); bindDashboardActions(); connectRunStream(activeRunId); } async function renderSessionsScreen(): Promise { const { runs, sessions } = await loadActiveRunData(); app.innerHTML = shell( "sessions", `

Runs

${runsTable(runs)}

TUI Sessions

${activeRunId === undefined ? "" : `${escapeHtml(activeRunId)}`}
${sessions.length === 0 ? empty("No sessions") : sessionsTable(sessions)}
`, ); bindDashboardActions(); connectRunStream(activeRunId); } async function renderTemplates(): Promise { const { templates } = await api.get<{ templates: TemplateSummary[] }>("/api/templates"); app.innerHTML = shell( "templates", `

Templates

${templates.length} loaded
${templates .map( (template) => ` `, ) .join("")}
NameVersionRolesPhasesHash
${escapeHtml(template.name)} ${template.version} ${arrayLength(template.definition.roles)} ${arrayLength(template.definition.phases)} ${escapeHtml(template.hash.slice(0, 16))}
`, ); } async function renderPersonas(): Promise { const { personas } = await api.get<{ personas: PersonaSummary[] }>("/api/personas"); app.innerHTML = shell( "personas", `

Personas

${personas.length} loaded
${personas .map( (persona) => ` `, ) .join("")}
NameVersionBackendRiskCapabilities
${escapeHtml(persona.name)} ${persona.version} ${escapeHtml(persona.definition.backend ?? "")} ${escapeHtml(persona.definition.maxRiskLevel ?? "")} ${arrayLength(persona.definition.capabilities)}
`, ); } async function renderNewRun(): Promise { const [{ templates }, { personas }] = await Promise.all([ api.get<{ templates: TemplateSummary[] }>("/api/templates"), api.get<{ personas: PersonaSummary[] }>("/api/personas"), ]); app.innerHTML = shell( "new-run", `

New Run

${templates.length} templates ยท ${personas.length} personas
`, ); bindNewRunForm(); } function shell(active: string, content: string): string { return `

Devflow

Local workflow control plane

${content}
`; } function doctorPanel(): string { const status = highestDoctorStatus(doctorChecks); const visible = doctorChecks.filter((check) => check.status !== "pass"); return `
Doctor ${ visible.length === 0 ? "All visible checks pass" : visible .map((check) => `${escapeHtml(check.name)}: ${escapeHtml(check.detail)}`) .join("") }
`; } function runsTable(runs: RunSummary[]): string { if (runs.length === 0) { return empty("No runs yet"); } return ` ${runs .map( (run) => ` `, ) .join("")}
StateRepoBranchCreated
${escapeHtml(run.state)} ${escapeHtml(compactPath(run.repoPath))} ${escapeHtml(run.baseBranch)} ${escapeHtml(formatDateTime(run.createdAt))}
`; } function runDetail(status: RunStatus, sessions: SessionSummary[]): string { return `
State${escapeHtml(status.run.state)}
Worktree${escapeHtml(compactPath(status.run.worktreeRoot))}
Final report${escapeHtml(status.run.finalReportPath ?? "")}

Phases

${status.phases .map( (phase) => ``, ) .join("")}
PhaseStateAttempts
${escapeHtml(phase.phaseKey)}${escapeHtml(phase.state)}${phase.attempts}

Approvals

${ status.approvals.length === 0 ? empty("No approvals") : `${status.approvals.map(approvalRow).join("")}
` }

TUI Sessions

${sessions.length === 0 ? empty("No sessions") : sessionsTable(sessions)}

Event Tail

${escapeHtml([...status.eventsTail.map((event) => `${event.seq} ${event.type}`), ...liveEvents].slice(-40).join("\n"))}
`; } function sessionsTable(sessions: SessionSummary[]): string { return `${sessions.map(sessionRow).join("")}
RoleBackendStateCWDArtifactRecovery
`; } function sessionRow(session: SessionSummary): string { const artifact = session.expectedArtifactPath === null ? "" : `${session.expectedSchema ?? ""} ${compactPath(session.expectedArtifactPath)}`; return ` ${escapeHtml(session.roleId)} ${escapeHtml(session.backend)} ${escapeHtml(session.state)} ${escapeHtml(compactPath(session.cwd))} ${escapeHtml(artifact)} ${session.recoveryAttempts} `; } function approvalRow(approval: RunStatus["approvals"][number]): string { const disabled = approval.state !== "pending" ? "disabled" : ""; const actionCells = approvalDecisionActions .map((action) => approvalButton(approval.id, action, disabled)) .join(""); const error = approvalDecisionActions .map((action) => approvalErrors.get(approvalActionKey(approval.id, action))) .find((message) => message !== undefined); return ` ${escapeHtml(approval.gateKey)} ${escapeHtml(approval.state)} ${actionCells} ${error === undefined ? "" : `${escapeHtml(error)}`} `; } function approvalButton( approvalId: string, action: ApprovalDecisionAction, disabled: string, ): string { const danger = action === "reject" || action === "abort" ? " danger" : ""; return ``; } function approvalActionLabel(action: ApprovalDecisionAction): string { if (action === "request_changes") { return "Request Changes"; } return action.slice(0, 1).toUpperCase() + action.slice(1); } function bindDashboardActions(): void { document.querySelector('[data-action="refresh"]')?.addEventListener("click", () => { void render(); }); for (const button of document.querySelectorAll("[data-run-id]")) { button.addEventListener("click", () => { activeRunId = button.dataset.runId; liveEvents = []; void render(); }); } for (const button of document.querySelectorAll("[data-approval]")) { button.addEventListener("click", async () => { const action = parseApprovalAction(button.dataset.action); if (activeRunId === undefined || button.dataset.approval === undefined || action === null) { return; } const key = approvalActionKey(button.dataset.approval, action); const clientToken = approvalTokens.get(key) ?? crypto.randomUUID(); approvalTokens.set(key, clientToken); disableApprovalButtons(button.dataset.approval); try { await api.post(`/api/runs/${activeRunId}/approvals/${button.dataset.approval}`, { action, clientToken, }); approvalTokens.delete(key); approvalErrors.delete(key); await render(); } catch (error) { approvalErrors.set(key, error instanceof Error ? error.message : String(error)); await render(); } }); } } function parseApprovalAction(value: string | undefined): ApprovalDecisionAction | null { return approvalDecisionActions.includes(value as ApprovalDecisionAction) ? (value as ApprovalDecisionAction) : null; } function approvalActionKey(approvalId: string, action: ApprovalDecisionAction): string { return `${approvalId}:${action}`; } function disableApprovalButtons(approvalId: string): void { for (const button of document.querySelectorAll("[data-approval]")) { if (button.dataset.approval === approvalId) { button.disabled = true; } } } function bindNewRunForm(): void { document .querySelector("#new-run-form") ?.addEventListener("submit", async (event) => { event.preventDefault(); const form = event.currentTarget as HTMLFormElement; const data = new FormData(form); const status = document.querySelector("#form-status"); try { const scenariosText = stringFormValue(data, "scenarios"); const body = { baseBranch: stringFormValue(data, "baseBranch"), repoPath: stringFormValue(data, "repoPath"), requirementsMd: stringFormValue(data, "requirementsMd"), templateName: stringFormValue(data, "templateName"), templateVersion: Number(stringFormValue(data, "templateVersion")), ...(scenariosText.length === 0 ? {} : { scenarios: JSON.parse(scenariosText) as unknown }), }; const result = await api.post<{ runId: string }>("/api/runs", body); activeRunId = result.runId; window.location.hash = "#/dashboard"; } catch (error) { if (status !== null) { status.textContent = error instanceof Error ? error.message : String(error); } } }); } function connectRunStream(runId: string | undefined): void { if (eventSource !== undefined && connectedRunId === runId) { return; } eventSource?.close(); eventSource = undefined; connectedRunId = undefined; if (runId === undefined) { return; } const source = new EventSource(api.sseUrl(`/sse/runs/${runId}`)); source.addEventListener("run.event_appended", (event) => { const payload = JSON.parse(String((event as MessageEvent).data)) as { type?: string }; liveEvents.push(`live ${payload.type ?? "event"}`); if (liveEvents.length > 50) { liveEvents = liveEvents.slice(-50); } void Promise.all([loadRunStatus(runId), loadRunSessions(runId)]).then(([status, sessions]) => { activeRunStatus = status; activeRunSessions = sessions; const main = document.querySelector("main"); const screen = screenFromHash(window.location.hash); if ( main !== null && (screen === "dashboard" || screen === "run-detail" || screen === "approvals" || screen === "sessions") ) { void render(); } }); }); source.addEventListener("transcript.chunk_appended", (event) => { const payload = JSON.parse(String((event as MessageEvent).data)) as { content?: string }; liveEvents.push(`transcript ${payload.content ?? ""}`); }); eventSource = source; connectedRunId = runId; } async function loadDoctorChecks(): Promise { try { const response = await api.get<{ checks: DoctorCheck[] }>("/api/doctor"); return response.checks; } catch { return [ { detail: "API doctor endpoint unavailable", name: "api", remediation: "Start apps/api or configure VITE_API_BASE.", status: "warn", }, ]; } } async function loadRunStatus(runId: string): Promise { return api.get(`/api/runs/${runId}`); } async function loadActiveRunData(): Promise<{ runs: RunSummary[]; sessions: SessionSummary[]; status: RunStatus | undefined; }> { const { runs } = await api.get<{ runs: RunSummary[] }>("/api/runs"); if (activeRunId === undefined && runs[0] !== undefined) { activeRunId = runs[0].id; } if (activeRunId === undefined) { activeRunStatus = undefined; activeRunSessions = []; return { runs, sessions: [], status: undefined }; } const [status, sessions] = await Promise.all([ loadRunStatus(activeRunId), loadRunSessions(activeRunId), ]); activeRunStatus = status; activeRunSessions = sessions; return { runs, sessions, status }; } async function loadRunSessions(runId: string): Promise { const response = await api.get<{ sessions: SessionSummary[] }>(`/api/runs/${runId}/sessions`); return response.sessions; } function screenLabel(screen: string): string { if (screen === "new-run") { return "New Run"; } if (screen === "run-detail") { return "Run Detail"; } if (screen === "sessions") { return "TUI Sessions"; } return screen .split("-") .map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`) .join(" "); } function stringFormValue(data: FormData, key: string): string { const value = data.get(key); return typeof value === "string" ? value : ""; } function arrayLength(value: unknown): number { return Array.isArray(value) ? value.length : 0; } function empty(text: string): string { return `
${escapeHtml(text)}
`; } function errorBanner(error: unknown): string { return `
Error${escapeHtml(error instanceof Error ? error.message : String(error))}
`; } function escapeHtml(value: string): string { return value .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); }