practice: ONE send lane — the register is read from the text

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.
This commit is contained in:
amosblomqvist
2026-10-05 03:46:32 +03:00
parent f460a90be9
commit 0bd3236af0
13 changed files with 101 additions and 130 deletions
+8 -8
View File
@@ -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
+6 -13
View File
@@ -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 (
<ThinkingBlock
testId="thinking-card"
dataCommit={data.commit}
label={data.commit ? "Your solution" : "Your thinking"}
text={data.text}
/>
);
// 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 <ThinkingBlock testId="thinking-card" label="Your thinking" text={data.text} />;
}
+22 -36
View File
@@ -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
</div>
</div>
{/* 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. */}
<div
data-testid="practice-thinking-field"
className="m-1.5 rounded-xl border border-border/70 bg-background/60 transition-colors focus-within:border-primary/50"
@@ -127,39 +122,30 @@ export function PracticeDock({ problemCallId, input, expanded, onExpand, onSubmi
onEscape();
return;
}
// ⌘↵ on macOS, Ctrl↵ elsewhere — the universal submit
// chord; here it COMMITS (the stronger of the two acts).
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
// Enter SENDS — the single lane (owner round 2, 2026-10-05);
// Shift+Enter is the newline, ⌘/Ctrl+Enter the chord too.
if (e.key === "Enter" && (!e.shiftKey || e.metaKey || e.ctrlKey)) {
e.preventDefault();
submit(true);
submit();
}
}}
placeholder="Work here — say as much of your thinking as you have. Nudge asks for a hint without committing; Commit submits an answer."
placeholder="Work here — say as much of your thinking as you have."
rows={4}
className={cn(
"field-sizing-content lh-input max-h-64 min-h-16 w-full resize-none bg-transparent px-2.5 pb-2 outline-none placeholder:text-muted-foreground",
)}
/>
{/* The two submit lanes — docked inside the field like the
composer's send, right-aligned. */}
<div className="flex items-center justify-end gap-1.5 px-1.5 pb-1.5">
{/* The one Send — docked inside the field like the composer's,
right-aligned. */}
<div className="flex items-center justify-end px-1.5 pb-1.5">
<button
type="button"
data-testid="practice-nudge"
data-testid="practice-send"
disabled={thinking.trim().length === 0}
onClick={() => submit(false)}
className="rounded-lg border border-border/70 px-3 py-1.5 text-sm text-muted-foreground transition-colors hover:text-foreground disabled:pointer-events-none disabled:opacity-40"
>
Nudge
</button>
<button
type="button"
data-testid="practice-commit"
disabled={thinking.trim().length === 0}
onClick={() => submit(true)}
onClick={() => submit()}
className="rounded-lg bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:pointer-events-none disabled:opacity-40"
>
Commit
Send
</button>
</div>
</div>
+3 -3
View File
@@ -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() });