Shared whiteboard and document editing¶
Whiteboards and documents live in the workspace artifact store, so every session and room of the project sees the same ones. The room's Shared work section separates creation and import controls from its list of the whiteboards and documents attached to the room's session; creating, importing or editing one attaches it. The open item and local recoveries stay listed; Project artifacts lists the rest of the project. Full and owner links can create, rename, import, edit and delete them concurrently. View links can observe and download. Opening an item affects that browser only; other participants get a followable entry. Below a window width of 1800px each editor opens in its own browser tab, leaving the room and its composer draft in the original tab; from 1800px it docks beside the chat instead (below). The editor tab reconnects independently and reopens its item on reload. The editor tab's header shows the item's title, editable in place by full and owner links, over its save state, such as Saved on host or a save failure; the editor reports both to the tab over its item port and performs the rename. A failed save also marks the editor menu, where Retry save then appears. The back button before the title shows the session in that tab. The host uses the same editor through a browser share link. Browser popup permission is needed to open a new tab. An editor tab shows its loading state immediately. If the item cannot be read, it keeps the requested item and displays an error with Retry; it does not fall back silently to the room. The back button cancels a pending editor opening.
Docked beside the chat¶
In a window at least 1800px wide, opening a whiteboard or document from Shared work, a card or a link, or creating or importing one, opens it in the room's own tab: the chat keeps an 860px column on the left and the editor takes the rest, under the room's header. The editor's head is compact: the back button, the item's title and save state, presence, and Open full ↗, which closes the docked editor and opens the item in its own tab. Its discussions sidebar starts open, on the right, as in the editor tab.
The chat follows the docked item: opening it selects the item's tab in the filter strip, listed even before the item's first turn, so the composer writes to the item's general discussion. The sidebar's row for the whole item reads in chat ← rather than opening a second composer for the same discussion; choosing it selects the item's tab again and focuses the chat's composer. That discussion's news is read in the chat, so the row raises none. Part discussions behave as in the editor tab. The chat's dock, including pending requests, stays in place beside the editor. In this room moves behind the header's room button, which opens it as a drawer over the editor's right side; it holds no news of its own, since requests appear in the chat. The page keeps the room's title, and presence reports the docked item as the page being viewed.
Narrowing the window below 1800px turns the docked editor full screen in the same tab, with its state and drafts; the sidebar's whole-item row becomes the ordinary discussion again. Widening the window does not dock it again; the next opened item docks. Closing the editor returns to the room and keeps the item's tab selected. HTML artifacts open in their panel as before.
From Emacs, o on an item's row in the artifacts cockpit, or clicking a
[title](shared://ID) link in the transcript, opens that item's editor tab in
the default browser with the session's full-control link. The editor exists
only in a room, so for a session that is not shared this first asks, with
/collab's disclosure, to start sharing it; the room then stays up until
/collab stop. The cockpit opened outside a session uses the room of the
item's own conversation, its dedicated session, and the lobby's Artifacts
tab does the same: a whiteboard or document opens in its editor there.
n in the artifacts cockpit or the session menu, or
M-x mevedel-artifacts-new-shared-item, creates one: it asks for Document
or Whiteboard and a title, then opens the new item the same way. The sharing
question comes before anything is created, so declining or cancelling a prompt
leaves no item and no room; a creation that fails starts no room either. From
a session the item is attached to it; elsewhere it opens in its own
conversation. A read-only session refuses before asking, and one that loses
control while the creation is queued creates nothing. Once created, an item is
kept: a failure to attach or open it is reported with its shared://ID
address. Creating records no version.
Every creator -- the browser, the model's SharedCreate and Emacs -- applies
the same rules: the kind is a whiteboard or a document, and the title,
trimmed, has 1 to 200 characters.
Documents use a continuous paper surface, serif body text, and grouped Text, Lists, and Insert menus. Room and editor controls share cool and warm palettes and system/light/dark appearance; changes follow the editor without replacing its document or discussion drafts. The whiteboard keeps a light drawing surface so authored colors remain unchanged.
Discussions opens the discussions sidebar, a sidebar on desktop and a full-width overlay on phones. It starts open on wide screens for documents. Drag its divider to widen it, up to half the viewport; Left/Right arrow keys also resize it, Home/End select the limits, and double-click resets its width. The width is retained with the item's private drafts. Closing the panel preserves drafts, captured context, and editor position. The editor follows the visible viewport above the keyboard; Escape closes the panel, and Ctrl/Command+Enter sends what its box holds. Downloads, recovery, retry, contribution history, and assistant highlighting are in the editor's ☰ menu, with a document's word count. A document's toolbar row holds its formatting, then undo, redo, Discussions and ☰; on a phone the formatting scrolls sideways while those actions stay, the Discussions button shows its icon and count, and the page runs edge to edge. The formatting menus open below their buttons, clear of the scrolling row. Pressing an image selects it for its tools, and closing the crop dialog returns to the document or board.
A whiteboard's Canvas background in that menu offers the theme's board
colour, Excalidraw's white, grey, blue, yellow and rose canvases, and a custom
opaque colour; the model sets it with SharedEdit's background action. The
colour belongs to the board: every participant sees it,
it is an attributed, revertible contribution and follows local Undo/Redo, and
PNG, SVG and .excalidraw exports and the model's board images carry it as
Excalidraw's viewBackgroundColor. Importing an Excalidraw file keeps its
canvas colour. Without a choice the board follows the room theme and exports
stay white. Outlined arrowheads are filled with the canvas colour.
A dark theme shows the canvas, its grid and its elements as Excalidraw does,
through invert(93%) hue-rotate(180deg), which keeps hues; images are
inverted back to their own pixels. The swatches preview the colours as the
board will show them. Stored colours, exports, and the model's board images
stay as authored.
Runtime and build¶
Shared editing requires Node 22.4 or newer on the Emacs host. Set
mevedel-shared-editing-node-program if it is not on PATH. Node stays local
when the session's execution target is remote; Emacs owns every durable
read and write on that target. Ordinary chat, static artifacts, and browser
viewing do not need Node; browser guests never install it.
Shared editing is optional. On connection the browser loads the saved item catalog independently, then asks the host to check the configured runtime, document schema, and packaged PNG renderer. This check creates no saved session or item. If startup or resource loading fails, creation, import, and opening items are disabled with an explanation. Existing items stay listed; open editors and locally stored recovery copies remain accessible.
After installing Node, changing mevedel-shared-editing-node-program, or repairing
helper resources, use Recheck availability in Shared work. The host
starts a fresh helper between queued operations, so repaired resources take
effect without cancelling an edit or discarding drafts. A successful check
reenables controls and reconnects an open editor to host state. Read-only links
can check availability and view items but still cannot edit them.
The minimum includes the switch disabling Node web storage, introduced in Node 22.4. The helper uses Yjs, Tiptap's shared schema, roughjs and perfect-freehand, and packaged resvg WASM with Excalifont, Nunito, Comic Shanns and Noto Sans for headless PNG rendering. It exposes only a private stdin/stdout connection to Emacs. End users receive generated bundles, renderer resources, and third-party notices; they do not run npm installation.
Document state also preserves Tiptap's position-history map for the document
fragment. The binding uses it to keep relative positions through structural
edits. It shares the item's size bound; unrelated shared roots remain invalid.
To rebuild after changing browser/helper source or dependency versions:
npm ci --prefix shared-editing
npm run build --prefix shared-editing
The same build produces the viewer's local Marked/DOMPurify bundle
(relay/viewer/markdown.js) and its license notice. Ordinary Markdown viewing
does not start the shared-editing helper or require Node on the host. Use
Node 26 for development and tests (as in CI); the host helper's minimum
runtime remains Node 22.4.
The board fonts are generated from Excalidraw's font subsets with
python3 shared-editing/fonts.py EXCALIDRAW_CHECKOUT (fontTools and brotli).
It writes the TTFs the host renders with, the WOFF2 files embedded in the
editor bundle, and font-metrics.json, the advance widths every client uses
to measure and wrap text. Rebuild the bundles afterwards.
Commit the lockfile and generated helper/viewer bundles together. The build also assembles license notices; font and renderer licenses accompany their packaged resources. Rebuild the relay to embed changed viewer assets. See dependency maintenance for automated update PRs and the generated-asset checks.
Editing and pointing¶
A whiteboard holds Excalidraw elements and draws them as Excalidraw does (ADR 0121): roughjs strokes and fills from each element's seed, perfect-freehand drawing, and Excalidraw's fonts and text layout. Every browser, the host's PNGs and the exports draw the same wobble.
The whiteboard's controls float over its canvas in islands, as Excalidraw's do: the editor menu (☰) at the top left, the drawing tools centred at the top with the current tool's hint below them, Library and Discussions at the top right, zoom with undo and redo at the bottom left, and the keyboard shortcuts (?) at the bottom right. Where the board is narrower than 1,100 pixels, as beside the sidebar, the tools move to its foot and the hint goes; on a phone-sized board the tools scroll sideways along the foot and ? goes too.
The drawing menu follows Excalidraw: hand (H), selection (V), rectangle (R), diamond (D), ellipse (O), sticky note (N), arrow (A), line (L), freehand drawing (P or X), Shape from drawing (Shift+X), text (T) and eraser (E), followed by the laser (K), Discuss (M) and image (9) tools. Number keys 1–8 and 0 select tools as in Excalidraw. Buttons expose tool names and keyboard shortcuts. Selection supports Shift multi-selection, arrow-key movement, Delete, and resizing. Dragging across empty canvas with the selection tool, including by touch, draws a selection box: releasing selects the objects it fully contains, Alt selects every object it touches, and Shift adds them to the current selection. Connectors are tested by their drawn path. A press without movement clears the selection. The box disappears on release, but its area stays attached to the selection, even when it contains no objects, until the selection changes or an object moves: Discuss uses it, and the area shows again while a new discussion about it is on screen. Box selection never edits content.
Selection follows Excalidraw's handles. A lone element shows an outline with eight resize handles and a rotation handle above it; a group or several elements share one frame with the same handles, and each group is outlined once. Corner handles keep the opposite corner in place and side handles the opposite side, in the element's rotated frame; Shift keeps the proportions, which images and text keep by default. Dragging a text corner scales its font; dragging its side wraps it to that width. Several elements scale from their common frame. The rotation handle turns the selection about its centre, snapping to 15° with Shift. A lone arrow or line shows its points instead: dragging a point moves it, dragging a segment's midpoint adds a bend there, and dropping an arrow end on a shape binds it. Double-clicking a group enters it, so its members select and edit one by one; Escape or clicking outside leaves it. Moving and resizing show a preview while the pointer is held, including selection handles and bound connectors. Geometry previews also travel over the existing presence channel, so other participants see movement before the save round trip. Their status reads Live movement · not saved yet. Releasing commits the geometry through the normal validated save path. Previews never enter content, exports, recovery drafts, or undo history; cancellation restores the committed scene without creating an edit. Commit, cancellation, disconnect, and a five-second expiry remove remote previews. A preview packet contains only up to 100 object IDs and bounding boxes, with the pending operation ID after release. New shapes, text, style, and keyboard edits use normal committed updates. Click inside a shape or a closed line to select it, filled or not; lines have a wider invisible hit area. Double-click a shape or press Enter to edit its label in place, with the label's font, size, alignment and line height; double-clicking empty canvas starts free text there. A label is an Excalidraw text element bound to its container: it wraps to the container, centres in it, moves with it, and grows the container when it no longer fits. Arrows carry labels the same way, with a gap cut into the line behind them. Clicking a label selects its container. Finishing a shape returns to selection with the shape selected, so a double-click edits it instead of creating more shapes. Freehand drawing keeps its tool, as strokes come in runs. The lock button (Q) before the tools keeps any drawing tool active after each shape; it belongs to the browser, not the board. Text is shared while typing; blur or Ctrl/Command+Enter finishes, and Escape cancels if another writer has not changed that text. Finishing with no text removes the label or text element.
Shape from drawing turns a stroke drawn by hand into a clean rectangle, diamond, ellipse, arrow or line when it matches one, using Excalidraw's recognizer; other strokes stay freehand drawings. A finished stroke waits 0.7 seconds for another, and strokes drawn within that pause are recognized together, so an arrow's head may be drawn after its shaft. An unrecognized group keeps each stroke as its own drawing. A recognized arrow binds its start to the shape the first stroke began on and its end to the shape under its tip, wherever the pen lifted. The result takes the current style and is an ordinary edit, so Undo restores the board as it was before the strokes. Strokes smaller than 25 screen pixels are not converted.
The selection panel follows Excalidraw's: it opens at the board's left with a selection, or an area left by a box selection, and on a board at least 1,100 pixels wide also with a drawing tool, for the next object. Its header names the selection and offers Discuss; × closes it until the selection or tool changes. On a phone-sized board it is a sheet over the board's foot, in place of the tools and zoom. It holds the style, the selection's Layers (send to back, bring to front) and Actions (duplicate, group, ungroup, lock and delete), and for a lone image the image tools. The style follows Excalidraw: stroke and background colours, hachure/cross-hatch/solid fill, stroke width, solid/dashed/dotted strokes, sloppiness (architect, artist, cartoonist), sharp or round edges, the arrow type (sharp, curved or elbow), start and end arrowheads (none, arrow, triangle, circle, bar), font (hand-drawn Excalifont, normal Nunito, code Comic Shanns), font size, text alignment, vertical alignment for a label inside a shape, and opacity. It shows only the sections that apply to the selection or the active drawing tool, changes the selected objects and their labels, and remembers the choices for new ones. A curved arrow runs through its points; switching a straight arrow to curved adds a bend to drag. An elbow arrow leaves and enters its bound shapes at the facing side and runs in horizontal and vertical segments between them, rerouted as the shapes move. When a shape stands in the way, the route detours around it with as few bends as it can, leaving and entering its shapes by whichever sides suit the detour; a shape behind either end, such as a lane holding the node, is not in the way. Text, lines, drawings and frames never block a route. Freehand drawing records a pen's pressure for every sample the browser reports; mouse and touch strokes simulate pressure from their speed. Stored line, arrow and stroke points keep 0.1 board units, and a freehand stroke drops samples within one unit of the last one it keeps, retaining its first and last. Pointer input repeats and jitters, and every sample costs storage, sync and model context. Elements stored before this rule keep their points until next written. Elements imported with other Excalidraw values, such as the cardinality arrowheads, keep and draw them. The editor menu adds Select all and Unlock all for the whole board. Duplicate (Ctrl/Command+D), Group (Ctrl/Command+G), Ungroup (Ctrl/Command+Shift+G), Delete, Escape to clear the selection, Enter or a double-click to edit text, and the arrow keys to nudge by 1 or, with Shift, 10 pixels work from the keyboard as well. Clicking any member of a group selects the whole group. A locked element cannot be selected, moved or erased until Unlock all. Duplicates and copies keep the bindings and labels among themselves and drop the others. ? lists the canvas keys. Selection handles retain their screen size as the view changes. Scrolling pans the board, and Shift turns a mouse wheel sideways; Ctrl/Command+scroll and a trackpad pinch zoom at the pointer, in proportion to the scroll. Panning, zooming, the zoom buttons, Fit, and the percentage button (reset to 100%) affect only the local viewport. The board fits existing objects once on opening. Fit and opening keep them clear of the controls floating over the canvas's top and foot, and zoom in no further than 100%, as Excalidraw's fit does. Both whiteboards and documents accept PNG, JPEG, and WebP through the board's image button or Insert → Image…, clipboard paste, or file drag-and-drop. Dropped images land at the board pointer or document insertion position; multiple files insert together and support Undo/Redo. Images are embedded in the item and retained in native exports; document HTML and Markdown exports also carry their image data. External image URLs are not fetched. A board stores each image once as an Excalidraw file named by its content hash; image elements refer to it, so repeated images and contribution history never repeat its bytes. Selecting an image exposes Crop image, Rotate 90°, Flip horizontal, Flip vertical, and Reset image in both editors. Crop opens a shared dialog with draggable edges, arrow-key adjustments (Shift for ten pixels), percentage fields, and a live result preview. Reset crop restores the full source while retaining orientation; Reset image clears both. Apply commits one undoable edit; Cancel leaves the image unchanged. A removed or changed image is reported instead of overwritten when applying a stale dialog.
On a board these are Excalidraw's crop, scale (flips) and angle
(rotation, about the image's centre): the original pixels stay in the file and
the renderer crops them. Cropping keeps the image's display scale. In a
document, the original stays in src; optional imageEdit holds the
normalized crop, quarter-turn rotation, flips, and rendered PNG together as one
attributed shared property, and both count toward image and item limits.
Board PNG and SVG downloads and document HTML/Markdown exports show only the
visible pixels: an SVG download replaces a cropped image with its cropped
rendering. The .excalidraw and native document files keep the original so
reset remains possible. Transformations follow local Undo/Redo and arrive at
other participants through the existing editor synchronization.
Arrows bind to shapes as in Excalidraw. A bound end in orbit mode sits on the
target's outline, offset by 5 + half its stroke width, toward the binding's
fixed point; inside mode places it at the fixed point. Bound ends follow moves
and resizes of their targets. Moving an arrow away from its targets unbinds
the ends whose targets stay behind. The editor and PNG/SVG exports share this
geometry; a binding to a deleted element is drawn unbound.
Element library¶
Library opens the sidebar's Library tab, beside Discussions; each toggle closes its own tab. It offers the libraries kept on the session host: My library, installed libraries, and mevedel's Built-in library. Each library is a collapsible section; My library starts open, the others closed, and the editor remembers which sections a participant opened while it stays loaded. The search field matches item names and library names across every library, opening the sections with matches. Clicking an item inserts copies with fresh identities at the centre of the view, selected for moving; groups, labels and bindings within the item are kept. The sidebar stays open beside the board; on a phone, where it covers the board, it closes to show the inserted item. The libraries load when the tab first shows.
Each library is an .excalidrawlib file in mevedel-shared-library-directory,
by default ~/.mevedel/whiteboard-libraries/, named after the file. Add
selection stores the selected objects and their labels in My library;
Import file… adds the items of an .excalidrawlib file there;
Download saves it. Items are removed with ×. Copying any Excalidraw
library file into the directory installs it as well. The libraries belong to
the host's Emacs, not to a session or board: every writable participant in
every room, and the assistant, sees the same libraries. Inserted items are
ordinary elements, so a board carries them without its libraries, as an
Excalidraw scene does. View links have no library.
An invalid or unsupported installed file reports its own error in the browser
and at shared://library; other libraries remain available, with the same
item numbering in the model's listing and preview sheet.
Browse public libraries lists the collection at
libraries.excalidraw.com with a search
field, sorted by downloads, recent updates, age or name. Opening one previews
its items; Install library saves it in the directory, where it stays
available in every whiteboard until the Remove button inside its opened
section, which names the library, removes it. Installed libraries are removed
whole: they mirror a published file, so useful items are kept by inserting
them and adding them to My library. The editor has no network
access, so Emacs fetches the index, download counts and library files from
mevedel-shared-library-catalog-url, and only library files listed there.
The built-in Database item replaces the former cylinder shape: a group of
an ellipse and lines, as Excalidraw has no cylinder element.
Ordinary board cursors move one named arrow per participant. Selecting Laser pointer makes mouse or pen hover visible locally and to collaborators; on a touchscreen, press and drag to point, then lift to stop. A bright tip, a compact name tag, and a short curved trail distinguish deliberate pointing from an ordinary cursor. Older trail segments fade independently within about half a second; a stationary tip stays visible while the participant points. Holding the mouse button, pen, or touch while pointing draws laser ink instead: the line stays for two seconds and fades over the next, so a participant can sketch a circle or arrow on top of the board without editing it.
Coordinates travel in board space and render through each receiver's viewport.
Remote cursors use a short transition from the displayed position to each
received sample, without predicting past it. Laser packets retain up to 64
input samples from the last 550 ms, rather than only each packet's last position.
The observer plays those samples with a short 55 ms delay, preserving curves
between network updates. Each sample is [x, y, age, ink]; held samples carry
an increasing ink index, which lets observers accumulate a line longer than one
packet's window. Each press skips an index, so separate strokes do not join. A stale gap starts a new pointer
instead of drawing a bridge across the board. Reduced-motion preferences remove
trails and interpolation. Leaving the canvas, switching tools, cancellation,
hiding the page, and disconnect clear the local pointing state; item changes
and peer departure clear remote state. Idle presence expires if its occasional
refresh is lost. Motion coalesces to at most 20 updates per second and retains
the final position. Presence is discarded behind queued content/control traffic;
it creates no revision, undo history, export marks, queued question, or model
context.
Working with the assistant¶
The model reads shared items with Read and Grep at shared:// addresses and
changes them with SharedCreate and SharedEdit. Every role has Read; the
editing tools are discoverable through ToolSearch/ToolCall to implementation
and worker roles. Their summaries name whiteboards and documents, and
ToolSearch's shared key separates them from artifacts, so a requested
whiteboard finds SharedCreate. The prompt's resource roster lists
shared:// once the project has an item.
| Address | Read returns |
|---|---|
shared:// |
the project's items with kind and title, marking those attached to the session, and shared://library |
shared://ID |
a header with kind, title, revision and the related addresses, then one line per element in drawing order or per top-level block in document order |
shared://ID/elements/ELEMENT |
one element or block in full, structurally formatted with lossless long-string chunks |
shared://ID/view.png |
the board rendered, long edge at most 2048 px |
shared://ID/images/KEY |
an embedded image: a board file by its fileId, a document image by the key its src shows |
shared://ID/discussions |
the item's discussions, the general one first, with their status, target, live quote and messages |
shared://ID/history |
retained contributions, newest first, with their changed ids |
shared://library, shared://library/NAME |
element library items as LIBRARY/ITEM-ID references |
shared://library[/NAME]/sheet.png |
a numbered sheet of the first 60 of those items |
An overview line is HASH JSON: a 12-character content hash of the element or
block, then its JSON. The hash is computed over sorted keys, so writers' key
order does not matter. Lines stay within Read's 2,000-character cap: past
1,800 characters a stroke's points and pressures become a count with the
element's address; other oversized entries become explicitly abbreviated
summaries pointing there. An abbreviated entry is not replacement JSON.
The full-element view formats nested structure and stroke points across lines.
Long string values or keys use the output-only string-chunks(...) notation:
decode its ordered JSON string lines and join them without separators before
passing the value to SharedEdit. Its header explains this notation; it never
changes stored content, image addresses or the hash. Long discussion and
contribution lines use the same lossless chunks. Document images appear as
shared://ID/images/KEY, never as image data. Text pages through Read's ordinary offset and limit and its
50 KiB output bound, so an item of any size can be read in parts. Grep searches
complete values while returning addresses and physical line numbers of ordinary
Read output. An item's overview, or bare shared://, also searches the existing
full-element resources behind abbreviated entries. Matching joins string-chunk
display boundaries internally, then maps each hit and its context back to the
actual Read lines, including all lines touched by a cross-chunk match. Images go through Read's ordinary media
delivery, so models without image support get its explicit refusal. Glob and
ApplyPatch do not accept shared://. Views are computed by the workspace's
editing host from committed state, without a browser.
Creating or editing an item attaches it to the model's session. When a turn that created or changed items settles, each of them gets a version in the store; an edit that changed nothing does not count.
SharedCreate creates a named item and returns its address. SharedEdit
applies patches, inserts library items, adds images, renames, sets a board's
background, or reverts a contribution from shared://ID/history. A patch change is
{id, hash, set, unset, after, afterId}:
setmerges fields into the current element or block andunsetremoves fields; the result is validated whole.afterreplaces the element or block;nulldeletes it.- A new element or block gives
afterwithout a hash. - Document blocks may give an
afterIdinsertion anchor, which may name a block inserted earlier in the same patch; insertions sharing an anchor keep patch order, each followed by those anchored to it. Their imagesrcvalues may keep theshared://ID/images/KEYform, which the host restores to the image data.
The host compares every given hash, or its absence, with the current target.
Any mismatch rejects the whole patch and returns each stale target's current
hash and line. Unrelated changes do not invalidate an otherwise valid patch.
Board elements use Excalidraw's field names; absent fields take Excalidraw's
defaults, labels are text elements with a containerId, and connections are
arrows with startBinding/endBinding. The derived Excalidraw fields
version, versionNonce, updated, isDeleted and boundElements are
refused. An image element in a patch must reference an existing file; the
image action adds a new one. insert places a library item with its
top-left corner at x, y as new elements.
Images in and out¶
The image action takes one source: a file path or a resource address the
model can read (work://, artifact://, or a shared://ID/images/KEY or
shared://ID/view.png of any item). It is authorized as a read, like Read, and
not as an edit (see permissions). The tool reads the bytes and
sends them to the host, which decides the format from the bytes, not the name:
PNG, JPEG and WebP are stored as they are, and an SVG is rasterized to a PNG at
twice its intrinsic size, the long edge at most 2,048 px, because boards and
documents hold only those three raster formats; the item still shows it at its
intrinsic size. The source file may be up to 4,700,000 bytes; the
16-megapixel, 8,192-px-per-side and 32-megapixel-per-item limits apply, and a
failure names the size or limit and what to change.
A board image gets a content-addressed file id, the SHA-1 of its stored bytes,
as a browser upload does, so identical bytes are stored once, and an element at
x, y (give both or neither; the default is below the existing content). A
document image becomes a block
after afterId (default: the end) with the optional alt. The display width
is the image's own up to 640 px unless width is given; the height keeps the
aspect ratio. The addition is one attributed contribution, the file is
retained with it, and Revert removes the image and a Revert of that Revert
restores it. The result lists the new element or block line and, for a board,
the resulting PNG.
SharedSaveImage goes the other way: its source is a shared://ID/images/KEY
or shared://ID/view.png address and its file_path a new file, which keeps the
image's own format and gets its folders created. It refuses an existing file
instead of overwriting it, which keeps Rewind exact, and is an edit tool, so
Plan refuses it and the destination needs write authority. Copying ordinary
files is Bash's job.
An edit result names the item, its revision, address and contribution, the deleted ids, and the stored lines of the elements it changed, including derived changes such as a label's container growing. Past 24 KiB of lines, the rest are reported as ids and hashes. It does not repeat the board. Whiteboard edits also return a PNG of the resulting canonical revision through the normal tool-media path, so the model can inspect the visual result without a separate read or a connected browser. Tools use normal permissions, Plan and read-only ceilings, cancellation, and result/media persistence. They work with every browser closed and never silently start a share.
Discussions in the editor¶
Whiteboards and documents follow the room's discussion model and its shared discussions sidebar. Discussions opens the sidebar's Discussions tab on the list: the whole item first, then the open discussions with their pin number, then the resolved ones folded into one row. The tab and the Discussions button count the open discussions, and the button shows a dot while news in another discussion is unread. A status line under the tabs reports the session's queued, paused, working, disconnected, or provider-failure state, and stays away while there is nothing to report. While the session is working, a spinner accompanies the status and the panel's toolbar button. The room chat shows Assistant working… in its status strip. These indicators follow the host's session activity, including tool execution, and clear on completion or disconnection. Reduced-motion preferences disable the spinning animation. A paused follow-up queue does not conceal a running turn. Posted messages and assistant answers are shared with the room; drafts stay in the current browser's recovery storage. View participants can read them.
Discuss (Ctrl/Command+Alt+M) opens a new discussion about the current selection: selecting document text shows it beside the passage, a board shows it in the selection panel, and the list's foot offers it, as Discuss selected passage, Discuss 2 selected objects or Discuss selected area. The new discussion names the Selected passage, Selected objects or Selected area and quotes it; the passage or area stays highlighted while it is on screen, and Refresh attaches the content as it is now. Start discussion posts it and keeps it open. Ask assistant, checked by default, also sends the message to the assistant; the choice is kept for the next message. A discussion's box replies to it, Show passage or Show on board shows its target, and its ⋯ menu holds Ask assistant again, which sends the discussion as it stands, and Resolve or Reopen. When a request needs attention, the discussion offers Review updated context, Send to assistant for reviewed context, or Retry for an unconfirmed request. The discussion about the whole item writes to it and takes file attachments. A message to the whole item asks about the item as it is when sent; it carries no reviewed capture, since none is shown. Each discussion's draft, the new discussion and the sidebar width survive receiving updates, closing the sidebar and reopening the editor; going back to the list drops a new discussion. The box starts at two lines and grows with its text, and no size depends on focus, so pressing a button never moves it. An unrecognized saved discussion-draft format is reported and reset without blocking recovery of document content or posted discussions.
In every discussion message box, Enter inserts a newline and Ctrl+Enter or ⌘+Enter submits through the sending button. Its tooltip shows the platform's shortcut, including when starting a discussion or replying to one.
Sending a selection without a message¶
A selection can be sent with no message, on a board or in a document: what is written in the selected content is then the request, for example a handwritten “draw it” beside a formula, or “Rephrase this paragraph” typed above the paragraph. A new discussion says so in its placeholder when Ask assistant is checked. A message to the whole board or document still needs text, because it has no instruction of its own, and so does a discussion that does not ask the assistant.
The browser sends the empty message and the host words it, so the room sees, and the model receives, the same line in place of a message; the discussion shows Sent the selection as the request. The line says the sender sent only the selection, tells the model to carry out the instructions written in it and, when there are none, to ask what the sender wants. This overrides the “user-provided data” label of the snapshot for the selection itself; the unselected content in the snapshot stays data, so another participant's text becomes an instruction only when someone selects and sends it. Afterwards the instruction text is removed from a document, where it would pollute the content, and left on a board unless it says otherwise. A written message always replaces the host's line. Once the turn completes, the discussion resolves itself.
What the assistant receives¶
A request carries the discussion's latest message, which is the visible request in the room and to the model, with the discussion so far in the shared context; unposted reply text never travels. Queue and delivery status appear in the discussion. Another AI request requires another message sent to the assistant or an explicit Ask assistant again. The host checks that the discussion has not moved on since the sender reviewed it, as well as its target; a concurrent reply requires sending again. Failed delivery retains its request identity for retry.
Clicking a document highlight opens its discussion; Show passage scrolls back to the live passage. Resolving hides its highlight.
A document discussion keeps its original quote and a collaboration-aware text anchor. The discussion marks a passage changed or removed after later edits. A changed passage requires Review updated context before sending; a removed passage cannot be sent. The immutable snapshot of an already sent request remains unchanged. In the editor sidebar, room transcript, and Emacs view, sent requests keep their authored text visible and collapse the snapshot under Shared context by default. Expand it to inspect the full sent context and attachment links; the summary names the item, scope, and revision. This affects display only, not what the model receives. Prompts edited on the host remain fully visible. Discussions are item annotations: native, document and board exports contain the content, not the discussions or conversation. A fresh import starts without them.
A whiteboard discussion is about objects, a board area, or both. Select objects or box-select an area and choose Discuss, or use the Discuss tool (M): it outlines the object under the pointer, discusses a clicked object, and discusses a dragged area together with the objects it contains. The tool stays active for further discussions; Escape returns to selection. Every open discussion shows a numbered pin at the top-right corner of its target, following moves live. Hovering a pin outlines its target and shows its latest message, message count, and whether its objects changed; clicking it opens the discussion in the sidebar. A ring turns around a pin while the assistant works on its discussion, the same way as for artifact discussions, and a dot marks news not yet opened. Resolved discussions have no pin, and pins renumber in order of the open discussions. Show on board selects the discussion's surviving objects and area and frames them. Pins, peeks and discussions are visible to view participants, who cannot post.
A board discussion records its object IDs, optional area, quote, and a
fingerprint of those objects and their labels. The quote names labelled
objects and counts unlabelled ones by type, such as freedraw ×24. In a
discussion, a request sent to the assistant shows only its sender and shared
context, since its text is the latest message. The discussion is
changed once an object moves, restyles, changes its label or is deleted, and
removed when none of its objects remain and it has no area. An area
outlives its objects. A request to the assistant attaches the target's surviving
objects and its area, subject to the same review of changed context; the host
accepts only objects from the discussion's own target and its exact area.
A box selection is discussed as the Selected area: it carries
the box's board region with the objects it contains, including an area with no
objects, so a request such as “put a legend here” names a place. Unselected
objects the area touches accompany it as context with their labels; images
appear by file reference, and their pixels only in the attached PNG, which
shows the area with a small margin and is scaled up to four times so small
areas stay legible. Selected objects bring their labels and the shapes their
arrows connect as context.
A private document highlight keeps a new discussion's passage visible while it
is on screen; closing the sidebar removes that highlight and reopening it
shows the passage again. The quote shows what will be sent before submission.
Opening controls, typing, and toggling the sidebar retain that capture.
Pending edits must save first. The browser sends a content hash of the capture
it showed, and the host compares it with a capture of its committed content; a
concurrent change rejects the request with an explicit Refresh context
recovery. It never substitutes a whole item for a missing selection. No
selection is too large to discuss. The model receives the reviewed content
as the hashed lines it reads at shared://ID, introduced by the item's address
and revision. A document selection gives its text and the blocks it spans; a
board selection or area gives its elements, then its labels, connected and
nearby elements. Past a 128 KiB budget the remaining lines are counted and the
model is told to read on at shared://ID. The discussion so far follows the
content, and the board PNG accompanies it as before.
Each document and whiteboard has its own model conversation. A request receives its reviewed content snapshot and recent turns about the same item, including requests from every one of its discussions. Other items and ordinary room chat are excluded. Ordinary room requests and room compaction summaries likewise exclude item discussion turns. The room transcript still shows their shared chronology, and lists each existing item as a discussion of its own: selecting it in the room shows its turns and sends room messages into its conversation as whole-item questions about the committed content (see scoped prompts). This is context selection, not a privacy boundary or a separate execution agent. Questions keep the session's permissions, tools, queue, and checkpoints, so they can edit content. Directive discussions retain their separate read-only contract.
When broader context matters, the assistant can search history://root with
Grep and read the matching line range with Read. This includes unsaved room
turns; history://saved covers archived segments. The prompt advertises those
resources, and the model chooses whether to retrieve them from the request's
meaning. No keyword trigger or automatic full-room attachment runs. For example,
“Make this consistent with our earlier architecture decision” can prompt a
search followed by reading the relevant exchange. An unavailable decision must
be retrieved or clarified, not assumed to have been included.
Canonical transcript segments remain the only conversation store. On subsequent requests, same-item turns are restored from archived segments and deduplicated by question identity against the live tail. The request includes up to 128,000 characters of prior complete turns, newest first in selection and chronological in presentation; omitted older history is explicitly disclosed and remains retrievable. The budget is applied while reading history: once the next unique item turn exceeds it, older segments are not opened. The current question and its archived duplicates do not consume that budget. Sparse item history can still require scanning all segments before reaching the budget. Retry identity checks remain exhaustive so old accepted questions cannot be submitted twice. The current question is retained separately. Item requests do not compact the unrelated room to make space. Current snapshots, tools, and provider context limits still apply; a large single turn may need a narrower question. The editor restores recent archived discussion on opening, reconnecting, or transcript replacement/removal, and reports truncated or unavailable history. Failure to read a needed archive prevents a model request from silently losing context, but does not prevent editing the item. Archives older than an already established truncation point are not consulted. Ordinary requests without trusted item attribution skip item-related transcript classification.
Accepted questions include item identity, title, committed revision, exact selected text or elements, the selected board area, bounded surrounding document blocks, labels, connector endpoints or touched neighbours, and a matching board PNG as a normal attachment. Questions use the ordinary queue with guest attribution; correlation metadata is persisted as model-invisible transcript audit data. Provider failures appear in the conversation. If the host edits a queued question, the panel explicitly marks it Edited on host and shows the revised input; it no longer claims that the original attachment is what the model received. Disconnected or rejected submissions retain the draft and request identity. Retrying an accepted question finds its queued or delivered record across browser reconnects, preventing a second turn. Editing the question or refreshing context creates a new identity; an intentional follow-up is a new question. Drawing, typing, posting without Ask assistant, and synchronization never start model turns automatically.
Durability and recovery¶
An item is <workspace>/.mevedel/artifacts/.state/ID/state.json, with meta.el
naming its kind, title and revision, and versions/. Catalogs read these small
metadata records without parsing the item's CRDT state or history.
Emacs serializes operations per
workspace in one queue, which every room and session of this Emacs shares,
validates candidates in the helper, checks continuing authority, and commits
before acknowledging Saved on host. Consecutive queued edits of one item
(updates, patches, inserts, renames and background changes) commit together:
each is computed from the state the previous one left, and up to 16 are
written by one commit, then acknowledged and announced in order. When an edit
continuing such a batch fails, the batch commits on its own. A batch commits
only while none of its edits was cancelled or lost its authority and this
Emacs still holds the item's lease as it did when the batch began; otherwise
every edit in it is reported unsaved, since each was computed from the one
before. Batched edits count toward the queue's limits. Under several
writers the commit, one fenced target program, otherwise made edits wait for
each other's commits. Embedded images travel with their item's
state. Ordinary read-only artifact addresses gain no new mutation capability.
Requests and helper replies arriving during a remote file operation retain
their continuations until the transport is idle, so TRAMP's temporary timer
lists cannot strand the workspace queue.
Across Emacs instances, an item lease decides who may commit
(mevedel-artifact-lease.el). It lives outside the store, under
.mevedel/leases/artifacts/ID/, so the store stays safe to commit. It uses
the session lease's generation records, target clock and expiry
(mevedel-session-lease-seconds): the first edit in an Emacs acquires it,
a timer renews it, and every state write proves ownership and expiry in the
same target program. Lease transitions, state and metadata writes, and item
deletion share a target-side flock on the stable
.mevedel/leases/artifacts/ directory, so a takeover cannot interleave with a
commit. The execution target must provide flock.
These file operations are serialized, but are not a crash-atomic multi-file
transaction. A failed program reports a save failure and can leave an incomplete
new item or state and catalog metadata at different revisions until retried.
After mevedel-artifact-lease-idle-seconds without edits, or when Emacs
exits, the lease is released. Another Emacs that tries to edit a held item
gets a read-only refusal and asks the holder to hand it over; the holder
releases it to that Emacs at its next renewal once its queue for the item is
idle and nobody has edited the item there for 10 seconds, so a person still
drawing keeps it. A request that is not repeated within one lease period
lapses, so a requester who walked away does not take the item later. A
lease whose holder stopped renewing, such as a suspended laptop's, is taken
over by the next edit, from Emacs or a browser, and Emacs says so. Each write
replaces the whole item under the lease, so a late write from the old holder
fails instead of overwriting.
While the lease is held and was renewed within the last minute, an edit does
not read the target clock again. While this Emacs still holds the lease it
last committed under, and renewed it within one lease period, an edit or read
starts from that commit instead of reading the item back: the lease fences every state write, so one fenced target program
per edit remains. Metadata has other writers, so the commit verifies it and,
when it changed meanwhile, rereads it and retries once. Reading needs no lease. An open editor that
receives a change skipping a revision, as after a hand-over between two Emacs
instances, rereads the whole item instead of applying only that change. If
that read fails, the editor says so and reads again with the next change.
Versions follow the store's caps. A version keeps the item's content, title
and discussions but not its receipts or contribution history. One is recorded
through the item's editing queue, after the saves before it, when a model
turn that changed the item settles, and on Save version (the
cockpit's s, or the store list in a room or the lobby). Restoring a version
is one ordinary, attributed edit through the queue: lineage, discussions and
history stay, concurrent editors receive it as an update, and the restore can
itself be reverted. The most blocks of a document that are the same in the
version and still in its order keep their identity, so discussions anchored in
them stay current, even around a moved block; the others are copied whole
from the version. Whiteboard restores update retained objects in place, so a
pending browser edit to an unchanged object still merges. Removed objects
retain the normal deletion-wins behavior.
Sessions only attach to items, so Resume, Save As, Fork and Rewind neither copy nor roll them back. Closing a browser or ending the share leaves the host original intact. A later share uses fresh credentials, following ADR 0114.
An editor sends a local edit as soon as no save of its own is awaiting the host's reply. Edits made while one is in flight join a single follow-up operation, sent when the reply arrives. Consecutive sends start at least 50 ms apart, so a slider or color-picker drag against a fast host does not commit, and spend an operation receipt on, every frame.
The viewer retains pending changes in browser storage, scoped to room and item. Reauthentication in the same logical share merges them. Disconnected, rejected, or storage-failed edits remain visibly pending and have a Recovery copy download. A recovery copy is explicitly named and represents that browser's content, including unsynchronized changes. It is not a host save. Storage quota errors ask the user to download before closing. Retry save rereads committed state and retries operation identities without duplicating content. A host storage failure leaves the item unchanged until a retried save succeeds. After a reload, locally retained items remain available as local recovery entries even when their share or host item is gone. They open read-only for recovery download until the host supplies valid current content and authority.
Deleting¶
Delete whiteboard/document… in the editor's ☰ menu, or d on its row
in the Emacs artifacts cockpit, deletes an item for everyone: its content,
embedded images, discussions and contribution history. There is no undo; the
browser's confirmation offers Download a copy, which can be imported
again. The deletion runs in the workspace's editing queue, so it lands after
any save in progress, and removes the item's whole artifact: versions, its
lease and its dedicated session. An editor showing the item in
another browser closes and names who deleted it; a later save to it fails
with "This item no longer exists". A browser keeps its local draft of a
deleted item only while it holds edits that never reached the host, listed
as a local recovery. The transcript keeps the item's discussion: its turns stay
in the room under All, their chip marked deleted, but the item loses its
tab, so nothing can be sent into a conversation about an item that no longer
exists. A browser showing that tab returns to All. A deleted HTML artifact's
discussion goes the same way. The room lists an item's tab only once the host
has listed its items, so a deleted item's tab does not reappear on reload. The
model cannot delete items.
Downloads and imports¶
Standard downloads use a committed revision: PNG/SVG or an Excalidraw file for
whiteboards, and Markdown/HTML or an editable native JSON file for documents.
The Excalidraw file (.excalidraw) is a complete Excalidraw scene: every
element field, fractional z-order indices, derived reverse references, label
and connector positions as displayed, and the image files in use. It opens in
excalidraw.com and other Excalidraw editors. Native and Excalidraw files
preserve supported content, relationships, and embedded assets. They exclude
credentials, presence, collaboration history, and undo stacks. Markdown and
images are viewable conversions with less editable structure.
Whiteboard PNG and SVG downloads use an opaque white background independent
of the room theme, without the editor grid or temporary highlights. SVG
downloads embed the fonts their text uses. SVG labels use individually
positioned text lines, preserving blank lines, and arrowheads use explicit
paths so Qt-based viewers retain the same text layout and arrows.
Import supports .excalidraw scenes (also as .json), the native document
format, Markdown/plain text documents, and raster images, which open as a new
whiteboard holding that image. It validates the complete input before creating
a new item and collaboration lineage, preserving the original. An Excalidraw
scene goes through Excalidraw's restore rules: legacy fields migrate, deleted
and invisible elements and unknown element types are dropped, and unusable
values take Excalidraw's defaults. Images in formats other than PNG, JPEG and
WebP are drawn as placeholders, and the room reports them after import.
Library files are imported in the editor's Library. Uploaded HTML,
JavaScript, external image URLs, and unsupported formats are rejected. There
are no legacy readers or migrations; whiteboards saved in mevedel's former
shape format no longer open.
Bounds are explicit: 16 MiB canonical state/native input, 4,000 elements and 200 image files per board, 4,000 points per stroke, 8 MiB per library, 200 targets per agent transaction, and 1 MiB document JSON excluding embedded image data, with bounded depth and node count. Documents allow up to 12 MiB of embedded image data within the canonical state limit. Raster images are limited to 16 megapixels each and 32 megapixels per item; browser image uploads are at most 4 MiB, and a file the assistant adds at 4,700,000 bytes. Recent revert snapshots are bounded by both count and bytes as described above; they expire before older snapshots block further saves. The receipt ledger has a 65,536-operation ceiling. A native export/import begins a fresh history when that ceiling is reached. A question's model context is held to 128 KiB, with the remainder named by address rather than refused. Transfers use bounded chunks; host and viewer queues also bound aggregate input size. Browser responses carry CRDT state or its update and compact contribution metadata. Full before/after snapshots stay on the host for Revert; image bytes are not repeated through the contribution list on every broadcast.
The packaged editor has an opaque iframe origin and an item-scoped message
port. CSP forbids networking and submitted forms while permitting local
editor dialogs and the board fonts embedded as data (font-src data:). Room credentials stay in the trusted viewer. The relay stays
content-blind. ADR 0120
records the change from model-only authorship to direct shared editing.
Checks¶
npm test --prefix shared-editing
npm run test:browser --prefix shared-editing
npx @emacs-eask/cli clean elc
npx @emacs-eask/cli test ert test/test-mevedel-shared-editing*.el test/test-mevedel-shared-library.el test/test-mevedel-tool-editing.el test/test-mevedel-collaboration-editing.el
npx @emacs-eask/cli test ert test/test-mevedel-tool-permission-read-access.el test/test-mevedel-pipeline-read-access.el
MEVEDEL_TEST_SHARED_EDITING=1 timeout 600s ./test/run-remote-acceptance.sh
The focused editor checks reproduce interior selection, inline text, cursor
replacement, local/remote laser trails, grouped contributions, export-neutral
assistant highlights, frozen context, stale/offline refusal, discussions with
and without the assistant, selections sent alone, reply rendering, disclosure
continuity, box selection, area discussions, board discussion pins, and a small
keyboard-sized viewport. The editor fixture stands in for Emacs's discussion
actions and records what the assistant would be asked.
test/board-tools.browser.mjs covers shape recognition, the library (with a
stand-in for the host's library requests), groups, locking, duplicates,
arrowheads and fonts; test/test-mevedel-shared-library.el covers the library
file and public collection through a local HTTP server.
The room scenario also checks new-discussion draft recovery across repeated reloads. Set
MEVEDEL_EDITOR_SCREENSHOTS=1 when running test/editor.browser.mjs to save
preview screenshots under .scratch/shared-collaborative-editing/.
The focused room design and navigation checks also run with
MEVEDEL_BROWSER=firefox after installing Playwright Firefox with
shared-editing/node_modules/.bin/playwright install firefox.
The room browser scenario checks the separate tab and preserved drafts,
as well as caret visibility after a mobile viewport resize. It uses a real
relay and isolated Emacs, multiple Chromium contexts, and a deterministic participant invoking actual native tools. It
requires Go, Emacs dependencies installed by Eask, and Playwright Chromium
(shared-editing/node_modules/.bin/playwright install chromium). The remote
command provisions the repository's temporary SSH/container fixture.