Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144<script lang="ts"> /** * MessageText — markdown-rendered message text for the web transcript (saga vr7kgx.11). * * The single place where user/assistant message text — and reasoning — is rendered as * markdown HTML. It isolates the `{@html}` boundary: `renderMarkdown` * (src/web/lib/markdown.ts) is the pure parse → DOMPurify-sanitize transform, so the * HTML here is already sanitized against the XSS allow-list. The live streaming caret * is appended AFTER the rendered HTML (it is a visual tail, not part of the markdown) * so it is never mangled by the parser. * * Reasoning (provider `thinking` blocks) is rendered here too, with `dim` so it keeps * the same markdown formatting as agent output while staying visually distinct (muted * palette + the FlightPlan `.thinking` left-rule treatment). Reasoning streams too, so * `streaming` is passed for fence-safety; `caret` is opt-in (reasoning shows no caret to * match its prior raw treatment — only the actively-streaming text block carries one). * * Boundary: presentational. Receives `text` + `streaming`; emits no events. */ import { renderMarkdown } from '../markdown';
export let text = ''; export let streaming = false; /** Show the live streaming caret. When unset, follows `streaming` (the text-block * default); reasoning passes `caret={false}` to keep its prior caret-less look. */ export let caret: boolean | undefined = undefined; /** Extra class for the container (e.g. "prose" lineage / readback styling hook). */ export let klass = ''; /** Dimmed "reasoning" variant — keeps markdown formatting but mutes the palette so * reasoning stays visually distinct from main agent output. */ export let dim = false;
$: rendered = renderMarkdown(text, { streaming }); // `caret` defaults to `streaming` *reactively* — a one-time prop default // (`export let caret = streaming`) would freeze at init and fail to drop the caret // when a live block finishes streaming. An explicit caret wins. $: showCaret = caret ?? streaming;</script>
<div class="prose-md {klass}" class:dim> <!-- eslint-disable-next-line svelte/no-at-html-tags -- safe: `rendered` is DOMPurify-sanitized (src/web/lib/markdown.ts, XSS suite in tests/web-markdown.vitest.ts) --> {@html rendered}{#if showCaret}<span class="caret" aria-hidden="true"></span>{/if}</div>
<style> .prose-md { max-width: 78ch; line-height: 1.6; color: var(--ink); overflow-wrap: anywhere; } /* dimmed "reasoning" variant — mutes the palette so reasoning stays visually distinct from main agent output (parity with the prior .prose.dim treatment). Prose + headings mute; code blocks keep their own inset surface so fenced code still reads clearly. */ .prose-md.dim { color: var(--ink-3); } .prose-md.dim :global(h1), .prose-md.dim :global(h2), .prose-md.dim :global(h3), .prose-md.dim :global(h4), .prose-md.dim :global(h5), .prose-md.dim :global(h6) { color: var(--ink-2); } .prose-md.dim :global(strong) { color: var(--ink-2); } .prose-md.dim :global(blockquote) { color: var(--ink-mute); } .prose-md.dim :global(a) { color: var(--accent-ink); } .prose-md.dim :global(.hljs-comment), .prose-md.dim :global(.hljs-quote) { color: var(--ink-mute); } /* block rhythm */ .prose-md :global(p) { margin: 0 0 var(--s-3); } .prose-md :global(p:last-child) { margin-bottom: 0; } .prose-md :global(h1) { margin: var(--s-4) 0 var(--s-2); font-size: 1.3em; font-weight: 600; color: var(--ink); } .prose-md :global(h2) { margin: var(--s-4) 0 var(--s-2); font-size: 1.18em; font-weight: 600; color: var(--ink); } .prose-md :global(h3) { margin: var(--s-3) 0 var(--s-2); font-size: 1.06em; font-weight: 600; color: var(--ink); } .prose-md :global(h4), .prose-md :global(h5), .prose-md :global(h6) { margin: var(--s-3) 0 var(--s-2); font-size: 1em; font-weight: 600; color: var(--ink); } .prose-md :global(ul), .prose-md :global(ol) { margin: 0 0 var(--s-3); padding-left: var(--s-5); } .prose-md :global(li) { margin: var(--s-1) 0; } .prose-md :global(li > ul), .prose-md :global(li > ol) { margin: var(--s-1) 0 0; } .prose-md :global(blockquote) { margin: 0 0 var(--s-3); padding: 0 0 0 var(--s-3); border-left: 2px solid var(--hairline-faint); color: var(--ink-3); } .prose-md :global(blockquote > p:last-child) { margin-bottom: 0; } .prose-md :global(hr) { border: 0; border-top: 1px solid var(--hairline); margin: var(--s-4) 0; } .prose-md :global(a) { color: var(--accent-ink); text-decoration: underline; text-underline-offset: 2px; } .prose-md :global(a:hover) { color: var(--accent); } /* inline code */ .prose-md :global(code) { font-family: var(--font-mono); font-size: 0.9em; background: var(--surface-inset); border-radius: var(--r-xs); padding: 0.1em 0.35em; } /* code blocks */ .prose-md :global(pre) { margin: 0 0 var(--s-3); padding: var(--s-3); background: var(--surface-inset); border: 1px solid var(--hairline-faint); border-radius: var(--r-sm); overflow-x: auto; max-width: 100%; } .prose-md :global(pre > code) { background: none; border: 0; padding: 0; font-size: 0.86em; line-height: 1.5; white-space: pre; color: var(--ink); } /* highlight.js token colors — retinted to the token system so both scenes read correctly */ .prose-md :global(.hljs-keyword), .prose-md :global(.hljs-selector-tag), .prose-md :global(.hljs-built_in) { color: var(--accent-ink); } .prose-md :global(.hljs-string), .prose-md :global(.hljs-attr), .prose-md :global(.hljs-meta-string) { color: var(--edge-clear); } .prose-md :global(.hljs-number), .prose-md :global(.hljs-literal) { color: var(--edge-hold); } .prose-md :global(.hljs-comment), .prose-md :global(.hljs-quote) { color: var(--ink-3); font-style: italic; } .prose-md :global(.hljs-title), .prose-md :global(.hljs-section), .prose-md :global(.hljs-function) { color: var(--ink); font-weight: 600; } .prose-md :global(.hljs-type), .prose-md :global(.hljs-class) { color: var(--accent); } .prose-md :global(.hljs-variable), .prose-md :global(.hljs-attribute) { color: var(--ink-2); } /* tables */ .prose-md :global(table) { border-collapse: collapse; margin: 0 0 var(--s-3); max-width: 100%; font-size: 0.92em; } .prose-md :global(th), .prose-md :global(td) { border: 1px solid var(--hairline); padding: var(--s-1) var(--s-2); text-align: left; } .prose-md :global(th) { background: var(--surface-hover); font-weight: 600; } .prose-md :global(td) { font-family: var(--font-mono); color: var(--ink-2); } .prose-md :global(td:first-child) { font-family: inherit; color: var(--ink); } /* GFM task-list checkboxes — inert */ .prose-md :global(input[type="checkbox"]) { margin-right: var(--s-1); accent-color: var(--accent); } /* the live caret (parity with FlightPlan's rolling caret) */ .prose-md .caret { display: inline-block; width: 7px; height: 1.05em; vertical-align: text-bottom; background: var(--caret); margin-left: 2px; animation: caret-blink 1s steps(2, start) infinite; } @keyframes caret-blink { 50% { opacity: 0; } } @media (prefers-reduced-motion: reduce) { .caret { animation: none; } }</style>