Render uploaded Markdown files as documents; remove the redundant connector strip - #366
Conversation
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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
| // eslint-disable-next-line @next/next/no-img-element -- arbitrary remote/user content, next/image needs configured domains | ||
| <img alt={alt ?? ""} loading="lazy" {...props} /> | ||
| ), | ||
| }} |
There was a problem hiding this comment.
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)
Reviewed by Cursor Bugbot for commit cb39be6. Configure here.
| .replace(/[^\w\s-]/g, "") | ||
| .replace(/\s+/g, "-") || "section" | ||
| ); | ||
| } |
There was a problem hiding this comment.
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)
Reviewed by Cursor Bugbot for commit cb39be6. Configure here.
There was a problem hiding this comment.
💡 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".
| // Before the code prefixes: "text/x-" would otherwise claim text/x-markdown. | ||
| if (mime === "text/markdown" || mime === "text/x-markdown") return "markdown"; |
There was a problem hiding this comment.
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 👍 / 👎.
| > | ||
| {content} |
There was a problem hiding this comment.
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 👍 / 👎.
| const res = await fetch(url); | ||
| if (!res.ok) throw new Error(`${res.status} ${res.statusText}`); | ||
| setContent(await res.text()); |
There was a problem hiding this comment.
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 👍 / 👎.
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>
There was a problem hiding this comment.
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).
❌ 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]); |
There was a problem hiding this comment.
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.
Reviewed by Cursor Bugbot for commit ff9c984. Configure here.


Summary
.mdfiles now render as formatted documents instead of raw source in an iframe: new"markdown"display type ingetDocumentDisplayTyperouted to a newMarkdownViewer(react-markdown + remark-gfm + remark-math + rehype-katex + highlight.js + mermaid — all already in the dependency tree, zero new runtime deps; only a@types/hastdevDep).mermaidfences rendered as diagrams, an "On this page" outline with scroll-spy and GitHub-style anchors, a rendered/source toggle (source mode reusesCodeViewer), and word count / reading time — all in design-token typography, both themes.Related
Checklist
pnpm checkpasses (lint + typecheck)pnpm --filter @launchstack/web testpassespackages/core/changed —pnpm changeset) — N/A, nopackages/*changesTesting
components/__tests__/MarkdownViewer.test.tsx: mime/extension classification, real GFM rendering (headings, table, task-list states, external-linktarget), outline construction + heading ids, source-view toggle, fetch-error retry. These render the actual remark/rehype pipeline, not mocks.apps/webjest suite: 2,158 passed / 0 failed;tsc --noEmitand eslint clean (0 errors, no new design-token warnings)./dev/markdown-viewer— a new auth-free dev harness (existing/dev/*pattern) that mounts the real component with a fixture over adata: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
rehype-rawis deliberately not used — uploads are untrusted, so raw HTML in a document stays inert; mermaid runs withsecurityLevel: "strict".text/markdown/text/x-markdownare checked before thetext/x-code-mime prefix, which would otherwise claim x-markdown; the.mdextension check sits before the code-extension regex.transformIgnorePatternsinapps/web/jest.config.jsnever matched pnpm's.pnpmstore 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 theesmMarkdownDepslist.next/dynamic, so the katex/hljs/mermaid weight is only paid when a markdown document is opened.ADD_TABSstays 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
.mdfiles are classified as a newmarkdowndisplay type and open in aMarkdownViewer(GFM, math, code, mermaid, outline, source toggle) instead of a raw iframe.DocumentViewerloads that viewer vianext/dynamicand threads an optionalhighlightinto markdown and code previews.Citation click-through wires grounded answers to the document modal:
AskPanelcitation rows are buttons (with optional page badges);WorkspaceShellmaps API references toThreadReference(page,matchText) and setsCitationHighlightwith anoncefor repeat jumps.find-text-rangenormalizes snippet text for DOM/PDF text-layer matching;PdfViewerWithNotes,CodeViewer, andMarkdownViewerscroll and overlay highlights.The corpus “Bring in more” connector strip is removed from
KnowledgePane(add flows stay in the Add Source modal). JesttransformIgnorePatternsnow use a sharedesmDepslist so pnpm paths and the remark/rehype graph transform under tests. A non-production/dev/markdown-viewerharness exercises the viewer with a fixturedata:URL.Reviewed by Cursor Bugbot for commit ff9c984. Bugbot is set up for automated code reviews on this repo. Configure here.