Contents

M13: working on a show together

Status: design, 2026-10-05. Builds on the service's sharing (docs/design/m12-sharing.md): show keys, one daemon per show, the host's hello rewrite, the daemon's method classes, per-person Claude keys, authorship and the stale-save refusal. This note makes sharing a whole way of working, the same in the installed Studio (Linux, Windows, macOS) and on reelwright.diy/app.

The model, in one paragraph

A show lives either on your computer (private to you) or on Reelwright, the service at reelwright.diy (where it can be shared). Anyone can share a show they own: they invite people by email with a role, each invited person accepts, and the owner can change anyone's role or remove them at any time. Everyone works on the one copy on the service, sees who else is in it and where, sees each other's changes as they happen, talks in comments pinned to the work, and is told what concerns them. The installed Studio signs in to Reelwright to open shared shows exactly as the browser does; a show on your computer is shared by putting it on Reelwright first.

We considered teams that only the service's owner creates. They are heavier for the common case (two writers on one show) and still need per-show roles, so this design shares shows directly. The service's owner keeps the switches (admin console, docs/design/m12-web-service.md): whether sharing is on, whether an invite may bring someone new to the service, link requests, and limits on collaborators per show.

Roles

Role Read, play, export Comment Change the show, direct, voice, render, chat Invite and manage people Delete, transfer
Owner (exactly one) yes yes yes yes yes
Editor yes yes yes only when the owner turns on "Editors can invite" (and never above Editor) no
Reviewer yes yes no no no
Viewer yes no no no no

The daemon enforces this from the session schema's method classes (read, comment, write), extended from M12's read/write; the host enforces sharing routes. Studio hides or disables what a role cannot do and says why ("You can review this show: ask Ada to make you an editor"), but the daemon's refusal is the guarantee. Model calls are paid by the person who asks, with their own key and cap; a render counts against the quota of the person who starts it.

Inviting and joining

  • Share… (show menu, palette, the Show overview, the title bar's people) opens the share dialog: the people with their roles, pending invitations with their state, an Invite field (one or more emails, a role, an optional note), "Editors can invite", and Copy link.
  • An invitation is a record (show, email, role, invited by, note, created, expires in 14 days, token hashed). Reelwright emails "Ada invited you to edit The Marigold on Reelwright" with Accept. Accepting needs signing in as the invited address (the link names it; a different signed-in account is told so). Someone with no account gets one on accepting when the admin's "Invites may bring new people" is on (default on), else the owner is told the person must be invited to Reelwright first. Invitations can be resent and revoked; declined and expired ones say so in the dialog.
  • In Studio the invited person sees invitations on the welcome screen, in the project switcher (Invitations, with Accept and Decline) and in the notifications. Accepted shows join "Shared with you".
  • Link: Copy link gives the show's address. Someone who opens it without access sees "Request access" (with a note); the owner (and inviting editors) get a notification and approve with a role or decline. Requests are rate-limited per person and show.
  • Changing roles and removing: at once, for sockets too: a demoted person's daemon connection is re-labelled; a removed person's socket closes and Studio says "Ada stopped sharing The Marigold with you".
  • Leaving: anyone but the owner can leave a show.
  • Transfer: the owner offers ownership to an editor; the editor accepts; the old owner becomes an editor. The show's folder moves to the new owner's workspace and its key changes; old links redirect.
  • Deleting: only the owner, after typing the show's name, with the collaborators listed; it ends every session in it and tells everyone.

Working at the same time

  • Presence: each person has a colour and initials. The title bar shows who is in the show; hovering says where (section, episode, timeline frame). Clicking someone follows them (Studio goes where they are) until you act. Timeline and script show other people's playheads and cursors' lines in their colour.
  • Live changes: every change set applied in the show is broadcast; Studio refreshes what it shows and says briefly who did what ("Ada edited the Lighthouse's script").
  • Scripts: live (docs/design/co-writing.md, since 2026-10-11): everyone in a script writes in one text, sees the others' typing as it is typed and their carets with their names, and Undo is their own. Where a script is not live (a show on this computer, an older Studio), saving merges: the editor sends the text it started from (base); the daemon three-way merges by line (base, theirs on disk, mine). Edits to different lines merge and the editor says "Merged with Ada's changes"; overlapping lines open a conflict view, mine and theirs side by side per hunk, to keep either or both. Nothing is written over unseen.
  • Proposals: the showrunner's proposals in a shared show say who asked; editors and the owner approve or reject; reviewers and viewers see them.
  • Undo undoes only your own changes (M12), and outlasts a restart. History (the show's versions, docs/design/m3-showrunner.md "Versions") shows who saved each, by name, and can restore any version as a new change.

Comments

Comments are part of the show (comments/<slug>.thread.json in the show's folder, so they travel with it and work on a show on your computer too), served by the daemon: comments.list, comments.create, comments.reply, comments.edit, comments.delete (your own), comments.resolve / reopen. A thread is anchored to a script line range (kept on the text as it changes, by matching the quoted lines), a timeline frame or shot, a document (a character, a set, a prop) or the show. Bodies are plain text with @mentions of people in the show. Studio shows them in the script's gutter, as pins on the timeline ruler, on documents, and in a Comments panel with filters (open, mine, mentioning me, resolved).

Notifications

The service keeps each person's notifications: invitations, access requests, ownership offers, mentions, replies to your threads, a role change, removal, and a render you started finishing. Studio's bell (status bar) lists them with unread counts and goes to the thing on click. Email: invitations and access requests at once; mentions and replies at once or as a daily digest or not at all, each person's choice (account settings). Every email has a one-click way to stop that kind.

The installed Studio

  • Sign in to Reelwright (welcome screen, show menu, palette): the email and the code, in Studio, against /api/auth/request and /api/auth/verify with client: "studio", which answers a device token instead of a cookie. The token is kept in Studio's user folder (owner-only file permissions), named in the account's sessions ("Studio on Linux, signed in 5 Oct"), and revocable there and in the admin console. Signing out forgets it.
  • Shows on Reelwright appear in the welcome screen and the switcher beside the shows on this computer, marked with a cloud, in "Yours" and "Shared with you", with invitations on top. Opening one does what the browser does: /api/shows/<key>/open, then the daemon's socket with a ticket, files over /api/file with the token. Renders run on the service and download when asked. Offline, they are listed but say they need a connection.
  • Sharing a show on your computer: Share… on a local show offers "Put it on Reelwright to share it": Studio uploads the show (a git bundle of its history plus the files git does not keep, checksummed) to the person's account; the service checks it like any Show Pack and makes it a show there; Studio opens it from Reelwright and marks the local folder "Now on Reelwright", with a button to open that one. The local folder is left as it was.
  • A copy to keep: any show on Reelwright you can read can be downloaded as a Show Pack.

The service's owner

The admin console (docs/design/m12-web-service.md) adds: sharing on/off, link requests on/off, invites may bring new people on/off, most collaborators per show, every shared show with its people, and invitations service-wide (revoke).

Security

Invitation and request tokens are random, single use, expiring, stored hashed. Accepting checks the signed-in address. Everything is audited. Comments and notes never reach a model unless someone asks Claude with them in view. Device tokens are hashed at rest on the service and scoped to one person; they never open the admin console (that needs a browser session).

Phases

  1. Service: invitations with acceptance, the four roles and method classes, requests, leave, transfer, delete, notifications and their emails, device tokens and the upload.
  2. Daemon: comments, three-way merge on save, presence with places, live change notes.
  3. Studio (both editions): Share dialog, invitations, notifications, presence and follow, comments, merge and conflict view, Sign in to Reelwright and shows on Reelwright on desktop, Put it on Reelwright.

Each phase lands gated, with core and engine tests, and a live proof: two people (one in the installed Studio, one in a browser) in one show, an invitation accepted, a reviewer's comment answered by the owner, a merge of two edits to one script, a removal.

So far

Phase 1: the service

On the service (core/src/host; the routes are in docs/design/m12-web-service.md, "The API the web Studio uses"):

  • Roles. ShareRole is editor, reviewer or viewer (the owner is the workspace's, never a share). A database from M12 has its shares table made again with the wider check, every row kept (store.ts). The host says the role in the hello's as (reviewer too), and the daemon's method classes hold each role to it (phase 2).
  • Invitations (invitations.ts, table show_invitations): one per address and show (another replaces it), inv_<16 hex> to the API, a 32-byte token in the email's link (<base>/invitations/<token>, its SHA-256 kept), 14 days, an optional note (1000 characters), audited (invitation.created|resent|revoked|accepted|declined, then show.shared). The email ("Ada invited you to edit Tea Time on Reelwright", with Accept and the note) goes through mail.ts; someone with an account also gets it in their notifications. Accepting by id needs the signed-in address to be the invited one (anyone else hears 404); by the link, a different account is told whose it is (403). Signed out, the link's Accept goes to /login?invitation=<token>, and the sign-in that carries the token is the acceptance: an address with no account gets one (a member, audited user.created "member (show invitation)") only while the setting "invites may bring new people" is on, and then only through that token. Declining needs only the link. Resending gives a new token and another 14 days (the old link stops); revoking ends it. 50 invitations a person a day, 20 addresses a form; an address with no account is refused (no_account) when the setting is off, and the owner, someone in the show and a disabled account are refused too.
  • Who lets people in: the owner and an admin in the show; an editor when the owner turns on "Editors can invite" (PATCH …/sharing, table show_options), for invitations they sent (resend, revoke) and for requests. Nobody is invited above editor.
  • Requests (table access_requests): someone signed in who opens a link (<owner>~<show>) asks with a note; the owner and inviting editors are notified (and emailed); one open request per person and show (asking again answers the same one), a day's wait after a decline, ten an hour a person. Approving takes a role; the person is told.
  • The service's switches are the admin console's settings, read at each use, so an owner's change applies to the next request (sharing-settings.ts sharingSettingsOf over settings.ts; the rules in invitations.ts see only the SharingSettings interface). Sharing on is Features' switch (features.sharing; off, invitations, requests and approvals are refused, and a show reaches only its owner). The Sharing section holds the rest, all as the service came until an owner changes them: invitations may bring new people (on; never while sign-ups are closed, access.signups), requests from a show's link (on), and the most people a show may have (shares and open invitations; none). The console's Settings page has them with a Save, audited settings.sharing.
  • Quotas hold here too: an upload, or accepting a show offered to you, is one more show, so the console's quota of shows and storage refuses it as it refuses a new show (quota_shows, quota_storage; CollabApi's roomForShow is the server's own check).
  • Leaving (POST …/leave): anyone but the owner; their sockets to it close with 4403.
  • Transfer (transfer.ts, tables ownership_offers, show_moves): the owner offers the show to an editor (14 days, one offer a show, withdrawn or declined), who is notified; on accepting, its daemon stops (every socket 1012), the folder moves to the new owner's shows under a free id, everyone keeps their role, the old owner becomes an editor (and is told), open invitations, requests and "Editors can invite" go with it, and the move is kept: Sharing.resolve follows it when nothing is at the old place, so old keys, links and last_show open the show where it is now (a stranger's old key is still a 404).
  • Deleting (DELETE /api/shows/<key> {confirm}): the owner only, naming the show's title; its daemon stops, every socket in it closes with 4403 and "Ada deleted Tea Time", its people, invitations, requests and offer go, the folder is moved aside and removed, it leaves everyone's archived shows, and everyone in it is told. It is the service's one delete: Studio's Show settings asks for the name to be typed and sends it as confirm.
  • A person's own list (2026-10-05): anyone in a show may copy it as a show of their own (POST …/duplicate: a new history committing as them, nothing of the owner's commits, one more show on their quotas through roomForShow) and archive it on their own list (POST …/archive, kept by key in <data>/users/<id>/archived.json, carried to the new key when the show is transferred). Studio's Show settings follows the role from the hello: the owner and editors change it, a reviewer or a viewer sees it read-only, Delete is the owner's; the daemon refuses the same (show.get, show.storage read; show.update, show.cover, show.restyle, show.clean write).
  • Notifications (notifications.ts, tables notifications, mail_prefs, mail_secret): notify(userId, kind, payload) is the internal API later phases call (HostServer.notifications), for invitation, access_request, ownership_offer, role_changed, removed, show_deleted, mention, reply and render_finished. Kept 90 days. Each person's email, by kind: at once, in the daily digest or off (invitations, requests and ownership offers at once or off only; a finished render off until asked). The digest goes at 08:00 UTC with what came before that hour and is still unread, at most one a day; a timer in the host asks every five minutes (runDigest, on the store's clock). Every email has a stop link, <base>/unsubscribe?t=<token>: the address and the kind, signed with HMAC-SHA256 under a secret kept in the database, and the RFC 8058 one-click headers. Removing someone and changing their role notify them.
  • Device tokens (devices.ts, table device_tokens): POST /api/auth/verify with client: "studio" and a device name answers {token, device} and sets no cookie. The token (rwd_ and 32 random bytes; its SHA-256 kept) signs in every API route as Authorization: Bearer; /api/session and …/open then always name the socket in full with a one-time ticket (the installed Studio has no cookie to open it with). It is one person's, ends after 180 days unused, is listed in GET /api/sessions beside the browser sessions (name, made, last used; written at most once a minute) and revoked there (DELETE /api/sessions/<id>) or by POST /api/logout on that device; a disabled account's token signs nobody in. It never opens the admin console (browser_only, 403). No CORS: the installed Studio is not a browser, and the service answers no preflight. The console lists a person's devices beside their browser sessions (name, last use) with Revoke, and its Sign out everywhere revokes them all.
  • Putting a show on Reelwright (upload.ts): POST /api/uploads takes a zip (application/zip) of history.bundle, a git bundle of every branch, and files/<path> for each file git does not keep (untracked and not ignored, or changed since the last commit), with its SHA-256 in X-Reelwright-SHA256. reelwright host pack <show> makes it (Studio's Put it on Reelwright will call the core the same way). Over MAX_UPLOAD_MB (256 by default) it is refused from its length before a byte is read, or as it streams (413, the connection closed); a wrong checksum is a 400. It is unpacked under <data>/incoming/, away from everyone's shows: entries read from the zip's directory, sizes and CRC-32s checked, no zip64 or encryption, at most 20,000 files and four times the limit unpacked, no absolute path, .. or .git part; the bundle cloned (symlinks off, the remote removed), refused if it holds a link or a submodule; the loose files laid over it; then checked as reelwright validate checks any Show Pack (422 invalid_show with the first errors). Only then is it moved into the uploader's shows, under a free id from its title, committing as them. One upload at a time a person; audited show.uploaded with its size and hash.
  • A copy to keep (GET /api/shows/<key>/pack): anyone who may read the show gets a zip of its folder under <id>/ (what git keeps and what it does not ignore; renders and caches in .reelwright/ stay behind). The owner's copy also has <id>.bundle, its whole history; nobody else's does, since every commit names the owner's address as committer. Audited show.downloaded. Studio's Show settings' Export as Show Pack downloads it on the web.
  • Proof: core/test/host-invitations.test.ts, host-notifications.test.ts, host-transfer.test.ts, host-devices.test.ts, host-upload.test.ts, host-sharing.test.ts (the migration), host-server.test.ts (M12's sharing, now by invitation), host-console.test.ts (the Sharing section saved by its form and applied to the next request; a person's installed Studios listed, one revoked, all signed out), each rule mutation-checked; on a real host, tools/web/collab-check.ts (invitation by dev mail, acceptance, roles, a newcomer's account, a device token's socket, a show up and back identical) and tools/web/sharing-check.ts (M12's checks, joining by invitation).
  • Left for the next phase: all of Studio (phase 3). The daemon takes reviewer in a hello's as and has the comment method class (phase 2, below), and the host hears the daemon's notices and tells people of finished renders (below).

Phase 2: the daemon

  • Method classes (2026-10-05): x-methods access is read, comment, write or tool (a read tool reads, any other writes). refusal(role, access) in core/src/session/daemon.ts is the whole rule: a viewer reads (anything else: session.read_only, "You can view this show: ask its owner to make you an editor", or for a comment "…but not comment on it: ask its owner to make you a reviewer or an editor"); a reviewer reads and comments (a write: session.comment_only, "You can review this show: read it and comment. Ask its owner to make you an editor to change it."); an editor and the owner do everything. The service's hello says as.role owner, editor, reviewer or viewer; the desktop names nobody and is the owner.
  • Three-way merge of script saves (2026-10-05): core/src/studio/merge.ts is the pure diff3 by line (Myers' diff in linear space, a work budget past which a stretch counts as wholly changed). A line is its text with its own break, so parts always join back to a text; CRLF reads as LF and the merge takes mine's line breaks; edits on neighbouring lines merge; insertions at one place by both conflict unless the same. core/src/studio/script-save.ts replaces M12's stale-save refusal for write_screenplay and direct_episode: with base (and baseText, else the copy a change kept: ChangeStore.recall) a save over a newer script merges, and the answer's merged (ToolsCallResult.merged: path, hash, text, with, whose changes came in) says it ("Merged with Ada's changes." leads the summary); where both changed the same lines nothing is written and the error is screenplay.conflict with error.data.conflict (ScreenplayConflict: theirs {hash, text}, with, conflicts, parts, each same text or a conflict with base, theirs, mine, each {from, count, text}). Studio resolves by joining the parts with the version kept per conflict (theirs then mine for both: resolveMerge) and saving with theirs.hash as base. A save from a text nothing kept, or over a script deleted since, is refused with screenplay.changed as before.
  • Comments (2026-10-05): a new document kind, CommentThread (comment.schema.json), one file a thread at comments/<slug>.thread.json (id thread.<slug>), validated with the show (reelwright validate) and committed to the show's own git as the person who wrote it, but left out of readShow (no tool, graph, docs.list or model sees it) and of the file watcher's doc.event. core/src/studio/comments.ts (Comments) keeps them, the daemon serves them: comments.list (read) and comments.create, reply, edit, delete, resolve, reopen (comment). Anchors: show, document (doc), timeline (episode, frame, shot), and script (path, from, to, and the quoted lines with the lines before and after). A script thread is placed by its quote each time it is listed (core/src/studio/anchors.ts): the same lines where they were (here), elsewhere (moved, neighbours first, then the nearest), lines much like them (edited, letter-pair likeness at least 0.6), else lost. Edit: your own only; delete: your own, or any as the owner (a whole thread when every comment is yours). @mentions match people in the show (everyone who said hello to the daemon, and everyone in a thread) by whole name or a first name only one has; on the desktop they are plain text and comments name nobody. Every change says comment.event (reason, id, comment, by, thread) to everyone in the show.
  • Presence with places (2026-10-05): session.place (read class) takes where the person is: section, episode, document, screenplay, line (the caret), frame and shot (the playhead), and editing (the script open has unsaved changes). session.people gives each person their color (personColor: one of twelve, from a hash of their id, so the same every time) and their place (of a person's windows, the one that said so last). It is said at most every 250 ms (peopleEveryMs): the first change at once, a burst after it once, as it ended; hellos and leaving go the same way. screenplays.get answers editing: the others whose place is that script with unsaved changes ("Ada is editing this script").
  • Live change notes (2026-10-05): a change.event for a change set just applied carries note, who did what in a sentence (core/src/studio/changes/notes.ts, from the change set alone, so another process's changes say it too): "Ada edited the Lighthouse's script.", "Bo wrote Tea Time's script.", "The showrunner changed Barnaby Culpepper for Ada.", "Bo undid a change to Barnaby Culpepper.", "Ada directed the Lighthouse (cold open).", "Ada changed Otto, the Chief and 2 more."; a person on the desktop is "You". Studio shows the others' and leaves out its own person's.
  • Notices for the service (2026-10-05): DaemonOptions.notify(notice) hears each CommentNotice, { userId, kind: "mention" | "reply", thread, comment, by: {id, name}, anchor, excerpt (200 characters at most), at }: a person @mentioned (never the writer), and, for a reply, everyone who wrote in the thread before (unless the reply mentions them); an edit tells only whom it newly mentions. reelwright serve --grant-fd prints each as a line {"notice": …} on stdout after its {port, token} line.

The service hears the daemon

  • Mentions and replies (2026-10-05): the host's processSpawner reads every stdout line a daemon prints after its first (a {"notice": …} line is a notice; anything else goes to the log), and DaemonPool.onNotice hands each one on with the daemon that said it, while that daemon runs. HostServer reads it at the edge (readDaemonNotice: a person's id, mention or reply, a thread and a comment id of the comment schema's shapes, the writer's id, an anchor, the excerpt) and passes it on only when both the person told and the writer are in that daemon's show now and are not the same person; the show is always the daemon's own, never the notice's word. It becomes notify(userId, kind, {show, by, text: the excerpt, thread, comment, anchor}): the bell, and email as the person chooses (at once by default).
  • A finished render (2026-10-05): the console's render watch (jobs.ts RenderWatch, onFinished) hears a render the host saw someone start end done on its own socket, and the host tells them (render_finished, the episode and quality, the show; the bell only, until they ask for email). A render that failed, was cancelled or whose daemon stopped under it says nothing.
  • Proof: host-daemons.test.ts (the spawner hears each notice after the first line and logs other output; the pool hands notices on with their daemon, and none once it stopped), host-notifications.test.ts (a stand-in daemon's notices reaching the bell and email by the person's own key, and none to or from someone outside the show, to the writer, or that does not read; a real reelwright serve --grant-fd daemon, where Bo's comment mentioning Ada reaches her and her reply reaches him), host-console.test.ts (a render through the pipe: done tells whoever started it, failed tells nobody), each mutation-checked.

Phase 3: the installed Studio, and merging in both editions

  • Sign in to Reelwright (2026-10-05; RwAccount, RwSignInDialog): from the first screen's "On Reelwright", the show menu's Reelwright section, the switcher (signed out) and the palette. The email, then the emailed code, against /api/auth/request and /api/auth/verify with client: "studio" and the device's name (RwAccount.device_name, "Studio on Linux"). The token is kept in reelwright-account.json beside Studio's settings, the file made owner-only (0600) before the token goes in; never in a show's folder, a log or a command line. RwAccount.token() is the one way to read it (the core's upload has it on its standard input; the plan bridge will too). Signed in, the dialog says who, on which service and as which device, with Sign out (POST /api/logout revokes the device there, then it is forgotten here). A 401 to any request forgets it and says "Reelwright signed this Studio out", and a show open from there closes. Another service is named under "another Reelwright service…" (its address with /app), or by REELWRIGHT_SERVICE.
  • Shows on Reelwright (RwAccount.list_shows, kept in the account file, so they are listed offline): the first screen's "On Reelwright" lists "Yours" and "Shared with you", each with a cloud and whose it is ("Ada's · you can edit it"); the switcher lists them after the folders, marked with a cloud and not removable. Studio keys them reelwright:<key> (RwAccount.key_of). Opening one is the browser's way: POST /api/shows/<key>/open with the token, then RwShowSession.switch_to_url to the socket it names with a one-time ticket, the show left kept warm as any; each reconnect opens it again for a new ticket (RwSessionClient.renew, set per show in _prepare), and a 403 or 404 then closes it and says it is gone or no longer shared. The hello's role holds: a viewer and a reviewer get nothing that writes (RwSessionClient.is_view_only is now true for a reviewer too, who is "Reviewing" in the rail). The hello's show.root is remembered, and RwFiles reads any path under it over /api/file with the token (RwFiles.is_remote: previews, the playback job and its frozen assets, takes); a render's Watch downloads it to Studio's folder first and Show in folder is not offered. Such a show is no recent folder; Open the show again, Reelwright's log and a problem report's show are not offered for it. Show settings treat it as the web edition does (_on_service, _ask_service): copying, packing (saved where the person says), archiving and deleting it ask the service with the token, and it has no folder to rename. A show the person archived there is listed under Archived with the folders, and the first screen lists the rest under Yours and Shared with you.
  • Put it on Reelwright (RwPutOnlineDialog, RwShowUpload, core/src/host/put.ts): the show menu's "Put it on Reelwright…" on a show of this computer (and the palette), signing in first when needed. The dialog says what goes and that the folder stays, then runs reelwright host upload <dir> --to <service> through the daemon command's core with the token on its standard input. The core packs as host pack does, now leaving renders/ out as well as .reelwright/, and sends it, one line a step: {"stage":"packing"}, {"stage":"uploading", "sent","total"} at each whole percent, then {"done":{"show","bytes"}} or {"error":{"status","code","message"}} (the service's refusal, or unreachable, not_a_show). The dialog shows a bar ("Sending 1.2 MB of 3.4 MB…", then "Reelwright is checking the show…"), then the show made with Open it on Reelwright, or the refusal in the service's words with what to do (a quota: make room; too large; a show with errors; no connection) and Try again; Stop kills the core it started. The folder is then marked (RwProjectState's online, the copy's key): its menu says "Open it on Reelwright", and its row on the first screen "Now on Reelwright too" with a cloud button to the copy.
  • Merging, and the conflict view (RwScriptEditor, RwConflictView; both editions): Save and Direct send base and baseText, the text they started from, so the daemon merges whatever was saved since; a draft put back from a show's place is based on its saved text and merges too (its notice says Save merges). A merged answer puts the merged script in the editor as one Ctrl+Z step, the caret where it was, and says "Merged with Ada's changes." in the editor's usual colour (RwConflictView.merged_note, as the core's mergedNote); typing on while it saved keeps the person's text, based on what they sent, for the next save to merge. screenplay.conflict shows the conflict view in the script's place: what the daemon said, and per place a card with "As it was", "Theirs" (whose: "Ada's") and "Yours" side by side, Keep Theirs, Yours, Both (theirs, then the person's, as resolveMerge) or As it was, and the kept lines editable; what both kept is a line count between them. Save waits for a choice in every place (the line under the message counts those left), joins the parts (resolved_text: an edited place keeps a line of its own) and saves, or directs again, with theirs' hash and text as its base; Back leaves the person's text unsaved.
  • Proof: core/test/host-upload.test.ts (renders and caches stay out of the pack; putShow as the token's person, progress in order to the last byte; a quota, too large, a token the service does not know, nothing listening, no show; host upload reading the token from stdin, never printing it); engine/tests/ui/rw_account_test.gd, rw_conflict_view_test.gd, engine/tests/app/rw_studio_reelwright_test.gd, and rw_script_editor_test.gd (the base text), each mutation-checked; and on a real service, tools/web/desktop-check.ts: the installed Studio, headless in Xvfb with a screenshot of each step, signs Bo in with the mailed code, lists and opens Ada's show as an editor, saves its script merged with Ada's change and then resolves her change to the same line in the conflict view (her show's file has every change), puts a show of his computer on Reelwright (no renders) and opens the copy, the token owner-only and in no log, transcript, screenshot or show; then the web export in Chromium, as Ada, resolves a conflict with Bo's save.
  • Left: Connect Claude for a show on Reelwright in the installed Studio (the person's key on the service; with the plan bridge). The Share dialog, invitations in Studio, the bell, presence and follow, and comments are another part of this phase.