From e0538d92ae12d3ede02cac2fb6b5d2e946221b4e Mon Sep 17 00:00:00 2001 From: Ammar Date: Mon, 27 Apr 2026 12:54:05 -0500 Subject: [PATCH 01/31] =?UTF-8?q?=F0=9F=A4=96=20fix:=20stabilize=20streami?= =?UTF-8?q?ng=20transcript=20layout?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gate assistant meta rows until stream settlement, keep markdown component identity stable across stream completion, avoid reasoning collapse/height transitions mid-turn, remove redundant RAF autoscroll, and consolidate chat bottom layout lanes.\n\n---\n\n_Generated with `mux` • Model: `openai:gpt-5.5` • Thinking: `xhigh` • Cost: `5.89`_\n\n --- .../ChatInputDecorationStack.test.tsx | 361 --------------- .../ChatPane/ChatInputDecorationStack.tsx | 118 ----- src/browser/components/ChatPane/ChatPane.tsx | 60 ++- .../ChatPane/LayoutStackLane.test.tsx | 423 ++++++++++++++++++ ...criptTailStack.tsx => LayoutStackLane.tsx} | 87 ++-- .../ChatPane/TranscriptTailStack.test.tsx | 275 ------------ .../features/Messages/AssistantMessage.tsx | 53 +-- .../features/Messages/MessageWindow.test.tsx | 154 +++++++ .../features/Messages/MessageWindow.tsx | 27 +- .../Messages/ReasoningMessage.test.tsx | 106 ++++- .../features/Messages/ReasoningMessage.tsx | 75 ++-- src/browser/hooks/useAutoScroll.ts | 17 - tests/ui/chat/bottomLayoutShift.test.ts | 9 +- 13 files changed, 861 insertions(+), 904 deletions(-) delete mode 100644 src/browser/components/ChatPane/ChatInputDecorationStack.test.tsx delete mode 100644 src/browser/components/ChatPane/ChatInputDecorationStack.tsx create mode 100644 src/browser/components/ChatPane/LayoutStackLane.test.tsx rename src/browser/components/ChatPane/{TranscriptTailStack.tsx => LayoutStackLane.tsx} (53%) delete mode 100644 src/browser/components/ChatPane/TranscriptTailStack.test.tsx create mode 100644 src/browser/features/Messages/MessageWindow.test.tsx diff --git a/src/browser/components/ChatPane/ChatInputDecorationStack.test.tsx b/src/browser/components/ChatPane/ChatInputDecorationStack.test.tsx deleted file mode 100644 index 623d7cd924..0000000000 --- a/src/browser/components/ChatPane/ChatInputDecorationStack.test.tsx +++ /dev/null @@ -1,361 +0,0 @@ -import "../../../../tests/ui/dom"; - -import { afterEach, beforeEach, describe, expect, it } from "bun:test"; -import { cleanup, render, waitFor } from "@testing-library/react"; -import { installDom } from "../../../../tests/ui/dom"; - -import { ChatInputDecorationStack } from "./ChatInputDecorationStack"; -import type { LayoutStackItem } from "./layoutStack"; - -let cleanupDom: (() => void) | null = null; -let originalResizeObserver: typeof ResizeObserver | undefined; -const resizeCallbacks = new Map(); - -class ResizeObserverMock { - private readonly callback: ResizeObserverCallback; - - constructor(callback: ResizeObserverCallback) { - this.callback = callback; - } - - observe(target: Element) { - resizeCallbacks.set(target, [...(resizeCallbacks.get(target) ?? []), this.callback]); - } - - unobserve(target: Element) { - const remainingCallbacks = (resizeCallbacks.get(target) ?? []).filter( - (callback) => callback !== this.callback - ); - if (remainingCallbacks.length === 0) { - resizeCallbacks.delete(target); - return; - } - resizeCallbacks.set(target, remainingCallbacks); - } - - disconnect() { - for (const [target, callbacks] of resizeCallbacks) { - const remainingCallbacks = callbacks.filter((callback) => callback !== this.callback); - if (remainingCallbacks.length === 0) { - resizeCallbacks.delete(target); - continue; - } - resizeCallbacks.set(target, remainingCallbacks); - } - } - - takeRecords(): ResizeObserverEntry[] { - return []; - } -} - -function emitResize(target: Element, height: number) { - const contentRect = { - x: 0, - y: 0, - width: 0, - height, - top: 0, - right: 0, - bottom: height, - left: 0, - toJSON: () => ({}), - } satisfies DOMRectReadOnly; - const entry: ResizeObserverEntry = { - target, - contentRect, - borderBoxSize: [] as unknown as readonly ResizeObserverSize[], - contentBoxSize: [] as unknown as readonly ResizeObserverSize[], - devicePixelContentBoxSize: [] as unknown as readonly ResizeObserverSize[], - }; - - for (const callback of resizeCallbacks.get(target) ?? []) { - callback([entry], {} as ResizeObserver); - } -} - -function getRenderedStack(container: HTMLElement): HTMLDivElement { - const stack = container.querySelector('[data-component="stable-stack"]'); - expect(stack).toBeTruthy(); - if (stack?.tagName !== "DIV") { - throw new Error("Expected stack to exist"); - } - return stack as HTMLDivElement; -} - -function getStackContent(container: HTMLElement): Element { - const content = getRenderedStack(container).firstElementChild; - expect(content).toBeTruthy(); - if (!content) { - throw new Error("Expected content to exist"); - } - return content; -} - -async function waitForResizeObservation(target: Element): Promise { - await waitFor(() => { - const callbacks = resizeCallbacks.get(target); - if (!callbacks || callbacks.length === 0) { - throw new Error("Resize observer is not attached yet"); - } - }); -} - -async function waitForHydratingStack( - container: HTMLElement, - minHeightPx: number -): Promise { - return waitFor(() => { - const stack = getRenderedStack(container); - expect(stack.style.minHeight).toBe(`${minHeightPx}px`); - return stack; - }); -} - -function createTextDecoration(key: string, text: string): LayoutStackItem { - return { - key, - node:
{text}
, - }; -} - -function createHiddenDecoration(key = "idle-decoration"): LayoutStackItem { - return { - key, - node: