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:-"); + }); +});