/** * Read-only web workspace — ticket #13, lot F2 (first shippable increment). * * The minimal post-pairing surface: list the projects, open one **read-only**, * and show a snapshot of its live/work state. No PTY (xterm over WS = F3), no * mutation — every call goes through the existing transport-neutral gateways * (`project`, `workState`) via DI, so no component touches `@tauri-apps/api`. * * It reuses the frozen read-model types (`ProjectWorkState`) and only calls the * commands B4 puts on the read-only allowlist: `list_projects`, `open_project`, * `get_project_work_state`. A deliberately small surface — the full IDE (layout, * agents, terminals) is out of scope until the streaming lots. */ import { useCallback, useEffect, useState } from "react"; import type { GatewayError, Project, ProjectWorkState } from "@/domain"; import { useGateways } from "@/app/di"; import { Button, Panel, Spinner } from "@/shared"; import { WebAgentCell } from "./WebAgentCell"; function describe(e: unknown): string { if (e && typeof e === "object" && "message" in e) { return String((e as GatewayError).message); } return String(e); } export function WebWorkspace() { const { project, workState } = useGateways(); const [projects, setProjects] = useState(null); const [error, setError] = useState(null); const [openId, setOpenId] = useState(null); const [snapshot, setSnapshot] = useState(null); const [loadingSnapshot, setLoadingSnapshot] = useState(false); // The agent currently opened in a live cell (CLI streamed over the WS), if any. const [openAgentId, setOpenAgentId] = useState(null); const openRoot = projects?.find((p) => p.id === openId)?.root ?? null; const refresh = useCallback(async () => { setError(null); try { setProjects(await project.listProjects()); } catch (e) { setError(describe(e)); } }, [project]); useEffect(() => { void refresh(); }, [refresh]); const openReadOnly = useCallback( async (projectId: string) => { setError(null); setLoadingSnapshot(true); setOpenId(projectId); setSnapshot(null); setOpenAgentId(null); try { // Read-only: open resolves the project server-side, then we read the // live/work-state snapshot. No layout/agents/PTY are mounted. await project.openProject(projectId); setSnapshot(await workState.getProjectWorkState(projectId)); } catch (e) { setError(describe(e)); } finally { setLoadingSnapshot(false); } }, [project, workState], ); return (

Projets

{error && (

{error}

)} {projects === null ? ( Chargement des projets… ) : projects.length === 0 ? (

Aucun projet.

) : (
    {projects.map((p) => (
  • ))}
)} {openId && (

État (lecture seule) read-only

{loadingSnapshot && }
{snapshot ? ( ) : loadingSnapshot ? (

Chargement de l'état…

) : (

Aucun état disponible.

)}
)} {openId && openRoot && openAgentId && (

Agent

{/* Re-mount the cell per agent so a switch relaunches/reattaches cleanly. */}
)}
); } /** Pure render of the read-only work-state snapshot + per-agent "open" affordance. */ function WorkStateSnapshot({ snapshot, openAgentId, onOpenAgent, }: { snapshot: ProjectWorkState; openAgentId: string | null; onOpenAgent: (agentId: string) => void; }) { if (snapshot.agents.length === 0) { return

Aucun agent actif.

; } return (
    {snapshot.agents.map((a) => (
  • {a.name} {a.live ? `live · ${a.live.kind}` : "offline"} {a.busy.state === "busy" ? "busy" : "idle"}
  • ))}
); }