diff --git a/apps/x/apps/renderer/src/components/bg-tasks-view.tsx b/apps/x/apps/renderer/src/components/bg-tasks-view.tsx index bb972eff..3ef3e3ab 100644 --- a/apps/x/apps/renderer/src/components/bg-tasks-view.tsx +++ b/apps/x/apps/renderer/src/components/bg-tasks-view.tsx @@ -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 as a follow-up) @@ -560,9 +561,7 @@ function OutputPane({ slug, taskName, refreshKey }: { slug: string; taskName: st ) : viewSource ? (
{body}
) : ( - - {body} - + )} diff --git a/apps/x/apps/renderer/src/components/rich-markdown-viewer.tsx b/apps/x/apps/renderer/src/components/rich-markdown-viewer.tsx new file mode 100644 index 00000000..da6342fe --- /dev/null +++ b/apps/x/apps/renderer/src/components/rich-markdown-viewer.tsx @@ -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 ( +
+ +
+ ) +} diff --git a/apps/x/apps/renderer/src/styles/editor.css b/apps/x/apps/renderer/src/styles/editor.css index 3d270bb2..dbf9f067 100644 --- a/apps/x/apps/renderer/src/styles/editor.css +++ b/apps/x/apps/renderer/src/styles/editor.css @@ -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; +} diff --git a/apps/x/packages/core/src/application/lib/knowledge-note-style.ts b/apps/x/packages/core/src/application/lib/knowledge-note-style.ts index a70e4186..8b1d566b 100644 --- a/apps/x/packages/core/src/application/lib/knowledge-note-style.ts +++ b/apps/x/packages/core/src/application/lib/knowledge-note-style.ts @@ -1,9 +1,9 @@ /** * The canonical writing style for content written into the user's knowledge - * base. Imported by both the `doc-collab` skill (so Copilot picks it up on - * note edits) and the live-note run-agent prompt (so background runs use the - * same rules without having to load the skill on every fire). One source of - * truth, two consumers. + * base. Imported by the `doc-collab` skill (so Copilot picks it up on note + * edits), the live-note run-agent prompt, and the background-task run-agent + * prompt (so background runs use the same rules without having to load the + * skill on every fire). * * If you change this guide, restart the dev server / rebuild — both consumers * inline it at module load. @@ -113,6 +113,186 @@ Common note types and the target shape for each: - **GitHub / issue digests**: \`- [](<issue_url>) · <repo> · <state> · <updated>\`. - **Tweets / social digests**: \`- [<truncated text or topic>](<post_url>) · @<author> · <when>\`. +## Rich Markdown block formats + +The renderer turns specially-tagged fenced code blocks into styled UI: tables, charts, calendars, emails, embeds, and more. Reach for these when the data has structure that benefits from a visual treatment; stay with plain markdown when prose, a markdown table, or bullets carry the meaning just as well. Pick **at most one block per output region** unless the user asks for a multi-section layout — and follow the exact fence language and shape, since anything unparseable renders as a small "Invalid X block" error card. + +Do **not** emit \`task\` blocks — those are user-authored input mechanisms, not agent outputs. + +### \`table\` — tabular data (JSON) + +Use for: scoreboards, leaderboards, comparisons, multi-row status digests. + +\`\`\`table +{ + "title": "Top stories on Hacker News", + "columns": ["Rank", "Title", "Points", "Comments"], + "data": [ + {"Rank": 1, "Title": "Show HN: ...", "Points": 842, "Comments": 312}, + {"Rank": 2, "Title": "...", "Points": 530, "Comments": 144} + ] +} +\`\`\` + +Required: \`columns\` (string[]), \`data\` (array of objects keyed by column name). Optional: \`title\`. + +### \`chart\` — line / bar / pie chart (JSON) + +Use for: time series, categorical breakdowns, share-of-total. Skip if a single sentence carries the meaning. + +\`\`\`chart +{ + "chart": "line", + "title": "USD/INR — last 7 days", + "x": "date", + "y": "rate", + "data": [ + {"date": "2026-04-13", "rate": 83.41}, + {"date": "2026-04-14", "rate": 83.38} + ] +} +\`\`\` + +Required: \`chart\` ("line" | "bar" | "pie"), \`x\` (field name on each row), \`y\` (field name on each row), and **either** \`data\` (inline array of objects) **or** \`source\` (workspace path to a JSON-array file). Optional: \`title\`. + +### \`mermaid\` — diagrams (raw Mermaid source) + +Use for: relationship maps, flowcharts, sequence diagrams, gantt charts, mind maps. + +\`\`\`mermaid +graph LR + A[Project Alpha] --> B[Sarah Chen] + A --> C[Acme Corp] + B --> D[Q3 Launch] +\`\`\` + +Body is plain Mermaid source — no JSON wrapper. + +### \`calendar\` — list of events (JSON) + +Use for: upcoming meetings, agenda digests, day/week views. + +\`\`\`calendar +{ + "title": "Today", + "events": [ + { + "summary": "1:1 with Sarah", + "start": {"dateTime": "2026-04-20T10:00:00-07:00"}, + "end": {"dateTime": "2026-04-20T10:30:00-07:00"}, + "location": "Zoom", + "conferenceLink": "https://zoom.us/j/..." + } + ] +} +\`\`\` + +Required: \`events\` (array). Each event optionally has \`summary\`, \`start\`/\`end\` (object with \`dateTime\` ISO string OR \`date\` "YYYY-MM-DD" for all-day), \`location\`, \`htmlLink\`, \`conferenceLink\`, \`source\`. Optional top-level: \`title\`, \`showJoinButton\` (bool). + +### \`emails\` — multi-thread email digest (JSON) + +Use for: surfacing a compact inbox-style digest of several relevant threads. + +\`\`\`emails +{ + "title": "Q3 planning threads", + "emails": [ + { + "subject": "Q3 launch readiness", + "from": "sarah@acme.com", + "date": "2026-04-19T16:42:00Z", + "summary": "Sarah confirms timeline; flagged blocker on infra capacity.", + "latest_email": "Hey — quick update on Q3...\\n\\nThanks,\\nSarah" + } + ] +} +\`\`\` + +Required: \`emails\` (array of \`email\` objects). Optional top-level: \`title\`. + +### \`email\` — single email or thread digest (JSON) + +Use for: surfacing one important thread — latest message body, summary of prior context, optional draft reply. + +\`\`\`email +{ + "subject": "Q3 launch readiness", + "from": "sarah@acme.com", + "date": "2026-04-19T16:42:00Z", + "summary": "Sarah confirms timeline; flagged blocker on infra capacity.", + "latest_email": "Hey — quick update on Q3...\\n\\nThanks,\\nSarah" +} +\`\`\` + +Required: \`latest_email\` (string). Optional: \`threadId\`, \`summary\`, \`subject\`, \`from\`, \`to\`, \`date\`, \`past_summary\`, \`draft_response\`, \`response_mode\` ("inline" | "assistant" | "both"). + +For digests of **many** threads, prefer an \`emails\` block or a compact markdown table — \`email\` is for one thread at a time. + +### \`image\` — single image (JSON) + +Use for: charts, screenshots, photos you have a URL or workspace path for. + +\`\`\`image +{ + "src": "https://example.com/forecast.png", + "alt": "Weather forecast", + "caption": "Bay Area · April 20" +} +\`\`\` + +Required: \`src\` (URL or workspace path). Optional: \`alt\`, \`caption\`. + +### \`embed\` — YouTube / Figma / Tweet embed (JSON) + +Use for: linking to a video, design, or tweet that should render inline. + +\`\`\`embed +{ + "provider": "youtube", + "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", + "caption": "Latest demo" +} +\`\`\` + +Required: \`provider\` ("youtube" | "figma" | "tweet" | "generic"), \`url\`. Optional: \`caption\`. + +### \`iframe\` — arbitrary embedded webpage (JSON) + +Use for: live dashboards, status pages, trackers — anything that has its own webpage and benefits from being live, not snapshotted. + +\`\`\`iframe +{ + "url": "https://status.example.com", + "title": "Service status", + "height": 600 +} +\`\`\` + +Required: \`url\` (must be \`https://\` or \`http://localhost\`). Optional: \`title\`, \`caption\`, \`height\` (240–1600), \`allow\` (Permissions-Policy string). + +### \`transcript\` — long transcript (JSON) + +Use for: meeting transcripts, voice-note dumps — bodies that benefit from a collapsible UI. + +\`\`\`transcript +{"transcript": "[00:00] Speaker A: Welcome everyone..."} +\`\`\` + +Required: \`transcript\` (string). + +### \`prompt\` — starter Copilot prompt (YAML) + +Use for: end-of-output "next step" cards. The user clicks **Run** and the chat sidebar opens with the underlying instruction submitted to Copilot. + +\`\`\`prompt +label: Draft replies to today's emails +instruction: | + For each unanswered email in the digest above, draft a 2-line reply + in my voice and present them as a checklist for me to approve. +\`\`\` + +Required: \`label\` (short title shown on the card), \`instruction\` (the longer prompt). Note: this block uses **YAML**, not JSON. + ## When prose IS appropriate - A **1-3 sentence opening summary** at the top of a complex note (a "lede") — concise enough to scan. diff --git a/apps/x/packages/core/src/knowledge/live-note/agent.ts b/apps/x/packages/core/src/knowledge/live-note/agent.ts index 66cacbc3..f8d23b3f 100644 --- a/apps/x/packages/core/src/knowledge/live-note/agent.ts +++ b/apps/x/packages/core/src/knowledge/live-note/agent.ts @@ -82,165 +82,6 @@ ${KNOWLEDGE_NOTE_STYLE_GUIDE} The style guide above is the canonical writing style for everything you emit into the body. The objective may specify a particular shape ("3-column markdown table: Location | Local Time | Offset") — when it does, follow it exactly. When it doesn't, walk the ladder above and pick the tightest shape that fits the data. -# Output Block Types - -The note renderer turns specially-tagged fenced code blocks into styled UI: tables, charts, calendars, embeds, and more. Reach for these when the data has structure that benefits from a visual treatment; stay with plain markdown when prose, a markdown table, or bullets carry the meaning just as well. Pick **at most one block per output region** unless the objective asks for a multi-section layout — and follow the exact fence language and shape, since anything unparseable renders as a small "Invalid X block" error card. - -Do **not** emit \`task\` blocks — those are user-authored input mechanisms, not agent outputs. - -## \`table\` — tabular data (JSON) - -Use for: scoreboards, leaderboards, comparisons, multi-row status digests. - -\`\`\`table -{ - "title": "Top stories on Hacker News", - "columns": ["Rank", "Title", "Points", "Comments"], - "data": [ - {"Rank": 1, "Title": "Show HN: ...", "Points": 842, "Comments": 312}, - {"Rank": 2, "Title": "...", "Points": 530, "Comments": 144} - ] -} -\`\`\` - -Required: \`columns\` (string[]), \`data\` (array of objects keyed by column name). Optional: \`title\`. - -## \`chart\` — line / bar / pie chart (JSON) - -Use for: time series, categorical breakdowns, share-of-total. Skip if a single sentence carries the meaning. - -\`\`\`chart -{ - "chart": "line", - "title": "USD/INR — last 7 days", - "x": "date", - "y": "rate", - "data": [ - {"date": "2026-04-13", "rate": 83.41}, - {"date": "2026-04-14", "rate": 83.38} - ] -} -\`\`\` - -Required: \`chart\` ("line" | "bar" | "pie"), \`x\` (field name on each row), \`y\` (field name on each row), and **either** \`data\` (inline array of objects) **or** \`source\` (workspace path to a JSON-array file). Optional: \`title\`. - -## \`mermaid\` — diagrams (raw Mermaid source) - -Use for: relationship maps, flowcharts, sequence diagrams, gantt charts, mind maps. - -\`\`\`mermaid -graph LR - A[Project Alpha] --> B[Sarah Chen] - A --> C[Acme Corp] - B --> D[Q3 Launch] -\`\`\` - -Body is plain Mermaid source — no JSON wrapper. - -## \`calendar\` — list of events (JSON) - -Use for: upcoming meetings, agenda digests, day/week views. - -\`\`\`calendar -{ - "title": "Today", - "events": [ - { - "summary": "1:1 with Sarah", - "start": {"dateTime": "2026-04-20T10:00:00-07:00"}, - "end": {"dateTime": "2026-04-20T10:30:00-07:00"}, - "location": "Zoom", - "conferenceLink": "https://zoom.us/j/..." - } - ] -} -\`\`\` - -Required: \`events\` (array). Each event optionally has \`summary\`, \`start\`/\`end\` (object with \`dateTime\` ISO string OR \`date\` "YYYY-MM-DD" for all-day), \`location\`, \`htmlLink\`, \`conferenceLink\`, \`source\`. Optional top-level: \`title\`, \`showJoinButton\` (bool). - -## \`email\` — single email or thread digest (JSON) - -Use for: surfacing one important thread — latest message body, summary of prior context, optional draft reply. - -\`\`\`email -{ - "subject": "Q3 launch readiness", - "from": "sarah@acme.com", - "date": "2026-04-19T16:42:00Z", - "summary": "Sarah confirms timeline; flagged blocker on infra capacity.", - "latest_email": "Hey — quick update on Q3...\\n\\nThanks,\\nSarah" -} -\`\`\` - -Required: \`latest_email\` (string). Optional: \`threadId\`, \`summary\`, \`subject\`, \`from\`, \`to\`, \`date\`, \`past_summary\`, \`draft_response\`, \`response_mode\` ("inline" | "assistant" | "both"). - -For digests of **many** threads, prefer a \`table\` (Subject | From | Snippet) — \`email\` is for one thread at a time. - -## \`image\` — single image (JSON) - -Use for: charts, screenshots, photos you have a URL or workspace path for. - -\`\`\`image -{ - "src": "https://example.com/forecast.png", - "alt": "Weather forecast", - "caption": "Bay Area · April 20" -} -\`\`\` - -Required: \`src\` (URL or workspace path). Optional: \`alt\`, \`caption\`. - -## \`embed\` — YouTube / Figma / Tweet embed (JSON) - -Use for: linking to a video, design, or tweet that should render inline. - -\`\`\`embed -{ - "provider": "youtube", - "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", - "caption": "Latest demo" -} -\`\`\` - -Required: \`provider\` ("youtube" | "figma" | "tweet" | "generic"), \`url\`. Optional: \`caption\`. - -## \`iframe\` — arbitrary embedded webpage (JSON) - -Use for: live dashboards, status pages, trackers — anything that has its own webpage and benefits from being live, not snapshotted. - -\`\`\`iframe -{ - "url": "https://status.example.com", - "title": "Service status", - "height": 600 -} -\`\`\` - -Required: \`url\` (must be \`https://\` or \`http://localhost\`). Optional: \`title\`, \`caption\`, \`height\` (240–1600), \`allow\` (Permissions-Policy string). - -## \`transcript\` — long transcript (JSON) - -Use for: meeting transcripts, voice-note dumps — bodies that benefit from a collapsible UI. - -\`\`\`transcript -{"transcript": "[00:00] Speaker A: Welcome everyone..."} -\`\`\` - -Required: \`transcript\` (string). - -## \`prompt\` — starter Copilot prompt (YAML) - -Use for: end-of-output "next step" cards. The user clicks **Run** and the chat sidebar opens with the underlying instruction submitted to Copilot, with this note attached as a file mention. - -\`\`\`prompt -label: Draft replies to today's emails -instruction: | - For each unanswered email in the digest above, draft a 2-line reply - in my voice and present them as a checklist for me to approve. -\`\`\` - -Required: \`label\` (short title shown on the card), \`instruction\` (the longer prompt). Note: this block uses **YAML**, not JSON. - # Interpreting the Objective The objective was authored in a prior conversation you cannot see. Treat it as a **self-contained spec**. If ambiguous, pick what a reasonable user of a knowledge tracker would expect: