feat: the plan verdict gate — acceptance is the plan→teach seam
The accept-plan UI lands (docs/probe-teach-split.md §4, formerly the interim rule): presenting the plan no longer moves the phase — the learner's ACCEPT does. While the presented plan awaits its verdict, the composer's send square becomes two stacked buttons (green Accept plan / red Decline); typed text rides the verdict as his note, Enter keeps sending plain side questions (phase unmoved), decline asks for a revision and the buttons stay. The seam: accept sends a user message carrying a data-plan-accepted part (PlanAcceptedData in contracts/chat.ts, TeacherUIDataParts 'plan-accepted'); phaseAt3 reads it as the plan→teach event (plan_init demoted to presentation-only); convertToModelMessages drops data parts, so the teacher sees only the text. Mid-lesson rewrites need no fresh verdict — acceptance happens once, at the first presentation. The teacher's planning prompt now ends the turn at presentation and waits for the verdict (never teaches into an unaccepted plan). Gates: tsc clean, 304/304 (phases tests rewritten to the acceptance seam incl. decline/side-question/failed-init cases; composer verdict render tests), lint, build. Live GLM smoke ALL PASS: phase pinned at 'plan' after plan_init, presentation turn carried NO check, accept click flipped the summary to 'teach', then tagged checks moved the map (current=rates, visited=composite); zero console errors.
This commit is contained in:
+40
-1
@@ -21,7 +21,7 @@ import { SubagentSteerCard } from "@/components/artifacts/steer-card";
|
||||
import { Composer } from "@/components/composer";
|
||||
import { JobsContext } from "@/components/jobs-context";
|
||||
import { StatsBar } from "@/components/stats-bar";
|
||||
import { PlanBlock, PlanPopup } from "@/components/plan-block";
|
||||
import { PlanBlock, PlanPopup, latestTree } from "@/components/plan-block";
|
||||
import { PlanWindow } from "@/components/plan-window";
|
||||
import type { PlanBlockData } from "@/src/core";
|
||||
import { QuestionDock } from "@/components/question-dock";
|
||||
@@ -181,6 +181,37 @@ export function Chat({
|
||||
// The live plan popup (wave 4): opened by any transcript plan block.
|
||||
const [planOpen, setPlanOpen] = useState(false);
|
||||
|
||||
// ── The plan verdict gate (docs/probe-teach-split.md §4) ──
|
||||
// The plan has been presented (a tree exists) and the session still sits in
|
||||
// the planning phase → the composer's send square becomes the stacked
|
||||
// Accept / Decline buttons. Acceptance is the explicit plan→teach seam
|
||||
// (a data-plan-accepted part on the message); decline asks for revision.
|
||||
const planVerdict = useMemo(
|
||||
() => phaseAt3(messages) === "plan" && latestTree(messages) !== null,
|
||||
[messages],
|
||||
);
|
||||
const sendVerdict = useCallback(
|
||||
(accepted: boolean) => {
|
||||
const note = text.trim();
|
||||
setText("");
|
||||
if (accepted) {
|
||||
void sendMessage({
|
||||
parts: [
|
||||
{ type: "text", text: note || "The plan is accepted — begin teaching." },
|
||||
{
|
||||
type: "data-plan-accepted",
|
||||
id: `pa_${Date.now().toString(36)}_${crypto.randomUUID().slice(0, 8)}`,
|
||||
data: { at: new Date().toISOString() },
|
||||
},
|
||||
],
|
||||
});
|
||||
} else {
|
||||
void sendMessage({ text: note || "I'd like the plan revised before we begin." });
|
||||
}
|
||||
},
|
||||
[text, sendMessage],
|
||||
);
|
||||
|
||||
// ── Session usage (wave-4 stats bar) ──
|
||||
// The money channel: the record's turns log, folded client-side by
|
||||
// core/prices.ts. Seeded once on mount (the page doesn't carry turns),
|
||||
@@ -489,6 +520,14 @@ export function Chat({
|
||||
onSend={onSend}
|
||||
onStop={() => stop()}
|
||||
canSend={canSend}
|
||||
verdict={
|
||||
planVerdict
|
||||
? {
|
||||
onAccept: () => sendVerdict(true),
|
||||
onDecline: () => sendVerdict(false),
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
}
|
||||
onSubmitAnswer={(output) => {
|
||||
|
||||
Reference in New Issue
Block a user