ui: plan canvas ink extent + the keyboard-first round
Plan canvas (owner report: arrows clipped at the viewBox edge in dist — measured 14px/20px cuts in the owner's real plans): blockLayout now unions node boxes with every edge path's control-point hull (a bezier never leaves it; the layout calls edgePathD with the renderer's exact inputs). Dagre's node extent stays the floor, so ordinary plans render byte-identical; the viewBox origin shifts only when ink bulges past zero. Verified on both real chats; pins use the owner's real linear-algebra tree (golden-real-data). Keyboard-first (owner's law: Enter always submits, arrows always cycle; text fields the only exception): - Enter guard split (enter-submits): a focused BUTTON keeps native Enter only when keyboard-walked (:focus-visible) or inside an open overlay (menus/dialogs); pointer-clicked transcript buttons submit, preventDefault suppressing the re-activation underneath. - Focus hygiene (focus-hygiene): pointer clicks on transient controls (agent/user thinking toggles, copy, mermaid expand, plan chip) blur to body — the state clicking empty space produces. - Session search owns the walk: Up/Down wrap-cycle the filtered rows, Enter opens the walked row or the first match (quick-open), then blurs and keeps the query. The Alt-arrow session cycle is RETIRED. - Registry: model-menu walk/pick entries removed (obvious in-field navigation, not settings material); question-dock keys stay displayOnly per owner ruling. Gates: 1489/1489, typecheck, lint. Verified live end-to-end.
This commit is contained in:
@@ -2,7 +2,7 @@ import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { PlanMarginalia, planExists } from "../components/plan-window.tsx";
|
||||
import { blockLayout, type PopupMessage } from "../components/plan-block.tsx";
|
||||
import { blockLayout, edgePathD, type LayoutNode, type PopupMessage } from "../components/plan-block.tsx";
|
||||
import type { PlanTree } from "../src/core/contracts.ts";
|
||||
|
||||
// The plan marginalia: the bare diagram in the dock band's right flank —
|
||||
@@ -88,3 +88,82 @@ describe("PlanMarginalia render", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── The canvas must cover the arrows (owner, 2026-10-10) ────────────────────
|
||||
// The owner's REAL linear-algebra plan (chat_mv19m6xe, plan_init's mermaid
|
||||
// parsed to nodes/edges — golden fixtures use real data, owner ruling
|
||||
// 2026-10-10): a cross-column edge's bezier control points reached x=776
|
||||
// while dagre's node-extent width was 754 — 14px of arrow clipped at the
|
||||
// viewBox edge (the Hume plan cut 20px the same way).
|
||||
const LINEAR_ALGEBRA_NODES: LayoutNode[] = [
|
||||
{ id: "vector-space-axioms", title: "What a vector space is" },
|
||||
{ id: "span-basis-dimension", title: "Span, basis, dimension" },
|
||||
{ id: "matrix-as-map", title: "Matrices as maps" },
|
||||
{ id: "elimination-rank", title: "Elimination exposes rank" },
|
||||
{ id: "determinant-volume", title: "Det as volume scaling" },
|
||||
{ id: "orthogonality-complements", title: "Orthogonality and complements" },
|
||||
{ id: "projection-theorem", title: "The projection theorem" },
|
||||
{ id: "transpose-adjoint", title: "The transpose as bridge" },
|
||||
{ id: "normal-equations", title: "Least squares, normal equations" },
|
||||
{ id: "eigen-diagonalization", title: "Eigenvectors as coordinates" },
|
||||
{ id: "spectral-theorem", title: "The spectral theorem" },
|
||||
{ id: "pca-synthesis", title: "Goal: PCA and least-squares derivations" }
|
||||
];
|
||||
const LINEAR_ALGEBRA_EDGES: ReadonlyArray<{ from: string; to: string }> = [
|
||||
{ from: "span-basis-dimension", to: "matrix-as-map" },
|
||||
{ from: "matrix-as-map", to: "elimination-rank" },
|
||||
{ from: "matrix-as-map", to: "determinant-volume" },
|
||||
{ from: "matrix-as-map", to: "orthogonality-complements" },
|
||||
{ from: "span-basis-dimension", to: "orthogonality-complements" },
|
||||
{ from: "orthogonality-complements", to: "projection-theorem" },
|
||||
{ from: "orthogonality-complements", to: "transpose-adjoint" },
|
||||
{ from: "matrix-as-map", to: "transpose-adjoint" },
|
||||
{ from: "projection-theorem", to: "normal-equations" },
|
||||
{ from: "transpose-adjoint", to: "normal-equations" },
|
||||
{ from: "matrix-as-map", to: "normal-equations" },
|
||||
{ from: "determinant-volume", to: "eigen-diagonalization" },
|
||||
{ from: "matrix-as-map", to: "eigen-diagonalization" },
|
||||
{ from: "span-basis-dimension", to: "eigen-diagonalization" },
|
||||
{ from: "eigen-diagonalization", to: "spectral-theorem" },
|
||||
{ from: "transpose-adjoint", to: "spectral-theorem" },
|
||||
{ from: "orthogonality-complements", to: "spectral-theorem" },
|
||||
{ from: "spectral-theorem", to: "pca-synthesis" },
|
||||
{ from: "normal-equations", to: "pca-synthesis" }
|
||||
];
|
||||
|
||||
describe("blockLayout ink extent", () => {
|
||||
it("covers every edge path's control points — arrows never clip (owner, 2026-10-10)", () => {
|
||||
const L = blockLayout(LINEAR_ALGEBRA_NODES, LINEAR_ALGEBRA_EDGES);
|
||||
// The viewBox extent must reach the furthest control point of
|
||||
// every drawn edge (a bezier never leaves its control-point hull).
|
||||
let ctrlMaxX = -Infinity;
|
||||
for (const e of LINEAR_ALGEBRA_EDGES) {
|
||||
const pts = L.edgePts.get(`${e.from}->${e.to}`);
|
||||
if (!pts) continue;
|
||||
const target = {
|
||||
x: L.x.get(e.to)!,
|
||||
y: L.y.get(e.to)!,
|
||||
w: L.nodeW.get(e.to)!,
|
||||
h: L.nodeH.get(e.to)!,
|
||||
};
|
||||
const nums = edgePathD(pts, target).match(/-?\d+(?:\.\d+)?/g) ?? [];
|
||||
for (let i = 0; i + 1 < nums.length; i += 2)
|
||||
ctrlMaxX = Math.max(ctrlMaxX, parseFloat(nums[i]!));
|
||||
}
|
||||
assert.ok(ctrlMaxX > -Infinity, "the fixture has edges");
|
||||
assert.ok(
|
||||
ctrlMaxX <= (L.originX ?? 0) + L.width + 0.5,
|
||||
`control point ${ctrlMaxX} exceeds the canvas right edge ${(L.originX ?? 0) + L.width}`,
|
||||
);
|
||||
});
|
||||
|
||||
it("the real plan's overhang actually EXTENDS the canvas past the node extent", () => {
|
||||
const L = blockLayout(LINEAR_ALGEBRA_NODES, LINEAR_ALGEBRA_EDGES);
|
||||
let nodeMaxX = -Infinity;
|
||||
for (const n of LINEAR_ALGEBRA_NODES)
|
||||
nodeMaxX = Math.max(nodeMaxX, (L.x.get(n.id) ?? 0) + (L.nodeW.get(n.id) ?? 0));
|
||||
// Pre-fix dagre width was 754 with ink at 776 — width must stay ≥
|
||||
// ink, which here strictly exceeds the node extent.
|
||||
assert.ok(L.width > nodeMaxX + 1, `layout width ${L.width} should exceed node extent ${nodeMaxX}`);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user