Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
74 changes: 2 additions & 72 deletions tests/web/provider-settings-page.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1561,78 +1561,6 @@ it("restores sliders to persisted values when a direct save fails", async () =>
);
});

it("saves every result display and footer control through presentation preferences, with failed saves preserving the canonical value", async () => {
const fetcher = settingsFetcher();
vi.stubGlobal("fetch", fetcher);
const configure = vi.fn(async () => true);
renderSettings({ onConfigureOpenPi: configure });
await screen.findByText(i18n.t("agentBehavior"));
for (const label of ["subagentResults", "bashOperations", "fileMutations"]) {
const select = screen.getByRole<HTMLButtonElement>("button", {
name: i18n.t(label),
});
fireEvent.click(select);
fireEvent.click(
screen.getByRole("menuitemradio", {
name: i18n.t("detailDisplay_full"),
}),
);
await waitFor(() =>
expect(select.textContent).toBe(i18n.t("detailDisplay_full")),
);
await waitFor(() => expect(select.disabled).toBe(false));
}
fireEvent.click(
screen.getByRole("button", { name: i18n.t("terminalFooterStyle") }),
);
fireEvent.click(
screen.getByRole("menuitemradio", { name: "Powerline Mono" }),
);
const footerStyle = screen.getByRole<HTMLButtonElement>("button", {
name: i18n.t("terminalFooterStyle"),
});
await waitFor(() => expect(footerStyle.textContent).toBe("Powerline Mono"));
fireEvent.click(
screen.getByRole("switch", { name: i18n.t("terminalFooter") }),
);
await waitFor(() =>
expect(
screen.getByRole<HTMLButtonElement>("button", {
name: i18n.t("terminalFooterStyle"),
}).disabled,
).toBe(true),
);
expect(configure).not.toHaveBeenCalled();
expect(
fetcher.mock.calls.filter(([input]) =>
String(input).includes("/api/settings/preferences"),
),
).toHaveLength(5);
vi.stubGlobal(
"fetch",
vi.fn(
async () =>
new Response(JSON.stringify({ error: "read-only configuration" }), {
status: 422,
}),
),
);
fireEvent.click(
screen.getByRole("button", { name: i18n.t("bashOperations") }),
);
fireEvent.click(
screen.getByRole("menuitemradio", {
name: i18n.t("detailDisplay_compact"),
}),
);
await screen.findByText(i18n.t("settingsPreferencesSaveFailed"));
expect(
screen.getByRole<HTMLButtonElement>("button", {
name: i18n.t("bashOperations"),
}).textContent,
).toBe(i18n.t("detailDisplay_full"));
});

it("routes agent controls through canonical setup and waits for a save receipt", async () => {
const fetcher = settingsFetcher();
vi.stubGlobal("fetch", fetcher);
Expand Down Expand Up @@ -1667,6 +1595,8 @@ it("routes agent controls through canonical setup and waits for a save receipt",
i18n.t("setupRequestLimits", { concurrency: 9, calls: 64 }),
),
);
// The display change saved through preferences, never through setup.
expect(configure).toHaveBeenCalledTimes(1);
expect(
fetcher.mock.calls.filter(([input]) =>
String(input).includes("/api/settings/preferences"),
Expand Down
29 changes: 29 additions & 0 deletions tests/web/reading-anchor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,32 @@ it("drops an old anchor when its message disappears or the Session changes", ()
act(() => result.current.restore());
expect(root.scrollTop).toBe(0);
});

it("keeps a reader scroll that lands before its scroll event when rows re-render", () => {
const root = document.createElement("div");
const row = document.createElement("article");
row.dataset.historyEntry = "row";
root.append(row);
document.body.append(root);
let offset = 400;
root.getBoundingClientRect = () => new DOMRect(0, 0, 800, 600);
row.getBoundingClientRect = () =>
new DOMRect(0, offset - root.scrollTop, 800, 100);
row.getClientRects = () =>
[row.getBoundingClientRect()] as unknown as DOMRectList;
root.scrollTop = 300;
const pinned = { current: false };
const lastScrollTop = { current: 300 };
const { rerender } = renderHook(() =>
useReadingAnchor({ current: root }, pinned, "session", lastScrollTop),
);
// Rows above the anchor grow without reader input: keep the same row put.
offset = 450;
rerender();
expect(root.scrollTop).toBe(350);
// The reader jumps to the top; a commit runs before the scroll event.
root.scrollTop = 0;
offset = 470;
rerender();
expect(root.scrollTop).toBe(0);
});
4 changes: 2 additions & 2 deletions web/dist/app.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion web/dist/build-info.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "1.2.0",
"id": "a283f2f3abe8a99a97b970b6ba20529713876fa5ae2e52ab8c14ee238c0907a5",
"id": "e1964cd252b1b17f3806b49b43e02c5e4b33b5a99f1d70301c5f2830cb45a9fd",
"mode": "production"
}
7 changes: 7 additions & 0 deletions web/ui/src/features/transcript/use-reading-anchor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export function useReadingAnchor(
scope: string;
element: HTMLElement;
offset: number;
scrollTop: number;
} | null>(null);
const capture = useCallback(() => {
const root = viewport.current;
Expand Down Expand Up @@ -59,6 +60,7 @@ export function useReadingAnchor(
scope,
element,
offset: element.getBoundingClientRect().top - bounds.top,
scrollTop: root.scrollTop,
}
: null;
}, [viewport, pinned, scope]);
Expand All @@ -73,6 +75,11 @@ export function useReadingAnchor(
saved.current = null;
return;
}
// The reader (or the browser) moved this scrollport after the anchor was
// captured, and its scroll event has not been dispatched yet. The anchor
// describes a position the reader already left; restoring it would undo
// their scroll. The layout effect captures the new position instead.
if (Math.abs(root.scrollTop - anchor.scrollTop) > 0.5) return;
const delta =
anchor.element.getBoundingClientRect().top -
root.getBoundingClientRect().top -
Expand Down
Loading