fix: address presentation review findings
This commit is contained in:
@@ -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 */
|
||||
|
||||
Reference in New Issue
Block a user