/**
 * @file Workspace.tsx
 * @description Merged workspace page combining lanes (agent work units) and runs
 * (Claude Code spawns). Displays a horizontal lane strip at the top with counters,
 * the selected lane's pipeline map, and run configuration/console/history below.
 * Runs are tied to lanes: starting a run posts to POST /api/lanes/:id/start.
 * When a cwd isn't owned by any lane, calls POST /api/lanes/ensure first.
 * Polls GET /api/sessions?status=active alongside the run list so Claude Code
 * sessions started outside the dashboard also appear in the active-runs list.
 *
 * @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
 */
/* =============================================================================
 * MODULE_GUIDE — extended in-file reference (comments only; safe to read, never executed)
 * =============================================================================
 * ## Design constraints
 * - Local-first: no telemetry leaves the machine unless the user configures webhooks.
 * - Fail-safe hooks path on the server must never block Claude Code; UI mirrors that
 *   philosophy by degrading gracefully (empty states, stale badges, reconnect loops).
 * - Destructive flows stay behind explicit confirmation modals and server-side gates.
 * - Internationalization: user-visible strings belong in i18n JSON, not literals here.
 * - The console never writes a lane's stage: no POST /api/lanes/:id/stage calls.
 *
 * ## Internal dependencies
 * - `../lib/api` — REST client including lanes and run endpoints.
 * - `../lib/types` — Lane, Lane Counts, run handles, etc.
 * - `../lib/eventBus` — WebSocket subscription.
 * - `../components/lanes/` — LaneCard, PipelineMap, DestructiveLaneModal.
 * - `../components/run/` — RunConsole, RunSetup, RunHistory, ActiveRunsSwitcher.
 *
 * ## Public surface
 * - `Workspace` — merged page; see TSDoc on the symbol for behavior.
 *
 * ============================================================================= */
/* -----------------------------------------------------------------------------
 * EXPORT CATALOG
 * **Workspace**
 *   The main exported page component. Merges lanes and runs into one workspace.
 *
 * ----------------------------------------------------------------------------- */

import { useCallback, useEffect, useState, useSyncExternalStore } from "react";
import { useTranslation } from "react-i18next";
import { Plus } from "lucide-react";
import { api } from "../lib/api";
import type { CwdSuggestion, RunListResponse } from "../lib/api";
import type { Lane, LaneFeature, LaneCounts, ProofFeature, Session, WSMessage } from "../lib/types";
import { eventBus } from "../lib/eventBus";
import { LaneConsolePane } from "../components/run/LaneConsolePane";
import PipelineMap from "../components/lanes/PipelineMap";
import LaneCard from "../components/lanes/LaneCard";
import LaneStripCard from "../components/lanes/LaneStripCard";
import { AddLaneModal } from "../components/lanes/AddLaneModal";
import { readSplitViewState, writeSplitViewState } from "../lib/splitViewStorage";
import type { SplitViewState, SplitLayout } from "../lib/splitViewStorage";

// ── Page ──────────────────────────────────────────────────────────────

function SplitLayoutToggle({
  splitView,
  setLayout,
}: {
  splitView: SplitViewState;
  setLayout: (layout: SplitLayout) => void;
}) {
  const { t: tLanes } = useTranslation("lanes");

  return (
    <div className="flex items-center gap-1.5">
      {([1, 2, 4] as const).map((n) => (
        <button
          key={n}
          type="button"
          aria-pressed={splitView.layout === n}
          onClick={() => setLayout(n)}
          className={`rounded border px-2 py-1 text-xs ${
            splitView.layout === n
              ? "border-accent bg-accent/15 text-accent"
              : "border-border text-fg-secondary hover:border-border-light"
          }`}
        >
          {tLanes("splitView.paneCount", { count: n })}
        </button>
      ))}
    </div>
  );
}

function ConsoleArea({
  lanes,
  selectedLaneId,
  splitView,
  setLayout,
  setPaneLaneId,
  binaryStatus,
  cwdSuggestions,
  activeRuns,
  externalSessions,
  wsConnected,
  defaultCwd,
  onHasActiveRunChange,
  onLaneCreated,
  onLaneIdChange,
  showToggle = true,
}: {
  lanes: Lane[];
  selectedLaneId: number | null;
  splitView: SplitViewState;
  setLayout: (layout: SplitLayout) => void;
  setPaneLaneId: (index: number, id: number) => void;
  binaryStatus: { found: boolean; path: string | null } | null;
  cwdSuggestions: CwdSuggestion[];
  activeRuns: RunListResponse | null;
  externalSessions: Session[];
  wsConnected: boolean;
  defaultCwd: string;
  onHasActiveRunChange: (val: boolean) => void;
  onLaneCreated: (lane: Lane) => void;
  onLaneIdChange: (id: number | null) => void;
  showToggle?: boolean;
}) {
  return (
    <>
      {showToggle && <SplitLayoutToggle splitView={splitView} setLayout={setLayout} />}
      {splitView.layout === 1 ? (
        <LaneConsolePane
          lanes={lanes}
          laneId={selectedLaneId}
          showLaneSelector={false}
          onLaneIdChange={onLaneIdChange}
          onLaneCreated={onLaneCreated}
          binaryStatus={binaryStatus}
          cwdSuggestions={cwdSuggestions}
          activeRuns={activeRuns}
          externalSessions={externalSessions}
          wsConnected={wsConnected}
          defaultCwd={defaultCwd}
          onHasActiveRunChange={onHasActiveRunChange}
        />
      ) : (
        <div
          className={`grid flex-1 min-h-0 gap-3 ${
            splitView.layout === 2 ? "grid-cols-2" : "grid-cols-2 grid-rows-2"
          }`}
        >
          {splitView.paneLaneIds.map((id, i) => (
            <LaneConsolePane
              key={i}
              lanes={lanes}
              laneId={id}
              showLaneSelector
              onLaneIdChange={(newId) => setPaneLaneId(i, newId)}
              onLaneCreated={onLaneCreated}
              binaryStatus={binaryStatus}
              cwdSuggestions={cwdSuggestions}
              activeRuns={activeRuns}
              externalSessions={externalSessions}
              wsConnected={wsConnected}
              defaultCwd={defaultCwd}
            />
          ))}
        </div>
      )}
    </>
  );
}

export function Workspace() {
  const { t: tLanes } = useTranslation("lanes");
  const wsConnected = useSyncExternalStore(eventBus.onConnection, () => eventBus.connected);

  // Lane state
  const [lanes, setLanes] = useState<Lane[]>([]);
  const [counts, setCounts] = useState<LaneCounts>({ total: 0, running: 0, needs_you: 0, dead: 0 });
  const [selectedLaneId, setSelectedLaneId] = useState<number | null>(null);
  const [laneActionError, setLaneActionError] = useState<string | null>(null);
  const [addLaneOpen, setAddLaneOpen] = useState(false);
  const [viewedFeatureSlug, setViewedFeatureSlug] = useState<string | null>(null);
  const [features, setFeatures] = useState<LaneFeature[]>([]);
  const [pipelineTemplates, setPipelineTemplates] = useState<
    { id: string; name: string; nodes: { id: string }[] }[]
  >([]);
  const [viewedFeature, setViewedFeature] = useState<LaneFeature | null>(null);
  const [proofFeatures, setProofFeatures] = useState<ProofFeature[]>([]);

  // Run state kept at page level: shared across every pane, or drives the
  // lane strip itself rather than any one pane's form.
  const [activeRuns, setActiveRuns] = useState<RunListResponse | null>(null);
  // Claude Code sessions running outside the dashboard (a `claude` the user
  // started in a terminal tab). They are not dashboard runs, so the run list
  // would otherwise show nothing while two agents are working.
  const [externalSessions, setExternalSessions] = useState<Session[]>([]);
  const [binaryStatus, setBinaryStatus] = useState<{ found: boolean; path: string | null } | null>(
    null
  );
  const [cwdSuggestions, setCwdSuggestions] = useState<CwdSuggestion[]>([]);
  const [defaultCwd, setDefaultCwd] = useState<string>("");
  const [paneHasActiveRun, setPaneHasActiveRun] = useState(false);

  // Split view state
  const [splitView, setSplitView] = useState<SplitViewState>(() => readSplitViewState());
  // Info block (pickers, LaneCard, PipelineMap, proof gallery) starts shown in
  // single-pane view and collapsed in split view, but the toggle below can
  // override that in either mode — the user decides, not the layout.
  const [infoExpanded, setInfoExpanded] = useState(() => splitView.layout === 1);
  const showInfo = infoExpanded;

  const setLayout = useCallback((layout: SplitLayout) => {
    setSplitView((prev) => {
      const paneLaneIds = Array.from({ length: layout }, (_, i) => prev.paneLaneIds[i] ?? null);
      const next = { layout, paneLaneIds };
      writeSplitViewState(next);
      return next;
    });
  }, []);

  const setPaneLaneId = useCallback((index: number, id: number) => {
    setSplitView((prev) => {
      const paneLaneIds = [...prev.paneLaneIds];
      paneLaneIds[index] = id;
      const next = { ...prev, paneLaneIds };
      writeSplitViewState(next);
      return next;
    });
  }, []);

  // Pre-flight: probe binary + active runs + cwd suggestions + lanes on mount
  const refreshLanes = useCallback(async () => {
    try {
      const r = await api.lanes.list();
      setLanes(r.lanes);
      setCounts(r.counts);
      setSelectedLaneId((cur) =>
        cur !== null && r.lanes.some((l) => l.id === cur) ? cur : (r.lanes[0]?.id ?? null)
      );
    } catch {
      // Silent fail for lanes load
    }
  }, []);

  useEffect(() => {
    api.run
      .binary()
      .then(setBinaryStatus)
      // Fetch failure (server unreachable, proxy misrouted, etc.) isn't proof
      // `claude` is missing from PATH — leave the probe unresolved rather than
      // showing a misleading "claude missing" banner for an unrelated fault.
      .catch(() => undefined);
    api.lanes
      .pipelines()
      .then((r) => setPipelineTemplates(r.pipelines))
      .catch(() => undefined);
    void refreshLanes();
    api.run
      .cwds()
      .then((r) => {
        setCwdSuggestions(r.items);
        // Pre-fill cwd with the user's home directory — a neutral default.
        // Spawning in the dashboard's own cwd would make ad-hoc runs inherit
        // this repo's project context (.claude/agents, skills, rules,
        // CLAUDE.md, .mcp.json), which is almost never what an ad-hoc run
        // wants and can bloat the initial request (issue #202). Fall back to
        // the dashboard cwd when no home suggestion exists. The user can
        // change it; we just don't want an invisible default.
        const home = r.items.find((s) => s.kind === "home");
        const dashboard = r.items.find((s) => s.kind === "dashboard");
        const preferred = home || dashboard;
        if (preferred) {
          setDefaultCwd(preferred.path);
        }
      })
      .catch(() => undefined);

    // Subscribe to lane updates from the event bus
    return eventBus.subscribe((msg: WSMessage) => {
      if (msg.type !== "lane_update") return;
      const payload = msg.data as { lane?: Lane; removed?: number };
      if (payload.removed !== undefined) {
        void refreshLanes();
        return;
      }
      const lane = payload.lane;
      if (!lane) return;
      setLanes((cur) => {
        const i = cur.findIndex((l) => l.id === lane.id);
        if (i === -1) {
          void refreshLanes();
          return cur;
        }
        const next = [...cur];
        next[i] = lane;
        return next;
      });
    });
  }, [refreshLanes]);

  // A reconnect (e.g. the dashboard server restarting) resumes the WS but does
  // not replay missed lane_update diffs, so a lane whose fields changed while
  // disconnected — status, needs_action — would keep showing its pre-restart
  // badges forever with no further server-side change to broadcast. Refetch
  // the full list whenever the socket comes back up.
  useEffect(
    () =>
      eventBus.onConnection((isConnected) => {
        if (isConnected) void refreshLanes();
      }),
    [refreshLanes]
  );

  // Clean up stale pane lane IDs when lanes load
  useEffect(() => {
    if (!lanes.length) return;
    setSplitView((prev) => {
      const paneLaneIds = prev.paneLaneIds.map((id) =>
        id !== null && lanes.some((l) => l.id === id) ? id : null
      );
      if (paneLaneIds.every((id, i) => id === prev.paneLaneIds[i])) return prev;
      const next = { ...prev, paneLaneIds };
      writeSplitViewState(next);
      return next;
    });
  }, [lanes]);

  const refreshList = useCallback(() => {
    api.run
      .list()
      .then(setActiveRuns)
      .catch(() => undefined);
    api.sessions
      .list({ status: "active", limit: 50, sort_by: "updated_at", sort_desc: true })
      .then((r) => setExternalSessions(r.sessions))
      .catch(() => undefined);
  }, []);

  // Background poll so the run list and history reflect external changes
  // (server-boot reconciliation, sibling tabs, direct DB edits) even when
  // no WS event fires. Lighter than typical WS gaps; aggressive enough that
  // status flips appear within seconds without needing a manual refresh.
  useEffect(() => {
    refreshList();
    const tick = setInterval(() => {
      refreshList();
    }, 5000);
    return () => clearInterval(tick);
  }, [refreshList]);

  // Refresh whenever the tab regains focus / visibility - typical when the
  // user comes back from running `claude` in a terminal and wants to see the
  // current state of every run without waiting for the next poll.
  useEffect(() => {
    const onFocus = () => refreshList();
    const onVis = () => {
      if (document.visibilityState === "visible") refreshList();
    };
    window.addEventListener("focus", onFocus);
    document.addEventListener("visibilitychange", onVis);
    return () => {
      window.removeEventListener("focus", onFocus);
      document.removeEventListener("visibilitychange", onVis);
    };
  }, [refreshList]);

  const currentLane = selectedLaneId !== null ? lanes.find((l) => l.id === selectedLaneId) : null;

  // Feature list follows the selected lane, resets the viewer on lane switch.
  useEffect(() => {
    setViewedFeatureSlug(null);
    setViewedFeature(null);
    if (currentLane === null || currentLane === undefined) {
      setFeatures([]);
      return;
    }
    api.lanes.features
      .list(currentLane.id)
      .then((data) => setFeatures(data.features))
      .catch(() => setFeatures([]));
  }, [currentLane?.id]);

  // Fetch the archived snapshot when the picker selects one — read-only, never
  // touches the live lane.
  useEffect(() => {
    if (!currentLane || !viewedFeatureSlug) {
      setViewedFeature(null);
      return;
    }
    let cancelled = false;
    api.lanes.features
      .show(currentLane.id, viewedFeatureSlug)
      .then((data) => {
        if (!cancelled) setViewedFeature(data.feature);
      })
      .catch(() => {
        if (!cancelled) setViewedFeature(null);
      });
    return () => {
      cancelled = true;
    };
  }, [currentLane?.id, viewedFeatureSlug]);

  // Proof gallery follows the selected lane.
  useEffect(() => {
    if (!currentLane) {
      setProofFeatures([]);
      return;
    }
    api.lanes.proof
      .list(currentLane.id)
      .then((data) => setProofFeatures(data.features))
      .catch(() => setProofFeatures([]));
  }, [currentLane?.id, viewedFeatureSlug]);

  // Resolve the active feature slug: either the user-selected one, or the lane's active_feature_id
  const activeFeatureSlug =
    viewedFeatureSlug ??
    (currentLane?.active_feature_id
      ? features.find((f) => f.id === currentLane?.active_feature_id)?.slug
      : null);
  const proofFeature = activeFeatureSlug
    ? proofFeatures.find((f) => f.slug === activeFeatureSlug)
    : null;

  // Viewport locked only when a live run is showing (TerminalView needs locked
  // viewport for chat scrolling). The config-card screen needs normal page flow
  // so the form is fully reachable on short windows.
  const viewportLocked = paneHasActiveRun;

  const handleLaneAction = async (id: number, action: string, body?: Record<string, unknown>) => {
    setLaneActionError(null);
    try {
      if (action === "reset" || action === "remove" || action === "purge") {
        await api.lanes.action(id, action, {
          confirm: true,
          expect: body?.expect,
          ...(body?.force === true ? { force: true } : {}),
        });
      } else {
        await api.lanes.action(id, action, body);
      }
      if (action === "remove") await refreshLanes();
    } catch (err) {
      setLaneActionError(err instanceof Error ? err.message : tLanes("actionErrorUnknown"));
    }
  };

  // Mirrors `ccam lanes pipeline <template> <id>`: same PATCH, same
  // stage-mismatch warning when the current declared stage matches no node
  // in the newly chosen template.
  const handlePipelineChange = async (id: number, pipeline: string) => {
    setLaneActionError(null);
    try {
      const { lane } = await api.lanes.update(id, { pipeline });
      await refreshLanes();
      // `stage` defaults to the DB sentinel "idle" until a driving session ever
      // calls `ccam stage` — that's the normal state for most lanes and never
      // matches a real pipeline node, so it isn't a mismatch worth surfacing.
      if (lane.stage !== "idle" && !lane.pipeline_nodes.some((n) => n.state === "current")) {
        setLaneActionError(
          tLanes("pipelinePicker.stageMismatch", {
            stage: lane.stage,
            pipeline: lane.pipeline,
            nodes: lane.pipeline_nodes.map((n) => n.id).join(", "),
          })
        );
      }
    } catch (err) {
      setLaneActionError(err instanceof Error ? err.message : tLanes("actionErrorUnknown"));
    }
  };

  return (
    <div
      className={
        viewportLocked
          ? "h-[calc(100vh-2.5rem)] lg:h-[calc(100vh-3rem)] flex flex-col gap-5"
          : "space-y-5"
      }
    >
      {/* Page header: what this screen is, and the four numbers that say
          whether anything needs a human right now. */}
      <div className="flex flex-wrap items-center gap-3 border-b border-border pb-3">
        <h2 className="text-base font-semibold tracking-tight text-fg-primary">
          {tLanes("title")}
        </h2>
        <div className="flex flex-wrap items-center gap-1.5 text-xs">
          <span
            data-testid="count-total"
            className="rounded-full bg-surface-2 px-2.5 py-0.5 text-fg-secondary"
          >
            {counts.total} {tLanes("countTotal")}
          </span>
          <span
            data-testid="count-running"
            className="rounded-full bg-blue-600/20 px-2.5 py-0.5 text-blue-400"
          >
            {counts.running} {tLanes("countRunning")}
          </span>
          {counts.needs_you > 0 && (
            <span
              data-testid="count-needs-you"
              className="rounded-full bg-status-warning/20 px-2.5 py-0.5 text-status-warning"
            >
              {counts.needs_you} {tLanes("countNeedsYou")}
            </span>
          )}
          {counts.dead > 0 && (
            <span
              data-testid="count-dead"
              className="rounded-full bg-status-danger/20 px-2.5 py-0.5 text-status-danger"
            >
              {counts.dead} {tLanes("countDead")}
            </span>
          )}
        </div>
        <button
          onClick={() => setAddLaneOpen(true)}
          className="ml-auto flex items-center gap-1.5 rounded border border-border-light px-3 py-1 text-xs text-fg-secondary transition-colors hover:border-border-light hover:text-fg-primary"
          title={tLanes("addLane")}
        >
          <Plus className="h-3.5 w-3.5" />
          {tLanes("add")}
        </button>
      </div>

      {/* Lane strip + detail stack vertically: the strip is a horizontally
          scrolling row (holds any number of lanes), the detail/console area
          below it gets the full width and never competes for it. */}
      <div className="flex min-h-0 flex-1 flex-col gap-4">
        <div data-testid="lane-strip" className="flex shrink-0 flex-row gap-2 overflow-x-auto pb-1">
          {lanes.map((l) => (
            <LaneStripCard
              key={l.id}
              lane={l}
              selected={selectedLaneId === l.id}
              onSelect={() => setSelectedLaneId(l.id)}
            />
          ))}
          {!lanes.length && (
            <p className="text-sm text-fg-muted">
              {tLanes("emptyState")} <code>ccam lanes add --cwd $(pwd)</code>
            </p>
          )}
        </div>

        {/* The selected lane's pipeline — the thing you actually come to this
            page to read, beside the list rather than stacked under it. */}
        {currentLane && (
          <section
            data-testid="lane-detail"
            className="flex min-h-0 min-w-0 flex-1 flex-col card p-4"
          >
            <div className="mb-3 flex flex-wrap items-center justify-between gap-2">
              <span className="truncate text-sm font-semibold text-fg-primary">
                {tLanes("laneHeader", {
                  id: currentLane.id,
                  title: currentLane.title || currentLane.cwd,
                  pipeline: currentLane.pipeline_name,
                })}
              </span>
              <div className="flex items-center gap-2">
                <button
                  type="button"
                  data-testid="lane-detail-toggle"
                  aria-expanded={infoExpanded}
                  onClick={() => setInfoExpanded((v) => !v)}
                  className="rounded border border-border-light px-2 py-1 text-xs text-fg-secondary transition-colors hover:text-fg-primary"
                >
                  {infoExpanded ? tLanes("laneDetail.hide") : tLanes("laneDetail.show")}
                </button>
                <SplitLayoutToggle splitView={splitView} setLayout={setLayout} />
              </div>
            </div>
            {showInfo && (
              <div className="max-h-[45vh] shrink-0 overflow-y-auto">
                <div className="mb-3 flex flex-wrap items-baseline gap-2">
                  <span className="text-[11px] font-semibold uppercase tracking-widest text-fg-muted">
                    {tLanes("cardId", { id: currentLane.id })}
                  </span>
                  <select
                    data-testid="pipeline-picker"
                    aria-label={tLanes("pipelinePicker.label")}
                    className="rounded border border-border bg-surface-1 px-2 py-0.5 text-xs text-fg-secondary disabled:opacity-60"
                    value={currentLane.pipeline}
                    disabled={!!viewedFeature}
                    title={
                      viewedFeature
                        ? tLanes("features.viewingArchived", { slug: viewedFeature.slug })
                        : undefined
                    }
                    onChange={(e) => void handlePipelineChange(currentLane.id, e.target.value)}
                  >
                    {(pipelineTemplates.length
                      ? pipelineTemplates
                      : [{ id: currentLane.pipeline, name: currentLane.pipeline_name, nodes: [] }]
                    ).map((p) => (
                      <option key={p.id} value={p.id}>
                        {p.nodes.length ? `${p.name} (${p.nodes.length})` : p.name}
                      </option>
                    ))}
                  </select>
                  {/* `stage` defaults to the DB sentinel "idle" until the driving
                      session ever calls `ccam stage` — that string collides with
                      `status`'s own "idle"/"running" vocabulary, so a lane that is
                      actively running but has never declared a stage read as if it
                      were sitting idle. Show a distinct label instead of the raw
                      sentinel whenever it doesn't match any node this pipeline
                      actually has. */}
                  <span className="rounded bg-surface-2 px-2 py-0.5 text-xs text-fg-secondary">
                    {currentLane.pipeline_nodes.some((n) => n.id === currentLane.stage)
                      ? currentLane.stage
                      : tLanes("stageUndeclared")}
                  </span>
                  {currentLane.detected_stage && (
                    <span
                      data-testid="detail-auto-stage"
                      title={currentLane.detected_signal || undefined}
                      className="rounded border border-dashed border-status-warning px-2 py-0.5 text-xs text-status-warning"
                    >
                      {tLanes("autoStage", { stage: currentLane.detected_stage })}
                    </span>
                  )}
                  {features.length > 0 && (
                    <select
                      data-testid="feature-picker"
                      className="rounded border border-border bg-surface-1 px-2 py-0.5 text-xs"
                      value={viewedFeatureSlug ?? ""}
                      onChange={(e) => setViewedFeatureSlug(e.target.value || null)}
                    >
                      <option value="">{tLanes("features.live")}</option>
                      {features.map((f) => (
                        <option key={f.slug} value={f.slug}>
                          {f.slug}
                          {f.archived_at ? ` (${tLanes("features.archived")})` : ""}
                        </option>
                      ))}
                    </select>
                  )}
                </div>
                <div className="mb-3">
                  <LaneCard
                    lane={currentLane}
                    onAction={(a, b) => handleLaneAction(currentLane.id, a, b)}
                    childWorktrees={lanes.filter(
                      (l) => l.source_repo === currentLane.cwd && l.id !== currentLane.id
                    )}
                    onSelectLane={setSelectedLaneId}
                  />
                </div>
                <div className="mb-3">
                  <PipelineMap
                    nodes={
                      viewedFeature ? viewedFeature.pipeline_nodes : currentLane.pipeline_nodes
                    }
                    detectedSignal={viewedFeature ? undefined : currentLane.detected_signal}
                  />
                  {viewedFeature && (
                    <p data-testid="feature-viewer-banner" className="mb-2 text-xs text-fg-muted">
                      {tLanes("features.viewingArchived", { slug: viewedFeature.slug })}
                    </p>
                  )}
                </div>
                {proofFeature &&
                  (Object.keys(proofFeature.groups).length > 0 || proofFeature.ticket_report) && (
                    <div data-testid="proof-gallery" className="mt-2">
                      {proofFeature.ticket_report && (
                        <a
                          href={`/api/lanes/${currentLane.id}/proof/${proofFeature.ticket_report}`}
                          target="_blank"
                          rel="noreferrer"
                          className="text-xs text-fg-muted"
                        >
                          {tLanes("proof.ticketReport")}
                        </a>
                      )}
                      {Object.entries(proofFeature.groups).map(([group, images]) => (
                        <div key={group} className="mt-1">
                          <span className="text-xs text-fg-muted">
                            {group} · {images.length}
                          </span>
                          <div className="flex flex-wrap gap-1">
                            {images.slice(0, 8).map((img) => (
                              <img
                                key={img}
                                loading="lazy"
                                className="h-16 w-16 rounded object-cover"
                                src={api.lanes.proof.imageUrl(
                                  currentLane.id,
                                  proofFeature.slug,
                                  group,
                                  img
                                )}
                                alt={img}
                              />
                            ))}
                            {images.length > 8 && (
                              <span className="text-xs text-fg-muted">+{images.length - 8}</span>
                            )}
                          </div>
                        </div>
                      ))}
                    </div>
                  )}
              </div>
            )}
            <div className="flex min-h-0 flex-1 flex-col gap-2 border-t border-border pt-3">
              <ConsoleArea
                lanes={lanes}
                selectedLaneId={selectedLaneId}
                splitView={splitView}
                setLayout={setLayout}
                setPaneLaneId={setPaneLaneId}
                binaryStatus={binaryStatus}
                cwdSuggestions={cwdSuggestions}
                activeRuns={activeRuns}
                externalSessions={externalSessions}
                wsConnected={wsConnected}
                defaultCwd={defaultCwd}
                onHasActiveRunChange={setPaneHasActiveRun}
                onLaneCreated={(lane) =>
                  setLanes((prev) => (prev.some((l) => l.id === lane.id) ? prev : [...prev, lane]))
                }
                onLaneIdChange={setSelectedLaneId}
                showToggle={false}
              />
            </div>
          </section>
        )}

        {!currentLane && (
          <div className="flex min-h-0 flex-1 flex-col gap-2">
            <ConsoleArea
              lanes={lanes}
              selectedLaneId={selectedLaneId}
              splitView={splitView}
              setLayout={setLayout}
              setPaneLaneId={setPaneLaneId}
              binaryStatus={binaryStatus}
              cwdSuggestions={cwdSuggestions}
              activeRuns={activeRuns}
              externalSessions={externalSessions}
              wsConnected={wsConnected}
              defaultCwd={defaultCwd}
              onHasActiveRunChange={setPaneHasActiveRun}
              onLaneCreated={(lane) =>
                setLanes((prev) => (prev.some((l) => l.id === lane.id) ? prev : [...prev, lane]))
              }
              onLaneIdChange={setSelectedLaneId}
            />
          </div>
        )}
      </div>

      <AddLaneModal
        open={addLaneOpen}
        cwdSuggestions={cwdSuggestions}
        onClose={() => setAddLaneOpen(false)}
        onAdded={(lane) => {
          setLanes((prev) => (prev.some((l) => l.id === lane.id) ? prev : [...prev, lane]));
          setSelectedLaneId(lane.id);
          void refreshLanes();
        }}
      />

      {laneActionError && (
        <p
          role="alert"
          className="rounded border border-status-danger bg-status-danger/40 px-3 py-2 text-sm text-status-danger"
        >
          {tLanes("actionError", { message: laneActionError })}
        </p>
      )}
    </div>
  );
}
