Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions apps/backend/src/config/installed-apps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,22 +25,24 @@ function uniqueExisting(paths: Array<string | null>): string[] {
return existing;
}

function resolveDevManifest(): string | null {
function resolveDevManifest(packagePath: string): string | null {
try {
const root = process.env.DEUS_REPO_ROOT ?? resolveRepoRoot(process.cwd());
return resolve(root, "packages/device-use/agentic-app.json");
return resolve(root, packagePath);
} catch {
return null;
}
}

function resolvePackagedManifest(): string | null {
function resolvePackagedManifest(relPath: string): string | null {
const resourcesPath = (process as { resourcesPath?: string }).resourcesPath;
if (!resourcesPath) return null;
return resolve(resourcesPath, "agentic-apps/device-use/agentic-app.json");
return resolve(resourcesPath, relPath);
}

export const INSTALLED_APP_MANIFESTS: readonly string[] = uniqueExisting([
resolvePackagedManifest(),
resolveDevManifest(),
resolvePackagedManifest("agentic-apps/device-use/agentic-app.json"),
resolveDevManifest("packages/device-use/agentic-app.json"),
resolvePackagedManifest("agentic-apps/pencil/agentic-app.json"),
resolveDevManifest("packages/pencil/agentic-app.json"),
]);
124 changes: 122 additions & 2 deletions bun.lock

Large diffs are not rendered by default.

57 changes: 57 additions & 0 deletions packages/pencil/agentic-app.json
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"]
}
65 changes: 65 additions & 0 deletions packages/pencil/build.ts
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");
25 changes: 25 additions & 0 deletions packages/pencil/package.json
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"
}
}
81 changes: 81 additions & 0 deletions packages/pencil/skills/pencil/SKILL.md
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
```
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

## 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.)
Loading
Loading