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:
+8
-8
@@ -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
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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() });
|
||||
|
||||
Reference in New Issue
Block a user