ui: ProblemCard/ConcludeCard redesigned — the solution is the hero

Owner round (2026-10-05), Slice C:

- ProblemCard: ALWAYS OPEN — the answered card's container + eyebrow
  discipline ('Problem' label, lh-prose statement), no chevron, no
  collapsed bar. The dock mirrors it in Slice D.
- ConcludeCard: the reveal is the modelSolution committed at POSE time,
  shown verbatim as the hero — never re-authored (the slim schema's
  law). The verdict rides below in the quiz-explanation register
  (small, muted 'Verdict:' row). Nothing collapsible. Legacy inputs
  with their own solutionReveal fall back to it.
- The reveal join: modelSolutionForConclude (native-messages.ts) — the
  newest cleanly-posed problem before the conclude call; refused poses
  reveal nothing. projectView enriches tool-conclude parts with
  part.reveal at projection; ArtifactPartProps carries the field
  (contracts/chat.ts).

Pinned: always-open problem card; hero-first conclude layout with the
verdict after it; legacy fallback; the join (clean pose, refused pose,
multi-problem records) and the view enrichment.
This commit is contained in:
amosblomqvist
2026-10-05 02:29:16 +03:00
parent d19df86b13
commit 5d253a933c
6 changed files with 142 additions and 65 deletions
+40 -2
View File
@@ -27,6 +27,8 @@ import {
} from "../src/core/native-messages.ts";
import { CONCLUDE_ACK_TEXT } from "../src/core/contracts/chat.ts";
import { dismissAppends, inputAppends, parseChatInput, parseDismissInput } from "../src/core/input-events.ts";
import { modelSolutionForConclude } from "../src/core/native-messages.ts";
import { projectView } from "../src/core/index.ts";
// ── builders (native-shaped test fixtures) ───────────────────────────────────
@@ -50,7 +52,7 @@ function problemPose(callId = "call_p1", statement = "Show that…"): LhMessage[
id: `msg_a_${callId}`,
role: "assistant",
content: [
{ type: "toolCall", id: callId, name: PRACTICE_PROBLEM_TOOL, arguments: { statement, mode: "recall" } },
{ type: "toolCall", id: callId, name: PRACTICE_PROBLEM_TOOL, arguments: { statement, mode: "recall", modelSolution: statement } },
],
timestamp: 1,
...STAMPS,
@@ -110,7 +112,7 @@ describe("activeProblemOf (the conclude-before-next-problem gate's fold)", () =>
const messages = problemPose();
const active = activeProblemOf(messages);
assert.equal(active?.toolCallId, "call_p1");
assert.deepEqual(active?.input, { statement: "Show that…", mode: "recall" });
assert.deepEqual(active?.input, { statement: "Show that…", mode: "recall", modelSolution: "Show that…" });
});
it("thinking submissions keep it active (nudges never conclude)", () => {
@@ -345,3 +347,39 @@ describe("needsContinuation (a persisted pose ack owes nothing)", () => {
assert.equal(needsContinuation(messages), true);
});
});
// ── the reveal join (owner round, 2026-10-05) ───────────────────────────────
describe("modelSolutionForConclude (the conclusion's hero is the pose's commitment)", () => {
it("returns the cleanly-posed problem's modelSolution before the conclude", () => {
const messages = [...problemPose(), ...concludeCall("solved")];
assert.equal(modelSolutionForConclude(messages, "call_conclude_solved"), "Show that…");
});
it("skips refused poses — a rejected pose revealed nothing", () => {
const rejected = problemPose("call_bad", "A different student…");
(rejected[1]! as unknown as { isError: boolean }).isError = true;
const messages = [...rejected, ...concludeCall("solved")];
assert.equal(modelSolutionForConclude(messages, "call_conclude_solved"), undefined);
});
it("the newest pose before ITS conclude wins (multi-problem records)", () => {
const messages = [
...problemPose("call_p1", "first"),
...concludeCall("solved"),
...problemPose("call_p2", "second"),
...concludeCall("movedOn"),
];
assert.equal(modelSolutionForConclude(messages, "call_conclude_movedOn"), "second");
assert.equal(modelSolutionForConclude(messages, "call_conclude_solved"), "first");
});
it("projectView enriches the conclude part with the reveal", () => {
const messages = [...problemPose(), ...concludeCall("solved")];
const view = projectView(messages);
const conclude = view.flatMap((m) => m.parts).find((p) => p.type === "tool-conclude");
assert.ok(conclude, "the conclude part projects");
assert.equal(conclude!.reveal, "Show that…", "the view-seam join carries the committed solution");
});
});