Deletes the vanilla shell and mounts App.tsx on #root. main.ts and the 23 views it drove are gone; App.tsx composes the ported components — sidebar, multi-view, command bar, spawn modal, help and restart overlays, daemon link, drawer — around the catalog and layout stores. The pure parts of four deleted modules moved next to the components that still use them: the spawn-form model and spawnSimilar into components/spawn, the transcript's markdown/format helpers and the diff into components/structured, the help overlay's grouping into components/dialogs. Document-level event names now live in uiEvents.ts. Deduplicated two pairs the migration left side by side: the sidebar uses dialogs/KillDialog and common/ContextMenu. It also listens for the command bar's kill/rename events, which nothing had picked up since catalog.ts went away. styles.css stays as the token and base layer, with `#app` renamed to `#root` and the pane-body rules re-keyed off the classes the React panes actually carry. Closes #253
53 lines
2.2 KiB
TypeScript
53 lines
2.2 KiB
TypeScript
// Pure text helpers for the structured transcript, kept when
|
|
// structuredRender.ts's imperative DOM builders were deleted (#253):
|
|
// markdown rendering (sanitized) and the elapsed/context formatters.
|
|
|
|
import { marked, Renderer } from "marked";
|
|
|
|
import { escapeHtml } from "../../html.js";
|
|
|
|
/** Small inline renderer retained for compact labels and focused tests. */
|
|
export function renderInlineMarkdown(text: string): string {
|
|
const escaped = escapeHtml(text);
|
|
return escaped
|
|
.replace(/`([^`]+)`/g, "<code>$1</code>")
|
|
.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
|
|
.replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, "<em>$1</em>");
|
|
}
|
|
|
|
/** Render assistant output as GFM, then sanitize the complete result before it
|
|
* enters the DOM. This gives transcripts proper tables, lists, code blocks and
|
|
* links without trusting model-authored HTML or URL attributes. */
|
|
export function renderMarkdown(text: string): string {
|
|
const renderer = new Renderer();
|
|
renderer.html = ({ text: raw }) => escapeHtml(raw);
|
|
renderer.image = ({ text: alt }) => escapeHtml(alt ? `[image: ${alt}]` : "[image]");
|
|
renderer.link = ({ href, title, tokens }) => {
|
|
const label = renderer.parser.parseInline(tokens);
|
|
const scheme = /^([a-z][a-z\d+.-]*):/i.exec(href.trim())?.[1]?.toLowerCase();
|
|
if (scheme && !["http", "https", "mailto"].includes(scheme)) return label;
|
|
const safeTitle = title ? ` title="${escapeHtml(title)}"` : "";
|
|
return `<a href="${escapeHtml(href)}"${safeTitle}>${label}</a>`;
|
|
};
|
|
|
|
return marked.parse(text, { async: false, gfm: true, renderer });
|
|
}
|
|
|
|
function compactNumber(value: number): string {
|
|
if (Math.abs(value) < 1_000) return String(value);
|
|
if (Math.abs(value) < 1_000_000) return `${(value / 1_000).toFixed(value % 1_000 === 0 ? 0 : 1)}k`;
|
|
return `${(value / 1_000_000).toFixed(value % 1_000_000 === 0 ? 0 : 1)}m`;
|
|
}
|
|
|
|
export function formatElapsed(milliseconds: number): string {
|
|
const seconds = Math.max(0, Math.floor(milliseconds / 1_000));
|
|
if (seconds < 60) return `${seconds}s`;
|
|
const minutes = Math.floor(seconds / 60);
|
|
const remainder = seconds % 60;
|
|
return remainder === 0 ? `${minutes}m` : `${minutes}m ${remainder}s`;
|
|
}
|
|
|
|
export function formatContextTokens(value: number): string {
|
|
return `${compactNumber(value)} context`;
|
|
}
|