From c79b0d4e6e3d1d17f1a1f8d296d01e85ff1b1492 Mon Sep 17 00:00:00 2001 From: amosblomqvist Date: Tue, 6 Oct 2026 05:31:00 +0300 Subject: [PATCH] =?UTF-8?q?dock:=20the=20practice=20fold=20morphs=20like?= =?UTF-8?q?=20teaching=20=E2=80=94=20every=20docked=20variant=20carries=20?= =?UTF-8?q?its=20fold=20state?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Owner report 2026-10-06: practice sessions 'open and close instantly' while teaching morphs. Live instrumented run (Element.prototype.animate wrapped, three transitions recorded) proved the pose/conclude morphs fire identically in practice (54px↔327.5px, 120ms, same easing) — the asymmetry was the FOLD: the shell's height-morph key read `mode:${pending ? expanded : "-"}` and pending (the question's tool call) is null in practice, so a practice dock's bar↔form toggle never changed the key — no height morph, instant jump, only the 160ms content fade. The ternary predates practice sessions (aeef28d, August); the remembered Oct-5 fix (ec8315b) added the fade only. Round 16's pause flow raised fold exposure until the seam surfaced. Fix: dockStateKey(mode, docked, expanded) — components/dock-state-key.ts — widened to !!pending || !!practice (the fold machine's own ownership rule). Typing growth keeps the key (never animates); the fold rides the exact branch teaching has used for months. Pinned in tests/dock-state-key.test.ts. --- components/chat.tsx | 7 ++++++- components/dock-state-key.ts | 18 +++++++++++++++++ tests/dock-state-key.test.ts | 38 ++++++++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 components/dock-state-key.ts create mode 100644 tests/dock-state-key.test.ts diff --git a/components/chat.tsx b/components/chat.tsx index a93a433..930fab8 100644 --- a/components/chat.tsx +++ b/components/chat.tsx @@ -24,6 +24,7 @@ import { artifactRenderers } from "@/components/artifacts"; import { SubagentSteerCard } from "@/components/artifacts/steer-card"; import { Composer } from "@/components/composer"; import { useEnterSubmits } from "@/components/enter-submits"; +import { dockStateKey } from "@/components/dock-state-key"; import { QueuedSteersBody, type QueuedSteerLine } from "@/components/queued-steers"; import { DictationBar, joinDictation, useDictation } from "@/components/dictation"; import { JobsContext } from "@/components/jobs-context"; @@ -2108,7 +2109,11 @@ function DockOverlay({ { duration: 160, easing: "cubic-bezier(0.2, 0.8, 0.2, 1)" }, ); }, [expanded]); - const stateKey = `${mode}:${pending ? expanded : "-"}`; + // The height-morph key (components/dock-state-key.ts): the expanded + // term (the fold) belongs to EVERY docked variant — question AND + // practice (owner fix, 2026-10-06). Typing growth keeps the key and + // never animates; the fold/unfold morphs like teaching always has. + const stateKey = dockStateKey(mode, !!pending || !!practice, expanded); const prevRef = useRef<{ key: string; height: number } | null>(null); useIsoLayoutEffect(() => { const el = shellRef.current; diff --git a/components/dock-state-key.ts b/components/dock-state-key.ts new file mode 100644 index 0000000..7e339e0 --- /dev/null +++ b/components/dock-state-key.ts @@ -0,0 +1,18 @@ +/** + * The dock shell's variant key (the WAAPI height-morph trigger, + * components/chat.tsx DockOverlay). Whenever this string CHANGES between + * renders, the shell morphs its height (120ms) and re-reserves the + * transcript tail; when it stays the same, height changes are measured + * only (typing growth never animates). + * + * The expanded term (the fold: full form ↔ slim bar) belongs to EVERY + * docked variant — question AND practice. For two months it keyed on + * `pending` (the question's tool call) alone, which is null in practice + * sessions: the practice dock folded and re-expanded with an INSTANT + * height jump while teaching morphed (owner report 2026-10-06, "opens + * and closes instantly"). The fold machine itself already treats + * practice as a dock owner; this key now agrees. + */ +export function dockStateKey(mode: string, docked: boolean, expanded: boolean): string { + return docked ? `${mode}:${expanded}` : `${mode}:-`; +} diff --git a/tests/dock-state-key.test.ts b/tests/dock-state-key.test.ts new file mode 100644 index 0000000..2ca0964 --- /dev/null +++ b/tests/dock-state-key.test.ts @@ -0,0 +1,38 @@ +/** + * The dock shell's variant key — the WAAPI height-morph trigger. Pinned + * after the practice-fold fix (owner, 2026-10-06): the expanded term + * belongs to every docked variant, so a practice dock's fold/unfold + * morphs exactly like a teaching question's. Before the fix the key + * read `pending ? expanded : "-"` — pending is null in practice, the + * key never changed on the fold, and the practice dock's height jumped + * instantly between form and bar while teaching morphed. + */ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; + +import { dockStateKey } from "../components/dock-state-key.ts"; + +describe("dockStateKey (the height-morph trigger)", () => { + it("a docked variant carries its fold state — practice included", () => { + assert.equal(dockStateKey("practice:call_p1", true, true), "practice:call_p1:true"); + assert.equal(dockStateKey("practice:call_p1", true, false), "practice:call_p1:false"); + assert.equal(dockStateKey("question:call_q1", true, true), "question:call_q1:true"); + assert.equal(dockStateKey("question:call_q1", true, false), "question:call_q1:false"); + }); + + it("the fold toggling the key is what animates: same variant + same fold = same key (typing growth never morphs)", () => { + assert.equal( + dockStateKey("practice:call_p1", true, true), + dockStateKey("practice:call_p1", true, true), + ); + assert.notEqual( + dockStateKey("practice:call_p1", true, true), + dockStateKey("practice:call_p1", true, false), + ); + }); + + it("an undocked band (the composer) has no fold term", () => { + assert.equal(dockStateKey("chat", false, true), "chat:-"); + assert.equal(dockStateKey("chat", false, false), "chat:-"); + }); +});