)}
- {/* The unified bottom row — same slot and radii as the chat composer:
- the text field plus Submit, which sends text + selection together. */}
+ {/* The learner's half of the dock — a composer TWIN: a labeled, bordered
+ field with Submit docked inside, exactly like the chat composer's
+ send. Select modes get "Your thinking" (Enter makes a newline — it
+ is for dumping, soon dictated; the button submits); text mode
+ keeps "Your answer" with Enter-to-submit. */}
);
diff --git a/tests/render-cards.test.tsx b/tests/render-cards.test.tsx
index f1c00d8..35ff8fc 100644
--- a/tests/render-cards.test.tsx
+++ b/tests/render-cards.test.tsx
@@ -56,8 +56,12 @@ describe("render smoke tests", () => {
assert.match(html, /Which planet is closest to the sun\?/);
assert.match(html, /Mercury/);
assert.match(html, /I don't know|I don't know/);
- // The unified bottom row: note field + Submit answer live INSIDE the dock.
- assert.match(html, /Add a note/, "note field is the dock's bottom row");
+ // The learner's voice: a labeled "Your thinking" composer-twin lives
+ // INSIDE the dock — never called optional, inviting the dump.
+ assert.ok(html.includes('data-testid="thinking-field"'), "thinking field present");
+ assert.match(html, /Your thinking/, "labeled as the learner's voice");
+ assert.match(html, /Why this answer\? What are you unsure about\?/, "placeholder invites, not apologizes");
+ assert.ok(!html.includes("optional"), "the field is never called optional");
assert.match(html, /Submit answer/, "send becomes Submit answer");
// The v1 desync bug: options are buttons with ARIA roles, not native inputs.
assert.ok(!html.includes('type="radio"'), "no native radio inputs");
@@ -69,15 +73,50 @@ describe("render smoke tests", () => {
assert.ok(!html.includes("innermost"), "explanation must not render pre-answer");
});
- it("ask dock renders Other + note field, no grading affordance", () => {
+ it("ask dock renders the editable Other row + thinking field, no grading affordance", () => {
const html = renderDock({
toolName: "ask_user_question",
toolCallId: "a1",
input: { question: "How confident are you?", options: [{ label: "Very" }, { label: "Somewhat" }] } as never,
});
- assert.match(html, /Other/);
assert.ok(!html.includes("I don't know"), "ask has no 'I don't know'");
- assert.match(html, /Add a note/, "ask select-mode also gets the note row");
+ // Other is an editable OPTION ROW — a sibling of the options, with the
+ // radio circle + an inline input; picking it never displaces thinking.
+ assert.ok(html.includes('data-testid="other-option"'), "Other is an option row");
+ assert.match(html, /Type your own answer/, "type directly into the option");
+ assert.ok(html.includes('role="radio"'), "Other row keeps the option role");
+ assert.match(html, /aria-checked="false"/, "circle starts unfilled");
+ assert.ok(html.includes('data-testid="thinking-field"'), "thinking field coexists with Other");
+ assert.match(html, /Your thinking/, "ask select-mode also gets the learner's voice");
+ });
+
+ it("phase scales the thinking field: teaching invites depth, probing stays quick", () => {
+ const teach = renderToStaticMarkup(
+ {}}
+ onSubmit={() => {}}
+ />,
+ );
+ assert.ok(teach.includes("min-h-[4.5rem]"), "teach: a generous dump area");
+ const probe = renderDock({ toolName: "quiz", toolCallId: "q1", input: quizInput as never });
+ assert.ok(!probe.includes("min-h-[4.5rem]"), "probe (default): stays compact");
+ assert.ok(probe.includes("min-h-10"), "probe: one quiet row");
+ });
+
+ it("text-mode asks keep Enter-to-submit and the answer label", () => {
+ const html = renderDock({
+ toolName: "ask_user_question",
+ toolCallId: "a2",
+ input: { question: "Explain the chain rule in your own words." } as never,
+ });
+ assert.ok(html.includes('data-testid="answer-field"'), "text mode is the answer field");
+ assert.match(html, /Your answer/, "labeled as the answer");
+ assert.ok(!html.includes('data-testid="thinking-field"'), "no thinking field in text mode (v1)");
});
it("AnsweredCard grades quiz answers (✓/✗ + explanation)", () => {
@@ -88,6 +127,24 @@ describe("render smoke tests", () => {
assert.ok(html.includes("✓") && html.includes("✗"));
});
+ it("AnsweredCard promotes the learner's thinking to a first-class block", () => {
+ const answer = buildAnswer(quizQD(), {
+ values: ["Venus"],
+ note: "I picked Venus because it looks brightest — but now I realize brightness isn't distance.",
+ });
+ const html = renderToStaticMarkup();
+ assert.ok(html.includes('data-testid="your-thinking"'), "the thinking block exists");
+ assert.match(html, /Your thinking/, "labeled as the learner's voice");
+ assert.match(html, /brightness isn't distance/, "the thinking itself renders");
+ assert.ok(!html.includes("Note:"), "no longer a muted 'Note:' footnote");
+ // The thinking sits ABOVE the verdict's explanation — the dialogue of
+ // thinking, not an afterthought.
+ assert.ok(
+ html.indexOf("your-thinking") < html.indexOf("innermost"),
+ "thinking before the explanation",
+ );
+ });
+
it("AnsweredCard does NOT grade ask answers", () => {
const qd: QuestionData = {
id: "a1",