Skip to content

Render uploaded Markdown files as documents; remove the redundant connector strip - #366

Merged
Deodat-Lawson merged 4 commits into
mainfrom
claude/ui-improvements-47dff4
Aug 30, 2026
Merged

Render uploaded Markdown files as documents; remove the redundant connector strip#366
Deodat-Lawson merged 4 commits into
mainfrom
claude/ui-improvements-47dff4

Conversation

@Deodat-Lawson

@Deodat-Lawson Deodat-Lawson commented Aug 29, 2026

Copy link
Copy Markdown
Owner

Summary

  • Uploaded .md files now render as formatted documents instead of raw source in an iframe: new "markdown" display type in getDocumentDisplayType routed to a new MarkdownViewer (react-markdown + remark-gfm + remark-math + rehype-katex + highlight.js + mermaid — all already in the dependency tree, zero new runtime deps; only a @types/hast devDep).
  • The viewer ships GFM tables/task lists/footnotes, KaTeX math, highlighted code blocks with per-block copy, mermaid fences rendered as diagrams, an "On this page" outline with scroll-spy and GitHub-style anchors, a rendered/source toggle (source mode reuses CodeViewer), and word count / reading time — all in design-token typography, both themes.
  • Removes the "Bring in more" connector strip from the bottom of the corpus view; it duplicated every entry point the Add Source modal already offers.

Related

Checklist

  • pnpm check passes (lint + typecheck)
  • pnpm --filter @launchstack/web test passes
  • Changeset added (if packages/core/ changed — pnpm changeset) — N/A, no packages/* changes
  • New env vars documented — N/A, none added
  • UI changes exercised in a browser (not just a green build)
  • Docs updated if behavior changed — N/A

Testing

  • 8 new tests in components/__tests__/MarkdownViewer.test.tsx: mime/extension classification, real GFM rendering (headings, table, task-list states, external-link target), outline construction + heading ids, source-view toggle, fetch-error retry. These render the actual remark/rehype pipeline, not mocks.
  • Full apps/web jest suite: 2,158 passed / 0 failed; tsc --noEmit and eslint clean (0 errors, no new design-token warnings).
  • Exercised live at /dev/markdown-viewer — a new auth-free dev harness (existing /dev/* pattern) that mounts the real component with a fixture over a data: URL. Verified visually plus a DOM audit: KaTeX display+inline, mermaid SVG (8 nodes), 17 hljs tokens, checkbox states, footnotes, anchors, and both theme palettes.

Notes for reviewers

  • Security default: rehype-raw is deliberately not used — uploads are untrusted, so raw HTML in a document stays inert; mermaid runs with securityLevel: "strict".
  • Classification ordering matters: text/markdown / text/x-markdown are checked before the text/x- code-mime prefix, which would otherwise claim x-markdown; the .md extension check sits before the code-extension regex.
  • Jest fix riding along: transformIgnorePatterns in apps/web/jest.config.js never matched pnpm's .pnpm store paths, so the pre-existing ESM allowlist (react-markdown etc.) was dead — importing react-markdown under jest failed outright. It's now a .pnpm-aware pattern pair; future ESM-only test deps go in the esmMarkdownDeps list.
  • The viewer is loaded via next/dynamic, so the katex/hljs/mermaid weight is only paid when a markdown document is opened.
  • ADD_TABS stays in _workspace/types.ts — the Add Source modal and its tests still consume it; only the duplicate strip UI is gone.

🤖 Generated with Claude Code


Note

Medium Risk
User-uploaded markdown is rendered client-side (mermaid with strict mode, no raw HTML pipeline), and citation highlighting depends on fuzzy text matching that may miss or mis-locate passages in edge cases.

Overview
Uploaded .md files are classified as a new markdown display type and open in a MarkdownViewer (GFM, math, code, mermaid, outline, source toggle) instead of a raw iframe. DocumentViewer loads that viewer via next/dynamic and threads an optional highlight into markdown and code previews.

Citation click-through wires grounded answers to the document modal: AskPanel citation rows are buttons (with optional page badges); WorkspaceShell maps API references to ThreadReference (page, matchText) and sets CitationHighlight with a nonce for repeat jumps. find-text-range normalizes snippet text for DOM/PDF text-layer matching; PdfViewerWithNotes, CodeViewer, and MarkdownViewer scroll and overlay highlights.

The corpus “Bring in more” connector strip is removed from KnowledgePane (add flows stay in the Add Source modal). Jest transformIgnorePatterns now use a shared esmDeps list so pnpm paths and the remark/rehype graph transform under tests. A non-production /dev/markdown-viewer harness exercises the viewer with a fixture data: URL.

Reviewed by Cursor Bugbot for commit ff9c984. Bugbot is set up for automated code reviews on this repo. Configure here.

Deodat-Lawson and others added 2 commits August 29, 2026 16:53
The strip duplicated every entry point that the Add Source modal already
offers, at the bottom of the library where it read as page content rather
than an action. The modal (and the empty state's "Add your first source")
remain the entry points; ADD_TABS stays in types.ts for both.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Uploaded .md files were classified as "text" and shown as raw source in an
iframe. They now get their own "markdown" display type (text/markdown and
text/x-markdown mimes — checked before the text/x- code prefix that would
otherwise claim them — plus .md/.markdown/.mdown/.mkd extensions) routed to
a new MarkdownViewer.

The viewer is built on react-markdown + remark-gfm + remark-math +
rehype-katex + highlight.js + mermaid, all already in the dependency tree —
no new runtime deps (only a @types/hast devDep). rehype-raw is deliberately
absent: uploads are untrusted, so raw HTML stays inert, and mermaid runs
with securityLevel strict. Features: token-based document typography,
an outline sidebar with scroll-spy and GitHub-style heading anchors, a
rendered/source toggle (source mode reuses CodeViewer), per-block code
copy, and word count / reading time. Loaded via next/dynamic so only
markdown documents pay for the katex/hljs/mermaid bundle.

A /dev/markdown-viewer harness mounts the real component with a fixture
served over a data: URL, following the existing /dev/* preview pattern.

Also fixes apps/web jest transformIgnorePatterns: the ESM allowlist never
matched pnpm's .pnpm store paths, so react-markdown could not be imported
under jest at all. The list is now applied through a .pnpm-aware pattern
pair, which is what lets the new viewer tests render the real pipeline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 29, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
launch-stack Error Error Aug 30, 2026 5:33pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
pdr-ai-v2 Ignored Ignored Aug 30, 2026 5:33pm

// eslint-disable-next-line @next/next/no-img-element -- arbitrary remote/user content, next/image needs configured domains
<img alt={alt ?? ""} loading="lazy" {...props} />
),
}}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Unstable components remount on re-render

Medium Severity

The components map passed to ReactMarkdown is recreated every render, so pre, a, table, and img are new function identities each time. React then remounts every CodeBlock and MermaidBlock. Scroll-spy updates to activeHeading, plus toolbar actions like copy or outline toggle, wipe highlighted code, reset per-block copy state, and send mermaid diagrams back through “Rendering diagram…”.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit cb39be6. Configure here.

Comment thread apps/web/src/app/employer/documents/types/document.ts
Comment thread apps/web/src/app/employer/documents/components/MarkdownViewer.tsx
.replace(/[^\w\s-]/g, "")
.replace(/\s+/g, "-") || "section"
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Slugify drops non-ASCII headings

Low Severity

slugify strips every character outside [A-Za-z0-9_] via [^\w\s-], so headings in other scripts (and letters like Ü or é) collapse to section, section-1, and so on. Outline clicks and in-document # links no longer match the slugs authors actually write.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit cb39be6. Configure here.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cb39be65ca

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +86 to +87
// Before the code prefixes: "text/x-" would otherwise claim text/x-markdown.
if (mime === "text/markdown" || mime === "text/x-markdown") return "markdown";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Prefer Markdown extensions over generic text MIME types

When an uploaded .md file has the common generic MIME type text/plain, this exact-match check is skipped and the subsequent mime.startsWith("text/") branch returns text before filename inference runs. AddSourceModal.registerDocument persists the browser-provided File.type, so affected uploads continue opening in the old iframe instead of the new Markdown viewer; classify Markdown extensions before the generic text fallback, or allow generic MIME types to fall through to extension detection.

Useful? React with 👍 / 👎.

Comment on lines +436 to +437
>
{content}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Bound the amount of Markdown parsed in the browser

Passing the complete file to ReactMarkdown has no size guard even though /api/upload-local accepts files up to DOCUMENT_LIMITS.MAX_FILE_SIZE_MB (128 MB). Opening a large Markdown upload therefore builds a correspondingly huge string and syntax tree synchronously on the UI thread, which can freeze or exhaust the viewer tab; reject oversized Markdown previews or fall back to a bounded/raw representation.

Useful? React with 👍 / 👎.

Comment on lines +168 to +170
const res = await fetch(url);
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
setContent(await res.text());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Ignore responses from superseded Markdown fetches

If url changes while the previous request is still in flight, both fetchContent calls remain active and whichever resolves last overwrites content and loading; a slow response for document A can therefore be rendered under document B's title and controls. Add an AbortController or effect cleanup/current-request guard before committing the response.

Useful? React with 👍 / 👎.

Deodat-Lawson and others added 2 commits August 30, 2026 13:30
Citation rows under a grounded answer were inert text. They are now buttons:
clicking one opens the document viewer scrolled to the cited passage with a
gold highlight over it. The backend always returned page + matchText per
reference — the workspace UI just dropped them; ThreadReference now carries
both, and AskPanel shows a page badge.

Locating the passage is a shared utility (~/lib/find-text-range.ts): matching
runs lowercased, markdown-marker-stripped, and fully whitespace-free — block
boundaries and PDF text-layer line breaks drop whitespace, so any
space-sensitive match fails across lines — with prefix-window fallbacks for
clipped snippets and a char map back to a real DOM Range. aria-hidden opts
chrome (line-number gutters, the overlays themselves) out of the search.

Viewers: PdfViewerWithNotes searches the pdf.js text layer (retrying as each
page's layer renders) and reuses the note-overlay quad math; MarkdownViewer
and CodeViewer draw absolutely-positioned rects over the Range. Jumps are
instant rather than smooth — animated scrolling across thousands of pixels is
disorienting. Formats without a text DOM just open the document.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-47dff4

Conflicts were all dependency infrastructure:
- jest.config.js: both sides rewrote transformIgnorePatterns to survive
  pnpm's .pnpm store paths. Kept this branch's list-driven pattern pair and
  folded main's better-auth allowlist entries (better-auth, @better-auth,
  @better-fetch, @noble, nanostores, kysely, defu, rou3, uncrypto, jose)
  into the shared esmDeps list, with a [\w+.-]* tail on scoped packages so
  one spelling matches both the hoisted and flattened store dirs.
- package.json: took main's better-auth description; the @types/hast devDep
  from this branch auto-merged.
- pnpm-lock.yaml: regenerated from main's lockfile plus the merged manifest.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

There are 3 total unresolved issues (including 2 from previous reviews).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit ff9c984. Configure here.

// Instant, not smooth: a citation can sit thousands of pixels down a
// long document, and an animated scroll there is slow and disorienting.
startEl?.scrollIntoView?.({ block: "center" });
}, [highlight, content, loading, viewMode]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Citation overlay misses layout shifts

Medium Severity

The citation overlay is measured once from [highlight, content, loading, viewMode]. MermaidBlock and CodeBlock then replace placeholders asynchronously and change document height. Overlays and scrollIntoView stay at the pre-diagram positions, so the cited passage is highlighted and scrolled to the wrong place.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ff9c984. Configure here.

@Deodat-Lawson
Deodat-Lawson merged commit 3fb78fd into main Aug 30, 2026
11 of 16 checks passed
@Deodat-Lawson
Deodat-Lawson deleted the claude/ui-improvements-47dff4 branch August 30, 2026 17:49
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.

1 participant