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 00000000..7bc89d08 --- /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 d7f10a5a..488038ff 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 f9196497..967c1989 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 d4910f65..aa06598e 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 ( - <> - -