AkurAI Build
Menu

popagent

public

Latest change baffb3c2ac834de138dc9ac86330273851896d24 - Move task board to root overview by AkurAI Build

import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
  ActionBarPrimitive,
  AssistantRuntimeProvider,
  type DataMessagePartProps,
  ComposerPrimitive,
  MessagePrimitive,
  ThreadPrimitive,
  useAuiState,
} from "@assistant-ui/react";
import { MarkdownTextPrimitive } from "@assistant-ui/react-markdown";
import { AssistantChatTransport, useChatRuntime } from "@assistant-ui/react-ai-sdk";
import type { UIMessage } from "ai";
import {
  Archive,
  ArchiveRestore,
  ArrowUp,
  Bot,
  BookOpen,
  Brain,
  CheckCircle2,
  Copy,
  ChevronDown,
  ChevronRight,
  FolderGit2,
  Hash,
  Clock3,
  Loader2,
  Menu,
  LayoutDashboard,
  MessageSquare,
  MessageSquarePlus,
  MoreVertical,
  Pencil,
  Search,
  RefreshCw,
  Settings2,
  Sparkles,
  Square,
  Trash2,
  ThumbsDown,
  ThumbsUp,
  Wrench,
  X,
  XCircle,
} from "lucide-react";
import remarkGfm from "remark-gfm";
import {
  type AgentActivity,
  type AgentRuntimeSettings,
  type AgentTask,
  type AgentWorkspace,
  type HookAuditRun,
  type HookRunListResponse,
  type ModelCatalogResponse,
  type Session as ApiSession,
  type SessionListResponse,
  type TaskListResponse,
  type WorkspaceListResponse,
  type WorkspaceChatDeleteResponse,
  type SessionSummary,
} from "../api-types";
import { isPlaintextSecretToolName } from "../chat-messages";
import { apiFetch } from "./api";
import { AgentSettingsPage } from "./AgentSettingsPage";
import { BrowserPanel } from "./BrowserPanel";
import { ChannelPage } from "./ChannelPage";
import { DocumentationWorkspace } from "./DocumentationPage";
import { MemoryDrawer } from "./MemoryDrawer";
import { ModelPicker } from "./ModelPicker";
import { filterByQuery, parseToolDisplayMode, TOOL_DISPLAY_KEY, type ToolDisplayMode } from "./preferences";
import { appRoutePath, parseAppRoute, type AppRoute } from "./routes";
import { singleFlight } from "./single-flight";
import { subscribeToasts, toast, type Toast } from "./toast";
import { WorkspaceChatDeleteDialog, WorkspaceDialog, WorkspaceOverview } from "./WorkspaceOverview";
import { useFocusTrap } from "./use-focus-trap";
type Session = ApiSession<UIMessage>;
type AppHistoryState = { settingsReturnTo?: string };
type RouteNavigationOptions = { replace?: boolean; state?: unknown };

const SUGGESTIONS = [
  { label: "What can you do?", prompt: "What can you help me with?" },
  { label: "What time is it?", prompt: "What time is it right now?" },
  { label: "Compare Bun and Node", prompt: "Summarize the pros and cons of Bun vs Node" },
];


type ToolPartProps = {
  toolName?: string;
  args?: unknown;
  result?: unknown;
  status?: { type?: string } | string;
};

function MarkdownText() {
  return <MarkdownTextPrimitive remarkPlugins={[remarkGfm]} className="md-content" />;
}

function jsonPreview(value: unknown) {
  if (value === undefined) return undefined;
  try {
    return JSON.stringify(value, null, 2);
  } catch {
    return String(value);
  }
}
function ToolCall({ mode, ...props }: ToolPartProps & { mode: ToolDisplayMode }) {
  const [open, setOpen] = useState(mode === "expanded");
  const sensitive = isPlaintextSecretToolName(props.toolName);
  const details = [
    ["Arguments", jsonPreview(sensitive ? "[redacted]" : props.args)],
    ["Result", jsonPreview(sensitive ? "[redacted]" : props.result)],
  ].filter((item): item is [string, string] => Boolean(item[1]));
  const status = typeof props.status === "string" ? props.status : props.status?.type;
  const expanded = mode === "expanded" || open;

  return (
    <div className={`my-2 rounded border bg-panel-2 text-xs ${mode === "timeline" ? "border-accent/35 shadow-[inset_0_1px_0_rgba(255,255,255,0.05)]" : "border-line"}`}>
      <button
        type="button"
        onClick={() => setOpen((value) => !value)}
        className="flex w-full items-center gap-2 px-3 py-2 text-left text-ink-dim"
        aria-expanded={expanded}
      >
        <Wrench className="size-3.5 text-accent" />
        <span className="min-w-0 flex-1 truncate"><code className="text-ink">{props.toolName ?? "tool"}</code></span>
        <span className="capitalize">{status ?? "running"}</span>
        {details.length ? expanded ? <ChevronDown className="size-3.5" /> : <ChevronRight className="size-3.5" /> : null}
      </button>
      {expanded && details.length ? (
        <div className="space-y-2 border-t border-line px-3 py-2">
          {details.map(([label, value]) => (
            <div key={label}>
              <div className="mb-1 font-medium text-ink-dim">{label}</div>
              <pre className="max-h-52 overflow-auto whitespace-pre-wrap break-all rounded bg-bg p-2 text-[11px] text-ink">{value}</pre>
            </div>
          ))}
        </div>
      ) : null}
    </div>
  );
}
function AgentActivityPart({ data }: DataMessagePartProps) {
  const activity = data as AgentActivity;
  if (!activity || typeof activity !== "object" || typeof activity.agentName !== "string") return null;
  const tools = Array.isArray(activity.tools) ? activity.tools.filter((tool): tool is string => typeof tool === "string") : [];
  const text = typeof activity.text === "string" ? activity.text : "";
  return (
    <section className="my-3 rounded border border-accent/25 bg-accent-soft/40 p-3" aria-label={`${activity.agentName} activity`}>
      <div className="flex flex-wrap items-center gap-2 text-xs">
        <span className="flex items-center gap-1.5 font-medium text-ink"><Bot className="size-3.5 text-accent" />{activity.agentName}</span>
        <span className="text-ink-dim">iteration {activity.iteration}{activity.maxIterations ? ` of ${activity.maxIterations}` : ""}</span>
        <span className={`ml-auto rounded-sm px-2 py-0.5 ${activity.isFinal ? "bg-emerald-500/15 text-emerald-300" : "bg-accent/15 text-accent"}`}>{activity.isFinal ? "complete" : activity.finishReason}</span>
      </div>
      {tools.length ? <div className="mt-2 flex flex-wrap gap-1">{tools.map((tool) => <code key={tool} className="rounded bg-bg/70 px-1.5 py-0.5 text-[10px] text-ink-muted">{tool}</code>)}</div> : null}
      {text ? <div className="mt-2 max-h-44 overflow-y-auto whitespace-pre-wrap text-xs leading-relaxed text-ink-muted">{text}</div> : null}
    </section>
  );
}


function UserMessage() {
  return (
    <MessagePrimitive.Root className="group flex flex-col items-end py-2.5">
      <div className="user-bubble max-w-[85%] rounded rounded-br-sm border px-4 py-2.5 md:max-w-[75%]">
        <MessagePrimitive.Parts components={{ Text: MarkdownText }} />
      </div>
      <ActionBarPrimitive.Root hideWhenRunning autohide="not-last" className="mt-1 flex gap-1 opacity-0 transition-opacity focus-within:opacity-100 group-hover:opacity-100">
        <ActionBarPrimitive.Copy aria-label="Copy message" className="liquid-control flex size-8 items-center justify-center rounded text-ink-muted hover:text-ink data-[copied]:text-accent">
          <Copy className="size-3.5" />
        </ActionBarPrimitive.Copy>
      </ActionBarPrimitive.Root>
    </MessagePrimitive.Root>
  );
}

function traceIdFrom(metadata: unknown): string | undefined {
  if (!metadata || typeof metadata !== "object" || !("custom" in metadata)) return undefined;
  const custom = metadata.custom;
  if (!custom || typeof custom !== "object" || !("traceId" in custom)) return undefined;
  return typeof custom.traceId === "string" ? custom.traceId : undefined;
}

function makeAssistantMessage(toolMode: ToolDisplayMode) {
  return function AssistantMessage() {
    const traceId = useAuiState((state) => traceIdFrom(state.message.metadata));
    const [feedback, setFeedback] = useState<1 | -1>();
    const rate = async (value: 1 | -1) => {
      if (!traceId || feedback) return;
      const response = await apiFetch("/api/observability/feedback", {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({ traceId, value }),
      });
      if (response.ok) setFeedback(value);
      else toast("Unable to record response feedback");
    };
    return (
      <MessagePrimitive.Root className="flex gap-3.5 py-2.5">
        <div className="assistant-mark mt-1 flex size-7 shrink-0 items-center justify-center rounded border">
          <Bot className="size-4 text-accent" />
        </div>
        <div className="group min-w-0 max-w-[90%] pt-1 md:max-w-[85%]">
          <MessagePrimitive.Parts
            components={{
              Text: MarkdownText,
              data: { by_name: { agentActivity: AgentActivityPart } },
              tools: { Fallback: (props) => <ToolCall {...props} mode={toolMode} /> },
            }}
          />
          <ActionBarPrimitive.Root hideWhenRunning autohide="not-last" className="mt-1 flex gap-1 opacity-0 transition-opacity focus-within:opacity-100 group-hover:opacity-100">
            <ActionBarPrimitive.Copy aria-label="Copy response" className="liquid-control flex size-8 items-center justify-center rounded text-ink-muted hover:text-ink data-[copied]:text-accent">
              <Copy className="size-3.5" />
            </ActionBarPrimitive.Copy>
            <ActionBarPrimitive.Reload aria-label="Regenerate response" className="liquid-control flex size-8 items-center justify-center rounded text-ink-muted hover:text-ink">
              <RefreshCw className="size-3.5" />
            </ActionBarPrimitive.Reload>
            {traceId ? <><button type="button" aria-label="Rate response helpful" aria-pressed={feedback === 1} disabled={feedback !== undefined} onClick={() => void rate(1)} className="liquid-control flex size-8 items-center justify-center rounded text-ink-muted hover:text-ink aria-pressed:text-accent disabled:opacity-60"><ThumbsUp className="size-3.5" /></button><button type="button" aria-label="Rate response unhelpful" aria-pressed={feedback === -1} disabled={feedback !== undefined} onClick={() => void rate(-1)} className="liquid-control flex size-8 items-center justify-center rounded text-ink-muted hover:text-ink aria-pressed:text-red-400 disabled:opacity-60"><ThumbsDown className="size-3.5" /></button><button type="button" aria-label="Copy trace ID" onClick={() => void navigator.clipboard.writeText(traceId)} className="liquid-control rounded px-2 font-mono text-[10px] text-ink-dim hover:text-ink">trace {traceId.slice(0, 8)}</button></> : null}
          </ActionBarPrimitive.Root>
        </div>
      </MessagePrimitive.Root>
    );
  };
}

function Composer() {
  return (
    <ComposerPrimitive.Root className="liquid-composer flex min-h-14 items-end gap-2 rounded border p-2 transition-colors focus-within:border-accent/40">
      <ComposerPrimitive.Input
        placeholder="Message popagent…"
        rows={1}
        className="max-h-40 min-h-11 flex-1 resize-none bg-transparent px-2.5 py-2.5 text-ink outline-none placeholder:text-ink-dim"
      />
      <ThreadPrimitive.If running={false}>
        <ComposerPrimitive.Send aria-label="Send message" className="brand-orb flex size-11 shrink-0 items-center justify-center rounded transition-transform hover:scale-[1.03] disabled:opacity-30">
          <ArrowUp className="size-4" />
        </ComposerPrimitive.Send>
      </ThreadPrimitive.If>
      <ThreadPrimitive.If running>
        <ComposerPrimitive.Cancel aria-label="Stop response" className="liquid-control flex size-11 shrink-0 items-center justify-center rounded text-ink">
          <Square className="size-3.5" />
        </ComposerPrimitive.Cancel>
      </ThreadPrimitive.If>
    </ComposerPrimitive.Root>
  );
}

function EmptyState() {
  return (
    <div className="flex h-full flex-col items-center justify-end gap-5 pb-6 text-center md:pb-12">
      <div className="brand-orb flex size-14 items-center justify-center rounded"><Sparkles className="size-7" /></div>
      <div><h1 className="text-2xl font-semibold tracking-[-0.035em]">What can I help with?</h1><p className="mt-1.5 text-sm text-ink-muted">Choose a model and start building.</p></div>
      <div className="flex max-w-full gap-2 overflow-x-auto px-1 pb-1 md:flex-wrap md:justify-center">
        {SUGGESTIONS.map(({ label, prompt }) => (
          <ThreadPrimitive.Suggestion key={prompt} prompt={prompt} method="replace" autoSend className="liquid-control min-h-11 shrink-0 rounded px-3.5 py-2 text-sm text-ink-muted hover:text-ink">{label}</ThreadPrimitive.Suggestion>
        ))}
      </div>
    </div>
  );
}


function RunningIndicator() {
  return (
    <>
      <ThreadPrimitive.If running><div className="flex items-center gap-2 px-4 py-1 text-xs text-ink-dim md:px-10"><Loader2 className="size-3 animate-spin" />thinking…</div></ThreadPrimitive.If>
      <div aria-live="polite" className="sr-only">
        <ThreadPrimitive.If running>Assistant is responding</ThreadPrimitive.If>
        <ThreadPrimitive.If running={false}>Response complete</ThreadPrimitive.If>
      </div>
    </>
  );
}

function Toasts() {
  const [items, setItems] = useState<Toast[]>([]);
  useEffect(() => subscribeToasts(setItems), []);
  if (!items.length) return null;
  return (
    <div role="status" aria-live="polite" className="fixed inset-x-0 bottom-4 z-50 flex flex-col items-center gap-2 px-4">
      {items.map((item) => (
        <div key={item.id} className="liquid-popover max-w-md rounded border border-red-400/40 px-4 py-2.5 text-sm text-ink shadow-2xl">{item.message}</div>
      ))}
    </div>
  );
}

function formatTokens(tokens: number): string {
  if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(1)}M`;
  if (tokens >= 1_000) return `${Math.round(tokens / 1_000)}K`;
  return String(tokens);
}

function inputTokensFrom(value: unknown): number | undefined {
  if (!value || typeof value !== "object" || !("inputTokens" in value)) return undefined;
  return typeof value.inputTokens === "number" ? value.inputTokens : undefined;
}

function ContextUsage({ limit }: { limit: number }) {
  const measured = useAuiState((state) => {
    for (let index = state.thread.messages.length - 1; index >= 0; index--) {
      const metadata = state.thread.messages[index]?.metadata;
      const direct = metadata && typeof metadata === "object" && "usage" in metadata ? inputTokensFrom(metadata.usage) : undefined;
      const custom = inputTokensFrom(metadata?.custom?.usage);
      if (direct !== undefined || custom !== undefined) return direct ?? custom;
    }
  });
  const textLength = useAuiState((state) => state.thread.messages.reduce((total, message) => total + message.content.reduce((partTotal, part) => partTotal + (part.type === "text" ? part.text.length : 0), 0), 0));
  if (limit <= 0) return null;
  const used = measured ?? Math.ceil(textLength / 4);
  const percent = Math.min((used / limit) * 100, 100);
  return (
    <div className="mt-2" title={`${measured === undefined ? "Estimated " : ""}${used.toLocaleString()} of ${limit.toLocaleString()} context tokens used`}>
      <div className="mb-1 flex justify-between text-[11px] text-ink-dim"><span>Context</span><span>{measured === undefined ? "≈" : ""}{formatTokens(used)} / {formatTokens(limit)}</span></div>
      <div role="progressbar" aria-label="Context window usage" aria-valuemin={0} aria-valuemax={limit} aria-valuenow={Math.min(used, limit)} className="h-1.5 overflow-hidden rounded-sm bg-panel-2"><div className="h-full rounded-sm bg-accent transition-[width] duration-300" style={{ width: `${percent}%` }} /></div>
    </div>
  );
}

function HookTimeline({ runs }: { runs: HookAuditRun[] }) {
  const groups = useMemo(() => {
    const grouped = new Map<string, HookAuditRun[]>();
    for (const run of runs) {
      const key = run.turnId ?? "session";
      grouped.set(key, [...(grouped.get(key) ?? []), run]);
    }
    return [...grouped.entries()];
  }, [runs]);
  const [open, setOpen] = useState(false);
  if (!runs.length) return null;
  return (
    <div className="border-t border-line bg-panel/70 px-4 py-2 md:px-10">
      <div className="mx-auto max-w-3xl">
        <button type="button" onClick={() => setOpen((value) => !value)} className="flex items-center gap-2 text-xs text-ink-dim" aria-expanded={open}>
          <Clock3 className="size-3.5 text-accent" />Hook activity · {groups.length} {groups.length === 1 ? "turn" : "turns"}{open ? <ChevronDown className="size-3" /> : <ChevronRight className="size-3" />}
        </button>
        {open ? <div className="mt-2 max-h-40 space-y-2 overflow-y-auto border-l border-accent/40 pl-3">{groups.map(([turnId, entries], index) => <div key={turnId}><div className="mb-1 text-[10px] uppercase tracking-wide text-ink-dim">{turnId === "session" ? "Session" : `Turn ${index + 1}`}</div>{entries.map((run) => <div key={run.id} className="flex items-start gap-2 py-0.5 text-xs"><span className="mt-0.5">{run.status.startsWith("failed") || run.status === "denied" || run.error ? <XCircle className="size-3 text-red-400" /> : <CheckCircle2 className="size-3 text-emerald-400" />}</span><span className="text-ink">{run.eventName}</span><span className="text-ink-dim">{run.handlerId} · {Math.round(run.durationMs)}ms</span>{run.reason || run.error ? <span className="text-amber-300">{run.reason ?? run.error}</span> : null}</div>)}</div>)}</div> : null}
      </div>
    </div>
  );
}

function ThreadWorkspace({
  session,
  models,
  contextWindows,
  toolMode,
  onSaved,
  onOpenSessions,
  onOpenMemory,
  settingsOpen,
  onToggleSettings,
  onChooseToolMode,
}: {
  session: Session;
  models: string[];
  contextWindows: Record<string, number>;
  toolMode: ToolDisplayMode;
  onSaved: (session: Session) => void;
  onOpenSessions: () => void;
  onOpenMemory: () => void;
  settingsOpen: boolean;
  onToggleSettings: () => void;
  onChooseToolMode: (mode: ToolDisplayMode) => void;
}) {
  const [model, setModel] = useState(session.model);
  const [hookRuns, setHookRuns] = useState<HookAuditRun[]>([]);
  const modelRef = useRef(model);
  modelRef.current = model;
  const loadHooks = useCallback(async () => {
    const response = await apiFetch(`/api/sessions/${session.id}/hooks`);
    if (response.ok) setHookRuns(((await response.json()) as HookRunListResponse).runs);
  }, [session.id]);
  useEffect(() => { void loadHooks(); }, [loadHooks]);
  const save = useCallback(async (messages: UIMessage[]) => {
    let response = await apiFetch(`/api/sessions/${session.id}`, { method: "PUT", headers: { "content-type": "application/json" }, body: JSON.stringify({ model: modelRef.current, messages, revision: session.revision }) });
    if (response.status === 409) {
      const conflict = (await response.json()) as { session?: Session };
      if (conflict.session) response = await apiFetch(`/api/sessions/${session.id}`, { method: "PUT", headers: { "content-type": "application/json" }, body: JSON.stringify({ model: modelRef.current, messages, revision: conflict.session.revision }) });
    }
    if (response.ok) onSaved((await response.json()) as Session);
    else toast("This conversation could not be saved");
    await loadHooks();
  }, [loadHooks, onSaved, session.id, session.revision]);
  const runtime = useChatRuntime({ id: session.id, messages: session.messages, transport: new AssistantChatTransport({
    api: "/api/chat",
    body: () => ({ model: modelRef.current, workspaceId: session.workspaceId }),
    prepareSendMessagesRequest: ({ id, messages }) => ({
      body: { id, model: modelRef.current, workspaceId: session.workspaceId, messages: [messages.at(-1)] },
    }),
  }), onFinish: ({ messages }) => void save(messages) });
  const AssistantMessage = useMemo(() => makeAssistantMessage(toolMode), [toolMode]);
  return (
    <AssistantRuntimeProvider runtime={runtime}>
      <main className="flex min-w-0 flex-1 flex-col">
        <header className="liquid-header flex items-center gap-2 px-2 md:px-3">
          <button type="button" aria-label="Open sessions" onClick={onOpenSessions} className="liquid-control flex size-10 shrink-0 items-center justify-center rounded lg:hidden"><Menu className="size-4" /></button>
          <span className="min-w-0 flex-1 truncate px-1 text-sm font-medium tracking-[-0.01em] lg:hidden">{session.title}</span>
          <div className="ml-auto flex items-center gap-1.5">
            <ModelPicker models={models} value={model} onChange={setModel} />
            <button type="button" aria-label="Open memory" onClick={onOpenMemory} className="liquid-control flex size-10 items-center justify-center rounded text-ink-muted hover:text-ink"><Brain className="size-4" /></button>
            <div className="relative">
              <button type="button" aria-label="Tool display settings" aria-expanded={settingsOpen} onClick={onToggleSettings} className="liquid-control flex size-10 items-center justify-center rounded text-ink-muted hover:text-ink"><Settings2 className="size-4" /></button>
              {settingsOpen ? <div className="liquid-popover absolute right-0 top-12 z-20 w-52 border p-1.5">{(["compact", "expanded", "timeline"] as const).map((mode) => <button key={mode} type="button" onClick={() => onChooseToolMode(mode)} className={`min-h-11 w-full rounded px-3 text-left text-sm capitalize ${toolMode === mode ? "bg-accent-soft text-ink" : "text-ink-muted hover:bg-panel-2 hover:text-ink"}`}>{mode}</button>)}</div> : null}
            </div>
          </div>
        </header>
        <ThreadPrimitive.Root className="flex min-h-0 flex-1 flex-col">
          <ThreadPrimitive.Viewport className="flex-1 overflow-y-auto px-4 md:px-10"><div className="mx-auto h-full max-w-3xl py-5 md:py-8"><ThreadPrimitive.Empty><EmptyState /></ThreadPrimitive.Empty><ThreadPrimitive.Messages components={{ UserMessage, AssistantMessage }} /></div></ThreadPrimitive.Viewport>
          <RunningIndicator />
          <HookTimeline runs={hookRuns} />
          <div className="px-4 pb-[max(1rem,env(safe-area-inset-bottom))] md:px-10"><div className="mx-auto max-w-3xl"><Composer /><ContextUsage limit={contextWindows[model] ?? 0} /></div></div>
        </ThreadPrimitive.Root>
      </main>
      <BrowserPanel threadId={session.id} />
    </AssistantRuntimeProvider>
  );
}



function DeleteDialog({ session, onCancel, onDelete }: { session?: SessionSummary; onCancel: () => void; onDelete: (session: SessionSummary) => void }) {
  const dialogRef = useRef<HTMLDivElement>(null);
  useFocusTrap(dialogRef, Boolean(session));
  useEffect(() => {
    if (!session) return;
    dialogRef.current?.querySelector<HTMLButtonElement>("button")?.focus();
    const close = (event: KeyboardEvent) => {
      if (event.key === "Escape") onCancel();
    };
    document.addEventListener("keydown", close);
    return () => document.removeEventListener("keydown", close);
  }, [onCancel, session]);
  if (!session) return null;
  return <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/65 p-4"><div ref={dialogRef} role="dialog" aria-modal="true" aria-labelledby="delete-title" className="w-full max-w-md rounded border border-line bg-panel p-5 shadow-2xl"><h2 id="delete-title" className="text-lg font-semibold">Delete “{session.title}”?</h2><p className="mt-2 text-sm leading-relaxed text-ink-dim">This permanently deletes the session transcript and its Mastra thread data. Independent long-term memories are retained.</p><div className="mt-5 flex justify-end gap-2"><button type="button" onClick={onCancel} className="min-h-11 rounded border border-line px-4 text-sm">Cancel</button><button type="button" onClick={() => onDelete(session)} className="min-h-11 rounded bg-red-500/90 px-4 text-sm font-medium text-white">Delete</button></div></div></div>;
}

function SessionSidebar({
  mobile,
  inert,
  active,
  channelActive,
  sessions,
  workspaces,
  selectedWorkspaceId,
  query,
  archived,
  menuSessionId,
  onClose,
  onOverview,
  onOpenChannel,
  onSelectWorkspace,
  onCreate,
  onOpenDocumentation,
  onQuery,
  onArchiveView,
  onSelect,
  onToggleMenu,
  onRename,
  onArchive,
  onDelete,
  onOpenSettings,
}: {
  inert?: boolean;
  mobile: boolean;
  active?: Session;
  channelActive: boolean;
  sessions: SessionSummary[];
  workspaces: AgentWorkspace[];
  selectedWorkspaceId?: string;
  query: string;
  archived: boolean;
  menuSessionId?: string;
  onClose: () => void;
  onOverview: () => void;
  onOpenChannel: () => void;
  onSelectWorkspace: (id: string) => void;
  onCreate: () => void;
  onOpenDocumentation: () => void;
  onQuery: (query: string) => void;
  onArchiveView: (archived: boolean) => void;
  onSelect: (id: string) => void;
  onToggleMenu: (id?: string) => void;
  onRename: (session: SessionSummary) => void;
  onArchive: (session: SessionSummary, archived: boolean) => void;
  onDelete: (session: SessionSummary) => void;
  onOpenSettings: () => void;
}) {
  return (
    <aside inert={inert || undefined} role={mobile ? "dialog" : undefined} aria-modal={mobile || undefined} aria-label="Session navigation" className={`${mobile ? "fixed inset-y-0 left-0 z-30 flex w-full max-w-80 lg:hidden" : "hidden w-64 shrink-0 lg:flex"} liquid-sidebar flex-col border-r shadow-2xl lg:shadow-none`}>
      <div className="flex min-h-16 items-center gap-2.5 px-4">
        <div className="brand-orb flex size-8 items-center justify-center rounded"><Bot className="size-4" /></div>
        <span className="font-semibold tracking-[-0.025em]">popagent</span>
        {mobile ? <button autoFocus type="button" aria-label="Close sessions" onClick={onClose} className="liquid-control ml-auto flex size-11 items-center justify-center rounded"><X className="size-4" /></button> : null}
      </div>
      <nav className="px-2 pb-3" aria-label="Workspaces">
        <button type="button" onClick={onOverview} className={`flex min-h-11 w-full items-center gap-2 rounded px-2.5 text-left text-sm ${!selectedWorkspaceId && !channelActive ? "active-session text-ink" : "text-ink-muted hover:bg-panel-2 hover:text-ink"}`}><LayoutDashboard className="size-4 shrink-0 text-accent" />Overview</button>
        <button type="button" onClick={onOpenChannel} className={`mt-1 flex min-h-11 w-full items-center gap-2 rounded px-2.5 text-left text-sm ${channelActive ? "active-session text-ink" : "text-ink-muted hover:bg-panel-2 hover:text-ink"}`}><Hash className="size-4 shrink-0 text-accent" />#general<span className="ml-auto size-2 rounded-full bg-emerald-500" aria-label="Live" /></button>
        <div className="mt-2 px-2 text-[10px] font-medium uppercase tracking-[0.12em] text-ink-dim">Workspaces</div>
        <div className="mt-1 max-h-40 overflow-y-auto">
          {workspaces.map((workspace) => <button key={workspace.id} type="button" onClick={() => onSelectWorkspace(workspace.id)} title={workspace.repositoryPath} className={`flex min-h-11 w-full items-center gap-2 rounded px-2.5 text-left text-sm ${selectedWorkspaceId === workspace.id ? "active-session text-ink" : "text-ink-muted hover:bg-panel-2 hover:text-ink"}`}><FolderGit2 className="size-3.5 shrink-0" /><span className="truncate">{workspace.name}</span></button>)}
        </div>
      </nav>
      <div className="mx-3 grid grid-cols-2 gap-2">
        <button type="button" aria-label="Create new chat" disabled={!selectedWorkspaceId} onClick={onCreate} className="liquid-control flex min-h-11 items-center gap-2 rounded px-3 text-sm text-ink disabled:cursor-not-allowed disabled:opacity-40"><MessageSquarePlus className="size-4 text-accent" />Chat</button>
        <button type="button" disabled={!selectedWorkspaceId} onClick={onOpenDocumentation} className="liquid-control flex min-h-11 items-center gap-2 rounded px-3 text-sm text-ink disabled:cursor-not-allowed disabled:opacity-40"><BookOpen className="size-4 text-accent" />Docs</button>
      </div>
      <div className="px-3 pt-3">
        <label className="liquid-control flex min-h-11 items-center gap-2 rounded px-3"><Search className="size-3.5 text-ink-muted" /><input aria-label="Search chats" value={query} onChange={(event) => onQuery(event.currentTarget.value)} placeholder="Search chats" className="min-w-0 flex-1 bg-transparent text-sm outline-none" /></label>
        <div className="mt-2 grid grid-cols-2 rounded border border-line bg-black/10 p-1 text-xs">
          <button type="button" onClick={() => onArchiveView(false)} className={`min-h-10 rounded transition-colors ${!archived ? "bg-panel-2 text-ink shadow-sm" : "text-ink-muted hover:text-ink"}`}>Active</button>
          <button type="button" onClick={() => onArchiveView(true)} className={`min-h-10 rounded transition-colors ${archived ? "bg-panel-2 text-ink shadow-sm" : "text-ink-muted hover:text-ink"}`}>Archived</button>
        </div>
      </div>
      <div className="mt-3 min-h-0 flex-1 overflow-y-auto px-2">
        <div className="px-2 pb-2 text-[11px] font-medium uppercase tracking-[0.12em] text-ink-dim">{archived ? "Archived" : "Sessions"}</div>
        {sessions.map((session) => (
          <div key={session.id} className={`relative mb-1 flex items-center rounded border transition-colors ${active?.id === session.id ? "active-session border-accent/20" : "border-transparent hover:bg-panel-2"}`}>
            <button aria-label={`Open chat: ${session.title}`} onClick={() => onSelect(session.id)} className={`flex min-h-11 min-w-0 flex-1 items-center gap-2 px-2.5 text-left text-sm ${active?.id === session.id ? "text-ink" : "text-ink-muted"}`}><MessageSquare className="size-3.5 shrink-0" /><span className="truncate">{session.title}</span></button>
            <button type="button" aria-label={`Chat actions: ${session.title}`} aria-expanded={menuSessionId === session.id} onClick={() => onToggleMenu(menuSessionId === session.id ? undefined : session.id)} className="flex size-11 shrink-0 items-center justify-center rounded text-ink-muted hover:bg-panel-2 hover:text-ink"><MoreVertical className="size-4" /></button>
            {menuSessionId === session.id ? (
              <div className="liquid-popover absolute right-1 top-11 z-20 w-40 border p-1.5">
                <button type="button" onClick={() => onRename(session)} className="flex min-h-11 w-full items-center gap-2 rounded px-3 text-sm text-ink-muted hover:bg-panel-2 hover:text-ink"><Pencil className="size-4" />Rename</button>
                <button type="button" onClick={() => onArchive(session, !archived)} className="flex min-h-11 w-full items-center gap-2 rounded px-3 text-sm text-ink-muted hover:bg-panel-2 hover:text-ink">{archived ? <ArchiveRestore className="size-4" /> : <Archive className="size-4" />}{archived ? "Restore" : "Archive"}</button>
                <button type="button" onClick={() => onDelete(session)} className="flex min-h-11 w-full items-center gap-2 rounded px-3 text-sm text-red-300 hover:bg-red-500/10"><Trash2 className="size-4" />Delete</button>
              </div>
            ) : null}
          </div>
        ))}
      </div>
      <button type="button" onClick={onOpenSettings} className="mx-3 mb-3 flex min-h-11 items-center gap-2 rounded px-3 text-sm text-ink-muted transition-colors hover:bg-panel-2 hover:text-ink"><Settings2 className="size-4" />Settings</button>
    </aside>
  );
}

export function Chat() {
  const [pathname, setPathname] = useState(() => window.location.pathname);
  const route = useMemo(() => parseAppRoute(pathname), [pathname]);
  const navigateRoute = useCallback((next: AppRoute, options: RouteNavigationOptions = {}) => {
    const nextPath = appRoutePath(next);
    if (options.replace) window.history.replaceState(options.state ?? null, "", nextPath);
    else window.history.pushState(options.state ?? null, "", nextPath);
    setPathname(nextPath);
  }, []);
  useEffect(() => {
    const syncPath = () => setPathname(window.location.pathname);
    window.addEventListener("popstate", syncPath);
    return () => window.removeEventListener("popstate", syncPath);
  }, []);
  useEffect(() => {
    const canonicalPath = appRoutePath(route);
    if (pathname === canonicalPath) return;
    window.history.replaceState(window.history.state, "", canonicalPath);
    setPathname(canonicalPath);
  }, [pathname, route]);
  const [models, setModels] = useState<string[]>([]);
  const [contextWindows, setContextWindows] = useState<Record<string, number>>({});
  const [workspaces, setWorkspaces] = useState<AgentWorkspace[]>([]);
  const [tasks, setTasks] = useState<AgentTask[]>([]);
  const [taskStepLimit, setTaskStepLimit] = useState<number>();
  const [sessions, setSessions] = useState<SessionSummary[]>([]);
  const [selectedWorkspaceId, setSelectedWorkspaceId] = useState<string>();
  const [active, setActive] = useState<Session>();
  const [query, setQuery] = useState("");
  const [archived, setArchived] = useState(false);
  const [sidebarOpen, setSidebarOpen] = useState(false);
  const [memoryOpen, setMemoryOpen] = useState(false);
  const [settingsOpen, setSettingsOpen] = useState(false);
  const agentSettingsOpen = route.page === "settings";
  const [workspaceDialogOpen, setWorkspaceDialogOpen] = useState(false);
  const [workspaceTarget, setWorkspaceTarget] = useState<AgentWorkspace>();
  const [workspaceChatsDeleteTarget, setWorkspaceChatsDeleteTarget] = useState<AgentWorkspace>();
  const [workspaceChatsDeleting, setWorkspaceChatsDeleting] = useState(false);
  const [workspaceChatsDeleteError, setWorkspaceChatsDeleteError] = useState("");
  const [menuSessionId, setMenuSessionId] = useState<string>();
  const [deleteTarget, setDeleteTarget] = useState<SessionSummary>();
  const sessionButtonRef = useRef<HTMLButtonElement>(null);
  const memoryButtonRef = useRef<HTMLButtonElement>(null);
  const agentSettingsButtonRef = useRef<HTMLElement>(null);
  const restoreSettingsFocusRef = useRef(false);
  const [toolMode, setToolMode] = useState<ToolDisplayMode>(() => typeof localStorage === "undefined" ? "compact" : parseToolDisplayMode(localStorage.getItem(TOOL_DISPLAY_KEY)));
  const selectedWorkspace = workspaces.find((workspace) => workspace.id === selectedWorkspaceId);
  const visibleSessions = useMemo(
    () => filterByQuery(selectedWorkspaceId ? sessions.filter((session) => session.workspaceId === selectedWorkspaceId) : [], query),
    [query, selectedWorkspaceId, sessions],
  );
  const loadSessions = useCallback(async (showArchived: boolean) => {
    const response = await apiFetch(`/api/sessions?archived=${showArchived}`);
    if (!response.ok) throw new Error(`Unable to load sessions: HTTP ${response.status}`);
    setSessions(((await response.json()) as SessionListResponse).sessions);
  }, []);
  const loadWorkspaces = useCallback(async () => {
    const response = await apiFetch("/api/workspaces");
    if (!response.ok) throw new Error(`Unable to load workspaces: HTTP ${response.status}`);
    setWorkspaces(((await response.json()) as WorkspaceListResponse).workspaces);
  }, []);
  const loadTasks = useCallback(async () => {
    const response = await apiFetch("/api/tasks");
    if (!response.ok) throw new Error(`Unable to load tasks: HTTP ${response.status}`);
    setTasks(((await response.json()) as TaskListResponse).tasks);
  }, []);
  const createBackgroundTask = useCallback(async (workspaceId: string, prompt: string): Promise<boolean> => {
    try {
      const response = await apiFetch("/api/tasks", {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({ workspaceId, prompt }),
      });
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      await loadTasks();
      return true;
    } catch {
      toast("Unable to start task");
      return false;
    }
  }, [loadTasks]);
  const cancelBackgroundTask = useCallback(async (task: AgentTask) => {
    try {
      const response = await apiFetch(`/api/tasks/${task.id}/cancel`, { method: "POST" });
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      await loadTasks();
    } catch {
      toast("Unable to cancel task");
    }
  }, [loadTasks]);
  const closeSidebar = useCallback(() => {
    setSidebarOpen(false);
    requestAnimationFrame(() => sessionButtonRef.current?.focus());
  }, []);
  const closeMemory = useCallback(() => {
    setMemoryOpen(false);
    requestAnimationFrame(() => memoryButtonRef.current?.focus());
  }, []);
  const closeAgentSettings = useCallback(() => {
    restoreSettingsFocusRef.current = true;
    const returnTo = (window.history.state as AppHistoryState | null)?.settingsReturnTo;
    if (returnTo) window.history.back();
    else navigateRoute({ page: "overview" }, { replace: true });
  }, [navigateRoute]);
  useEffect(() => {
    if (route.page === "settings" || !restoreSettingsFocusRef.current) return;
    restoreSettingsFocusRef.current = false;
    if (route.page !== "channel") setTimeout(() => {
      if (agentSettingsButtonRef.current?.isConnected) agentSettingsButtonRef.current.focus();
      else sessionButtonRef.current?.focus();
    });
  }, [route.page]);
  const selectSession = useCallback((id: string) => {
    navigateRoute({ page: "chat", sessionId: id });
    setSidebarOpen(false);
    setMenuSessionId(undefined);
  }, [navigateRoute]);
  const openWorkspace = useCallback((id: string) => {
    navigateRoute({ page: "workspace", workspaceId: id });
    setSidebarOpen(false);
    setMenuSessionId(undefined);
  }, [navigateRoute]);
  const createSession = useCallback(singleFlight(async (model: string | undefined, workspaceId: string) => {
    const response = await apiFetch("/api/sessions", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ ...(model ? { model } : {}), workspaceId }),
    });
    if (!response.ok) throw new Error(`Unable to create session: HTTP ${response.status}`);
    const created = (await response.json()) as Session;
    setArchived(false);
    setSessions((current) => [created, ...current.filter((session) => session.id !== created.id)]);
    setSelectedWorkspaceId(workspaceId);
    setActive(created);
    navigateRoute({ page: "chat", sessionId: created.id });
    setSidebarOpen(false);
    return created;
  }), [navigateRoute]);
  const [loadFailed, setLoadFailed] = useState(false);
  const [needsKey, setNeedsKey] = useState(false);
  const initialLoad = useCallback(() => {
    setLoadFailed(false);
    void apiFetch("/api/models")
      .then(async (response) => {
        if (!response.ok) throw new Error(`Unable to load models: HTTP ${response.status}`);
        return response.json() as Promise<ModelCatalogResponse>;
      })
      .then((catalog) => {
        setModels(catalog.models);
        setContextWindows(catalog.contextWindows);
      })
      .catch(() => toast("Unable to load the model catalog"));
    void Promise.all([loadSessions(false), loadWorkspaces()])
      .catch(() => setLoadFailed(true));
    void loadTasks().catch(() => toast("Unable to load task activity"));
    void apiFetch("/api/settings/agent-runtime")
      .then(async (response) => {
        if (!response.ok) throw new Error(`Unable to load task limits: HTTP ${response.status}`);
        setTaskStepLimit(((await response.json()) as AgentRuntimeSettings).supervisorMaxSteps);
      })
      .catch(() => toast("Unable to load task progress limit"));
  }, [loadSessions, loadTasks, loadWorkspaces]);
  useEffect(() => initialLoad(), [initialLoad]);
  useEffect(() => {
    if (selectedWorkspaceId) return;
    const timer = window.setInterval(() => void loadTasks().catch(() => undefined), 2_000);
    return () => window.clearInterval(timer);
  }, [loadTasks, selectedWorkspaceId]);
  useEffect(() => {
    const onUnauthorized = () => setNeedsKey(true);
    window.addEventListener("popagent:unauthorized", onUnauthorized);
    return () => window.removeEventListener("popagent:unauthorized", onUnauthorized);
  }, []);
  useEffect(() => {
    if (route.page === "settings" || needsKey) return;
    setSidebarOpen(false);
    setMenuSessionId(undefined);
    if (route.page === "overview") {
      setSelectedWorkspaceId(undefined);
      setActive(undefined);
      if (archived) {
        setArchived(false);
        void loadSessions(false).catch(() => toast("Unable to load workspace chats"));
      }
      return;
    }
    if (route.page === "workspace" || route.page === "documentation") {
      setSelectedWorkspaceId(route.workspaceId);
      setActive(undefined);
      return;
    }
    if (route.page === "channel") {
      setSelectedWorkspaceId(undefined);
      setActive(undefined);
      return;
    }
    if (active?.id === route.sessionId) return;
    let cancelled = false;
    void apiFetch(`/api/sessions/${route.sessionId}`)
      .then(async (response) => {
        if (cancelled) return;
        if (response.ok) {
          const session = (await response.json()) as Session;
          setActive(session);
          setSelectedWorkspaceId(session.workspaceId);
        } else if (response.status === 404) {
          toast("Chat not found");
          navigateRoute({ page: "overview" }, { replace: true });
        }
      })
      .catch(() => { if (!cancelled) toast("Unable to load chat"); });
    return () => { cancelled = true; };
  }, [active?.id, archived, loadSessions, navigateRoute, needsKey, route]);
  useEffect(() => {
    if (!sidebarOpen) return;
    const close = (event: KeyboardEvent) => {
      if (event.key === "Escape") closeSidebar();
    };
    document.addEventListener("keydown", close);
    return () => document.removeEventListener("keydown", close);
  }, [closeSidebar, sidebarOpen]);
  const handleSaved = useCallback((saved: Session) => { setActive(saved); setSessions((current) => [saved, ...current.filter((session) => session.id !== saved.id)]); }, []);
  const renameSession = async (session: SessionSummary) => {
    setMenuSessionId(undefined);
    const title = prompt("Rename chat", session.title)?.trim();
    if (!title) return;
    const response = await apiFetch(`/api/sessions/${session.id}`, { method: "PATCH", headers: { "content-type": "application/json" }, body: JSON.stringify({ title }) });
    if (response.ok) {
      const updated = (await response.json()) as Session;
      setSessions((current) => current.map((item) => item.id === updated.id ? updated : item));
    }
  };
  const setSessionArchived = async (session: SessionSummary, value: boolean) => {
    const response = await apiFetch(`/api/sessions/${session.id}`, { method: "PATCH", headers: { "content-type": "application/json" }, body: JSON.stringify({ archived: value }) });
    if (response.ok) {
      setSessions((current) => current.filter((item) => item.id !== session.id));
      if (active?.id === session.id) {
        setActive(undefined);
        navigateRoute({ page: "workspace", workspaceId: session.workspaceId });
      }
    }
  };
  const deleteSession = async (session: SessionSummary) => {
    const response = await apiFetch(`/api/sessions/${session.id}`, { method: "DELETE" });
    if (response.ok) {
      setDeleteTarget(undefined);
      setSessions((current) => current.filter((item) => item.id !== session.id));
      if (active?.id === session.id) {
        setActive(undefined);
        navigateRoute({ page: "workspace", workspaceId: session.workspaceId });
      }
    }
  };
  const deleteWorkspaceChats = async () => {
    const workspace = workspaceChatsDeleteTarget;
    if (!workspace) return;
    setWorkspaceChatsDeleting(true);
    setWorkspaceChatsDeleteError("");
    const response = await apiFetch(`/api/workspaces/${workspace.id}/sessions`, { method: "DELETE" });
    if (response.ok) {
      const { deleted } = await response.json() as WorkspaceChatDeleteResponse;
      setSessions((current) => current.filter((session) => session.workspaceId !== workspace.id));
      if (active?.workspaceId === workspace.id) setActive(undefined);
      setWorkspaceChatsDeleteTarget(undefined);
      toast(deleted ? `Deleted ${deleted} workspace chat${deleted === 1 ? "" : "s"}` : "This workspace has no chats");
    } else {
      const body = await response.json().catch(() => ({})) as { error?: string };
      setWorkspaceChatsDeleteError(body.error ?? "Unable to delete workspace chats");
    }
    setWorkspaceChatsDeleting(false);
  };
  const chooseToolMode = (mode: ToolDisplayMode) => {
    setToolMode(mode);
    localStorage.setItem(TOOL_DISPLAY_KEY, mode);
    setSettingsOpen(false);
  };
  const showOverview = () => {
    navigateRoute({ page: "overview" });
    setSelectedWorkspaceId(undefined);
    setActive(undefined);
    setArchived(false);
    setSidebarOpen(false);
    void loadSessions(false).catch(() => toast("Unable to load workspace chats"));
  };
  const openWorkspaceDialog = (workspace?: AgentWorkspace) => {
    setWorkspaceTarget(workspace);
    setWorkspaceDialogOpen(true);
  };
  const handleWorkspaceSaved = (saved?: AgentWorkspace) => {
    const target = workspaceTarget;
    setWorkspaceDialogOpen(false);
    setWorkspaceTarget(undefined);
    if (saved) {
      setWorkspaces((current) => [saved, ...current.filter((workspace) => workspace.id !== saved.id)]);
      if (!target) openWorkspace(saved.id);
    } else if (target) {
      setWorkspaces((current) => current.filter((workspace) => workspace.id !== target.id));
      if (selectedWorkspaceId === target.id) showOverview();
    }
    void loadWorkspaces().catch(() => toast("Unable to refresh workspaces"));
  };
  const sidebarProps = {
    active,
    channelActive: route.page === "channel",
    sessions: visibleSessions,
    workspaces,
    selectedWorkspaceId,
    query,
    archived,
    menuSessionId,
    onClose: closeSidebar,
    onOverview: showOverview,
    onOpenChannel: () => { navigateRoute({ page: "channel", channelId: "general" }); setSidebarOpen(false); },
    onSelectWorkspace: openWorkspace,
    onCreate: () => {
      if (selectedWorkspaceId) {
        void createSession(active?.model, selectedWorkspaceId)
          .catch(() => toast("Unable to create a chat"));
      }
    },
    onOpenDocumentation: () => {
      if (selectedWorkspaceId) navigateRoute({ page: "documentation", workspaceId: selectedWorkspaceId });
      setSidebarOpen(false);
    },
    onQuery: setQuery,
    onArchiveView: (value: boolean) => {
      setArchived(value);
      void loadSessions(value).catch(() => toast("Unable to load workspace chats"));
    },
    onSelect: selectSession,
    onToggleMenu: setMenuSessionId,
    onRename: (session: SessionSummary) => void renameSession(session),
    onArchive: (session: SessionSummary, value: boolean) => void setSessionArchived(session, value),
    onDelete: (session: SessionSummary) => { setMenuSessionId(undefined); setDeleteTarget(session); },
    onOpenSettings: () => {
      agentSettingsButtonRef.current = document.activeElement as HTMLElement;
      setSidebarOpen(false);
      navigateRoute(
        { page: "settings", section: "agents" },
        { state: { settingsReturnTo: appRoutePath(route) } satisfies AppHistoryState },
      );
    },
  };
  if (needsKey) {
    return (
      <div className="app-shell flex h-full items-center justify-center p-6">
        <form
          className="liquid-popover w-full max-w-sm rounded border p-6"
          onSubmit={(event) => {
            event.preventDefault();
            const key = new FormData(event.currentTarget).get("key");
            if (typeof key === "string" && key.trim()) {
              localStorage.setItem("popagent:api-key", key.trim());
              setNeedsKey(false);
              initialLoad();
            }
          }}
        >
          <h1 className="text-lg font-semibold">Access key required</h1>
          <p className="mt-1 text-sm text-ink-muted">Enter the access key configured on this server.</p>
          <input name="key" type="password" autoFocus aria-label="Access key" className="liquid-control mt-4 block min-h-11 w-full rounded px-3 text-ink outline-none" />
          <button type="submit" className="brand-orb mt-4 min-h-11 w-full rounded text-sm font-medium">Unlock</button>
        </form>
      </div>
    );
  }
  if (loadFailed) {
    return (
      <div className="app-shell flex h-full items-center justify-center p-6">
        <div className="liquid-popover w-full max-w-sm rounded border p-6 text-center">
          <h1 className="text-lg font-semibold">Unable to load chats</h1>
          <p className="mt-1 text-sm text-ink-muted">The server did not respond. Check that popagent is running.</p>
          <button type="button" onClick={initialLoad} className="brand-orb mt-4 min-h-11 w-full rounded text-sm font-medium">Retry</button>
        </div>
      </div>
    );
  }
  return (
    <div className="app-shell flex h-full overflow-hidden">
      <Toasts />
      <SessionSidebar inert={sidebarOpen || memoryOpen || agentSettingsOpen || workspaceDialogOpen || Boolean(workspaceChatsDeleteTarget)} mobile={false} {...sidebarProps} />
      {sidebarOpen ? (
        <>
          <button type="button" tabIndex={-1} aria-hidden="true" aria-label="Close session navigation" onClick={closeSidebar} className="fixed inset-0 z-20 bg-black/70 lg:hidden" />
          <SessionSidebar mobile {...sidebarProps} />
        </>
      ) : null}
      <div inert={sidebarOpen || memoryOpen || agentSettingsOpen || workspaceDialogOpen || Boolean(workspaceChatsDeleteTarget) || undefined} className="relative flex min-w-0 flex-1">
        {route.page === "channel" ? (
          <ChannelPage
            channelId={route.channelId}
            workspaces={workspaces}
            focusSettingsOnMount={restoreSettingsFocusRef.current}
            onOpenNavigation={() => { sessionButtonRef.current = document.activeElement as HTMLButtonElement; setSidebarOpen(true); }}
            onOpenSettings={() => {
              agentSettingsButtonRef.current = document.activeElement as HTMLElement;
              navigateRoute({ page: "settings", section: "channels" }, { state: { settingsReturnTo: appRoutePath(route) } satisfies AppHistoryState });
            }}
          />
        ) : !selectedWorkspaceId ? (
          <WorkspaceOverview
            workspaces={workspaces}
            sessions={sessions}
            tasks={tasks}
            taskStepLimit={taskStepLimit}
            onCreateTask={createBackgroundTask}
            onCancelTask={(task) => void cancelBackgroundTask(task)}
            onOpen={openWorkspace}
            onManage={openWorkspaceDialog}
            onOpenNavigation={() => { sessionButtonRef.current = document.activeElement as HTMLButtonElement; setSidebarOpen(true); }}
          />
        ) : route.page === "documentation" && selectedWorkspace ? (
          <DocumentationWorkspace
            workspace={selectedWorkspace}
            path={route.path}
            onOpen={(path, workspaceId) => navigateRoute({ page: "documentation", workspaceId: workspaceId ?? selectedWorkspace.id, ...(path ? { path } : {}) })}
            onOpenNavigation={() => { sessionButtonRef.current = document.activeElement as HTMLButtonElement; setSidebarOpen(true); }}
          />
        ) : active ? (
          <ThreadWorkspace
            key={active.id}
            session={active}
            models={models}
            contextWindows={contextWindows}
            toolMode={toolMode}
            onSaved={handleSaved}
            onOpenSessions={() => { sessionButtonRef.current = document.activeElement as HTMLButtonElement; setSidebarOpen(true); }}
            onOpenMemory={() => { memoryButtonRef.current = document.activeElement as HTMLButtonElement; setMemoryOpen(true); }}
            settingsOpen={settingsOpen}
            onToggleSettings={() => setSettingsOpen((value) => !value)}
            onChooseToolMode={chooseToolMode}
          />
        ) : (
          <main className="flex min-w-0 flex-1 flex-col">
            <header className="liquid-header flex items-center gap-2 px-2 md:px-3">
              <button type="button" aria-label="Open workspace navigation" onClick={() => { sessionButtonRef.current = document.activeElement as HTMLButtonElement; setSidebarOpen(true); }} className="liquid-control flex size-10 items-center justify-center rounded lg:hidden"><Menu className="size-4" /></button>
              <span className="min-w-0 flex-1 truncate text-sm font-medium">{selectedWorkspace?.name ?? "Workspace"}</span>
            </header>
            <div className="flex flex-1 items-center justify-center p-6 text-center">
              <div className="max-w-md">
                <div className="brand-orb mx-auto flex size-14 items-center justify-center rounded"><FolderGit2 className="size-7" /></div>
                <h1 className="mt-4 text-2xl font-semibold tracking-[-0.035em]">{selectedWorkspace?.name ?? "Workspace"}</h1>
                <code className="mt-2 block truncate text-xs text-ink-dim">{selectedWorkspace?.repositoryPath}</code>
                <p className="mt-3 text-sm text-ink-muted">Choose a conversation, edit the project documentation, or start a new chat in this repository.</p>
                <div className="mt-5 flex flex-wrap justify-center gap-2"><button type="button" onClick={() => void createSession(undefined, selectedWorkspaceId).catch(() => toast("Unable to create a chat"))} className="brand-orb min-h-11 rounded px-5 text-sm font-medium"><MessageSquarePlus className="mr-2 inline size-4" />New chat</button><button type="button" onClick={() => navigateRoute({ page: "documentation", workspaceId: selectedWorkspaceId })} className="liquid-control min-h-11 rounded px-5 text-sm"><BookOpen className="mr-2 inline size-4" />Documentation</button><button type="button" onClick={() => { if (selectedWorkspace) { setWorkspaceChatsDeleteError(""); setWorkspaceChatsDeleteTarget(selectedWorkspace); } }} className="min-h-11 rounded border border-red-500/30 px-5 text-sm text-red-300 hover:bg-red-500/10"><Trash2 className="mr-2 inline size-4" />Delete all chats</button></div>
              </div>
            </div>
          </main>
        )}
      </div>
      {memoryOpen ? <button type="button" tabIndex={-1} aria-hidden="true" aria-label="Close memory overlay" onClick={closeMemory} className="fixed inset-0 z-30 bg-black/70" /> : null}
      <MemoryDrawer open={memoryOpen} onClose={closeMemory} />
      <DeleteDialog session={deleteTarget} onCancel={() => setDeleteTarget(undefined)} onDelete={(session) => void deleteSession(session)} />
      <AgentSettingsPage
        open={agentSettingsOpen}
        section={route.page === "settings" ? route.section : "agents"}
        onSectionChange={(section) => navigateRoute(
          { page: "settings", section },
          { replace: true, state: window.history.state },
        )}
        onClose={closeAgentSettings}
        workspaces={workspaces}
        workspaceId={selectedWorkspaceId}
      />
      <WorkspaceDialog open={workspaceDialogOpen} workspace={workspaceTarget} onClose={() => { setWorkspaceDialogOpen(false); setWorkspaceTarget(undefined); }} onSaved={handleWorkspaceSaved} />
      <WorkspaceChatDeleteDialog workspace={workspaceChatsDeleteTarget} deleting={workspaceChatsDeleting} error={workspaceChatsDeleteError} onCancel={() => { setWorkspaceChatsDeleteTarget(undefined); setWorkspaceChatsDeleteError(""); }} onDelete={() => void deleteWorkspaceChats()} />
    </div>
  );
}