mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-06-13 22:34:06 +00:00
fix(webui): auto-collapse completed activity
This commit is contained in:
parent
547f81e4aa
commit
8fedee276b
@ -225,13 +225,18 @@ export function AgentActivityCluster({
|
|||||||
|
|
||||||
const [userToggledOuter, setUserToggledOuter] = useState(false);
|
const [userToggledOuter, setUserToggledOuter] = useState(false);
|
||||||
const [outerOpenLocal, setOuterOpenLocal] = useState(false);
|
const [outerOpenLocal, setOuterOpenLocal] = useState(false);
|
||||||
|
const [completionHoldOpen, setCompletionHoldOpen] = useState(false);
|
||||||
const [now, setNow] = useState(() => Date.now());
|
const [now, setNow] = useState(() => Date.now());
|
||||||
const activityScrollRef = useRef<HTMLDivElement>(null);
|
const activityScrollRef = useRef<HTMLDivElement>(null);
|
||||||
const activityContentRef = useRef<HTMLDivElement>(null);
|
const activityContentRef = useRef<HTMLDivElement>(null);
|
||||||
const autoFollowActivityRef = useRef(true);
|
const autoFollowActivityRef = useRef(true);
|
||||||
const scrollFrameRef = useRef<number | null>(null);
|
const scrollFrameRef = useRef<number | null>(null);
|
||||||
/** Live work and the trace directly attached to an answer read like a visible trail. */
|
const wasTurnStreamingRef = useRef(isTurnStreaming);
|
||||||
const outerExpanded = userToggledOuter ? outerOpenLocal : isTurnStreaming || hasBodyBelow;
|
const wasTurnStreaming = wasTurnStreamingRef.current;
|
||||||
|
/** Live work stays open; completed work briefly shows the done state, then tucks away. */
|
||||||
|
const outerExpanded = userToggledOuter
|
||||||
|
? outerOpenLocal
|
||||||
|
: isTurnStreaming || completionHoldOpen || (wasTurnStreaming && !isTurnStreaming);
|
||||||
|
|
||||||
const hasLiveEditingFiles = isTurnStreaming && hasEditingFiles;
|
const hasLiveEditingFiles = isTurnStreaming && hasEditingFiles;
|
||||||
const singleFilePath = fileCount === 1 ? primaryFilePath : undefined;
|
const singleFilePath = fileCount === 1 ? primaryFilePath : undefined;
|
||||||
@ -377,6 +382,19 @@ export function AgentActivityCluster({
|
|||||||
return () => window.clearInterval(interval);
|
return () => window.clearInterval(interval);
|
||||||
}, [isTurnStreaming]);
|
}, [isTurnStreaming]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const wasStreaming = wasTurnStreamingRef.current;
|
||||||
|
wasTurnStreamingRef.current = isTurnStreaming;
|
||||||
|
if (isTurnStreaming) {
|
||||||
|
setCompletionHoldOpen(false);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (!wasStreaming || userToggledOuter) return undefined;
|
||||||
|
setCompletionHoldOpen(true);
|
||||||
|
const timeout = window.setTimeout(() => setCompletionHoldOpen(false), 900);
|
||||||
|
return () => window.clearTimeout(timeout);
|
||||||
|
}, [isTurnStreaming, userToggledOuter]);
|
||||||
|
|
||||||
const onActivityScroll = useCallback(() => {
|
const onActivityScroll = useCallback(() => {
|
||||||
const el = activityScrollRef.current;
|
const el = activityScrollRef.current;
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
import { AgentActivityCluster } from "@/components/thread/AgentActivityCluster";
|
import { AgentActivityCluster } from "@/components/thread/AgentActivityCluster";
|
||||||
import type { CliAppInfo, McpPresetInfo, UIMessage } from "@/lib/types";
|
import type { CliAppInfo, McpPresetInfo, UIMessage } from "@/lib/types";
|
||||||
@ -293,6 +293,53 @@ describe("AgentActivityCluster", () => {
|
|||||||
await waitFor(() => expect(marker).toHaveClass("animate-in"));
|
await waitFor(() => expect(marker).toHaveClass("animate-in"));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("briefly shows completed activity, then auto-collapses before the answer", () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
const liveReasoning: UIMessage = {
|
||||||
|
id: "r-collapse",
|
||||||
|
role: "assistant",
|
||||||
|
content: "",
|
||||||
|
reasoning: "checking files",
|
||||||
|
reasoningStreaming: true,
|
||||||
|
isStreaming: true,
|
||||||
|
createdAt: 1,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const { rerender } = render(
|
||||||
|
<AgentActivityCluster
|
||||||
|
messages={[liveReasoning]}
|
||||||
|
isTurnStreaming
|
||||||
|
hasBodyBelow
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(screen.getByTestId("agent-activity-scroll")).toBeInTheDocument();
|
||||||
|
|
||||||
|
rerender(
|
||||||
|
<AgentActivityCluster
|
||||||
|
messages={[{
|
||||||
|
...liveReasoning,
|
||||||
|
reasoningStreaming: false,
|
||||||
|
isStreaming: false,
|
||||||
|
}]}
|
||||||
|
isTurnStreaming={false}
|
||||||
|
hasBodyBelow
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByTestId("agent-activity-scroll")).toBeInTheDocument();
|
||||||
|
act(() => {
|
||||||
|
vi.advanceTimersByTime(901);
|
||||||
|
});
|
||||||
|
expect(screen.queryByTestId("agent-activity-scroll")).not.toBeInTheDocument();
|
||||||
|
expect(screen.getByRole("button", { name: /1 steps/i })).toHaveAttribute(
|
||||||
|
"aria-expanded",
|
||||||
|
"false",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
vi.useRealTimers();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("renders file edit totals and a compact expanded file list", async () => {
|
it("renders file edit totals and a compact expanded file list", async () => {
|
||||||
const restoreMotion = installReducedMotion();
|
const restoreMotion = installReducedMotion();
|
||||||
try {
|
try {
|
||||||
@ -583,6 +630,8 @@ describe("AgentActivityCluster", () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: /1 tool calls/i }));
|
||||||
|
|
||||||
expect(screen.getByText("Shell")).toBeInTheDocument();
|
expect(screen.getByText("Shell")).toBeInTheDocument();
|
||||||
expect(screen.getByText(/cat << 'EOF' \| bash · script, 6 lines/)).toBeInTheDocument();
|
expect(screen.getByText(/cat << 'EOF' \| bash · script, 6 lines/)).toBeInTheDocument();
|
||||||
expect(screen.queryByText(/SECRET_TOKEN/)).not.toBeInTheDocument();
|
expect(screen.queryByText(/SECRET_TOKEN/)).not.toBeInTheDocument();
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user