Skip to content

feat(desktop): add file-backed Excalidraw editing - #84096

Open
kidclone3 wants to merge 15 commits into
NousResearch:mainfrom
kidclone3:feat/desktop-excalidraw-upstream
Open

feat(desktop): add file-backed Excalidraw editing#84096
kidclone3 wants to merge 15 commits into
NousResearch:mainfrom
kidclone3:feat/desktop-excalidraw-upstream

Conversation

@kidclone3

@kidclone3 kidclone3 commented Aug 11, 2026

Copy link
Copy Markdown

What does this PR do?

Adds a native, file-backed Excalidraw workflow to Hermes Desktop. A user can open a .excalidraw document in a real editor pane, manipulate it directly, and let the agent inspect or mutate the same document through guarded tools.

The design keeps authority at the correct boundaries:

  • Electron/backend filesystem code owns validated reads, locking, conflict detection, and atomic persistence.
  • The Desktop renderer owns the mounted editor, pane focus, and presentation state.
  • The agent receives Excalidraw tools only for Desktop sessions through the existing session-scoped desktop_ui toolset; no process environment variable decides whether a GUI capability exists.
  • The file is the shared contract. Renderer and agent changes reconcile through a stable fingerprint and element IDs rather than renderer-private state.

This is intentionally separate from the existing ASCII-to-Excalidraw preview skill in #17720 and the offline tldraw proposal in #66896. Those generate or preview diagrams; this PR adds a persistent, editable .excalidraw document pane with agent tools.

No duplicate was found for the native file-backed Excalidraw workflow. #75457 is adjacent work covering guarded external local-file and obsidian://open link handling rather than in-app document editing. This PR has a narrower .excalidraw interception path that opens drawings inside the native Hermes editor. The PRs touch the same Markdown and Electron link-routing surfaces and may require coordination or rebasing, but neither depends on the other.

Related Issue

None.

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 🔒 Security fix
  • 📝 Documentation update
  • ✅ Tests (adding or improving test coverage)
  • ♻️ Refactor (no behavior change)
  • 🎯 New skill (bundled or hub)

Changes Made

Desktop editor and pane lifecycle

  • Added src/plugins/excalidraw/ExcalidrawPane.tsx, backed by the real @excalidraw/excalidraw editor and loaded lazily with its production stylesheet.
  • Added a drawing controller that loads persisted scene data, serializes edits, coalesces writes, suppresses self-write echoes, and exposes explicit conflict/reload behavior.
  • Routes editor saves through backend compare-and-swap under the same per-document lock as agent mutations, so an editor cannot overwrite a newer agent change.
  • Added Excalidraw pane identity, registration, layout persistence, focus bridging, and cleanup to the existing pane-shell/plugin registries rather than creating a second window or routing system.
  • Preserved editor state across normal pane visibility changes; pane focus determines which open drawing becomes the agent's implicit target.
  • Added an awaitable Electron close barrier for full renderer windows. Pending drawing saves flush before pane/window/app teardown; conflicts, renderer failure, or the bounded 4-second timeout veto close instead of losing edits.

Session-scoped gateway integration

  • Added excalidraw.open and excalidraw.changed event contracts to the shared Hermes protocol.
  • Added focused-drawing RPC state with durable session identity and teardown cleanup.
  • Exposed Excalidraw tools through the existing Desktop UI toolset selected from the session source. Remote/cloud Desktop connections receive the same capability without relying on HERMES_DESKTOP.
  • Forwarded gateway events through the existing JSON-RPC fan-out so the editor and agent observe one ordered event path.

Validated document mutations

  • Added tools/excalidraw_document.py as the file-format boundary for .excalidraw documents.
  • Validates canonical paths and document envelopes before reads or writes.
  • Preserves unknown top-level, element, app-state, and file fields so Hermes does not erase data introduced by newer Excalidraw versions.
  • Normalizes live elements without exposing deleted records as active scene content.
  • Applies add/update/delete operations by stable element ID, rejects invalid patches and collisions, and reports affected IDs.
  • Guards mutations with an expected fingerprint, a per-document lock, a fresh under-lock re-read, and atomic replacement so stale agents cannot silently overwrite newer editor changes.
  • Enforces reference integrity for bindings/groups/container links when an element is deleted.

Agent tools and skill

  • Added open_excalidraw, read_excalidraw, excalidraw_add, excalidraw_update, and excalidraw_delete tool handlers and schemas.
  • Mutating/opening operations use the existing approval boundary before filesystem or Desktop side effects.
  • Omitted paths resolve only through the currently focused Desktop drawing; ambiguous or absent focus fails explicitly.
  • Added the bundled excalidraw skill with the supported tool workflow, fingerprint discipline, mutation guidance, and verification steps.

Regression coverage

  • Added Python behavior tests for parsing, preservation, locking, fingerprint conflicts, mutation invariants, approval ordering, session focus, tool exposure, gateway registration, and RPC cleanup.
  • Added renderer and Electron tests for document control, focus ownership, plugin event handling, editor remount behavior, pane state, gateway boot routing, close-request coalescing, conflict veto, timeout failure, and one-shot successful close retry.
  • Added a real Electron/Playwright regression in apps/desktop/e2e/excalidraw.spec.ts. It mounts the production editor, draws a rectangle, persists it, drags the same stable element ID, verifies changed coordinates on disk, and rejects renderer feedback-loop diagnostics.
  • Added explicit E2E bootstrap/teardown ownership so failed setup cannot leak the mock server, sandbox, or temporary drawing directory, and captured renderer diagnostics remain attached to the original error.
  • Added and locked the exact Excalidraw dependency graph required by the Desktop build.

How to Test

From the repository root, run the focused Python integration coverage:

HERMES_PYTHON=/path/to/python-with-pytest \
  scripts/run_tests.sh \
  tests/hermes_cli/test_web_server_fs.py \
  tests/tools/test_excalidraw_document.py \
  tests/tools/test_excalidraw_tools.py \
  tests/tui_gateway/test_gui_surface_toolsets.py \
  tests/tui_gateway/test_protocol.py \
  tests/tui_gateway/test_session_id_injection.py \
  tests/gateway/test_api_server_runs.py

Expected: 123 passed.

From apps/desktop, verify TypeScript and renderer contracts:

npm run typecheck
npx vitest run \
  electron/window-close-barrier.test.ts \
  src/lib/window-close-barrier.test.ts \
  src/lib/desktop-fs.test.ts \
  src/plugins/excalidraw \
  src/sdk/index.test.ts \
  src/store/gateway.test.ts \
  src/app/gateway/hooks/use-gateway-boot.test.ts

Expected: typecheck passes; 64 tests pass.

Build the packaged Desktop artifact:

npm run build

Expected: renderer, Electron main/preload bundles, build-stamp assertion, and native dependency staging all succeed.

Run the real-editor E2E with the required isolated Desktop environment:

source ~/.hermes/venvs/hermes-dev/bin/activate
export HERMES_DESKTOP_APP_NAME=HermesDev
export HERMES_DESKTOP_USER_DATA_DIR="$(mktemp -d -t hermes-desktop-dev.XXXXXX)"
export HERMES_DESKTOP_HERMES_ROOT="$(git rev-parse --show-toplevel)"
npx playwright test e2e/excalidraw.spec.ts --reporter=list

Expected: 1 passed; the test draws and moves one real Excalidraw rectangle and verifies both persisted states.

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run pytest tests/ -q and all tests pass — focused wrapper coverage passed (123 tests); the full Python suite was not run locally
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform: macOS 15.6 (Apple Silicon)

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) — bundled skill and tool schemas document the workflow
  • I've updated cli-config.yaml.example if I added/changed config keys — N/A, no config keys added
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — N/A, the feature uses existing plugin/toolset contracts
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide — filesystem paths use canonical platform APIs; E2E was exercised on macOS
  • I've updated tool descriptions/schemas if I changed tool behavior — five new Excalidraw schemas are included

For New Skills

  • This skill is broadly useful to most users (if bundled) — it documents the native Desktop drawing workflow
  • SKILL.md follows the standard format (frontmatter, trigger conditions, steps, pitfalls)
  • No external dependencies that aren't already available (prefer stdlib, curl, existing Hermes tools)
  • I've tested the skill end-to-end with a live model invocation — tool, protocol, renderer, and real-editor E2E paths were tested directly instead

Screenshots / Logs

Focused verification on the rebased branch:

Python:    123 passed, 0 failed
Vitest:     64 passed, 0 failed
TypeScript: passed
Build:      passed
Playwright:  1 passed, 0 failed

Wall time: 1.14 seconds

@alt-glitch alt-glitch added type/feature New feature or request comp/desktop Electron desktop app (apps/desktop/*) comp/gateway Gateway runner, session dispatch, delivery comp/tui Terminal UI (ui-tui/ + tui_gateway/) comp/cli CLI entry point, hermes_cli/, setup wizard P3 Low — cosmetic, nice to have needs-decision Awaiting maintainer decision before any implementation sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data labels Aug 11, 2026
@kidclone3
kidclone3 marked this pull request as ready for review August 12, 2026 12:10
@kidclone3
kidclone3 requested a review from a team August 12, 2026 12:10
@kidclone3
kidclone3 force-pushed the feat/desktop-excalidraw-upstream branch from 22311e9 to 8f5163a Compare August 12, 2026 17:29
@kidclone3

Copy link
Copy Markdown
Author
Screenshot 2026-08-13 at 11 32 37 Here is the result

@Enough1122

Copy link
Copy Markdown
Contributor

AI code review — automated review for reference, author can ignore or act on any point.

feat(desktop): add file-backed Excalidraw editing

  • Dependency footprint: @excalidraw/excalidraw@0.18.1 brings a large tree including sass@1.51.0 (2022-era), pica, roughjs, pako, etc. into the renderer bundle. The editor is lazy-imported, which is good, but verify the production bundle-size impact and whether sass can be excluded from the shipped chunk (it is a build-time tool that should not be a runtime dep of the editor module).
  • fs_write_text CAS hashes the existing file with target.read_bytes() — a full in-memory read of the current file, uncapped (only the write content is size-limited). fs_read_text streams the hash in 64KB chunks; the CAS path should do the same to avoid a memory spike when saving against a large drawing/notes file.
  • TreeGroup minimized-retention change: all pane content now stays mounted when minimized (absolute inset-0 invisible), which changes resource behavior for every pane — a minimized terminal or chat surface now keeps running — not just Excalidraw. PaneVisibleContext is set false when minimized so chat subscriptions can gate, but confirm every kept-alive pane actually honors it; otherwise hidden panes keep consuming CPU/network.
  • Minor: store.ts uses single-line if (x) {y} blocks (inconsistent with the repo's prettier formatting) and Array.prototype.with() (ES2023) — fine for the Electron target, but note it if the shared build chain targets older engines.
  • desktopRuntimeIdentity/cache-key format change (old local::… style vs new local/remote:kind:profile:target) invalidates previously cached desktop-FS entries on upgrade; benign (cache miss) but worth confirming no persisted state embeds the old key format.

@kidclone3
kidclone3 force-pushed the feat/desktop-excalidraw-upstream branch from 5e57f15 to 7db0231 Compare August 17, 2026 03:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cli CLI entry point, hermes_cli/, setup wizard comp/desktop Electron desktop app (apps/desktop/*) comp/gateway Gateway runner, session dispatch, delivery comp/tui Terminal UI (ui-tui/ + tui_gateway/) needs-decision Awaiting maintainer decision before any implementation P3 Low — cosmetic, nice to have sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants