Markdown Tables Not Rendered #76

Closed
opened 2026-06-02 09:04:36 +00:00 by rawan · 3 comments
Member

When the agent outputs a markdown table (| Step | Result | with |------|--------|), it renders as plain pipe-separated text instead of an HTML table. This makes structured output hard to read.

image

When the agent outputs a markdown table (| Step | Result | with |------|--------|), it renders as plain pipe-separated text instead of an HTML table. This makes structured output hard to read. ![image](/attachments/78c4a52f-f69f-418c-95e2-cd7cd3f32c96)
Author
Member

Implementation Spec for Issue #76 — Markdown Tables Not Rendered

Objective

Make agent/assistant-emitted GitHub-Flavored Markdown tables render as real HTML <table> elements in the chat UI, including the common case where the table is written directly beneath an intro line with no intervening blank line.

Root Cause

The chat UI uses a hand-rolled, dependency-free markdown renderer (not marked/markdown-it/comrak). Assistant/user messages render through ChatThread.tsx -> MessageBody.tsx -> Markdown.tsx.

Markdown.tsx already has full table support (a table block type, TABLE_SEP_RE, isTableRow/splitRow, a table-detection branch, and proper <table>/<thead>/<tbody> rendering with Tailwind borders). GFM tables and the issue's exact |------|--------| separator parse correctly.

The defect is in the paragraph-accumulation loop in parseBlocks. When a table is immediately preceded by a non-blank text line with no blank line in between (e.g. an agent writing Here are the results: then the table on the next line), the loop greedily consumes consecutive non-blank lines and stops only on blank lines, code fences, headings, HRs, lists, and blockquotes — but NOT at a table start. So the intro line plus the entire table (header + |---| separator + body rows) get swallowed into a single paragraph block and rendered as plain pipe-separated text. This matches the reported symptom exactly. Tables that begin a message or follow a blank line already render correctly.

Requirements

  • A markdown table directly below a text line (no blank line separator) must render as an HTML table.
  • Existing behavior for blank-line-separated and message-leading tables must be preserved.
  • No new runtime dependencies; keep the node-building, no-innerHTML approach.
  • The fix must be self-contained within the parser; rendering and CSS already work.

Files to Modify

  • crates/hero_shrimp_web/ui/src/components/Markdown.tsx — add a table-start guard to the paragraph-accumulation loop (plus a small helper). Only source change required.

Implementation Plan

Step 1: Add an isTableStart helper

Files: crates/hero_shrimp_web/ui/src/components/Markdown.tsx

  • After splitRow, add a helper encapsulating the table-start condition:
    isTableRow(lines[i]) && i + 1 < lines.length && TABLE_SEP_RE.test(lines[i + 1])
  • Centralizes the test so the table branch and paragraph loop cannot drift.
    Dependencies: none

Step 2: Use the helper in the table-detection branch

Files: crates/hero_shrimp_web/ui/src/components/Markdown.tsx

  • Replace the inline condition in the existing table branch with isTableStart(lines, i).
  • Behavior-preserving refactor; single source of truth.
    Dependencies: Step 1

Step 3: Stop paragraph accumulation at a table boundary

Files: crates/hero_shrimp_web/ui/src/components/Markdown.tsx

  • Add && !isTableStart(lines, i) to the paragraph while condition.
  • The paragraph loop ends before the table header, leaving the index at the header so the next iteration hits the table branch and parses the table correctly.
    Dependencies: Step 1

Step 4: Rebuild the embedded UI bundle

Files: build artifacts served via rust-embed

  • After editing the TSX, run cd crates/hero_shrimp_web/ui && npm run build so the new content-hashed bundle is embedded and served.
    Dependencies: Steps 1-3

Acceptance Criteria

  • A table written directly under a non-blank text line (no blank line) renders as an HTML table with header and body rows.
  • The exact issue example (| Step | Result | over |------|--------|) renders as a table, both when preceded by text and when standalone.
  • Tables preceded by a blank line or at the start of a message still render correctly (no regression).
  • The preceding intro text renders as its own paragraph, not merged into the table.
  • Plain pipe-containing prose that is not a table still renders as a normal paragraph (no false positives).
  • No new npm/runtime dependencies; rendering still uses SolidJS nodes (no innerHTML).
  • UI bundle rebuilt and the new asset embedded/served.

Notes

  • The renderer is intentionally hand-rolled to avoid innerHTML sanitization concerns and to integrate file-chip/image linking. Do not swap in a third-party markdown library.
  • Table styling already exists via inline Tailwind classes; no CSS change needed.
  • Out of scope: single-column tables (|------|) currently fail TABLE_SEP_RE since it requires at least two columns; the issue's table has two columns, so this fix resolves the report. Relaxing the regex can be a separate change.
## Implementation Spec for Issue #76 — Markdown Tables Not Rendered ### Objective Make agent/assistant-emitted GitHub-Flavored Markdown tables render as real HTML `<table>` elements in the chat UI, including the common case where the table is written directly beneath an intro line with no intervening blank line. ### Root Cause The chat UI uses a hand-rolled, dependency-free markdown renderer (not marked/markdown-it/comrak). Assistant/user messages render through `ChatThread.tsx` -> `MessageBody.tsx` -> `Markdown.tsx`. `Markdown.tsx` already has full table support (a `table` block type, `TABLE_SEP_RE`, `isTableRow`/`splitRow`, a table-detection branch, and proper `<table>/<thead>/<tbody>` rendering with Tailwind borders). GFM tables and the issue's exact `|------|--------|` separator parse correctly. The defect is in the paragraph-accumulation loop in `parseBlocks`. When a table is immediately preceded by a non-blank text line with no blank line in between (e.g. an agent writing `Here are the results:` then the table on the next line), the loop greedily consumes consecutive non-blank lines and stops only on blank lines, code fences, headings, HRs, lists, and blockquotes — but NOT at a table start. So the intro line plus the entire table (header + `|---|` separator + body rows) get swallowed into a single `paragraph` block and rendered as plain pipe-separated text. This matches the reported symptom exactly. Tables that begin a message or follow a blank line already render correctly. ### Requirements - A markdown table directly below a text line (no blank line separator) must render as an HTML table. - Existing behavior for blank-line-separated and message-leading tables must be preserved. - No new runtime dependencies; keep the node-building, no-`innerHTML` approach. - The fix must be self-contained within the parser; rendering and CSS already work. ### Files to Modify - `crates/hero_shrimp_web/ui/src/components/Markdown.tsx` — add a table-start guard to the paragraph-accumulation loop (plus a small helper). Only source change required. ### Implementation Plan #### Step 1: Add an `isTableStart` helper Files: `crates/hero_shrimp_web/ui/src/components/Markdown.tsx` - After `splitRow`, add a helper encapsulating the table-start condition: `isTableRow(lines[i]) && i + 1 < lines.length && TABLE_SEP_RE.test(lines[i + 1])` - Centralizes the test so the table branch and paragraph loop cannot drift. Dependencies: none #### Step 2: Use the helper in the table-detection branch Files: `crates/hero_shrimp_web/ui/src/components/Markdown.tsx` - Replace the inline condition in the existing table branch with `isTableStart(lines, i)`. - Behavior-preserving refactor; single source of truth. Dependencies: Step 1 #### Step 3: Stop paragraph accumulation at a table boundary Files: `crates/hero_shrimp_web/ui/src/components/Markdown.tsx` - Add `&& !isTableStart(lines, i)` to the paragraph `while` condition. - The paragraph loop ends before the table header, leaving the index at the header so the next iteration hits the table branch and parses the table correctly. Dependencies: Step 1 #### Step 4: Rebuild the embedded UI bundle Files: build artifacts served via rust-embed - After editing the TSX, run `cd crates/hero_shrimp_web/ui && npm run build` so the new content-hashed bundle is embedded and served. Dependencies: Steps 1-3 ### Acceptance Criteria - [ ] A table written directly under a non-blank text line (no blank line) renders as an HTML table with header and body rows. - [ ] The exact issue example (`| Step | Result |` over `|------|--------|`) renders as a table, both when preceded by text and when standalone. - [ ] Tables preceded by a blank line or at the start of a message still render correctly (no regression). - [ ] The preceding intro text renders as its own paragraph, not merged into the table. - [ ] Plain pipe-containing prose that is not a table still renders as a normal paragraph (no false positives). - [ ] No new npm/runtime dependencies; rendering still uses SolidJS nodes (no `innerHTML`). - [ ] UI bundle rebuilt and the new asset embedded/served. ### Notes - The renderer is intentionally hand-rolled to avoid `innerHTML` sanitization concerns and to integrate file-chip/image linking. Do not swap in a third-party markdown library. - Table styling already exists via inline Tailwind classes; no CSS change needed. - Out of scope: single-column tables (`|------|`) currently fail `TABLE_SEP_RE` since it requires at least two columns; the issue's table has two columns, so this fix resolves the report. Relaxing the regex can be a separate change.
Author
Member

Verification Results

The project has no JS unit-test runner configured for the UI (ui/package.json has no test script). Verification was done via the production build plus a standalone parser test that replicates the exact parseBlocks logic with the fix applied.

UI build

cd crates/hero_shrimp_web/ui && npm run build — succeeded. 99 modules transformed, new content-hashed bundle emitted to crates/hero_shrimp_web/static/assets/ and embedded via rust-embed.

Parser block-detection checks

Case Result
Intro line directly above table (no blank line) paragraph + table (was: one paragraph)
Standalone table table
Intro line + blank line + table paragraph + table (no regression)
Pipe-containing prose that is not a table paragraph (no false positive)

All four cases produce the expected blocks. The reported case — a table written directly under an intro line — now yields a separate paragraph and a real HTML table instead of plain pipe-separated text.

Note: pre-existing TypeScript type errors exist in unrelated components (ChatActivity, CostBadge, DiffViewer, HeroWelcome, store.ts). They are not introduced by this change and do not affect the Vite build (the build does not run tsc). Markdown.tsx has no type errors.

## Verification Results The project has no JS unit-test runner configured for the UI (`ui/package.json` has no `test` script). Verification was done via the production build plus a standalone parser test that replicates the exact `parseBlocks` logic with the fix applied. ### UI build `cd crates/hero_shrimp_web/ui && npm run build` — succeeded. 99 modules transformed, new content-hashed bundle emitted to `crates/hero_shrimp_web/static/assets/` and embedded via rust-embed. ### Parser block-detection checks | Case | Result | |------|--------| | Intro line directly above table (no blank line) | paragraph + table (was: one paragraph) | | Standalone table | table | | Intro line + blank line + table | paragraph + table (no regression) | | Pipe-containing prose that is not a table | paragraph (no false positive) | All four cases produce the expected blocks. The reported case — a table written directly under an intro line — now yields a separate paragraph and a real HTML table instead of plain pipe-separated text. Note: pre-existing TypeScript type errors exist in unrelated components (ChatActivity, CostBadge, DiffViewer, HeroWelcome, store.ts). They are not introduced by this change and do not affect the Vite build (the build does not run `tsc`). `Markdown.tsx` has no type errors.
Author
Member

Implementation Summary

Fixed markdown tables not rendering when written directly beneath an intro line.

Root cause

The hand-rolled markdown parser in crates/hero_shrimp_web/ui/src/components/Markdown.tsx accumulated paragraph lines until it hit a blank line or another block type, but did not stop at a table boundary. A table placed directly under a non-blank text line (no blank line in between) was absorbed into the paragraph and rendered as plain pipe-separated text.

Changes

  • crates/hero_shrimp_web/ui/src/components/Markdown.tsx
    • Added an isTableStart(lines, i) helper centralizing the table-start condition (table row followed by a separator line).
    • Refactored the existing table-detection branch to use the helper.
    • Added !isTableStart(lines, i) as a stop condition in the paragraph-accumulation loop so paragraph capture halts at a table boundary, letting the table parse as its own block.
  • Rebuilt the embedded UI bundle (npm run build) so the new content-hashed asset is served via rust-embed.

Verification

  • UI production build succeeded.
  • Standalone parser checks confirmed: intro+table (no blank line) now yields a separate paragraph and table; standalone tables and blank-line-separated tables still render correctly; pipe-containing prose that is not a table is not misdetected.

No new dependencies were added and no CSS changes were needed (table styling already existed). The fix is contained to the parser.

## Implementation Summary Fixed markdown tables not rendering when written directly beneath an intro line. ### Root cause The hand-rolled markdown parser in `crates/hero_shrimp_web/ui/src/components/Markdown.tsx` accumulated paragraph lines until it hit a blank line or another block type, but did not stop at a table boundary. A table placed directly under a non-blank text line (no blank line in between) was absorbed into the paragraph and rendered as plain pipe-separated text. ### Changes - `crates/hero_shrimp_web/ui/src/components/Markdown.tsx` - Added an `isTableStart(lines, i)` helper centralizing the table-start condition (table row followed by a separator line). - Refactored the existing table-detection branch to use the helper. - Added `!isTableStart(lines, i)` as a stop condition in the paragraph-accumulation loop so paragraph capture halts at a table boundary, letting the table parse as its own block. - Rebuilt the embedded UI bundle (`npm run build`) so the new content-hashed asset is served via rust-embed. ### Verification - UI production build succeeded. - Standalone parser checks confirmed: intro+table (no blank line) now yields a separate paragraph and table; standalone tables and blank-line-separated tables still render correctly; pipe-containing prose that is not a table is not misdetected. No new dependencies were added and no CSS changes were needed (table styling already existed). The fix is contained to the parser.
rawan closed this issue 2026-06-03 13:31:12 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lhumina_code/hero_shrimp#76
No description provided.