fix: keep scene 8 transcript visible

This commit is contained in:
lda
2026-07-12 05:28:44 +07:00 Verified
parent d1128c0b49
commit 7165c74d83
6 changed files with 92 additions and 3 deletions
@@ -10,6 +10,7 @@ type AuthoringConversationProps = {
readonly activePhase: AuthoringPhaseId;
readonly surface: "stage" | "dock";
readonly requestOverride?: string | undefined;
readonly scrollMode?: "active" | "start" | undefined;
readonly runAction?: { readonly label: string; readonly disabled: boolean; readonly run: () => void } | undefined;
};
@@ -19,6 +20,7 @@ export const AuthoringConversation = ({
activePhase,
surface,
requestOverride,
scrollMode,
runAction,
}: AuthoringConversationProps) => (
<AssistantOperatorThread
@@ -26,6 +28,7 @@ export const AuthoringConversation = ({
surface={surface}
messages={projectPreparedAuthoringThread(throughPhase, requestOverride)}
activeToolGroupId={authoringToolGroupId(activePhase)}
scrollMode={scrollMode}
ariaLabel="prepared authoring conversation"
runAction={runAction}
/>
@@ -45,6 +45,10 @@ describe("Scene8ChatEntry", () => {
await user.click(screen.getByRole("button", { name: "Send" }));
expect(screen.getByText(/let me inspect the available sources/i)).toBeInTheDocument();
expect(screen.getByRole("button", { name: /discover.*4 tool calls/i })).toBeInTheDocument();
expect(screen.getByRole("region", { name: "authoring chat entry" })).toHaveAttribute(
"data-entry-phase",
"submitted",
);
expect(screen.queryByRole("button", { name: /run prepared workflow/i })).not.toBeInTheDocument();
expect(screen.getByRole("button", { name: "Send" })).toBeDisabled();
});
@@ -24,7 +24,11 @@ export const Scene8ChatEntry = ({ state, dispatch }: Scene8ChatEntryProps) => {
};
return (
<section className="agent-handoff-scene__entry" aria-label="authoring chat entry">
<section
className="agent-handoff-scene__entry"
aria-label="authoring chat entry"
data-entry-phase={state.phase}
>
<div className="agent-handoff-scene__intro">
<span>Scene 8 · agent handoff</span>
<h1>What should the workflow author prepare?</h1>
@@ -62,6 +66,7 @@ export const Scene8ChatEntry = ({ state, dispatch }: Scene8ChatEntryProps) => {
throughPhase="discover"
activePhase="discover"
surface="stage"
scrollMode="start"
requestOverride={state.request}
/>
</div>
@@ -179,6 +179,53 @@ describe("AssistantOperatorThread", () => {
}
});
it("can keep a first-stage group anchored at the start of the transcript", async () => {
const setScrollTop = vi.fn();
const originalDescriptor = Object.getOwnPropertyDescriptor(HTMLDivElement.prototype, "scrollTop");
const messages: ReadonlyArray<AgentMessage> = [
{
id: "assistant-start-scroll-text",
role: "assistant",
parts: [
{ type: "text", text: "The first authoring turn is visible." },
],
},
{
id: "authoring-discover-tools",
role: "assistant",
parts: [
{ type: "tool-call", call: { id: "authoring-discover-command-0", name: "workflow.sources.list", input: { phase: "discover" } } },
{ type: "tool-result", result: { callId: "authoring-discover-command-0", name: "workflow.sources.list", status: "success", output: {} } },
],
},
];
try {
Object.defineProperty(HTMLDivElement.prototype, "scrollTop", {
configurable: true,
get: () => 0,
set: setScrollTop,
});
render(
<AssistantOperatorThread
mode="full"
surface="stage"
messages={messages}
activeToolGroupId="authoring-discover"
scrollMode="start"
/>,
);
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> = [
{
@@ -24,6 +24,7 @@ type AssistantOperatorThreadProps = {
readonly mode: "hidden" | "full" | "rail" | "dock";
readonly messages: ReadonlyArray<AgentMessage>;
readonly runAction?: { readonly label: string; readonly disabled: boolean; readonly run: () => void } | undefined;
readonly scrollMode?: "active" | "start" | undefined;
readonly submitApproval?: (() => void) | undefined;
readonly requestRevision?: (() => void) | undefined;
readonly ariaLabel?: string | undefined;
@@ -250,6 +251,7 @@ export const AssistantOperatorThread = ({
mode,
messages,
runAction,
scrollMode = "active",
submitApproval,
requestRevision,
ariaLabel = "operator conversation",
@@ -282,13 +284,17 @@ export const AssistantOperatorThread = ({
if (!viewport || !activeGroup) return;
const bottomAlignedTop = activeGroup.offsetTop + activeGroup.offsetHeight - viewport.clientHeight;
const dockCenteredTop = activeGroup.offsetTop - (viewport.clientHeight - activeGroup.offsetHeight) / 2;
const requestedTop = surface === "dock" ? dockCenteredTop : bottomAlignedTop;
const requestedTop = scrollMode === "start"
? 0
: surface === "dock"
? dockCenteredTop
: bottomAlignedTop;
const top = Math.min(
Math.max(0, requestedTop),
Math.max(0, viewport.scrollHeight - viewport.clientHeight),
);
viewport.scrollTop = top;
}, [activeToolGroupId, projected, surface]);
}, [activeToolGroupId, projected, scrollMode, surface]);
const setToolGroupOpen = useCallback((groupId: string, open: boolean) => {
setToolGroupOverrides((current) => {
@@ -3042,6 +3042,30 @@
height: 100%;
}
.agent-handoff-scene__entry[data-entry-phase="submitted"] {
grid-template-rows: auto minmax(0, 1fr) auto;
}
.agent-handoff-scene__entry[data-entry-phase="submitted"] .agent-handoff-scene__entry-thread {
grid-row: 2;
}
.agent-handoff-scene__entry[data-entry-phase="submitted"] .agent-handoff-scene__composer {
grid-row: 3;
gap: 0.4rem;
padding: 0.65rem 1rem;
box-shadow: none;
}
.agent-handoff-scene__entry[data-entry-phase="submitted"] .agent-handoff-scene__composer textarea {
min-height: 3.25rem;
max-height: 4.5rem;
}
.agent-handoff-scene__entry[data-entry-phase="submitted"] .agent-handoff-scene__composer-help {
display: none;
}
.agent-handoff-scene__intro {
width: min(100%, 52rem);
margin-inline: auto;