ssh.md gains Sharing a workbook (who is where, editing together, undo is yours, one writer, saving, what runs) and says where unsaved work goes in a room; architecture.md the room layer with an operation's path from one session to another and the participant interface; limits.md shared sessions in BenchmarkSessions. The share tape records two sessions in tmux panes. Both roadmap steps move to Shipped.
- commit
- 0ce529e721879ae186f3eab2cb169cf17a6841c2
- parent
- 110127fdf513a9d5a7793d04565263f273314489
- author
- Dami <dami.etoile@gmail.com>
- kind
- docs risk score 0.5 of 3, Jev's estimate
| M | README.md | 4 | + |
| M | ROADMAP.md | 24 | ++---- |
| A | demos/ | 33 | +++++++++ |
| A | demos/ | 59 | +++++++++++++++ |
| M | docs/ | 95 | ++++++++++++++++++++++-- |
| M | docs/ | 3 | +- |
| M | docs/ | 16 | ++++- |
| M | docs/ | 20 | +++++- |
| A | docs/ | bin | |
| M | docs/ | 7 | ++- |
| M | docs/ | 115 | ++++++++++++++++++++++++------ |
| M | internal/ | 10 | ++- |
@@ -69,6 +69,10 @@ rest: building from a clone, importing files, `012 serve` and settings.
6969 SSH, your terminal's colors or any of hundreds of schemes, optional vim7070 keys, and `012 serve` to reach your sheets over SSH7171 ([keys](docs/reference/keys.md), [themes](docs/terminal/themes.md), [SSH](docs/terminal/ssh.md)).72+- **Together over SSH**: sessions of `012 serve` opening the same file73+ share it live, each with its own cursor, the others' in their colors,74+ and undo that takes back only your own changes75+ ([sharing a workbook](docs/terminal/ssh.md#sharing-a-workbook)).7276 - **JEV functions** ask TypeSafe's hosted model about your data from a7377 formula ([JEV](docs/formulas/jev.md)).7478 @@ -36,24 +36,13 @@ completed by nu as they're written.
3636 |---|---|---|3737 | A command's signature and description for the word at the caret, on the context line, through `nu --ide-hover` or `nu --lsp` | Writing a cell without leaving for `help` | S |3838 39-### 3. Toward multiplayer40-41-Sessions share one workbook through the server that already hosts them42-(`012 serve`), so every change is ordered in one place: the workbook's one43-mutation path (`Batch`/`Change`) becomes a stream of operations, the same44-log macros record. Each step is useful on its own.45-46-| Step | Result | Size |47-|---|---|---|48-| Shared viewing over SSH: several `012 serve` sessions open the same workbook; one edits, the others follow live with their own cursor, scroll and theme; presence shows who is where | Watch-along and review, one writer | M |49-| Shared editing over SSH: every session edits, the server orders operations (no CRDT needed while one server holds the workbook), per-user undo, presence and edit ownership shown in the grid, saves by the server | Multiplayer 012 over SSH | L |50-51-### 4. Agents39+### 3. Agents5240 5341 Agents work on workbooks through the same operations people do (the5442 `Batch`/`Change` path), so their edits are undoable, attributed and5543 shown by `012 diff`, under the same trust rules as macros. Live mode is56-section 3's shared editing with an agent as one participant.44+`012 serve`'s shared editing with an agent as one participant45+([Architecture](docs/contributing/architecture.md#shared-workbooks)).5746 5847 | Step | Result | Size |5948 |---|---|---|@@ -66,8 +55,9 @@ tickets (the Go transport in `FelineStateMachine/allons`
6655 `local/transport/iroh`, which needs cgo and a prebuilt iroh-ffi archive, so6756 it would sit behind a build tag) and the web (`NimbleMarkets/go-booba`6857 serves Bubble Tea over WebSocket/WebTransport with ghostty-web; Bubble Tea69-v2 support unverified). They would reuse the server-ordered operation70-stream above rather than a design of their own.58+v2 support unverified). They would reuse the rooms of `012 serve`, whose59+server orders every participant's operations, rather than a design of60+their own.7161 7262 ## Shipped7363 @@ -120,6 +110,8 @@ stream above rather than a design of their own.
120110 - Color schemes and a config file: [Themes](docs/terminal/themes.md), [Configuration](docs/reference/config.md)121111 - A high-contrast theme at WCAG AAA, and every state readable without color: [Themes](docs/terminal/themes.md#high-contrast), [UX](docs/contributing/ux.md#reading-without-color)122112 - `012 serve` over SSH, with files on the ssh command line and recovery of unsaved work: [Serving over SSH](docs/terminal/ssh.md)113+- Shared viewing over SSH: sessions opening one file share it live, each with its own cursor, the others' pointers and names shown, one writer handing writing over: [Serving over SSH](docs/terminal/ssh.md#sharing-a-workbook)114+- Shared editing over SSH: everyone edits, the server orders every step, undo takes back only your own, changes by others marked, saves and notebook runs by the room: [Serving over SSH](docs/terminal/ssh.md#sharing-a-workbook)123115 - Shift+Enter, Ctrl+I and keys held to preview, with the kitty keyboard protocol: [Keys and mouse](docs/reference/keys.md#keys-the-terminal-has-to-tell-apart)124116 - Sixel chart images on terminals without kitty graphics, drawn after the frame and redrawn as the screen moves: [Charts](docs/sheets/charts.md)125117 @@ -0,0 +1,33 @@
1+2+# share-demo: two people in one workbook over 012 serve, as the share3+# tape records them: a server on a budget in out/share/served, reached4+# with ssh as ann in the top tmux pane and as bob in the bottom one.5+# `share-demo stop` ends the server. Runs from demos/.6+set -e7+d=out/share8+port=23999+if [ "$1" = stop ]; then10+ tmux -L 012demo kill-server 2>/dev/null || true11+ kill "$(cat $d/serve.pid)" 2>/dev/null || true12+ exit 013+fi14+rm -rf $d15+mkdir -p $d/served16+ssh-keygen -q -t ed25519 -N '' -C demo -f $d/id17+cp $d/id.pub $d/authorized_keys18+chmod 600 $d/authorized_keys19+cat > $d/served/budget.012 <<'EOF'20+{"version": 2, "cells": {"A1": "Household budget", "A3": "Rent", "B3": "1200", "A4": "Food", "B4": "450",21+ "A5": "Power", "B5": "90", "A6": "Total", "B6": "=SUM(B3:B5)"}}22+EOF23+../bin/012 serve --listen 127.0.0.1:$port --authorized-keys $d/authorized_keys --host-key $d/host_key $d/served 2>$d/serve.log &24+echo $! >$d/serve.pid25+until grep -q "on 127.0.0.1:$port" $d/serve.log 2>/dev/null; do sleep 0.1; done26+echo "[127.0.0.1]:$port $(cat $d/host_key.pub)" >$d/known_hosts27+ssh="ssh -F /dev/null -i $d/id -o IdentitiesOnly=yes -o IdentityAgent=none -o UserKnownHostsFile=$d/known_hosts -o LogLevel=ERROR -p $port -t"28+tmux -L 012demo -f /dev/null new-session -d -s share -x "$(tput cols)" -y "$(tput lines)" "$ssh -l ann 127.0.0.1 budget.012"29+tmux -L 012demo set -g status off30+tmux -L 012demo set -g pane-border-style fg=brightblack31+tmux -L 012demo set -g pane-active-border-style fg=cyan32+tmux -L 012demo split-window -v "sleep 1; $ssh -l bob 127.0.0.1 budget.012"33+exec tmux -L 012demo attach -t share@@ -0,0 +1,59 @@
1+# Sharing a workbook over 012 serve: ann (top pane) and bob (bottom)2+# open the same budget through ssh, in tmux panes (Ctrl+B and an arrow3+# moves between them). Bob changes the food line, ann the rent: each4+# sees the other's change arrive, their pointer in its color and their5+# initial on the row header. Ann lists who's here; bob's undo takes6+# back only his own change. Needs tmux, ssh and ssh-keygen.7+Output out/share.gif8+Source lib/setup.tape9+Set Width 130010+Set Height 90011+12+Hide13+Type "clear; lib/share-demo" Enter14+Sleep 3s15+Show16+17+Sleep 800ms18+Down19+Down20+Down21+Right22+Type "480"23+Enter24+Sleep 600ms25+Right26+Right27+Sleep 800ms28+Ctrl+B29+Up30+Sleep 500ms31+Down32+Down33+Right34+Type "1300"35+Enter36+Sleep 1.5s37+Screenshot out/stills/share-presence.png38+Ctrl+K39+Type@100ms "who's here"40+Sleep 500ms41+Enter42+Sleep 2s43+Escape44+Sleep 500ms45+Ctrl+B46+Down47+Sleep 500ms48+Ctrl+Z49+Sleep 2s50+Screenshot out/stills/share-undo.png51+Sleep 1s52+53+Hide54+Ctrl+B55+Type ":kill-server"56+Enter57+Sleep 500ms58+Type "lib/share-demo stop" Enter59+Sleep 500ms@@ -31,6 +31,7 @@ internal/nushell a notebook cell's run: nu as a process, tables in as NUON files
3131 module 012.nu, the nushell module with sheet, embedded for 012 nu --module and --install-module3232 internal/telemetry opt-in JSON log and OTLP export of spans, events and frame stats3333 internal/serve the SSH server: auth, host key, a Model per session (charm.land/wish/v2)34+internal/room shared workbooks: a room per file, a seat per participant, taking turns on the one workbook3435 internal/confine resolving typed file names, confined to a directory when served3536 internal/stress synthetic worst-case sheets for the -tags stress benchmarks3637 internal/ui the Bubble Tea model: modes, menus, overlays, rendering@@ -71,7 +72,7 @@ flowchart TD
7172 direction LR7273 ui[internal/ui] ~~~ fileio[internal/fileio] ~~~ live[internal/live] ~~~ nushell[internal/nushell]7374 chart[internal/chart] ~~~ jev[internal/jev] ~~~ stress[internal/stress] ~~~ oracle[oracle]74- headless[internal/headless] ~~~ diff[internal/diff]75+ headless[internal/headless] ~~~ diff[internal/diff] ~~~ room[internal/room]7576 end7677 callers --> sheet[internal/sheet]7778 sheet --> functions[internal/functions]@@ -320,9 +321,8 @@ window drops the oldest rows (read back from the region's cells and
320321 written again one row up); inside an open step (a macro run) what reads321322 the cells recalculates when the step ends. An op carries whole322323 values and names nothing but its region, so applying the same ops in the323-same order to the same workbook makes the same cells: it is what a324-session following another's over `012 serve` would be sent, beside the325-steps `Batch` records. `OnLive` times each op for telemetry.324+same order to the same workbook makes the same cells, whoever applies325+it. `OnLive` times each op for telemetry.326326 327327 ### Functions328328 @@ -794,11 +794,88 @@ sequenceDiagram
794794 795795 Each session runs its `tea.Program` over wish's emulated PTY; the796796 client's environment is what the model detects the terminal from, and797-its `confine.Root` resolves every file name. Everything a model knows lives in the model, so sessions798-share nothing but read-only tables (the command and function799-registries), the process-wide telemetry and, with JEV on, the HTTP800-client; each gets its own `jev.Cache`, whose queue belongs to that801-session's program. See [Serving over SSH](../terminal/ssh.md).797+its `confine.Root` resolves every file name. Everything a model knows798+lives in the model, so sessions share nothing but read-only tables (the799+command and function registries), the process-wide telemetry, with JEV800+on the HTTP client (each gets its own `jev.Cache`, whose queue belongs801+to that session's program), and the workbooks of the rooms they're in802+(below). See [Serving over SSH](../terminal/ssh.md).803+804+### Shared workbooks805+806+Sessions opening the same file (its path on disk is the key) or the807+same named room share one `*sheet.Workbook` (`internal/room`). There is808+nothing to merge: the server holds the workbook, and participants take809+turns on it. A `room.Registry` holds the rooms; `Join` seats a810+participant, opening the room on the file's workbook when nobody has811+it open, and the last `Leave` closes it. A seat's `Do` is a turn: the812+room's lock held, the workbook's author set to the seat, so every step813+made is attributed; when the turn ends, the room notes the operations814+it made and tells the others. A served session runs its model under815+`ui.Shared`, which makes every `Init`, `Update` and `View` a turn,816+publishes where the user is (`Seat.Move`), and between turns joins and817+leaves rooms, never holding two locks. An operation's way from one818+session's key to another's screen:819+820+```mermaid821+sequenceDiagram822+ participant A as session A (ui.Shared)823+ participant R as the room (room.Seat.Do)824+ participant W as the workbook825+ participant B as session B826+ A->>R: a key: Update, a turn827+ R->>W: SetAuthor(A's seat)828+ A->>W: Sheet.Set, Batch: a step, recalculated829+ W->>W: the step pushed: author, operation number (sheet.Op)830+ R->>R: turn ends: the new operations marked (who, which cells)831+ R-->>B: Participant.Notify832+ B->>R: roomMsg: Update, a turn833+ B->>R: the marks since it last looked, the others' presence834+ B->>B: View in its turn: the change, A's pointer in A's color835+```836+837+Undo in a shared history takes back the author's own latest step,838+from under others' later steps when none of them changed what it839+changed, and refuses otherwise, naming whose step is in the way840+(`sheet/authors.go`, where the rule is written down); cancelling a841+dialog undoes the user's own steps since it opened (`Checkpoint`,842+`UndoTo`). `TestRandomSharedEdits` holds it to undoing, in any order843+it allows, back to the start. A step left open across turns (a macro844+run, `Workbook.Begin`) holds the others' input until it ends, and one845+left open by a session that crashed is closed (`EndStep`).846+847+What the room runs is the room's: linked files are followed by the848+seat there longest (`Seat.Keeper`), notebook cells' runs and streams849+(`nbRuns`) are a value the room keeps (`Seat.Value`), and what their850+commands report goes to whoever keeps the room then (`Seat.Post`,851+`Take`), so a run outlasts the session that started it. The room's852+last save (`Seat.SetSaved`) is what every session counts unsaved853+changes from, and the last session out keeps them for recovery.854+855+A participant is anything with a name that can be told the room856+changed; everything else it does, it does through its seat:857+858+| `room.Participant` | What it is |859+|---|---|860+| `Name()` | who they are to the others: the SSH user |861+| `Notify()` | the room changed; called with the room locked, so it only schedules a turn (a session sends its program a `roomMsg`) |862+863+| `room.Seat` | What it does |864+|---|---|865+| `Do(fn)` | a turn: `fn` on the workbook, the lock held, steps in the seat's name |866+| `Move`, `Peers`, `Marks`, `Last` | where this participant is; the others and where they are; who changed which cells since an operation |867+| `Writing`, `HandTo`, `Writer` | the one-writer mode's role |868+| `Saved`, `SetSaved`, `Value`, `Keeper`, `Post`, `Take` | what the room shares: its file's saved state, values such as the notebook runs, and messages for whoever keeps it |869+| `Leave` | the seat given up; reports whether the room closed |870+871+The interface stays this small so an MCP client attached to the872+server can be a participant beside sessions (ROADMAP.md, Agents):873+`internal/mcp`'s tools stand on a `Backend` of `View`, `Scratch` and874+`Change`, and a room's would be a seat, `View` and `Change` its turns875+(the change one `Batch`, attributed to the client's seat, so its own876+undo and the others' marks follow), `Scratch` a copy of the workbook877+taken in a turn, and `Notify` what tells the client the workbook878+changed.802879 803880 **Unsaved work.** `Server.Shutdown` closes a channel every session804881 watches; a session quitting on it, or on its idle timeout, asks its@@ -111,7 +111,8 @@ the same way.
111111 Every change to a workbook goes through `Batch(Change{...}, fn)` and the112112 history `step`: snapshot what changes, apply, mark dirty, recalculate. Undo,113113 telemetry, recalculation and the file's dirty flag hang off that one path,114-and so will anything later that needs to see every change (live sharing).114+and the rooms of `012 serve`, which order everyone's steps and tell each115+participant what the others changed ([Architecture](architecture.md#shared-workbooks)).115116 New mutations use it; they never write cells around it. A change made over116117 several calls, as a macro run makes one call per message, opens its step117118 with `Workbook.Begin` and closes it when done, so it undoes as one; inside@@ -488,7 +488,8 @@ read-heavy recalculation.
488488 489489 `BenchmarkSessions` (`internal/serve/stress_test.go`) opens N sessions,490490 each over its own SSH connection on loopback at 120 x 40, optionally491-opening the same 1000 x 26 sheet of numbers in every one, then has every491+opening the same 1000 x 26 sheet of numbers in every one, a copy each492+(`--share off`) or shared in one room, then has every492493 session press an arrow key at once, 200 times, timing each key from the493494 client's write to the first bytes of its frame arriving. The clients494495 run in the same process, so the heap and CPU figures include their side@@ -501,6 +502,8 @@ for 120 frames a second, as the local app does (`-benchtime 200x`):
501502 | 10, 1000 x 26 numbers | 1.9 MiB | 3.9 / 8.3 / 10.8 ms | 0.79 ms |502503 | 50, new sheet | 1.30 MiB | 5.0 / 8.7 / 17.8 ms | 0.53 ms |503504 | 50, 1000 x 26 numbers | 1.9 MiB | 5.1 / 8.9 / 12.8 ms | 0.66 ms |505+| 10, 1000 x 26 numbers, shared | 1.5 MiB | 5.0 / 8.3 / 16.6 ms | 1.27 ms |506+| 50, 1000 x 26 numbers, shared | 1.5 MiB | 7.1 / 15.8 / 33.2 ms | 0.79 ms |504507 505508 Latency doesn't move from 10 to 50 sessions: it's Bubble Tea's frame506509 pacing (at most one frame every 8.3 ms, so a key waits half a frame on@@ -517,6 +520,17 @@ second would keep about four cores busy: frames are drawn only when a
517520 session's screen changes, so the rate is the most keys can cause, not a518521 cost of being connected.519522 523+Sessions sharing a workbook hold it once, so each holds only its524+screen. They take turns on it: each key is handled under the room's525+lock, and moves a pointer every other session draws, so a key costs a526+frame in every session of the room (CPU per frame counts the pressing527+session's alone), and fifty sessions pressing keys in the same528+millisecond queue for the lock, the last waiting about two frames. A529+change reaching the others is quick next to the frame: from the end of530+one session's turn to another's frame showing it takes about 0.1 ms531+(`TestSharedEditWithinAFrame`), so it goes out with the other's next532+frame.533+520534 ## Pivot tables521535 522536 A pivot table is recomputed whole whenever a cell of its source range is@@ -45,9 +45,22 @@ failure names the fewest of the seed's edits that still fail. It takes
4545 about a second; `-randedit.seeds=20000` runs more, and4646 `FuzzRandomEdits` (in `make fuzz`) lets the fuzzer choose the edits.4747 48+`TestRandomSharedEdits` (`randedit_shared_test.go`) makes the same49+edits as three authors taking turns in one workbook with its history50+shared, as a room of `012 serve` holds it, then has them undo their own51+steps in turns drawn from the seed, out of order wherever the rule52+allows: someone can always undo, an undo redone at once gives back the53+file it undid, and once every step is undone the workbook is back where54+it started.55+4856 The UI is tested by sending4957 Bubble Tea messages (keys, mouse, paste, window size) to the model and50-reading what `View` renders, without a terminal.58+reading what `View` renders, without a terminal. Sessions sharing a59+workbook are driven the same way through `ui.Shared`, each room's60+changes handed to the others as their programs would get them61+(`share_test.go`), and in real Bubble Tea programs typing at once under62+the race detector, with the time an edit takes to reach another63+session's frame (`sharelive_test.go`).5164 5265 ## End to end, through libghostty5366 @@ -72,7 +85,10 @@ restores the terminal and keeps the work; release builds have no such key.
7285 7386 `e2e/ssh_test.go` runs `012 serve` and reaches it with the system's7487 `ssh` client inside the same libghostty terminal, so the server path75-is tested as a user sees it (skipped when `ssh` isn't installed).88+is tested as a user sees it (skipped when `ssh` isn't installed). `e2e/share_test.go` has two people89+reach one `012 serve` that way, each in a terminal of their own, and90+share a file; screens of a shared workbook start the others' sessions91+first (`screen.peers`).7692 7793 `make e2e` builds libghostty-vt from source with Zig into `.deps/` on first7894 use. It is its own Go module so cgo never reaches the main binary.Binary file.
@@ -54,7 +54,7 @@ mouse and the vim keymap added.
5454 | Key | Action |5555 |---|---|5656 | Del, Backspace | Clear the selection; formatting stays |57-| Ctrl+Z; Ctrl+Y, Ctrl+Shift+Z | Undo; redo |57+| Ctrl+Z; Ctrl+Y, Ctrl+Shift+Z | Undo; redo. In a workbook shared over `012 serve`, your own changes only ([Sharing a workbook](../terminal/ssh.md#sharing-a-workbook)) |5858 | Ctrl+C, Ctrl+X, Ctrl+V | Copy, cut, paste; references adjust as in Sheets, and copies also go to the system clipboard |5959 | Ctrl+Shift+V | Paste values only |6060 | Paste in the terminal | Tab-separated or multi-line text fills a block of cells |@@ -128,6 +128,11 @@ keeping its cells.
128128 | Shift+F10, right-click | The cell, column or row menu |129129 | F1, Ctrl+/ | Keyboard shortcuts |130130 131+In a workbook shared over `012 serve`, **File > Who's here** (or `who` in the132+palette) lists the others and goes to one's cell, and **File > Hand over133+writing** passes writing on in a one-writer room; neither has a key of134+its own ([Sharing a workbook](../terminal/ssh.md#sharing-a-workbook)).135+131136 ## Macros132137 133138 | Key | Action |@@ -8,26 +8,29 @@ sidebar_position: 3
88 `012 serve` runs an SSH server that gives every session its own 012, in99 the client's own terminal, on the files of one directory. It's for1010 reaching your sheets from another machine or a tablet with an SSH app,11-without installing anything there. It is not shared editing: two12-sessions are two separate spreadsheets. Shared viewing and editing over13-`012 serve` are planned: see [toward multiplayer](../../ROADMAP.md#3-toward-multiplayer).11+without installing anything there, and for working on one workbook12+together: sessions that open the same file share it, each with its own13+cursor, and every change reaches the others as it's made14+([Sharing a workbook](#sharing-a-workbook)).1415 1516 ```sh1617 012 serve ~/sheets # serve ~/sheets on 127.0.0.1:23121718 ssh -p 2312 localhost # from this machine, on a new sheet1819 ssh -t -p 2312 localhost budget.012 # opening budget.012 from ~/sheets20+ssh -t -p 2312 localhost @standup # joining the named room standup1921 ```2022 2123 A file name after the host opens that file, as `012 budget.012` does2224 locally: a `.012` sheet, a new sheet to be saved under that name when23-there's no such file, or a file to import (`data.csv`). It must be one24-word (quote a name with spaces twice: `ssh -t host "'my book.012'"`)25-and needs `-t`, since ssh only asks for a terminal on its own when26-there's no command.25+there's no such file, or a file to import (`data.csv`). A name starting26+with `@` joins a named room instead. It must be one word (quote a name27+with spaces twice: `ssh -t host "'my book.012'"`) and needs `-t`, since28+ssh only asks for a terminal on its own when there's no command.2729 2830 ```2931 012 serve [--listen addr] [--authorized-keys file] [--host-key file]30- [--idle-timeout 30m] [--max-sessions 8] [--log file] [--otlp url] [dir]32+ [--idle-timeout 30m] [--max-sessions 8] [--share edit|view|off]33+ [--log file] [--otlp url] [dir]3134 ```3235 3336 | Flag | Default | What |@@ -38,6 +41,7 @@ there's no command.
3841 | `--host-key` | `<config dir>/012/ssh_host_ed25519_key` | The server's key, generated on first run |3942 | `--idle-timeout` | `30m` | End a session after this long without input, keeping its unsaved changes; `0` never does |4043 | `--max-sessions` | `8` | Sessions at once; more are turned away with a message |44+| `--share` | `edit` | Sessions opening the same file share it: `edit`, everyone edits; `view`, one writes and the others follow; `off`, each session gets a copy of its own |4145 | `--log`, `--otlp` | off | Telemetry, as for the app ([Observability](../contributing/observability.md)) |4246 4347 The config directory is `os.UserConfigDir()`: `~/Library/Application@@ -134,20 +138,76 @@ served directory on the server (a symbolic link swapped in between the
134138 check and the open), and anything a holder of an authorized key does135139 with the files they're given.136140 141+## Sharing a workbook142+143+Sessions that open the same file (by any name that leads to it) are in144+one room, and so are those joining the same named room (`@standup`,145+which starts as a new sheet until someone saves it under a name). A146+room is one workbook the server holds: every session sees every change147+as it's made, whoever made it, while keeping its own cursor, scroll,148+selection, sheet shown, theme and modes. A session on a new sheet or an149+imported file has a workbook of its own until it's saved and opened150+again.151+152+153+154+**Who is where.** Each other person in the room has a color: their155+pointer's cell is drawn in it, double-underlined, their initial is on156+that row's header, and their name is on the status line. A cell someone157+else changed in the last 30 seconds has a `▘` in its corner, and the158+context line on it says who changed it and how; on someone's pointer it159+says who is there, and whether they're typing. **File > Who's here**160+lists everyone with the cell they're on, and picking one goes there.161+162+**Editing together.** Everyone edits (with `--share edit`). The server163+takes each session's keys and clicks in turn, so every change is made164+whole, in one order, through the one path every change takes in 012:165+two entries in one cell leave the later one, and the one who typed166+over the other's change is told, as is the one typing while someone167+else changed the cell.168+169+**Undo is yours.** Ctrl+Z takes back your own latest change, even when170+others changed other cells since: theirs stay. When someone changed the171+same cells after you (the same cells, widths, formats, names or rules,172+or rows and columns inserted or deleted on that sheet, or any change to173+the list of sheets), undo refuses and says who: take back yours by174+hand, or ask them. Redo is yours on the same terms. Formulas that read175+a cell you undid recalculate as usual.176+177+**One writer.** With `--share view`, the first in a room writes and178+the others follow: they move, scroll, select, copy and look around as179+they like, but can't change the workbook, and the status line says who180+writes. The writer hands writing over with **File > Hand over writing**;181+when the writer leaves, whoever has been there longest writes.182+183+**Saving is the room's.** Any session saves the one workbook to its184+file, and the others' `modified` clears with it. Quitting while others185+are in the room asks nothing: they still have the workbook. The last186+one to leave is asked about unsaved changes as in a local 012, and the187+last session to end without quitting keeps them (see188+[Unsaved work](#unsaved-work)).189+190+**What runs is the room's.** Files linked to the workbook are followed191+once for everyone, and notebook cells run once: any session may run or192+stop them, everyone sees them running and their outputs, and they go on193+when the one who started them leaves, until the last one does.194+137195 ## Each session138196 139-Every session is a separate 012 with nothing shared but the process:140-its own workbook, undo history, clipboard, theme, JEV cache and141-terminal state. What it learns about the terminal comes from the142-client: the window size and its changes, the colors (from the `TERM`143-the client sends), the light or dark background, kitty or sixel graphics, and144-the clipboard, which Ctrl+C sets on the client's machine through OSC 52.197+Besides the shared workbook, every session is a separate 012 with198+nothing shared but the process: its own cursor and selection, undo of199+its own changes, clipboard, theme, JEV cache and terminal state. What200+it learns about the terminal comes from the client: the window size and201+its changes, the colors (from the `TERM` the client sends), the light or202+dark background, kitty or sixel graphics, and the clipboard, which203+Ctrl+C sets on the client's machine through OSC 52.145204 146205 [Macros](../sheets/macros.md) can be recorded, run, renamed and deleted in a147206 session, but not edited as scripts: that would start an editor on the148207 server. A file's macros ask for trust once per session, since the149208 session isn't the server's own computer, and scripts have no file,150-network or clock access in any case.209+network or clock access in any case. A macro running in a shared210+workbook is one change: the others' keys wait until it ends.151211 152212 ## Notebooks153213 @@ -184,6 +244,11 @@ saving over a file that changed on disk still asks first. A sheet never
184244 saved is kept as `(untitled)-<time>.012` and offered when a session185245 starts on a new sheet.186246 247+In a shared workbook, only the last session in the room keeps them:248+one that ends while others remain leaves the workbook to them, and says249+so when it idled out. Restoring kept changes opens the room again on250+them, once nobody else is in it.251+187252 It stays small: nothing is kept for a workbook without changes or with188253 no cells, and only the newest three files per name are kept. The189254 directory is created `0700` and its files `0600`, and it's hidden, so no@@ -192,21 +257,25 @@ it. A session whose client just goes away (a dropped connection)
192257 keeps nothing, and neither does quitting, which asks about unsaved193258 changes as the local app does.194259 195-## Two sessions, one file260+## Files changed elsewhere196261 197-Opening the same file in two sessions gives two copies. Saving over a198-file that changed on disk since it was opened or last saved in this199-session (someone else saved it) asks first on the context line:200-Enter overwrites it, S saves under another name, Esc cancels. The same201-check protects the local app from other programs writing the file.262+Saving over a file that changed on disk since it was opened or last263+saved (another program wrote it, or a session with `--share off`) asks264+first on the context line: Enter overwrites it, S saves under another265+name, Esc cancels. The same check protects the local app from other266+programs writing the file.202267 203268 ## Limits204269 205270 - An idle session is closed without saving over its file; unsaved206271 changes go to a recovery file instead.207-- A dropped connection loses unsaved changes.272+- A dropped connection loses unsaved changes, unless others share the273+ workbook.274+- Sessions in one room take turns: a long change (a sort of a million275+ rows, a macro) makes the others wait for it.208276 - Each session holds its screen's buffers, about a megabyte, plus the209- sheets it opens; two sessions opening one file hold two copies. Fifty277+ sheets it opens; sessions sharing a file hold one workbook between278+ them. A change reaches the others' screens within a frame. Fifty210279 sessions typing at once get their frames as fast as ten do: see211280 [Bounds of support](../contributing/limits.md#serving-over-ssh) for212281 the measurements.@@ -27,7 +27,7 @@ import (
2727 2828 func BenchmarkSessions(b *testing.B) {2929 for _, n := range []int{10, 50} {30- for _, shape := range []string{"empty", "dense-1000x26"} {30+ for _, shape := range []string{"empty", "dense-1000x26", "dense-1000x26-shared"} {3131 b.Run(fmt.Sprintf("sessions=%d/%s", n, shape), func(b *testing.B) { benchSessions(b, n, shape) })3232 }3333 }@@ -35,7 +35,13 @@ func BenchmarkSessions(b *testing.B) {
3535 3636 func benchSessions(b *testing.B, n int, shape string) {3737 key := newKey(b)38- srv, addr, dir := testServer(b, func(o *Options) { o.MaxSessions, o.IdleTimeout = n, 0 }, key)38+ // A copy of the sheet each, but for the shared shape: one workbook39+ // in one room, every key moving a pointer the others draw.40+ share := "off"41+ if shape == "dense-1000x26-shared" {42+ share = "edit"43+ }44+ srv, addr, dir := testServer(b, func(o *Options) { o.MaxSessions, o.IdleTimeout, o.Share = n, 0, share }, key)3945 if shape != "empty" {4046 writeDense(b, filepath.Join(dir, "dense.012"))4147 }