ui: ProblemCard/ConcludeCard redesigned — the solution is the hero
Owner round (2026-10-05), Slice C:
- ProblemCard: ALWAYS OPEN — the answered card's container + eyebrow
discipline ('Problem' label, lh-prose statement), no chevron, no
collapsed bar. The dock mirrors it in Slice D.
- ConcludeCard: the reveal is the modelSolution committed at POSE time,
shown verbatim as the hero — never re-authored (the slim schema's
law). The verdict rides below in the quiz-explanation register
(small, muted 'Verdict:' row). Nothing collapsible. Legacy inputs
with their own solutionReveal fall back to it.
- The reveal join: modelSolutionForConclude (native-messages.ts) — the
newest cleanly-posed problem before the conclude call; refused poses
reveal nothing. projectView enriches tool-conclude parts with
part.reveal at projection; ArtifactPartProps carries the field
(contracts/chat.ts).
Pinned: always-open problem card; hero-first conclude layout with the
verdict after it; legacy fallback; the join (clean pose, refused pose,
multi-problem records) and the view enrichment.
This commit is contained in:
@@ -270,5 +270,9 @@ export interface ArtifactPartProps {
|
||||
toolCallId: string;
|
||||
input?: unknown;
|
||||
output?: unknown;
|
||||
/** The view-seam join for conclude cards (owner round, 2026-10-05):
|
||||
* the modelSolution committed at POSE time, folded onto the part by
|
||||
* projectView — the reveal is never re-authored. */
|
||||
reveal?: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -328,6 +328,40 @@ function cleanToolCallIds(messages: readonly LhMessage[]): Set<string> {
|
||||
return ids;
|
||||
}
|
||||
|
||||
/** The conclusion's reveal (owner round, 2026-10-05): the hero of a
|
||||
* conclude card is the modelSolution committed at POSE time — the newest
|
||||
* cleanly-posed problem BEFORE this conclude call. The reveal is never
|
||||
* re-authored (the slim schema dropped solutionReveal); legacy conclude
|
||||
* inputs that still carry their own solutionReveal fall back to it at the
|
||||
* card. Undefined when the pose carries no string solution. */
|
||||
export function modelSolutionForConclude(
|
||||
messages: readonly LhMessage[],
|
||||
concludeCallId: string,
|
||||
): string | undefined {
|
||||
let mi = messages.findIndex(
|
||||
(m) =>
|
||||
(m as { role?: string }).role === "assistant" &&
|
||||
(m as AssistantMessage).content.some(
|
||||
(b) => b.type === "toolCall" && b.id === concludeCallId,
|
||||
),
|
||||
);
|
||||
if (mi < 0) return undefined;
|
||||
const clean = cleanToolCallIds(messages);
|
||||
for (; mi >= 0; mi--) {
|
||||
const m = messages[mi]!;
|
||||
if ((m as { role?: string }).role !== "assistant") continue;
|
||||
const assistant = m as AssistantMessage;
|
||||
for (let bi = assistant.content.length - 1; bi >= 0; bi--) {
|
||||
const block = assistant.content[bi]!;
|
||||
if (block.type !== "toolCall" || block.name !== PRACTICE_PROBLEM_TOOL) continue;
|
||||
if (!clean.has(block.id)) continue; // a refused pose revealed nothing
|
||||
const solution = (block.arguments as { modelSolution?: unknown } | undefined)?.modelSolution;
|
||||
return typeof solution === "string" ? solution : undefined;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function activeProblemOf(
|
||||
messages: readonly LhMessage[],
|
||||
): { toolCallId: string; input: unknown } | null {
|
||||
|
||||
+9
-1
@@ -34,6 +34,7 @@
|
||||
|
||||
import type { AssistantMessage, ToolResultMessage } from "@earendil-works/pi-ai";
|
||||
import type { LhMarkerMessage, LhMessage } from "./native-messages.ts";
|
||||
import { modelSolutionForConclude, PRACTICE_CONCLUDE_TOOL } from "./native-messages.ts";
|
||||
import { QUESTION_TOOLS } from "./pending.ts";
|
||||
import type { SubagentArtifactData, SubagentResultMeta } from "./contracts/jobs.ts";
|
||||
import type { PlanBlockData } from "./contracts/chat.ts";
|
||||
@@ -194,7 +195,14 @@ export function projectView(messages: readonly LhMessage[], live?: LiveTurnState
|
||||
} else if (block.type === "thinking") {
|
||||
reasoning.push({ type: "reasoning", id: `${m.id}-r${reasoningN++}`, text: block.thinking, state: "done" });
|
||||
} else if (block.type === "toolCall") {
|
||||
rest.push(toolPartOf(block, live, streamingMessage));
|
||||
const part = toolPartOf(block, live, streamingMessage);
|
||||
// The conclusion's reveal join (owner round, 2026-10-05): the
|
||||
// hero of a conclude card is the modelSolution committed at POSE
|
||||
// time — a record fold, enriched here so the card renders pure.
|
||||
if (block.name === PRACTICE_CONCLUDE_TOOL) {
|
||||
part.reveal = modelSolutionForConclude(messages, block.id);
|
||||
}
|
||||
rest.push(part);
|
||||
}
|
||||
}
|
||||
const parts: LhViewPart[] = [...reasoning, ...rest];
|
||||
|
||||
Reference in New Issue
Block a user