diff --git a/specs/012a-markdown-prose-rendering/plan.md b/specs/012a-markdown-prose-rendering/plan.md new file mode 100644 index 000000000..dcc5a7910 --- /dev/null +++ b/specs/012a-markdown-prose-rendering/plan.md @@ -0,0 +1,34 @@ +# Implementation Plan: Fix Core Markdown Layout Bugs + +**Branch**: `feat/markdown-layout-bugs` | **Date**: 2026-04-28 | **Spec**: [spec.md](spec.md) + +## Summary + +Five targeted changes to `message.tsx` fixing structural rendering bugs. One file, ~10 lines changed. No new dependencies. Must ship before 012b (tables) and 012c (GFM elements). + +## Technical Context + +**Language**: TypeScript/React +**Target file**: `components/frontend/src/components/ui/message.tsx` +**Testing**: vitest (`npx vitest run`) +**Risk**: Medium — outer wrapper changes affect every bot message; scroll-position preservation in `MessagesTab.tsx` must be verified + +## Files + +``` +components/frontend/src/components/ui/ +└── message.tsx # MODIFY: 5 targeted changes (lines 78, 90, 93, 314, 321, + new overrides) +``` + +## Change Summary + +| Line | Before | After | +|------|--------|-------| +| 314 | `"text-sm text-foreground font-mono"` | `"text-sm text-foreground"` | +| 321 | `
` | `
` | +| 78 | `mb-[0.2rem]` | `mb-2` | +| 90 | `space-y-1` (ul) | `space-y-1.5` | +| 93 | `space-y-1` (ol) | `space-y-1.5` | +| new | — | `strong`, `em`, `del` component overrides | + +The streaming cursor `` at line 329 is **not modified**. Its `inline-block align-middle` classes keep it at the text baseline regardless of parent display type. diff --git a/specs/012a-markdown-prose-rendering/spec.md b/specs/012a-markdown-prose-rendering/spec.md new file mode 100644 index 000000000..13ef15317 --- /dev/null +++ b/specs/012a-markdown-prose-rendering/spec.md @@ -0,0 +1,121 @@ +# Feature Specification: Fix Core Markdown Layout Bugs + +**Feature Branch**: `feat/markdown-layout-bugs` +**Created**: 2026-04-28 +**Status**: Draft +**Input**: Desired prose rendering for bot messages: block layout, proportional typography, paragraph spacing, and inline element styling. Prerequisite for specs 012b and 012c. + +## Overview + +Bot message prose renders in the application's proportional sans-serif font with clear vertical rhythm. All block-level elements — paragraphs, headings, lists, tables — stack vertically in normal document flow and occupy the full message container width. Paragraph spacing is visually distinct. Bold, italic, and strikethrough text carry the correct weight, style, and `text-foreground` color. Inline code is the only monospace element in a message. + +The animated streaming cursor appears at the text baseline of the last rendered character, staying on the same line via `inline-block` and `align-middle`. + +Explicit component overrides for ``, ``, and `` ensure these elements receive the correct Tailwind color utilities rather than relying on browser defaults. List item gap (`space-y-1.5`) is harmonized with paragraph spacing (`mb-2`) for consistent vertical rhythm throughout a message. + +This spec is prerequisite for specs 012b (table rendering) and 012c (GFM elements) — block-level elements require vertical document flow to render correctly. + +--- + +## User Scenarios & Testing + +### Block Layout for ReactMarkdown Output + +The `` output wrapper uses block layout (`w-full`) so that block-level elements — paragraphs, tables, lists, headings — stack vertically and fill the container. This is the structural foundation that all other markdown rendering (tables, blockquotes, list indentation) depends on. + +The streaming cursor `` uses `inline-block` and `align-middle`, which positions it at the text baseline regardless of the parent wrapper's display type — no special handling is required. + +**Acceptance**: Given any bot message with multiple block-level markdown elements, when rendered, the elements stack vertically, table rows are horizontal, and the streaming cursor appears at the text baseline of the last rendered character. + +--- + +### User Story 1 — Prose Text Is Readable (Priority: P1) + +A user reads a multi-paragraph bot response. Paragraphs are clearly separated. Headings, bold, and italic text render in sans-serif, not monospace. Inline code is the only monospace element. + +**Why this priority**: The `font-mono` wrapper affects every bot message. Its removal is the highest-impact single-line change in this file. + +**Independent Test**: Send a message with two paragraphs, `**bold**`, `*italic*`, and `` `code` ``. Verify paragraphs are in a proportional font, bold is heavier, italic is slanted, only inline code is monospace. + +**Acceptance Scenarios**: + +1. **Given** a bot message with body text, **When** rendered, **Then** the message content wrapper (`div` at line 314) does NOT carry the `font-mono` CSS class; `font-family` resolves to the project's sans-serif stack (Inter / system-ui) +2. **Given** `**bold**` in a bot message, **When** rendered, **Then** the `` element has `font-weight: 600` or higher and `font-family` resolves to sans-serif +3. **Given** `*italic*` in a bot message, **When** rendered, **Then** the `` element has `font-style: italic` and `font-family` resolves to sans-serif +4. **Given** inline `` `code` `` in a bot message, **When** rendered, **Then** the `` element has `font-family` resolving to a monospace stack — the only element in the message that does + +--- + +### User Story 2 — Paragraphs Have Readable Spacing (Priority: P1) + +A user reads a bot message with three or more paragraphs. Each paragraph is visually distinct from the next with clear vertical separation. + +**Why this priority**: `mb-[0.2rem]` (3.2px) affects every multi-paragraph response. At `text-sm` this is imperceptible. + +**Independent Test**: Send a message with three short paragraphs. Verify each paragraph's bottom margin is at least 8px (Tailwind `mb-2`). + +**Acceptance Scenarios**: + +1. **Given** a bot message with 3+ paragraphs, **When** rendered, **Then** each `

` (rendered as a `

` by the component override) has `margin-bottom` of exactly `0.5rem` (8px, `mb-2`) +2. **Given** a bot message with 3+ paragraphs in a scroll container with `max-h-96`, **When** rendered, **Then** the container scrolls correctly and does not clip the last paragraph's bottom margin +3. **Given** the session messages list (`MessagesTab.tsx`) with 20+ messages already rendered, **When** a new bot message arrives with the corrected paragraph spacing, **Then** the viewport position of previously rendered messages is unaffected — the layout change to the new message does not cause a visible scroll jump in content above it + +--- + +### User Story 3 — Bold, Italic, and Strikethrough Render Correctly (Priority: P1) + +A user receives a message using `**bold**`, `*italic*`, and `~~strikethrough~~`. All three render with correct visual style in sans-serif. + +**Why this priority**: These are the most common inline formatting elements. They were broken by the `font-mono` wrapper — once the wrapper is fixed, explicit overrides ensure the correct color and weight. + +**Independent Test**: Send a message with all three inline formats. Verify each is visually distinct from plain text. + +**Acceptance Scenarios**: + +1. **Given** `**bold text**` in a bot message, **When** rendered, **Then** the `` element uses `font-semibold` (`font-weight: 600`) and `text-foreground` color +2. **Given** `*italic text*` in a bot message, **When** rendered, **Then** the `` element uses `italic` font style and `text-foreground` color +3. **Given** `~~strikethrough~~` in a bot message, **When** rendered, **Then** the `` element has `text-decoration: line-through` and `opacity: 0.7` (`opacity-70`) + +--- + +### Edge Cases + +- When a bot message contains only a code block (no paragraphs), the `font-mono` removal must not affect code rendering — the `code` override already sets `font-mono` explicitly. +- When the message is actively streaming and the inline wrapper changes to block, the animated cursor `` must appear at the baseline of the last rendered character on the same line (not on a new line below). Verify by streaming a multi-word sentence and confirming cursor stays inline. +- When a message contains nested formatting (`**bold with _italic_ inside**`), both overrides must compose correctly. +- When a scroll container loads earlier messages above the current viewport, paragraph spacing increase must not cause visible scroll jump (MessagesTab scroll-preservation logic). + +--- + +## Requirements + +### Functional Requirements + +- **FR-001**: `message.tsx` line 321 MUST change `className="inline"` to `className="w-full"`. The streaming cursor `` at line 329 MUST NOT be modified — it already positions correctly with `inline-block` and `align-middle`. +- **FR-002**: `message.tsx` line 314 MUST remove `font-mono` from the class list. The resulting class string must be `"text-sm text-foreground"` (plus the existing conditional `!isBot && "py-2 px-4"`). `font-mono` must remain on the `code` component override (lines 57 and 68) and nowhere else in the message content wrapper tree. +- **FR-003**: The `p` component override MUST change `mb-[0.2rem]` to `mb-2`. The `leading-relaxed` class and `text-muted-foreground` color MUST be retained. +- **FR-004**: `defaultComponents` MUST include a `strong` override: `{children}`. +- **FR-005**: `defaultComponents` MUST include an `em` override: `{children}`. +- **FR-006**: `defaultComponents` MUST include a `del` override: `{children}`. +- **FR-007**: The `ul` and `ol` overrides MUST change `space-y-1` to `space-y-1.5` to harmonize list item gap with corrected paragraph spacing. +- **FR-008**: All Tailwind classes using theme colors MUST reference Tailwind utilities (`bg-muted`, `text-muted-foreground`, `text-foreground`, `border-border`) — not raw CSS variable names. + +### Key Entities + +- **`defaultComponents`** (`message.tsx:38`): The `Components` map passed to ``; primary target for all inline-element overrides in this spec. +- **Content wrapper div** (`message.tsx:314`): The outer `
` applying `font-mono` and `text-sm` to all markdown output. FR-002 targets this line. +- **ReactMarkdown wrapper div** (`message.tsx:321`): The `
` immediately wrapping ``. FR-001 targets this line. +- **Streaming cursor ``** (`message.tsx:329`): The blinking cursor rendered during streaming. Not modified by this spec. + +--- + +## Success Criteria + +### Measurable Outcomes + +- **SC-001**: The content wrapper div at line 314 does not contain `font-mono` in its `className` (grep-verifiable) +- **SC-002**: The ReactMarkdown wrapper div at line 321 has `className="w-full"` (grep-verifiable) +- **SC-003**: `defaultComponents` contains entries for `strong`, `em`, and `del` (grep-verifiable) +- **SC-004**: The `p` override contains `mb-2` not `mb-[0.2rem]` (grep-verifiable) +- **SC-005**: A streaming bot message with paragraph text shows the animated cursor on the same line as the last rendered character, not on a new line below it (manual visual test during streaming) +- **SC-006**: `cd components/frontend && npx vitest run` passes with no new failures diff --git a/specs/012a-markdown-prose-rendering/tasks.md b/specs/012a-markdown-prose-rendering/tasks.md new file mode 100644 index 000000000..2dc434e49 --- /dev/null +++ b/specs/012a-markdown-prose-rendering/tasks.md @@ -0,0 +1,53 @@ +# Tasks: Fix Core Markdown Layout Bugs + +**Input**: Design documents from `/specs/012a-markdown-prose-rendering/` + +## Phase 1: Structural Wrapper Fixes + +- [ ] T001 [Prereq] `message.tsx:321` — change `className="inline"` to `className="w-full"`. Do NOT touch line 329 (streaming cursor). +- [ ] T002 [FR-002] `message.tsx:314` — remove `font-mono` from the class string. Result: `cn("text-sm text-foreground", !isBot && "py-2 px-4")`. + +### Commit: `fix(frontend): remove inline wrapper and font-mono from message content` + +--- + +## Phase 2: Spacing Fixes + +- [ ] T010 [FR-003] `message.tsx` `p` override — change `mb-[0.2rem]` to `mb-2` +- [ ] T011 [FR-007] `message.tsx` `ul` override — change `space-y-1` to `space-y-1.5` +- [ ] T012 [FR-007] `message.tsx` `ol` override — change `space-y-1` to `space-y-1.5` + +### Commit: `fix(frontend): increase paragraph and list spacing in markdown renderer` + +--- + +## Phase 3: Inline Element Overrides + +- [ ] T020 [FR-004] Add `strong` to `defaultComponents`: `strong: ({ children }) => {children}` +- [ ] T021 [FR-005] Add `em` to `defaultComponents`: `em: ({ children }) => {children}` +- [ ] T022 [FR-006] Add `del` to `defaultComponents`: `del: ({ children }) => {children}` + +### Commit: `feat(frontend): add strong, em, del component overrides to markdown renderer` + +--- + +## Phase 4: Verify + +- [ ] T030 Run `cd components/frontend && npx vitest run` — confirm no failures +- [ ] T031 Manually stream a bot message with `**bold** *italic* ~~strike~~` — confirm sans-serif, no monospace leak +- [ ] T032 Manually send a 3-paragraph bot message — confirm 8px margin visible between paragraphs +- [ ] T033 Visually confirm streaming cursor stays on same line as last character during active streaming +- [ ] T034 Check `MessagesTab` scroll behavior: load a session with 20+ messages, scroll to a middle message, trigger a new message — confirm viewport does not jump + +### Commit (if lint fixes needed): `chore: lint fixes` + +--- + +## Dependencies + +- Phase 1 → independent (start here) +- Phase 2 → independent of Phase 1 (can be parallel) +- Phase 3 → independent of Phases 1 and 2 +- Phase 4 → depends on Phases 1–3 +- Spec 012b → depends on this spec being merged +- Spec 012c → depends on this spec being merged diff --git a/specs/012b-markdown-table-rendering/plan.md b/specs/012b-markdown-table-rendering/plan.md new file mode 100644 index 000000000..35b7c42c1 --- /dev/null +++ b/specs/012b-markdown-table-rendering/plan.md @@ -0,0 +1,32 @@ +# Implementation Plan: Markdown Table Rendering + +**Branch**: `feat/markdown-tables` | **Date**: 2026-04-28 | **Spec**: [spec.md](spec.md) +**Depends on**: `feat/markdown-layout-bugs` (spec 012a) merged to main + +## Summary + +Add six table component overrides to `message.tsx` and share them with `tool-message.tsx` via a new shared constant file. Move `markdownComponents` in `tool-message.tsx` from inside the `ExpandableMarkdown` function body to module level. No new npm dependencies; uses existing Tailwind utilities and react-markdown API. + +## Technical Context + +**Language**: TypeScript/React +**Target files**: `components/frontend/src/components/ui/message.tsx`, `tool-message.tsx`, new `markdown-components.ts` +**Testing**: vitest + manual visual verification +**Risk**: Low — new component overrides do not affect existing elements; `prose-sm` on ExpandableMarkdown may conflict (verify with devtools) + +## Files + +``` +components/frontend/src/ +├── lib/ +│ └── markdown-components.ts # NEW: exports sharedMarkdownComponents (table overrides; extended by 012c) +└── components/ui/ + ├── message.tsx # MODIFY: import and spread sharedMarkdownComponents into defaultComponents + └── tool-message.tsx # MODIFY: move markdownComponents to module level; spread sharedMarkdownComponents +``` + +## Architecture Note + +Extracting table overrides into `src/lib/markdown-components.ts` (not `src/components/ui/`) keeps library-style shared utilities separate from UI components. The exported constant is named `sharedMarkdownComponents` — not `tableComponents` — because spec 012c adds blockquote, hr, img, and h4–h6 overrides to the same constant; a table-specific name would be misleading. Both `message.tsx` and `tool-message.tsx` spread the shared map into their local component definitions, preserving any component-specific overrides. + +`markdownComponents` in `tool-message.tsx` is currently defined inside the `ExpandableMarkdown` function body (lines 66–93). It must be lifted to module level so it can be defined once and reference the imported `sharedMarkdownComponents` without re-instantiating the object on every render. diff --git a/specs/012b-markdown-table-rendering/spec.md b/specs/012b-markdown-table-rendering/spec.md new file mode 100644 index 000000000..cf01fd86b --- /dev/null +++ b/specs/012b-markdown-table-rendering/spec.md @@ -0,0 +1,109 @@ +# Feature Specification: Markdown Table Rendering + +**Feature Branch**: `feat/markdown-tables` +**Created**: 2026-04-28 +**Status**: Draft +**Input**: GFM table rendering requirements for bot messages and tool results. Depends on spec 012a being merged first. + +## Overview + +GFM tables in bot messages and tool results render with visible borders on all cells, padded content, a visually distinct header row, and horizontal overflow protection for wide tables. All colors use theme-aware Tailwind utilities (`border-border`, `bg-muted`, `text-foreground`, `text-muted-foreground`) so tables adapt automatically to light and dark mode without hardcoded values. + +Both `message.tsx` and `tool-message.tsx` share identical table component overrides via `sharedMarkdownComponents` in `src/lib/markdown-components.ts`, ensuring consistent table rendering wherever markdown appears in the UI. + +**Prerequisite**: Spec 012a must be merged. The `className="inline"` wrapper removed in 012a is what allows tables to render in block flow — without that fix, table rows collapse into inline content regardless of component overrides. + +**Design note on prose vs. custom overrides**: The project uses `prose prose-sm dark:prose-invert` in `file-content-viewer.tsx` and `recent-updates-dialog.tsx`. The custom-overrides approach chosen here for `message.tsx` and `tool-message.tsx` is a pre-existing architectural inconsistency, not a new decision. This spec does not resolve that inconsistency; it extends the custom-overrides pattern already established in `message.tsx` since introducing `prose` would require removing the existing component map, re-testing all message rendering, and aligning `tool-message.tsx`'s `prose-sm` usage — scope for a separate architectural decision. + +--- + +## User Scenarios & Testing + +### User Story 1 — Tables Are Readable (Priority: P1) + +A user asks the agent to summarize data. The agent responds with a GFM table. The user can distinguish columns, read cells, and identify headers. + +**Why this priority**: Tables are currently the most visually broken element — completely unstyled. This is the primary trigger for this spec set. + +**Independent Test**: Send a bot message containing a 3-column, 4-row GFM table. Verify borders, padding, and header background are visible. + +**Acceptance Scenarios**: + +1. **Given** a bot message containing a GFM table, **When** rendered in light mode, **Then** every `` and `` element has a `1px solid` border using the `border-border` Tailwind utility, and padding of `px-3 py-2` +2. **Given** the same table in dark mode, **When** the theme is toggled, **Then** borders and header backgrounds use the same `border-border`/`bg-muted` utilities and remain visible without hardcoded colors +3. **Given** a table's `` cells, **When** rendered, **Then** headers have `bg-muted` background, `font-medium` weight, and `text-left` alignment — visually distinct from `` data cells +4. **Given** a table with `` rows in ``, **When** rendered, **Then** alternating rows do NOT require background striping (out of scope); rows are separated by cell borders alone + +--- + +### User Story 2 — Wide Tables Do Not Break Layout (Priority: P1) + +A user receives a table with 6+ columns whose total width exceeds the message container. The page does not scroll horizontally; the table scrolls independently within its container. + +**Why this priority**: An unstyled wide table causes horizontal page overflow, breaking the entire session layout. + +**Independent Test**: Send a bot message with an 8-column table. Verify the page body has no horizontal scrollbar; the table itself is scrollable. + +**Acceptance Scenarios**: + +1. **Given** a table wider than the message container, **When** rendered, **Then** the `` element is wrapped in a `
` and the page body does not scroll horizontally +2. **Given** the overflow wrapper, **When** the viewport is 375px wide (mobile), **Then** the table is independently scrollable and no content is clipped + +--- + +### User Story 3 — Tables Render Consistently in Tool Messages (Priority: P1) + +The same GFM table renders identically whether it appears in a bot message or inside a tool result's `ExpandableMarkdown`. + +**Why this priority**: `tool-message.tsx` uses a separate `markdownComponents` map with no table overrides, so tool result tables are completely unstyled even after message.tsx is fixed. + +**Independent Test**: Render the same table markdown in a bot message and in a tool result. Compare side-by-side — borders, padding, and header styling must match. + +**Acceptance Scenarios**: + +1. **Given** a tool result containing a GFM table rendered via `ExpandableMarkdown`, **When** displayed, **Then** `
` section (some GFM parsers allow this) must still render with bordered `` cells. +- A table immediately followed by a paragraph must have `my-2` (8px vertical margin) to separate it from adjacent content. + +--- + +## Requirements + +### Functional Requirements + +- **FR-001**: `defaultComponents` in `message.tsx` MUST add a `table` override: `
` and `` elements have the same `border-border`, `px-3 py-2`, and `bg-muted` (for headers) as bot message tables +2. **Given** `tool-message.tsx` applies `prose-sm` as a class on the `ExpandableMarkdown` wrapper, **When** the custom table overrides are applied, **Then** the Tailwind component utility classes on ``/`` take precedence over any `prose-sm` base styles (verified by inspecting computed styles in browser devtools) + +--- + +### Edge Cases + +- A table cell containing inline code, bold text, or a link must render correctly — nested inline elements inside cells inherit the table override's `text-sm text-foreground`. +- A single-column table must render with borders on both sides of the single column. +- A table with no `
{children}
`. The `overflow-x-auto` wrapper is on the containing `
`, not the `` element itself. The `
` element itself does NOT carry `border border-border` — with `border-collapse: collapse`, the outermost cell borders already form the table's outer edge; a separate table border is redundant. +- **FR-002**: `defaultComponents` MUST add a `thead` override: `{children}`. +- **FR-003**: `defaultComponents` MUST add a `tbody` override: `{children}` (no additional classes required; row separation comes from cell borders). +- **FR-004**: `defaultComponents` MUST add a `tr` override: `{children}`. The `` element does NOT carry border classes — with `border-collapse: collapse`, row separation comes from the `border border-border` on `` is redundant and produces double borders. +- **FR-005**: `defaultComponents` MUST add a `th` override: ``. +- **FR-006**: `defaultComponents` MUST add a `td` override: ``. +- **FR-007**: `tool-message.tsx`'s `markdownComponents` MUST receive the identical `table`, `thead`, `tbody`, `tr`, `th`, and `td` overrides defined in FR-001 through FR-006. The shared overrides MUST be extracted into a named export `sharedMarkdownComponents` in `src/lib/markdown-components.ts`, importable by both files to prevent drift. The name `sharedMarkdownComponents` (not `tableComponents`) is required because spec 012c adds non-table overrides to the same constant. Additionally, `markdownComponents` in `tool-message.tsx` is currently defined inside the `ExpandableMarkdown` function body — it MUST be moved to module level before the spread can import the shared constant without re-instantiating it on every render. +- **FR-008**: All border, background, and text colors MUST use Tailwind utilities that reference theme variables (`border-border`, `bg-muted`, `text-foreground`, `text-muted-foreground`) — no hardcoded colors. +- **FR-009**: The `prose-sm` class applied to `ExpandableMarkdown` wrappers in `tool-message.tsx` (lines 467, 643, 692) MUST NOT override the custom table component classes. If computed-style conflicts are found during implementation, the `markdownComponents` table overrides take precedence (add `!important` via arbitrary Tailwind values only as a last resort; prefer removing conflicting `prose-sm` classes first). + +### Key Entities + +- **`defaultComponents`** (`message.tsx:38`): Primary target; receives table/thead/tbody/tr/th/td overrides. +- **`markdownComponents`** (`tool-message.tsx:66`): Parallel map that must receive identical table overrides via shared constant. Currently defined inside the `ExpandableMarkdown` function body — must be moved to module level as part of this spec. +- **`sharedMarkdownComponents`** (new shared constant): Extracted component override map to be imported by both `message.tsx` and `tool-message.tsx`. Location: `components/frontend/src/lib/markdown-components.ts`. Named `sharedMarkdownComponents` (not `tableComponents`) to accommodate the non-table entries added in spec 012c. +- **`ExpandableMarkdown`** (`tool-message.tsx`): The component applying `prose-sm` wrapper class; must not conflict with table overrides. + +--- + +## Success Criteria + +### Measurable Outcomes + +- **SC-001**: A 3-column, 4-row GFM table in a bot message renders with visible borders on all cells in both light and dark mode (manual visual test; Cypress: `cy.get('table td').should('have.css', 'border-width', '1px')`) +- **SC-002**: A table with 8 columns in a 1280px viewport does not cause `document.documentElement.scrollWidth > document.documentElement.clientWidth` (Cypress automatable) +- **SC-003**: The same table markdown in a bot message and in a tool result `ExpandableMarkdown` produces visually identical border, padding, and header styling (manual side-by-side comparison) +- **SC-004**: `border-collapse` is applied to the `
` and `` cells; a separate `border-b` on `
{children}{children}
` element so adjacent cell borders do not double up (Cypress: `cy.get('table').should('have.css', 'border-collapse', 'collapse')`) +- **SC-005**: `cd components/frontend && npx vitest run` passes with no new failures diff --git a/specs/012b-markdown-table-rendering/tasks.md b/specs/012b-markdown-table-rendering/tasks.md new file mode 100644 index 000000000..d9dae8a5e --- /dev/null +++ b/specs/012b-markdown-table-rendering/tasks.md @@ -0,0 +1,54 @@ +# Tasks: Markdown Table Rendering + +**Input**: Design documents from `/specs/012b-markdown-table-rendering/` +**Prerequisite**: Spec 012a (`feat/markdown-layout-bugs`) merged to main and this branch rebased on it + +## Phase 1: Shared Component File + +- [ ] T001 [FR-007] Create `components/frontend/src/lib/markdown-components.ts` +- [ ] T002 [FR-001–006] Add `sharedMarkdownComponents` export containing overrides for `table`, `thead`, `tbody`, `tr`, `th`, `td` using the exact Tailwind classes from spec FR-001 through FR-006 +- [ ] T003 Verify the file has no `any` types and exports a `Components`-compatible type from `react-markdown` + +### Commit: `feat(frontend): add shared markdown table component overrides` + +--- + +## Phase 2: Wire into message.tsx + +- [ ] T010 [FR-001] In `message.tsx`, import `sharedMarkdownComponents` from `../../lib/markdown-components` +- [ ] T011 Spread `sharedMarkdownComponents` into `defaultComponents`: `const defaultComponents: Components = { ...sharedMarkdownComponents, code: ..., p: ..., ... }` + +### Commit: `feat(frontend): wire table components into message.tsx markdown renderer` + +--- + +## Phase 3: Wire into tool-message.tsx + +- [ ] T020 [FR-007] In `tool-message.tsx`, move the `markdownComponents` definition from inside the `ExpandableMarkdown` function body to module level (currently lines 66–93, inside the function). This is required before the shared import can work without re-instantiating on every render. +- [ ] T021 [FR-007] Import `sharedMarkdownComponents` from `../../lib/markdown-components` and spread into the now-module-level `markdownComponents`: `const markdownComponents: Components = { ...sharedMarkdownComponents, ... }` +- [ ] T022 [FR-009] Open browser devtools and inspect a rendered tool-result table: verify `th`/`td` computed styles show `border-width: 1px` and `padding-left: 12px` (px-3). Note: `prose-sm` is declared via `@plugin "@tailwindcss/typography"` in `globals.css` but NOT registered in `tailwind.config.js` plugins array — if computed styles show no `prose-sm` interference, that may be why (no action needed). If conflicts ARE observed, remove the `prose-sm` class from the relevant `ExpandableMarkdown` wrappers (lines 467, 643, 692) rather than using `!important` overrides. + +### Commit: `feat(frontend): wire table components into tool-message.tsx markdown renderer` + +--- + +## Phase 4: Verify + +- [ ] T030 Run `cd components/frontend && npx vitest run` — confirm no failures +- [ ] T031 Manually send a bot message with a 3-column, 4-row GFM table in light mode — confirm borders, padding, header background visible +- [ ] T032 Toggle to dark mode — confirm borders and header remain visible (no hardcoded colors) +- [ ] T033 Send a bot message with an 8-column table — confirm no horizontal page scroll (`scrollWidth === clientWidth`) +- [ ] T034 Send a tool-use message that produces a table in the result — confirm same styling as bot message table +- [ ] T035 Check mobile viewport (375px) — confirm table scrolls horizontally within its container, page body does not scroll + +### Commit (if lint fixes needed): `chore: lint fixes` + +--- + +## Dependencies + +- Phase 1 → start here (no dependencies on other phases) +- Phase 2 → depends on Phase 1 +- Phase 3 → depends on Phase 1 (independent of Phase 2) +- Phase 4 → depends on Phases 1–3 +- Spec 012c → can share the `markdown-components.ts` file introduced here diff --git a/specs/012c-markdown-gfm-rendering/plan.md b/specs/012c-markdown-gfm-rendering/plan.md new file mode 100644 index 000000000..90f9acc1a --- /dev/null +++ b/specs/012c-markdown-gfm-rendering/plan.md @@ -0,0 +1,29 @@ +# Implementation Plan: Complete GFM Element Support + +**Branch**: `feat/markdown-gfm-elements` | **Date**: 2026-04-28 | **Spec**: [spec.md](spec.md) +**Depends on**: `feat/markdown-layout-bugs` (spec 012a) merged to main +**Extends**: `markdown-components.ts` introduced in spec 012b (if merged; otherwise create it here) + +## Summary + +Add four element overrides to the shared `markdown-components.ts` constant: `blockquote`, `hr`, `img`, and `h4`/`h5`/`h6`. Both `message.tsx` and `tool-message.tsx` inherit them automatically through the spread pattern introduced in 012b. No new npm dependencies. + +## Technical Context + +**Language**: TypeScript/React +**Target files**: `components/frontend/src/components/ui/markdown-components.ts` (primary), `message.tsx` and `tool-message.tsx` only if 012b is not yet merged +**Testing**: vitest + manual visual verification +**Risk**: Low — all additions are new component overrides that do not interact with existing overrides + +## Files + +``` +components/frontend/src/components/ui/ +└── markdown-components.ts # MODIFY: add blockquote, hr, img, h4, h5, h6 overrides +``` + +If spec 012b has not been merged when this branch is cut, `markdown-components.ts` must be created here and wired into both `message.tsx` and `tool-message.tsx` following the same pattern described in 012b's plan. + +## H4–H6 Decision Gate + +Before implementing T020–T022, confirm with the team whether LLM output in this product ever generates H4–H6 headings. If not, skip those tasks and note the decision in the PR description. The spec marks this story P3 and explicitly allows it to be cut. diff --git a/specs/012c-markdown-gfm-rendering/spec.md b/specs/012c-markdown-gfm-rendering/spec.md new file mode 100644 index 000000000..5afbadabc --- /dev/null +++ b/specs/012c-markdown-gfm-rendering/spec.md @@ -0,0 +1,129 @@ +# Feature Specification: Complete GFM Element Support + +**Feature Branch**: `feat/markdown-gfm-elements` +**Created**: 2026-04-28 +**Status**: Draft +**Input**: Visual styling requirements for blockquotes, horizontal rules, images, and extended heading levels in bot messages. Depends on spec 012a being merged first. + +## Overview + +Bot messages containing blockquotes, horizontal rules, images, or extended heading levels render with appropriate visual treatment consistent with the rest of the message design: + +- **`blockquote`** (P1): Themed left border (`border-l-4 border-border`) with indented italic text in `text-muted-foreground`, making quoted content immediately distinguishable from prose. Elevated to P1 because AI responses use `>` quoting frequently — citations, emphasis, user input echoes. +- **`hr`** (P2): Full-width line in the theme's border color (`border-t border-border`) with vertical margin, providing visual section separation in dark and light mode. +- **`img`** (P2): Constrained to container width (`max-w-full`) with rounded corners, preventing layout overflow regardless of source image dimensions. +- **`h4`/`h5`/`h6`** (P3): Follow the established heading scale — progressively smaller than H3 (`text-sm font-medium`). May be cut if the team confirms LLM output does not generate these levels. + +All overrides apply identically to `tool-message.tsx` via the shared `sharedMarkdownComponents` constant from spec 012b. After this spec, `sharedMarkdownComponents` covers all common GFM elements: tables, blockquote, hr, img, and h4–h6. + +**Prerequisite**: Spec 012a must be merged. The `font-mono` and `inline` wrapper fixes in 012a are required for blockquotes and other block-level elements to render in proper block flow with correct typography. + +**Parallel development risk**: Both this spec and spec 012b add entries to `src/lib/markdown-components.ts` and export `sharedMarkdownComponents`. If 012b and 012c are developed on concurrent branches, the shared file is a merge conflict surface. The recommended sequence is to merge 012b first and rebase this branch on top. If parallel development is unavoidable, see tasks.md for the conflict resolution protocol. + +--- + +## User Scenarios & Testing + +### User Story 1 — Blockquotes Are Visually Distinct (Priority: P1) + +A user receives a bot response that quotes a user's earlier message or cites a source using `>`. The quoted text is immediately recognizable as quoted content, distinct from prose. + +**Why this priority**: Blockquotes appear in nearly every multi-turn conversation where the agent references prior context. Without styling, quoted text is indistinguishable from regular prose — users lose the attribution signal entirely. + +**Independent Test**: Send a bot message with `> quoted text`. Verify a left border and indentation appear. + +**Acceptance Scenarios**: + +1. **Given** a blockquote (`> text`) in a bot message, **When** rendered, **Then** the `
` element has a `border-l-4` left border using `border-border` color and `pl-4` left padding +2. **Given** a blockquote, **When** rendered, **Then** the blockquote text uses `text-muted-foreground` color and `italic` style, visually lighter than surrounding prose +3. **Given** a multi-line blockquote (`> line 1\n> line 2`), **When** rendered, **Then** both lines are contained within the same styled blockquote block with consistent border and padding +4. **Given** a blockquote in dark mode, **When** the theme is toggled, **Then** the border uses `border-border` (theme-aware) and remains visible + +--- + +### User Story 2 — Horizontal Rules Separate Content (Priority: P2) + +A user receives a response using `---` to separate sections. The separator renders as a full-width horizontal line in the theme's border color. + +**Why this priority**: Section separators are a common document structure element. Browser-default `
` may render with incorrect color in dark mode. + +**Independent Test**: Send a bot message containing `---`. Verify a horizontal line appears across the message width. + +**Acceptance Scenarios**: + +1. **Given** a horizontal rule (`---`) in a bot message, **When** rendered, **Then** an `
` element appears with `border-t border-border my-4` — full-width, theme-colored, with vertical margin +2. **Given** a horizontal rule in dark mode, **When** the theme is toggled, **Then** the border color resolves to the dark-mode `--border` value (no hardcoded color) + +--- + +### User Story 3 — Images Do Not Break Layout (Priority: P2) + +A bot message contains a markdown image (`![alt](url)`). The image renders within the message container without causing horizontal overflow or covering adjacent content. + +**Why this priority**: Without `max-width`, a large image expands beyond the message container and breaks the session layout. This is a layout-safety requirement, not an aesthetic one. + +**Independent Test**: Render a bot message with an image URL pointing to a 2000px-wide image. Verify the image does not cause horizontal overflow. + +**Acceptance Scenarios**: + +1. **Given** a bot message containing `![alt](url)` where the image is wider than the container, **When** rendered, **Then** the `` element has `max-w-full` and `rounded` classes and does not cause `scrollWidth > clientWidth` on the page +2. **Given** a broken image URL, **When** rendered, **Then** the `alt` text is visible and the element does not stretch beyond its container + +--- + +### User Story 4 — H4–H6 Headings Follow the Size Scale (Priority: P3) + +A bot message uses four or more heading levels. H4–H6 are progressively smaller than H3 and larger than body text. + +**Why this priority**: Lower heading levels are rarely generated by LLMs. This is a completeness item. If the team determines H4–H6 never appear in practice, this story may be cut without affecting P1 or P2 deliverables. + +**Independent Test**: Send a message with `####`, `#####`, `######`. Verify each is smaller than the one above and larger than `text-sm` body text. + +**Acceptance Scenarios**: + +1. **Given** an H4 heading, **When** rendered, **Then** the element has `text-xs font-medium text-foreground mb-1` — visually smaller than H3 (`text-sm font-medium`) and larger than `text-xs text-muted-foreground` body text by weight contrast +2. **Given** H4, H5, H6 in the same message, **When** rendered, **Then** each level's `font-size` (computed) is less than or equal to the level above it + +--- + +### Edge Cases + +- A blockquote containing a nested blockquote (`>> nested`) must render with correct nesting (the inner blockquote gets its own border-l-4 and pl-4 inside the outer one). +- A blockquote containing inline code must not apply `font-mono` to the blockquote prose — only to the `` element within it. +- An `` rendered inside a table cell must still obey `max-w-full` to avoid breaking the cell. +- H4–H6 immediately following H1–H3 must have consistent `mb-*` spacing so the heading hierarchy feels uniform. +- A `---` rule following a paragraph with `mb-2` must not produce excessive whitespace — the `my-4` on `
` accounts for both top and bottom gap. + +--- + +## Requirements + +### Functional Requirements + +- **FR-001**: `defaultComponents` in `message.tsx` MUST add a `blockquote` override: `
{children}
`. +- **FR-002**: `defaultComponents` MUST add an `hr` override: `
`. +- **FR-003**: `defaultComponents` MUST add an `img` override: `{alt`. The `??` fallback is required because react-markdown types `src` and `alt` as `string | undefined`; omitting the fallback produces a TypeScript error. The override MUST pass through `src` and `alt` props from the markdown source. +- **FR-004**: `defaultComponents` MUST add `h4`, `h5`, and `h6` overrides following the existing h1–h3 scale: + - `h4`: `

{children}

` + - `h5`: `
{children}
` + - `h6`: `
{children}
` +- **FR-005**: The identical `blockquote`, `hr`, `img`, and `h4`/`h5`/`h6` overrides MUST be applied to `tool-message.tsx`'s `markdownComponents`. These SHOULD be extracted into the shared `markdown-components.ts` constant introduced in spec 012b (FR-007) to avoid duplication. +- **FR-006**: All colors MUST use Tailwind utilities referencing theme variables (`border-border`, `text-muted-foreground`, `text-foreground`) — no hardcoded hex or `rgba` values. + +### Key Entities + +- **`defaultComponents`** (`message.tsx:38`): Receives all new element overrides in this spec. +- **`markdownComponents`** (`tool-message.tsx:66`): Receives the same overrides via the shared constant from 012b. +- **`markdown-components.ts`** (shared constant from spec 012b): The canonical source for all shared component overrides; extended in this spec with blockquote, hr, img, h4–h6. + +--- + +## Success Criteria + +### Measurable Outcomes + +- **SC-001**: A bot message with `> blockquote` renders with a visible left border and indented italic text in both light and dark mode (manual visual test; Cypress: `cy.get('blockquote').should('have.css', 'border-left-width', '4px')`) +- **SC-002**: A bot message with `---` renders a full-width `
` element with no hardcoded color (inspect computed `border-color` resolves to the theme's `--border` value) +- **SC-003**: A bot message with a 2000px-wide image does not cause horizontal page scroll (`document.documentElement.scrollWidth === document.documentElement.clientWidth`) +- **SC-004**: A bot message with `####` renders an element with `font-size` smaller than the H3 override's `text-sm` (14px) — verifiable with `cy.get('h4').invoke('css', 'font-size').then(parseFloat).should('be.lte', 14)` — or the story is cut if team confirms LLMs never generate H4–H6 +- **SC-005**: `cd components/frontend && npx vitest run` passes with no new failures diff --git a/specs/012c-markdown-gfm-rendering/tasks.md b/specs/012c-markdown-gfm-rendering/tasks.md new file mode 100644 index 000000000..50cea72ae --- /dev/null +++ b/specs/012c-markdown-gfm-rendering/tasks.md @@ -0,0 +1,59 @@ +# Tasks: Complete GFM Element Support + +**Input**: Design documents from `/specs/012c-markdown-gfm-rendering/` +**Prerequisite**: Spec 012a (`feat/markdown-layout-bugs`) merged to main and this branch rebased on it +**Coordination with 012b**: Both 012b and 012c write to `components/frontend/src/lib/markdown-components.ts` and export `sharedMarkdownComponents`. + +- **Option A (preferred)**: Merge spec 012b to main first, then rebase this branch. `markdown-components.ts` already exists with the `sharedMarkdownComponents` export — add the new GFM element overrides directly to the same object. No conflict possible. +- **Option B (parallel development)**: Create `markdown-components.ts` at `src/lib/markdown-components.ts` with only the 012c overrides under the same export name. When 012b merges first and you rebase: git will report a conflict in `markdown-components.ts`. Resolve by merging BOTH sets of overrides into the single `sharedMarkdownComponents` object — keep all table entries from 012b AND all GFM element entries from 012c. Do NOT discard either set. + +## Phase 1: Blockquote (P1) + +- [ ] T001 [FR-001] Add `blockquote` to the shared component map in `markdown-components.ts`: + `blockquote: ({ children }) =>
{children}
` +- [ ] T002 Manual visual test: bot message with `> quoted text` — confirm left border, italic, muted color in light and dark mode + +### Commit: `feat(frontend): add blockquote override to markdown renderer` + +--- + +## Phase 2: Horizontal Rule and Image (P2) + +- [ ] T010 [FR-002] Add `hr` to the shared component map: + `hr: () =>
` +- [ ] T011 [FR-003] Add `img` to the shared component map: + `img: ({ src, alt }) => {alt` +- [ ] T012 Manual visual test: bot message with `---` — confirm full-width themed separator +- [ ] T013 Manual visual test: bot message with a wide image — confirm no horizontal page scroll + +### Commit: `feat(frontend): add hr and img overrides to markdown renderer` + +--- + +## Phase 3: H4–H6 Headings (P3 — confirm before implementing) + +- [ ] T020 [Decision] Confirm with team: do LLM responses in this product ever generate H4–H6? If NO, skip T021–T023 and document the decision in the PR. +- [ ] T021 [FR-004] Add `h4` override: `h4: ({ children }) =>

{children}

` +- [ ] T022 [FR-004] Add `h5` override: `h5: ({ children }) =>
{children}
` +- [ ] T023 [FR-004] Add `h6` override: `h6: ({ children }) =>
{children}
` + +### Commit: `feat(frontend): add h4-h6 overrides to markdown renderer` + +--- + +## Phase 4: Verify + +- [ ] T030 Run `cd components/frontend && npx vitest run` — confirm no failures +- [ ] T031 Confirm `tool-message.tsx` tables, blockquotes, and separators pick up the new overrides automatically (they spread `markdown-components.ts` — no file change needed if 012b is merged) +- [ ] T032 Grep for `any` types in changed files: `grep -n ': any' components/frontend/src/lib/markdown-components.ts` + +### Commit (if lint fixes needed): `chore: lint fixes` + +--- + +## Dependencies + +- Phase 1 (blockquote) → independent; start here +- Phase 2 (hr, img) → independent of Phase 1 +- Phase 3 (h4–h6) → independent; gated on team decision +- Phase 4 → depends on Phases 1–3