fix: harden presentation replay state
This commit is contained in:
@@ -167,8 +167,7 @@ Implementation order:
|
||||
shows input/interruption/decision only, and resume/output/trace expose
|
||||
scroll-contained proof panes. Implementation:
|
||||
[`presentation demo proof composition`](historical/superpowers/plans/2026-07-09-presentation-demo-proof-composition.md).
|
||||
23. Future: presenter companion, final scene visuals, evidence assets, and
|
||||
rehearsal timing.
|
||||
23. Future: presenter companion and defense evidence assets.
|
||||
24. Add a static slide/appendix shell only after presentation mode is clear.
|
||||
Astro remains an option, not the default next surface.
|
||||
25. Completed: presentation agent authoring story creates a canonical prepared
|
||||
|
||||
@@ -405,7 +405,9 @@ describe("useDemoTimeline", () => {
|
||||
|
||||
it("replay revision request resumes through the negative branch", async () => {
|
||||
vi.useFakeTimers();
|
||||
const { result } = renderHook(() => useDemoTimeline(null, vi.fn()));
|
||||
const { result } = renderHook(() =>
|
||||
useDemoTimeline("http://127.0.0.1:8765/rpc", vi.fn()),
|
||||
);
|
||||
act(() => result.current.setMode("replay"));
|
||||
act(() => result.current.start());
|
||||
for (let i = 0; i < 3; i++) {
|
||||
|
||||
@@ -294,7 +294,7 @@ export const useDemoTimeline = (
|
||||
comment,
|
||||
outcome: "cancelled",
|
||||
};
|
||||
if (target === null) {
|
||||
if (state.mode === "replay") {
|
||||
const recording = activeRecording.current;
|
||||
if (recording) {
|
||||
const revisionRecording = revisionReplayRecording(recording);
|
||||
@@ -312,7 +312,7 @@ export const useDemoTimeline = (
|
||||
return;
|
||||
}
|
||||
dispatch({ type: "continue_review" });
|
||||
}, [projectTransientState, resetRuntime, target]);
|
||||
}, [projectTransientState, resetRuntime, state.mode]);
|
||||
|
||||
const restart = useCallback(() => {
|
||||
resetRuntime();
|
||||
|
||||
@@ -341,9 +341,10 @@ describe("PresentationRoute", () => {
|
||||
expect(screen.getByRole("button", { name: "Request revision" })).toBeEnabled();
|
||||
});
|
||||
|
||||
it("requests revision and resumes Scene 10 through the negative branch", async () => {
|
||||
it("keeps a replay fallback on the revision-requested branch despite a configured live target", async () => {
|
||||
const user = userEvent.setup();
|
||||
setReplayMode();
|
||||
window.sessionStorage.setItem("lda.workflowConsole.target", "http://127.0.0.1:8765/rpc");
|
||||
mockedCallOperation.mockRejectedValue(new Error("target unavailable"));
|
||||
window.location.hash = "#scene/typed-human-boundary/approval";
|
||||
const { PresentationRoute } = await import("./PresentationRoute.js");
|
||||
render(<PresentationRoute />);
|
||||
|
||||
@@ -186,7 +186,7 @@ export const PresentationRoute = () => {
|
||||
|
||||
setApprovalState("revision_requested");
|
||||
await demo.requestRevision("Request revisions before creating issues.");
|
||||
if (presentationTarget.mode === "live") await demo.next();
|
||||
if (demo.state.mode === "live") await demo.next();
|
||||
dispatch({
|
||||
type: "jump",
|
||||
location: {
|
||||
|
||||
@@ -83,4 +83,16 @@ describe("PreparedAuthoringLifecycleScene", () => {
|
||||
expect(active).toBeInTheDocument();
|
||||
expect(active).toHaveTextContent("Deployment");
|
||||
});
|
||||
|
||||
it("falls back to discovery for an unexpected storyboard beat", () => {
|
||||
const scene = findScene("prepared-lifecycle")!;
|
||||
const knownBeat = findBeat("prepared-lifecycle", "discover")!;
|
||||
const unexpectedBeat = { ...knownBeat, id: "unexpected" };
|
||||
|
||||
render(<PreparedAuthoringLifecycleScene scene={scene} beat={unexpectedBeat} />);
|
||||
|
||||
expect(screen.getByRole("region", { name: "discovery evidence" })).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("authoring phase rail").querySelector("[data-active='true']"))
|
||||
.toHaveTextContent("Discover");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,7 +26,9 @@ const phases: readonly { readonly id: AuthoringPhaseId; readonly label: string }
|
||||
* remains below the canvas as a beat-synchronized assistant dock.
|
||||
*/
|
||||
export const PreparedAuthoringLifecycleScene = ({ scene, beat }: PreparedAuthoringLifecycleSceneProps) => {
|
||||
const beatId = beat.id as AuthoringPhaseId;
|
||||
// Storyboard beats normally match these IDs. Discovery is a safe projection
|
||||
// if a future beat reaches this scene before its authoring mapping is added.
|
||||
const beatId = phases.find((phase) => phase.id === beat.id)?.id ?? "discover";
|
||||
const projection = projectPreparedAuthoringPhase(beatId);
|
||||
|
||||
return (
|
||||
|
||||
@@ -49,8 +49,10 @@ describe("projectPreparedAuthoringPhase", () => {
|
||||
const diagnosticCmd = phase.commands.find(
|
||||
(cmd) => cmd.result === "diagnostic" && cmd.detail?.includes("no state projection"),
|
||||
);
|
||||
const repairCmd = phase.commands.find((cmd) => cmd.command.includes("draft set-output"));
|
||||
expect(diagnosticCmd).toBeDefined();
|
||||
expect(phase.commands.some((cmd) => cmd.command.includes("draft set-output"))).toBe(true);
|
||||
expect(repairCmd).toBeDefined();
|
||||
expect(diagnosticCmd).not.toBe(repairCmd);
|
||||
});
|
||||
|
||||
it("uses real public command syntax from the recording", () => {
|
||||
|
||||
@@ -142,11 +142,7 @@ describe("AssistantOperatorThread", () => {
|
||||
|
||||
it("scrolls the active authoring group into the dock viewport", async () => {
|
||||
const setScrollTop = vi.fn();
|
||||
Object.defineProperty(HTMLDivElement.prototype, "scrollTop", {
|
||||
configurable: true,
|
||||
get: () => 0,
|
||||
set: setScrollTop,
|
||||
});
|
||||
const originalDescriptor = Object.getOwnPropertyDescriptor(HTMLDivElement.prototype, "scrollTop");
|
||||
const messages: ReadonlyArray<AgentMessage> = [
|
||||
{
|
||||
id: "authoring-draft-tools",
|
||||
@@ -158,16 +154,47 @@ describe("AssistantOperatorThread", () => {
|
||||
},
|
||||
];
|
||||
|
||||
render(
|
||||
<AssistantOperatorThread
|
||||
mode="dock"
|
||||
surface="dock"
|
||||
messages={messages}
|
||||
activeToolGroupId="authoring-draft"
|
||||
/>,
|
||||
);
|
||||
try {
|
||||
Object.defineProperty(HTMLDivElement.prototype, "scrollTop", {
|
||||
configurable: true,
|
||||
get: () => 0,
|
||||
set: setScrollTop,
|
||||
});
|
||||
render(
|
||||
<AssistantOperatorThread
|
||||
mode="dock"
|
||||
surface="dock"
|
||||
messages={messages}
|
||||
activeToolGroupId="authoring-draft"
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => expect(setScrollTop).toHaveBeenCalledWith(0));
|
||||
await waitFor(() => expect(setScrollTop).toHaveBeenCalledWith(0));
|
||||
} finally {
|
||||
if (originalDescriptor) {
|
||||
Object.defineProperty(HTMLDivElement.prototype, "scrollTop", originalDescriptor);
|
||||
} else {
|
||||
delete (HTMLDivElement.prototype as { scrollTop?: number }).scrollTop;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("pairs a lone tool call with its result", () => {
|
||||
const messages: ReadonlyArray<AgentMessage> = [
|
||||
{
|
||||
id: "assistant-tool-result",
|
||||
role: "assistant",
|
||||
parts: [
|
||||
{ type: "tool-call", call: { id: "call-1", name: "readRunTrace", input: {} } },
|
||||
{ type: "tool-result", result: { callId: "call-1", name: "readRunTrace", status: "success", output: { frames: 3 } } },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const { container } = render(<AssistantOperatorThread mode="dock" messages={messages} />);
|
||||
|
||||
expect(container.querySelector('[data-slot="tool-fallback-result"]')).toBeInTheDocument();
|
||||
expect(screen.getByText(/"frames": 3/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders structured tool results through the generated fallback result slot", () => {
|
||||
|
||||
@@ -157,7 +157,7 @@ const AssistantMessageBody = ({
|
||||
const part = parts[index]!;
|
||||
if (part.type === "text") {
|
||||
rendered.push(
|
||||
<p key={`text-${part.text}`} style={{ whiteSpace: "pre-line" }}>{part.text}</p>,
|
||||
<p key={`text-${index}`} style={{ whiteSpace: "pre-line" }}>{part.text}</p>,
|
||||
);
|
||||
index += 1;
|
||||
continue;
|
||||
@@ -174,9 +174,14 @@ const AssistantMessageBody = ({
|
||||
const logicalTools = calls.length > 0 ? calls : toolRun;
|
||||
|
||||
if (logicalTools.length === 1 && !messageId.startsWith("authoring-")) {
|
||||
const tool = logicalTools[0]!;
|
||||
const pairedResult = tool.type === "tool-call"
|
||||
? toolRun.find((candidate): candidate is Extract<ToolRenderPart, { readonly type: "tool-result" }> =>
|
||||
candidate.type === "tool-result" && candidate.toolCallId === tool.toolCallId)
|
||||
: undefined;
|
||||
rendered.push(
|
||||
<div key={`tool-${logicalTools[0]!.toolCallId ?? logicalTools[0]!.toolName}`}>
|
||||
{renderContentPart(logicalTools[0]!, submitApproval, requestRevision)}
|
||||
<div key={`tool-${tool.toolCallId ?? tool.toolName}`}>
|
||||
{renderContentPart(tool, submitApproval, requestRevision, undefined, pairedResult)}
|
||||
</div>,
|
||||
);
|
||||
continue;
|
||||
|
||||
Reference in New Issue
Block a user