fix: multi-select Other coexistence + the malformed-assessment probe-done crash
Two live bugs from the modal-realism stress runs: 1. Multi-select ask: picking the custom 'Other…' row cleared every already-picked option, and picking any option cleared a typed custom answer — single-select displacement logic applied to both modes. The dock's selection is now a pure state machine in question-core (toggleOption/toggleOther over DockSelection, unit-tested): single displaces (unchanged), multi coexists — neither channel clears the other; the typed text always survives. 2. ProbeDoneCard crashed on a REJECTED probe_done call: the model stringified 'assessment' (the same double-encoding habit as the options bug), zod rejected the call, the model retried — but the card's input-preview fallback trusted the errored part's unvalidated input shape and died on assessment.strands.map. The preview now reads input only while the call is pending or streaming (guarded with Array.isArray); an output-error part renders a quiet malformed-and-retried note instead of the garbage. Pinned: question-core.test.ts (3 dock-selection machine cases), render-cards.test.tsx (4 probe-done card cases incl. the exact live stringified shape, plus the multi-select ask markup pin). Gates: tsc, lint, build, 315/315.
This commit is contained in:
@@ -278,6 +278,44 @@ export interface AnswerRequest {
|
||||
note?: string;
|
||||
}
|
||||
|
||||
// ─── dock selection (the client state machine, pure for tests) ─────────────
|
||||
|
||||
/** The question dock's selection state (0-based indices in display order). */
|
||||
export interface DockSelection {
|
||||
selected: Set<number>;
|
||||
otherSelected: boolean;
|
||||
dontKnow: boolean;
|
||||
}
|
||||
|
||||
export const emptyDockSelection = (): DockSelection => ({ selected: new Set(), otherSelected: false, dontKnow: false });
|
||||
|
||||
/**
|
||||
* Toggling a listed option. Single-select: the option becomes THE answer and
|
||||
* displaces "Other". Multi-select: the option toggles alongside its siblings
|
||||
* AND "Other" — selections are independent (picking options must never clear
|
||||
* a custom answer, and picking the custom row must never clear them).
|
||||
*/
|
||||
export function toggleOption(mode: SelectMode, sel: DockSelection, idx: number): DockSelection {
|
||||
if (mode === "multi-select") {
|
||||
const selected = new Set(sel.selected);
|
||||
if (selected.has(idx)) selected.delete(idx);
|
||||
else selected.add(idx);
|
||||
return { selected, otherSelected: sel.otherSelected, dontKnow: false };
|
||||
}
|
||||
return { selected: new Set([idx]), otherSelected: false, dontKnow: false };
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggling the ask "Other…" row. Single-select: picking it makes it THE
|
||||
* answer, displacing the listed options (the typed text always survives).
|
||||
* Multi-select: it toggles alongside the listed selections and clears
|
||||
* nothing.
|
||||
*/
|
||||
export function toggleOther(mode: SelectMode, sel: DockSelection): DockSelection {
|
||||
if (mode === "multi-select") return { ...sel, otherSelected: !sel.otherSelected, dontKnow: false };
|
||||
return { selected: new Set(), otherSelected: true, dontKnow: false };
|
||||
}
|
||||
|
||||
export const QUESTION_ENTRY = "lh_question";
|
||||
export const QUESTION_ANSWERED_ENTRY = "lh_question_answered";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user