DSH OpenPencil

DSH OpenPencil

The DeepSeek Harness plugin for OpenPencil — preview, inspect, and edit real .op documents inside a conversation.
Exact Multi-Frame Previews • Interactive Canvas • Managed Editor • Agent-Native Design Tools

npm: @zseven-w/dsh-openpencil · Current plugin release: 0.1.0-rc.14 · Editor panel verified on DSH Desktop 0.2.1-rc.3

English · 简体中文 · 繁體中文 · 日本語 · 한국어 · Français · Español · Deutsch · Português · Русский · हिन्दी · Türkçe · ไทย · Tiếng Việt · Bahasa Indonesia

npm CI Stars License Discord


DSH OpenPencil — multi-frame preview and sidebar editor

Exact multi-frame .op previews with an interactive canvas and the managed editor workbench

## Why DSH OpenPencil DSH OpenPencil connects [DeepSeek Harness](https://github.com/deepseek-ai/DSH) with [OpenPencil](https://github.com/ZSeven-W/openpencil) so an Agent drives a real, editable, interactive design canvas instead of returning a generated image.
### 🖼️ Exact Multi-Frame Previews The installed OpenPencil headless exporter renders design-faithful previews: the first top-level frame as a large replay-safe PNG, plus a horizontally scrollable thumbnail rail, click-to-select, and previous/next navigation for multi-frame documents. ### 🗺️ Interactive Canvas "Open interactive canvas" lazily mounts the read-only OpenPencil Web SDK with pan, zoom, and fit — inspect any page, nested node, or inactive page without leaving the conversation.
### ✏️ Managed Editor With `editable: true`, the edit action opens the managed OpenPencil editor — selection, layers, properties, drawing tools, undo/redo, and explicit save semantics — in a resizable right-hand workbench with a full-screen option. ### 🤖 Agent-Native Design Tools Five direct-canvas tools plus six `openpencil_pipeline_*` tools let the Agent create, inspect, refine, publish, modify, and read a real canvas through managed OpenPencil runtimes.
### 🔐 Capability-Gated Grants Image and document grants are signed, hash-bound capabilities. Browser metadata never exposes an arbitrary host path, and signed preview/editor capabilities never enter the canonical tool result or model context. ### ⚡ Transactional Safety A full-pipeline document stays in a private unpublished draft until every native and DSH quality gate passes. Publication never overwrites an existing path, and aborts or failed batches leave no empty target behind.
### 🌍 Follows DSH Look & Feel The tool card and managed editor follow DSH's Chinese/English locale and light/dark theme without reloading the editing session. ### 🎯 One Complete Workflow "Requirement → private draft → semantic batches → live user previews → deterministic structure/layout/quality validation → exact-PNG-integrity atomic publish" — one complete loop inside DSH.
## Install into DSH DSH is a separate package. Install it once if you do not already have it: ```sh npm install -g @deepseek-ai/dsh@latest ``` Then add the plugin to a profile and start the web app: ```sh dsh plugin --profile web add @zseven-w/dsh-openpencil@latest dsh web ``` The plugin is still on a prerelease line. Releases use the npm `latest` tag; install from `latest` to receive the current version. The older `next` tag is not the active release channel. For local development, build the checkout, link its absolute path into the Web profile, and then restart DSH: ```sh pnpm run build dsh plugin --profile web add link:/absolute/path/to/dsh-openpencil dsh web ``` The `link:` dependency exposes subsequent rebuilds from this checkout, but DSH must be fully restarted after replacing the profile dependency because the shipped Web profile does not hot-reload host bundles by default. Prefer not to install DSH globally? Run the same two steps through `pnpm dlx`: ```sh pnpm dlx --package=@deepseek-ai/dsh@latest dsh plugin --profile web add @zseven-w/dsh-openpencil@latest pnpm dlx --package=@deepseek-ai/dsh@latest dsh web ``` > The OpenPencil plugin is public and requires no npm token. If the DSH prerelease itself requires registry authentication, keep that credential in a user-level or temporary npm config outside the checkout. This repository intentionally contains no registry credentials. ## Design Tools Optional style selection uses `openpencil_style_plan` → `openpencil_style_confirm` → `openpencil_pipeline_begin` with the original brief and `style_selection` handle. Catalog browsing and exact-guide choice work without a model recommendation; the recommendation provider is off by default. This handoff supports the script engine. App-agent and commerce handoffs remain unsupported; ordinary app-agent generation still works. | Tool | What it does | | --- | --- | | `openpencil_new` | Compatible fast path for simple jobs: runs one transactional QuickJS `batch_design` script, publishes with create-if-absent semantics, and returns an editable presentation. Prefer the full pipeline below for production design. | | `openpencil_pipeline_begin` | Starts an owner-scoped private draft, internally seeds its only root, returns `rootNodeId`, `continuationStyle`, and the compact runtime-matched canvas/build contract, then immediately opens the same live draft in the sidebar; the target `.op` remains unpublished. | | `openpencil_pipeline_context` | Loads one bounded guideline, style, theme, or UI-kit detail that is genuinely missing from the begin contract; it is not a startup-context refresh loop. | | `openpencil_pipeline_batch` | Runs at most two direct native `I(...)`/`K(...)` generation scripts: the bounded first-visible-viewport script returned by begin's `next`, then one script for every remaining region. Before exposing the second preview, an authored-structure gate rejects empty category/product helpers, atomically restores the first-batch snapshot, and permits one corrected second script without consuming the two-script budget. Each successful transaction attempts an exact user-facing PNG and must be followed immediately, without narration, by its returned `next`. A third generation script is rejected; only the complete repair gate returned by finish may authorize one bounded `U(...)` QuickJS repair script. | | `openpencil_pipeline_inspect` | Provides manual diagnostics only when the user explicitly requests them; ordinary generation never uses it as a preview or model-inspection step. | | `openpencil_pipeline_finish` | In one healthy call, finalizes and validates the document, automatically renders the exact post-final root preview, and atomically publishes with `createIfAbsent`. One `U(...)`-only repair and one final finish are authorized only when the result is `needs_correction`, `canContinue: true`, and contains a complete non-empty `repairTargets` array with `omitted: 0`; every other non-published result is terminal. | | `openpencil_pipeline_abort` | Discards the unpublished draft without creating the target file. | | `openpencil_create` | Applies a transactional `batch_design` program to generate or restructure nodes on an existing live canvas. | | `openpencil_edit` | Modifies an explicit node or the single node selected by the user. | | `openpencil_render` | Creates an immutable, content-addressed `.op` snapshot and renders every top-level frame on the active page — optional `scale` and `editable`. | | `openpencil_selection` | Reads the exact nodes selected in the live editor canvas. | ## Agent Design Workflow ### Model tier for design turns Generation quality tracks the model driving the pipeline turns. The two-batch contract works on fast tiers, but layout judgment, copywriting, and contract adherence improve markedly on a stronger or reasoning-enabled model. For design-heavy profiles, prefer a non-flash tier (for example `deepseek-v4` with `reasoningEffort` enabled over `deepseek-v4-flash`) in the profile's `agent-default-model`; keep the fast tier for chat-only profiles where design output is incidental. Desktop commerce Hero geometry is validated before batch two: every right-side visual child and layered shape must stay inside the fixed Hero inner width and height. An oversized visual is rolled back with the entire first batch instead of inflating the live canvas or reaching final publication. Generation receipts expose only committed node mappings and previews; native diagnostics are deferred to finish so they produce one exact repair transaction instead of a speculative mid-generation loop. For an ordinary one-line request, call `openpencil_pipeline_begin` immediately without spending a model step loading the optional skill, then use the fixed fast path: the bounded first direct-JS batch from begin's `next` → a second and final direct-JS batch containing every remaining region → one `openpencil_pipeline_finish`. The current first batch renders only the **first visible viewport**: a complete navigation/header plus a polished primary hero/content selected from the brief, capped at **32 `I`/`K` calls and 8 KiB**. This budget preserves real structure and visual hierarchy without reopening an unbounded generation loop; secondary cards, detail, and below-fold content wait for the second batch. For commerce, begin now selects the concrete App-aligned `ecommerce-modern-light` recipe before drawing: a white base with warm-tinted section rhythm, restrained orange actions, 1120px centered content, a 56px Hero display, and visible copy in the language of the user's request (a Chinese request keeps Chinese copy, except for an optional short ASCII brand). A desktop commerce Hero is a full-width horizontal frame with `padding:[64,160]`; its inner content is copy 512px + gap 64px + product visual 448px, and both headline and subtitle use `width:"fill_container"`. Never combine a fixed 1120px Hero width with that padding. Generic commerce creates the visual directly under Hero as `I(hero,{type:"image",name:"Hero product image",width:448,height:360,imageSearchQuery:"gray loveseat isolated photo"})`: `imageSearchQuery` is a direct node field, never an `image:{...}` object, and the image is neither wrapped nor mixed with decorative shapes or reused by a product card. It is enriched as soon as the first batch commits. An explicit `layout:"none"` stack of 4–6 positioned layers, including at least one ellipse/path, is allowed only when the user explicitly requests illustration or no photos. Plain stacked rounded rectangles, an empty right field, overflow, or an undersized decorative tile are rejected and atomically rolled back before batch two. Platform and viewport come from the latest direct user request: a model-expanded `brief` cannot silently turn an unspecified request into a mobile canvas. When the brief provides no brand, immediately use a neutral shop name or short placeholder that fits its context, without comparing naming options. When no logo asset is supplied, use a text-only brand; do not invent a letter badge or give a text node a background, fixed height, or effects. Begin creates the only root and opens the private live canvas immediately. After every successful begin or batch call, make the next required call immediately without narration, planning, comparison, inspection, or any unrelated tool call. A healthy finish runs native finalization first, enriches the canonical image slots, renders the post-final user preview, and publishes in that same call. The App-aligned commerce preset fixes the 64px Header at `padding:[0,160]`: the Header is a `navbar`, its Nav is one `nav-links` collection of 44px `nav-link` items, and Header actions are a `toolbar` of 44×44 `icon-button` frames rather than bare icons. The 160×48 primary CTA is a `button`, contains its own label, and uses the AA pair `#C2410C`/`#FFFFFF`. All three product cards stay in one coherent collection. For a generic home brief, the exact runtime-validated set is `gray armchair isolated photo`, `artemide tolomeo lamp photo`, and `potted plant isolated photo`; the last card must be labeled as a potted plant rather than a vase. The begin result is the compact authoritative run contract. Omit `path` unless the user explicitly named one; the plugin creates the concrete collision-resistant `.op` filename, avoiding template syntax or a retrying begin/abort loop. Preserve its exact `rootNodeId`, `continuationStyle`, `canvas`, and `buildContract`, then implement the bounded first script exactly as `next` specifies without broadening its scope or inserting a user-facing reasoning message. After its automatic preview, write the second and final script for all remaining regions with arrays and loops. This direct two-script flow keeps execution bounded and responsive. Both generation scripts use direct `I(...)`/`K(...)` QuickJS plus begin's exact `rootNodeId`, `continuationStyle`, and `buildContract`. Inside `run_code`, construct the embedded source with a `String.raw` tagged template so escapes and intended text newlines reach QuickJS unchanged. Use this exact multiline wrapper: ```js const draftId = ""; const script = String.raw`...`; const r = await tools.openpencil_pipeline_batch({ draftId, script }); return r; ``` Quote the exact `begin.draftId` into the standalone `draftId` string first, then declare `script`. The fixed tool argument object contains only `draftId` and `script`; never append `canvasWidth`, another field, or return inside it. Return only `r` without logging, printing, or stringifying it. Each QuickJS batch has a fresh scope: local bindings do not cross batches. `I` and `K` return opaque node-id strings, not mutable nodes: use a binding only as a later `I`/`K` parent and never assign `binding.x`, `binding.y`, or any other member. In batch 2, never recreate Page, App Content, Header, or Hero. Prefer attaching each new bound section rail directly to begin's `rootNodeId`. If batch 1 created a shared content wrapper, reuse it only through the exact nodeId returned by batch 1; never rebuild another wrapper with the same name. `rootNodeId` is already the page frame: attach top-level regions directly, never create another Page/root wrapper. Only frame/group bindings may parent children; round artwork containing an icon uses a frame plus `cornerRadius`, never an ellipse parent. Every semantic container—Header, Nav, Search, Hero, Card, Section, Toolbar, Button, or CTA—must be assigned from `I`/`K` and immediately receive all intended visible children through that binding; never leave it empty or place its intended children as siblings. A minimal valid header is `const h=I(root,{type:"frame",name:"Header",layout:"horizontal"}); I(h,{type:"text",content:"Shop",fontFamily:"Inter, system-ui, sans-serif"});`. On mobile, only full-width chrome/full-bleed sections attach directly to root; bare text, icons, small controls, and section titles must be inside a bound rail/section with a 24px left/right gutter. Every category card contains a real visual tile plus its label; a mobile category item uses a 56×56 tile frame. Heterogeneous product cards never reuse one glyph as fake product art: use a matching distinct icon/shape or omit that media wrapper. A desktop commerce category rail spans the 1120px content width with `justifyContent:"space_between"`; its product rail contains exactly three equal `fill_container` cards with 24px gaps and no unused right tail. Every Button/CTA frame must immediately receive its visible text/icon child through the binding returned when that frame is created, never as a sibling. The minimum icon is `{type:'icon_font',name:'Search icon',iconFontName:'search',width:20,height:20}`: `name` is the layer label, while `iconFontName` selects the glyph and must come from the verified Lucide set, including `smartphone`/`camera` for electronics and `utensils`/`sandwich`/`croissant` for food; never substitute `lamp` for electronics or `coffee` for food. The broader verified set also includes `home`, `search`, `shopping-bag`, `shopping-cart`, `user`, `heart`, `star`, `plus`, `arrow-right`, `sparkles`, `sun`, `apple`, `snowflake`, `droplet`, `cookie`, `leaf`, `coffee`, `package`, `gift`, `baby`, `spray-can`, `lamp`, `sofa`, `armchair`, `shirt`, `headphones`, `laptop`, `monitor`, `gamepad-2`, `watch`, `palette`, and `cake`; compose shapes when none fits. Outside commerce, default to exactly one `type:"image"` leaf unless the user explicitly requests multiple images. Commerce uses exactly three distinct product images. Every query stays at four English words or fewer, names one concrete product, and never requests a lifestyle, collection, or broad category; generic home uses the exact validated set above. Isolated queries require positive isolated/cutout/white-background metadata rather than merely accepting a photograph. Multi-word queries must match at least two meaningful metadata tokens. Never put a lone small icon inside a large fixed product-media frame. Every Hero/Product/Art/Media frame has exactly one primary visual—one image with a concrete English `imageSearchQuery`, or one substantial composed-shape visual—never an image plus an icon placeholder; image nodes are leaves, never parents. Every generated text node explicitly uses `fontFamily: "Inter, system-ui, sans-serif"`; ordinary values are `fontSize: 16` and `lineHeight: 1.5`. Desktop keeps its bundled Inter, while the web host—which deliberately does not bundle Inter—uses the generic fallback without a missing-font prompt. Never use bare `Inter`. A CJK `lineHeight < 1.3` is raised to `1.5`, and a numeric text height becomes `fit_content` unless `textGrowth` explicitly requests `fixed-width-height`. Override size and line height only for headings or special typography. The runtime contract remains the single source for every other current node, style, script, and repair rule. A third generation script is rejected. Only the complete repair gate described below may authorize one third and final bounded QuickJS repair script, using `U(...)` only for the named targets. Category rails use one distinct matching icon per label. Each `