Skip to content

fix(workspace): tables, headings and lists in agent replies get a typographic treatment (#2616) - #2684

Merged
vybe merged 5 commits into
devfrom
feature/2616-transcript-typography
Sep 11, 2026
Merged

vybe merged 5 commits into
devfrom
feature/2616-transcript-typography

Conversation

@obasilakis

Copy link
Copy Markdown
Contributor

Closes #2616

What was wrong

components/portal/PortalMarkdown.vue styled exactly four things — paragraphs, unordered lists, links, and code (#2515). Tailwind's preflight resets everything else, so every other element an agent can write arrived with the browser reset and nothing else: a GFM table with no rules, no padding and no header separation; headings at body weight and size; ordered lists with their numbers gone. The transcript was the one markdown surface in the app with no typographic treatment, and the table was simply where it showed worst.

What changed

All of it lands in PortalMarkdown.vue, the single mount point that already owns render + style + copy as one unit — so the bubble, the room and any future surface inherit one look instead of a second sheet being copied out.

  • Tables get the platform's own anatomy (mono-caps header on chrome, 6/12 cell padding, a rule per row), as an object one step off the bubble tint — the same shape a code block already has, not a third look. The scroll-viewport pair is lifted from CanvasKit.vue (abilityai/trinity-enterprise#537 / bug(canvas): every block layout an agent can produce renders unbroken in the workspace column — gallery test + fixes #2583): display: block makes the table its own horizontal viewport so a ten-column table scrolls instead of widening the bubble, and overflow-wrap: normal in the cells stops an inherited "break anywhere" squeezing an auto-layout table to one character per line. width: fit-content keeps a two-column table from drawing its border around an acre of nothing.
  • Numeric columns align right with tabular-nums. GFM alignment survives sanitising as the align attribute, but a presentational hint loses to author CSS, so the right/center cases are restated explicitly.
  • Headings get a bounded ladder: # Title names a section of the reply, not the page, so it tops out at the design system's section size (18/650) and lands on the meta overline by h3 — three visibly distinct steps, all inside the six-size scale.
  • Ordered lists, list items, nested lists, blockquotes and horizontal rules. The blockquote takes ck-callout's 3px left rule, so a quoted line reads the same whether the agent wrote it into a message or onto a canvas.

Kept the hand-rolled sheet rather than adopting prose prose-sm dark:prose-invert: the plugin would have had to be un-styled back off pre, code and the heading scale to preserve #2515 and keep an # h1 out of page-title size, which is a larger and riskier diff than covering the missing element set directly. The issue names either as acceptable.

Not touched

The sanitiser. This is CSS on platform-owned selectors — no new admitted tags, attributes, classes or inline styles. Code blocks keep their #2515 behaviour: labelled bar, always-visible Copy, wrap-at-the-edge pre.

One assertion narrowed, not deleted

portalAgentBubble.spec.js asserted the stylesheet contained no overflow-x at all. The claim #2515 actually makes is about the pre — a scroller inside a chat bubble hides the end of a line behind a gesture nobody makes — written as a claim about the whole sheet. A table must scroll in its own viewport (AC #2 here), so the assertion now looks at the pre rule it was always about.

Verification

  • tests/unit/portalMarkdownTypography.spec.js — 12 new cases: what the configured marked emits (table with align attributes, ordered + nested lists, heading run, blockquote, hr) and that the sheet covers that element set, keeps the heading ladder bounded, keeps overflow-wrap: anywhere confined to the pre, admits nothing new through the sanitiser, and is still the only copy.
  • npm run test:unit — 2599 passed. The one failure, portalAgentsAtCentre.spec.js > the agent row time is tight, reproduces on a clean origin/dev checkout and is unrelated to this branch.
  • npm run check:tokens — OK.
  • Visual: light and dark, at 1100px and at 430px, against the local stack.

The raw-colour baseline re-freeze is its own commit with the growth named (+21 gray in this file, raw_nongray still 0), per the contract's "never absorbed into a feature diff".

🤖 Generated with Claude Code

https://claude.ai/code/session_01GawbQRicLvNMaPknFT8BfR

obasilakis and others added 2 commits September 10, 2026 14:33
`.prose-portal` styled exactly four things — paragraphs, unordered lists,
links and code — and Tailwind's preflight reset everything else, so every
other element an agent can write arrived with the browser reset and nothing
else: a GFM table with no rules and no padding, headings at body weight,
ordered lists with their numbers gone.

The element set is now covered where the render, the style and the copy
handler already live (PortalMarkdown.vue), so the bubble, the room and any
future surface inherit one look rather than a second one being copied out:

- Tables get the platform's own anatomy — mono-caps header on chrome, 6/12
  cell padding, a rule per row — as an object one step off the bubble tint,
  the same shape a code block already has. The scroll-viewport pair is lifted
  from CanvasKit (#2583): `display: block` makes the table its own horizontal
  viewport so a ten-column table scrolls instead of widening the bubble, and
  `overflow-wrap: normal` in the cells stops an inherited "break anywhere"
  squeezing an auto-layout table to one character per line. `width:
  fit-content` keeps a two-column table from drawing its border around an
  acre of nothing.
- GFM alignment survives sanitising as the `align` attribute, but a
  presentational hint loses to author CSS, so the right/center cases are
  restated and the table carries `tabular-nums`.
- Headings get a bounded ladder: `# Title` names a section of the reply, not
  the page, so it tops out at the design system's section size (18/650) and
  lands on the meta overline by h3 — three visibly distinct steps, all inside
  the six-size scale.
- Ordered lists, list items, nested lists, blockquotes (the `ck-callout` 3px
  left rule, so a quoted line reads the same in a message and on a canvas)
  and horizontal rules.

Nothing here touches the sanitiser: the treatment is CSS on platform-owned
selectors, no new admitted tags, attributes or classes. Code blocks keep the
#2515 behaviour — labelled bar, always-visible Copy, wrap-at-the-edge `pre`.

One #2515 assertion is narrowed rather than deleted: it read "the sheet has
no `overflow-x` at all", which was a claim about the `pre` (a scroller inside
a chat bubble hides the end of a line behind a gesture nobody makes) written
as a claim about the whole stylesheet. A table must scroll in its own
viewport, so the assertion now looks at the `pre` rule it was always about.

Verified in light and dark at 1100px and at 430px.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GawbQRicLvNMaPknFT8BfR
Its own commit, with the growth named, per the design-system contract — a
deliberate increase is never absorbed into a feature diff.

- components/portal/PortalMarkdown.vue: raw_gray 13 -> 34 (+21). Giving the
  Workspace transcript a typographic treatment (#2616) costs a surface,
  border and ink pair per element in BOTH themes — table, header row, cells,
  headings, blockquote, rule — and those are gray by definition ("everything
  else is gray"). raw_nongray stays 0; hardcoded_colors stays 0.
- components/portal/PortalJumpToLatest.vue: newly recorded at 1 gray. It
  arrived on dev after the last freeze and had no entry.

#2605's `refrozen` record is carried forward by hand: the regenerator writes
the file wholesale and would have dropped the history of why the ceiling sits
where it does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GawbQRicLvNMaPknFT8BfR
Comment thread src/frontend/tests/unit/portalMarkdownTypography.spec.js Fixed
CodeQL read the helper's one-pass `replace(/<!--[\s\S]*?-->/g, '')` as
js/incomplete-multi-character-sanitization (high): nested input
(`<!--<!-- -->`) leaves a live `<!--` behind. Nothing untrusted reaches
this helper — it reads a checked-in `.vue` file so the assertions are not
confused by prose in comments — but the loop is the rule's prescribed fix
and the stronger strip, and `hardeningGuide.spec.js` already carries the
same fixpoint shape for the same reason, so this matches the established
pattern rather than dismissing the alert.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lh6WrChj2636YEy5UZmZ1w
Comment thread src/frontend/tests/unit/portalMarkdownTypography.spec.js Fixed
obasilakis and others added 2 commits September 10, 2026 15:23
…2616)

The first attempt ran the fixpoint loop over all three comment strips
chained together, and CodeQL flagged the same rule again: the query only
recognises the loop as a complete sanitiser when the flagged replace's
result is assigned straight back to the loop variable, which a chain of
three breaks. Split out `withoutHtmlComments` so the shape matches
`hardeningGuide.spec.js` exactly — the one form already proven clean on
dev — and chain the JavaScript comment strips after it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lh6WrChj2636YEy5UZmZ1w
Conflict was `raw-color-baseline.json` alone, and only because both sides
re-froze it: dev's own frontend work moved `base/fieldClasses.js` 11 -> 15
gray and recorded `portal/PortalJumpToLatest.vue`, while this branch
recorded `portal/PortalMarkdown.vue` 13 -> 34.

The file is generated, so it was resolved by regenerating it against the
merged tree rather than by hand-picking hunks — the ratchet
(`tests/unit/rawColorRatchet.spec.js`) requires the baseline to be EXACT,
and a hand-merged entry that is merely plausible re-permits regressions up
to a stale ceiling. Both sides' `refrozen` prose is kept: dev's block plus
this branch's `_2616_note`, minus its now-stale sentence about
PortalJumpToLatest, which dev has since recorded itself.

Result differs from dev by exactly one entry (PortalMarkdown.vue, the
change this PR makes) and from this branch by exactly one (fieldClasses.js,
the change dev makes).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lh6WrChj2636YEy5UZmZ1w
@vybe

vybe commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

merge-train 2026-09-10 (evening run): riding. Your 21018c48 dev merge already produced the union the ratchet needs — PortalMarkdown 13 → 34 named in its own note, fieldClasses.js at dev's 15, totals 8744 equal to the scanner on the merged tree, dev's named_increases notes intact — so nothing was pushed to your branch. 44/44 on the ratchet + both typography specs against that head.

For the record, since the next person to hit this conflict will reach for it: node scripts/scan-raw-colors.mjs . --baseline raw-color-baseline.json overwrites the file with only generated/branch/commit/files/totals and drops the whole refrozen block, so it is not a resolution for a baseline conflict; the hand-merge you did is.

Two nits, not blockers: ul, ol { margin: 0.5rem 0 } lacks the :first-child / :last-child zeroing every other block got, so a reply that opens or closes on a list picks up 8px it did not have before; and _2616_note sits at refrozen._2616_note while its three siblings live under refrozen.named_increases. GitHub Advanced Security's two comments are the single-pass comment-strip on a checked-in test fixture — not real, and you fixed them anyway.

@vybe vybe left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

merge-train: batch validated on train/20260911-0925 (#2701)

@vybe
vybe merged commit a57d314 into dev Sep 11, 2026
26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants