import { createContext, useContext, type ReactNode } from "react"; import type { Orientation, NodeId, LeafShellSpec, Direction } from "./tree"; import type { PaneId, SshHost, SessionContext } from "../../ipc"; /** * Orchestration context — every piece of shared state and every operation * that a Pane / SplitNode / LeafPane might call. Lives in React context so * descendants can `useOrchestration()` without prop drilling. * * activeLeafId comes in as a plain value (re-derived by App's useState). * React's context is reactive: when the App-level Provider updates the * value, ALL consumers re-render. No Svelte-style props-don't-propagate * trap here. */ export interface Orchestration { // Read-only state activeLeafId: NodeId | null; /** WSL distros enumerated from `wsl.exe -l -q`. PowerShell is a separate * shell kind, not in this list. */ distros: string[]; /** Saved SSH hosts loaded from `hosts.json`. Reactive — changes when the * user edits hosts via {@link openHostManager}. */ hosts: SshHost[]; // Tree mutations split: (leafId: NodeId, orientation: Orientation) => void; close: (leafId: NodeId) => void; /** Change the shell on a leaf (WSL distro / PowerShell / SSH host). * Always forces a respawn — the helper in tree.ts swaps the leaf id so * the renderer remounts XtermPane. */ setShell: (leafId: NodeId, spec: LeafShellSpec) => void; setLabel: (leafId: NodeId, label: string | undefined) => void; toggleBroadcast: (leafId: NodeId) => void; /** Flip the per-pane mcpAllow flag. Default-deny; chip in the pane * toolbar drives this. */ toggleMcpAllow: (leafId: NodeId) => void; // SSH host management openHostManager: () => void; // Per-pane orchestration setActive: (leafId: NodeId) => void; registerPaneId: (leafId: NodeId, paneId: PaneId | null) => void; broadcastFrom: (originLeafId: NodeId, dataB64: string) => void; notify: (message: string) => void; // Drag-header-to-swap. dragSourceId / dragOverId are reactive so leaves // can apply hover/source styling. The lifecycle methods are stable. dragSourceId: NodeId | null; dragOverId: NodeId | null; beginHeaderDrag: (leafId: NodeId) => void; setHeaderDragOver: (leafId: NodeId | null) => void; endHeaderDrag: (commitSwap: boolean) => void; // Per-leaf idle reporting. LeafPanes call reportLeafIdle when their // own quiet-state crosses the threshold; App aggregates so the titlebar // can show an "N idle" count without spamming toast notifications. reportLeafIdle: (leafId: NodeId, idle: boolean) => void; // Multi-window pane transfer --------------------------------------------- /** Pop a pane out of the current workspace into a fresh top-level window. * The PTY stays alive across the move (the new window's XtermPane * adopts the existing PaneId; scrollback ring is replayed). */ moveToNewWindow: (leafId: NodeId) => void; /** * Navigate focus from within a pane's key-handler. XtermPane emits the * intent; LeafPane/App resolve the target leaf and set it active. * * `{ kind: "direction", dir }` — move to the spatial neighbour in that * direction using the same flattenLayout geometry as Ctrl+Shift+Arrow. * `{ kind: "index", n }` — focus the Nth leaf in DFS (walkLeaves) order, * 1-indexed, clamped to the leaf count (so Alt+9 with 3 panes picks pane 3). */ navigateTo: (intent: NavigateIntent) => void; /** Returns a PaneId only for leaves that just arrived via a window * transfer (so LeafPane can pass `existingPaneId` to XtermPane to skip * the spawn). One-shot — App clears the entry once the pane has * registered. */ getInitialPaneIdFor: (leafId: NodeId) => PaneId | undefined; /** cwd -> the newest claude session's current context occupancy, for the * per-pane context-fill indicator. A leaf looks itself up by `leaf.cwd`; * absent for non-claude / unmatched panes. Polled by App. */ paneContext: Map; } /** Discriminated intent emitted by XtermPane's key handler. App resolves * the actual target leaf from the current tree without XtermPane needing * to know anything about layout geometry or leaf ordering. */ export type NavigateIntent = | { kind: "direction"; dir: Direction } | { kind: "index"; n: number }; const OrchestrationContext = createContext(null); export function OrchestrationProvider({ value, children, }: { value: Orchestration; children: ReactNode; }) { return ( {children} ); } export function useOrchestration(): Orchestration { const orch = useContext(OrchestrationContext); if (!orch) { throw new Error( "useOrchestration() must be called inside ", ); } return orch; }