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/requestand/api/auth/verifywithclient: "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/filewith 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
- Service: invitations with acceptance, the four roles and method classes, requests, leave, transfer, delete, notifications and their emails, device tokens and the upload.
- Daemon: comments, three-way merge on save, presence with places, live change notes.
- 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.
ShareRoleiseditor,reviewerorviewer(the owner is the workspace's, never a share). A database from M12 has itssharestable made again with the wider check, every row kept (store.ts). The host says the role in the hello'sas(reviewertoo), and the daemon's method classes hold each role to it (phase 2). - Invitations (
invitations.ts, tableshow_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, thenshow.shared). The email ("Ada invited you to edit Tea Time on Reelwright", with Accept and the note) goes throughmail.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, auditeduser.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, tableshow_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.tssharingSettingsOfoversettings.ts; the rules ininvitations.tssee only theSharingSettingsinterface). 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, auditedsettings.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'sroomForShowis the server's own check). - Leaving (
POST …/leave): anyone but the owner; their sockets to it close with 4403. - Transfer (
transfer.ts, tablesownership_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.resolvefollows it when nothing is at the old place, so old keys, links andlast_showopen 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 asconfirm. - 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 throughroomForShow) 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.storageread;show.update,show.cover,show.restyle,show.cleanwrite). - Notifications (
notifications.ts, tablesnotifications,mail_prefs,mail_secret):notify(userId, kind, payload)is the internal API later phases call (HostServer.notifications), forinvitation,access_request,ownership_offer,role_changed,removed,show_deleted,mention,replyandrender_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, tabledevice_tokens):POST /api/auth/verifywithclient: "studio"and adevicename answers{token, device}and sets no cookie. The token (rwd_and 32 random bytes; its SHA-256 kept) signs in every API route asAuthorization: Bearer;/api/sessionand…/openthen 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 inGET /api/sessionsbeside the browser sessions (name, made, last used; written at most once a minute) and revoked there (DELETE /api/sessions/<id>) or byPOST /api/logouton 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/uploadstakes a zip (application/zip) ofhistory.bundle, a git bundle of every branch, andfiles/<path>for each file git does not keep (untracked and not ignored, or changed since the last commit), with its SHA-256 inX-Reelwright-SHA256.reelwright host pack <show>makes it (Studio's Put it on Reelwright will call the core the same way). OverMAX_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.gitpart; 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 asreelwright validatechecks any Show Pack (422invalid_showwith 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; auditedshow.uploadedwith 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. Auditedshow.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) andtools/web/sharing-check.ts(M12's checks, joining by invitation). - Left for the next phase: all of Studio (phase 3). The daemon takes
reviewerin a hello'sasand has thecommentmethod 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-methodsaccess isread,comment,writeortool(a read tool reads, any other writes).refusal(role, access)incore/src/session/daemon.tsis 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 saysas.roleowner,editor,reviewerorviewer; the desktop names nobody and is the owner. - Three-way merge of script saves (2026-10-05):
core/src/studio/merge.tsis 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.tsreplaces M12's stale-save refusal forwrite_screenplayanddirect_episode: withbase(andbaseText, else the copy a change kept:ChangeStore.recall) a save over a newer script merges, and the answer'smerged(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 isscreenplay.conflictwitherror.data.conflict(ScreenplayConflict:theirs{hash, text},with,conflicts,parts, eachsametext or aconflictwithbase,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 withtheirs.hashas base. A save from a text nothing kept, or over a script deleted since, is refused withscreenplay.changedas before. - Comments (2026-10-05): a new document kind,
CommentThread(comment.schema.json), one file a thread atcomments/<slug>.thread.json(idthread.<slug>), validated with the show (reelwright validate) and committed to the show's own git as the person who wrote it, but left out ofreadShow(no tool, graph, docs.list or model sees it) and of the file watcher'sdoc.event.core/src/studio/comments.ts(Comments) keeps them, the daemon serves them:comments.list(read) andcomments.create,reply,edit,delete,resolve,reopen(comment). Anchors:show,document(doc),timeline(episode,frame,shot), andscript(path,from,to, and thequoted lines with the linesbeforeandafter). 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), elselost. 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 sayscomment.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),frameandshot(the playhead), andediting(the script open has unsaved changes).session.peoplegives each person theircolor(personColor: one of twelve, from a hash of their id, so the same every time) and theirplace(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.getanswersediting: the others whose place is that script with unsaved changes ("Ada is editing this script"). - Live change notes (2026-10-05): a
change.eventfor a change set just applied carriesnote, 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 eachCommentNotice,{ 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-fdprints 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
processSpawnerreads every stdout line a daemon prints after its first (a{"notice": …}line is a notice; anything else goes to the log), andDaemonPool.onNoticehands each one on with the daemon that said it, while that daemon runs.HostServerreads it at the edge (readDaemonNotice: a person's id,mentionorreply, 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 becomesnotify(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.tsRenderWatch,onFinished) hears a render the host saw someone start enddoneon 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 realreelwright serve --grant-fddaemon, 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/requestand/api/auth/verifywithclient: "studio"and the device's name (RwAccount.device_name, "Studio on Linux"). The token is kept inreelwright-account.jsonbeside 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/logoutrevokes 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 byREELWRIGHT_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 themreelwright:<key>(RwAccount.key_of). Opening one is the browser's way:POST /api/shows/<key>/openwith the token, thenRwShowSession.switch_to_urlto 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_onlyis now true for a reviewer too, who is "Reviewing" in the rail). The hello'sshow.rootis remembered, andRwFilesreads any path under it over/api/filewith 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 runsreelwright host upload <dir> --to <service>through the daemon command's core with the token on its standard input. The core packs ashost packdoes, now leavingrenders/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, orunreachable,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'sonline, 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 sendbaseandbaseText, 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'smergedNote); typing on while it saved keeps the person's text, based on what they sent, for the next save to merge.screenplay.conflictshows 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, asresolveMerge) 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;putShowas 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 uploadreading 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, andrw_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.