AkurAI Build
Menu

popagent

public

Latest change 74d33974c7d2e99d005596af3bce84f97c7b5f56 - Fix autonomous scheduling and runtime panel behavior by AkurAI Build

import { useEffect, useRef, useState } from "react";
import { Activity, AlertTriangle, Bot, CheckCircle2, CircleDashed, FolderGit2, Hammer, LoaderCircle, Menu, MessageSquare, Plus, Sparkles, Trash2, X, Zap } from "lucide-react";
import type { AgentTask, AgentWorkspace, AutomationStatus, ModelProfile, SessionSummary } from "../api-types";
import { apiFetch } from "./api";
import { useFocusTrap } from "./use-focus-trap";
import { TaskKanbanBoard } from "./TaskKanban";

type WorkspaceOverviewProps = {
  workspaces: AgentWorkspace[];
  sessions: SessionSummary[];
  tasks: AgentTask[];
  models?: string[];
  modelProfiles?: Record<string, ModelProfile>;
  taskStepLimit?: number;
  onCreateTask: (workspaceId: string, prompt: string, model: string) => Promise<boolean>;
  onCancelTask: (task: AgentTask) => void;
  onRemoveTask: (task: AgentTask) => Promise<boolean>;
  onOpen: (workspaceId: string) => void;
  onManage: (workspace?: AgentWorkspace) => void;
  onOpenNavigation: () => void;
};

function latestActivity(workspace: AgentWorkspace, sessions: SessionSummary[], tasks: AgentTask[]): string {
  const timestamps = [
    workspace.updatedAt,
    ...sessions.filter((session) => session.workspaceId === workspace.id).map((session) => session.updatedAt),
    ...tasks.filter((task) => task.workspaceId === workspace.id).map((task) => task.completedAt ?? task.startedAt ?? task.createdAt),
  ].filter(Boolean).map((value) => new Date(value).getTime());
  const latest = Math.max(...timestamps);
  return Number.isFinite(latest) ? new Date(latest).toLocaleString() : "No activity";
}

function TaskCreateDialog({
  open,
  workspaces,
  workspaceId,
  models,
  profiles,
  model,
  prompt,
  creating,
  onWorkspaceChange,
  onPromptChange,
  onModelChange,
  onCancel,
  onSubmit,
}: {
  open: boolean;
  models: string[];
  profiles: Record<string, ModelProfile>;
  model: string;
  workspaces: AgentWorkspace[];
  workspaceId: string;
  prompt: string;
  creating: boolean;
  onWorkspaceChange: (workspaceId: string) => void;
  onPromptChange: (prompt: string) => void;
  onModelChange: (model: string) => void;
  onCancel: () => void;
  onSubmit: () => void;
}) {
  const dialogRef = useRef<HTMLDivElement>(null);
  useFocusTrap(dialogRef, open);
  useEffect(() => {
    if (!open) return;
    dialogRef.current?.querySelector<HTMLSelectElement>("select")?.focus();
    const close = (event: KeyboardEvent) => {
      if (event.key === "Escape" && !creating) onCancel();
    };
    document.addEventListener("keydown", close);
    return () => document.removeEventListener("keydown", close);
  }, [creating, onCancel, open]);
  if (!open) return null;
  return <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 p-4">
    <div ref={dialogRef} role="dialog" aria-modal="true" aria-labelledby="start-task-title" className="w-full max-w-xl rounded-xl border border-line bg-bg p-5 shadow-2xl md:p-6">
      <div className="flex items-start justify-between gap-4">
        <div><h2 id="start-task-title" className="text-xl font-semibold tracking-[-0.03em]">Start agent work</h2><p className="mt-1 text-sm leading-relaxed text-ink-muted">Fusion sets direction. Popagent delegates repository execution to the local Ornith build crew.</p></div>
        <button type="button" aria-label="Close task dialog" disabled={creating} onClick={onCancel} className="flex size-10 shrink-0 items-center justify-center rounded-md text-ink-dim hover:bg-panel-2 hover:text-ink disabled:opacity-50"><X className="size-4" /></button>
      </div>
      <div className="mt-6 space-y-4">
        <label className="block text-xs font-medium text-ink-dim">Repository workspace
          <select aria-label="Task workspace" value={workspaceId} onChange={(event) => onWorkspaceChange(event.currentTarget.value)} className="mt-1.5 min-h-11 w-full rounded-md border border-line bg-panel px-3 text-sm text-ink outline-none focus:border-accent/50">
            {workspaces.map((workspace) => <option key={workspace.id} value={workspace.id}>{workspace.name}</option>)}
          </select>
        </label>
        <fieldset>
          <legend className="text-xs font-medium text-ink-dim">Execution route</legend>
          <div className="mt-1.5 grid gap-2 sm:grid-cols-3">
            {models.filter((candidate) => profiles[candidate]).map((candidate) => {
              const profile = profiles[candidate];
              if (!profile) return null;
              const Icon = profile.role === "orchestrator" ? Sparkles : profile.role === "fast" ? Zap : Hammer;
              return <button key={candidate} type="button" aria-pressed={candidate === model} onClick={() => onModelChange(candidate)} className={`min-h-28 rounded-lg border p-3 text-left transition-colors ${candidate === model ? "border-accent/50 bg-accent-soft" : "border-line bg-panel hover:border-accent/25"}`}>
                <span className="flex items-center gap-2 text-sm font-medium text-ink"><Icon className="size-4 text-accent" />{profile.label}</span>
                <span className="mt-2 block text-[11px] leading-relaxed text-ink-muted">{profile.summary}</span>
              </button>;
            })}
          </div>
        </fieldset>
        <label className="block text-xs font-medium text-ink-dim">Outcome and evidence
          <textarea aria-label="Task prompt" rows={6} value={prompt} onChange={(event) => onPromptChange(event.currentTarget.value)} placeholder="What should change, and what proof should the agents return?" className="mt-1.5 block w-full resize-y rounded-md border border-line bg-panel p-3 text-sm leading-relaxed text-ink outline-none focus:border-accent/50" />
        </label>
      </div>
      <div className="mt-6 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
        <button type="button" disabled={creating} onClick={onCancel} className="min-h-11 rounded-md border border-line px-4 text-sm text-ink-muted hover:bg-panel-2 hover:text-ink disabled:opacity-50">Cancel</button>
        <button type="button" disabled={creating || !workspaceId || !prompt.trim()} onClick={onSubmit} className="min-h-11 rounded-md bg-accent px-5 text-sm font-medium text-white disabled:opacity-50">{creating ? "Starting…" : "Start task"}</button>
      </div>
    </div>
  </div>;
}

export function TaskRemoveDialog({ task, removing, onCancel, onRemove }: { task?: AgentTask; removing: boolean; onCancel: () => void; onRemove: () => void }) {
  const dialogRef = useRef<HTMLDivElement>(null);
  useFocusTrap(dialogRef, Boolean(task));
  useEffect(() => {
    if (!task) return;
    dialogRef.current?.querySelector<HTMLButtonElement>("button")?.focus();
    const close = (event: KeyboardEvent) => {
      if (event.key === "Escape" && !removing) onCancel();
    };
    document.addEventListener("keydown", close);
    return () => document.removeEventListener("keydown", close);
  }, [onCancel, removing, task]);
  if (!task) return null;
  return <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 p-4">
    <div ref={dialogRef} role="alertdialog" aria-modal="true" aria-labelledby="remove-task-title" className="w-full max-w-md rounded-xl border border-line bg-bg p-5 shadow-2xl">
      <h2 id="remove-task-title" className="text-lg font-semibold">Remove task from board?</h2>
      <p className="mt-2 text-sm leading-relaxed text-ink-muted">This permanently removes the task history and evidence from Popagent. It does not undo repository changes.</p>
      <p className="mt-3 line-clamp-3 rounded-md bg-panel p-3 text-xs text-ink-muted">{task.prompt}</p>
      <div className="mt-5 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
        <button type="button" disabled={removing} onClick={onCancel} className="min-h-11 rounded-md border border-line px-4 text-sm text-ink-muted disabled:opacity-50">Keep task</button>
        <button type="button" disabled={removing} onClick={onRemove} className="min-h-11 rounded-md bg-red-500 px-4 text-sm font-medium text-white disabled:opacity-50">{removing ? "Removing…" : "Remove task"}</button>
      </div>
    </div>
  </div>;
}

export function WorkspaceOverview({
  workspaces,
  sessions,
  tasks,
  models = [],
  modelProfiles = {},
  taskStepLimit,
  onCreateTask,
  onCancelTask,
  onRemoveTask,
  onOpen,
  onManage,
  onOpenNavigation,
}: WorkspaceOverviewProps) {
  const startTaskButtonRef = useRef<HTMLButtonElement>(null);
  const [taskDialogOpen, setTaskDialogOpen] = useState(false);
  const [taskWorkspaceId, setTaskWorkspaceId] = useState(workspaces[0]?.id ?? "");
  const [taskPrompt, setTaskPrompt] = useState("");
  const [taskModel, setTaskModel] = useState(models.includes("best-orchestrator") ? "best-orchestrator" : models[0] ?? "");
  const [creatingTask, setCreatingTask] = useState(false);
  const [removeTarget, setRemoveTarget] = useState<AgentTask>();
  const [removingTask, setRemovingTask] = useState(false);
  const [automation, setAutomation] = useState<AutomationStatus>();
  useEffect(() => {
    const controller = new AbortController();
    const load = async () => {
      const response = await apiFetch("/api/autonomy/status", { signal: controller.signal });
      if (response.ok) setAutomation(await response.json() as AutomationStatus);
    };
    void load().catch(() => undefined);
    const timer = setInterval(() => void load().catch(() => undefined), 2_000);
    return () => { controller.abort(); clearInterval(timer); };
  }, []);
  useEffect(() => {
    if (!workspaces.some((workspace) => workspace.id === taskWorkspaceId)) setTaskWorkspaceId(workspaces[0]?.id ?? "");
  }, [taskWorkspaceId, workspaces]);
  useEffect(() => {
    if (!models.includes(taskModel)) setTaskModel(models.includes("best-orchestrator") ? "best-orchestrator" : models[0] ?? "");
  }, [models, taskModel]);
  const workspaceNames = Object.fromEntries(workspaces.map((workspace) => [workspace.id, workspace.name]));
  const statusCounts = {
    queued: tasks.filter((task) => task.status === "queued").length,
    running: tasks.filter((task) => task.status === "running" || task.status === "cancelling").length,
    attention: tasks.filter((task) => task.status === "failed" || task.status === "cancelled" || task.status === "dead-letter").length,
    completed: tasks.filter((task) => task.status === "completed").length,
  };
  const statusCards = [
    { label: "Queued", aria: "queued", count: statusCounts.queued, detail: "Waiting to start", icon: CircleDashed, color: "text-sky-400" },
    { label: "Running", aria: "running", count: statusCounts.running, detail: "Agents working now", icon: LoaderCircle, color: "text-accent" },
    { label: "Needs attention", aria: "needing attention", count: statusCounts.attention, detail: "Failed or stopped", icon: AlertTriangle, color: "text-amber-400" },
    { label: "Completed", aria: "completed", count: statusCounts.completed, detail: "Finished tasks", icon: CheckCircle2, color: "text-emerald-500" },
  ];
  const closeTaskDialog = () => {
    if (creatingTask) return;
    setTaskDialogOpen(false);
    requestAnimationFrame(() => startTaskButtonRef.current?.focus());
  };
  const submitTask = async () => {
    if (!taskWorkspaceId || !taskPrompt.trim() || !taskModel || creatingTask) return;
    setCreatingTask(true);
    try {
      if (await onCreateTask(taskWorkspaceId, taskPrompt.trim(), taskModel)) {
        setTaskPrompt("");
        setTaskDialogOpen(false);
        requestAnimationFrame(() => startTaskButtonRef.current?.focus());
      }
    } finally {
      setCreatingTask(false);
    }
  };
  const removeTask = async () => {
    if (!removeTarget || removingTask) return;
    setRemovingTask(true);
    try {
      if (await onRemoveTask(removeTarget)) setRemoveTarget(undefined);
    } finally {
      setRemovingTask(false);
    }
  };
  return (
    <main className="min-w-0 flex-1 overflow-y-auto px-4 py-6 md:px-8 md:py-10">
      <div className="mx-auto max-w-[90rem]">
        <header className="flex flex-wrap items-end justify-between gap-5">
          <div>
            <div className="mb-2 flex items-center gap-2 text-xs font-medium uppercase tracking-[0.14em] text-accent">
              <button type="button" aria-label="Open workspace navigation" onClick={onOpenNavigation} className="liquid-control flex size-10 items-center justify-center rounded lg:hidden"><Menu className="size-4" /></button>
              <Bot className="size-4" />Operations
            </div>
            <h1 className="text-3xl font-semibold tracking-[-0.04em]">Agent command center</h1>
            <p className="mt-2 max-w-2xl text-sm leading-relaxed text-ink-muted">See what is running, resolve work that needs attention, and start agent work in any repository.</p>
          </div>
          <div className="flex w-full flex-col-reverse gap-2 sm:w-auto sm:flex-row">
            <button type="button" onClick={() => onManage()} className="liquid-control flex min-h-11 items-center justify-center gap-2 rounded px-4 text-sm text-ink"><Plus className="size-4 text-accent" />New workspace</button>
            <button ref={startTaskButtonRef} type="button" aria-haspopup="dialog" disabled={!workspaces.length} onClick={() => setTaskDialogOpen(true)} className="flex min-h-11 items-center justify-center gap-2 rounded-md bg-accent px-5 text-sm font-medium text-white disabled:opacity-50"><Plus className="size-4" />Start task</button>
          </div>
        </header>

        {modelProfiles["best-orchestrator"] ? <section aria-label="Agent execution architecture" className="mt-7 overflow-hidden rounded-xl border border-accent/20 bg-panel">
          <div className="grid md:grid-cols-[1fr_auto_1fr]">
            <div className="p-5">
              <div className="flex items-center gap-2 text-xs font-semibold uppercase tracking-[0.12em] text-accent"><Sparkles className="size-4" />Fusion direction</div>
              <h2 className="mt-3 text-lg font-semibold tracking-[-0.02em]">Sol Max × Fable 5</h2>
              <p className="mt-1 max-w-lg text-sm leading-relaxed text-ink-muted">Both reason on important work. OmniRoute reconciles their proposals into one instruction stream.</p>
            </div>
            <div aria-hidden="true" className="hidden items-center px-2 md:flex"><div className="h-px w-12 bg-gradient-to-r from-accent/20 via-accent to-accent/20" /><span className="mx-2 text-[10px] font-semibold uppercase tracking-[0.12em] text-accent">handoff</span><div className="h-px w-12 bg-gradient-to-r from-accent/20 via-accent to-accent/20" /></div>
            <div className="border-t border-line p-5 md:border-l md:border-t-0">
              <div className="flex items-center gap-2 text-xs font-semibold uppercase tracking-[0.12em] text-accent"><Hammer className="size-4" />Local build crew</div>
              <h2 className="mt-3 text-lg font-semibold tracking-[-0.02em]">Research · Implement · Review</h2>
              <p className="mt-1 max-w-lg text-sm leading-relaxed text-ink-muted">Titan Ornith agents inspect repositories, make changes, and verify locally without spending cloud-model quota.</p>
            </div>
          </div>
        </section> : null}

        <section aria-label="Work status" className="mt-7 grid grid-cols-2 gap-3 xl:grid-cols-4">
          {statusCards.map(({ label, aria, count, detail, icon: Icon, color }) => <article key={label} aria-label={`${count} ${count === 1 ? "task" : "tasks"} ${aria}`} className="rounded-xl border border-line bg-panel p-4">
            <div className="flex items-center justify-between gap-3"><span className="text-xs font-medium text-ink-muted">{label}</span><Icon aria-hidden="true" className={`size-4 ${color} ${label === "Running" && count ? "animate-pulse motion-reduce:animate-none" : ""}`} /></div>
            <div className="mt-3 text-2xl font-semibold tabular-nums tracking-[-0.04em]">{count}</div>
            <p className="mt-1 text-[11px] text-ink-dim">{detail}</p>
          </article>)}
        </section>
        {automation ? <section aria-label="Autonomous improvement status" className="mt-4 rounded-xl border border-line bg-panel p-4"><div className="flex flex-wrap items-center justify-between gap-3"><div><p className="text-xs font-medium uppercase tracking-wide text-accent">Autonomous improvement</p><p className="mt-1 text-sm text-ink-muted">{automation.state} · {automation.selectedWorkspaceIds.length} selected {automation.selectedWorkspaceIds.length === 1 ? "repository" : "repositories"}</p></div><span className="text-xs text-ink-dim">Runs continuously</span></div></section> : null}

        <TaskKanbanBoard tasks={tasks} stepLimit={taskStepLimit} workspaceNames={workspaceNames} heading="Live work" onCancel={onCancelTask} onRemove={setRemoveTarget} />

        <section className="mt-10" aria-labelledby="repositories-title">
          <div className="flex items-end justify-between gap-4">
            <div><h2 id="repositories-title" className="text-lg font-semibold tracking-[-0.02em]">Repositories</h2><p className="mt-1 text-xs text-ink-dim">Open a workspace for its chats, documentation, and repository context.</p></div>
            <span className="text-xs tabular-nums text-ink-dim">{workspaces.length} workspace{workspaces.length === 1 ? "" : "s"}</span>
          </div>
          <div className="mt-4 grid gap-4 md:grid-cols-2 xl:grid-cols-3">
            {workspaces.map((workspace) => {
              const workspaceSessions = sessions.filter((session) => session.workspaceId === workspace.id);
              const workspaceTasks = tasks.filter((task) => task.workspaceId === workspace.id);
              const activeTasks = workspaceTasks.filter((task) => task.status === "queued" || task.status === "running" || task.status === "cancelling");
              return (
                <article key={workspace.id} className="group rounded-xl border border-line bg-panel p-5 transition-colors hover:border-accent/30">
                  <div className="flex items-start gap-3">
                    <div className="brand-orb flex size-10 shrink-0 items-center justify-center rounded"><FolderGit2 className="size-5" /></div>
                    <div className="min-w-0 flex-1"><h3 className="truncate font-semibold tracking-[-0.02em]">{workspace.name}</h3><code className="mt-1 block truncate text-xs text-ink-dim" title={workspace.repositoryPath}>{workspace.repositoryPath}</code></div>
                    <button type="button" onClick={() => onManage(workspace)} className="rounded px-2 py-1 text-xs text-ink-dim hover:bg-panel-2 hover:text-ink">Edit</button>
                  </div>
                  <div className="mt-5 grid grid-cols-2 gap-2">
                    <div className="rounded border border-line bg-bg/40 p-3"><div className="flex items-center gap-1.5 text-xs text-ink-dim"><MessageSquare className="size-3.5" />Chats</div><div className="mt-1 text-xl font-semibold">{workspaceSessions.length}</div></div>
                    <div className="rounded border border-line bg-bg/40 p-3"><div className="flex items-center gap-1.5 text-xs text-ink-dim"><Activity className="size-3.5" />Active work</div><div className="mt-1 text-xl font-semibold">{activeTasks.length}</div></div>
                  </div>
                  <div className="mt-4 text-[11px] text-ink-dim">Last activity · {latestActivity(workspace, workspaceSessions, workspaceTasks)}</div>
                  <button type="button" onClick={() => onOpen(workspace.id)} className="mt-4 min-h-11 w-full rounded bg-accent-soft px-4 text-sm font-medium text-ink transition-colors hover:bg-accent/25">Open workspace</button>
                </article>
              );
            })}
          </div>
        </section>
      </div>
      <TaskCreateDialog
        open={taskDialogOpen}
        workspaces={workspaces}
        models={models}
        profiles={modelProfiles}
        model={taskModel}
        workspaceId={taskWorkspaceId}
        prompt={taskPrompt}
        creating={creatingTask}
        onWorkspaceChange={setTaskWorkspaceId}
        onPromptChange={setTaskPrompt}
        onModelChange={setTaskModel}
        onCancel={closeTaskDialog}
        onSubmit={() => void submitTask()}
      />
      <TaskRemoveDialog task={removeTarget} removing={removingTask} onCancel={() => setRemoveTarget(undefined)} onRemove={() => void removeTask()} />
    </main>
  );
}

export function WorkspaceChatDeleteDialog({
  workspace,
  deleting,
  error,
  onCancel,
  onDelete,
}: {
  workspace?: AgentWorkspace;
  deleting: boolean;
  error: string;
  onCancel: () => void;
  onDelete: () => void;
}) {
  const dialogRef = useRef<HTMLDivElement>(null);
  useFocusTrap(dialogRef, Boolean(workspace));
  useEffect(() => {
    if (!workspace) return;
    dialogRef.current?.querySelector<HTMLButtonElement>("button")?.focus();
    const close = (event: KeyboardEvent) => {
      if (event.key === "Escape" && !deleting) onCancel();
    };
    document.addEventListener("keydown", close);
    return () => document.removeEventListener("keydown", close);
  }, [deleting, onCancel, workspace]);
  if (!workspace) return null;
  return <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 p-4">
    <div ref={dialogRef} role="dialog" aria-modal="true" aria-labelledby="workspace-chat-delete-title" className="w-full max-w-md rounded border border-line bg-bg p-5 shadow-2xl">
      <div className="flex items-center gap-2 text-red-300"><Trash2 className="size-5" /><h2 id="workspace-chat-delete-title" className="text-lg font-semibold text-ink">Delete all chats in “{workspace.name}”?</h2></div>
      <p className="mt-3 text-sm leading-relaxed text-ink-dim">This permanently deletes all active and archived chats in this workspace, including their transcripts and Mastra thread data. Independent long-term memories are retained.</p>
      {error ? <p role="alert" className="mt-3 text-sm text-red-300">{error}</p> : null}
      <div className="mt-5 flex justify-end gap-2"><button type="button" disabled={deleting} onClick={onCancel} className="min-h-11 rounded border border-line px-4 text-sm disabled:opacity-50">Cancel</button><button type="button" disabled={deleting} onClick={onDelete} className="min-h-11 rounded bg-red-500/90 px-4 text-sm font-medium text-white disabled:opacity-50">{deleting ? "Deleting…" : "Delete all chats"}</button></div>
    </div>
  </div>;
}

type WorkspaceDialogProps = {
  workspace?: AgentWorkspace;
  open: boolean;
  onClose: () => void;
  onSaved: (workspace?: AgentWorkspace) => void;
};

export function WorkspaceDialog({ workspace, open, onClose, onSaved }: WorkspaceDialogProps) {
  const [name, setName] = useState("");
  const [repositoryPath, setRepositoryPath] = useState("");
  const [saving, setSaving] = useState(false);
  const [error, setError] = useState<string>();
  const dialogRef = useRef<HTMLDivElement>(null);
  useFocusTrap(dialogRef, open);

  useEffect(() => {
    if (!open) return;
    setName(workspace?.name ?? "");
    setRepositoryPath(workspace?.repositoryPath ?? "");
    setError(undefined);
    const close = (event: KeyboardEvent) => {
      if (event.key === "Escape") onClose();
    };
    document.addEventListener("keydown", close);
    return () => document.removeEventListener("keydown", close);
  }, [onClose, open, workspace]);

  if (!open) return null;
  const save = async () => {
    setSaving(true);
    setError(undefined);
    try {
      const response = await apiFetch(workspace ? `/api/workspaces/${workspace.id}` : "/api/workspaces", {
        method: workspace ? "PATCH" : "POST",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({ name, repositoryPath }),
      });
      if (response.ok) {
        onSaved((await response.json()) as AgentWorkspace);
      } else {
        const body = await response.json().catch(() => ({})) as { error?: string };
        setError(body.error ?? `Unable to save workspace: HTTP ${response.status}`);
      }
    } catch {
      setError("Unable to save workspace");
    } finally {
      setSaving(false);
    }
  };
  const remove = async () => {
    if (!workspace || !confirm(`Delete the “${workspace.name}” workspace?`)) return;
    setSaving(true);
    setError(undefined);
    try {
      const response = await apiFetch(`/api/workspaces/${workspace.id}`, { method: "DELETE" });
      if (response.ok) onSaved();
      else {
        const body = await response.json().catch(() => ({})) as { error?: string };
        setError(body.error ?? `Unable to delete workspace: HTTP ${response.status}`);
      }
    } catch {
      setError("Unable to delete workspace");
    } finally {
      setSaving(false);
    }
  };

  return (
    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 p-4">
      <div ref={dialogRef} role="dialog" aria-modal="true" aria-labelledby="workspace-dialog-title" className="w-full max-w-lg rounded border border-line bg-panel p-5 shadow-2xl">
        <div className="flex items-center gap-3">
          <div className="brand-orb flex size-9 items-center justify-center rounded"><FolderGit2 className="size-4" /></div>
          <div className="min-w-0 flex-1"><h2 id="workspace-dialog-title" className="font-semibold">{workspace ? "Edit workspace" : "New workspace"}</h2><p className="text-xs text-ink-dim">Repository paths are relative to the configured workspace root.</p></div>
          <button type="button" aria-label="Close workspace editor" onClick={onClose} className="liquid-control flex size-10 items-center justify-center rounded"><X className="size-4" /></button>
        </div>
        <label className="mt-5 block text-xs font-medium text-ink-dim">Name<input autoFocus value={name} onChange={(event) => setName(event.currentTarget.value)} className="mt-1.5 min-h-11 w-full rounded border border-line bg-bg px-3 text-sm text-ink outline-none focus:border-accent/40" /></label>
        <label className="mt-4 block text-xs font-medium text-ink-dim">Repository path<input value={repositoryPath} onChange={(event) => setRepositoryPath(event.currentTarget.value)} placeholder="repository-name" className="mt-1.5 min-h-11 w-full rounded border border-line bg-bg px-3 font-mono text-sm text-ink outline-none focus:border-accent/40" /></label>
        {error ? <p className="mt-3 text-sm text-red-300">{error}</p> : null}
        <div className="mt-5 flex items-center justify-between gap-3">
          {workspace && workspace.id !== "default" ? <button type="button" disabled={saving} onClick={() => void remove()} className="min-h-11 rounded px-3 text-sm text-red-300 hover:bg-red-500/10 disabled:opacity-50">Delete workspace</button> : <span />}
          <div className="flex gap-2"><button type="button" disabled={saving} onClick={onClose} className="min-h-11 rounded border border-line px-4 text-sm disabled:opacity-50">Cancel</button><button type="button" disabled={saving || !name.trim() || !repositoryPath.trim()} onClick={() => void save()} className="min-h-11 rounded bg-accent px-4 text-sm font-medium text-white disabled:opacity-40">{saving ? "Saving…" : "Save workspace"}</button></div>
        </div>
      </div>
    </div>
  );
}