Skip to main content

Building advanced apps (Excalidraw / drawdb class)

The most powerful Digitorn apps — a diagram canvas, an ER-schema designer, a spreadsheet, a document editor — are not written from scratch. Each is a capable existing frontend adopted whole and bound to the agent through @digitornai/sdk. The heavy lifting (the editor) is borrowed; the value Digitorn adds is a thin bridge that makes the agent and the user co-drive the same live document.

Every claim on this page is anchored to real, shipped apps you can read:

AppAdopted frontendBridge source
Excalidraw (canvas)npm @excalidraw/excalidrawpackages/digitorn-sdk/examples/excalidraw/src/App.tsx
drawdb (ER schema)the whole drawdb OSS repo, vendored.../examples/drawdb/src/digitorn/SyncBridge.jsx
Univer (spreadsheet)npm @univerjs/presets.../examples/univer/src/App.tsx + agent-actions.ts
LaTeX (document viewer)npm pdfjs-dist.../examples/latex/src/App.tsx

Read 48-digitorn-sdk.md for every hook's exact signature, 47-custom-workspace-views.md for the view YAML, and 44-client-manifest.md for the ui.workspace block. This page is how you put them together into something advanced. Never rewrite a capable editor — adopt one.

Two adoption routes​

  1. Wrap an npm component (Excalidraw, Univer, a PDF viewer). The app is a thin App.tsx that renders the component and wires the bridge inline. Fastest and cleanest when a good component exists on npm.
  2. Vendor a whole OSS repo (drawdb). Copy the project's src/ into the app, add a small src/digitorn/ bridge folder (drawdb: SyncBridge.jsx, ThemeSync.jsx, main.jsx), and build. Use this when the capability lives in a full app, not a single package.

Either way the built dist/ ships at {app}/web/dist/ and the app declares ui.workspace.render_mode: react + entry_file: (the document file the editor centers on — scene.excalidraw, diagram.drawdb.json, workbook.json, main.tex).

The bridge: bind the editor to ONE workdir file​

The core idea (verified in both excalidraw/src/App.tsx and drawdb/.../SyncBridge.jsx): the editor's document is bound to one workdir file with useSharedDoc, so it round-trips through the agent — the agent edits the file → the editor updates; the user edits the canvas → the file updates → the agent sees it next turn.

tsx
const [scene, setScene] = useSharedDoc<Scene>("scene.excalidraw", { elements: [] });

That single line is deceptively simple. Making it not-broken requires four invariants — both example bridges implement all four, independently, because skipping any one produces a write loop or a clobbered document:

  1. Fingerprint baseline. Compute a fingerprint() of the meaningful state (elements, not zoom/scroll/selection) and keep it in a ref (appliedFp). It is the shared baseline for both directions.
  2. Drop your own echo. A programmatic update you push into the editor fires the editor's onChange; if its fingerprint matches appliedFp, it is your own echo — drop it, don't write it back. (Excalidraw also sets a suppress ref right before a programmatic updateScene to catch the echo.)
  3. Read-only until interacted. The editor fires an empty onChange([]) while it mounts. Gate writes behind an interacted ref so that mount-time empty state cannot overwrite a document the agent already wrote to the file.
  4. High-level inputs expand INTO state, never rewritten. An input like diagram.mmd (Excalidraw) or schema.dbml (drawdb) is the agent's small, human-readable source; you expand it into the real document but treat it as an INPUT you never write back — otherwise expansion loops forever. On a failure, write the error to a file the agent can read (diagram.error.txt) instead of clobbering a good document.

If your bridge scrolls, churns the file on zoom, or wipes the agent's work on load, it is missing one of these four. They are the whole game.

Exploit the SDK to the maximum​

An advanced app uses far more than useSharedDoc. The full power surface, each verified against the SDK source:

The agent drives the LIVE app (the "wow" layer)​

useAgentActions lets the agent invoke semantic actions on the running editor — not just write files. The app defines what each action means; the agent calls it by name through the preview channel.

tsx
useAgentActions({
add_chart: (args) => chartManager.create(toSpec(args)),
switch_sheet: ({ name }) => workbook.setActiveSheet(String(name)),
select: ({ range }) => selection.set(String(range)),
});

This is exactly Univer's edge (examples/univer/src/agent-actions.ts): the agent creates and updates native charts on the live canvas in natural language, while the user keeps full manual control of the same engine — no conflict. Any advanced app should expose its high-value operations this way.

The app asks the agent (select → Ask AI)​

A view cannot directly drive the turn — useChat().send, abort, and useApprovals().approve/reject are exported but rejected by the daemon from inside a view (its preview token is a read-only identity: "session belongs to another user"). That limit is real; don't build a UI that pretends to send as the user.

What a view CAN do is ask the host to relay an instruction, which the host posts into the chat under the user's own auth: sendToAgent(instruction, context) and the ready-made askAI({ anchor, context }) bubble. This is the powerful "select something in the editor → Ask AI → the agent acts on exactly that" pattern, and the askAI UI ships inside the SDK so every app gets the same flow:

tsx
const rect = range.getBoundingClientRect();
askAI({ anchor: rect, context: { kind: "selection", source: "document.docx", text } });

The agent SEES the app​

useAgentSnapshot(capture) publishes a real PNG of the view when the agent requests one (it writes snapshot.request; your capture writes the PNG and stamps snapshot.ready). Wire it when the app can export its own canvas cleanly (Excalidraw's exportToBlob) for a sharper image than the generic preview.snapshot tool's headless shot.

Host chrome, live build, review, theme​

  • useTopBarActions([...]) + switchView(id) — put the app's own buttons and menus into the host top bar (Deploy, Export, Refresh); they appear only while this view is active. style gives a native-looking primary CTA.
  • useCompile() — a one-click rebuild of the current document with no agent turn (the app owns the button via useTopBarActions, this owns the action).
  • usePreviewAttach() — render your own empty state until the agent has built something, then switch to the live build; keeps switching on every new build. The primitive behind a Lovable-style build→preview.
  • reportProposals(state) / onReviewCommand(handler) — surface a review affordance ("12 suggestions") and accept/reject/next/prev pending changes, for a suggest-then-apply workflow.
  • useTheme() + useThumbnailMode() — match the host's light/dark live, and render clean (no chrome, fit-to-frame) when captured for a gallery thumbnail.

Four complete archetypes​

1. Canvas editor — Excalidraw (wrap an npm component)​

text
deps:        @excalidraw/excalidraw + @excalidraw/mermaid-to-excalidraw
entry_file: scene.excalidraw # useSharedDoc document
bridge: examples/excalidraw/src/App.tsx
uses: useSharedDoc + all 4 invariants, useFile("diagram.mmd") high-level
input, useAgentSnapshot (exportToBlob), useTheme, useThumbnailMode

The whole editor is the npm component; the bridge binds {elements, appState, files} to scene.excalidraw, expands diagram.mmd into elements, and strips the stock socials/Export in favor of a Digitorn "Generate PNG/SVG" that writes to the workdir.

2. ER-schema designer — drawdb (vendor a whole OSS repo)​

text
adopt:       the drawdb OSS repo, copied into src/
entry_file: diagram.drawdb.json
bridge: examples/drawdb/src/digitorn/SyncBridge.jsx (+ ThemeSync.jsx, main.jsx)
uses: useSharedDoc + all 4 invariants, useFile("schema.dbml") high-level
input (DBML → diagram), useThumbnailMode (fit on load)

The bridge reads drawdb's own React contexts (useDiagram, useAreas, …), binds their combined state to diagram.drawdb.json, and lets the agent author with readable DBML that expands into the visual schema.

3. Spreadsheet — Univer (wrap npm + agent-driven canvas)​

text
deps:        @univerjs/presets (+ conditional-formatting, data-validation, …)
entry_file: workbook.json
bridge: examples/univer/src/App.tsx + agent-actions.ts
uses: useSharedDoc (workbook), useAgentActions (create/update native
charts, scroll, switch sheet, select — on the LIVE sheet)

The standout: agent-actions.ts exposes live-sheet operations the agent drives in natural language, beating a manual-only tool — while the user edits the same engine simultaneously.

4. Document viewer — LaTeX (wrap a renderer, read-only preview)​

text
deps:        pdfjs-dist
entry_file: main.tex
bridge: examples/latex/src/App.tsx
uses: useWorkspaceEndpoint().fileUrl to load the compiled PDF, useWatch to
re-render when the agent recompiles

Not every advanced app is a two-way editor. LaTeX is a render/preview archetype: the agent compiles main.tex → PDF with its own tools, and the view shows it live via pdfjs, re-rendering on each rebuild.

Build and ship​

Same for all four:

bash
# in .digitorn-work/<app>/ — source stays out of the app bundle
npm install @digitornai/sdk socket.io-client <the-adopted-frontend>
# socket.io-client is a required peer dependency of @digitornai/sdk
# vite.config: base: "./", build.outDir: "dist"
npm run build
mkdir -p ../../web/dist && cp -r dist/* ../../web/dist/ # only the build crosses in

Then declare it:

yaml
ui:
workspace:
render_mode: react
entry_file: scene.excalidraw # the document the editor centers on
position: right
default_view: preview

base: "./" is mandatory (the bundle is served from a nested path); without it the view loads blank. At runtime the app needs no Node — web/dist/ is static.

Caveats (all verified — keep them true)​

  • A view can't act as the account owner. useChat().send/abort and approvals are blocked from a view; use sendToAgent/askAI to relay through the host instead.
  • The four bridge invariants are not optional — skipping any one gives a write loop or a clobbered document.
  • Adopt, don't rewrite. If a capable frontend exists (npm or OSS), wrap or vendor it; hand-writing an Excalidraw-class editor is neither realistic nor the point.
  • Only claim what you built and verified live in Studio (preview.inspect): the editor renders, the agent's edits appear, the user's edits reach the file.