Files
pastilhasandClaude Fable 5 a0349cc7a3 the officer shell: designs per user, and the handoff desk
manifest, schema, router and two panels. the editor is hosted and
saving; the last step is not finished and the readme says so.

designs are rows scoped per user, with userId in every WHERE rather
than checked after the read — another person's design is 'not found',
so the id space cannot be probed. the Doc is stored whole as jsonb
because upstream adds fields to it constantly and none of them should
be a migration here.

the handoff is the reason the plugin exists and it is the part that
does not work: /api/chat has NO session-creation route, a session is
constructed by the browser opening a websocket, so a plugin has nothing
to POST to. onExport returns false and the clipboard still runs, which
is upstream's own behaviour — degraded, not broken. docs/handoff.md
names the one platform change that would finish it, and argues it
should be generic rather than ours.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-05 12:16:35 +01:00

111 lines
5.7 KiB
Markdown

# Canvas
Sketch a Material 3 screen, and hand the prompt to the agent.
The editor is [M3E Canvas](https://github.com/lnkiai/m3e-canvas) by lnkiai, MIT, vendored under
`web/vendor/`. Officer contributes none of the drawing, none of the Material 3 vocabulary and none of
the prompt wording. **What Officer contributes is the last step**: upstream's export button writes the
prompt to your clipboard to paste into a coding tool, and here it should open a chat session with the
prompt already in it.
**Status: the editor is hosted and saving. The handoff is not finished** — see below, and
`docs/handoff.md` for exactly why.
---
## What works
- The full editor, in a workspace panel, drawn in its own Material 3 vocabulary.
- **Designs belong to a person, not a browser.** Upstream keeps one document in `localStorage`; here
they are rows in `m3e_canvas_documents`, scoped per user, listed in a side panel, and they follow
you between machines.
- Saving is automatic and debounced, and a pending save survives closing the panel.
- Selection is `?doc=<id>` in the URL, so a design is addressable, bookmarkable and survives a reload.
## What does not
**The handoff.** `POST /_handoff` holds the prompt and hands back a `chatUrl`, but **nothing reads it
yet**, because `/api/chat` has no session-creation route — a session is constructed by the browser
opening a websocket, so a plugin has nothing to POST to. `onExport` therefore returns `false` and the
editor still writes the clipboard, which is exactly upstream's behaviour. Degraded, not broken.
Finishing it is one small change in `/chat/new`, and it is a platform change rather than this
plugin's to make. `docs/handoff.md` argues it should probably be generic rather than ours.
**Nothing has run.** Both halves build and the platform discovers all four parts, but no route has
answered a request, `db:push` has not been run so the table does not exist, and nobody has drawn
anything. Treat every claim above as "written and compiled".
---
## The vendoring, and why it is scripted
```
./scripts/vendor.sh [ref] # default: main
```
One command reproduces `web/vendor/` from nothing: clone upstream, rewrite imports, apply our host
wiring, write `PROVENANCE.md`. **Never edit anything under `web/vendor/` by hand.**
Three things make this less obvious than it looks.
**The import rewrite is a correctness requirement, not tidiness.** Upstream imports through a `@/`
alias, and this platform already binds `@/*` to `src/apps/officer-web` and `@/components/*` to its own
shared components. An unrewritten `import { M3Node } from "@/components/M3Node"` resolves to
**Officer's** directory — a wrong module rather than a build error, which is the worst way to be
broken.
**Our changes are a script, not a `.patch`.** Between `cfde698` and `5c83ac6`, one day apart, upstream
changed an import line, re-indented a 3,000-line component from 6 spaces to 8, and added an
`editAccess` guard. A context diff fails on all three, none of which concern us. So
`scripts/apply-host.py` anchors on structure with flexible whitespace, and **stops loudly** when an
anchor genuinely disappears. It did that twice during the first build, correctly both times.
**Everything else goes through `web/adapter.ts`.** No Officer file imports `web/vendor/` directly. When
upstream renames something, the repair is one file rather than every panel.
## Upstream, and why we vendor
The adapter was offered upstream first — [issue #55](https://github.com/lnkiai/m3e-canvas/issues/55)
and [PR #56](https://github.com/lnkiai/m3e-canvas/pull/56) — and declined, for two reasons worth
recording. The PR wired one of three export paths and so did not actually unblock embedding, which was
our error. And embedding is not on their roadmap and is "a single-user need so far", which is fair.
They also said the adapter's design is sound and that they would start from it if that changes, and
named what a complete version needs: the `Canvas` extraction plus **every** export path. Both are done
here. **The door is open in principle**, and the thing that would change their mind is this plugin
existing and working — a second user is a better argument than a better patch.
### What we do not use
`lib/ai.ts` and `components/AiPanel.tsx` are vendored but never surfaced. They keep model-provider API
keys in `localStorage` and call the provider from the browser, which is not how anything here should
work. They were deleted in the first attempt, which turned out to mean editing vendored code —
`Canvas.tsx` references them throughout — so they stay, inert without a key. A faithful copy carrying
an unused module beats a doctored one that cannot be re-vendored.
### Three export paths, not five
A scan for `clipboard.writeText` finds five. Two are `ShareMenu`'s share-link and its "ask an AI"
blurb, which are not the generated prompt and do not belong behind `onExport`. Written down because
the count looks wrong at a glance, and the last round of this work was rejected for wiring one path
out of three.
---
## Files
```
manifest.ts one grantable permission; /_handoff is owner-only
db/schema.ts m3e_canvas_documents — the Doc stored whole as jsonb
api/router.ts documents CRUD, scoped per user in every WHERE; the handoff desk
web/adapter.ts the ONLY door to vendored code
web/host.ts our host module — upstream declined it, so it is ours
web/CanvasPanel.tsx the editor, given storage and an export sink
web/DesignsPanel.tsx the list — the one concept upstream has no notion of
web/vendor/ upstream, do not edit
scripts/vendor.sh re-vendor, one command
scripts/apply-host.py our changes to vendored code
docs/handoff.md why the last step is unfinished, and what would finish it
```