fix(ui): prevent dashboard chat messages from overlapping (#123713)

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
ClawSweeper
2026-08-14 10:00:01 -07:00
committed by GitHub
parent 1306ec805d
commit a875ead7ef
4 changed files with 49 additions and 6 deletions
@@ -31,10 +31,12 @@ suite.define(() => {
await page.goto(`${suite.server.baseUrl}skills`);
await gateway.waitForRequest("skills.status");
await expect
.poll(() => gateway.getRequests("skills.status"))
.toEqual([expect.objectContaining({ params: { agentId: "main" } })]);
await page.getByText("No skills found.").waitFor();
const requests = await gateway.getRequests("skills.status");
expect(requests.length).toBeGreaterThan(0);
expect(requests.map((request) => request.params)).toEqual(
requests.map(() => ({ agentId: "main" })),
);
});
});
});
@@ -51,6 +51,27 @@ describe("chat transcript controller", () => {
}
});
it("measures newly inserted rows after Lit connects them", async () => {
// Lit invokes ref callbacks while a new row is still detached. Browsers
// report a zero offsetHeight there, which must not become the row's
// durable virtual size before the following user bubble is positioned.
transcriptDomState.detachedRowHeight = 0;
const transcript = createTestTranscript();
const container = document.body.appendChild(document.createElement("div"));
const props = threadProps("pane-commentary-insert", "agent:main:session-a", [
{ role: "assistant", content: "commentary", timestamp: 1_000 },
{ role: "user", content: "next turn", timestamp: 2_000 },
]);
render(renderChatThread(props, transcript), container);
transcript.hostConnected();
transcript.hostUpdated();
await flushDeferredRowPrune();
render(renderChatThread(props, transcript), container);
expect(transcriptRows(container)[1]?.style.transform).toBe("translateY(100px)");
});
it("pauses an unmeasurable restore until loading commits an empty transcript", () => {
const transcript = createTestTranscript();
const container = document.body.appendChild(document.createElement("div"));
@@ -132,7 +132,22 @@ class ChatSessionVirtualizerHost implements ReactiveControllerHost, ChatTranscri
if (!callback) {
callback = (element?: Element) => {
if (element instanceof HTMLElement) {
this.virtualizerController.getVirtualizer().measureElement(element);
if (element.isConnected) {
this.virtualizerController.getVirtualizer().measureElement(element);
} else {
// Lit invokes refs before the row is connected. Measuring a new
// key there records offsetHeight=0, so a following row can share
// its transform and paint over it until ResizeObserver catches up.
queueMicrotask(() => {
if (
element.isConnected &&
element.dataset.virtualRowKey === key &&
this.rowIndexesByKey.has(key)
) {
this.virtualizerController.getVirtualizer().measureElement(element);
}
});
}
return;
}
// Re-stamps (e.g. the chat<->dashboard face switch) re-invoke each
@@ -4,7 +4,7 @@ import { resetThreadPresentation } from "./chat-thread-interactions.ts";
export const observedElements = new Set<Element>();
export const resizeObservers = new Set<RecordingResizeObserver>();
export const transcriptDomState = { measuredRowHeight: 100 };
export const transcriptDomState = { measuredRowHeight: 100, detachedRowHeight: 100 };
class RecordingResizeObserver implements ResizeObserver {
private readonly targets = new Set<Element>();
@@ -95,9 +95,14 @@ export function installTranscriptDomMocks(): void {
observedElements.clear();
resizeObservers.clear();
transcriptDomState.measuredRowHeight = 100;
transcriptDomState.detachedRowHeight = 100;
vi.stubGlobal("ResizeObserver", RecordingResizeObserver);
vi.spyOn(HTMLElement.prototype, "offsetHeight", "get").mockImplementation(
() => transcriptDomState.measuredRowHeight,
function (this: HTMLElement) {
return this.isConnected
? transcriptDomState.measuredRowHeight
: transcriptDomState.detachedRowHeight;
},
);
vi.spyOn(Element.prototype, "getBoundingClientRect").mockReturnValue({
x: 0,