fix(frontend): restore readable model activity timeline

This commit is contained in:
User
2026-07-15 14:55:57 +02:00
parent 9274f07987
commit 30e76a968d
3 changed files with 48 additions and 112 deletions
+26 -59
View File
@@ -9,18 +9,17 @@ import { ModelActivityPanel } from "./ModelActivityPanel";
beforeEach(() => useSessionStore.getState().resetSession());
test("renders only thinking and status from a mixed F1 sequence", () => {
test("renders question, reasoning, and response in chronological order", () => {
const store = useSessionStore.getState();
store.setPhase("F1");
store.setLastUserEntry({ kind: "input", text: "How many patients?" });
store.applyEvent({ type: "system_event", event: "agent_start" });
store.setLastUserEntry({ kind: "input", text: "How many **patients**?" });
store.applyEvent({ type: "info", level: "warning", text: "Retrying schema lookup" });
store.applyEvent({ type: "activity_delta", text: "Inspecting **schema**." });
store.applyEvent({ type: "text_delta", text: "I found *42* patients." });
store.applyEvent({
type: "activity_event",
activity: { kind: "tool", toolCallId: "tool-1", toolName: "bash", status: "running" },
activity: { kind: "tool", toolCallId: "tool-1", toolName: "bash", status: "completed" },
});
store.applyEvent({ type: "text_delta", text: "Let me run another bash search." });
store.applyEvent({ type: "activity_delta", text: "Inspecting **schema**." });
store.applyEvent({ type: "info", level: "warning", text: "Retrying schema lookup" });
store.applyEvent({
type: "ui_request",
ui_request: { id: "gate-1", widget: "select", phase: "F1_review", title: "Confirm cohort" },
@@ -28,16 +27,17 @@ test("renders only thinking and status from a mixed F1 sequence", () => {
render(<ModelActivityPanel onClose={vi.fn()} />);
expect(screen.getAllByRole("article")).toHaveLength(2);
expect(screen.getByText("schema").tagName).toBe("STRONG");
expect(screen.getByText("Retrying schema lookup")).toBeInTheDocument();
expect(screen.queryByText("How many patients?")).not.toBeInTheDocument();
const rows = screen.getAllByRole("article");
expect(rows).toHaveLength(3);
expect(within(rows[0]).getByText("Question")).toBeInTheDocument();
expect(within(rows[0]).getByText("patients").tagName).toBe("STRONG");
expect(within(rows[1]).getByText("Reasoning")).toBeInTheDocument();
expect(within(rows[1]).getByText("schema").tagName).toBe("STRONG");
expect(within(rows[2]).getByText("Response")).toBeInTheDocument();
expect(within(rows[2]).getByText("42").tagName).toBe("EM");
expect(screen.queryByText("Retrying schema lookup")).not.toBeInTheDocument();
expect(screen.queryByText("Confirm cohort")).not.toBeInTheDocument();
expect(screen.queryByText("bash")).not.toBeInTheDocument();
expect(screen.queryByText("Let me run another bash search.")).not.toBeInTheDocument();
expect(useSessionStore.getState().transcript).toEqual([
{ role: "assistant", text: "Let me run another bash search." },
]);
});
test("uses an explicit default-deny activity-kind allowlist", () => {
@@ -47,10 +47,10 @@ test("uses an explicit default-deny activity-kind allowlist", () => {
expect(predicate).toBeTypeOf("function");
if (!predicate) return;
for (const kind of ["thinking", "status"] satisfies ActivityKind[]) {
for (const kind of ["prompt", "thinking", "assistant"] satisfies ActivityKind[]) {
expect(predicate({ kind, phase: "F1", text: kind })).toBe(true);
}
for (const kind of ["prompt", "gate", "assistant", "tool", "lifecycle"] satisfies ActivityKind[]) {
for (const kind of ["status", "gate", "tool", "lifecycle"] satisfies ActivityKind[]) {
expect(predicate({ kind, phase: "F1", text: kind })).toBe(false);
}
expect(predicate({ kind: "future-kind" as ActivityKind, phase: "F1", text: "future" })).toBe(false);
@@ -59,9 +59,8 @@ test("uses an explicit default-deny activity-kind allowlist", () => {
test("shows the empty state when the raw log contains only hidden entries", () => {
useSessionStore.setState({
activityLog: [
{ kind: "prompt", phase: "F1", text: "How many patients?" },
{ kind: "status", phase: "F1", text: "Status" },
{ kind: "gate", phase: "F1", text: "Confirm cohort" },
{ kind: "assistant", phase: "F1", text: "Let me try another command." },
{ kind: "tool", phase: "F1", text: "bash", toolCallId: "tool-1", status: "completed" },
{ kind: "lifecycle", phase: "F1", text: "Turn end" },
],
@@ -75,7 +74,7 @@ test("shows the empty state when the raw log contains only hidden entries", () =
test("keeps all timeline entries visible without an expansion control", () => {
const activityLog: ActivityEntry[] = Array.from({ length: 8 }, (_, index) => ({
kind: "status",
kind: "thinking",
phase: `F${index + 1}`,
text: `timeline-${index + 1}`,
}));
@@ -87,39 +86,10 @@ test("keeps all timeline entries visible without an expansion control", () => {
expect(screen.queryByRole("button", { name: /show (more|less)/i })).not.toBeInTheDocument();
});
test("labels a warning in text instead of relying on its semantic color", () => {
useSessionStore.setState({
activityLog: [
{ kind: "status", phase: "F3", text: "Retrying retrieval", level: "warning" },
],
});
render(<ModelActivityPanel onClose={vi.fn()} />);
const row = screen.getByText("Retrying retrieval").closest("article");
expect(row).not.toBeNull();
expect(within(row!).getByText("warning")).toBeInTheDocument();
});
test("renders thinking markdown without assistant narration", () => {
useSessionStore.setState({
activityLog: [
{ kind: "thinking", phase: "F2", text: "Considering **history**." },
{ kind: "assistant", phase: "F2", text: "Use *current* records." },
],
});
render(<ModelActivityPanel onClose={vi.fn()} />);
expect(screen.getByText("history").tagName).toBe("STRONG");
expect(screen.queryByText("current")).not.toBeInTheDocument();
expect(screen.getAllByRole("article")).toHaveLength(1);
});
test("close calls onClose without mutating the activity log", async () => {
const onClose = vi.fn();
const activityLog: ActivityEntry[] = [
{ kind: "status", phase: "F1", text: "Keep this row" },
{ kind: "thinking", phase: "F1", text: "Keep this row" },
];
useSessionStore.setState({ activityLog });
render(<ModelActivityPanel onClose={onClose} />);
@@ -148,7 +118,7 @@ function setScrollGeometry(
});
}
test("follows appended visible activity while near the bottom", () => {
test("follows appended visible response while near the bottom", () => {
useSessionStore.setState({
activityLog: [{ kind: "thinking", phase: "F1", text: "First" }],
});
@@ -156,10 +126,8 @@ test("follows appended visible activity while near the bottom", () => {
const viewport = screen.getByTestId("activity-scroll");
setScrollGeometry(viewport, { scrollHeight: 400, clientHeight: 100, scrollTop: 300 });
act(() => useSessionStore.getState().applyEvent({ type: "info", text: "Second" }));
act(() => useSessionStore.getState().applyEvent({ type: "text_delta", text: "Second" }));
expect(viewport.scrollTop).toBe(400);
expect(viewport).toHaveAttribute("tabindex", "0");
});
test("preserves manual scroll position when visible activity arrives", () => {
@@ -171,20 +139,19 @@ test("preserves manual scroll position when visible activity arrives", () => {
setScrollGeometry(viewport, { scrollHeight: 400, clientHeight: 100, scrollTop: 100 });
fireEvent.scroll(viewport);
act(() => useSessionStore.getState().applyEvent({ type: "info", text: "Second" }));
act(() => useSessionStore.getState().applyEvent({ type: "text_delta", text: "Second" }));
expect(viewport.scrollTop).toBe(100);
});
test("does not bottom-follow when only a hidden prompt arrives", () => {
test("does not bottom-follow when only a hidden status arrives", () => {
useSessionStore.setState({
activityLog: [{ kind: "status", phase: "F1", text: "Visible status" }],
activityLog: [{ kind: "thinking", phase: "F1", text: "Visible reasoning" }],
});
render(<ModelActivityPanel onClose={vi.fn()} />);
const viewport = screen.getByTestId("activity-scroll");
setScrollGeometry(viewport, { scrollHeight: 400, clientHeight: 100, scrollTop: 300 });
act(() => useSessionStore.getState().setLastUserEntry({ kind: "input", text: "Hidden prompt" }));
act(() => useSessionStore.getState().applyEvent({ type: "info", text: "Hidden status" }));
expect(viewport.scrollTop).toBe(300);
});