From 42911dfb176a80a4f77296e00b03dcaa0931e071 Mon Sep 17 00:00:00 2001 From: Nickk'z Date: Sat, 31 Oct 2026 02:02:37 +0530 Subject: [PATCH] Render markdown documents in the Workspace Rendered view Opening a .md file in the Workspace (or the standalone artifact editor) showed the raw source in the Rendered view: literal "##", backticks and list markers instead of formatted headings, code and lists. A markdown file is a text document, which fell straight through to the plain source textarea in both views, so Rendered and Code looked the same. Markdown documents now render properly: the Rendered view shows the formatted document using the same renderer the conversation uses, and the Code view holds the editable raw source, so the text stays editable. Other text documents are unchanged. The markdown surfaces are split into a small new file so the shared surface switcher stays under its size cap (its baseline is tightened to match). A test covers the markdown detection and that Rendered formats while Code stays an editable source. --- .../[id]/artifact-editor-markdown.test.tsx | 61 +++++++++++++++ .../[id]/artifact-editor-model.test.ts | 12 +++ .../artifacts/[id]/artifact-editor-model.ts | 9 +++ .../artifacts/[id]/artifact-editor-state.ts | 6 ++ .../[id]/artifact-editor-surfaces.tsx | 33 ++------ .../[id]/artifact-editor-text-surfaces.tsx | 77 +++++++++++++++++++ .../components/foundations/ratchet.test.ts | 2 +- 7 files changed, 172 insertions(+), 28 deletions(-) create mode 100644 frontend/app/(library)/agent/artifacts/[id]/artifact-editor-markdown.test.tsx create mode 100644 frontend/app/(library)/agent/artifacts/[id]/artifact-editor-text-surfaces.tsx diff --git a/frontend/app/(library)/agent/artifacts/[id]/artifact-editor-markdown.test.tsx b/frontend/app/(library)/agent/artifacts/[id]/artifact-editor-markdown.test.tsx new file mode 100644 index 000000000..7bc89d08c --- /dev/null +++ b/frontend/app/(library)/agent/artifacts/[id]/artifact-editor-markdown.test.tsx @@ -0,0 +1,61 @@ +// A markdown source document shows the FORMATTED document in the Rendered view +// (no raw "##" or backticks) and the editable raw source in the Code view. A +// non-markdown source document keeps the plain editable surface in both. +import { describe, expect, test } from "bun:test"; +import { renderToStaticMarkup } from "react-dom/server"; + +import type { WorkpieceEditorController } from "./artifact-editor-state"; +import { WorkpieceSurfaces } from "./artifact-editor-surfaces"; + +const SOURCE = "## Pull requests\n\n**bold** text and `code`\n\n- item one"; + +function editor(overrides: Partial): WorkpieceEditorController { + return { + artifactId: "a1", + pdfEmbed: false, + isMarkdown: false, + isRich: false, + isSheetGrid: false, + isSlidesEditor: false, + label: "Document source", + loading: false, + value: SOURCE, + sizeBytes: 10, + pdfPreviewUrl: "", + canonicalSource: () => SOURCE, + setSource: () => {}, + ...overrides, + } as unknown as WorkpieceEditorController; +} + +describe("markdown workpiece surfaces", () => { + test("Rendered view formats markdown instead of showing the raw source", () => { + const html = renderToStaticMarkup( + , + ); + // The heading and emphasis are real elements, not literal markers. + expect(html).toContain("Pull requests"); + expect(html).toContain(""); + expect(html).not.toContain("## Pull requests"); + expect(html).not.toContain("**bold**"); + // It is a read-only preview, not the editable textarea. + expect(html).not.toContain(" { + const html = renderToStaticMarkup( + , + ); + expect(html).toContain(" { + const html = renderToStaticMarkup( + , + ); + expect(html).toContain(" { expect(artifactEditorMode(artifact("pdf", "Report.pdf"))).toBe("pdf-text"); }); + test("recognizes markdown source documents by extension", () => { + expect(isMarkdownArtifact("AGENTS.md")).toBe(true); + expect(isMarkdownArtifact("README.markdown")).toBe(true); + expect(isMarkdownArtifact("Notes.MD")).toBe(true); + expect(isMarkdownArtifact("guide.mdx")).toBe(true); + expect(isMarkdownArtifact("script.py")).toBe(false); + expect(isMarkdownArtifact("data.json")).toBe(false); + expect(isMarkdownArtifact("plain.txt")).toBe(false); + expect(isMarkdownArtifact("Makefile")).toBe(false); + }); + test("round-trips quoted CSV cells", () => { const rows = parseCsv('name,notes\n"ACME, Inc.","line 1\nline 2"\nquote,"a ""b"""'); expect(rows).toEqual([ diff --git a/frontend/app/(library)/agent/artifacts/[id]/artifact-editor-model.ts b/frontend/app/(library)/agent/artifacts/[id]/artifact-editor-model.ts index d7f10a5ae..488038ff7 100644 --- a/frontend/app/(library)/agent/artifacts/[id]/artifact-editor-model.ts +++ b/frontend/app/(library)/agent/artifacts/[id]/artifact-editor-model.ts @@ -21,6 +21,15 @@ export type ArtifactEditorMode = | "slides-json" | "pdf-text"; +/** Text documents whose source is markdown: the Rendered view formats them + * (headings, bold, lists, code) instead of showing the raw source. */ +const MARKDOWN_EXTENSIONS = new Set(["md", "markdown", "mdx"]); + +/** True when a source document should render as formatted markdown. */ +export function isMarkdownArtifact(name: string): boolean { + return MARKDOWN_EXTENSIONS.has(artifactFileExtension(name)); +} + export function artifactEditorMode( artifact: Pick & Readonly<{ diff --git a/frontend/app/(library)/agent/artifacts/[id]/artifact-editor-state.ts b/frontend/app/(library)/agent/artifacts/[id]/artifact-editor-state.ts index f9196497d..967c1989e 100644 --- a/frontend/app/(library)/agent/artifacts/[id]/artifact-editor-state.ts +++ b/frontend/app/(library)/agent/artifacts/[id]/artifact-editor-state.ts @@ -26,6 +26,7 @@ import type { OrgChange } from "@/lib/org-changes"; import { type ArtifactEditorMode, artifactEditorMode, + isMarkdownArtifact, presentationTemplate, richDocumentTemplate, sanitizeRichHtml, @@ -171,6 +172,9 @@ export interface WorkpieceEditorController { readonly isRich: boolean; readonly isSlidesEditor: boolean; readonly isSheetGrid: boolean; + /** A markdown source document: the Rendered view shows formatted markdown and + * the Code view holds the editable source. */ + readonly isMarkdown: boolean; /** A byte-authoritative (published) PDF: render the embedded preview, never * the text editor or raw bytes. */ readonly pdfEmbed: boolean; @@ -237,6 +241,7 @@ export function useWorkpieceEditor( const isRich = editorMode === "rich-document"; const isSlidesEditor = editorMode === "slides-json"; const isSheetGrid = editorMode === "sheet-grid"; + const isMarkdown = editorMode === "source-document" && isMarkdownArtifact(artifact.name); const normalizeValue = useCallback( (next: string) => (isRich ? sanitizeRichHtml(next) : next), @@ -478,6 +483,7 @@ export function useWorkpieceEditor( isRich, isSlidesEditor, isSheetGrid, + isMarkdown, pdfEmbed, pdfPreviewUrl: pdfEmbed ? withCacheBust(artifact.preview_url, reloadNonce) : artifact.preview_url, sizeBytes: artifact.size_bytes, diff --git a/frontend/app/(library)/agent/artifacts/[id]/artifact-editor-surfaces.tsx b/frontend/app/(library)/agent/artifacts/[id]/artifact-editor-surfaces.tsx index d4910f657..aa06598e3 100644 --- a/frontend/app/(library)/agent/artifacts/[id]/artifact-editor-surfaces.tsx +++ b/frontend/app/(library)/agent/artifacts/[id]/artifact-editor-surfaces.tsx @@ -39,6 +39,7 @@ import { import { type CSSProperties, type RefObject, useId, useState } from "react"; import { Select, SelectItem } from "@/components/base/select/select"; import type { WorkpieceEditorController } from "./artifact-editor-state"; +import { MarkdownWorkpieceSurface, SourceSurface } from "./artifact-editor-text-surfaces"; import { sanitizeRichHtml } from "./artifact-editor-model"; import { DeckSlideCanvas } from "./deck-canvas"; import { SheetGridSurface } from "./sheet-grid"; @@ -923,33 +924,6 @@ export function DeckSurface({ ); } -export function SourceSurface({ - label, - loading, - value, - onChange, -}: { - readonly label: string; - readonly loading: boolean; - readonly value: string; - readonly onChange: (value: string) => void; -}) { - return ( - <> - -