ui: the chat comeback — side-based authorship, restored quotes, tucked whys
Blockquotes: the callout override had displaced Streamdown's own styled blockquote component, so plain quotes rendered as bare unstyled text since that commit. The pass-through branch now restores Streamdown's verbatim classes (border-l-4, muted italic). Explanations tucked for ALL questions (owner): the prediction's Why / Hide-why toggle is universal — every quiz answered card sits its explanation behind the same collapsed button. Same pattern, one law. The thinking row's rhythm: my-1 -> my-3. The 4px margin hugged the options on non-graded answered cards (owner report); 12px matches the card's mt-2/mt-3 spacing grammar everywhere the row renders. Side-based authorship (owner round, 2026-10-07): the document shape blurred user into agent — three left-rail idioms (user text, user thinking, quoted prose) collided once quotes rendered. User rows go RIGHT: 'You' messages and practice thinking cards hug the right edge at 85% width with the rail mirrored (border-r), text still reading left-to-right. In-card thinking (inside the answered card, the agent's context) stays left. Transcript thinking rows arrive EXPANDED — the thinking was clearly sent. Cmd+T (toggle-user-thinking, chat scope, cheat-sheet row) sets every user-thinking row to one state via a module store: explicit sets stomp individual toggles, late mounts honor the last set (the state initializer reads the store, so static render reflects it too).
This commit is contained in:
@@ -30,6 +30,12 @@ describe("kbdEventForAction — the one spelling for action events", () => {
|
||||
});
|
||||
|
||||
describe("the ⌘B chain — keypress → binding → action → event", () => {
|
||||
it("mod+t is the toggle-user-thinking action (owner round, 2026-10-07)", () => {
|
||||
assert.equal(actionForBinding("mod+t"), "toggle-user-thinking");
|
||||
assert.equal(bindingForAction("toggle-user-thinking"), "mod+t");
|
||||
assert.equal(kbdEventForAction("toggle-user-thinking"), "lh:kbd-toggle-user-thinking");
|
||||
});
|
||||
|
||||
it("mod+b is the toggle-sidebar action", () => {
|
||||
assert.equal(actionForBinding("mod+b"), "toggle-sidebar");
|
||||
assert.equal(bindingForAction("toggle-sidebar"), "mod+b");
|
||||
|
||||
@@ -241,7 +241,10 @@ describe("render smoke tests", () => {
|
||||
const answer = buildAnswer(quizQD(), { values: ["Venus"] });
|
||||
const html = renderToStaticMarkup(<AnsweredCard answer={answer} />);
|
||||
assert.match(html, /incorrect/);
|
||||
assert.match(html, /innermost/, "explanation renders post-answer");
|
||||
// Tucked for ALL questions (owner, 2026-10-07): the explanation sits
|
||||
// behind the Why toggle — never center stage.
|
||||
assert.ok(html.includes("tucked-explanation"), "the Why toggle mounts");
|
||||
assert.ok(!html.includes("innermost"), "explanation hidden until opened");
|
||||
// Glyph-shaped SVG marks (owner preference) — pinned via data-mark.
|
||||
assert.ok(html.includes('data-mark="check"') && html.includes('data-mark="x"'));
|
||||
});
|
||||
@@ -302,8 +305,8 @@ describe("render smoke tests", () => {
|
||||
// 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",
|
||||
html.indexOf("your-thinking") < html.indexOf("tucked-explanation"),
|
||||
"thinking before the explanation toggle",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -574,11 +577,11 @@ describe("predict vs check rendering", () => {
|
||||
assert.ok(html.includes("tucked-explanation"), "it sits behind the toggle");
|
||||
});
|
||||
|
||||
it("a check answer keeps the inline explanation", () => {
|
||||
it("a check answer tucks the explanation like every question (owner, 2026-10-07)", () => {
|
||||
const answer = buildAnswer(quizQD(), { values: ["Venus"] });
|
||||
const html = renderToStaticMarkup(<AnsweredCard answer={answer} />);
|
||||
assert.ok(html.includes("Why:"), "checks explain inline (current behavior)");
|
||||
assert.ok(!html.includes("tucked-explanation"));
|
||||
assert.ok(html.includes("tucked-explanation"), "the prediction tuck is now universal");
|
||||
assert.ok(!html.includes("Why:"), "no inline explanation — behind the toggle");
|
||||
});
|
||||
|
||||
it("the reading-thinking channel is LIFTED above the card as its own user-side block", () => {
|
||||
|
||||
@@ -14,6 +14,7 @@ import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { MessageResponse } from "../components/ai-elements/message.tsx";
|
||||
import { ThinkingBlock, setAllUserThinkingOpen } from "@/components/thinking-block";
|
||||
import { MermaidOverlay, SessionMermaidContext, chartSize } from "../components/session-overlay.tsx";
|
||||
import { PlanPopup } from "../components/plan-block.tsx";
|
||||
import { MarkdownCode, codeSource } from "../components/md-blocks.tsx";
|
||||
@@ -229,3 +230,43 @@ describe("parseAlertMarker (pure)", () => {
|
||||
assert.equal(parseAlertMarker(em), null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("md-blocks — the restored plain blockquote (owner round, 2026-10-07)", () => {
|
||||
it("a plain quote carries Streamdown's own styling (the override displaced it)", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<MessageResponse mode="static">{"Before.\n\n> An ordinary quote.\n\nAfter."}</MessageResponse>,
|
||||
);
|
||||
assert.ok(html.includes("border-l-4"), "the left border is back");
|
||||
assert.ok(html.includes("text-muted-foreground italic"), "muted italic — the quote register");
|
||||
assert.ok(!html.includes("md-callout"), "a plain quote is not a callout box");
|
||||
});
|
||||
});
|
||||
|
||||
describe("ThinkingBlock — side-based authorship + ⌘T sync (owner round, 2026-10-07)", () => {
|
||||
it("the default row stays left with the my-3 rhythm", () => {
|
||||
const html = renderToStaticMarkup(<ThinkingBlock text="left side" />);
|
||||
assert.ok(html.includes("border-l-2"), "left rail");
|
||||
assert.ok(html.includes("my-3"), "the 12px rhythm (was my-1)");
|
||||
});
|
||||
|
||||
it("mirrored rows hug the right edge, rail flipped, arriving open", () => {
|
||||
const html = renderToStaticMarkup(<ThinkingBlock text="right side" mirror defaultOpen />);
|
||||
assert.ok(html.includes('data-side="right"'));
|
||||
assert.ok(html.includes("ml-auto"), "pushed to the right edge");
|
||||
assert.ok(html.includes("border-r-2") && !html.includes("border-l-2"), "rail on the right");
|
||||
assert.match(html, /aria-expanded="true"/);
|
||||
assert.ok(html.includes("right side"), "text visible on arrival");
|
||||
});
|
||||
|
||||
it("⌘T sets every row — explicit sets stomp individual state, late mounts honor the last set", () => {
|
||||
setAllUserThinkingOpen(false);
|
||||
const collapsed = renderToStaticMarkup(<ThinkingBlock text="synced" defaultOpen />);
|
||||
assert.match(collapsed, /aria-expanded="false"/);
|
||||
assert.ok(!collapsed.includes(">synced<"), "the open default was stomped");
|
||||
setAllUserThinkingOpen(true);
|
||||
const reopened = renderToStaticMarkup(<ThinkingBlock text="synced" />);
|
||||
assert.match(reopened, /aria-expanded="true"/);
|
||||
// restore the null default for other tests in this file
|
||||
setAllUserThinkingOpen(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -176,11 +176,15 @@ describe("the practice transcript cards", () => {
|
||||
assert.doesNotMatch(html, /The solution/);
|
||||
});
|
||||
|
||||
it("ThinkingCard: one label, collapsed arrival (legacy commit flags ignored)", () => {
|
||||
it("ThinkingCard: one label, right side, expanded arrival (owner round, 2026-10-07)", () => {
|
||||
const trace = renderToStaticMarkup(<ThinkingCard data={{ problemCallId: "c", commit: false, text: "stuck at step two" }} />);
|
||||
assert.match(trace, /YOUR THINKING/);
|
||||
assert.match(trace, /aria-expanded="false"/);
|
||||
assert.ok(!trace.includes("stuck at step two"), "the text is one click away");
|
||||
// Transcript rows arrive EXPANDED (clearly conveyed as sent) and
|
||||
// MIRRORED right (side-based authorship).
|
||||
assert.match(trace, /aria-expanded="true"/);
|
||||
assert.ok(trace.includes("stuck at step two"), "the text is visible on arrival");
|
||||
assert.ok(trace.includes('data-side="right"'), "mirrored to the right side");
|
||||
assert.ok(trace.includes("border-r-2"), "the rail is on the right");
|
||||
const legacy = renderToStaticMarkup(<ThinkingCard data={{ problemCallId: "c", commit: true, text: "It is 42." }} />);
|
||||
assert.match(legacy, /YOUR THINKING/);
|
||||
assert.doesNotMatch(legacy, /Your solution/);
|
||||
|
||||
Reference in New Issue
Block a user