feat: compose continuous authoring workspace
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { cleanup, render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { AgentMessage } from "../../demo/agent/events.js";
|
||||
@@ -140,6 +140,36 @@ describe("AssistantOperatorThread", () => {
|
||||
.toHaveAttribute("aria-expanded", "true");
|
||||
});
|
||||
|
||||
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 messages: ReadonlyArray<AgentMessage> = [
|
||||
{
|
||||
id: "authoring-draft-tools",
|
||||
role: "assistant",
|
||||
parts: [
|
||||
{ type: "tool-call", call: { id: "authoring-draft-command-0", name: "runWorkflowCommand", input: {} } },
|
||||
{ type: "tool-result", result: { callId: "authoring-draft-command-0", name: "runWorkflowCommand", status: "success", output: {} } },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
render(
|
||||
<AssistantOperatorThread
|
||||
mode="dock"
|
||||
surface="dock"
|
||||
messages={messages}
|
||||
activeToolGroupId="authoring-draft"
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => expect(setScrollTop).toHaveBeenCalledWith(0));
|
||||
});
|
||||
|
||||
it("renders structured tool results through the generated fallback result slot", () => {
|
||||
const messages: ReadonlyArray<AgentMessage> = [
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useMemo, useState, type ReactNode } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
||||
import type { ToolCallMessagePartStatus } from "@assistant-ui/react";
|
||||
import {
|
||||
ToolFallbackArgs,
|
||||
@@ -190,6 +190,7 @@ const AssistantMessageBody = ({
|
||||
rendered.push(
|
||||
<ToolGroupRoot
|
||||
key={`tool-group-${toolRunStart}`}
|
||||
data-tool-group-id={groupId}
|
||||
{...(phaseLabel
|
||||
? {
|
||||
open: openToolGroups.has(groupId),
|
||||
@@ -252,6 +253,7 @@ export const AssistantOperatorThread = ({
|
||||
activeToolGroupId,
|
||||
}: AssistantOperatorThreadProps) => {
|
||||
const projected = useMemo(() => projectAgentMessagesForAssistant(messages), [messages]);
|
||||
const viewportRef = useRef<HTMLDivElement>(null);
|
||||
const [openToolGroups, setOpenToolGroups] = useState<ReadonlySet<string>>(
|
||||
() => new Set(activeToolGroupId ? [activeToolGroupId] : []),
|
||||
);
|
||||
@@ -260,6 +262,21 @@ export const AssistantOperatorThread = ({
|
||||
if (activeToolGroupId) setOpenToolGroups(new Set([activeToolGroupId]));
|
||||
}, [activeToolGroupId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeToolGroupId) return;
|
||||
// The same transcript can be much taller than the Scene 9 dock. Keep the
|
||||
// beat-owned group visible without maintaining a second scroll-state model.
|
||||
const viewport = viewportRef.current;
|
||||
const activeGroup = viewport
|
||||
?.querySelector<HTMLElement>(`[data-tool-group-id="${activeToolGroupId}"]`);
|
||||
if (!viewport || !activeGroup) return;
|
||||
const top = Math.max(
|
||||
0,
|
||||
activeGroup.offsetTop + activeGroup.offsetHeight - viewport.clientHeight,
|
||||
);
|
||||
viewport.scrollTop = top;
|
||||
}, [activeToolGroupId, projected]);
|
||||
|
||||
const setToolGroupOpen = useCallback((groupId: string, open: boolean) => {
|
||||
setOpenToolGroups((current) => {
|
||||
const next = new Set(current);
|
||||
@@ -283,7 +300,7 @@ export const AssistantOperatorThread = ({
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
<div className="assistant-thread">
|
||||
<div className="assistant-thread__viewport">
|
||||
<div ref={viewportRef} className="assistant-thread__viewport">
|
||||
{projected.map((message) => {
|
||||
if (message.role === "user") {
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user