dsh-surface
Render live HTML pages inside DeepSeekRuntime conversations — previews, reports, and forms that can answer back.
インストール
npx -p @deepseek-ai/dsh dsh plugin --profile web add dsh-surfaceドキュメント
README 全文を読む ↗dsh-surface
Live pages in the conversation. Preview, report, or ask.
dsh-surface is a DeepSeekRuntime plugin that renders a complete HTML page inside the conversation. Use it for UI previews, data reports, mockups, calculators — or for forms and approvals where the page can send a structured answer back to the agent through the same tool call.

Why
Most chat UIs can show text or a fixed component tree. A page is different: it can lay out a real report, run a chart, look exactly like the UI you are building, and collect an answer without making the user translate their choice into prose.
dsh-surface gives the model one tool:
- render a page — a self-contained HTML document, sandboxed, themed with the host's tokens, auto-sized;
- optionally wait for it — the tool call blocks until the user submits, cancels, or the timeout expires;
- return data, not prose — the page calls
dshSurface.submit(data, summary)and the agent receives{ answered, summary, data }in the same turn.
It is not a component library, and it is not an artifact manager. It is the page surface between the model and the user.
Features
- Any self-contained HTML — inline CSS/JS, SVG, canvas; no external requests.
- Sandboxed by default — opaque origin, no
allow-same-origin, CSP blocks network, forms, nested frames, and top-level navigation. - Follows the host theme —
--dsw-*tokens are bridged to--background,--foreground,--chart-*,--radius, and friends. - Auto height — the frame reports its content height and the card sizes itself.
- Blocking answers —
awaitAnswer-style semantics without a separate question panel. - OpenChamber-compatible pages —
window.openchamber.submit(data, summary)is an alias ofwindow.dshSurface. - Small and auditable — one host file, one client bundle, a 12-case test suite.
Install
Requires DeepSeekRuntime with the Web UI (dsh web) and Node.js ^22.19.0 || >=24.
dsh plugin --profile web add dsh-surface
Restart dsh web, then refresh the browser. The plugin activates on the next session.
The package is published as
dsh-surface. If you are installing from a checkout instead, usedsh plugin --profile web add link:/path/to/dsh-surfaceand runnpm install && npm run buildfirst.
Use it
Ask for a page, or point the model at one:
Render a one-page Q3 revenue report with a bar chart and a table. Use the
rendertool; make the page self-contained.
Here is our release checklist. Render it as a page with an approve / reject choice and a note field. On submit call
dshSurface.submit({ decision, note }, summary).
The model calls the render tool with a complete HTML document. The page appears in the tool card. If it calls dshSurface.submit, the call stays open until the answer arrives.
Tool reference
| Field | Type | Required | Meaning |
|---|---|---|---|
html | string | yes | A complete, self-contained HTML document (max 200,000 bytes). |
title | string | no | Card title; the page's own `` still names the frame. |
timeoutSeconds | number | no | How long to wait for a submission. Default 600, max 3600. |
The tool returns one JSON string:
{ "answered": true, "summary": "Approved with a note", "data": { "decision": "approve", "note": "ship it" } }
On timeout it returns { "answered": false, "reason": "timeout" }. On cancellation the call fails with the host's cancellation error.
Page API
Inside the page:
Approve
| API | Meaning |
|---|---|
dshSurface.submit(data, summary) | Send the answer. data must be JSON-serializable (≤ 16 KB); summary is a short human-readable line (≤ 500 chars). Returns a Promise. Must be called from a user gesture. |
dshSurface.answered | Whether an answer has been accepted. |
dshSurface.answer | The accepted answer, when available. |
dshRender / openchamber | Aliases kept for pages written before the rename. |
The page may also listen for dsh-surface:result on window to react to the host's acknowledgement.
Security model
- The page runs in an iframe with
sandbox="allow-scripts allow-forms"and noallow-same-origin. It has an opaque origin and cannot read the host page, cookies, or storage. - A document-level CSP allows inline scripts/styles and
data:images, and blocksconnect-src, nested frames, forms, and navigation. - The submission route is registered on the host's authenticated
/apichannel (ctx.connection.fetch). Only the owner peer reaches it; the host checks Host/Origin and browser authentication before the handler runs. - Payload limits: 500-character summary, 16 KB JSON data, 200 KB HTML.
- Pages should not ask for secrets. Treat any page content as untrusted.
See docs/architecture.md for the full lifecycle.
Compatibility
| Surface | Status |
|---|---|
dsh web (0.2.1-rc.2) | Tested end to end |
| dsh headless / TUI | No page surface; the tool waits until its timeout and returns answered: false |
| Desktop (Electron) | Not yet verified; custom fetch over the desktop IPC bridge needs a real run |
Known limitations
- The page folds when the turn ends. The card lives inside the tool-call group; after the turn settles, that group collapses. The answer remains visible in the transcript, and expanding the group brings the page back. A
conversation.chat.turnTailcard is the planned fix. - No local images yet. Only
data:/ inline images render; reading workspace files and enforcing a budget is not implemented. - HTML lives in the tool arguments. A large page is part of the session log and may be resent with later turns. An artifact-backed page (short id + file) is the planned improvement.
- No waiting notification. If you switch sessions, nothing tells you that a page is waiting; the tool does not appear in a global inbox.
- A host restart loses the wait. The pending map is in memory; after a restart the call is interrupted and the page becomes read-only.
- No persistence or versions. The plugin stores nothing of its own; every render is a new call.
Development
npm install
npm run check # lint + typecheck + tests + build + format check
npm test # node:test
npm run build # lib/index.js + lib/client.js
Project layout:
src/index.ts host: render tool, wait registry, answer route
src/wait.ts pending calls (first answer wins, cancel, timeout)
src/answer.ts POST body validation and responses
src/client/index.tsx client plugin: tool.call.toolview registration
src/client/RenderCard.tsx iframe card, theme, height, submit bridge
src/client/shell.ts frame document, CSP, height script, bridge
src/client/theme.ts --dsw-* → page variables
tests/ wait/answer unit tests
examples/approval.html a ready-to-use approval page
See CONTRIBUTING.md before opening a pull request.
License
MIT © 2026 woolsen