# v1.14.x-html-artifact-panes tasks
## B1 — Backups
- [ ] `apps/server/src/schema.sql.bak-v1.14.x-html-`
- [ ] `apps/server/src/services/inference/stream-phase.ts.bak-v1.14.x-html-`
- [ ] `apps/server/src/services/inference/parts.ts.bak-v1.14.x-html-`
- [ ] `apps/server/src/routes/messages.ts.bak-v1.14.x-html-`
- [ ] `apps/web/src/components/MessageBubble.tsx.bak-v1.14.x-html-`
- [ ] `apps/web/src/api/client.ts.bak-v1.14.x-html-`
- [ ] `apps/web/src/api/types.ts.bak-v1.14.x-html-`
- [ ] `data/AGENTS.md.bak-v1.14.x-html-`
## B2 — Recon (STOP after this step)
- [ ] Read existing `Pane` discriminated union and locate the workspace splitter component
- [ ] Read `MessageBubble.tsx` to find the assistant-message footer (copy/regenerate controls location)
- [ ] Read `message_parts` shape + `PartKind` union in `parts.ts`
- [ ] Read `stream-phase.ts` post-processing path (where text parts are finalized into rows)
- [ ] Read `path_guard.ts` to confirm write semantics for `/opt//.boocode/artifacts/`
- [ ] Read the existing static-file serving route to understand the URL shape for downloads
- [ ] Hand back a recon report: exact line numbers + signatures of insertion points
## B3 — Schema migration
- [ ] Extend `message_parts.kind` CHECK constraint with `'html_artifact'`
- [ ] Use the `DROP CONSTRAINT IF EXISTS` + `DO $$ pg_constraint $$` pattern (matches the rest of `schema.sql`)
- [ ] Confirm idempotent on re-run: apply twice in dev, no error
## B4 — Backend: HTML detection
- [ ] Extend `PartKind` union in `apps/server/src/services/inference/parts.ts` with `'html_artifact'`
- [ ] In `stream-phase.ts` post-processing: detect text parts starting with `` (case-insensitive, trimmed) OR wrapped in fenced ` ```html ` block
- [ ] Title resolution helper: `` tag → first `` text → first 80 chars of inner text
- [ ] Write the `html_artifact` part with payload `{html_content, char_count, title}` via the existing `insertParts` helper
- [ ] 1MB cap check before write: abort stream with friendly error if exceeded
- [ ] Detection is opportunistic — does NOT replace the text part, just adds a sibling `html_artifact` part
## B5 — Backend: artifacts service
- [ ] NEW `apps/server/src/services/artifacts.ts`
- [ ] `deriveMarkdownSlug(messageContent: string): string` — first `# ` heading → first 6 words → lowercase + hyphenate
- [ ] `deriveHtmlSlug(payload: HtmlArtifactPayload): string` — `` → first `` → first 6 words of inner text → lowercase + hyphenate
- [ ] `writeMarkdownArtifact(message, projectRoot): Promise<{path, url}>` — slug + timestamp + write to `/.boocode/artifacts/`
- [ ] `writeHtmlArtifact(part, projectRoot): Promise<{path, url}>` — same shape
- [ ] Path-guard both writes via existing helpers
- [ ] Ensure `/.boocode/artifacts/` exists (mkdir recursive)
## B6 — Backend: download endpoint
- [ ] NEW endpoint registration: `POST /api/chats/:id/messages/:msg_id/artifacts/download?fmt=md|html`
- [ ] Fastify route in `apps/server/src/routes/messages.ts` (or new `artifacts.ts` route file — decide during impl)
- [ ] Zod schema on `?fmt=` query param
- [ ] Resolve message + (for HTML) the `html_artifact` part
- [ ] Call `writeMarkdownArtifact` or `writeHtmlArtifact` per `fmt`
- [ ] Return `{path, url}`
- [ ] Error path: 404 if `fmt=html` requested but no html_artifact part exists
## B7 — Backend: STOP checkpoint after B3–B6
- [ ] `npx tsc --noEmit -p apps/server` — 0 errors
- [ ] Smoke download endpoint via `curl http://100.114.205.53:9500/api/chats//messages//artifacts/download?fmt=md` against a real message
- [ ] Hand back diff + curl output
## B8 — Frontend: Pane discriminated union extension
- [ ] Extend `Pane` discriminated union with two variants:
- `{ kind: 'markdown_artifact', message_id: string }`
- `{ kind: 'html_artifact', message_id: string, html_content: string, title: string }`
- [ ] Update `validatePanes` to handle the new variants (no-op if message_id still exists)
- [ ] Mirror types in `apps/web/src/api/types.ts` (`MessagePart` discriminator + new pane variants)
## B9 — Frontend: pane components
- [ ] NEW `apps/web/src/components/MarkdownArtifactPane.tsx`
- Header: title + Copy button (raw source via `navigator.clipboard.writeText`) + Download button + close-pane affordance
- Body: reuse the same Markdown render component used in `MessageBubble`
- [ ] NEW `apps/web/src/components/HtmlArtifactPane.tsx`
- Header: title + Download button + close-pane affordance (NO Copy)
- Body: ``
- [ ] Wire both into the workspace splitter's pane-type registry
## B10 — Frontend: MessageBubble affordance
- [ ] Add "Open in pane" icon button to assistant message footer (next to existing copy/regenerate controls)
- [ ] On click: dispatch workspace-pane action
- If message has `html_artifact` part → open as html_artifact pane (with title + html_content from the part)
- Else → open as markdown_artifact pane
- [ ] Mobile tap target: `max-md:min-h-[44px] max-md:min-w-[44px]`
## B11 — Frontend: API client
- [ ] `api.messages.downloadArtifact(chatId, msgId, fmt: 'md' | 'html')` → POST to the new endpoint
- [ ] Returns `{path, url}` — Copy button uses raw text from the message; Download button uses the returned URL
## B12 — Frontend: STOP checkpoint after B8–B11
- [ ] `npx tsc -p apps/web/tsconfig.app.json --noEmit` — 0 errors (root tsc may miss web errors per CLAUDE.md)
- [ ] `pnpm -C apps/web build` succeeds (including the `U+2500-259F` guard)
- [ ] Hand back diff
## B13 — AGENTS.md guidance
- [ ] Add HTML-on-request rule to `data/AGENTS.md`
- [ ] Inline "avoid AI slop" design conventions (no centered layouts, no purple gradients, no uniform rounded corners, no Inter font)
- [ ] Cite Thariq Shihipar's blog post (May 2026) as the source
## B14 — Smoke (STOP at end, full report)
- [ ] Markdown pane happy path (open, render, copy, download)
- [ ] HTML pane happy path (open, render, JS executes, download — no Copy button)
- [ ] HTML security exfil attempt — `fetch()` blocked by `connect-src 'none'`
- [ ] HTML security DOM access — sandbox without `allow-same-origin` enforces opaque origin
- [ ] Opt-in opportunistic detection — first reply Markdown, follow-up "render as HTML" produces artifact
- [ ] 1MB cap — synthetic test, streaming aborts with friendly error
- [ ] Path-guard on download — hand-crafted `../` slug rejected
- [ ] Persistence — pane state survives hard reload via `sessions.workspace_panes`
## B15 — OpenSpec docs + release
- [ ] Mark this `tasks.md` checkboxes complete after each step
- [ ] Append retrospective bullet to bottom of v1.14.x-html section in `boocode_roadmap.md`
- [ ] Add `CHANGELOG.md` entry with the assigned tag (e.g. `v1.14.1-html-artifact-panes` — final patch number assigned at ship time depending on order vs v1.14 outer loop)
- [ ] Hand back to Sam for tag + commit