Multi-agent audit + aggressive cleanup across server/web/coder/booterm, delivered behind a DEFER discipline so none of the in-flight files were touched. Removes dead code/deps/columns, dedups server + coder helpers, and splits the oversized modules (tools.ts, opencode-server.ts, sentinel-summaries, turn.ts, TerminalPane.tsx) behind stable contracts. Adds 78 parity/unit tests (server 587, coder 323); fixes two latent bugs (ChatPane queue keys, FileViewerOverlay blank-line parity). Intended tag: v2.7.12-audit-cleanup.
92 lines
3.3 KiB
TypeScript
92 lines
3.3 KiB
TypeScript
// v1.14.x-html-artifact-panes: dedicated full-height Markdown viewer used
|
|
// when a user clicks "Open in pane" on an assistant message that has NO
|
|
// html_artifact part. Header carries Copy (raw source) + Download (server-
|
|
// materialised .md under <projectRoot>/.boocode/artifacts/) + close.
|
|
//
|
|
// Pane state is a reference only (chat_id + message_id + title); the markdown
|
|
// body is fetched on mount from GET /api/chats/:chat_id/messages by locating
|
|
// the matching message_id. This keeps sessions.workspace_panes jsonb small
|
|
// and the assistant message row remains the single source of truth.
|
|
import { useEffect, useState } from 'react';
|
|
import { toast } from 'sonner';
|
|
import { api } from '@/api/client';
|
|
import type { MarkdownArtifactState } from '@/api/types';
|
|
import { MarkdownRenderer } from './MarkdownRenderer';
|
|
import { ArtifactPaneHeader } from './ArtifactPaneHeader';
|
|
import { useArtifactDownload } from '@/hooks/useArtifactDownload';
|
|
|
|
interface Props {
|
|
chatId: string;
|
|
state: MarkdownArtifactState;
|
|
onClose: () => void;
|
|
}
|
|
|
|
export function MarkdownArtifactPane({ chatId, state, onClose }: Props) {
|
|
const [justCopied, setJustCopied] = useState(false);
|
|
const [content, setContent] = useState<string | null>(null);
|
|
const [loadError, setLoadError] = useState<string | null>(null);
|
|
const { downloading, download } = useArtifactDownload(chatId, state.message_id, 'md');
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
setContent(null);
|
|
setLoadError(null);
|
|
void (async () => {
|
|
try {
|
|
// No single-message GET endpoint exists; the chat-messages list is
|
|
// already cached server-side and the lookup is O(n) over a small
|
|
// window. Cheaper than adding a new route for one call site.
|
|
const messages = await api.chats.messages(chatId);
|
|
if (cancelled) return;
|
|
const msg = messages.find((m) => m.id === state.message_id);
|
|
if (!msg) {
|
|
setLoadError('Message not found');
|
|
return;
|
|
}
|
|
setContent(msg.content ?? '');
|
|
} catch (err) {
|
|
if (cancelled) return;
|
|
setLoadError(err instanceof Error ? err.message : 'failed to load message');
|
|
}
|
|
})();
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [chatId, state.message_id]);
|
|
|
|
async function copy() {
|
|
if (content === null) return;
|
|
try {
|
|
await navigator.clipboard.writeText(content);
|
|
setJustCopied(true);
|
|
setTimeout(() => setJustCopied(false), 1200);
|
|
} catch (err) {
|
|
toast.error(err instanceof Error ? err.message : 'copy failed');
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="flex flex-col h-full min-h-0">
|
|
<ArtifactPaneHeader
|
|
title={state.title}
|
|
defaultTitle="Markdown artifact"
|
|
onDownload={() => void download()}
|
|
downloadDisabled={downloading || content === null}
|
|
onClose={onClose}
|
|
onCopy={() => void copy()}
|
|
justCopied={justCopied}
|
|
copyDisabled={content === null}
|
|
/>
|
|
<div className="flex-1 min-h-0 overflow-auto px-4 py-3 text-sm">
|
|
{loadError ? (
|
|
<div className="text-destructive">Failed to load: {loadError}</div>
|
|
) : content === null ? (
|
|
<div className="text-muted-foreground">Loading…</div>
|
|
) : (
|
|
<MarkdownRenderer content={content} />
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|