/** * Recursive terminal-layout grid (L4). Renders a {@link LayoutTree} as nested * `Split` / `Grid` containers down to `Leaf` cells, each hosting a * {@link TerminalView} (L3). Provides the spreadsheet-style interactions: * * - **resize**: drag the separator between two split children → recomputes the * two adjacent weights (`resizeAdjacent`, pure) → `mutateLayout` Resize; * - **split**: per-cell buttons split a leaf into rows/columns → Split; * - **merge**: when a split has > 1 child, a cell can collapse its parent split * onto itself (spreadsheet-like cell merge) → Merge. * - **agent**: each leaf cell has a dropdown to pin an agent; persisted via * `setCellAgent` in the layout (#3). * * Pure presentation: all behaviour comes from {@link useLayout}, which speaks to * the {@link LayoutGateway} port — no `invoke()` here. Track *sizing* is the pure * {@link normalizeWeights} function, kept out of the render for testability. */ import { useEffect, useRef, useState } from "react"; import type { Agent } from "@/domain"; import type { LayoutNode } from "@/domain"; import type { ConversationDetails, LiveAgent, OpenTerminalOptions, TerminalHandle, } from "@/ports"; import { MediatedInput, ResumeConversationPopup, TerminalView, useAgentBusy, } from "@/features/terminals"; import { useGateways } from "@/app/di"; import { leaves, normalizeWeights, resizeAdjacent } from "./layout"; import { useLayout, type LayoutViewModel } from "./useLayout"; interface LayoutGridProps { /** Project whose layout to render. */ projectId: string; /** Working directory new terminals open in (the project root). */ cwd: string; /** Active layout id; when provided the grid loads/mutates this layout. */ layoutId?: string; } export function LayoutGrid({ projectId, cwd, layoutId }: LayoutGridProps) { const vm = useLayout(projectId, layoutId); if (!vm.layout) { return (
{vm.error}
) : (Loading layout…
)}{vm.error}
)}