practice: the chain table — every reasoning chain in one selectable overlay (owner-approved plan, 2026-09-29)

- core formatChainLine: names + arrow one-liner, singleton marker, pure
- graph-view Chains button (chrome row, empty-graph hidden) opens the
  overlay: fuzzy search (names/aliases/statements), collapsible chain →
  member → statement rows, birth-session + created columns, no
  understanding-levels column
- pinned Selected section derives the agents' actual input live
  (chainsForSelection over the selection; premise without its
  conclusion shows as the singleton it becomes)
- Generate opens the creation dialog in CUSTOM mode: selection summary
  + dated default title, posts {nodeIds, params, note}; cluster mode
  untouched on /practice
- live-verified on the owner's dev server: 219 chains, derived preview,
  search-never-filters-Selected, dialog handoff (selection pass-through
  bug caught + fixed en route), Esc/backdrop dismiss, zero writes
- practice UI files re-normalized to tabs (my earlier prettier runs had
  drifted them to the 2-space default; whitespace-only)

17 render/unit pins added; gates: tsc 0, 1174/1174, lint 0, build 0
This commit is contained in:
amosblomqvist
2026-09-29 22:41:52 +03:00
parent 06032769d2
commit 155d065fd1
12 changed files with 1845 additions and 961 deletions
+42 -1
View File
@@ -66,13 +66,14 @@
import { useCallback, useDeferredValue, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import Link from "next/link";
import { FileTextIcon, Loader2Icon, SearchIcon, SettingsIcon, UploadIcon, WaypointsIcon } from "lucide-react";
import { FileTextIcon, ListTreeIcon, Loader2Icon, SearchIcon, SettingsIcon, UploadIcon, WaypointsIcon } from "lucide-react";
import { incrementalLayout, type GraphCostSummary, type GraphNode, type GraphSnapshot } from "@/src/core";
import { GraphCanvasRenderer, type GraphRenderSettings } from "./graph-canvas.ts";
import { usePopoverDismiss } from "@/components/popover-dismiss";
import { useActivity } from "@/components/activity-dock";
import { MenuShell } from "@/components/menu-shell.tsx";
import { DeleteNodeDialog } from "@/components/delete-node-dialog.tsx";
import { ChainTableOverlay } from "@/components/chain-table.tsx";
import { Hint } from "@/components/ui/hint";
import {
ARROW_LENGTH_LADDER,
@@ -860,6 +861,7 @@ export function GraphChromeRow({
onImportFiles,
settings,
changeSettings,
onOpenChains,
}: {
isEmpty: boolean;
query: string;
@@ -873,6 +875,9 @@ export function GraphChromeRow({
onImportFiles: (files: FileList | null) => void;
settings: GraphRenderSettings;
changeSettings: (update: (cur: RenderSettings) => RenderSettings) => void;
/** Opens the chain table overlay (the chains-overlay plan,
* 2026-09-29). */
onOpenChains: () => void;
}) {
return (
<>
@@ -902,6 +907,24 @@ export function GraphChromeRow({
)}
</div>
)}
{/* The chain table (chains overlay, owner plan 2026-09-29) — the
graph's free-form practice entry. Own container (36px band),
between the search and Import. */}
{!isEmpty && (
<div className="graph-chrome-fade-in shrink-0 rounded-xl border bg-card/85 px-1 py-[3px] shadow-lg backdrop-blur">
<Hint label="Every reasoning chain, selectable for practice" side="bottom">
<button
type="button"
onClick={onOpenChains}
data-testid="open-chain-table"
className="flex h-7 items-center gap-1.5 rounded-lg px-2.5 text-xs text-muted-foreground transition-colors hover:bg-accent"
>
<ListTreeIcon className="size-3" />
Chains
</button>
</Hint>
</div>
)}
{/* Import notes — its own container (36px, header band); ALWAYS
rendered (the empty-graph ruling above — the entry point). */}
<div className="shrink-0 rounded-xl border bg-card/85 px-1 py-[3px] shadow-lg backdrop-blur">
@@ -1050,6 +1073,9 @@ export function GraphView() {
// Fetch the derived graph. Re-runs when a background pipeline run
// finishes (refreshTick) — its merges just landed in the ledger.
const [refreshTick, setRefreshTick] = useState(0);
// The chain table overlay (chains-overlay plan, 2026-09-29) —
// component-local: a browsing surface, not graph state.
const [chainsOpen, setChainsOpen] = useState(false);
useEffect(() => {
let cancelled = false;
fetch("/api/graph")
@@ -1529,6 +1555,20 @@ export function GraphView() {
}}
/>
{/* The chain table overlay (chains-overlay plan, 2026-09-29):
ABSOLUTE INSIDE the graph area (the session-overlay idiom —
never a body portal) so the app chrome stays sharp and
reachable; the overlay itself is read-only toward the graph
and owns its selection + the custom practice dialog. */}
<ChainTableOverlay
open={chainsOpen && !isEmpty}
nodes={snapshot?.nodes ?? []}
edges={snapshot?.edges ?? []}
sessions={snapshot?.sessions ?? {}}
fileSources={snapshot?.fileSources ?? {}}
onClose={() => setChainsOpen(false)}
/>
{/* Legend + filters: search box and per-state toggle rows. */}
{/* Top-right controls — separate floating containers (owner,
graph-view round 2). Row order (2026-09-17): search,
@@ -1576,6 +1616,7 @@ export function GraphView() {
onImportFiles={onImportFiles}
settings={settings}
changeSettings={changeSettings}
onOpenChains={() => setChainsOpen(true)}
/>
</div>