From 0bd3236af0a97b20bb0c03f891272c464721d02c Mon Sep 17 00:00:00 2001 From: amosblomqvist Date: Mon, 5 Oct 2026 03:46:32 +0300 Subject: [PATCH] =?UTF-8?q?practice:=20ONE=20send=20lane=20=E2=80=94=20the?= =?UTF-8?q?=20register=20is=20read=20from=20the=20text?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Owner round 2 (2026-10-05), Slice H: the Nudge/Commit two-lane buttons are gone. One Send; Enter sends, Shift+Enter is a newline. Whether a submission claims an answer is apparent from the thinking itself — the agent reads it (hint ladder for traces, moves+verdict for claims). - The thinking input loses the commit field end-to-end: ChatInput, parseChatInput, newThinkingMessage (details and the model-facing label both collapse to the single 'Your thinking'), the optimistic marker. - Legacy records' commit flags are deliberately unread. - Doctrine: the two labeled channels become one read instruction; 'answer commits' section retitled 'Claimed answers' — a claim is not a progress signal. - ThinkingCard: one label, collapsed arrival (the unified row). - The dock placeholder no longer explains lanes that no longer exist. --- components/chat.tsx | 16 +++---- components/practice-cards.tsx | 19 +++----- components/practice-dock.tsx | 58 +++++++++-------------- components/session-core.ts | 6 +-- src/agent/prompts/practice/00-base.md | 6 +-- src/agent/prompts/practice/10-doctrine.md | 8 ++-- src/core/contracts/chat.ts | 6 ++- src/core/input-events.ts | 13 ++--- src/core/native-messages.ts | 19 ++++---- tests/practice-flow.test.ts | 17 +++---- tests/practice-record.test.ts | 29 ++++-------- tests/render-practice-surface.test.tsx | 22 +++++---- tests/session-core.test.ts | 12 ++--- 13 files changed, 101 insertions(+), 130 deletions(-) diff --git a/components/chat.tsx b/components/chat.tsx index 30d99b8..443a9f1 100644 --- a/components/chat.tsx +++ b/components/chat.tsx @@ -448,16 +448,16 @@ export function Chat({ void resumeStream(); }, [resumeStream]); - // The practice dock's two lanes (owner, 2026-10-04): both POST one - // thinking input against the ACTIVE problem — commit=false is the - // nudge (hint request), commit=true the answer commit. Each starts a - // turn; the dock stays mounted (the problem is still active until the - // agent concludes). + // The practice dock's send (ONE lane, owner 2026-10-05): POSTs the + // thinking against the ACTIVE problem — whether it claims an answer is + // read from the text by the agent, not declared here. Starts a turn; + // the dock stays mounted (the problem is still active until the agent + // concludes). const submitThinking = useCallback( - (commit: boolean, text: string) => { + (text: string) => { const problem = activeProblemRef.current; if (!problem) return; - void send({ type: "thinking", problemCallId: problem.toolCallId, commit, text }); + void send({ type: "thinking", problemCallId: problem.toolCallId, text }); }, [send], ); @@ -1631,7 +1631,7 @@ function DockOverlay({ * question (the composer is replaced); null in teaching sessions. */ practice: { toolCallId: string; input: unknown } | null; /** (commit, text) — the practice dock's two submit lanes. */ - onSubmitThinking: (commit: boolean, text: string) => void; + onSubmitThinking: (text: string) => void; /** Teach-phase pending question: arrives collapsed behind the reveal button (W5). */ requiresReveal: boolean; /** The reading-thinking draft (owner 2026-10-03) — owned by ChatView so diff --git a/components/practice-cards.tsx b/components/practice-cards.tsx index 5df2a23..6f86520 100644 --- a/components/practice-cards.tsx +++ b/components/practice-cards.tsx @@ -24,7 +24,8 @@ import { ThinkingBlock } from "@/components/thinking-block"; /** The practice-thinking marker's details (native-messages.ts). */ export interface PracticeThinkingData { problemCallId: string; - commit: boolean; + /** Legacy records (pre 2026-10-05) carry the old two-lane flag; ignored. */ + commit?: boolean; text: string; } @@ -104,16 +105,8 @@ export function ConcludeCard({ part }: ArtifactPartProps) { } export function ThinkingCard({ data }: { data: PracticeThinkingData }) { - // The unified row (owner round, 2026-10-05): one style — collapsed - // arrival, 50%-italic quote block on expand. The register survives only - // in the label ("Your solution" when committed) and the data-commit - // flag for the tests. - return ( - - ); + // The unified row (owner rounds, 2026-10-05): one style, one label — + // collapsed arrival, "Your thinking", 50%-italic quote block on expand. + // Legacy records' commit flag is deliberately unread (one lane now). + return ; } diff --git a/components/practice-dock.tsx b/components/practice-dock.tsx index 18917fc..4cbd724 100644 --- a/components/practice-dock.tsx +++ b/components/practice-dock.tsx @@ -10,16 +10,11 @@ import { cn } from "@/lib/utils"; * The practice dock (practice sessions, owner design 2026-10-04): an * ACTIVE problem replaces the chat composer — the persistent bottom blob * morphs into the problem. The agent's statement above; the learner's - * half below — "Your thinking", a labeled composer-twin field (the - * question dock's discipline: Enter makes a newline, it is for dumping) - * with TWO submit buttons: - * - * - **Nudge** — submit the thinking WITHOUT a committed answer: working - * trace, hint request (the ladder answers, one rung per request). - * - **Commit** — the thinking contains an arrived answer. - * - * ⌘/Ctrl+Enter commits (the universal submit chord). One problem may - * carry many submissions; each starts a turn. + * half below — "Your thinking", a labeled field with ONE Send button + * (owner round 2, 2026-10-05): Enter sends, Shift+Enter makes a newline. + * There are no lanes — whether a submission claims an answer is apparent + * from the thinking itself; the agent reads it. One problem may carry + * many submissions; each starts a turn. * * Collapse (scroll-up → slim bar) is owned by the DockOverlay; this * component is content only, and the expanded form stays MOUNTED while @@ -40,8 +35,8 @@ export interface PracticeDockProps { /** Collapse state, owned by the DockOverlay. */ expanded: boolean; onExpand: () => void; - /** (commit, text) — POST {type:"thinking"}; commit=false is the nudge. */ - onSubmit: (commit: boolean, text: string) => void; + /** text — POST {type:"thinking"} (one lane; the register is read from the text). */ + onSubmit: (text: string) => void; /** Escape in the thinking field stops dictation (threaded from the page). */ onEscape?: () => void; } @@ -65,12 +60,12 @@ export function PracticeDock({ problemCallId, input, expanded, onExpand, onSubmi true, ); - const submit = (commit: boolean) => { + const submit = () => { const text = thinkingRef.current.trim(); if (text.length === 0) return; thinkingRef.current = ""; setThinking(""); - onSubmit(commit, text); + onSubmit(text); }; /* ── the collapsed bar ── */ @@ -103,8 +98,8 @@ export function PracticeDock({ problemCallId, input, expanded, onExpand, onSubmi - {/* The learner's half — a composer TWIN (the question dock's - discipline): Enter makes a newline; the buttons submit. */} + {/* The learner's half: Enter SENDS (owner round 2, 2026-10-05); + Shift+Enter makes a newline — less to decide about. */}
- {/* The two submit lanes — docked inside the field like the - composer's send, right-aligned. */} -
+ {/* The one Send — docked inside the field like the composer's, + right-aligned. */} +
-
diff --git a/components/session-core.ts b/components/session-core.ts index 547821b..f2e3363 100644 --- a/components/session-core.ts +++ b/components/session-core.ts @@ -529,9 +529,9 @@ export class SessionModel { if (input.type === "text") optimistic.push({ ...newUserMessage(input.text), id: localId() }); else if (input.type === "thinking") { // The practice submissions (2026-10-04): the marker lands - // optimistically — the register (trace / commit) renders from - // its details the moment the button fires. - optimistic.push({ ...newThinkingMessage(input.problemCallId, input.commit, input.text), id: localId() }); + // optimistically — the card renders from its details the + // moment the button fires. + optimistic.push({ ...newThinkingMessage(input.problemCallId, input.text), id: localId() }); } else if (input.type === "answer") { const tool = pendingCallTool([...this.base, ...this.tail], input.toolCallId) ?? "quiz"; optimistic.push({ ...newAnswerResult(input.toolCallId, tool, input.answer), id: localId() }); diff --git a/src/agent/prompts/practice/00-base.md b/src/agent/prompts/practice/00-base.md index fa20121..95bd427 100644 --- a/src/agent/prompts/practice/00-base.md +++ b/src/agent/prompts/practice/00-base.md @@ -23,10 +23,10 @@ He has to be able to trust you completely; one confidently-delivered hallucinati Two tools, and everything else is conversation. The mechanics only — the method (when to pose, what to pose, how to respond) is the doctrine's. -**`problem`** — poses one problem. The statement field is what the learner sees (markdown and LaTeX render); the modelSolution is your private commitment, hidden from the learner until you conclude. The call ends the turn. The learner then works in a thinking field; their submissions arrive back as labeled messages: +**`problem`** — poses one problem. The statement field is what the learner sees (markdown and LaTeX render); the modelSolution is your private commitment, hidden from the learner until you conclude. The call ends the turn. The learner then works in a thinking field; each submission arrives back as a *Your thinking* message. Whether it claims an answer is IN THE TEXT, not declared anywhere — read it: -- *Thinking trace (working, not committed):* they are mid-work and asking for a nudge. This is a hint request — the hint ladder applies, one rung per request. A trace may span several of these. -- *Committed answer:* they claim an answer. Extract the moves, deliver the verdict. Remember: a commit is not a progress signal. +- Working trace (mid-work, stuck, or asking for a nudge): the hint ladder applies, one rung per request. A trace may span several submissions. +- A claimed answer: extract the moves, deliver the verdict. A claim is not a progress signal. Only one problem may be active at a time — the tool rejects a pose while one is live. Conclude before posing the next. diff --git a/src/agent/prompts/practice/10-doctrine.md b/src/agent/prompts/practice/10-doctrine.md index 4bba06e..3142dd2 100644 --- a/src/agent/prompts/practice/10-doctrine.md +++ b/src/agent/prompts/practice/10-doctrine.md @@ -81,7 +81,7 @@ The statement never announces its target. If the situation makes the applicable ## VI. Hints -The policy covers every hint request and every intermediate submission that is not an answer commit — including a bare "I'm stuck" with an empty trace, where there is nothing to diagnose and you therefore start at the top of the ladder. +The policy covers every submission that does not claim an answer — including a bare "I'm stuck" with an empty trace, where there is nothing to diagnose and you therefore start at the top of the ladder. The economics first: revealed information cannot be unrevealed, and the learner can always think longer and ask again. So help is held to the minimum — regardless of how much the learner is struggling. @@ -104,7 +104,7 @@ Three structural rules: The rung a problem needed is a difficulty readout — only R0–R1 means the problem was about right; R3 and above means it was over the edge. Let this feed the next demand's difficulty. -## VII. Answer commits +## VII. Claimed answers The thinking trace carries junk by design. Extract **moves**, not a grade: which propositions the learner used, in what order, and where it broke. @@ -116,7 +116,7 @@ Classify every miss: cue miss (nothing came), discrimination error (wrong candid ## VIII. Progression -An answer commit is not a signal to progress. You own progression, not the commit count. A problem may take many commits; staying is the default while yield remains. +A claimed answer is not a signal to progress. You own progression, not the answer count. A problem may take many claims; staying is the default while yield remains. Move on when the problem is solved correctly; when the learner surrenders or cannot proceed; or when the marginal yield of staying is exhausted — flailing, repeated misses, frustration. Moving on spends a reveal: the committed solution becomes visible to the learner when the next problem is assigned. Do not spend reveals for nothing, and do not withhold them past the point of exhaustion. @@ -136,5 +136,5 @@ If the learner brings a problem from outside — an exam problem, something they - Never reveal what the learner can still generate — one rung per request, verification before correction. - Never re-ask in the same surface. - Never teach beyond a surgical line. -- Never mistake a commit for a progress signal. +- Never mistake a claimed answer for a progress signal. - Never soften a verdict or inflate one. diff --git a/src/core/contracts/chat.ts b/src/core/contracts/chat.ts index 9700bc9..52796c2 100644 --- a/src/core/contracts/chat.ts +++ b/src/core/contracts/chat.ts @@ -186,8 +186,10 @@ export const HANDOFF_TEXT = /** The model-facing register of a thinking submission (newThinkingMessage * prefixes the learner's text with this label). Hints and commits ride * one channel — the label is the ONLY difference the model sees. */ -export const THINKING_HINT_LABEL = "Thinking trace (working, not committed)"; -export const THINKING_COMMIT_LABEL = "Committed answer"; +/** The single submission label (owner round, 2026-10-05): one send + * lane — whether a submission claims an answer is read from the text, + * not declared by a button. */ +export const THINKING_LABEL = "Your thinking"; /** The conclude tool's ack — shared by the executed call (agent layer) and * the dismissal-synthesized pair (input-events), so a concluded problem diff --git a/src/core/input-events.ts b/src/core/input-events.ts index 52f2c4e..3c591d2 100644 --- a/src/core/input-events.ts +++ b/src/core/input-events.ts @@ -45,10 +45,10 @@ export type ChatInput = | { type: "answer"; toolCallId: string; answer: QuestionAnswerData } | { type: "continue" } /** The practice dock's two buttons (owner, 2026-10-04): the learner's - * thinking on the ACTIVE problem — commit=false is the hint request - * (working, not committed), commit=true the answer commit. One + * thinking on the ACTIVE problem — one lane (owner 2026-10-05): + * whether it claims an answer is read from the text. One * problem may carry many submissions; each is its own turn. */ - | { type: "thinking"; problemCallId: string; commit: boolean; text: string }; + | { type: "thinking"; problemCallId: string; text: string }; /** A wake-pipeline steer delivery — same turn composition, server-origin. * Wave C (2026-09-26): the idle wake carries EVERY queued marker as its @@ -168,13 +168,10 @@ export function parseChatInput(input: unknown): ChatInput { if (typeof i.problemCallId !== "string" || i.problemCallId.length === 0) { throw new ChatInputError("thinking input requires problemCallId"); } - if (typeof i.commit !== "boolean") { - throw new ChatInputError("thinking input requires commit (boolean)"); - } if (typeof i.text !== "string" || i.text.trim().length === 0) { throw new ChatInputError("thinking input requires non-empty text"); } - return { type: "thinking", problemCallId: i.problemCallId, commit: i.commit, text: i.text }; + return { type: "thinking", problemCallId: i.problemCallId, text: i.text }; default: throw new ChatInputError(`unknown input type: ${String(i.type)}`); } @@ -207,7 +204,7 @@ export function inputAppends(record: { messages: LhMessage[] }, input: ChatInput if (!active || active.toolCallId !== input.problemCallId) { throw new ChatInputError("thinking input targets no active problem"); } - return [...dismissalConclusion(record), newThinkingMessage(input.problemCallId, input.commit, input.text)]; + return [...dismissalConclusion(record), newThinkingMessage(input.problemCallId, input.text)]; } } } diff --git a/src/core/native-messages.ts b/src/core/native-messages.ts index a32fb86..de0c005 100644 --- a/src/core/native-messages.ts +++ b/src/core/native-messages.ts @@ -39,8 +39,7 @@ import { QUESTION_TOOLS } from "./pending.ts"; import { CONCLUDE_ACK_TEXT, HANDOFF_TEXT, - THINKING_COMMIT_LABEL, - THINKING_HINT_LABEL, + THINKING_LABEL, } from "./contracts/chat.ts"; import { compactAnswerForModel, type QuestionAnswerData } from "./question-core.ts"; @@ -224,20 +223,20 @@ export function newGraphContextMessage(picks: readonly GraphContextPick[], chain }; } -/** The learner's thinking submission (the practice dock's two buttons): - * a user-register message the model reads WITH its label (hint vs - * commit — the register is the whole difference), while `details` keeps - * the raw payload for the transcript card. One problem may carry many +/** The learner's thinking submission (ONE send lane, owner round + * 2026-10-05): the register is no longer declared by a button — the + * model READS the text to tell working traces from claimed answers. The + * content label is the single "Your thinking"; `details` keeps the raw + * payload for the transcript card. One problem may carry many * submissions. */ -export function newThinkingMessage(problemCallId: string, commit: boolean, text: string): LhMarkerMessage { - const label = commit ? THINKING_COMMIT_LABEL : THINKING_HINT_LABEL; +export function newThinkingMessage(problemCallId: string, text: string): LhMarkerMessage { return { id: newMessageId(), role: "custom", customType: "practice-thinking", - content: `${label}:\n${text}`, + content: `${THINKING_LABEL}:\n${text}`, display: true, - details: { problemCallId, commit, text }, + details: { problemCallId, text }, timestamp: Date.now(), }; } diff --git a/tests/practice-flow.test.ts b/tests/practice-flow.test.ts index 8f0cd5f..3ce9a83 100644 --- a/tests/practice-flow.test.ts +++ b/tests/practice-flow.test.ts @@ -137,15 +137,16 @@ describe("practice flow (scripted model)", () => { assert.equal(needsContinuation(afterPose), false); assert.equal(activeProblemOf(afterPose)?.toolCallId, "call-p1"); - // ── Turn 2: the commit arrives as a labeled thinking message ── + // ── Turn 2: the submission arrives as a single-lane thinking message ── await drain( await handleChatRequest( - post(chat.id, { type: "thinking", problemCallId: "call-p1", commit: true, text: "|G|=p; ⟨x⟩ has order dividing p" }), + post(chat.id, { type: "thinking", problemCallId: "call-p1", text: "|G|=p; ⟨x⟩ has order dividing p" }), factory, ), ); - // The model saw the COMMIT label, not the hint label. - assert.ok(lastSaw(calls, "Committed answer:"), "the conclude turn's prompt carries the committed answer"); + // The model saw the one label (owner round 2, 2026-10-05): the + // register is read from the text, not declared by a button. + assert.ok(lastSaw(calls, "Your thinking:"), "the conclude turn's prompt carries the thinking"); const afterConclude = chatStore.getChat(chat.id)!.messages; const concludeAck = afterConclude.find( (m) => (m as ToolResultMessage).role === "toolResult" && (m as ToolResultMessage).toolName === "conclude", @@ -157,7 +158,7 @@ describe("practice flow (scripted model)", () => { assert.equal(activeProblemOf(afterConclude), null); }); - it("the nudge (commit:false) carries the hint label", async () => { + it("a working-trace submission keeps the problem active", async () => { const chat = createPracticeChat("Test: practice nudge"); const { engine, calls } = scripted([ () => fauxAssistantMessage([fauxToolCall("problem", POSE, { id: "call-p2" })], { stopReason: "toolUse" }), @@ -167,12 +168,12 @@ describe("practice flow (scripted model)", () => { await drain(await handleChatRequest(post(chat.id, { type: "text", text: "go" }), factory)); await drain( await handleChatRequest( - post(chat.id, { type: "thinking", problemCallId: "call-p2", commit: false, text: "stuck at the second step" }), + post(chat.id, { type: "thinking", problemCallId: "call-p2", text: "stuck at the second step" }), factory, ), ); - assert.ok(lastSaw(calls, "Thinking trace (working, not committed):"), "the nudge turn's prompt carries the hint label"); - // The problem is still active — a nudge never concludes. + assert.ok(lastSaw(calls, "Your thinking:"), "the turn's prompt carries the thinking"); + // The problem is still active — a trace never concludes. assert.equal(activeProblemOf(chatStore.getChat(chat.id)!.messages)?.toolCallId, "call-p2"); }); diff --git a/tests/practice-record.test.ts b/tests/practice-record.test.ts index 3713a8e..0596037 100644 --- a/tests/practice-record.test.ts +++ b/tests/practice-record.test.ts @@ -118,7 +118,7 @@ describe("activeProblemOf (the conclude-before-next-problem gate's fold)", () => it("thinking submissions keep it active (nudges never conclude)", () => { const messages = [ ...problemPose(), - newThinkingMessage("call_p1", false, "stuck at the second step"), + newThinkingMessage("call_p1", "stuck at the second step"), ]; assert.equal(activeProblemOf(messages)?.toolCallId, "call_p1"); }); @@ -193,52 +193,43 @@ describe("pendingProblemDismissalOf (the marker waiting for its synthesis)", () // ── the input channel ──────────────────────────────────────────────────────── describe("the thinking input", () => { - it("parses and appends the marker against the active problem", () => { + it("parses and appends the marker against the active problem (ONE lane)", () => { const input = parseChatInput({ type: "thinking", problemCallId: "call_p1", - commit: false, text: "I think the inactivity constraint applies here", }); assert.deepEqual(input, { type: "thinking", problemCallId: "call_p1", - commit: false, text: "I think the inactivity constraint applies here", }); const appends = inputAppends({ messages: problemPose() }, input); assert.equal(appends.length, 1); const marker = appends[0] as { customType: string; content: string; details: unknown }; assert.equal(marker.customType, "practice-thinking"); - assert.match(marker.content, /Thinking trace \(working, not committed\):\n/); + // The single label (owner round 2, 2026-10-05): the register is read + // from the text by the agent, never declared by a button. + assert.match(marker.content, /Your thinking:\n/); assert.deepEqual(marker.details, { problemCallId: "call_p1", - commit: false, text: "I think the inactivity constraint applies here", }); }); - it("commit=true carries the committed register", () => { - const appends = inputAppends( - { messages: problemPose() }, - { type: "thinking", problemCallId: "call_p1", commit: true, text: "The answer is 42 because…" }, - ); - assert.match((appends[0] as { content: string }).content, /Committed answer:\n/); - }); - it("400s against a stale or unknown problem call", () => { assert.throws( - () => inputAppends({ messages: problemPose() }, { type: "thinking", problemCallId: "call_ghost", commit: true, text: "x" }), + () => inputAppends({ messages: problemPose() }, { type: "thinking", problemCallId: "call_ghost", text: "x" }), /targets no active problem/, ); assert.throws( - () => inputAppends({ messages: [] }, { type: "thinking", problemCallId: "call_p1", commit: true, text: "x" }), + () => inputAppends({ messages: [] }, { type: "thinking", problemCallId: "call_p1", text: "x" }), /targets no active problem/, ); }); it("empty text 400s at parse", () => { - assert.throws(() => parseChatInput({ type: "thinking", problemCallId: "c", commit: false, text: " " }), /non-empty/); + assert.throws(() => parseChatInput({ type: "thinking", problemCallId: "c", text: " " }), /non-empty/); }); }); @@ -289,11 +280,11 @@ describe("the dismiss-problem input (ask-style discard)", () => { describe("toLlmMessages (practice shapes)", () => { it("the thinking marker flattens to a labeled user message", () => { - const out = toLlmMessages(asAgentMessages([newThinkingMessage("call_p1", true, "It is 42")])); + const out = toLlmMessages(asAgentMessages([newThinkingMessage("call_p1", "It is 42")])); assert.equal(out.length, 1); const user = out[0] as { role: string; content: Array<{ type: string; text: string }> }; assert.equal(user.role, "user"); - assert.match(user.content[0]!.text, /^Committed answer:\nIt is 42$/); + assert.match(user.content[0]!.text, /^Your thinking:\nIt is 42$/); }); it("the dismissal marker is UI-only (the synthesized pair carries the face)", () => { diff --git a/tests/render-practice-surface.test.tsx b/tests/render-practice-surface.test.tsx index cdf3ac0..d6f6b01 100644 --- a/tests/render-practice-surface.test.tsx +++ b/tests/render-practice-surface.test.tsx @@ -2,8 +2,8 @@ * THE PRACTICE SURFACE — SSR render pins (the render-suite discipline: * pure bodies, renderToStaticMarkup, testids). The dock (the active * problem replaces the composer: statement above, "Your thinking" - * composer-twin below, the TWO submit lanes — Nudge without a commit, - * Commit with one; ⌘/Ctrl+Enter commits), the transcript cards (the + * field below with ONE Send (Enter sends, Shift+Enter is a newline)), + * the transcript cards (the * problem's compact-expandable statement — commitment NEVER renders; * the conclusion's outcome + verdict + reveal; the learner's thinking * register cards), and the view projection (the thinking marker's @@ -56,8 +56,8 @@ describe("PracticeDock", () => { // The measured column cap (the old fixed 40vh is only the fallback). assert.match(html, /--dock-max-h/); assert.match(html, /Your thinking/); - assert.match(html, /data-testid="practice-nudge"/); - assert.match(html, /data-testid="practice-commit"/); + assert.match(html, /data-testid="practice-send"/); + assert.ok(!html.includes("practice-nudge") && !html.includes("practice-commit"), "the two-lane buttons are gone"); // The buttons carry their lane in the disabled gate; both start // disabled (empty field). assert.match(html, /disabled/); @@ -142,12 +142,14 @@ describe("the practice transcript cards", () => { assert.doesNotMatch(html, /The solution/); }); - it("ThinkingCard: the register is the title (trace vs solution)", () => { + it("ThinkingCard: one label, collapsed arrival (legacy commit flags ignored)", () => { const trace = renderToStaticMarkup(); assert.match(trace, /Your thinking/); - assert.doesNotMatch(trace, /Your solution/); - const commit = renderToStaticMarkup(); - assert.match(commit, /Your solution/); + assert.match(trace, /aria-expanded="false"/); + assert.ok(!trace.includes("stuck at step two"), "the text is one click away"); + const legacy = renderToStaticMarkup(); + assert.match(legacy, /Your thinking/); + assert.doesNotMatch(legacy, /Your solution/); }); }); @@ -155,13 +157,13 @@ describe("the practice transcript cards", () => { describe("the practice view projection", () => { it("the thinking marker projects as a user-side data part (register in details)", () => { - const marker = newThinkingMessage("call_p1", true, "It is 42."); + const marker = newThinkingMessage("call_p1", "It is 42."); const view = projectView([marker as LhMessage]); assert.equal(view.length, 1); assert.equal(view[0]!.role, "user"); const dataPart = view[0]!.parts[0]!; assert.equal(dataPart.type, "data-practice-thinking"); - assert.deepEqual(dataPart.data, { problemCallId: "call_p1", commit: true, text: "It is 42." }); + assert.deepEqual(dataPart.data, { problemCallId: "call_p1", text: "It is 42." }); }); it("the problem pose's ack keeps the part input-available (the dock's mount)", () => { diff --git a/tests/session-core.test.ts b/tests/session-core.test.ts index 04ae9bd..e152fbb 100644 --- a/tests/session-core.test.ts +++ b/tests/session-core.test.ts @@ -1012,11 +1012,11 @@ describe("the live-view law — live is positive evidence, not session busyness" describe("absorbed — the custom role (the doubled thinking card)", () => { it("absorbs the optimistic thinking marker by customType + details", () => { - const optimistic = { ...newThinkingMessage("call-p1", false, "my trace"), id: "c_opt1" } as LhMessage; - const serverCopy = { ...newThinkingMessage("call-p1", false, "my trace"), id: "srv_1" } as LhMessage; + const optimistic = { ...newThinkingMessage("call-p1", "my trace"), id: "c_opt1" } as LhMessage; + const serverCopy = { ...newThinkingMessage("call-p1", "my trace"), id: "srv_1" } as LhMessage; assert.ok(absorbed(optimistic, [...poseRecord(), serverCopy]), "same submission → the local copy drops"); - // A DIFFERENT submission (commit flag, text) is its own marker. - const other = { ...newThinkingMessage("call-p1", true, "my trace"), id: "srv_2" } as LhMessage; + // A DIFFERENT submission (different text) is its own marker. + const other = { ...newThinkingMessage("call-p1", "a later, different trace"), id: "srv_2" } as LhMessage; assert.ok(!absorbed(optimistic, [...poseRecord(), other]), "different details → not the same submission"); // The c_ gate: server stamps are never absorbed, whatever the role. assert.ok(!absorbed(serverCopy, [optimistic]), "server ids never drop"); @@ -1027,14 +1027,14 @@ describe("absorbed — the custom role (the doubled thinking card)", () => { // the tail (client id), then the +400ms adoption fetch swaps in the // write-ahead record (server id). ONE card must remain. const m = model({ chatId: "chat_x", initialMessages: poseRecord() }); - const optimistic = { ...newThinkingMessage("call-p1", false, "my trace"), id: "c_opt1" } as LhMessage; + const optimistic = { ...newThinkingMessage("call-p1", "my trace"), id: "c_opt1" } as LhMessage; m.applyWireEvent({ type: "message_start", message: optimistic }); assert.equal( m.getState().nativeMessages.filter((x) => (x as { role?: string }).role === "custom").length, 1, "the optimistic marker is in the tail", ); - const serverCopy = { ...newThinkingMessage("call-p1", false, "my trace"), id: "srv_1" } as LhMessage; + const serverCopy = { ...newThinkingMessage("call-p1", "my trace"), id: "srv_1" } as LhMessage; m.adoptRecord([...poseRecord(), serverCopy]); const customs = m.getState().nativeMessages.filter((x) => (x as { role?: string }).role === "custom"); assert.equal(customs.length, 1, "the server copy replaced the optimistic one — no doubling");