feat: connect prepared run and refine presenter notes
This commit is contained in:
@@ -2,6 +2,7 @@ import { AssistantOperatorThread } from "../chat/AssistantOperatorThread.js";
|
||||
import {
|
||||
authoringToolGroupId,
|
||||
projectPreparedAuthoringThread,
|
||||
projectPreparedRunRequestExchange,
|
||||
type AuthoringPhaseId,
|
||||
} from "./authoring-recording.js";
|
||||
import {
|
||||
@@ -17,6 +18,7 @@ type AuthoringConversationProps = {
|
||||
readonly requestOverride?: string | undefined;
|
||||
readonly requestOverrides?: PreparedLifecycleSubmittedOverrides | undefined;
|
||||
readonly scrollMode?: "active" | "start" | undefined;
|
||||
readonly runRequested?: string | null | undefined;
|
||||
};
|
||||
|
||||
/** Renders the same prepared conversation at full-stage or compact-dock scale. */
|
||||
@@ -27,17 +29,25 @@ export const AuthoringConversation = ({
|
||||
requestOverride,
|
||||
requestOverrides,
|
||||
scrollMode,
|
||||
}: AuthoringConversationProps) => (
|
||||
<AssistantOperatorThread
|
||||
mode={surface === "stage" ? "full" : "dock"}
|
||||
surface={surface}
|
||||
messages={projectPreparedAuthoringThread(
|
||||
runRequested,
|
||||
}: AuthoringConversationProps) => {
|
||||
const messages = [
|
||||
...projectPreparedAuthoringThread(
|
||||
recordingPhaseForStep(throughPhase),
|
||||
requestOverride,
|
||||
requestOverrides,
|
||||
)}
|
||||
activeToolGroupId={authoringToolGroupId(recordingPhaseForStep(activePhase))}
|
||||
scrollMode={scrollMode}
|
||||
ariaLabel="prepared authoring conversation"
|
||||
/>
|
||||
);
|
||||
),
|
||||
...(runRequested ? projectPreparedRunRequestExchange(runRequested) : []),
|
||||
];
|
||||
|
||||
return (
|
||||
<AssistantOperatorThread
|
||||
mode={surface === "stage" ? "full" : "dock"}
|
||||
surface={surface}
|
||||
messages={messages}
|
||||
activeToolGroupId={authoringToolGroupId(recordingPhaseForStep(activePhase))}
|
||||
scrollMode={runRequested ? "end" : scrollMode}
|
||||
ariaLabel="prepared authoring conversation"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
+22
-1
@@ -7,7 +7,12 @@ import { PreparedAuthoringLifecycleScene } from "./PreparedAuthoringLifecycleSce
|
||||
|
||||
afterEach(() => cleanup());
|
||||
|
||||
const renderBeat = (beatId: string, onAdvance?: () => void, discussionRail?: ReactNode) => {
|
||||
const renderBeat = (
|
||||
beatId: string,
|
||||
onAdvance?: () => void,
|
||||
discussionRail?: ReactNode,
|
||||
onRunPreparedWorkflow?: () => Promise<void>,
|
||||
) => {
|
||||
const scene = findScene("prepared-lifecycle");
|
||||
const beat = findBeat("prepared-lifecycle", beatId);
|
||||
if (!scene || !beat) throw new Error(`missing prepared-lifecycle/${beatId}`);
|
||||
@@ -16,6 +21,7 @@ const renderBeat = (beatId: string, onAdvance?: () => void, discussionRail?: Rea
|
||||
scene={scene}
|
||||
beat={beat}
|
||||
onAdvance={onAdvance}
|
||||
onRunPreparedWorkflow={onRunPreparedWorkflow}
|
||||
discussionRail={discussionRail}
|
||||
/>,
|
||||
);
|
||||
@@ -173,6 +179,21 @@ describe("PreparedAuthoringLifecycleScene", () => {
|
||||
expect(onAdvance).toHaveBeenCalledTimes(advances ? 1 : 0);
|
||||
});
|
||||
|
||||
it("answers the terminal run request without advancing the presentation", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onAdvance = vi.fn();
|
||||
const onRunPreparedWorkflow = vi.fn().mockResolvedValue(undefined);
|
||||
renderBeat("deployment", onAdvance, undefined, onRunPreparedWorkflow);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /send message/i }));
|
||||
|
||||
expect(screen.getByRole("textbox", { name: /message to authoring assistant/i })).toHaveValue("");
|
||||
expect(screen.getByRole("button", { name: /run.*1 tool call/i })).toBeInTheDocument();
|
||||
expect(screen.getByText(/three proposed issues are ready for review/i)).toBeInTheDocument();
|
||||
expect(onRunPreparedWorkflow).toHaveBeenCalledOnce();
|
||||
expect(onAdvance).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each([
|
||||
["discover", "Discover"],
|
||||
["draft", "Draft"],
|
||||
|
||||
@@ -18,6 +18,7 @@ type PreparedAuthoringLifecycleSceneProps = {
|
||||
readonly scene: SceneDefinition;
|
||||
readonly beat: SceneBeatDefinition;
|
||||
readonly onAdvance?: (() => void) | undefined;
|
||||
readonly onRunPreparedWorkflow?: (() => Promise<void>) | undefined;
|
||||
readonly discussionRail?: ReactNode;
|
||||
};
|
||||
|
||||
@@ -40,7 +41,7 @@ const steps = [
|
||||
* Each beat shows a persistent prepared assistant beside one dominant phase
|
||||
* projection sourced from the prepared authoring recording.
|
||||
*/
|
||||
export const PreparedAuthoringLifecycleScene = ({ scene, beat, onAdvance, discussionRail }: PreparedAuthoringLifecycleSceneProps) => {
|
||||
export const PreparedAuthoringLifecycleScene = ({ scene, beat, onAdvance, onRunPreparedWorkflow, discussionRail }: PreparedAuthoringLifecycleSceneProps) => {
|
||||
const [messageState, dispatch] = useReducer(
|
||||
preparedLifecycleMessageReducer,
|
||||
initialPreparedLifecycleMessageState,
|
||||
@@ -81,7 +82,12 @@ export const PreparedAuthoringLifecycleScene = ({ scene, beat, onAdvance, discus
|
||||
dispatch({ type: "artifact_submitted" });
|
||||
onAdvance?.();
|
||||
}
|
||||
if (step === "deployment") dispatch({ type: "run_requested" });
|
||||
if (step === "deployment") {
|
||||
dispatch({ type: "run_requested" });
|
||||
// This is the same mode-aware action as the footer control; the
|
||||
// presentation remains on this beat so the presenter advances it.
|
||||
void onRunPreparedWorkflow?.();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="prepared-lifecycle-scene__presentation">
|
||||
|
||||
@@ -41,7 +41,8 @@ describe("PresentationAssistantPane", () => {
|
||||
);
|
||||
expect(screen.getByRole("heading", { name: /authoring assistant/i })).toBeInTheDocument();
|
||||
expect(screen.getByText(/current phase: validate/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/prepared replay only/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/prepared assistant transcript/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/final run request uses the configured demo target/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("exposes an explicit support role when composed in the lifecycle scene", () => {
|
||||
@@ -129,6 +130,15 @@ describe("PresentationAssistantPane", () => {
|
||||
cleanup();
|
||||
renderPane("deployment", { runRequested: "Run this deployment" });
|
||||
expect(screen.getByRole("button", { name: /send message/i })).toBeDisabled();
|
||||
expect(screen.getByText("Run this deployment")).toBeInTheDocument();
|
||||
expect(screen.getByText(/starting lda_report_case_study\.default with the prepared input/i))
|
||||
.toBeInTheDocument();
|
||||
const runTools = screen.getByRole("button", { name: /run.*1 tool call/i });
|
||||
expect(runTools).toHaveAttribute("aria-expanded", "false");
|
||||
await user.click(runTools);
|
||||
await user.click(screen.getByRole("button", { name: /startRun/i }));
|
||||
expect(screen.getByText(/run_recorded_lda_report/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/paused at the typed issue-review boundary/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders submitted overrides while keeping the replay tools canonical", () => {
|
||||
|
||||
@@ -58,7 +58,10 @@ export const PresentationAssistantPane = ({
|
||||
};
|
||||
const { submit, handleKeyDown } = usePreparedComposerSubmit(
|
||||
canSubmit,
|
||||
() => onSubmit(draft),
|
||||
() => {
|
||||
onSubmit(draft);
|
||||
setDraft("");
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -74,7 +77,7 @@ export const PresentationAssistantPane = ({
|
||||
<h2>Authoring assistant</h2>
|
||||
<p>Current phase: {phaseLabels[phase]}</p>
|
||||
<p className="presentation-assistant-pane__disclosure">
|
||||
Prepared replay only. No live actions or RPC calls.
|
||||
Prepared assistant transcript. The final run request uses the configured demo target.
|
||||
</p>
|
||||
</header>
|
||||
<div className="presentation-assistant-pane__conversation">
|
||||
@@ -83,6 +86,7 @@ export const PresentationAssistantPane = ({
|
||||
activePhase={phase}
|
||||
surface="stage"
|
||||
requestOverrides={submittedOverrides}
|
||||
runRequested={runRequested}
|
||||
/>
|
||||
</div>
|
||||
<form className="presentation-assistant-pane__composer" onSubmit={submit}>
|
||||
@@ -106,7 +110,7 @@ export const PresentationAssistantPane = ({
|
||||
</p>
|
||||
{runRequested !== null ? (
|
||||
<p role="status" className="presentation-assistant-pane__run-status">
|
||||
Run request prepared for the next execution slice.
|
||||
Prepared run receipt added. Continue when you are ready.
|
||||
</p>
|
||||
) : null}
|
||||
</form>
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
authoringToolGroupId,
|
||||
projectPreparedAuthoring,
|
||||
projectPreparedAuthoringThread,
|
||||
projectPreparedRunRequestExchange,
|
||||
type AuthoringPhaseId,
|
||||
} from "./authoring-recording.js";
|
||||
|
||||
@@ -225,3 +226,27 @@ describe("projectPreparedAuthoringThread", () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("projectPreparedRunRequestExchange", () => {
|
||||
it("projects the prepared start receipt and typed interrupt without navigation actions", () => {
|
||||
const messages = projectPreparedRunRequestExchange("Run the saved deployment.");
|
||||
const parts = messages.flatMap((message) => message.parts);
|
||||
|
||||
expect(messages[0]).toMatchObject({ role: "user", parts: [{ text: "Run the saved deployment." }] });
|
||||
expect(parts).toContainEqual(expect.objectContaining({
|
||||
type: "tool-call",
|
||||
call: expect.objectContaining({ name: "startRun" }),
|
||||
}));
|
||||
expect(parts).toContainEqual(expect.objectContaining({
|
||||
type: "tool-result",
|
||||
result: expect.objectContaining({
|
||||
output: expect.objectContaining({
|
||||
run_id: "run_recorded_lda_report",
|
||||
status: "interrupted",
|
||||
resume_readiness: "ready",
|
||||
}),
|
||||
}),
|
||||
}));
|
||||
expect(parts.some((part) => part.type === "presentation-action")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -354,5 +354,62 @@ export const projectPreparedAuthoringThread = (
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Projects the terminal prepared request into the same chat vocabulary as a
|
||||
* live run, while retaining the stable replay run identity used by later scenes.
|
||||
*/
|
||||
export const projectPreparedRunRequestExchange = (
|
||||
request: string,
|
||||
): readonly AgentMessage[] => {
|
||||
const callId = "authoring-run-request-call";
|
||||
return [
|
||||
agentTextMessage("authoring-run-request-user", "user", request),
|
||||
agentTextMessage(
|
||||
"authoring-run-request-starting",
|
||||
"assistant",
|
||||
"Starting lda_report_case_study.default with the prepared input.",
|
||||
),
|
||||
{
|
||||
// The authoring prefix reuses the compact phase tool-group treatment;
|
||||
// presenters can expand the real-shaped receipt only when useful.
|
||||
id: "authoring-run-tools",
|
||||
role: "assistant",
|
||||
parts: [
|
||||
agentToolCallPart(callId, "startRun", {
|
||||
operation: "workflow.runs.start",
|
||||
deployment_id: "lda_report_case_study.default",
|
||||
input_file: "examples/lda_report_workflow/run-input.json",
|
||||
equivalent_cli:
|
||||
"uv run wf --url http://127.0.0.1:8765/rpc run start lda_report_case_study.default --input-file examples/lda_report_workflow/run-input.json",
|
||||
}),
|
||||
agentToolResultPart(callId, "startRun", "success", {
|
||||
artifact_id: "lda_report_case_study",
|
||||
artifact_version: 1,
|
||||
deployment_id: "lda_report_case_study.default",
|
||||
diagnostics: [],
|
||||
interrupt: {
|
||||
id: "interrupt:review_issues",
|
||||
kind: "issue_review",
|
||||
node_id: "review_issues",
|
||||
outcomes: ["submitted", "cancelled"],
|
||||
proposed_issue_count: 3,
|
||||
resumable: true,
|
||||
typed: true,
|
||||
},
|
||||
resume_readiness: "ready",
|
||||
run_id: "run_recorded_lda_report",
|
||||
status: "interrupted",
|
||||
trace_count: 6,
|
||||
}),
|
||||
],
|
||||
},
|
||||
agentTextMessage(
|
||||
"authoring-run-request-paused",
|
||||
"assistant",
|
||||
"The run started and paused at the typed issue-review boundary. Three proposed issues are ready for review.",
|
||||
),
|
||||
];
|
||||
};
|
||||
|
||||
/** Returns the full prepared authoring recording. */
|
||||
export const projectPreparedAuthoring = (): readonly PreparedAuthoringPhase[] => recording;
|
||||
|
||||
+5
-4
@@ -43,14 +43,14 @@ describe("prepared lifecycle staged message state", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves edited draft text exactly when submitting draft", () => {
|
||||
it("preserves submitted text exactly and clears the composer draft", () => {
|
||||
const edited = preparedLifecycleMessageReducer(initialPreparedLifecycleMessageState, {
|
||||
type: "draft_edited",
|
||||
draft: " Check only the report binding. ",
|
||||
});
|
||||
|
||||
expect(preparedLifecycleMessageReducer(edited, { type: "draft_submitted" })).toEqual({
|
||||
draft: edited.draft,
|
||||
draft: "",
|
||||
submittedOverrides: { validate: edited.draft },
|
||||
runRequested: null,
|
||||
});
|
||||
@@ -63,7 +63,7 @@ describe("prepared lifecycle staged message state", () => {
|
||||
});
|
||||
|
||||
expect(preparedLifecycleMessageReducer(state, { type: "artifact_submitted" })).toEqual({
|
||||
draft: state.draft,
|
||||
draft: "",
|
||||
submittedOverrides: { deployment: state.draft },
|
||||
runRequested: null,
|
||||
});
|
||||
@@ -76,7 +76,7 @@ describe("prepared lifecycle staged message state", () => {
|
||||
});
|
||||
|
||||
expect(preparedLifecycleMessageReducer(state, { type: "discover_submitted" })).toEqual({
|
||||
draft: state.draft,
|
||||
draft: "",
|
||||
submittedOverrides: { discover: state.draft },
|
||||
runRequested: null,
|
||||
});
|
||||
@@ -106,6 +106,7 @@ describe("prepared lifecycle staged message state", () => {
|
||||
const requested = preparedLifecycleMessageReducer(state, { type: "run_requested" });
|
||||
|
||||
expect(requested.runRequested).toBe("Run this deployment");
|
||||
expect(requested.draft).toBe("");
|
||||
expect(requested.submittedOverrides).toEqual({});
|
||||
expect(preparedLifecycleMessageReducer(requested, { type: "run_requested" })).toBe(requested);
|
||||
});
|
||||
|
||||
@@ -57,6 +57,9 @@ const submitOverride = (
|
||||
|
||||
return {
|
||||
...state,
|
||||
// The submitted text is preserved in the transcript override; leaving it
|
||||
// in the composer would leak the previous phase's request into the next.
|
||||
draft: "",
|
||||
submittedOverrides: {
|
||||
...state.submittedOverrides,
|
||||
[destination]: state.draft,
|
||||
@@ -79,7 +82,7 @@ export const preparedLifecycleMessageReducer = (
|
||||
return submitOverride(state, "deployment");
|
||||
case "run_requested":
|
||||
return state.runRequested === null && hasText(state.draft)
|
||||
? { ...state, runRequested: state.draft }
|
||||
? { ...state, draft: "", runRequested: state.draft }
|
||||
: state;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@ type PreparedComposerSubmitHandlers = {
|
||||
};
|
||||
|
||||
export const PREPARED_COMPOSER_HELP =
|
||||
"Shift+Enter adds a new line. This is a deterministic prepared replay, not a live model request.";
|
||||
"Shift+Enter adds a new line. Responses are prepared; the final run request may use the configured workflow target.";
|
||||
|
||||
/** Shares the prepared-replay composer keyboard and form submission contract. */
|
||||
export const usePreparedComposerSubmit = (
|
||||
|
||||
Reference in New Issue
Block a user