blob: 115ca20bd4f3a89a6c4073448566ddfd966cf4ca [file]
/**
* Markdown rendering layer — eager entry.
*
* This module is intentionally lightweight: it only owns the
* `MakaUriContext` (which the renderer installs once at the App root)
* and a thin `Markdown` wrapper that `React.lazy`-loads the heavy
* Astryx Markdown renderer from `./markdown-body.js` on first use.
*
* Why the split: the markdown pipeline is by far the heaviest thing the
* chat shell transitively imports, yet it's only needed once a message
* actually renders. On a fresh launch (no active session) nothing ever
* mounts `<Markdown>`, so forcing the browser to parse hundreds of KB
* the Markdown component before first paint was pure overhead. With the
* lazy split, that code is parsed on demand the first time a message appears,
* and cached for every subsequent render.
*
* Secret redaction happens eagerly in this wrapper so the Suspense fallback
* is safe. The remaining trust-boundary contract (URI allowlist, safe-scheme
* external gate, broken-link inline errors) lives in `markdown-body.tsx`;
* see that file for the routing rationale.
*
* PR-UI-LIB-EXTRACT-6 (WAWQAQ msg `510fef52`, round 7/10): pulled out
* of `components.tsx`. `MakaUriContext` was already a public export
* (the renderer's main.tsx provides the dispatcher), so `index.ts`
* re-exports the new module to keep the `@maka/ui` surface identical.
* `Markdown` and its rendering helpers remain package-private — only consumed
* within `@maka/ui`.
*/
import { createContext, lazy, Suspense } from 'react';
import { redactSecrets } from './redact.js';
import { isProgressiveStreamingEnabled } from './streaming-presentation.js';
// Heavy pipeline — parsed on first `<Markdown>` mount, not at app boot.
const MarkdownBody = lazy(() => import('./markdown-body.js').then((m) => ({ default: m.MarkdownBody })));
export function Markdown(props: {
text: string;
streaming?: boolean;
/** Block rhythm. Transcript turns pass `compact`; documents leave it. */
density?: 'default' | 'compact';
}) {
const safeText = redactSecrets(props.text);
const streaming = isProgressiveStreamingEnabled(props.streaming);
return (
<Suspense
// Settled history can show the safe source while the renderer loads.
// A live stream must stay behind Astryx's display cursor; showing the
// full source here would flash the unreached tail before Astryx mounts.
fallback={streaming ? null : (
<div className="maka-markdown maka-markdown-pending" style={{ whiteSpace: 'pre-wrap' }}>
{safeText}
</div>
)}
>
<MarkdownBody text={safeText} streaming={streaming} density={props.density} />
</Suspense>
);
}
/**
* PR-UI-RENDER-2 — context for the internal-link dispatcher.
*
* The desktop renderer installs the dispatcher once at the App root
* (see `apps/desktop/src/renderer/main.tsx`). The dispatcher takes a
* typed `MakaUriDest` and routes to whatever real navigation surface
* the app uses (e.g. `setNavSelection({section: 'settings', tab: ...})`
* for `kind: 'settings'`, or `composer.prefill(text)` for `kind:
* 'compose'`). The Markdown link renderer never invokes navigation
* directly — that's the dispatcher's job, and the dispatcher is the
* single chokepoint to add observability / consent prompts later.
*
* Defined here (eager) rather than in `markdown-body.tsx` (lazy) so the
* context identity is stable across the eager/lazy boundary — the
* renderer installs the provider against THIS module's export, and the
* lazy body reads it via `useContext(MakaUriContext)` imported back
* from here.
*/
export const MakaUriContext = createContext<((dest: import('./maka-uri.js').MakaUriDest) => void) | undefined>(undefined);