Plan blocks in chat: full DAG borderless in the flow, fragment + glow, live popup (wave-4 item 3)

- components/plan-block.tsx: hand-built SVG (user decision) — deterministic
  layered layout (Kahn topo + longest-path depth, rows centered, authoring
  tie-break), bezier edges, the retired panel's status color language.
- PlanBlock renders data-plan-block parts AS CARRIED (frozen snapshots,
  never re-derived): kind full = borderless, viewBox = layout extent so
  nodes hug the column edges, natural height; kind neighborhood = own
  small labeled 'Plan fragment' container with the current node glowing
  (pulsing primary halo).
- PlanPopup: full-screen overlay with the LIVE plan — latest tree from the
  transcript, statuses derived at click time via derivePlanStatuses
  (contracts/plan.ts). Esc / backdrop / X close; drag pan, wheel
  zoom-to-cursor, zoom buttons; fit-floor semantics harvested from the
  retired tree panel (zoom floor = the fit, pan locks at it).
- chat.tsx: data-plan-block case in the part switch + popup mount.
- src/core/ui-message.ts: TeacherUIDataParts gains "plan-block": PlanBlockData
  (additive, type-only — lane touch flagged to master).
- tests/render-plan-block.test.tsx: pure layout (layering, centering,
  dangling-edge tolerance), wrap, frozen-snapshot render, live derivation.
This commit is contained in:
amosblomqvist
2026-09-11 03:26:56 +03:00
parent df58bb87f2
commit bb369a077b
4 changed files with 724 additions and 1 deletions
+16
View File
@@ -21,6 +21,8 @@ import { SubagentSteerCard } from "@/components/artifacts/steer-card";
import { Composer } from "@/components/composer";
import { JobsContext } from "@/components/jobs-context";
import { StatsBar } from "@/components/stats-bar";
import { PlanBlock, PlanPopup } from "@/components/plan-block";
import type { PlanBlockData } from "@/src/core";
import { QuestionDock } from "@/components/question-dock";
import { SidebarToggle } from "@/components/sidebar";
import { SubAgentsPanel } from "@/components/sub-agents-panel";
@@ -177,6 +179,9 @@ export function Chat({
}
}, [chatId, setMessages, sendMessage, status]);
// The live plan popup (wave 4): opened by any transcript plan block.
const [planOpen, setPlanOpen] = useState(false);
// ── Session usage (wave-4 stats bar) ──
// The money channel: the record's turns log, folded client-side by
// core/prices.ts. Seeded once on mount (the page doesn't carry turns),
@@ -389,6 +394,16 @@ export function Chat({
);
case "step-start":
return pi > 0 ? <Separator key={pi} className="my-3" /> : null;
case "data-plan-block":
// The plan in the chat (wave 4): a frozen snapshot
// block — rendered as carried, never re-derived.
return (
<PlanBlock
key={pi}
data={part.data as PlanBlockData}
onOpen={() => setPlanOpen(true)}
/>
);
case "tool-quiz":
case "tool-ask_user_question": {
// Pending/streaming questions live in the dock, not the
@@ -464,6 +479,7 @@ export function Chat({
<TreePanel messages={messages} />
</div>
</div>
{planOpen && <PlanPopup messages={messages} onClose={() => setPlanOpen(false)} />}
</JobsContext.Provider>
</ChatActionsContext.Provider>
);