-
Notifications
You must be signed in to change notification settings - Fork 0
Embed Pencil as agentic app with live editor bridge #270
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 1 commit
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
b2b0a54
Embed Pencil as an agentic app with live editor bridge
zvadaadam 866bf51
Improve Pencil first-run cold start
cursoragent 22afaa2
Remove redundant Pencil web sign-in
cursoragent 836ccd2
Fix Pencil MCP batch design bridge
cursoragent 75f53e3
Polish Pencil packaging and MCP bridge
cursoragent 5d4a88f
Merge Pencil cold start improvements
cursoragent 99a5e33
Merge remote-tracking branch 'origin/main' into zvadaadam/embed-penci…
zvadaadam d7f1a6e
Fix pencil_new hang and "no design" UI after creation
zvadaadam 99384a3
Sync switcher with iframe canvas, save designs in workspace/designs
zvadaadam 903891b
Address CodeRabbit review — security, reliability, polish
zvadaadam File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| { | ||
| "$schema": "https://agenticapps.dev/schema/v1.json", | ||
| "protocolVersion": "1", | ||
|
|
||
| "id": "pencil.app", | ||
| "name": "Pencil", | ||
| "description": "AI-powered design tool for web and mobile apps. Agents can generate, modify, and export .pen design files. Backed by the Pencil CLI; no Pencil Desktop required.", | ||
| "version": "0.2.0", | ||
|
|
||
| "launch": { | ||
| "command": "node", | ||
| "args": [ | ||
| "./dist/serve.js", | ||
| "--port", | ||
| "{port}", | ||
| "--workspace", | ||
| "{workspace}", | ||
| "--storage", | ||
| "{storage.workspace}" | ||
| ], | ||
| "ready": { | ||
| "type": "http", | ||
| "path": "/health", | ||
| "timeoutMs": 30000 | ||
| } | ||
| }, | ||
|
|
||
| "ui": { | ||
| "url": "http://127.0.0.1:{port}/" | ||
| }, | ||
|
|
||
| "agent": { | ||
| "tools": { | ||
| "type": "mcp-http", | ||
| "url": "http://127.0.0.1:{port}/mcp" | ||
| }, | ||
| "bootstrap": "ALL design work happens on the live iframe canvas — every op the agent makes renders in real time and the user watches it build. Workflow: (1) `pencil_get_active` or `pencil_list_designs` to know what's open / what exists; (2) `pencil_new` to start a blank canvas, OR `pencil_open` to switch to an existing .pen; (3) `get_guidelines` once at the start (provides .pen op syntax + style); (4) `get_editor_state({ include_schema: true })` to read the document; (5) drive the design with `batch_design` ops — small batches (≤25 ops) so progress is visible. Other live tools as needed: `batch_get`, `snapshot_layout`, `get_screenshot`, `find_empty_space_on_canvas`, `replace_all_matching_properties`, `search_all_unique_properties`, `set_variables`, `get_variables`, `export_nodes`, `open_document`. Call `mcp__deus__read_app_skill({ appId: 'pencil.app' })` for full guidance." | ||
| }, | ||
|
|
||
| "storage": { | ||
| "workspace": "{workspace}/.pencil" | ||
| }, | ||
|
|
||
| "lifecycle": { | ||
| "scope": "workspace", | ||
| "stopTimeoutMs": 5000 | ||
| }, | ||
|
|
||
| "requires": [ | ||
| { | ||
| "type": "platform", | ||
| "os": "darwin" | ||
| } | ||
| ], | ||
|
|
||
| "skills": ["skills/pencil/SKILL.md"] | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| // packages/pencil/build.ts | ||
| // | ||
| // Two esbuild bundles: the Node launcher (src/serve.ts → dist/serve.js) | ||
| // and the browser-side iframe controller (src/ui/app.ts → dist/ui/app.js). | ||
| // Static assets (parent.html, styles.css) are copied as-is. | ||
| // | ||
| // Run with: `bun run build` from the package root, or `bunx tsx build.ts`. | ||
|
|
||
| import esbuild from "esbuild"; | ||
| import { copyFileSync, mkdirSync, rmSync } from "node:fs"; | ||
| import { dirname, join } from "node:path"; | ||
| import { fileURLToPath } from "node:url"; | ||
|
|
||
| const root = dirname(fileURLToPath(import.meta.url)); | ||
| const distDir = join(root, "dist"); | ||
|
|
||
| rmSync(distDir, { recursive: true, force: true }); | ||
| mkdirSync(join(distDir, "ui"), { recursive: true }); | ||
|
|
||
| // ---- Node launcher -------------------------------------------------------- | ||
| // | ||
| // Bundle into a single ESM file. Mark @pencil.dev/cli as external so we | ||
| // dynamically resolve its package.json at runtime via require.resolve() | ||
| // rather than baking a path into the bundle. The banner injects a CommonJS | ||
| // `require` so we can keep using `require.resolve()` from ESM. | ||
| await esbuild.build({ | ||
| entryPoints: [join(root, "src/serve.ts")], | ||
| bundle: true, | ||
| platform: "node", | ||
| target: "node18", | ||
| format: "esm", | ||
| outfile: join(distDir, "serve.js"), | ||
| external: [ | ||
| "@pencil.dev/cli", | ||
| "unzipper", | ||
| "@aws-sdk/client-s3", | ||
| "ws", | ||
| "bufferutil", | ||
| "utf-8-validate", | ||
| ], | ||
| banner: { | ||
| js: "import { createRequire } from 'node:module';\nconst require = createRequire(import.meta.url);\n", | ||
| }, | ||
| logLevel: "info", | ||
| }); | ||
|
|
||
| // ---- Browser iframe controller ------------------------------------------- | ||
| // | ||
| // Browser target with DOM types. Single-file bundle so the iframe loads | ||
| // one script tag. | ||
| await esbuild.build({ | ||
| entryPoints: [join(root, "src/ui/app.ts")], | ||
| bundle: true, | ||
| platform: "browser", | ||
| target: "es2022", | ||
| format: "esm", | ||
| outfile: join(distDir, "ui/app.js"), | ||
| logLevel: "info", | ||
| }); | ||
|
|
||
| // ---- Static assets -------------------------------------------------------- | ||
| copyFileSync(join(root, "src/ui/parent.html"), join(distDir, "ui/parent.html")); | ||
| copyFileSync(join(root, "src/ui/styles.css"), join(distDir, "ui/styles.css")); | ||
|
|
||
| console.log("[pencil] build complete"); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| { | ||
| "name": "@deus/pencil-app", | ||
| "version": "0.2.0", | ||
| "private": true, | ||
| "description": "Pencil design AAP — wraps the Pencil CLI as MCP-callable design tools.", | ||
| "type": "module", | ||
| "main": "dist/serve.js", | ||
| "files": [ | ||
| "dist", | ||
| "skills", | ||
| "agentic-app.json" | ||
| ], | ||
| "dependencies": { | ||
| "@pencil.dev/cli": "^0.2.5", | ||
| "unzipper": "^0.12.3", | ||
| "ws": "^8.18.3" | ||
| }, | ||
| "devDependencies": { | ||
| "@types/ws": "^8.18.1" | ||
| }, | ||
| "scripts": { | ||
| "build": "bunx tsx build.ts", | ||
| "typecheck": "tsc --noEmit" | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| --- | ||
| name: pencil | ||
| description: "Generate, modify, and export visual designs (web, mobile, marketing) by describing them and operating on a live canvas. Use whenever the user asks for a design, mockup, landing page, screen, layout, or any visual asset." | ||
| --- | ||
|
|
||
| # Pencil | ||
|
|
||
| Pencil renders a real interactive canvas in the user's panel. **Every op the agent makes shows up on the canvas immediately** — there's no batch mode, no spinner, no 30‑second waits. The user watches the design build. | ||
|
|
||
| ## Tool surface | ||
|
|
||
| All design work goes through the **live editor** tools (provided by the bundled MCP binary, bridged to the iframe). The agent never spawns a CLI subprocess. | ||
|
|
||
| ### Workspace navigation (4 — ours) | ||
|
|
||
| - `pencil_list_designs()` — every `.pen` in the workspace (workspace files + agent‑generated). Filesystem only. | ||
| - `pencil_get_active()` — which `.pen` is currently displayed in the panel. Use when the user says "this design" / "the open one". | ||
| - `pencil_open({ file? | name? })` — switch the editor panel to a different `.pen`. Workspace‑aware path resolution. | ||
| - `pencil_new({ name })` — create a brand‑new blank canvas. Sets the save target to `<workspace>/.pencil/designs/<name>.pen` and tells the editor to open a fresh empty document. After this, drive the design with `batch_design`. | ||
|
|
||
| ### Live editor (13 — Pencil's native tools, bridged) | ||
|
|
||
| - **`batch_design({ operations })`** — the workhorse. Run a script of insert/copy/update/replace/move/delete/image ops in one call. ≤25 ops per batch so the user sees progress. | ||
| - **`batch_get({ patterns?, nodeIds? })`** — read nodes by ID or pattern. Use to discover structure before editing. | ||
| - **`get_editor_state({ include_schema })`** — current document, selection, and (if requested) the `.pen` schema. Call once with `include_schema: true` at the start of a task; `false` for follow‑ups. | ||
| - `get_screenshot({ nodeId })` — PNG of any node for visual verification. | ||
| - `snapshot_layout({ parentId, maxDepth?, problemsOnly? })` — compact node tree for layout reasoning. | ||
| - `find_empty_space_on_canvas({ width, height, direction, padding })` — pick a non‑overlapping region for new content. | ||
| - `open_document(filePath | "new")` — switch documents at the editor level (lower‑level than `pencil_open`; prefer `pencil_open` for existing files because it also updates the panel switcher). | ||
| - `replace_all_matching_properties` / `search_all_unique_properties` — mass property edits / discovery. | ||
| - `set_variables` / `get_variables` — design tokens. | ||
| - `export_nodes({ nodeIds, format, scale, quality? })` — render specific nodes to image files. | ||
| - **`get_guidelines(category?, name?)`** — Pencil's own `.pen` syntax + style guides. **Always call `get_guidelines("general")` once at the start of any `batch_design` work** — the op syntax is non‑obvious and these guides are how you learn it. | ||
|
|
||
| ## Standard workflow | ||
|
|
||
| ``` | ||
| 1. get_guidelines("general") // load .pen op syntax | ||
| 2. pencil_get_active // know what's open | ||
| (or: pencil_list_designs → pencil_open / pencil_new) | ||
| 3. get_editor_state({ include_schema: true }) // load document + schema | ||
| 4. batch_design({ operations: [ ... ] }) // build, ≤25 ops/batch | ||
| 5. get_screenshot({ nodeId }) // verify visually | ||
| 6. iterate: batch_get → batch_design → screenshot | ||
| ``` | ||
|
|
||
| ## Patterns | ||
|
|
||
| **New design from scratch:** | ||
|
|
||
| ``` | ||
| user: "design me an agent control center" | ||
| → pencil_new({ name: "agent-control-center" }) // blank canvas, visible | ||
| → get_guidelines("general") // .pen syntax | ||
| → batch_design({ operations: [...frame, sidebar, header...] }) // user watches it appear | ||
| → batch_design({ operations: [...activity feed cards...] }) | ||
| → ... continue in small batches | ||
| ``` | ||
|
|
||
| **Edit an existing design:** | ||
|
|
||
| ``` | ||
| user: "make the title bigger" | ||
| → get_editor_state({ include_schema: true }) // find the title node | ||
| → batch_design({ operations: [ "U('title-id', { fontSize: 48 })" ] }) // immediate | ||
| ``` | ||
|
|
||
| **Switch context:** | ||
|
|
||
| ``` | ||
| user: "show me the dashboard instead" | ||
| → pencil_list_designs // find it | ||
| → pencil_open({ file: "design/dashboard.pen" }) // switch panel | ||
| ``` | ||
|
|
||
| ## Behavior notes | ||
|
|
||
| - **Small batches.** Even though `batch_design` accepts up to 25 ops per call, prefer 5–15 — the user perceives smoother progress with more frequent updates. | ||
| - **Read before write.** Always `get_editor_state` (or `batch_get` for targeted reads) before a `batch_design` that references existing nodes. Node IDs aren't guessable. | ||
| - **Guidelines are mandatory.** `get_guidelines("general")` returns the canonical `.pen` op syntax. Skipping this leads to invalid ops and wasted batches. | ||
| - **Auth.** The live editor tools work as long as the iframe is connected — no Pencil CLI key needed. (The CLI key in the sign‑in card is only used for the editor's own cloud features like AI image gen.) | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.