1// Package room shares workbooks between the people 012 serve hosts: a2// room is one workbook, opened by every session that opens the same3// file (or joins the same named room), and each participant has a seat4// in it. The server holds the workbook, so there is no merging: every5// participant acts on it in turn, under the room's lock, and the steps6// they make are ordered by the workbook's one mutation path (Batch and7// its undo steps), each attributed to its author, as a stream of8// operations (sheet.Op) the room hands every participant. A participant9// is anything with a name that can be told the room changed10// (Participant): a served session's model, a local session sharing its11// workbook with the agent, or the agent itself, an MCP client attached in12// live mode (internal/cowork, the Agent interface). See13// docs/contributing/architecture.md.14package room1516import (17 "slices"18 "sync"19 "time"2021 "src.rest/cat/012/internal/sheet"22)2324// Participant is one who takes part in a room. The room asks only this25// of it; everything else a participant does, it does through its Seat.26type Participant interface {27 // Name is who they are to the others: the SSH user of a session.28 Name() string29 // Notify tells them the room changed: someone else's operations,30 // where the others are, who writes, a save, a message posted for31 // them. It's called with the room locked, so it must neither block32 // nor call back into the room: they look at what changed on their33 // next turn (Seat.Do).34 Notify()35}3637// Agent is a participant that is one of the agents (live mode, an MCP client38// attached): the others see it marked as one, and it keeps nothing the39// room runs.40type Agent interface {41 Participant42 Agent()43}4445// Mode is who may change a room's workbook.46type Mode int4748const (49 // Edit lets every participant edit.50 Edit Mode = iota51 // View lets one participant, the writer, edit; the others follow.52 // The writer hands the role over explicitly (Seat.HandTo).53 View54)5556// Colors is how many colors participants are told apart by.57const Colors = 65859// Room is one shared workbook and the seats of those in it.60type Room struct {61 key string62 reg *Registry6364 mu sync.Mutex65 book *sheet.Workbook66 seats []*Seat // in the order they joined67 nextID int68 mode Mode69 writer int // the seat that writes in View mode70 saved Saved71 values map[string]any72 inbox []any73 closed bool7475 // seen is the last operation the room has looked at, version the76 // workbook's when a turn last ended, and marks the latest77 // operations' cells, for who changed what.78 seen uint6479 version uint6480 marks []Mark81 // alias is a key the room was saved under this turn, for the82 // registry once the room is unlocked.83 alias string84}8586// Saved is the room's file as last saved or opened: what every session87// in it counts unsaved changes from.88type Saved struct {89 State int // the workbook's StateID then90 Outputs int // its OutputsChanged then91 Name string // the file's name, as typed, "" for none92 Key string // the file's room key, its path: the room is its room too93 Stamp any // the file on disk then, the UI's to compare94}9596// Presence is where a participant is.97type Presence struct {98 Sheet *sheet.Sheet99 Cursor sheet.Addr100 Selection sheet.Rect101 Editing bool // typing into the cell at Cursor102}103104// Seat is a participant's place in a room.105type Seat struct {106 room *Room107 id int108 name string109 color int110 p Participant111 joined time.Time112 presence Presence113 moved bool // presence changed this turn114 left bool115 agent bool116}117118// Peer is what a participant sees of another: who, in what color,119// where, and whether they write.120type Peer struct {121 ID int122 Name string123 Color int124 Presence Presence125 Writing bool126 Joined time.Time127 Agent bool128}129130// Mark is an operation's place in the workbook: who changed which131// cells, for showing whose change a cell holds.132type Mark struct {133 Seq uint64134 Author int135 Name string136 Color int137 Kind sheet.OpKind138 Label string139 Sheet *sheet.Sheet140 Rect sheet.Rect141 At time.Time142}143144// maxMarks is how many of the latest operations the room remembers the145// cells of.146const maxMarks = 256147148// ID identifies the seat in the room: the author of its steps.149func (s *Seat) ID() int { return s.id }150151// Name is the participant's name.152func (s *Seat) Name() string { return s.name }153154// Color is the participant's color, from 0 to Colors-1.155func (s *Seat) Color() int { return s.color }156157// Key is the room's key: the file's path, or @name for a named room.158func (s *Seat) Key() string { return s.room.key }159160// Do runs fn as the participant's turn: with the room locked, the161// workbook's steps made in the participant's name. When the turn ends,162// the others are told of what changed.163func (s *Seat) Do(fn func(w *sheet.Workbook)) {164 r := s.room165 r.mu.Lock()166 defer r.endTurn(s)167 r.book.SetAuthor(s.id)168 fn(r.book)169}170171// endTurn takes note of the operations the turn made, and tells the172// others when anything changed.173func (r *Room) endTurn(by *Seat) {174 changed := r.catchUp()175 if v := r.book.Version(); v != r.version {176 r.version, changed = v, true177 }178 if changed || by.moved {179 by.moved = false180 r.notify(by)181 }182 alias := r.alias183 r.alias = ""184 r.mu.Unlock()185 if alias != "" {186 r.reg.alias(r, alias)187 }188}189190// catchUp marks the cells of the operations since the room last looked.191func (r *Room) catchUp() bool {192 ops, last := r.book.OpsSince(r.seen)193 if last == r.seen {194 return false195 }196 r.seen = last197 now := time.Now()198 for _, o := range ops {199 if o.Tabs || o.Macros || o.Sheet == nil {200 continue201 }202 m := Mark{Seq: o.Seq, Author: o.Author, Kind: o.Kind, Label: o.Label, Sheet: o.Sheet, Rect: o.Focus, At: now}203 if s := r.seat(o.Author); s != nil {204 m.Name, m.Color = s.name, s.color205 }206 r.marks = append(r.marks, m)207 }208 if len(r.marks) > maxMarks {209 r.marks = slices.Delete(r.marks, 0, len(r.marks)-maxMarks)210 }211 return true212}213214// notify tells every seat but by that the room changed.215func (r *Room) notify(by *Seat) {216 for _, s := range r.seats {217 if s != by {218 s.p.Notify()219 }220 }221}222223// seat is the seat with id, nil when it left.224func (r *Room) seat(id int) *Seat {225 for _, s := range r.seats {226 if s.id == id {227 return s228 }229 }230 return nil231}232233// Book is the room's workbook. Only use it in a turn (Do).234func (s *Seat) Book() *sheet.Workbook { return s.room.book }235236// Move sets where the participant is, for the others to see once the237// turn ends. Call it in a turn.238func (s *Seat) Move(p Presence) {239 if p != s.presence {240 s.presence, s.moved = p, true241 }242}243244// Peers are the others in the room, in the order they joined. Call it245// in a turn.246func (s *Seat) Peers() []Peer {247 var out []Peer248 for _, o := range s.room.seats {249 if o != s {250 out = append(out, o.peer())251 }252 }253 return out254}255256func (s *Seat) peer() Peer {257 return Peer{ID: s.id, Name: s.name, Color: s.color, Presence: s.presence, Writing: s.Writing(), Joined: s.joined, Agent: s.agent}258}259260// Agent reports whether the participant is one of the agents.261func (s *Seat) Agent() bool { return s.agent }262263// Closed reports whether the room closed: its last person left. Call264// it in a turn.265func (s *Seat) Closed() bool { return s.room.closed }266267// Touch tells the others, once the turn ends, that something they268// share changed without the workbook changing: a value of the room's269// (Value), such as the agent's suggestions. Call it in a turn.270func (s *Seat) Touch() { s.moved = true }271272// Peer is the participant with id, and whether they're here. Call it in273// a turn.274func (s *Seat) Peer(id int) (Peer, bool) {275 if o := s.room.seat(id); o != nil {276 return o.peer(), true277 }278 return Peer{}, false279}280281// Others is how many others are in the room. Call it in a turn.282func (s *Seat) Others() int { return len(s.room.seats) - 1 }283284// People is how many other people are in the room, agents left out:285// those who keep the workbook open when this one leaves. Call it in a286// turn.287func (s *Seat) People() int {288 n := 0289 for _, o := range s.room.seats {290 if o != s && !o.agent {291 n++292 }293 }294 return n295}296297// Marks are the operations since seq, oldest first, whoever made them.298// Call it in a turn.299func (s *Seat) Marks(since uint64) []Mark {300 m := s.room.marks301 i, _ := slices.BinarySearchFunc(m, since+1, func(x Mark, seq uint64) int { return int(x.Seq) - int(seq) })302 return m[i:]303}304305// Last is the latest operation's number. Call it in a turn.306func (s *Seat) Last() uint64 { return s.room.seen }307308// Mode is who may edit the room's workbook.309func (s *Seat) Mode() Mode { return s.room.mode }310311// Writing reports whether the participant may edit: in an Edit room312// everyone, in a View room the writer.313func (s *Seat) Writing() bool { return s.room.mode == Edit || s.room.writer == s.id }314315// Writer is the writer's name in a View room, "" in an Edit room.316func (s *Seat) Writer() string {317 if s.room.mode == Edit {318 return ""319 }320 if w := s.room.seat(s.room.writer); w != nil {321 return w.name322 }323 return ""324}325326// HandTo hands the writer's role to the participant with id, reporting327// false when this one doesn't write or there's no such participant.328// Call it in a turn.329func (s *Seat) HandTo(id int) bool {330 r := s.room331 if r.mode != View || r.writer != s.id || r.seat(id) == nil {332 return false333 }334 r.writer, s.moved = id, true335 return true336}337338// Saved is the room's file as last saved or opened. Call it in a turn.339func (s *Seat) Saved() Saved { return s.room.saved }340341// SetSaved records a save, or the file the room was opened from, for342// every session. Call it in a turn.343func (s *Seat) SetSaved(v Saved) {344 s.room.saved, s.moved = v, true345 if v.Key != "" && v.Key != s.room.key {346 s.room.alias = v.Key347 }348}349350// Value is the room's value for key, made by make the first time: state351// every participant shares, such as the notebook cells running. Call it352// in a turn.353func (s *Seat) Value(key string, make func() any) any {354 r := s.room355 if r.values == nil {356 r.values = map[string]any{}357 }358 v, ok := r.values[key]359 if !ok {360 v = make()361 r.values[key] = v362 }363 return v364}365366// Keeper reports whether the participant keeps what the room runs on367// its own (linked files followed, notebook cells running): the person368// who has been there longest, as agents run nothing of the room's. Call369// it in a turn.370func (s *Seat) Keeper() bool { return s.room.keeper() == s }371372// keeper is the person there longest, nil when only agents are.373func (r *Room) keeper() *Seat {374 for _, s := range r.seats {375 if !s.agent {376 return s377 }378 }379 return nil380}381382// Post leaves msg for the keeper, whoever it is when it takes it: what383// the room runs reports back this way rather than to the participant384// that started it, who may have left. It may be called from any385// goroutine, outside a turn.386func (s *Seat) Post(msg any) {387 r := s.room388 r.mu.Lock()389 defer r.mu.Unlock()390 k := r.keeper()391 if r.closed || k == nil {392 return393 }394 r.inbox = append(r.inbox, msg)395 k.p.Notify()396}397398// Take is what was posted for the keeper, if this is the keeper. Call399// it in a turn.400func (s *Seat) Take() []any {401 if !s.Keeper() || len(s.room.inbox) == 0 {402 return nil403 }404 in := s.room.inbox405 s.room.inbox = nil406 return in407}408409// Leave gives up the seat, reporting whether it was the last person410// there: the room closes then, and its workbook is the participant's411// alone. Agents still there are told, and find the room closed412// (Closed).413func (s *Seat) Leave() (last bool) {414 r := s.room415 r.mu.Lock()416 if s.left {417 r.mu.Unlock()418 return false419 }420 s.left = true421 i := slices.Index(r.seats, s)422 r.seats = slices.Delete(r.seats, i, i+1)423 empty := r.keeper() == nil424 if !empty && r.mode == View && r.writer == s.id {425 r.writer = r.keeper().id426 }427 r.notify(nil)428 r.mu.Unlock()429 if empty {430 return r.reg.close(r)431 }432 return false433}