practice UI: the dock, the transcript cards, layered-Esc discard
- PracticeDock (components/practice-dock.tsx): the active problem replaces the composer — statement (markdown+LaTeX) above, the 'Your thinking' composer-twin below with the two submit lanes (Nudge = thinking without a commit; Commit = answer commit; ⌘/Ctrl+Enter commits). Dictation claims the thinking field. - Transcript cards (practice-cards.tsx + the artifact registry): the problem card (compact-expandable statement — the commitment NEVER renders), the conclude card (outcome + verdict + reveal; the dismissal synthesis is one muted line), and the user-side thinking register cards (Your thinking / Your solution). - view.ts: the problem pose joins the placeholder fork (the ack never flips the part); practice-thinking markers project as data parts; problem-dismissed markers project nothing. - chat.tsx: activeProblemOf drives the dock branch (question > problem > composer), the esc layering gains the problem rung (below question, above turn) with its discard confirm + the silent marker route; the thinking submissions POST through the session's optimistic appends.
This commit is contained in:
@@ -18,6 +18,8 @@ import type { ToolResultMessage } from "@earendil-works/pi-ai";
|
||||
import {
|
||||
needsContinuation,
|
||||
newAnswerResult,
|
||||
newProblemDismissedMarker,
|
||||
newThinkingMessage,
|
||||
newUserMessage,
|
||||
projectView,
|
||||
type ChatInput,
|
||||
@@ -503,7 +505,12 @@ export class SessionModel {
|
||||
// adoption can tell them from the record's absorbed copies.
|
||||
const optimistic: LhMessage[] = [];
|
||||
if (input.type === "text") optimistic.push({ ...newUserMessage(input.text), id: localId() });
|
||||
else if (input.type === "answer") {
|
||||
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() });
|
||||
} 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() });
|
||||
}
|
||||
@@ -605,6 +612,27 @@ export class SessionModel {
|
||||
}
|
||||
}
|
||||
|
||||
/** The problem discard (practice sessions, owner 2026-10-04): the
|
||||
* ask-style dismissal — the UI-only marker lands optimistically, then
|
||||
* the SILENT route persists it (no turn, no wake). The composer
|
||||
* returns; the conclude synthesis rides the user's next input. */
|
||||
async dismissProblem(problemCallId: string): Promise<void> {
|
||||
if (!this.readyRef) return;
|
||||
this.mutate(() => {
|
||||
this.tail = [...this.tail, { ...newProblemDismissedMarker(problemCallId), id: localId() }];
|
||||
});
|
||||
try {
|
||||
const res = await this.fetch(`/api/chats/${this.chatId}/dismiss`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ input: { type: "dismiss-problem", problemCallId } }),
|
||||
});
|
||||
if (!res.ok) throw new Error(`The dismissal failed (${res.status}).`);
|
||||
} catch (err) {
|
||||
this.setError((err as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
/** Local detach: cancel the consume, keep the partial tail. The page's
|
||||
* settle watch adopts the record when the server turn finishes. */
|
||||
stop(): void {
|
||||
|
||||
Reference in New Issue
Block a user