artifacts: the height ratchet fixed, invert switch persisted + restyled (release polish 7.2)

- bridge measures the BODY (scrollHeight + rect + margins) instead of the
  viewport-clamped documentElement — documentElement.scrollHeight latches
  at the parent's own height, so demos could grow but never shrink on
  window resize (owner-reported); smoke: 1036 → 2386 → 1036 px
- invert now PERSISTS as details.ui.inverted on the steer record, written
  by the silent artifact-ui route (dismissal pattern: no wake, no SSE) and
  seeded through the projection (SubagentArtifactData.ui); survives
  navigation + reload, never enters teacher context or exports
- invert restyled as the house Toggle switch (settings controls) on both
  cards; demo header is the caption grammar: 'INTERACTIVE DEMO — title'
  left (title normal-case, same size/tracking), switch + download far
  right; figure header: label left, switch/download/open far right

1439 tests green (route + pure-seam pins added); live smoke passed
This commit is contained in:
amosblomqvist
2026-10-07 19:13:24 +03:00
parent 4070cc7926
commit 34f498b128
8 changed files with 312 additions and 61 deletions
+24
View File
@@ -271,12 +271,36 @@ export interface SubagentResultMeta {
export interface SubagentArtifactData {
kind: JobKind;
result: ResearchResult | DemoResult | VisualResult | PdfResult;
/** The learner's pinned UI state (invert switch) riding the record. */
ui?: { inverted?: boolean };
}
/** A result steer's marker details: the UI's meta + artifact in one
* payload (the projection splits it into metadata + data part). */
export interface SteerDetails extends SubagentResultMeta {
result: ResearchResult | DemoResult | VisualResult | PdfResult;
/** LEARNER UI state pinned onto the artifact (owner, 2026-10-07): the
* invert switch's position, persisted so navigating away and back (or
* restarting) keeps it. A VIEW preference — never enters the teacher's
* context, never exports (the markdown engine reads `result` only). */
ui?: { inverted?: boolean };
}
/** Set a steer message's ui.inverted (pure): returns a NEW message array
* with the target's details updated — no-op (same array) when the message
* is missing or not an invertible artifact steer. The silent route's
* whole body. */
export function setSteerUiInverted(
messages: readonly LhMessage[],
messageId: string,
inverted: boolean,
): LhMessage[] {
return messages.map((m) => {
if (m.id !== messageId) return m;
const d = (m as LhMarkerMessage).details as Partial<SteerDetails> | undefined;
if (!d || (d.kind !== "demo" && d.kind !== "visual")) return m;
return { ...m, details: { ...d, ui: { ...d.ui, inverted } } } as LhMessage;
});
}
/** Anything the wake pipeline may queue and deliver — a native marker. */
+6 -1
View File
@@ -368,6 +368,7 @@ function projectMarker(marker: LhMarkerMessage, out: LhViewMessage[]): void {
const artifact = marker.details as {
kind: SubagentArtifactData["kind"];
result: SubagentArtifactData["result"];
ui?: SubagentArtifactData["ui"];
};
out.push({
id: marker.id,
@@ -375,7 +376,11 @@ function projectMarker(marker: LhMarkerMessage, out: LhViewMessage[]): void {
metadata: { subagentResult: meta },
parts: [
textPart(marker.content),
{ type: "data-subagent-artifact", id: `sa_${marker.id}`, data: { kind: artifact.kind, result: artifact.result } },
{
type: "data-subagent-artifact",
id: `sa_${marker.id}`,
data: { kind: artifact.kind, result: artifact.result, ui: artifact.ui },
},
],
});
return;