v1.13.19-html-artifact-panes: pane-based artifact viewer with on-request HTML
Every assistant message gets an "Open in pane" affordance that opens the
message in the workspace splitter — Markdown pane (Copy + Download .md) by
default; HTML pane (Download .html only) when the model emits a self-contained
<!DOCTYPE html> or fenced ```html artifact. BOOCHAT.md rule keeps Markdown
default at every length; HTML opt-in on explicit user request.
Backend: services/artifacts.ts (slug derivation + write helpers with
symlink-escape guard via realpath-after-mkdir), routes/artifacts.ts (POST
download + GET stream with nosniff + CSP sandbox defense-in-depth), HTML
detection in finalizeCompletion writing a new message_parts.kind='html_artifact'
row (schema CHECK extended via v1.13.13 pattern), graceful 1MB cap via the
pure decideHtmlArtifactWrite helper. PartKind union extended.
Frontend: MarkdownRenderer.tsx extracted from MessageBubble's inline
MarkdownBody for reuse; MarkdownArtifactPane.tsx + HtmlArtifactPane.tsx with
loading/error states; pane state is reference-only ({chat_id, message_id,
title}) — content fetched on mount to keep workspace_panes jsonb small and
avoid 1MB blobs riding session_workspace_updated frames. iframe sandbox
locked to allow-scripts allow-clipboard-write allow-downloads with no
allow-same-origin, srcDoc not src. openInPane discriminates 404 (expected
fallback) from real errors (toast + bail). PanelRightOpen icon button with
mobile 44px tap-target.
31 new server unit tests including a real-symlink filesystem case; 332/332
server tests passing, tsc clean both sides, pnpm -C apps/web build green.
Smoke deferred to first deploy.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -276,6 +276,24 @@ export const api = {
|
||||
request<void>(`/api/chats/${chatId}/messages/${messageId}`, {
|
||||
method: 'DELETE',
|
||||
}),
|
||||
// v1.14.x-html-artifact-panes: write the artifact to
|
||||
// <projectRoot>/.boocode/artifacts/<slug>-<ts>.<ext> and return the
|
||||
// path + a /api/projects/.../artifacts/<filename> URL the browser can
|
||||
// GET to download. fmt=html requires the assistant message to carry an
|
||||
// html_artifact part (404 otherwise).
|
||||
downloadArtifact: (chatId: string, messageId: string, fmt: 'md' | 'html') =>
|
||||
request<{ path: string; url: string }>(
|
||||
`/api/chats/${chatId}/messages/${messageId}/artifacts/download?fmt=${fmt}`,
|
||||
{ method: 'POST' },
|
||||
),
|
||||
// v1.14.x-html-artifact-panes: fetch the html_artifact part payload so
|
||||
// HtmlArtifactPane can render the iframe srcdoc. 404 = no html_artifact
|
||||
// part on this message; MessageBubble uses that as a signal to fall back
|
||||
// to the markdown pane variant.
|
||||
getHtmlArtifact: (chatId: string, messageId: string) =>
|
||||
request<{ html_content: string; char_count: number; title: string }>(
|
||||
`/api/chats/${chatId}/messages/${messageId}/html_artifact`,
|
||||
),
|
||||
},
|
||||
|
||||
models: () => request<ModelInfo[]>('/api/models'),
|
||||
|
||||
@@ -316,7 +316,37 @@ export interface AskUserAnswerSet {
|
||||
// v1.9: 'settings' is an ephemeral pane kind — never persisted, always
|
||||
// singleton per workspace. The pane hook filters it out before writing to
|
||||
// localStorage and dedupes on insertion via toggleSettingsPane().
|
||||
export type WorkspacePaneKind = 'chat' | 'terminal' | 'agent' | 'empty' | 'settings';
|
||||
// v1.14.x-html-artifact-panes: 'markdown_artifact' + 'html_artifact' added.
|
||||
// Both carry payload state on the WorkspacePane row itself so
|
||||
// useWorkspacePanes's JSON-string dedup + persisted jsonb stay self-contained
|
||||
// — no extra fetch on rehydrate.
|
||||
export type WorkspacePaneKind =
|
||||
| 'chat'
|
||||
| 'terminal'
|
||||
| 'agent'
|
||||
| 'empty'
|
||||
| 'settings'
|
||||
| 'markdown_artifact'
|
||||
| 'html_artifact';
|
||||
|
||||
// v1.14.x: per-pane artifact payloads. Optional + namespaced so older saved
|
||||
// pane rows (without these fields) deserialize unchanged.
|
||||
// v1.14.x: pane state is a reference only — the pane component fetches the
|
||||
// actual content on mount. This keeps sessions.workspace_panes jsonb small and
|
||||
// makes the message body / html_artifact part the single source of truth.
|
||||
export interface MarkdownArtifactState {
|
||||
// chat_id is needed for the download endpoint
|
||||
// (POST /api/chats/:chat_id/messages/:msg_id/artifacts/download).
|
||||
chat_id: string;
|
||||
message_id: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface HtmlArtifactState {
|
||||
chat_id: string;
|
||||
message_id: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface WorkspacePane {
|
||||
id: string;
|
||||
@@ -324,6 +354,9 @@ export interface WorkspacePane {
|
||||
chatId?: string;
|
||||
chatIds: string[];
|
||||
activeChatIdx: number;
|
||||
// v1.14.x: populated only when kind === 'markdown_artifact' / 'html_artifact'.
|
||||
markdown_artifact_state?: MarkdownArtifactState;
|
||||
html_artifact_state?: HtmlArtifactState;
|
||||
}
|
||||
|
||||
export type WsFrame =
|
||||
|
||||
Reference in New Issue
Block a user