Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
49445cc
feat(site): migrate agent chat scrolling
DanielleMaywood Aug 13, 2026
8c4b929
fix(site): keep chat history paging stable across filtered pages and …
DanielleMaywood Aug 13, 2026
8d0badb
fix(site): stabilize merged read_file row keys and derive visible row…
DanielleMaywood Aug 13, 2026
1663956
fix(site): key singleton read_file rows as groups and gate paging on …
DanielleMaywood Aug 13, 2026
ce8f668
docs(site): describe the merged read_file row key correctly
DanielleMaywood Aug 13, 2026
6e78691
fix(site): render the terminal chat failure as a transcript row
DanielleMaywood Aug 13, 2026
17e99c0
docs(site): document the merged read_file key append tradeoff
DanielleMaywood Aug 13, 2026
0d030d3
fix(site): return to the live edge on submit and stabilize the live r…
DanielleMaywood Aug 13, 2026
2d716ea
fix(site): hoist the scroller provider and scroll to the live edge on…
DanielleMaywood Aug 13, 2026
e6971cc
fix(site): key the scroller provider by chat and scroll after compact…
DanielleMaywood Aug 13, 2026
2cd3df4
fix(site): scroll to the live edge only after a history edit succeeds
DanielleMaywood Aug 13, 2026
be3c297
fix(site): suppress the scroll anchor for turns active at first render
DanielleMaywood Aug 13, 2026
c1cad2a
fix(site): capture the initial active-turn anchor at mount
DanielleMaywood Aug 13, 2026
37c90a6
fix(site): capture the initial active-turn anchor from REST data
DanielleMaywood Aug 13, 2026
e779620
fix(site): capture the initial active anchor only after the initial q…
DanielleMaywood Aug 13, 2026
8fe5e11
fix(site): capture the initial active anchor in the gated subtree
DanielleMaywood Aug 13, 2026
c8a990c
fix(site): suppress the initial active turn's anchor by message id th…
DanielleMaywood Aug 14, 2026
3eb26d7
fix(site): suppress the initial anchor for interrupting chats too
DanielleMaywood Aug 14, 2026
35a0a3f
fix(site): gate history paging on transcript rows, not visible DOM rows
DanielleMaywood Aug 14, 2026
bc6dfb7
fix(site): hold history paging until fetched messages hydrate into th…
DanielleMaywood Aug 14, 2026
e523d37
fix(site): keep the history loader tied to real fetches during hydration
DanielleMaywood Aug 14, 2026
747223f
fix(site): wait one frame before paging so fresh rows can settle
DanielleMaywood Aug 14, 2026
e823acd
fix(site): compare hydrated chat messages by content, not identity
DanielleMaywood Aug 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
"@novnc/novnc": "^1.5.0",
"@pierre/diffs": "1.3.3",
"@pierre/trees": "1.0.0-beta.4",
"@shadcn/react": "0.3.0",
"@tanstack/react-query-devtools": "5.82.0",
"@xterm/addon-canvas": "0.7.0",
"@xterm/addon-fit": "0.11.0",
Expand Down Expand Up @@ -94,7 +95,6 @@
"react-confetti": "6.4.0",
"react-day-picker": "9.14.0",
"react-dom": "19.2.8",
"react-infinite-scroll-component": "7.2.1",
"react-markdown": "9.1.0",
"react-query": "npm:@tanstack/[email protected]",
"react-resizable-panels": "3.0.6",
Expand Down
34 changes: 19 additions & 15 deletions site/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

69 changes: 66 additions & 3 deletions site/src/pages/AgentsPage/AgentChatPage.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import type { FC } from "react";
import { useRef } from "react";
import { hashKey } from "react-query";
import { Outlet, useNavigate } from "react-router";
import { expect, spyOn, userEvent, waitFor, within } from "storybook/test";
Expand Down Expand Up @@ -50,7 +49,6 @@ import type { AgentsPageOutletContext } from "./AgentsPageLayout";
// Layout wrapper: provides outlet context for the child route.
// ---------------------------------------------------------------------------
const AgentChatPageLayout: FC = () => {
const scrollContainerRef = useRef<HTMLDivElement | null>(null);
return (
<div className="flex h-full">
<div className="flex min-w-0 flex-1 flex-col overflow-hidden">
Expand All @@ -74,7 +72,6 @@ const AgentChatPageLayout: FC = () => {
onToggleSidebarCollapsed: () => {},
onExpandSidebar: () => {},
onChatReady: () => {},
scrollContainerRef,
} satisfies AgentsPageOutletContext
}
/>
Expand Down Expand Up @@ -3149,6 +3146,72 @@ export const SendResponseAfterChatSwitch: Story = {
},
};

/**
* The send flow renders the durable user row once the server accepts the
* prompt, before the assistant turn produces any output.
*/
export const SendRendersDurableUserRowBeforeAssistantOutput: Story = {
parameters: {
queries: buildQueries(
{
id: CHAT_ID,
...baseChatFields,
title: "Durable send",
status: "waiting",
},
{ messages: [], queued_messages: [], has_more: false },
{ diffUrl: undefined },
),
},
beforeEach: () => {
spyOn(API.experimental, "getUserSkills").mockResolvedValue([]);
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
let releaseSend: (() => void) | undefined;
const sendGate = new Promise<void>((resolve) => {
releaseSend = resolve;
});
const sendSpy = spyOn(
API.experimental,
"createChatMessage",
).mockImplementation(async () => {
await sendGate;
return {
queued: false,
message: {
...MockChatMessage,
id: 60,
chat_id: CHAT_ID,
role: "user",
content: [{ type: "text", text: "Durable prompt" }],
},
};
});

const editor = await canvas.findByTestId("chat-message-input");
await userEvent.click(editor);
await userEvent.type(editor, "Durable prompt");
await userEvent.keyboard("{Enter}");
await waitFor(() => {
expect(sendSpy).toHaveBeenCalledTimes(1);
});

const timeline = within(await canvas.findByTestId("conversation-timeline"));
expect(
timeline.queryByTestId("chat-message-message:60"),
).not.toBeInTheDocument();

releaseSend?.();
expect(
await timeline.findByTestId("chat-message-message:60"),
).toHaveTextContent("Durable prompt");
// The turn is still waiting on its first chunk, so the durable row is in
// place before any assistant output exists.
expect(canvas.getByTestId("live-activity-slot")).toBeVisible();
},
};

const mockErrorChat: TypesGen.Chat = {
...MockChat,
id: CHAT_ID,
Expand Down
39 changes: 8 additions & 31 deletions site/src/pages/AgentsPage/AgentChatPage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import {
restoreOptimisticRequestSnapshot,
runPromoteQueuedMessage,
settlePromotedQueueHead,
submitEditAndScroll,
submitEdit,
useConversationEditingState,
waitForPendingChatSettingsSyncs,
} from "./AgentChatPage";
Expand Down Expand Up @@ -1246,63 +1246,40 @@ describe("useConversationEditingState", () => {
});
});

describe("submitEditAndScroll", () => {
describe("submitEdit", () => {
const dummyArgs = {
messageId: 42,
req: { content: [{ type: "text" as const, text: "edited" }] },
};

it("calls scrollToBottom after editMessage resolves", async () => {
const callOrder: string[] = [];
const editMessage = vi.fn(async () => {
callOrder.push("editMessage");
});
const scrollToBottom = vi.fn(() => {
callOrder.push("scrollToBottom");
});
it("awaits editMessage", async () => {
const editMessage = vi.fn().mockResolvedValue(undefined);

await submitEditAndScroll({
await submitEdit({
editMessage,
editArgs: dummyArgs,
scrollToBottom,
onError: vi.fn(),
});

expect(callOrder).toEqual(["editMessage", "scrollToBottom"]);
expect(editMessage).toHaveBeenCalledWith(dummyArgs);
});

it("does not call scrollToBottom when editMessage throws", async () => {
const scrollToBottom = vi.fn();
it("reports and rethrows an editMessage failure", async () => {
const onError = vi.fn();
const editMessage = vi.fn().mockRejectedValue(new Error("boom"));

await expect(
submitEditAndScroll({
submitEdit({
editMessage,
editArgs: dummyArgs,
scrollToBottom,
onError,
}),
).rejects.toThrow("boom");

expect(scrollToBottom).not.toHaveBeenCalled();
expect(onError).toHaveBeenCalledWith(
expect.objectContaining({ message: "boom" }),
);
});

it("tolerates null scrollToBottom", async () => {
const editMessage = vi.fn().mockResolvedValue(undefined);

await submitEditAndScroll({
editMessage,
editArgs: dummyArgs,
scrollToBottom: null,
onError: vi.fn(),
});

expect(editMessage).toHaveBeenCalled();
});
});

describe("sidebar tab persistence", () => {
Expand Down
Loading
Loading