fix: address presentation review findings

This commit is contained in:
lda
2026-07-11 05:17:35 +07:00 Verified
parent 531f934288
commit 371377716c
18 changed files with 140 additions and 61 deletions
@@ -91,7 +91,7 @@ export const useTimelineAgent = (
const canRun = demo.canStart && !demo.inFlight && demo.state.phase !== "running";
const runPreparedWorkflow = useCallback(async () => {
if (!demo.canStart || demo.inFlight) return;
if (!demo.canStart || demo.inFlight || demo.state.phase === "running") return;
demo.start(modeLabel);
setMessages((current) => appendToolMessage(
current,
@@ -41,6 +41,7 @@ export const DemoOutcomePanel = ({
<span>{lens.eyebrow}</span>
<strong>Same persisted run</strong>
<dl>
<div><dt>Run</dt><dd><code>{runId}</code></dd></div>
<div><dt>Operation</dt><dd><code>{operation?.operation ?? lens.proofLabel}</code></dd></div>
<div><dt>Status</dt><dd>{operation?.status ?? "completed"}</dd></div>
</dl>
@@ -65,7 +65,6 @@ export const DiscussionPanel = ({ branchId, onClose }: DiscussionPanelProps) =>
<span className="discussion-panel__badge">{branch.claimClass}</span>
<h2>{branch.title}</h2>
</div>
<p>{branch.summary}</p>
</header>
<main className="discussion-panel__body" aria-label="discussion body">
@@ -72,8 +72,8 @@ export const GuidedProductMoment = ({
<InterruptDecisionForm
interrupt={facts.interrupt}
runId={demo.state.events.find((e) => e.stage === "run_start")?.resultingIds.runId ?? "unknown"}
onSubmit={(ids, comment) => approvalActions?.submit(ids, comment)}
onCancel={() => approvalActions?.cancel()}
onSubmit={approvalActions?.canSubmit ? (ids, comment) => approvalActions.submit(ids, comment) : undefined}
onCancel={approvalActions?.canCancel ? () => approvalActions.cancel() : undefined}
terminalOutcome={approvalActions?.state === "submitted" ? "submitted" :
approvalActions?.state === "cancelled" ? "cancelled" : undefined}
/>
@@ -4,8 +4,8 @@ import type { RunFactsInterrupt } from "./demo-run-facts.js";
type InterruptDecisionFormProps = {
readonly interrupt: RunFactsInterrupt;
readonly runId: string;
readonly onSubmit: (selectedIssueIds: ReadonlyArray<string>, comment: string) => void;
readonly onCancel: () => void;
readonly onSubmit?: ((selectedIssueIds: ReadonlyArray<string>, comment: string) => void) | undefined;
readonly onCancel?: (() => void) | undefined;
readonly terminalOutcome?: "submitted" | "cancelled" | undefined;
};
@@ -33,7 +33,7 @@ export const InterruptDecisionForm = ({
const handleSubmit = useCallback(
(e: React.FormEvent) => {
e.preventDefault();
onSubmit([...selectedIds], comment);
onSubmit?.([...selectedIds], comment);
},
[selectedIds, comment, onSubmit],
);
@@ -103,13 +103,14 @@ export const InterruptDecisionForm = ({
</label>
<div className="interrupt-decision-form__actions">
<button type="submit" className="interrupt-decision-form__submit">
<button type="submit" className="interrupt-decision-form__submit" disabled={!onSubmit}>
Submit
</button>
<button
type="button"
className="interrupt-decision-form__cancel"
onClick={onCancel}
onClick={() => onCancel?.()}
disabled={!onCancel}
>
Cancel
</button>
@@ -36,8 +36,12 @@ export const OperatorChat = ({ state, messages, timelineAgent, onApprove, onDeny
: timelineAgent && timelineAgent.messages.length > 0
? timelineAgent.messages
: fallbackMessages(state);
const submit = timelineAgent?.submitSelectedIssues ?? onApprove;
const cancel = timelineAgent?.cancelReview ?? onDeny;
const submit = timelineAgent
? () => { timelineAgent.submitSelectedIssues().catch(console.error); }
: onApprove;
const cancel = timelineAgent
? () => { timelineAgent.cancelReview().catch(console.error); }
: onDeny;
const composition = compositionForState(state);
const presentationSurface = composition.chatTheme === "light" ? "editorial" : "night";
return (
@@ -34,7 +34,7 @@ const formatValue = (value: unknown): string | null => {
if (value === undefined) return null;
if (typeof value === "string") return value;
if (typeof value === "number" || typeof value === "boolean" || value === null) return String(value);
return JSON.stringify(value);
return JSON.stringify(value) ?? null;
};
const fieldKind = (propertySchema: unknown): SchemaApprovalFieldKind => {
@@ -0,0 +1,44 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { findBeat, findScene } from "../storyboard.js";
import { AgentHandoffScene } from "./AgentHandoffScene.js";
const renderBeat = (beatId: "request" | "handoff") => {
const scene = findScene("agent-handoff");
const beat = findBeat("agent-handoff", beatId);
if (!scene || !beat) throw new Error(`missing agent-handoff/${beatId}`);
return render(<AgentHandoffScene scene={scene} beat={beat} />);
};
describe("AgentHandoffScene", () => {
it("renders a log region named prepared authoring conversation", () => {
renderBeat("request");
expect(screen.getByRole("log", { name: "prepared authoring conversation" })).toBeInTheDocument();
});
it("renders separated user and assistant turns on the request beat", () => {
renderBeat("request");
const userMessages = screen.getAllByText(/report|workflow|prepare/i);
expect(userMessages.length).toBeGreaterThanOrEqual(1);
const assistantMessages = screen.getAllByText(/inspect|capabilities|sources|schemas|let me/i);
expect(assistantMessages.length).toBeGreaterThanOrEqual(1);
});
it("renders the full conversation on the handoff beat", () => {
renderBeat("handoff");
const userMessages = screen.getAllByText(/report|workflow|prepare|save/i);
expect(userMessages.length).toBeGreaterThanOrEqual(1);
const assistantMessages = screen.getAllByText(/inspect|sources|capabilities|compile|deployment|artifact/i);
expect(assistantMessages.length).toBeGreaterThanOrEqual(2);
});
it("does not render prepared workflow lifecycle content", () => {
renderBeat("request");
expect(screen.queryByText("prepared workflow lifecycle")).not.toBeInTheDocument();
});
it("does not include prepared workflow lifecycle content on handoff", () => {
renderBeat("handoff");
expect(screen.queryByText("prepared workflow lifecycle")).not.toBeInTheDocument();
});
});
@@ -0,0 +1,50 @@
import { useMemo } from "react";
import { agentTextMessage } from "../../demo/agent/events.js";
import { AssistantOperatorThread } from "../chat/AssistantOperatorThread.js";
import { projectPreparedAuthoring } from "./authoring-recording.js";
import type { SceneBeatDefinition, SceneDefinition } from "../storyboard.js";
type AgentHandoffSceneProps = {
readonly scene: SceneDefinition;
readonly beat: SceneBeatDefinition;
};
const requestMessages = [
agentTextMessage("handoff-user-1", "user", "We need to prepare a report workflow for the lda_report scenario. Use the available CLI tools to inspect, author, and deploy it."),
agentTextMessage("handoff-assistant-1", "assistant", "Let me inspect the available sources, capabilities, and schemas first."),
];
/**
* Full-screen prepared-authoring conversation for Scene 8.
*
* The request beat shows the operator asking the agent to prepare a report;
* the handoff beat reveals the full completed conversation with all phases.
* Neither run actions nor approval actions are passed because this is a
* prepared recording, not a live agent interaction.
*/
export const AgentHandoffScene = ({ beat }: AgentHandoffSceneProps) => {
const messages = useMemo(() => {
if (beat.id === "handoff") {
const recording = projectPreparedAuthoring();
const result: ReturnType<typeof agentTextMessage>[] = [];
let index = 0;
for (const phase of recording) {
for (const turn of phase.conversation) {
result.push(
agentTextMessage(`msg-${index++}`, turn.role, turn.text),
);
}
}
return result;
}
return requestMessages;
}, [beat.id]);
return (
<AssistantOperatorThread
mode="full"
messages={messages}
ariaLabel="prepared authoring conversation"
/>
);
};
@@ -130,7 +130,7 @@ describe("assistantRuntimeProjection", () => {
expect(projected[0]?.content).toEqual([
{
type: "tool-call",
toolCallId: "presentation-selectWorkflowNode",
toolCallId: "presentation-mixed-0-selectWorkflowNode",
toolName: "presentation.selectWorkflowNode",
args: { type: "selectWorkflowNode", nodeId: "review_issues" },
},
@@ -30,7 +30,7 @@ export type AssistantProjectedMessage = {
readonly content: ReadonlyArray<AssistantContentPart>;
};
const projectPart = (part: AgentMessagePart): AssistantContentPart[] => {
const projectPart = (part: AgentMessagePart, messageId: string, partIndex: number): AssistantContentPart[] => {
switch (part.type) {
case "text":
return [{ type: "text", text: part.text }];
@@ -52,7 +52,7 @@ const projectPart = (part: AgentMessagePart): AssistantContentPart[] => {
case "presentation-action":
return [{
type: "tool-call",
toolCallId: `presentation-${part.action.type}`,
toolCallId: `presentation-${messageId}-${partIndex}-${part.action.type}`,
toolName: `presentation.${part.action.type}`,
args: part.action,
}];
@@ -82,7 +82,7 @@ export const projectAgentMessagesForAssistant = (
messages: ReadonlyArray<AgentMessage>,
): AssistantProjectedMessage[] =>
messages.map((message) => {
const content = message.parts.flatMap(projectPart);
const content = message.parts.flatMap((part, index) => projectPart(part, message.id, index));
return {
id: message.id,
role: messageRoleFor(message),
@@ -201,7 +201,7 @@ const readOutputFacts = (
};
};
const formatRecord = (record: Record<string, unknown>, absentLabel: string): string =>
const formatRecord = (record: Record<string, unknown> | undefined, absentLabel: string): string =>
formatFactValue(record, absentLabel);
const readTraceFacts = (
@@ -225,11 +225,11 @@ const readTraceFacts = (
stepType: stringField(frame, "stepType") ?? "unknown",
outcome: stringField(frame, "outcome") ?? "unknown",
resolvedInputLabel: formatRecord(
recordField(frame, "resolvedInput") ?? {},
recordField(frame, "resolvedInput"),
"not captured in this recording",
),
outputLabel: formatRecord(recordField(frame, "output") ?? {}, "not captured in this recording"),
stateChangesLabel: formatRecord(recordField(frame, "stateChanges") ?? {}, "not captured in this recording"),
outputLabel: formatRecord(recordField(frame, "output"), "not captured in this recording"),
stateChangesLabel: formatRecord(recordField(frame, "stateChanges"), "not captured in this recording"),
}];
}),
};
@@ -89,7 +89,10 @@ const renderFigure = (overrides: Partial<React.ComponentProps<typeof Interactive
};
};
afterEach(() => cleanup());
afterEach(() => {
cleanup();
vi.restoreAllMocks();
});
describe("InteractiveFigure", () => {
it("renders conceptual labels and hides evidence pointers by default", () => {
@@ -241,8 +244,5 @@ describe("InteractiveFigure", () => {
renderFigure({ focusPath: [], size: "stage" });
expect(requestAnimationFrameSpy).toHaveBeenCalled();
requestAnimationFrameSpy.mockRestore();
cancelAnimationFrameSpy.mockRestore();
});
});
@@ -163,7 +163,8 @@ export const demoSurfaceForBeat = (
if (sceneId === "resume-output-evidence") {
if (beatId === "resume") return { primarySurface: "resume-decision", supportSurface: "output-summary" };
if (beatId === "output") return { primarySurface: "workflow-output", supportSurface: "none" };
return { primarySurface: "trace-evidence", supportSurface: "output-summary" };
if (beatId === "trace") return { primarySurface: "trace-evidence", supportSurface: "output-summary" };
throw new Error(`Unknown beat ${beatId} for scene resume-output-evidence`);
}
return { primarySurface: "none", supportSurface: "none" };
};
@@ -2,7 +2,7 @@ import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
const css = readFileSync(join(import.meta.dirname, "presentation.css"), "utf8");
const css = readFileSync(join(import.meta.dirname, "presentation.css"), "utf8").replace(/\r\n/g, "\n");
describe("presentation.css", () => {
it("allows hidden primary-region scrolling for browser zoom overflow", () => {
@@ -8,8 +8,12 @@ export type PresentationTargetHealth =
export type TargetProbeState = "none" | "checking" | "ready" | "failed";
const shortTarget = (target: string): string => {
const url = new URL(target);
return `${url.hostname}:${url.port || (url.protocol === "https:" ? "443" : "80")}`;
try {
const url = new URL(target);
return `${url.hostname}:${url.port || (url.protocol === "https:" ? "443" : "80")}`;
} catch {
return target;
}
};
export const presentationTargetHealth = ({
@@ -2417,17 +2417,7 @@
letter-spacing: 0.04em;
}
/* Factual Scene 10 layout grids */
.guided-product-moment__approval-grid {
display: grid;
grid-template-columns: minmax(13rem, 0.56fr) minmax(0, 1.44fr);
gap: 0.75rem;
align-items: stretch;
min-height: 0;
max-height: 100%;
overflow: hidden;
}
/* Child min-height for all fact grids */
.guided-product-moment__approval-grid > *,
.guided-product-moment__resume-grid > *,
.guided-product-moment[data-moment="output"] > .guided-product-moment__primary > * {
@@ -2557,16 +2547,6 @@
font-size: 0.78rem;
}
.guided-product-moment__resume-grid {
display: grid;
grid-template-columns: 1.4fr 1fr;
gap: 1rem;
align-items: stretch;
min-height: 0;
max-height: 100%;
overflow: hidden;
}
.guided-product-moment[data-moment="resume"] .run-facts-card,
.guided-product-moment[data-moment="output"] .run-facts-card {
max-height: 100%;
@@ -2595,9 +2575,4 @@
display: none;
}
@media (max-width: 900px) {
.guided-product-moment__approval-grid,
.guided-product-moment__resume-grid {
grid-template-columns: 1fr;
}
}
/* Responsive grid behavior lives in demo-workflow.css container query */