feat: connect scene 9 staged progression

This commit is contained in:
lda
2026-07-12 08:01:17 +07:00 Verified
parent 7615dfaf9b
commit 10c06c882d
5 changed files with 91 additions and 7 deletions
@@ -89,6 +89,68 @@ describe("PresentationRoute", () => {
expect(await screen.findByLabelText("authoring phase rail")).toBeInTheDocument(); expect(await screen.findByLabelText("authoring phase rail")).toBeInTheDocument();
}); });
it("advances Draft to Validate once and projects the edited request", async () => {
window.location.hash = "#scene/prepared-lifecycle/draft";
const { PresentationRoute } = await import("./PresentationRoute.js");
render(<PresentationRoute />);
const message = screen.getByRole("textbox", { name: /message to authoring assistant/i });
await userEvent.clear(message);
await userEvent.type(message, "Check this edited draft request");
await userEvent.click(screen.getByRole("button", { name: /send message/i }));
expect(window.location.hash).toBe("#scene/prepared-lifecycle/validate");
expect(await screen.findByText("Check this edited draft request")).toBeInTheDocument();
expect(screen.getByRole("complementary", { name: /prepared authoring assistant/i }))
.toHaveAttribute("data-phase", "validate");
});
it("advances Artifact to Deployment once and projects the edited request", async () => {
window.location.hash = "#scene/prepared-lifecycle/artifact";
const { PresentationRoute } = await import("./PresentationRoute.js");
render(<PresentationRoute />);
const message = screen.getByRole("textbox", { name: /message to authoring assistant/i });
await userEvent.clear(message);
await userEvent.type(message, "Save this edited deployment request");
await userEvent.click(screen.getByRole("button", { name: /send message/i }));
expect(window.location.hash).toBe("#scene/prepared-lifecycle/deployment");
expect(await screen.findByText("Save this edited deployment request")).toBeInTheDocument();
expect(screen.getByRole("complementary", { name: /prepared authoring assistant/i }))
.toHaveAttribute("data-phase", "deployment");
});
it("records a Deployment run request locally without an RPC", async () => {
window.location.hash = "#scene/prepared-lifecycle/deployment";
const { PresentationRoute } = await import("./PresentationRoute.js");
render(<PresentationRoute />);
await screen.findByRole("textbox", { name: /message to authoring assistant/i });
mockedCallOperation.mockClear();
await userEvent.click(screen.getByRole("button", { name: /send message/i }));
expect(await screen.findByRole("status")).toHaveTextContent(
"Run request prepared for the next execution slice.",
);
expect(mockedCallOperation.mock.calls.some(([operation]) => operation === "workflow.runs.start"))
.toBe(false);
});
it("keeps Validate submission on the current beat", async () => {
window.location.hash = "#scene/prepared-lifecycle/validate";
const { PresentationRoute } = await import("./PresentationRoute.js");
render(<PresentationRoute />);
const message = screen.getByRole("textbox", { name: /message to authoring assistant/i });
await userEvent.type(message, "Keep validating this draft");
await userEvent.click(screen.getByRole("button", { name: /send message/i }));
expect(window.location.hash).toBe("#scene/prepared-lifecycle/validate");
expect(screen.getByRole("complementary", { name: /prepared authoring assistant/i }))
.toHaveAttribute("data-phase", "validate");
});
it("renders audience progress chrome without rail or mode label", async () => { it("renders audience progress chrome without rail or mode label", async () => {
setReplayMode(); setReplayMode();
const { PresentationRoute } = await import("./PresentationRoute.js"); const { PresentationRoute } = await import("./PresentationRoute.js");
@@ -12,7 +12,7 @@ import {
createInitialPresentationState, createInitialPresentationState,
presentationReducer, presentationReducer,
} from "./presentation-state.js"; } from "./presentation-state.js";
import { hashForLocation } from "./storyboard-navigation.js"; import { hashForLocation, nextMainLocation } from "./storyboard-navigation.js";
import type { MainLocation } from "./storyboard.js"; import type { MainLocation } from "./storyboard.js";
import type { DemoApprovalActions, DemoApprovalUiState } from "./demo-approval-actions.js"; import type { DemoApprovalActions, DemoApprovalUiState } from "./demo-approval-actions.js";
import "./presentation.css"; import "./presentation.css";
@@ -232,6 +232,15 @@ export const PresentationRoute = () => {
[], [],
); );
const handleScene9Advance = useCallback(() => {
if (
state.location.kind !== "main"
|| state.location.sceneId !== "prepared-lifecycle"
|| (state.location.beatId !== "draft" && state.location.beatId !== "artifact")
) return;
dispatch({ type: "jump", location: nextMainLocation(state.location) });
}, [state.location]);
const handleOpenDiscussion = useCallback( const handleOpenDiscussion = useCallback(
(branchId: string) => dispatch({ type: "open_discussion", branchId }), (branchId: string) => dispatch({ type: "open_discussion", branchId }),
[], [],
@@ -253,6 +262,7 @@ export const PresentationRoute = () => {
approvalActions={approvalActions} approvalActions={approvalActions}
targetStatus={targetStatus} targetStatus={targetStatus}
jump={handleJump} jump={handleJump}
onScene9Advance={handleScene9Advance}
selectNode={(nodeId) => dispatch({ type: "select_node", nodeId })} selectNode={(nodeId) => dispatch({ type: "select_node", nodeId })}
openEvidence={() => dispatch({ type: "set_evidence_presentation", presentation: "inspector" })} openEvidence={() => dispatch({ type: "set_evidence_presentation", presentation: "inspector" })}
closeOverlay={() => dispatch({ type: "close_overlay" })} closeOverlay={() => dispatch({ type: "close_overlay" })}
@@ -25,6 +25,7 @@ type PresentationStageProps = {
readonly onRequestRevision?: (() => void) | undefined; readonly onRequestRevision?: (() => void) | undefined;
readonly targetStatus: PresentationTargetHealth; readonly targetStatus: PresentationTargetHealth;
readonly jump: (location: MainLocation) => void; readonly jump: (location: MainLocation) => void;
readonly onScene9Advance?: (() => void) | undefined;
readonly selectNode: (nodeId: string | null) => void; readonly selectNode: (nodeId: string | null) => void;
readonly openEvidence: () => void; readonly openEvidence: () => void;
readonly closeOverlay: () => void; readonly closeOverlay: () => void;
@@ -43,6 +44,7 @@ export const PresentationStage = ({
onRequestRevision, onRequestRevision,
targetStatus, targetStatus,
jump, jump,
onScene9Advance,
selectNode, selectNode,
openEvidence, openEvidence,
closeOverlay, closeOverlay,
@@ -87,6 +89,7 @@ export const PresentationStage = ({
}} }}
motionDisabled={state.motionDisabled} motionDisabled={state.motionDisabled}
approvalActions={approvalActions} approvalActions={approvalActions}
onScene9Advance={onScene9Advance}
/> />
)} )}
</section> </section>
@@ -34,6 +34,7 @@ type SceneBodyProps = {
readonly onFocusPathChange: (path: readonly string[]) => void; readonly onFocusPathChange: (path: readonly string[]) => void;
readonly motionDisabled: boolean; readonly motionDisabled: boolean;
readonly approvalActions?: DemoApprovalActions | undefined; readonly approvalActions?: DemoApprovalActions | undefined;
readonly onScene9Advance?: (() => void) | undefined;
}; };
const DiscussionLinks = ({ const DiscussionLinks = ({
@@ -304,7 +305,7 @@ const assertNever = (value: never): never => {
throw new Error(`Unexpected view: ${value}`); throw new Error(`Unexpected view: ${value}`);
}; };
export const SceneBody = ({ location, demo, timelineAgent, selectedNodeId, selectNode, openEvidence, openDiscussion, onFocusPathChange, motionDisabled, approvalActions }: SceneBodyProps) => { export const SceneBody = ({ location, demo, timelineAgent, selectedNodeId, selectNode, openEvidence, openDiscussion, onFocusPathChange, motionDisabled, approvalActions, onScene9Advance }: SceneBodyProps) => {
const sceneId = location.kind === "main" ? location.sceneId : "positioning"; const sceneId = location.kind === "main" ? location.sceneId : "positioning";
const beatId = location.kind === "main" ? location.beatId : "landscape"; const beatId = location.kind === "main" ? location.beatId : "landscape";
const scene = findScene(sceneId) ?? findScene("thesis")!; const scene = findScene(sceneId) ?? findScene("thesis")!;
@@ -342,7 +343,7 @@ export const SceneBody = ({ location, demo, timelineAgent, selectedNodeId, selec
case "agent": case "agent":
return <AgentHandoffScene scene={scene} beat={beat} />; return <AgentHandoffScene scene={scene} beat={beat} />;
case "demo-lifecycle": case "demo-lifecycle":
return <PreparedAuthoringLifecycleScene scene={scene} beat={beat} />; return <PreparedAuthoringLifecycleScene scene={scene} beat={beat} onAdvance={onScene9Advance} />;
case "demo": case "demo":
return ( return (
<DemoWorkflowScene <DemoWorkflowScene
@@ -15,6 +15,7 @@ import { StageCaption } from "../StageCaption.js";
type PreparedAuthoringLifecycleSceneProps = { type PreparedAuthoringLifecycleSceneProps = {
readonly scene: SceneDefinition; readonly scene: SceneDefinition;
readonly beat: SceneBeatDefinition; readonly beat: SceneBeatDefinition;
readonly onAdvance?: (() => void) | undefined;
}; };
const phases: readonly { readonly id: AuthoringPhaseId; readonly label: string }[] = [ const phases: readonly { readonly id: AuthoringPhaseId; readonly label: string }[] = [
@@ -31,7 +32,7 @@ const phases: readonly { readonly id: AuthoringPhaseId; readonly label: string }
* Each beat shows a persistent prepared assistant beside one dominant phase * Each beat shows a persistent prepared assistant beside one dominant phase
* projection sourced from the prepared authoring recording. * projection sourced from the prepared authoring recording.
*/ */
export const PreparedAuthoringLifecycleScene = ({ scene, beat }: PreparedAuthoringLifecycleSceneProps) => { export const PreparedAuthoringLifecycleScene = ({ scene, beat, onAdvance }: PreparedAuthoringLifecycleSceneProps) => {
const [messageState, dispatch] = useReducer( const [messageState, dispatch] = useReducer(
scene9MessageReducer, scene9MessageReducer,
initialScene9MessageState, initialScene9MessageState,
@@ -60,9 +61,16 @@ export const PreparedAuthoringLifecycleScene = ({ scene, beat }: PreparedAuthori
submittedOverrides={projectScene9SubmittedOverrides(messageState)} submittedOverrides={projectScene9SubmittedOverrides(messageState)}
runRequested={messageState.runRequested} runRequested={messageState.runRequested}
onDraftChange={(draft) => dispatch({ type: "draft_edited", draft })} onDraftChange={(draft) => dispatch({ type: "draft_edited", draft })}
onSubmit={() => { onSubmit={(submittedText) => {
if (beatId === "draft") dispatch({ type: "draft_submitted" }); dispatch({ type: "draft_edited", draft: submittedText });
if (beatId === "artifact") dispatch({ type: "artifact_submitted" }); if (beatId === "draft") {
dispatch({ type: "draft_submitted" });
onAdvance?.();
}
if (beatId === "artifact") {
dispatch({ type: "artifact_submitted" });
onAdvance?.();
}
if (beatId === "deployment") dispatch({ type: "run_requested" }); if (beatId === "deployment") dispatch({ type: "run_requested" });
}} }}
/> />