Render background task output with rich markdown

Add a read-only TipTap-backed RichMarkdownViewer and use it for Background Tasks output so rendered index.md files can display the same rich fenced blocks as notes, including email, calendar, chart, table, image, embed, transcript, and Mermaid blocks.

Keep the existing Source/Rendered toggle for raw markdown inspection, and hide editor-only delete controls in read-only output.

Move the rich block format examples out of the LiveNote-only prompt and into the shared knowledge note style guide. This gives both LiveNote and Background Task agents the same canonical renderer contract, including exact fenced-code schemas for rich Markdown blocks and the rule to avoid emitting task blocks as agent output.

Verified with:
- npm run build in apps/x/apps/renderer
- npm run build in apps/x/packages/core
This commit is contained in:
Ramnique Singh 2026-05-19 09:59:01 +05:30
parent 65f8e9d678
commit fe5e67f810
5 changed files with 322 additions and 166 deletions

View file

@ -18,6 +18,7 @@ import { toast } from '@/lib/toast'
import type { ConversationItem } from '@/lib/chat-conversation'
import { runLogToConversation } from '@/lib/run-to-conversation'
import { CompactConversation } from '@/components/compact-conversation'
import { RichMarkdownViewer } from '@/components/rich-markdown-viewer'
// ---------------------------------------------------------------------------
// Trigger helpers (inlined; extract to shared <TriggersEditor> as a follow-up)
@ -560,9 +561,7 @@ function OutputPane({ slug, taskName, refreshKey }: { slug: string; taskName: st
) : viewSource ? (
<pre className="overflow-x-auto whitespace-pre-wrap font-mono text-[13px] leading-relaxed">{body}</pre>
) : (
<Streamdown className="prose dark:prose-invert max-w-none [&>*:first-child]:mt-0 [&>*:last-child]:mb-0">
{body}
</Streamdown>
<RichMarkdownViewer content={body} />
)}
</div>
</div>

View file

@ -0,0 +1,106 @@
import { useEffect } from 'react'
import { EditorContent, useEditor } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import Link from '@tiptap/extension-link'
import Image from '@tiptap/extension-image'
import TaskList from '@tiptap/extension-task-list'
import TaskItem from '@tiptap/extension-task-item'
import { TableKit } from '@tiptap/extension-table'
import { Markdown } from 'tiptap-markdown'
import { TaskBlockExtension } from '@/extensions/task-block'
import { PromptBlockExtension } from '@/extensions/prompt-block'
import { ImageBlockExtension } from '@/extensions/image-block'
import { EmbedBlockExtension } from '@/extensions/embed-block'
import { IframeBlockExtension } from '@/extensions/iframe-block'
import { ChartBlockExtension } from '@/extensions/chart-block'
import { TableBlockExtension } from '@/extensions/table-block'
import { CalendarBlockExtension } from '@/extensions/calendar-block'
import { EmailBlockExtension, EmailsBlockExtension } from '@/extensions/email-block'
import { TranscriptBlockExtension } from '@/extensions/transcript-block'
import { MermaidBlockExtension } from '@/extensions/mermaid-block'
import { WikiLink } from '@/extensions/wiki-link'
import '@/styles/editor.css'
const BLANK_LINE_MARKER = '\u200B'
function preprocessMarkdown(markdown: string): string {
return markdown.replace(/\n{3,}/g, (match) => {
const emptyParagraphs = match.length - 2
let result = '\n\n'
for (let i = 0; i < emptyParagraphs; i += 1) {
result += BLANK_LINE_MARKER + '\n\n'
}
return result
})
}
export function RichMarkdownViewer({ content }: { content: string }) {
const editor = useEditor({
editable: false,
extensions: [
StarterKit.configure({
heading: {
levels: [1, 2, 3],
},
}),
Link.configure({
openOnClick: true,
HTMLAttributes: {
rel: 'noopener noreferrer',
target: '_blank',
},
}),
Image.configure({
inline: false,
allowBase64: true,
HTMLAttributes: {
class: 'editor-image',
},
}),
TaskBlockExtension,
PromptBlockExtension,
ImageBlockExtension,
EmbedBlockExtension,
IframeBlockExtension,
ChartBlockExtension,
TableBlockExtension,
CalendarBlockExtension,
EmailsBlockExtension,
EmailBlockExtension,
TranscriptBlockExtension,
MermaidBlockExtension,
WikiLink,
TaskList,
TaskItem.configure({
nested: true,
}),
TableKit.configure({
table: { resizable: false },
}),
Markdown.configure({
html: true,
breaks: true,
tightLists: false,
transformCopiedText: false,
transformPastedText: false,
}),
],
content: preprocessMarkdown(content),
editorProps: {
attributes: {
class: 'prose prose-sm max-w-none focus:outline-none',
},
},
})
useEffect(() => {
if (!editor) return
editor.chain().setMeta('addToHistory', false).setContent(preprocessMarkdown(content)).run()
}, [content, editor])
return (
<div className="tiptap-editor rich-markdown-viewer">
<EditorContent editor={editor} />
</div>
)
}

View file

@ -2020,3 +2020,33 @@
.dark .tiptap-editor .ProseMirror p.is-editor-empty:first-child::before {
color: rgba(255, 255, 255, 0.3);
}
/* Read-only renderer used by surfaces that need rich blocks without editor chrome. */
.rich-markdown-viewer {
display: block;
overflow: visible;
min-height: auto;
}
.rich-markdown-viewer .ProseMirror {
max-width: none;
margin: 0;
padding: 0;
}
.rich-markdown-viewer .ProseMirror:focus {
outline: none;
}
.rich-markdown-viewer .ProseMirror .task-block-delete,
.rich-markdown-viewer .ProseMirror .image-block-delete,
.rich-markdown-viewer .ProseMirror .embed-block-delete,
.rich-markdown-viewer .ProseMirror .iframe-block-delete,
.rich-markdown-viewer .ProseMirror .chart-block-delete,
.rich-markdown-viewer .ProseMirror .table-block-delete,
.rich-markdown-viewer .ProseMirror .calendar-block-delete,
.rich-markdown-viewer .ProseMirror .email-block-delete,
.rich-markdown-viewer .ProseMirror .email-draft-block-delete,
.rich-markdown-viewer .ProseMirror .mermaid-block-delete {
display: none;
}