M9: online files: the app remembers the files it put online

A file put on a server without the server's key stays its owner's after its
tab closes or the app quits. The app records it with the preferences (server,
id, name, owner token, the owner's links, what others join with; at most 200;
validated when read back) and never shows the token: not in command results,
ui.inspect, logs or debug output.

- Home > Online lists your online files: open one as its owner (a new tab, or
  its tab comes to the front), start and stop sharing, copy the code or link,
  rename on the server, remove from the server (confirmed), forget here. One
  request per server when the section opens; offline and gone states.
- collab.files, collab.openFile, collab.forgetFile; collab.manageFile takes
  its server and token from the record and gains share / unshare.
- Server: Admin owned (a compatible addition within protocol version 2) says
  what became of the files a list of owner tokens owns. Older servers leave
  the list as recorded.
- Tabs mark files in a live session (shared, or open only to you); toasts
  about a file that isn't in front name it; after a crash a recorded file
  reopens from its server as its owner.
- Draft catalogs (de, fr, ja, ko, pt-BR, zh-Hans) translate the 71 strings
  and 5 command labels they lacked and the new ones; es and es-419 follow.
- Docs: collab-protocol, server, mcp, control-protocol, parity pages, gaps,
  ROADMAP.
This commit is contained in:
bflatastic
2026-10-13 01:41:14 +07:00
committed by BFlat
parent 517b1a02ce
commit 501e4a6737
43 changed files with 3554 additions and 130 deletions
+66 -6
View File
@@ -15,7 +15,9 @@ standalone `figcraft-server` (`apps/figcraft-server`, running it: [`server.md`](
many files with persistence and compaction, and share links that let people edit or only view;
the app publishes to it, and a file's team library lives there for other files to use. Files are
shared with **join codes** (see *Sharing with a join code*): online through a server that names
its links with short codes, or on the local network with a code that carries the host's address. Sessions
its links with short codes, or on the local network with a code that carries the host's address.
A file put on a server stays its owner's: the app records it with its owner token and lists
**your online files** (see *Your online files*), which open again as their owner. Sessions
are per document: an app hosts, joins and publishes several files at once (see Hosting and
joining). The rest is the plan the next implementation PRs follow and update;
sections say what is built.
@@ -191,6 +193,13 @@ before join codes can't read either message and refuses the connection ("that me
read"): the app then says the server doesn't know join codes (joining) or shows the invite links in
place of a code (sharing). An older app never sends them and joins by link as before.
**Owned files** came later within version 2 the same way: `Admin owned` on an open hosting
connection asks what became of the files a list of `(file id, owner token)` pairs owns, in one
request. It is a new variant at the end of `Admin`, and its answer one at the end of the replies,
so everything an older peer reads is unchanged. A server from before it can't read the request and
refuses the connection ("that message couldn't be read"): the app then shows the files it has
recorded as they are, without their state on the server. An older app never sends it.
**Encoding (decided):** a binary message is a 1-byte wire version followed by **postcard** (serde),
which is compact and needs no schema files. Property values inside are compact JSON text: the
document types use tagged and flattened serde forms that only self-describing formats can read
@@ -222,6 +231,7 @@ Client → server:
| `Plugins {list {query} \| fetch {id, version?} \| publish {bundle} \| unpublish {id, version?}}` | A request on a plugin directory connection; each gets one `Plugins` reply, in order. `bundle` is a plugin bundle's bytes (`figcraft_plugin::bundle`). |
| `Resolve {protocol, client_version, code}` | Instead of `Hello`: ask which file a join code opens (the code's nine symbols). Answered with `Resolved`, then the server closes the connection; a code it doesn't know, too many wrong ones from the address, or a session hosted by an app is `Refused` with the reason. |
| `Admin {code {file, token}}` | A later managing request (with the server key, or the file's owner token on an open hosting connection): a new join code for the link of `file` whose token this is. A link has one code; asking again replaces it. |
| `Admin {owned {files: [[file, owner]]}}` | A later request, on any open hosting connection (also one that named no file when it opened): what became of the files these `(file id, owner token)` pairs own, at most 200 a request. Answered with `Admin owned`. |
Server → client:
@@ -242,6 +252,7 @@ Server → client:
| `Plugins {ready {publisher} \| list [entry] \| bundle {entry, bundle} \| published entry \| removed [version] \| failed {reason}}` | A plugin directory connection's answer: opened (with the key or not), the latest version of each plugin with the versions kept, a bundle and its entry (its SHA-256 among them), kept, unpublished, or why not. |
| `Resolved {file, token}` | The answer to `Resolve`: the file the code opens and its link's token, which the client then joins with in an ordinary `Hello` on a new connection (so reconnects, access and revocation are the link's). |
| `Admin {code {code}}` | The answer to `Admin code`: the link's new join code, shown this once (the server keeps only what it needs to recognise it). |
| `Admin {owned [{name, links, expires?} \| null]}` | The answer to `Admin owned`, one entry per pair asked about, in order: the file's name on the server, the ids of its share links (the owner's own and the ones made for others) and its expiry; `null` when that token manages no file of that id (removed, expired, never there, or a wrong token: they read the same). |
`Change` is one of: `Create {id, type, props}`, `Set {id, prop, value}`, `Delete {id}` (deleting a
node deletes its subtree; ids are never allocated twice), plus the document-level `SetDoc`, `Put`
@@ -544,6 +555,50 @@ resuming with its offline edits, and leaving the session going back to snapshot
- **In the browser** a code joins like a link (the page asks the server over its own WebSocket,
then opens the session's); sharing needs the desktop app.
*Built* (your online files; `figcraft_engine::online`, `collab.files`, `collab.openFile`,
`collab.forgetFile`, `collab.manageFile`):
- **What is recorded, and where.** When a file reaches a server without the server's key
(`collab.share` online, `collab.publish` on a server open to the public), the server's `hosted`
answer carries its owner token, once. The app records the file with the preferences, per user
and never in a document: the server's address (without a query), the file's id and name, the
owner token, the owner's own edit and view links, when it was put there and last opened here,
and the links and codes others join with while it is shared. At most 200 files, most recently
opened first; recording another drops the one opened longest ago. The preferences are saved at
once when the list changes, so a crash doesn't lose a token. A file published with the server's
key has no owner token and is the operator's to manage: it isn't recorded.
- **The token is a password.** It is stored on this computer only (the desktop app's preferences
file; the browser build keeps no preferences and can't publish), is never logged, and no
command result, summary or `ui.inspect` carries it or the owner's own links: `collab.files`
says `hasOwnerToken: true`. The record's debug output leaves the secrets out. A session opened
from a record holds no token either (`published.owner` is null there): requests that need it
read it from the record. What is read back from the preferences file is validated entry by
entry (addresses, ids, links, lengths, control characters); junk is dropped without costing
the other preferences.
- **Opening one again** (`collab.openFile {file, server?}`): the app joins with the owner's edit
link in an ordinary `Hello`, so reconnects and sync are a guest's, and the session is marked as
the owner's of that server file: `collab.stop` and `collab.share` there stop and start its
sharing with the recorded token, exactly as in the session that published it. A file that is
open already comes to the front instead. After a crash, the desktop app reopens a recorded file
that was open and in step with its server this way instead of from its recovery copy (the copy
is kept only as a fallback when the record is gone).
- **What became of them** (`collab.files {refresh: true}`): one `OpenHosting` connection per
server that names no file, then one `Admin owned` with every file recorded on that server.
A file the server answers `null` for shows as gone (removed or expired) and can be cleared; a
name changed elsewhere is taken over; share links that are no longer among the file's links
mark it as not shared. Nothing is asked until the list is opened (Home ▸ Online) or refreshed
by a command. A server that can't be reached, or is older than the request, leaves the records
as they are and is reported beside them (`problems`).
- **Managing from the list.** `collab.manageFile {file, action}` takes the server and the token
from the record when they aren't given: `share` makes new links and codes for others, `unshare`
revokes the recorded ones (as `collab.stop` does in a session), `rename` renames the file on the
server and in the record, `remove` deletes it from the server, drops the record and ends a
session open on it (the copy stays open). `collab.forgetFile` drops the record only: the file
stays on its server until it expires, and can't be managed from this computer again.
- **Asking opens nothing.** `Admin owned` reads the files' descriptions only: it doesn't count as
opening a file, so looking at the list never postpones a file's expiry. Each pair is checked
like an owner token on `OpenHosting` (its SHA-256 against the stored hash, in constant time).
*Built* (document sync between apps):
- The host's session runs the core's `Server`, seeded with the document it shared; the host's
@@ -786,8 +841,9 @@ saved state.
## Agents, MCP and the CLI
Everything stays a command: `collab.share`, `collab.host`, `collab.join`, `collab.publish`,
`collab.leave`, `collab.peers`, `collab.follow` are ordinary commands, so the CLI, the control channel and MCP can
host, publish and join sessions; an agent can be a participant with its own cursor.
`collab.leave`, `collab.peers`, `collab.follow`, `collab.files`, `collab.openFile`,
`collab.forgetFile` and `collab.manageFile` are ordinary commands, so the CLI, the control channel and MCP can
host, publish and join sessions and manage the files they put online; an agent can be a participant with its own cursor.
*Built*: the desktop app (control channel) and the MCP server's headless session (`figcraft-cli
mcp`) both have the network link installed; `collab.join` returns at once with status
@@ -854,7 +910,10 @@ chooses to share on the network; the default is the invite link with a random to
everyone; refusals say which. Spoofing `X-Forwarded-For` does nothing unless the connection comes
from the trusted proxy.
- **Open hosting** gives anyone who reaches the server the right to publish (within those limits),
to report a file they are in, and with an owner token to manage that one file. Everything else
to report a file they are in, and with an owner token to manage that one file and to ask what
became of it (`Admin owned`: a wrong token and a missing file are answered alike, so the request
tells nothing about files someone doesn't own; it counts against the address's message limit
like any other). Everything else
(listing files, other files' links, reports, blocks, the plugin directory's publishing) needs the
server key. Files are not end-to-end encrypted: the server reads what it keeps, and so can its
operator.
@@ -893,6 +952,7 @@ chooses to share on the network; the default is the invite link with a random to
5. **Standalone `figcraft-server`** with persistence, compaction, share links and permissions.
**Done** (`apps/figcraft-server` on the shared hub; edit and view links, the server key,
publishing from the app; see [`server.md`](server.md)), followed by open hosting for a public
server (owner tokens, limits per address, idle expiry, reports and blocks) and by sharing with
join codes (online through the server, or carrying the host's address on the local network).
server (owner tokens, limits per address, idle expiry, reports and blocks), by sharing with
join codes (online through the server, or carrying the host's address on the local network)
and by your online files (the app remembers what it owns and opens it again as its owner).
6. **Accounts**: people signing in, per-person permissions, teams and projects.
+2 -1
View File
@@ -15,7 +15,7 @@ cursors, selections, text carets, cursor chat and spotlights shown live ([`colla
| Real-time co-editing | yes | **core built, no networking** (#103): client-scoped ids, per-property change sets with fractional positions, server ordering and validation, optimistic clients, binary messages; randomized multi-client convergence tests over real commands. Live sessions over the network landed (`crates/collab-net`): the WebSocket transport (keep-alive, size and rate limits, reconnect with backoff), hosting from the app (this machine or the local network) with invite links, live cursors with name tags, selections, avatars and follow, `collab.*` commands. Document sync between apps works over them (the host's server orders every batch; guests open the host's file as a new tab). Sessions are per document: each open file has its own or none, several run at once (host one, join another, publish a third) and keep syncing behind the tab in front, and closing a file ends its session only. Undo is per person (only your own steps, others' later writes left alone), refused edits roll back with a toast, and a reconnect resumes as the same client with what it missed (or a snapshot) and sends what was made offline. The browser build joins (same messages through the browser's WebSocket API); `figcraft://` invite links open the desktop app and join. Styles, variables, collections, modes and comments made in a session take ids from their creator's space (the server checks it), so concurrent creations all survive; modes, per-mode values and replies inside one entry still resolve last-writer-wins. A standalone `figcraft-server` keeps many files, persists them (a snapshot plus a log, compacted), loses nothing confirmed across a restart (apps resume as the same participants) and takes files published from the app (File ▸ Publish to server…). The desktop app and the CLI open `wss://` links too (TLS through a reverse proxy), and releases ship the server for Linux with a deployment guide (systemd, Caddy, nginx, Docker) and `install.sh`. A server can be open to the public: anyone publishes without the key, within limits per address, and files nobody opens expire. Left: server archives beyond Linux | 25–55 |
| Live cursors, selections, viewports; follow / observation mode; spotlight | yes | **done, in live sessions**: named cursors, selection outlines, avatars, follow (click an avatar), others' text carets and selected characters with their names, a note when two people type in one layer; spotlight (your avatar's menu, `collab.spotlight`): everyone follows your view and page, people who join later too, anyone who moves their view stops following and a banner offers to follow again, a newer spotlight takes over. Left: polish (no accounts, so anyone in the session can spotlight) | 2–4 |
| Cursor chat, audio, high-fives | yes | **partial, in live sessions**: cursor chat (`/` opens a bubble at your pointer; everyone sees it as you type, it fades a few seconds after the last change, Enter sends and Escape takes it back, 100 characters; `collab.cursorChat` for agents); sent as ephemeral presence the host checks and rate-limits, never stored. No audio, high-fives or emote reactions | 2–5 |
| Share links, permissions (view / edit), teams and projects | yes | **partial**: sharing with a join code from the Share popover (online: the file goes to the server in Preferences and the server names its edit and view links with short codes; local network: the app hosts and the code carries its address; one field joins with a code or a link, in the popover and on the home screen); `figcraft-server` share links per file with edit or view access (a viewer sees changes and presence, a "View only" badge and only the moving tools; the server refuses its edits), revocable at once, tokens stored as hashes; managed from the app's Share popover (first links), the server's command line or a managing connection. On a server open to the public, whoever published a file manages it with its owner token (links, name, removal: `collab.manageFile`, no app UI yet), anyone in a file can report it, and the operator blocks addresses. No accounts, teams or projects | 8–17 |
| Share links, permissions (view / edit), teams and projects | yes | **partial**: sharing with a join code from the Share popover (online: the file goes to the server in Preferences and the server names its edit and view links with short codes; local network: the app hosts and the code carries its address; one field joins with a code or a link, in the popover and on the home screen); `figcraft-server` share links per file with edit or view access (a viewer sees changes and presence, a "View only" badge and only the moving tools; the server refuses its edits), revocable at once, tokens stored as hashes; managed from the app's Share popover (first links), the server's command line or a managing connection. On a server open to the public, whoever published a file manages it with its owner token (links, name, removal: `collab.manageFile`), which the app remembers: Home ▸ Online lists your online files (open again as owner, start and stop sharing, copy the code, rename, remove, forget), with one request per server for what became of them; anyone in a file can report it, and the operator blocks addresses. The list is per computer: no accounts, teams or projects | 8–17 |
| Comments: pins on frames and layers, threads, reactions, @mentions, resolve, unread, search and filters, links, in the presenter | yes (shared) | **done, local**: stored in the file, your name from Preferences; a behaviour suite from the documentation (pinning and following layers, threads, own edits and deletes, mentions, reactions, C and Shift+C, stepping, filters, saving, copy and paste); restoring a version keeps them. In a live session threads sync and anyone in it can reply, react and resolve (no accounts: a name is who you are) | (shared once multiplayer lands) |
| Version history: named versions, autosave checkpoints, view before restore, restore, duplicate | yes (cloud) | **done**, saved in the `.figcraft` file: restoring keeps the replaced design as an autosave first, a titled autosave becomes a named version, merging a branch records the file before and after. Versions are per person in a live session (a shared history needs accounts); no comparing two versions (Dev Mode compares a ready-for-dev layer with its state when marked) | — |
| Branching and merging | yes (paid plans) | **done, local**: branches saved in the `.figcraft` file, each opened in its own tab; Review and merge shows both sides' changes since the branch was made and the conflicts (the same property on both sides, or a layer deleted on one side and edited on the other), each resolved for the branch or main; merge as one undo step (the branch is archived, the main file's history gets a version for the merge), update from main, archive. Each branch keeps its own version history (saved, viewed and restored on the branch tab, kept when it is merged or archived); fills, strokes and effects merge item by item. Left: branches in live sessions; other lists (text runs, export settings) conflict as a whole | 1–2 |
@@ -26,6 +26,7 @@ cursors, selections, text carets, cursor chat and spotlights shown live ([`colla
| Date | Change | Summary |
|---|---|---|
| 2026-10-12 | minor | Your online files (M9): a file put on a server without its key stays its owner's after its tab closes or the app quits. The app records each one with the preferences (server, id, name, owner token, the owner's links, the codes others join with; at most 200; the token never in a command result, `ui.inspect`, a log or debug output) and Home ▸ **Online** lists them: open one as its owner (a new tab, or its tab comes to the front), Start / Stop sharing, Copy code / Copy link, Rename on the server, Remove from server (confirmed), Forget (the file stays on the server). Nothing is asked of a server until the section opens; then one request per server (`Admin owned`, a compatible addition within protocol version 2) says which files are still there, and a server that is offline or older leaves the list as recorded. Tabs of files in a live session carry a mark (shared, or open only to you), toasts about a file that isn't in front name it, and after a crash a recorded file reopens from its server as its owner. `collab.files`, `collab.openFile`, `collab.forgetFile`; `collab.manageFile` takes its server and token from the record and gains `share` / `unshare`. Files are known per computer, not per person (no accounts): percentages and hours unchanged |
| 2026-10-12 | minor | Sharing with a join code (M9, from the owner's use: sharing wasn't intuitive): the Share popover is one flow, **Online** (the file is published to the server in Preferences, built in `wss://collab.craftapps.dev/`, and the server names its edit and view links with nine-symbol codes like `K7M-2QX-9TD`) or **Local network** (the app hosts; a 20-symbol code carries its IPv4 address, port and a 44-bit secret, with check bits), one Start sharing button, the code large with Copy code and Copy link, who is here, can edit / can view online, and Stop sharing (online: the others' links and codes are revoked and they are disconnected, while the file stays on the server with its owner in it and can be shared again with new codes; removing it is a separate, confirmed step); **Join with a code** is one field (popover, home screen, Join dialog) for a code of either kind or a link; addresses, keys and ports moved under Advanced and Preferences ▸ Server for online sharing (reset, or empty to turn online sharing off). Protocol additions within version 2 (`Resolve` / `Resolved`, `Admin code`): the server keeps a stretched hash and the wrapped token, never the code, and limits wrong codes (10 a minute per address, a 15-minute lockout after 10 wrong, 1,000 wrong an hour for everyone); codes go with their link and file. Older servers leave the links in place of a code; older apps join by link. `collab.share`, `collab.join {code}`, `code` and `share` in `collab.peers`; the browser joins by code. Usability and hosting rather than a new capability: percentages and hours unchanged |
| 2026-10-12 | minor | Live sessions are per document (a fix from real use: after hosting or joining in one tab, a new or other file showed that session, and starting one there silently replaced it). Every open file has its own session or none; several run at once and keep syncing behind the tab in front; the Share popover, the people, the cursors, cursor chat, spotlight, follow and the read-only reason are the file in front's (each window its own); the `collab.*` commands act on the active file's session and `collab.peers` lists all of them (`sessions`); joining opens the session's file in its own tab without touching other sessions; closing a file ends its session only. One link carries the sessions, each under its own id (desktop, browser and tests). Percentages and hours unchanged |
| 2026-10-12 | minor | Public `figcraft-server` hardened against resource exhaustion after a security review: messages capped by what a connection is for (a file's size when open), byte-bounded send queues and a bound on bytes waiting for the hub, files checked as they grow (refused batches roll back with the reason; per-address and total caps count growth), the core's recent-batch memory bounded in bytes, no confirmation for changes that couldn't be written, team libraries only in key-published files on public servers with unpacking bounded by the file cap, usage reports kept in memory, layered IPv6 limits (/64, /56, /48), a global publish rate, connections reserved for sessions, per-address download shares, library connections cut on revoke and removal, a private data directory and key, and safer install.sh, systemd and backups. Percentages and hours unchanged |
+11 -3
View File
@@ -111,7 +111,14 @@ document can't be edited now (`readOnly`: the "View only" or "Publishing" reason
session itself is `engine.execute {command: "collab.peers"}` (sharing with a join code, hosting,
joining and publishing are `collab.share`, `collab.host`, `collab.join` and `collab.publish`, opening invite links `collab.registerLinks`,
see [mcp.md](mcp.md#live-sessions)). The app also joins the invite link it
was started with (`figcraft "figcraft://join?…"`).
was started with (`figcraft "figcraft://join?…"`). `online` is the home screen's Online section:
whether it shows (`open`), your online files as its rows read (`files: [{file, name, detail,
shared, state, hasOwnerToken}]`; never a token or a link of yours), whether the servers are being
asked (`checking`), how many couldn't say (`problems`), and the file whose removal is being
confirmed or renamed (`confirmRemove`, `rename`; Escape closes them); the list itself is
`engine.execute {command: "collab.files"}`. `tabs` lists the open files as the tab strip shows
them: `[{name, live}]`, `live` being `"shared"` (in a live session others get into), `"private"`
(a file of yours on a server, open only to you) or null.
### `ui.pointer {kind, x, y | sx, sy, to?, steps?, shift?, alt?, cmd?, ctrl?, pressure?, pressureTo?}`
@@ -169,10 +176,11 @@ separate calls. Returns `{queued}`.
Invoke a menu item exactly like clicking it (this may open a dialog). Returns `{ok, lastError}`.
### `ui.set {rightTab?, leftTab?, theme?, home?, palette?, mainMenu?, focusRegion?, colorPicker?, variables?, layerFilter?, collapseLayers?, exportPreview?, preview?, presentScroll?, colorProfileDialog?, stylePicker?, stylePickerEdit?, stylePickerView?, iconGallery?, protoEditor?, contextMenu?, contextSelectLayer?, commentOpen?, commentDraft?, commentFilter?, commentSort?, commentSearch?, commentReply?, commentEdit?, presentComments?, devCompare?, devCompareMode?, devCompareSplit?, annotationFilter?, resetFrameStats?, pluginsWindow?, pluginsTab?, pluginConfirm?, pluginForm?}`
### `ui.set {rightTab?, leftTab?, theme?, home?, homeSection?, palette?, mainMenu?, focusRegion?, colorPicker?, variables?, layerFilter?, collapseLayers?, exportPreview?, preview?, presentScroll?, colorProfileDialog?, stylePicker?, stylePickerEdit?, stylePickerView?, iconGallery?, protoEditor?, contextMenu?, contextSelectLayer?, commentOpen?, commentDraft?, commentFilter?, commentSort?, commentSearch?, commentReply?, commentEdit?, presentComments?, devCompare?, devCompareMode?, devCompareSplit?, annotationFilter?, resetFrameStats?, pluginsWindow?, pluginsTab?, pluginConfirm?, pluginForm?}`
Switch panels and modes: `rightTab` `design|prototype|inspect`, `leftTab` `file|assets`, `theme`,
`home` (bool), `palette` (bool: the Actions palette), `mainMenu` (bool, or a top-level menu such
`home` (bool), `homeSection` (`recents` or `online`: which section the home screen shows; opening
`online` asks each server once what became of your online files), `palette` (bool: the Actions palette), `mainMenu` (bool, or a top-level menu such
as `"Plugins"` to open the main menu with it open), `pluginsWindow` (bool: Plugins ▸ Manage
plugins…), `pluginsTab` (`installed|browse`: that window's tab), `pluginConfirm` (a plugin id of the
directory listed last: its install confirmation shows, as its Install button would; `null` closes
+4 -1
View File
@@ -85,7 +85,9 @@ No alpha blockers: the alpha gate in [`roadmap.md`](roadmap.md#alpha-gate) passe
styles, variables and comments that several people create at once all kept; sessions are per
document (each open file has its own or none, several at once, closing a file ends only its
own); files are shared with a short join code (online through the server in Preferences, or on
the local network with a code that carries the host's address) and joined by typing it; a standalone
the local network with a code that carries the host's address) and joined by typing it; the app
remembers the files it put online and lists them on the home screen (open again as owner, stop
and start sharing, rename, remove, forget), per computer; a standalone
`figcraft-server` keeps many files (snapshot plus log, compacted; restarts lose nothing
confirmed), takes files published from the app and hands out share links that let people edit
or only view; the desktop app and the browser reach it over `wss://` through a TLS reverse
@@ -390,6 +392,7 @@ No alpha blockers: the alpha gate in [`roadmap.md`](roadmap.md#alpha-gate) passe
| Date | Change | Summary |
|---|---|---|
| 2026-10-12 | minor | Gap 2: your online files landed (the app records the files it put on a server with their owner tokens, Home ▸ Online lists them and opens them again as their owner, with sharing, rename, removal and forget from the list; one request per server tells which are still there; tabs mark files in a live session). What is left is unchanged: accounts, teams and per-person permissions, shared comments and versions. Hours unchanged |
| 2026-10-12 | minor | Gap 2: sharing with a join code (Share ▸ Online or Local network, one Start sharing button, a short code to give others, one join field for a code or a link; the server names links with codes and limits wrong ones) added to the landed list; usability, so the estimate and ranking are unchanged |
| 2026-10-12 | minor | Gap 3: first measured fidelity data. Text box sizes and auto layout results of our example documents were compared with the same documents re-saved by other software; three text rules (whole-pixel line heights, advances on a 1024-per-em grid, letter spacing between glyphs only) were found and applied, and all 230 self-sizing text boxes in 7 documents now match to the pixel (one font family). Real files are still unchecked: hours and the ranking unchanged |
| 2026-10-12 | minor | Gap 2: live sessions are per document (each open file has its own session or none, several run at once, the window and the `collab.*` commands follow the file in front, closing a file ends its session only) added to the landed list; a fix, so the estimate and ranking are unchanged |
+9 -8
View File
@@ -78,17 +78,17 @@ supported by both apps (CJK, Arabic and Devanagari shaping, IME); Figma has no v
| Language | Code | UI strings translated | Dialogs / tooltips / help | Script support | Native review | Status | Hours to full |
|---|---|---|---|---|---|---|---|
| English | en | 1,982 / 1,982 (100%, source) | yes | Latin | yes (source) | full | 0 |
| Simplified Chinese (Mandarin) | zh-Hans | 1,982 / 1,982 (100%, draft) | yes | CJK text and IME in documents; UI text in an installed Chinese face, ahead of Japanese ones (web: the `FIGCRAFT_EMBED_CJK` build); no Traditional Chinese catalog (zh-TW, zh-HK read English) | no | draft | 1–2 (after native review) |
| Spanish | es, es-419 | 1,982 / 1,982 (100%, draft); es-419 overlays 90 Latin American wordings (draft) | yes | Latin | no | draft | 0–1 (after native review) |
| English | en | 2,076 / 2,076 (100%, source) | yes | Latin | yes (source) | full | 0 |
| Simplified Chinese (Mandarin) | zh-Hans | 2,076 / 2,076 (100%, draft) | yes | CJK text and IME in documents; UI text in an installed Chinese face, ahead of Japanese ones (web: the `FIGCRAFT_EMBED_CJK` build); no Traditional Chinese catalog (zh-TW, zh-HK read English) | no | draft | 1–2 (after native review) |
| Spanish | es, es-419 | 2,076 / 2,076 (100%, draft); es-419 overlays 98 Latin American wordings (draft) | yes | Latin | no | draft | 0–1 (after native review) |
| Hindi | hi | 0 (0%) | no | Devanagari shaping works in documents; UI font fallback to check | no | none | 4–7 |
| Arabic | ar | 0 (0%) | no | Bidi, shaping and RTL names work; the UI does not mirror | no | none | 9–15 |
| French | fr | 1,982 / 1,982 (100%, draft) | yes | Latin | no | draft | 1–2 (after native review) |
| Portuguese | pt-BR | 1,982 / 1,982 (100%, draft) | yes | Latin | no | draft | 1–2 (after native review) |
| French | fr | 2,076 / 2,076 (100%, draft) | yes | Latin | no | draft | 1–2 (after native review) |
| Portuguese | pt-BR | 2,076 / 2,076 (100%, draft) | yes | Latin | no | draft | 1–2 (after native review) |
| Indonesian | id | 0 (0%) | no | Latin | no | none | 3–5 |
| Japanese | ja | 1,982 / 1,982 (100%, draft) | yes | CJK text and IME in documents; UI text in an installed Japanese face (web: the `FIGCRAFT_EMBED_CJK` build) | no | draft | 1–2 (after native review) |
| German | de | 1,982 / 1,982 (100%, draft) | yes | Latin; long words elide in narrow fields | no | draft | 1–2 (after native review) |
| Korean | ko | 1,982 / 1,982 (100%, draft) | yes | Hangul text and IME in documents; UI text in an installed Korean face (web: the `FIGCRAFT_EMBED_CJK` build) | no | draft | 1–2 (after native review) |
| Japanese | ja | 2,076 / 2,076 (100%, draft) | yes | CJK text and IME in documents; UI text in an installed Japanese face (web: the `FIGCRAFT_EMBED_CJK` build) | no | draft | 1–2 (after native review) |
| German | de | 2,076 / 2,076 (100%, draft) | yes | Latin; long words elide in narrow fields | no | draft | 1–2 (after native review) |
| Korean | ko | 2,076 / 2,076 (100%, draft) | yes | Hangul text and IME in documents; UI text in an installed Korean face (web: the `FIGCRAFT_EMBED_CJK` build) | no | draft | 1–2 (after native review) |
| Vietnamese | vi | 0 (0%) | no | Latin with stacked diacritics | no | none | 3–5 |
Other languages: none shipped (Figma ships none beyond the eight above).
@@ -107,6 +107,7 @@ Vietnamese and Arabic. Traditional Chinese is not planned (no catalog; its users
| Date | Change | Summary |
|---|---|---|
| 2026-10-12 | minor | Drafts catch up again, and the counts with them: French, German, Brazilian Portuguese, Japanese, Korean and Simplified Chinese translate the 71 UI strings and 5 command labels they lacked since sharing with a join code and open hosting (the Share popover, join codes, owner tokens, reports, the .fig export notes), and with Spanish the 26 strings and 3 command labels of your online files (Home ▸ Online, its menu and dialogs, the marks on tabs); machine-drafted in each file's conventions, still drafts. The Latin American overlay adds computadora and agrega where the new Spanish says ordenador and añade (98 entries). 2,076 entries per catalog (1,503 UI strings, 544 command labels, 29 menu wordings) |
| 2026-10-12 | minor | Draft catalogs catch up: French, German, Brazilian Portuguese, Japanese and Korean translate the 66 UI strings and 17 command labels added since they were drafted (cursor chat and spotlight, the plugin directory, Rename layers, presentation and snapping keys, list levels, the rotation origin, placing with Enter…), Simplified Chinese the 46 and 12 it lacked; machine-drafted from the English in each file's conventions, still drafts (not in `COMPLETE`). The Latin American overlay adds Control for a gamepad controller and presiona in the plugin directory hint (90 entries, with Enter for the keycap). String counts re-measured: 1,417 UI keys, 536 command labels and 29 menu wordings (1,982 entries), every draft at 100%. Localization grade unchanged |
| 2026-10-12 | minor | Simplified Chinese draft catalog (gap 5): `zh-Hans.toml` translates every UI string (1,371 keys; plurals give only `other`, and `plural_one` never picks "one"), command label (524) and menu wording (29), machine-drafted in a concise UI style with full-width punctuation and marked as a draft awaiting native review; Preferences ▸ Language lists 简体中文, System maps zh, zh-CN, zh-SG and zh-Hans to it, and Traditional Chinese (zh-Hant, zh-TW, zh-HK, zh-MO) has no catalog and stays English (or the next OS language), never Simplified. A Chinese UI draws Han in a Chinese face first (Microsoft YaHei, PingFang SC, Noto Sans CJK SC), ahead of the craft-fonts build's Japanese BIZ UDPGothic, and changing the UI language lays the CJK faces out again (switching from Japanese to Chinese no longer keeps the Japanese face in front). String counts re-measured: 1,371 UI keys, 524 command labels and 29 menu wordings (1,924 entries); the French, German, Brazilian Portuguese, Japanese and Korean drafts have 1,899 of them (the 25 newer strings read English). Localization 36% → 39%, 31–54 h → 29–51 h |
| 2026-10-12 | minor | Latin American Spanish overlay (gap 5): `es-419.toml` lists only the 87 entries (61 UI strings, 26 command labels) whose wording differs in Latin American usage (agregar for añadir, presionar for pulsar, configuración for settings, Enter for the Intro key, control for a gamepad, resorte for a spring) and reads `es.toml`, then English, for the rest (a regional catalog overlays its language's); same tú register as Spanish; a draft awaiting native review. Preferences ▸ Language lists Español (Latinoamérica); System maps es-419, es-MX, es-AR, es-CO and the other Latin American and US Spanish tags to it, es-ES and es to Spanish. A regional catalog's lookups (UI strings, command labels, menu wordings) go overlay → base language → English; the check fails on an overlay entry that reads as in its base. Localization 35% → 36%, 32–55 h → 31–54 h |
+40 -7
View File
@@ -927,8 +927,9 @@ only: a host stops hosting it, a guest leaves it.
shared: false}`: the others' links and codes are revoked and they are disconnected at once (with
their copy), while you stay in the file, which stays on the server, open only to you
(`share.shared` is false, `code` null); `collab.share {}` then shares the same file again with
new codes, `collab.leave {}` leaves it there, and `collab.manageFile {action: "remove"}` with
`published.owner` takes it off the server.
new codes, `collab.leave {}` leaves it there, and `collab.manageFile {file, action: "remove"}`
takes it off the server (the file is recorded with your online files: see `collab.files`
below).
- `collab.host {port?, bind?}` starts hosting the active document (a session that document was in
ends; other documents' sessions go on): `bind` is `"local"` (this
machine, the default), `"lan"` (the local network) or an interface address; the port is 47900,
@@ -969,11 +970,43 @@ only: a host stops hosting it, a guest leaves it.
server says about its hosting, before publishing (`open`: no key needed; `idleDays`: how long
files published without the key are kept unopened, null when they are kept). A server older
than open hosting is reported as taking files only with its key.
- `collab.manageFile {server, file, owner, action, …}` manages a file published without the key,
with its owner token: `links` → `{links: [{id, access, label, created}]}`; `createLink {access?:
view|edit, label?}` → `{id, access, link}` (a new share link, shown once); `revoke {link}`
(whoever joined with it is disconnected), `rename {name}` (its name on the server) and `remove`
(whoever is in it is disconnected) → `{done}`. A wrong token, or another file's, is refused.
- `collab.files {refresh?}` → `{files: [{server, host, file, name, created, opened, shared, code,
viewCode, state, expires, session, document, hasOwnerToken}], problems: [{server, host, state,
reason}], max}`: **your online files**, the files this session (or, attached, the app with its
saved preferences) put on a server without the server's key, most recently opened first. `file`
is the id the other commands take; `shared` says whether others get in now, with `code` /
`viewCode` to hand out; `session` and `document` name the session and open document a file is
in, or are null. Without `refresh` nothing is asked of any server and `state` is what was last
heard (`"unknown"` until asked). `refresh: true` asks each server once what became of its files:
`"online"`, or `"gone"` (removed, or expired unopened); a name changed elsewhere and links
revoked elsewhere are taken over. A server that can't be reached, or is older than the request,
is listed in `problems` (`state: "offline"` with the `reason`, or `"unsupported"`) and its
files stay as they were. **Owner tokens and your own links are never in the result**
(`hasOwnerToken: true` stands for the token); they are kept with the preferences. The headless
session keeps its list only while it runs: note `published.owner` from `collab.peers` if a file
must be managed after it ends.
- `collab.openFile {file, server?}` joins one of your online files again as its owner →
`{session, status: "connecting", document, alreadyOpen, file, server, name}`; it opens as a new
document (poll `collab.peers` until `"live"`), where `collab.stop {}` and `collab.share {}` stop
and start its sharing. A file that is open already becomes the active document instead
(`alreadyOpen: true`, `document` its index). `server` is needed only when files on several
servers share an id. A file whose `state` is `"gone"` is refused.
- `collab.forgetFile {file, server?}` → `{forgotten, file, server, onServer}` drops a file from
the list **on this computer only**: the file stays on its server (`onServer`) until it expires,
and can't be managed from here again. To take it off the server use `collab.manageFile {file,
action: "remove"}` instead.
- `collab.manageFile {file, action, server?, owner?, …}` manages a file published without the key,
with its owner token. `server` and `owner` default to what is recorded for one of your online
files (`collab.files`); give both for a file that isn't recorded here. `links` → `{links: [{id,
access, label, created}]}`; `createLink {access?: view|edit, label?}` → `{id, access, link}` (a
new share link, shown once); `revoke {link}` (whoever joined with it is disconnected); `share`
→ `{shared: true, code, viewCode, editLink, viewLink}` (new links and join codes for others; the
same ones when it is shared already) and `unshare` → `{shared: false, …}` (the recorded links
and codes are revoked and whoever joined with them is disconnected; the file stays yours on the
server); `rename {name}` (its name on the server and in your list) and `remove` (the file is
deleted from the server, whoever is in it is disconnected, it leaves your list and a session
open on it here ends, its document staying open as a copy) → `{done}`. A wrong token, or
another file's, is refused.
- `collab.report {reason}` (at most 500 characters) reports the file you joined on a server to its
operator → `{sent}`; the session's message says when the server kept it, or why not (ten an hour
per address). Servers older than this version don't take reports (`hosting` is null).
+59 -6
View File
@@ -64,9 +64,11 @@ people you trust.
On a server [open to the public](#running-a-public-server) the address alone is enough
(`wss://figcraft.example.com/`): the dialog asks the server first and shows that no key is needed,
who runs it ("Hosted by …" and a link to its terms) and how long unopened files are kept. Publishing
then also gives an **owner token**, shown once with the links in a **File published** window: copy
it and keep it, it is the only way to revoke the links, rename the file or remove it later
(`collab.manageFile` from [MCP](mcp.md#live-sessions)). The Share popover shows the server's line
then also gives an **owner token**, shown once with the links in a **File published** window. The
app keeps it with your preferences and lists the file under [Home ▸ Online](#your-online-files),
where it can be opened, shared, renamed and removed later without the token in hand; copy it if
you want to manage the file from another computer or a script (`collab.manageFile` from
[MCP](mcp.md#live-sessions)). The Share popover shows the server's line
and the date the server deletes the file unless someone opens it.
## Sharing with a join code
@@ -86,7 +88,8 @@ working at once, whoever joined with them is disconnected ("Sharing was stopped
copy they have), and the owner stays in the file, which stays on the server, open only to them.
**Start sharing** again gives the same file new links and codes. **Share ▸ Advanced ▸ Remove from
server…** (it asks first) takes the file off the server; closing the tab or quitting changes
nothing on the server. To use **your** server for this, set its address
nothing on the server, and the file is still yours afterwards: see
[Your online files](#your-online-files). To use **your** server for this, set its address
(`wss://figcraft.example.com/`) in Preferences on every machine that shares or joins; a server
that needs its key is reached under **Advanced ▸ Publish to another server…**, or with
`collab.share {server: "wss://…/?key=…"}`, and gets codes too.
@@ -112,6 +115,54 @@ that needs its key is reached under **Advanced ▸ Publish to another server…*
no migration, existing files get codes the next time they are shared, and no setting turns codes
on or off. Behind the reverse proxy nothing changes (codes travel inside the WebSocket).
## Your online files
A file you put on a server without its key is yours, and stays where it is when its tab closes or
the app quits. The app remembers each one **with your preferences on this computer** (never in the
document): the server, the file's id and name, its owner token, your own links to it, when you put
it there and last opened it, and the code and links others join with while it is shared. The home
screen lists them under **Online**:
- **A row** shows the file's name, the server, **Shared** or **Not shared**, and when you last
opened it. Clicking it opens the file as its owner, in step with the server, in a tab of its
own (or brings its tab to the front when it is open already). In that tab **Stop sharing** and
**Start sharing** work as in the session that first shared it.
- **Its menu** (the button at the row's end): **Open**; **Stop sharing** / **Start sharing**
(others lose their links and codes, or get new ones; the file itself stays); **Copy code** and
**Copy link** while it is shared; **Rename on the server…**; **Remove from server…** (it asks
first: the file is deleted there for everyone and can't be brought back); **Forget on this
computer** (the row goes, **the file stays on the server** until it expires, and without its
token this computer can't manage it again).
- **Nothing is asked of a server until you open Online** (or act on a file). Then each server is
asked once, in the background, what became of your files there. A file the server no longer has
(removed, or expired unopened) reads **No longer on the server** and can be cleared from the
list. A server that can't be reached says so above the list, with **Try again**; the rows still
show what this computer remembers.
- **Tabs** of files in a live session carry a small mark after the name: waves for a session
others get into (a file you share or joined), a closed ring for a file of yours on a server that
only you get into now. Messages about a file that isn't in front name it ("Ada joined
Homepage").
- **After a crash**, a file of yours that was open and in step with its server opens again from
the server as its owner, rather than as an unsaved recovery copy.
The owner token is a password for that one file: it is kept only in the preferences file of your
user account (`state.json` in FigCraft's settings folder), is never logged, and never appears in
what commands return (`collab.files` says `hasOwnerToken: true`). Whoever can read that file can
manage your online files; a list of at most 200 files is kept (the one opened longest ago makes
room). The files are the ones published **from this computer**: another computer of yours doesn't
know them unless you manage them there with the token. A file published with the server's key has
no owner token and belongs to the server's operator: it isn't listed.
Scripts and agents use `collab.files {refresh?}`, `collab.openFile {file}`,
`collab.forgetFile {file}` and `collab.manageFile {file, action}` ([MCP](mcp.md#live-sessions)).
- **Upgrade note.** Asking what became of the files needs a server of this version or newer. An
older server still works for everything else: the list shows what the computer remembers, says
that the server can't tell what became of the files, and opening, sharing, renaming and removing
go on as before (they use requests the server already knows). To get the state of the files,
upgrade the server binary and restart it: the data directory needs no migration and there is no
setting to turn on.
## Joining
A [join code](#sharing-with-a-join-code) is the short way in. Share links look like the desktop
@@ -350,7 +401,9 @@ keeps the key for everything else.
runs it (`operator_contact`), its terms (`terms_url`) and how long unopened files are kept. A file
published without the key gets its edit and view links and an **owner token** (128 random bits,
shown once, kept only as its SHA-256): with it, whoever published the file lists, makes and
revokes its links, renames it on the server or removes it (`collab.manageFile`), and nothing else.
revokes its links, renames it on the server or removes it (`collab.manageFile`), asks whether
it is still there (what the app's [Online list](#your-online-files) does, for all its files in
one request), and nothing else.
An app older than open hosting is told the server is open when it tries a key.
- **Limits per address** (only when open): connections at once (`max_connections_per_ip`), new
connections a minute (`connects_per_minute`), files published an hour (`publishes_per_hour`),
@@ -402,7 +455,7 @@ What someone can do, by what they hold (the server is reached through TLS at the
| Someone with a **join code** | Exactly what its link gives (a view code: the view link's row; an edit code: the edit link's), until the code or its link is gone | Get a code for any other link; learn anything from a wrong code but that it is wrong |
| Someone with a file's **view link** | See that file and everyone's changes and presence in it; read its team library and usage; report it | Change it (the server refuses a viewer's edits); reach any other file; manage links |
| Someone with a file's **edit link** | Change that file, within its size cap; publish its team library (on a public server only for files published with the key); report it | Reach any other file; grow it past `max_file_mb` or its publisher's share; list, make or revoke links; rename or remove it |
| Whoever holds a file's **owner token** (published without the key) | List, make and revoke that file's links (disconnecting whoever joined with a revoked one, which is how the app stops sharing), ask for join codes for links whose token they hold, rename it on the server, remove it | Touch any other file, even with its id; list files; read reports; block; anything of the plugin directory |
| Whoever holds a file's **owner token** (published without the key) | List, make and revoke that file's links (disconnecting whoever joined with a revoked one, which is how the app stops sharing), ask for join codes for links whose token they hold, rename it on the server, remove it; ask whether it is still there, its name, link ids and expiry (asking doesn't count as opening it) | Touch any other file, even with its id; tell a file that doesn't exist from one they don't own; list files; read reports; block; anything of the plugin directory |
| The **operator** (server key, or the machine) | Everything above for every file, reports, blocks, the plugin directory; read every file on disk | Recover a lost link or owner token (only hashes are kept) |
Limits that hold against everyone: message sizes by what a connection is for, file sizes as they