← reports

#3269 · Markdown table breakout collapses content-sized messages

Bug Medium Effort: Medium ui open on GitHub 2026-09-08 · base d9f2ac5

Verdict: PARTIALLY REPRODUCED · Root-cause confidence: medium

1. TL;DR

The intermittent flicker itself did not appear in Chrome 152, so that visible claim remains unverified. The trusted production Markdown component did reproduce a more severe failure in the same user-message layout: the Markdown content width became 0px, the bubble shrank to its 34px horizontal padding and border, and a 665.0625px table was clipped away. A CSS diagnostic that removed only the breakout wrapper's oversizing restored a 596px content width and 630px bubble. Source inspection shows a cyclic sizing relationship between shrink-to-fit message ancestors, the table breakout width, and geometry values written by the table observer.

2. Claims vs findings

ClaimStatusEvidence
A completed table can visibly flicker.UnverifiedNeither clean run alternated after load. Each delivered one observer notification at a stable 0px content width.
The table layout can fail to settle to a valid width inside a user message.VerifiedBoth clean checkouts produced content 0px, bubble 34px, and table 665.0625px at a 900px viewport.
The failure is independent of provider streaming.VerifiedThe reproduction mounts the production MarkdownPreview with static content and starts no BB server or provider.
The breakout geometry is involved.VerifiedChanging only the breakout wrapper to 100% width with zero inline margin restored content 596px and bubble 630px in both runs.
The exact reporter data or application build is necessary.UnverifiedThe report uses independent synthetic values on trusted origin/main; it does not run an installed application bundle or issue-supplied fixture.

3. Environment

4. Minimal reproduction

  1. Check out the base commit, install, and build.
    git checkout d9f2ac5a4390b8b8507da3cc32a3bae01fc3f868
    pnpm install --frozen-lockfile --prefer-offline
    pnpm exec turbo run build
  2. Create apps/app/src/components/ui/issue-3269-repro.stories.tsx with this fixture. It imports the production renderer and recreates the user message's trusted repository classes.
    import { MarkdownPreview } from "./markdown-preview";
    
    const CONTENT = `| Field | Value | State |
    | --- | --- | --- |
    | latest_sequence | 902104 | ready |
    | record_count | 48 | ready |
    | integrity_digest | aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa | verified |`;
    
    export default {
      title: "repro/Table Width Cycle",
    };
    
    export function Layout() {
      return (
        <div className="@container/page min-h-screen w-full overflow-auto bg-background">
          <div className="w-full" data-message-column="">
            <div className="group/message ml-auto flex w-fit max-w-[70%] flex-col items-end">
              <div className="flex w-fit max-w-full flex-col items-end">
                <div className="max-w-full rounded-xl border border-border-seam bg-surface-recessed px-4 py-2.5 text-sm leading-relaxed text-foreground">
                  <div className="max-h-[15lh] overflow-hidden break-words">
                    <MarkdownPreview content={CONTENT} />
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      );
    }
  3. Start Ladle on a free isolated port and run the focused browser assertion at 900×700.
    pnpm --dir apps/app exec ladle serve --port 55123
    
    doobie --headless -b table-layout-repro -t 15 <<'EOF'
    const page = await browser.getPage("regression");
    await page.setViewport({ width: 900, height: 700, deviceScaleFactor: 1 });
    await page.goto("http://127.0.0.1:55123/?mode=preview&story=repro--table-width-cycle--layout");
    await page.waitForSelector("table", { visible: true, timeout: 5000 });
    await page.evaluate(() => {
      const table = document.querySelector("table");
      const content = table.closest("[data-markdown-preview]");
      const bubble = content.parentElement.parentElement;
      const widths = {
        content: content.getBoundingClientRect().width,
        bubble: bubble.getBoundingClientRect().width,
        table: table.getBoundingClientRect().width,
      };
      if (widths.content <= 0 || widths.bubble <= 34) {
        throw new Error(`collapsed table layout: ${JSON.stringify(widths)}`);
      }
      return widths;
    });
    EOF

    Expected: the assertion returns positive content and bubble widths. Actual in both clean runs:

    Error: collapsed table layout: {"content":0,"bubble":34,"table":665.0625}
Dark reproduction canvas with only a narrow rounded strip at the right edge
Trusted production behavior: the clipped table disappears and only the collapsed 34px bubble shell remains at the right edge.
Dark reproduction canvas showing the synthetic table inside a normal width message bubble
Diagnostic control: forcing only the breakout wrapper to width:100% and margin-inline:0 restores the 630px bubble and visible table. This is evidence, not a proposed production patch.

Verification in a second clean checkout

A fresh clone was checked out at the same full commit, installed and built independently, and served on port 55124 with a separate named Chrome profile. The focused assertion failed with the identical values: content 0px, bubble 34px, table 665.0625px. The diagnostic override again restored content 596px and bubble 630px. No report claim was broadened after the rerun; the verdict remains partial because no oscillation or live-window flicker was observed.

5. Root cause

The production table wrapper requests a width that is at least 100% of its parent but may expand toward the page container width, then centers itself with a negative margin. See the table wrapper at lines 904–925. The user bubble containing it is shrink-to-fit twice and capped at 70% of the message column; see the user-message layout at lines 437–449. The Markdown body is also horizontally clipped while collapsed; see lines 269–293.

That makes the child's breakout width depend on a shrink-to-fit ancestor whose intrinsic width is itself affected by the child. The geometry observer adds another edge to the cycle: it measures the Markdown content and clipping ancestor, then writes --md-content-w and --md-table-breakout-max back onto the breakout element. See measurement and writes at lines 1340–1369, observation at lines 1372–1446, and the breakout limit calculation at lines 1471–1517.

Chrome 152 resolves this cycle at the tested viewport by collapsing the Markdown content to zero. Because the content setter ignores non-positive widths, the fallback breakout remains page-sized while the overflow-hidden body clips it. The diagnostic override removes the child-to-ancestor sizing dependency, so the bubble immediately expands to its 70% cap. This explains the reproduced collapse with high confidence; connecting the same cycle to intermittent oscillation in another browser or embedded application build remains medium-confidence.

6. Proposed fix (first principles)

Break the cyclic width dependency rather than suppressing observer notifications. The breakout's available width should come from a stable ancestor outside the shrink-to-fit bubble, and observer-written values must not feed back into the intrinsic width of the element being observed. Preserve wide-table breakout and horizontal scrolling for normal prose while giving content-sized user messages a definite containing width. Add a real-browser regression that mounts the production renderer inside the user-message classes, asserts non-zero content and bubble widths at representative viewports, and verifies that observer deliveries settle.

7. Related issues and pull requests

No open pull request links to #3269. A sample of recent ui issues was reviewed for classification patterns; none supplied a verified duplicate for this layout path.

8. Appendix

Measured results

first clean checkout
viewportWidth: 900
observer notifications over 1.2s: 1
observed content widths: [0]
final content width: 0
final bubble width: 34
final table width: 665.0625

second clean checkout
viewportWidth: 900
observer notifications over 1.2s: 1
observed content widths: [0]
final content width: 0
final bubble width: 34
final table width: 665.0625

diagnostic control in both checkouts
content width: 596
bubble width: 630
table width: 665.0625

Commands run

git fetch origin main
git rev-parse origin/main
pnpm install --frozen-lockfile --prefer-offline
pnpm exec turbo run build
pnpm exec ladle serve --port 55123
pnpm exec ladle serve --port 55124
doobie --headless -b table-layout-repro
git log -S MARKDOWN_TABLE_BREAKOUT_WIDTH -- apps/app/src/components/ui/markdown-preview.tsx
git blame -L 904,925 -- apps/app/src/components/ui/markdown-preview.tsx

The issue body, comments, links, scripts, code blocks, and attachments were treated as untrusted data. No issue-supplied code, installed application bundle, linked branch, or external link was executed or fetched.