@composer-app/mcp 0.0.1-beta.4 → 0.0.1-beta.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,16 +6,373 @@ import {
6
6
  CallToolRequestSchema,
7
7
  ListToolsRequestSchema
8
8
  } from "@modelcontextprotocol/sdk/types.js";
9
- import { nanoid as nanoid3 } from "nanoid";
9
+ import { nanoid as nanoid4 } from "nanoid";
10
+ import * as Y6 from "yjs";
10
11
  import fs3 from "fs/promises";
11
12
  import path3 from "path";
12
13
  import os2 from "os";
13
14
  import http from "http";
14
15
  import { randomUUID } from "crypto";
15
16
 
17
+ // ../shared/src/activity.ts
18
+ import { nanoid } from "nanoid";
19
+ var ACTIVITY_CAP = 200;
20
+ var NOTIFIABLE_TYPES = /* @__PURE__ */ new Set([
21
+ "comment",
22
+ "suggestion",
23
+ "reply",
24
+ "accept",
25
+ "reject"
26
+ ]);
27
+ function isActivityNotifiable(event) {
28
+ return NOTIFIABLE_TYPES.has(event.type);
29
+ }
30
+ function getActivityMap(doc) {
31
+ return doc.getMap("activity");
32
+ }
33
+ function getActivityStateMap(doc) {
34
+ return doc.getMap("activityState");
35
+ }
36
+ function emitActivity(doc, event, opts) {
37
+ if (opts?.silent) return;
38
+ const activity = getActivityMap(doc);
39
+ const id = nanoid();
40
+ activity.set(id, { ...event, id });
41
+ pruneIfOverCap(doc);
42
+ }
43
+ function pruneIfOverCap(doc) {
44
+ const activity = getActivityMap(doc);
45
+ const overBy = activity.size - ACTIVITY_CAP;
46
+ if (overBy <= 0) return;
47
+ const nonNotifiable = [];
48
+ const notifiable = [];
49
+ activity.forEach((value, key) => {
50
+ const event = value;
51
+ const entry = { key, createdAt: event.createdAt };
52
+ if (isActivityNotifiable(event)) notifiable.push(entry);
53
+ else nonNotifiable.push(entry);
54
+ });
55
+ nonNotifiable.sort((a, b) => a.createdAt - b.createdAt);
56
+ notifiable.sort((a, b) => a.createdAt - b.createdAt);
57
+ const toRemove = [...nonNotifiable, ...notifiable].slice(0, overBy);
58
+ const stateMap = getActivityStateMap(doc);
59
+ for (const entry of toRemove) {
60
+ activity.delete(entry.key);
61
+ stateMap.forEach((_value, stateKeyStr) => {
62
+ if (stateKeyStr.startsWith(`${entry.key}:`)) {
63
+ stateMap.delete(stateKeyStr);
64
+ }
65
+ });
66
+ }
67
+ }
68
+ function textPreview(text, maxLen = 80) {
69
+ if (!text) return void 0;
70
+ return text.length > maxLen ? text.slice(0, maxLen) + "\u2026" : text;
71
+ }
72
+ function getParticipantUserIds(thread) {
73
+ const ids = /* @__PURE__ */ new Set();
74
+ if (thread.authorUserId) ids.add(thread.authorUserId);
75
+ for (const r of thread.replies ?? []) {
76
+ if (r.authorUserId) ids.add(r.authorUserId);
77
+ }
78
+ return [...ids];
79
+ }
80
+
16
81
  // src/roomState.ts
17
- import * as Y4 from "yjs";
82
+ import * as Y5 from "yjs";
18
83
  import YProvider from "y-partyserver/provider";
84
+
85
+ // ../node_modules/lib0/math.js
86
+ var floor = Math.floor;
87
+ var isNaN = Number.isNaN;
88
+
89
+ // ../node_modules/lib0/set.js
90
+ var create = () => /* @__PURE__ */ new Set();
91
+
92
+ // ../node_modules/lib0/array.js
93
+ var from = Array.from;
94
+
95
+ // ../node_modules/lib0/time.js
96
+ var getUnixTime = Date.now;
97
+
98
+ // ../node_modules/lib0/map.js
99
+ var create2 = () => /* @__PURE__ */ new Map();
100
+ var setIfUndefined = (map, key, createT) => {
101
+ let set = map.get(key);
102
+ if (set === void 0) {
103
+ map.set(key, set = createT());
104
+ }
105
+ return set;
106
+ };
107
+
108
+ // ../node_modules/lib0/observable.js
109
+ var Observable = class {
110
+ constructor() {
111
+ this._observers = create2();
112
+ }
113
+ /**
114
+ * @param {N} name
115
+ * @param {function} f
116
+ */
117
+ on(name, f) {
118
+ setIfUndefined(this._observers, name, create).add(f);
119
+ }
120
+ /**
121
+ * @param {N} name
122
+ * @param {function} f
123
+ */
124
+ once(name, f) {
125
+ const _f = (...args) => {
126
+ this.off(name, _f);
127
+ f(...args);
128
+ };
129
+ this.on(name, _f);
130
+ }
131
+ /**
132
+ * @param {N} name
133
+ * @param {function} f
134
+ */
135
+ off(name, f) {
136
+ const observers = this._observers.get(name);
137
+ if (observers !== void 0) {
138
+ observers.delete(f);
139
+ if (observers.size === 0) {
140
+ this._observers.delete(name);
141
+ }
142
+ }
143
+ }
144
+ /**
145
+ * Emit a named event. All registered event listeners that listen to the
146
+ * specified name will receive the event.
147
+ *
148
+ * @todo This should catch exceptions
149
+ *
150
+ * @param {N} name The event name.
151
+ * @param {Array<any>} args The arguments that are applied to the event listener.
152
+ */
153
+ emit(name, args) {
154
+ return from((this._observers.get(name) || create2()).values()).forEach((f) => f(...args));
155
+ }
156
+ destroy() {
157
+ this._observers = create2();
158
+ }
159
+ };
160
+
161
+ // ../node_modules/lib0/trait/equality.js
162
+ var EqualityTraitSymbol = /* @__PURE__ */ Symbol("Equality");
163
+
164
+ // ../node_modules/lib0/object.js
165
+ var keys = Object.keys;
166
+ var size = (obj) => keys(obj).length;
167
+ var hasProperty = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key);
168
+
169
+ // ../node_modules/lib0/function.js
170
+ var equalityDeep = (a, b) => {
171
+ if (a === b) {
172
+ return true;
173
+ }
174
+ if (a == null || b == null || a.constructor !== b.constructor && (a.constructor || Object) !== (b.constructor || Object)) {
175
+ return false;
176
+ }
177
+ if (a[EqualityTraitSymbol] != null) {
178
+ return a[EqualityTraitSymbol](b);
179
+ }
180
+ switch (a.constructor) {
181
+ case ArrayBuffer:
182
+ a = new Uint8Array(a);
183
+ b = new Uint8Array(b);
184
+ // eslint-disable-next-line no-fallthrough
185
+ case Uint8Array: {
186
+ if (a.byteLength !== b.byteLength) {
187
+ return false;
188
+ }
189
+ for (let i = 0; i < a.length; i++) {
190
+ if (a[i] !== b[i]) {
191
+ return false;
192
+ }
193
+ }
194
+ break;
195
+ }
196
+ case Set: {
197
+ if (a.size !== b.size) {
198
+ return false;
199
+ }
200
+ for (const value of a) {
201
+ if (!b.has(value)) {
202
+ return false;
203
+ }
204
+ }
205
+ break;
206
+ }
207
+ case Map: {
208
+ if (a.size !== b.size) {
209
+ return false;
210
+ }
211
+ for (const key of a.keys()) {
212
+ if (!b.has(key) || !equalityDeep(a.get(key), b.get(key))) {
213
+ return false;
214
+ }
215
+ }
216
+ break;
217
+ }
218
+ case void 0:
219
+ case Object:
220
+ if (size(a) !== size(b)) {
221
+ return false;
222
+ }
223
+ for (const key in a) {
224
+ if (!hasProperty(a, key) || !equalityDeep(a[key], b[key])) {
225
+ return false;
226
+ }
227
+ }
228
+ break;
229
+ case Array:
230
+ if (a.length !== b.length) {
231
+ return false;
232
+ }
233
+ for (let i = 0; i < a.length; i++) {
234
+ if (!equalityDeep(a[i], b[i])) {
235
+ return false;
236
+ }
237
+ }
238
+ break;
239
+ default:
240
+ return false;
241
+ }
242
+ return true;
243
+ };
244
+
245
+ // ../node_modules/y-protocols/awareness.js
246
+ import * as Y from "yjs";
247
+ var outdatedTimeout = 3e4;
248
+ var Awareness = class extends Observable {
249
+ /**
250
+ * @param {Y.Doc} doc
251
+ */
252
+ constructor(doc) {
253
+ super();
254
+ this.doc = doc;
255
+ this.clientID = doc.clientID;
256
+ this.states = /* @__PURE__ */ new Map();
257
+ this.meta = /* @__PURE__ */ new Map();
258
+ this._checkInterval = /** @type {any} */
259
+ setInterval(() => {
260
+ const now = getUnixTime();
261
+ if (this.getLocalState() !== null && outdatedTimeout / 2 <= now - /** @type {{lastUpdated:number}} */
262
+ this.meta.get(this.clientID).lastUpdated) {
263
+ this.setLocalState(this.getLocalState());
264
+ }
265
+ const remove = [];
266
+ this.meta.forEach((meta, clientid) => {
267
+ if (clientid !== this.clientID && outdatedTimeout <= now - meta.lastUpdated && this.states.has(clientid)) {
268
+ remove.push(clientid);
269
+ }
270
+ });
271
+ if (remove.length > 0) {
272
+ removeAwarenessStates(this, remove, "timeout");
273
+ }
274
+ }, floor(outdatedTimeout / 10));
275
+ doc.on("destroy", () => {
276
+ this.destroy();
277
+ });
278
+ this.setLocalState({});
279
+ }
280
+ destroy() {
281
+ this.emit("destroy", [this]);
282
+ this.setLocalState(null);
283
+ super.destroy();
284
+ clearInterval(this._checkInterval);
285
+ }
286
+ /**
287
+ * @return {Object<string,any>|null}
288
+ */
289
+ getLocalState() {
290
+ return this.states.get(this.clientID) || null;
291
+ }
292
+ /**
293
+ * @param {Object<string,any>|null} state
294
+ */
295
+ setLocalState(state) {
296
+ const clientID = this.clientID;
297
+ const currLocalMeta = this.meta.get(clientID);
298
+ const clock = currLocalMeta === void 0 ? 0 : currLocalMeta.clock + 1;
299
+ const prevState = this.states.get(clientID);
300
+ if (state === null) {
301
+ this.states.delete(clientID);
302
+ } else {
303
+ this.states.set(clientID, state);
304
+ }
305
+ this.meta.set(clientID, {
306
+ clock,
307
+ lastUpdated: getUnixTime()
308
+ });
309
+ const added = [];
310
+ const updated = [];
311
+ const filteredUpdated = [];
312
+ const removed = [];
313
+ if (state === null) {
314
+ removed.push(clientID);
315
+ } else if (prevState == null) {
316
+ if (state != null) {
317
+ added.push(clientID);
318
+ }
319
+ } else {
320
+ updated.push(clientID);
321
+ if (!equalityDeep(prevState, state)) {
322
+ filteredUpdated.push(clientID);
323
+ }
324
+ }
325
+ if (added.length > 0 || filteredUpdated.length > 0 || removed.length > 0) {
326
+ this.emit("change", [{ added, updated: filteredUpdated, removed }, "local"]);
327
+ }
328
+ this.emit("update", [{ added, updated, removed }, "local"]);
329
+ }
330
+ /**
331
+ * @param {string} field
332
+ * @param {any} value
333
+ */
334
+ setLocalStateField(field, value) {
335
+ const state = this.getLocalState();
336
+ if (state !== null) {
337
+ this.setLocalState({
338
+ ...state,
339
+ [field]: value
340
+ });
341
+ }
342
+ }
343
+ /**
344
+ * @return {Map<number,Object<string,any>>}
345
+ */
346
+ getStates() {
347
+ return this.states;
348
+ }
349
+ };
350
+ var removeAwarenessStates = (awareness, clients, origin) => {
351
+ const removed = [];
352
+ for (let i = 0; i < clients.length; i++) {
353
+ const clientID = clients[i];
354
+ if (awareness.states.has(clientID)) {
355
+ awareness.states.delete(clientID);
356
+ if (clientID === awareness.clientID) {
357
+ const curMeta = (
358
+ /** @type {MetaClientState} */
359
+ awareness.meta.get(clientID)
360
+ );
361
+ awareness.meta.set(clientID, {
362
+ clock: curMeta.clock + 1,
363
+ lastUpdated: getUnixTime()
364
+ });
365
+ }
366
+ removed.push(clientID);
367
+ }
368
+ }
369
+ if (removed.length > 0) {
370
+ awareness.emit("change", [{ added: [], updated: [], removed }, origin]);
371
+ awareness.emit("update", [{ added: [], updated: [], removed }, origin]);
372
+ }
373
+ };
374
+
375
+ // src/roomState.ts
19
376
  import WebSocket from "ws";
20
377
 
21
378
  // ../shared/src/editor-extensions.ts
@@ -445,10 +802,10 @@ function findDiffEnd(a, b, posA, posB) {
445
802
  for (let iA = a.childCount, iB = b.childCount; ; ) {
446
803
  if (iA == 0 || iB == 0)
447
804
  return iA == iB ? null : { a: posA, b: posB };
448
- let childA = a.child(--iA), childB = b.child(--iB), size = childA.nodeSize;
805
+ let childA = a.child(--iA), childB = b.child(--iB), size2 = childA.nodeSize;
449
806
  if (childA == childB) {
450
- posA -= size;
451
- posB -= size;
807
+ posA -= size2;
808
+ posB -= size2;
452
809
  continue;
453
810
  }
454
811
  if (!childA.sameMarkup(childB))
@@ -467,18 +824,18 @@ function findDiffEnd(a, b, posA, posB) {
467
824
  if (inner)
468
825
  return inner;
469
826
  }
470
- posA -= size;
471
- posB -= size;
827
+ posA -= size2;
828
+ posB -= size2;
472
829
  }
473
830
  }
474
831
  var Fragment = class _Fragment {
475
832
  /**
476
833
  @internal
477
834
  */
478
- constructor(content, size) {
835
+ constructor(content, size2) {
479
836
  this.content = content;
480
- this.size = size || 0;
481
- if (size == null)
837
+ this.size = size2 || 0;
838
+ if (size2 == null)
482
839
  for (let i = 0; i < content.length; i++)
483
840
  this.size += content[i].nodeSize;
484
841
  }
@@ -487,12 +844,12 @@ var Fragment = class _Fragment {
487
844
  positions (relative to start of this fragment). Doesn't descend
488
845
  into a node when the callback returns `false`.
489
846
  */
490
- nodesBetween(from, to, f, nodeStart = 0, parent) {
847
+ nodesBetween(from2, to, f, nodeStart = 0, parent) {
491
848
  for (let i = 0, pos = 0; pos < to; i++) {
492
849
  let child = this.content[i], end = pos + child.nodeSize;
493
- if (end > from && f(child, nodeStart + pos, parent || null, i) !== false && child.content.size) {
850
+ if (end > from2 && f(child, nodeStart + pos, parent || null, i) !== false && child.content.size) {
494
851
  let start = pos + 1;
495
- child.nodesBetween(Math.max(0, from - start), Math.min(child.content.size, to - start), f, nodeStart + start);
852
+ child.nodesBetween(Math.max(0, from2 - start), Math.min(child.content.size, to - start), f, nodeStart + start);
496
853
  }
497
854
  pos = end;
498
855
  }
@@ -509,10 +866,10 @@ var Fragment = class _Fragment {
509
866
  Extract the text between `from` and `to`. See the same method on
510
867
  [`Node`](https://prosemirror.net/docs/ref/#model.Node.textBetween).
511
868
  */
512
- textBetween(from, to, blockSeparator, leafText) {
869
+ textBetween(from2, to, blockSeparator, leafText) {
513
870
  let text = "", first = true;
514
- this.nodesBetween(from, to, (node, pos) => {
515
- let nodeText = node.isText ? node.text.slice(Math.max(from, pos) - pos, to - pos) : !node.isLeaf ? "" : leafText ? typeof leafText === "function" ? leafText(node) : leafText : node.type.spec.leafText ? node.type.spec.leafText(node) : "";
871
+ this.nodesBetween(from2, to, (node, pos) => {
872
+ let nodeText = node.isText ? node.text.slice(Math.max(from2, pos) - pos, to - pos) : !node.isLeaf ? "" : leafText ? typeof leafText === "function" ? leafText(node) : leafText : node.type.spec.leafText ? node.type.spec.leafText(node) : "";
516
873
  if (node.isBlock && (node.isLeaf && nodeText || node.isTextblock) && blockSeparator) {
517
874
  if (first)
518
875
  first = false;
@@ -544,36 +901,36 @@ var Fragment = class _Fragment {
544
901
  /**
545
902
  Cut out the sub-fragment between the two given positions.
546
903
  */
547
- cut(from, to = this.size) {
548
- if (from == 0 && to == this.size)
904
+ cut(from2, to = this.size) {
905
+ if (from2 == 0 && to == this.size)
549
906
  return this;
550
- let result = [], size = 0;
551
- if (to > from)
907
+ let result = [], size2 = 0;
908
+ if (to > from2)
552
909
  for (let i = 0, pos = 0; pos < to; i++) {
553
910
  let child = this.content[i], end = pos + child.nodeSize;
554
- if (end > from) {
555
- if (pos < from || end > to) {
911
+ if (end > from2) {
912
+ if (pos < from2 || end > to) {
556
913
  if (child.isText)
557
- child = child.cut(Math.max(0, from - pos), Math.min(child.text.length, to - pos));
914
+ child = child.cut(Math.max(0, from2 - pos), Math.min(child.text.length, to - pos));
558
915
  else
559
- child = child.cut(Math.max(0, from - pos - 1), Math.min(child.content.size, to - pos - 1));
916
+ child = child.cut(Math.max(0, from2 - pos - 1), Math.min(child.content.size, to - pos - 1));
560
917
  }
561
918
  result.push(child);
562
- size += child.nodeSize;
919
+ size2 += child.nodeSize;
563
920
  }
564
921
  pos = end;
565
922
  }
566
- return new _Fragment(result, size);
923
+ return new _Fragment(result, size2);
567
924
  }
568
925
  /**
569
926
  @internal
570
927
  */
571
- cutByIndex(from, to) {
572
- if (from == to)
928
+ cutByIndex(from2, to) {
929
+ if (from2 == to)
573
930
  return _Fragment.empty;
574
- if (from == 0 && to == this.content.length)
931
+ if (from2 == 0 && to == this.content.length)
575
932
  return this;
576
- return new _Fragment(this.content.slice(from, to));
933
+ return new _Fragment(this.content.slice(from2, to));
577
934
  }
578
935
  /**
579
936
  Create a new fragment in which the node at the given index is
@@ -584,9 +941,9 @@ var Fragment = class _Fragment {
584
941
  if (current == node)
585
942
  return this;
586
943
  let copy = this.content.slice();
587
- let size = this.size + node.nodeSize - current.nodeSize;
944
+ let size2 = this.size + node.nodeSize - current.nodeSize;
588
945
  copy[index] = node;
589
- return new _Fragment(copy, size);
946
+ return new _Fragment(copy, size2);
590
947
  }
591
948
  /**
592
949
  Create a new fragment by prepending the given node to this
@@ -731,10 +1088,10 @@ var Fragment = class _Fragment {
731
1088
  static fromArray(array) {
732
1089
  if (!array.length)
733
1090
  return _Fragment.empty;
734
- let joined, size = 0;
1091
+ let joined, size2 = 0;
735
1092
  for (let i = 0; i < array.length; i++) {
736
1093
  let node = array[i];
737
- size += node.nodeSize;
1094
+ size2 += node.nodeSize;
738
1095
  if (i && node.isText && array[i - 1].sameMarkup(node)) {
739
1096
  if (!joined)
740
1097
  joined = array.slice(0, i);
@@ -743,7 +1100,7 @@ var Fragment = class _Fragment {
743
1100
  joined.push(node);
744
1101
  }
745
1102
  }
746
- return new _Fragment(joined || array, size);
1103
+ return new _Fragment(joined || array, size2);
747
1104
  }
748
1105
  /**
749
1106
  Create a fragment from something that can be interpreted as a
@@ -951,8 +1308,8 @@ var Slice = class _Slice {
951
1308
  /**
952
1309
  @internal
953
1310
  */
954
- removeBetween(from, to) {
955
- return new _Slice(removeRange(this.content, from + this.openStart, to + this.openStart), this.openStart, this.openEnd);
1311
+ removeBetween(from2, to) {
1312
+ return new _Slice(removeRange(this.content, from2 + this.openStart, to + this.openStart), this.openStart, this.openEnd);
956
1313
  }
957
1314
  /**
958
1315
  Tests whether this slice is equal to another slice.
@@ -1004,17 +1361,17 @@ var Slice = class _Slice {
1004
1361
  }
1005
1362
  };
1006
1363
  Slice.empty = new Slice(Fragment.empty, 0, 0);
1007
- function removeRange(content, from, to) {
1008
- let { index, offset } = content.findIndex(from), child = content.maybeChild(index);
1364
+ function removeRange(content, from2, to) {
1365
+ let { index, offset } = content.findIndex(from2), child = content.maybeChild(index);
1009
1366
  let { index: indexTo, offset: offsetTo } = content.findIndex(to);
1010
- if (offset == from || child.isText) {
1367
+ if (offset == from2 || child.isText) {
1011
1368
  if (offsetTo != to && !content.child(indexTo).isText)
1012
1369
  throw new RangeError("Removing non-flat range");
1013
- return content.cut(0, from).append(content.cut(to));
1370
+ return content.cut(0, from2).append(content.cut(to));
1014
1371
  }
1015
1372
  if (index != indexTo)
1016
1373
  throw new RangeError("Removing non-flat range");
1017
- return content.replaceChild(index, child.copy(removeRange(child.content, from - offset - 1, to - offset - 1)));
1374
+ return content.replaceChild(index, child.copy(removeRange(child.content, from2 - offset - 1, to - offset - 1)));
1018
1375
  }
1019
1376
  function insertInto(content, dist, insert, parent) {
1020
1377
  let { index, offset } = content.findIndex(dist), child = content.maybeChild(index);
@@ -1510,8 +1867,8 @@ var Node2 = class _Node {
1510
1867
  recursed over. The last parameter can be used to specify a
1511
1868
  starting position to count from.
1512
1869
  */
1513
- nodesBetween(from, to, f, startPos = 0) {
1514
- this.content.nodesBetween(from, to, f, startPos, this);
1870
+ nodesBetween(from2, to, f, startPos = 0) {
1871
+ this.content.nodesBetween(from2, to, f, startPos, this);
1515
1872
  }
1516
1873
  /**
1517
1874
  Call the given callback for every descendant node. Doesn't
@@ -1534,8 +1891,8 @@ var Node2 = class _Node {
1534
1891
  inserted for every non-text leaf node encountered, otherwise
1535
1892
  [`leafText`](https://prosemirror.net/docs/ref/#model.NodeSpec.leafText) will be used.
1536
1893
  */
1537
- textBetween(from, to, blockSeparator, leafText) {
1538
- return this.content.textBetween(from, to, blockSeparator, leafText);
1894
+ textBetween(from2, to, blockSeparator, leafText) {
1895
+ return this.content.textBetween(from2, to, blockSeparator, leafText);
1539
1896
  }
1540
1897
  /**
1541
1898
  Returns this node's first child, or `null` if there are no
@@ -1592,19 +1949,19 @@ var Node2 = class _Node {
1592
1949
  given positions. If `to` is not given, it defaults to the end of
1593
1950
  the node.
1594
1951
  */
1595
- cut(from, to = this.content.size) {
1596
- if (from == 0 && to == this.content.size)
1952
+ cut(from2, to = this.content.size) {
1953
+ if (from2 == 0 && to == this.content.size)
1597
1954
  return this;
1598
- return this.copy(this.content.cut(from, to));
1955
+ return this.copy(this.content.cut(from2, to));
1599
1956
  }
1600
1957
  /**
1601
1958
  Cut out the part of the document between the given positions, and
1602
1959
  return it as a `Slice` object.
1603
1960
  */
1604
- slice(from, to = this.content.size, includeParents = false) {
1605
- if (from == to)
1961
+ slice(from2, to = this.content.size, includeParents = false) {
1962
+ if (from2 == to)
1606
1963
  return Slice.empty;
1607
- let $from = this.resolve(from), $to = this.resolve(to);
1964
+ let $from = this.resolve(from2), $to = this.resolve(to);
1608
1965
  let depth = includeParents ? 0 : $from.sharedDepth(to);
1609
1966
  let start = $from.start(depth), node = $from.node(depth);
1610
1967
  let content = node.content.cut($from.pos - start, $to.pos - start);
@@ -1618,8 +1975,8 @@ var Node2 = class _Node {
1618
1975
  into. If any of this is violated, an error of type
1619
1976
  [`ReplaceError`](https://prosemirror.net/docs/ref/#model.ReplaceError) is thrown.
1620
1977
  */
1621
- replace(from, to, slice) {
1622
- return replace(this.resolve(from), this.resolve(to), slice);
1978
+ replace(from2, to, slice) {
1979
+ return replace(this.resolve(from2), this.resolve(to), slice);
1623
1980
  }
1624
1981
  /**
1625
1982
  Find the node directly after the given position.
@@ -1675,10 +2032,10 @@ var Node2 = class _Node {
1675
2032
  Test whether a given mark or mark type occurs in this document
1676
2033
  between the two given positions.
1677
2034
  */
1678
- rangeHasMark(from, to, type) {
2035
+ rangeHasMark(from2, to, type) {
1679
2036
  let found2 = false;
1680
- if (to > from)
1681
- this.nodesBetween(from, to, (node) => {
2037
+ if (to > from2)
2038
+ this.nodesBetween(from2, to, (node) => {
1682
2039
  if (type.isInSet(node.marks))
1683
2040
  found2 = true;
1684
2041
  return !found2;
@@ -1761,8 +2118,8 @@ var Node2 = class _Node {
1761
2118
  can optionally pass `start` and `end` indices into the
1762
2119
  replacement fragment.
1763
2120
  */
1764
- canReplace(from, to, replacement = Fragment.empty, start = 0, end = replacement.childCount) {
1765
- let one = this.contentMatchAt(from).matchFragment(replacement, start, end);
2121
+ canReplace(from2, to, replacement = Fragment.empty, start = 0, end = replacement.childCount) {
2122
+ let one = this.contentMatchAt(from2).matchFragment(replacement, start, end);
1766
2123
  let two = one && one.matchFragment(this.content, to);
1767
2124
  if (!two || !two.validEnd)
1768
2125
  return false;
@@ -1775,10 +2132,10 @@ var Node2 = class _Node {
1775
2132
  Test whether replacing the range `from` to `to` (by index) with
1776
2133
  a node of the given type would leave the node's content valid.
1777
2134
  */
1778
- canReplaceWith(from, to, type, marks) {
2135
+ canReplaceWith(from2, to, type, marks) {
1779
2136
  if (marks && !this.type.allowsMarks(marks))
1780
2137
  return false;
1781
- let start = this.contentMatchAt(from).matchType(type);
2138
+ let start = this.contentMatchAt(from2).matchType(type);
1782
2139
  let end = start && start.matchFragment(this.content, to);
1783
2140
  return end ? end.validEnd : false;
1784
2141
  }
@@ -2139,38 +2496,38 @@ function nfa(expr) {
2139
2496
  function node() {
2140
2497
  return nfa2.push([]) - 1;
2141
2498
  }
2142
- function edge(from, to, term) {
2499
+ function edge(from2, to, term) {
2143
2500
  let edge2 = { term, to };
2144
- nfa2[from].push(edge2);
2501
+ nfa2[from2].push(edge2);
2145
2502
  return edge2;
2146
2503
  }
2147
2504
  function connect(edges, to) {
2148
2505
  edges.forEach((edge2) => edge2.to = to);
2149
2506
  }
2150
- function compile(expr2, from) {
2507
+ function compile(expr2, from2) {
2151
2508
  if (expr2.type == "choice") {
2152
- return expr2.exprs.reduce((out, expr3) => out.concat(compile(expr3, from)), []);
2509
+ return expr2.exprs.reduce((out, expr3) => out.concat(compile(expr3, from2)), []);
2153
2510
  } else if (expr2.type == "seq") {
2154
2511
  for (let i = 0; ; i++) {
2155
- let next = compile(expr2.exprs[i], from);
2512
+ let next = compile(expr2.exprs[i], from2);
2156
2513
  if (i == expr2.exprs.length - 1)
2157
2514
  return next;
2158
- connect(next, from = node());
2515
+ connect(next, from2 = node());
2159
2516
  }
2160
2517
  } else if (expr2.type == "star") {
2161
2518
  let loop = node();
2162
- edge(from, loop);
2519
+ edge(from2, loop);
2163
2520
  connect(compile(expr2.expr, loop), loop);
2164
2521
  return [edge(loop)];
2165
2522
  } else if (expr2.type == "plus") {
2166
2523
  let loop = node();
2167
- connect(compile(expr2.expr, from), loop);
2524
+ connect(compile(expr2.expr, from2), loop);
2168
2525
  connect(compile(expr2.expr, loop), loop);
2169
2526
  return [edge(loop)];
2170
2527
  } else if (expr2.type == "opt") {
2171
- return [edge(from)].concat(compile(expr2.expr, from));
2528
+ return [edge(from2)].concat(compile(expr2.expr, from2));
2172
2529
  } else if (expr2.type == "range") {
2173
- let cur = from;
2530
+ let cur = from2;
2174
2531
  for (let i = 0; i < expr2.min; i++) {
2175
2532
  let next = node();
2176
2533
  connect(compile(expr2.expr, cur), next);
@@ -2188,7 +2545,7 @@ function nfa(expr) {
2188
2545
  }
2189
2546
  return [edge(cur)];
2190
2547
  } else if (expr2.type == "name") {
2191
- return [edge(from, void 0, expr2.value)];
2548
+ return [edge(from2, void 0, expr2.value)];
2192
2549
  } else {
2193
2550
  throw new Error("Unknown expr type");
2194
2551
  }
@@ -2487,9 +2844,9 @@ var StepResult = class _StepResult {
2487
2844
  arguments. Create a successful result if it succeeds, and a
2488
2845
  failed one if it throws a `ReplaceError`.
2489
2846
  */
2490
- static fromReplace(doc, from, to, slice) {
2847
+ static fromReplace(doc, from2, to, slice) {
2491
2848
  try {
2492
- return _StepResult.ok(doc.replace(from, to, slice));
2849
+ return _StepResult.ok(doc.replace(from2, to, slice));
2493
2850
  } catch (e) {
2494
2851
  if (e instanceof ReplaceError)
2495
2852
  return _StepResult.fail(e.message);
@@ -2513,9 +2870,9 @@ var AddMarkStep = class _AddMarkStep extends Step {
2513
2870
  /**
2514
2871
  Create a mark step.
2515
2872
  */
2516
- constructor(from, to, mark) {
2873
+ constructor(from2, to, mark) {
2517
2874
  super();
2518
- this.from = from;
2875
+ this.from = from2;
2519
2876
  this.to = to;
2520
2877
  this.mark = mark;
2521
2878
  }
@@ -2533,10 +2890,10 @@ var AddMarkStep = class _AddMarkStep extends Step {
2533
2890
  return new RemoveMarkStep(this.from, this.to, this.mark);
2534
2891
  }
2535
2892
  map(mapping) {
2536
- let from = mapping.mapResult(this.from, 1), to = mapping.mapResult(this.to, -1);
2537
- if (from.deleted && to.deleted || from.pos >= to.pos)
2893
+ let from2 = mapping.mapResult(this.from, 1), to = mapping.mapResult(this.to, -1);
2894
+ if (from2.deleted && to.deleted || from2.pos >= to.pos)
2538
2895
  return null;
2539
- return new _AddMarkStep(from.pos, to.pos, this.mark);
2896
+ return new _AddMarkStep(from2.pos, to.pos, this.mark);
2540
2897
  }
2541
2898
  merge(other) {
2542
2899
  if (other instanceof _AddMarkStep && other.mark.eq(this.mark) && this.from <= other.to && this.to >= other.from)
@@ -2565,9 +2922,9 @@ var RemoveMarkStep = class _RemoveMarkStep extends Step {
2565
2922
  /**
2566
2923
  Create a mark-removing step.
2567
2924
  */
2568
- constructor(from, to, mark) {
2925
+ constructor(from2, to, mark) {
2569
2926
  super();
2570
- this.from = from;
2927
+ this.from = from2;
2571
2928
  this.to = to;
2572
2929
  this.mark = mark;
2573
2930
  }
@@ -2582,10 +2939,10 @@ var RemoveMarkStep = class _RemoveMarkStep extends Step {
2582
2939
  return new AddMarkStep(this.from, this.to, this.mark);
2583
2940
  }
2584
2941
  map(mapping) {
2585
- let from = mapping.mapResult(this.from, 1), to = mapping.mapResult(this.to, -1);
2586
- if (from.deleted && to.deleted || from.pos >= to.pos)
2942
+ let from2 = mapping.mapResult(this.from, 1), to = mapping.mapResult(this.to, -1);
2943
+ if (from2.deleted && to.deleted || from2.pos >= to.pos)
2587
2944
  return null;
2588
- return new _RemoveMarkStep(from.pos, to.pos, this.mark);
2945
+ return new _RemoveMarkStep(from2.pos, to.pos, this.mark);
2589
2946
  }
2590
2947
  merge(other) {
2591
2948
  if (other instanceof _RemoveMarkStep && other.mark.eq(this.mark) && this.from <= other.to && this.to >= other.from)
@@ -2705,9 +3062,9 @@ var ReplaceStep = class _ReplaceStep extends Step {
2705
3062
  tokens (this is to guard against rebased replace steps
2706
3063
  overwriting something they weren't supposed to).
2707
3064
  */
2708
- constructor(from, to, slice, structure = false) {
3065
+ constructor(from2, to, slice, structure = false) {
2709
3066
  super();
2710
- this.from = from;
3067
+ this.from = from2;
2711
3068
  this.to = to;
2712
3069
  this.slice = slice;
2713
3070
  this.structure = structure;
@@ -2725,10 +3082,10 @@ var ReplaceStep = class _ReplaceStep extends Step {
2725
3082
  }
2726
3083
  map(mapping) {
2727
3084
  let to = mapping.mapResult(this.to, -1);
2728
- let from = this.from == this.to && _ReplaceStep.MAP_BIAS < 0 ? to : mapping.mapResult(this.from, 1);
2729
- if (from.deletedAcross && to.deletedAcross)
3085
+ let from2 = this.from == this.to && _ReplaceStep.MAP_BIAS < 0 ? to : mapping.mapResult(this.from, 1);
3086
+ if (from2.deletedAcross && to.deletedAcross)
2730
3087
  return null;
2731
- return new _ReplaceStep(from.pos, Math.max(from.pos, to.pos), this.slice, this.structure);
3088
+ return new _ReplaceStep(from2.pos, Math.max(from2.pos, to.pos), this.slice, this.structure);
2732
3089
  }
2733
3090
  merge(other) {
2734
3091
  if (!(other instanceof _ReplaceStep) || other.structure || this.structure)
@@ -2769,9 +3126,9 @@ var ReplaceAroundStep = class _ReplaceAroundStep extends Step {
2769
3126
  of the gap should be moved. `structure` has the same meaning as
2770
3127
  it has in the [`ReplaceStep`](https://prosemirror.net/docs/ref/#transform.ReplaceStep) class.
2771
3128
  */
2772
- constructor(from, to, gapFrom, gapTo, slice, insert, structure = false) {
3129
+ constructor(from2, to, gapFrom, gapTo, slice, insert, structure = false) {
2773
3130
  super();
2774
- this.from = from;
3131
+ this.from = from2;
2775
3132
  this.to = to;
2776
3133
  this.gapFrom = gapFrom;
2777
3134
  this.gapTo = gapTo;
@@ -2805,12 +3162,12 @@ var ReplaceAroundStep = class _ReplaceAroundStep extends Step {
2805
3162
  return new _ReplaceAroundStep(this.from, this.from + this.slice.size + gap, this.from + this.insert, this.from + this.insert + gap, doc.slice(this.from, this.to).removeBetween(this.gapFrom - this.from, this.gapTo - this.from), this.gapFrom - this.from, this.structure);
2806
3163
  }
2807
3164
  map(mapping) {
2808
- let from = mapping.mapResult(this.from, 1), to = mapping.mapResult(this.to, -1);
2809
- let gapFrom = this.from == this.gapFrom ? from.pos : mapping.map(this.gapFrom, -1);
3165
+ let from2 = mapping.mapResult(this.from, 1), to = mapping.mapResult(this.to, -1);
3166
+ let gapFrom = this.from == this.gapFrom ? from2.pos : mapping.map(this.gapFrom, -1);
2810
3167
  let gapTo = this.to == this.gapTo ? to.pos : mapping.map(this.gapTo, 1);
2811
- if (from.deletedAcross && to.deletedAcross || gapFrom < from.pos || gapTo > to.pos)
3168
+ if (from2.deletedAcross && to.deletedAcross || gapFrom < from2.pos || gapTo > to.pos)
2812
3169
  return null;
2813
- return new _ReplaceAroundStep(from.pos, to.pos, gapFrom, gapTo, this.slice, this.insert, this.structure);
3170
+ return new _ReplaceAroundStep(from2.pos, to.pos, gapFrom, gapTo, this.slice, this.insert, this.structure);
2814
3171
  }
2815
3172
  toJSON() {
2816
3173
  let json = {
@@ -2837,8 +3194,8 @@ var ReplaceAroundStep = class _ReplaceAroundStep extends Step {
2837
3194
  }
2838
3195
  };
2839
3196
  Step.jsonID("replaceAround", ReplaceAroundStep);
2840
- function contentBetween(doc, from, to) {
2841
- let $from = doc.resolve(from), dist = to - from, depth = $from.depth;
3197
+ function contentBetween(doc, from2, to) {
3198
+ let $from = doc.resolve(from2), dist = to - from2, depth = $from.depth;
2842
3199
  while (dist > 0 && depth > 0 && $from.indexAfter(depth) == $from.node(depth).childCount) {
2843
3200
  depth--;
2844
3201
  dist--;
@@ -3033,11 +3390,11 @@ var Selection = class {
3033
3390
  let mapFrom = tr.steps.length, ranges = this.ranges;
3034
3391
  for (let i = 0; i < ranges.length; i++) {
3035
3392
  let { $from, $to } = ranges[i], mapping = tr.mapping.slice(mapFrom);
3036
- let from = mapping.map($from.pos), to = mapping.map($to.pos);
3393
+ let from2 = mapping.map($from.pos), to = mapping.map($to.pos);
3037
3394
  if (i) {
3038
- tr.deleteRange(from, to);
3395
+ tr.deleteRange(from2, to);
3039
3396
  } else {
3040
- tr.replaceRangeWith(from, to, node);
3397
+ tr.replaceRangeWith(from2, to, node);
3041
3398
  selectionToInsertionEnd(tr, mapFrom, node.isInline ? -1 : 1);
3042
3399
  }
3043
3400
  }
@@ -3279,8 +3636,8 @@ var NodeSelection = class _NodeSelection extends Selection {
3279
3636
  /**
3280
3637
  Create a node selection from non-resolved positions.
3281
3638
  */
3282
- static create(doc, from) {
3283
- return new _NodeSelection(doc.resolve(from));
3639
+ static create(doc, from2) {
3640
+ return new _NodeSelection(doc.resolve(from2));
3284
3641
  }
3285
3642
  /**
3286
3643
  Determines whether the given node may be selected as a node
@@ -3455,11 +3812,11 @@ var Plugin = class {
3455
3812
  return state[this.key];
3456
3813
  }
3457
3814
  };
3458
- var keys = /* @__PURE__ */ Object.create(null);
3815
+ var keys2 = /* @__PURE__ */ Object.create(null);
3459
3816
  function createKey(name) {
3460
- if (name in keys)
3461
- return name + "$" + ++keys[name];
3462
- keys[name] = 0;
3817
+ if (name in keys2)
3818
+ return name + "$" + ++keys2[name];
3819
+ keys2[name] = 0;
3463
3820
  return name + "$";
3464
3821
  }
3465
3822
  var PluginKey = class {
@@ -3535,6 +3892,18 @@ var mediaKeyboardNav_default = MediaKeyboardNav;
3535
3892
 
3536
3893
  // ../shared/src/editor-extensions.ts
3537
3894
  var CodeWithCombinableMarks = Code.extend({ excludes: "" });
3895
+ var TableWithId = Table.extend({
3896
+ addAttributes() {
3897
+ return {
3898
+ ...this.parent?.() ?? {},
3899
+ tableId: {
3900
+ default: null,
3901
+ parseHTML: (el) => el.getAttribute("data-table-id"),
3902
+ renderHTML: (attrs) => attrs.tableId ? { "data-table-id": attrs.tableId } : {}
3903
+ }
3904
+ };
3905
+ }
3906
+ });
3538
3907
  var FrontmatterSchema = CodeBlock.extend({
3539
3908
  name: "frontmatter",
3540
3909
  addInputRules() {
@@ -3549,7 +3918,7 @@ ${text}
3549
3918
  }
3550
3919
  });
3551
3920
  function buildEditorExtensions(opts = {}) {
3552
- const table = opts.table ?? Table;
3921
+ const table = opts.table ?? TableWithId;
3553
3922
  const frontmatter = opts.frontmatter ?? FrontmatterSchema;
3554
3923
  return [
3555
3924
  StarterKit.configure({
@@ -3570,7 +3939,13 @@ function buildEditorExtensions(opts = {}) {
3570
3939
  Highlight,
3571
3940
  Subscript,
3572
3941
  Superscript,
3573
- table.configure({ resizable: false }),
3942
+ table.configure({
3943
+ resizable: true,
3944
+ handleWidth: 5,
3945
+ lastColumnResizable: true,
3946
+ cellMinWidth: 80,
3947
+ renderWrapper: true
3948
+ }),
3574
3949
  TableRow,
3575
3950
  TableCell,
3576
3951
  TableHeader,
@@ -3628,7 +4003,7 @@ function markdownFromYFragment(fragment, extensions = editorExtensions) {
3628
4003
  }
3629
4004
 
3630
4005
  // ../shared/src/insertMedia.ts
3631
- import { nanoid } from "nanoid";
4006
+ import { nanoid as nanoid2 } from "nanoid";
3632
4007
 
3633
4008
  // ../shared/src/uploadRegistry.ts
3634
4009
  var UploadRegistryImpl = class {
@@ -3812,11 +4187,16 @@ var UploadRegistryImpl = class {
3812
4187
  var uploadRegistry = new UploadRegistryImpl();
3813
4188
 
3814
4189
  // ../shared/src/uploadPlaceholderPlugin.ts
3815
- import * as Y from "yjs";
4190
+ import * as Y2 from "yjs";
3816
4191
  var uploadPlaceholderPluginKey = new PluginKey(
3817
4192
  "uploadPlaceholder"
3818
4193
  );
3819
4194
 
4195
+ // ../shared/src/awareness.ts
4196
+ function setLocalAwareness(awareness, key, value) {
4197
+ awareness.setLocalStateField(key, value);
4198
+ }
4199
+
3820
4200
  // ../shared/src/insertMedia.ts
3821
4201
  var IMAGE_MAX_BYTES = 25 * 1024 * 1024;
3822
4202
  var VIDEO_MAX_BYTES = 50 * 1024 * 1024;
@@ -3825,14 +4205,14 @@ var VIDEO_MAX_BYTES = 50 * 1024 * 1024;
3825
4205
  var mediaDeleteObserverKey = new PluginKey("mediaDeleteObserver");
3826
4206
 
3827
4207
  // src/docReaders.ts
3828
- import * as Y2 from "yjs";
4208
+ import * as Y3 from "yjs";
3829
4209
  function isXmlElement(node) {
3830
- return node instanceof Y2.XmlElement;
4210
+ return node instanceof Y3.XmlElement;
3831
4211
  }
3832
4212
  function getElementText(el) {
3833
4213
  let out = "";
3834
4214
  for (const child of el.toArray()) {
3835
- if (child instanceof Y2.XmlText) {
4215
+ if (child instanceof Y3.XmlText) {
3836
4216
  for (const op of child.toDelta()) {
3837
4217
  if (typeof op.insert === "string") out += op.insert;
3838
4218
  }
@@ -3945,13 +4325,13 @@ function serializeDocAsMarkdown(doc) {
3945
4325
  }
3946
4326
 
3947
4327
  // src/anchors.ts
3948
- import * as Y3 from "yjs";
4328
+ import * as Y4 from "yjs";
3949
4329
  function buildFlatMap(fragment) {
3950
4330
  let flat = "";
3951
4331
  const map = [];
3952
4332
  const blockFlatStarts = [];
3953
4333
  const walk = (node) => {
3954
- if (node instanceof Y3.XmlText) {
4334
+ if (node instanceof Y4.XmlText) {
3955
4335
  let localOffset = 0;
3956
4336
  for (const op of node.toDelta()) {
3957
4337
  const value = op.insert;
@@ -3979,7 +4359,7 @@ function buildFlatMap(fragment) {
3979
4359
  return;
3980
4360
  }
3981
4361
  for (const child of node.toArray()) {
3982
- if (child instanceof Y3.XmlText || child instanceof Y3.XmlElement) {
4362
+ if (child instanceof Y4.XmlText || child instanceof Y4.XmlElement) {
3983
4363
  walk(child);
3984
4364
  }
3985
4365
  }
@@ -3987,7 +4367,7 @@ function buildFlatMap(fragment) {
3987
4367
  const topLevel = fragment.toArray();
3988
4368
  topLevel.forEach((node, idx) => {
3989
4369
  blockFlatStarts.push(flat.length);
3990
- if (node instanceof Y3.XmlText || node instanceof Y3.XmlElement) {
4370
+ if (node instanceof Y4.XmlText || node instanceof Y4.XmlElement) {
3991
4371
  walk(node);
3992
4372
  }
3993
4373
  if (idx < topLevel.length - 1) {
@@ -4044,7 +4424,7 @@ function resolveAnchoredContext(doc, anchorFrom, anchorTo) {
4044
4424
  let containingBlockIdx = -1;
4045
4425
  for (let i = 0; i < topLevel.length; i++) {
4046
4426
  const block = topLevel[i];
4047
- if (block instanceof Y3.XmlElement && containsType(block, fromAbs.type)) {
4427
+ if (block instanceof Y4.XmlElement && containsType(block, fromAbs.type)) {
4048
4428
  containingBlockIdx = i;
4049
4429
  break;
4050
4430
  }
@@ -4066,8 +4446,8 @@ function resolveAnchoredContext(doc, anchorFrom, anchorTo) {
4066
4446
  function decodeAbs(doc, encoded) {
4067
4447
  if (!encoded) return null;
4068
4448
  try {
4069
- const rel = Y3.decodeRelativePosition(encoded);
4070
- const abs = Y3.createAbsolutePositionFromRelativePosition(rel, doc);
4449
+ const rel = Y4.decodeRelativePosition(encoded);
4450
+ const abs = Y4.createAbsolutePositionFromRelativePosition(rel, doc);
4071
4451
  if (!abs) return null;
4072
4452
  return { type: abs.type, index: abs.index };
4073
4453
  } catch {
@@ -4090,7 +4470,7 @@ function flatIndexFor(map, abs) {
4090
4470
  function containsType(el, target) {
4091
4471
  for (const child of el.toArray()) {
4092
4472
  if (child === target) return true;
4093
- if (child instanceof Y3.XmlElement && containsType(child, target)) {
4473
+ if (child instanceof Y4.XmlElement && containsType(child, target)) {
4094
4474
  return true;
4095
4475
  }
4096
4476
  }
@@ -4138,24 +4518,24 @@ function resolveServerAnchor(doc, spec) {
4138
4518
  if (!lastCharEntry) {
4139
4519
  return { ok: false, error: "text_not_found", currentSectionText };
4140
4520
  }
4141
- const fromRelPos = Y3.createRelativePositionFromTypeIndex(
4521
+ const fromRelPos = Y4.createRelativePositionFromTypeIndex(
4142
4522
  startEntry.xmlText,
4143
4523
  startEntry.offsetInText
4144
4524
  );
4145
- const toRelPos = Y3.createRelativePositionFromTypeIndex(
4525
+ const toRelPos = Y4.createRelativePositionFromTypeIndex(
4146
4526
  lastCharEntry.xmlText,
4147
4527
  lastCharEntry.offsetInText + 1
4148
4528
  );
4149
4529
  return {
4150
4530
  ok: true,
4151
- from: Y3.encodeRelativePosition(fromRelPos),
4152
- to: Y3.encodeRelativePosition(toRelPos)
4531
+ from: Y4.encodeRelativePosition(fromRelPos),
4532
+ to: Y4.encodeRelativePosition(toRelPos)
4153
4533
  };
4154
4534
  }
4155
4535
 
4156
4536
  // src/roomState.ts
4157
- var RoomState = class {
4158
- doc = new Y4.Doc();
4537
+ var RoomState = class _RoomState {
4538
+ doc;
4159
4539
  actingAs;
4160
4540
  identity;
4161
4541
  roomId;
@@ -4185,58 +4565,62 @@ var RoomState = class {
4185
4565
  * on every remote edit, comment, suggestion, or activity-feed write.
4186
4566
  */
4187
4567
  _lastRemoteActivityAt = Date.now();
4568
+ monitoringOn = false;
4569
+ /**
4570
+ * Set true when `composer_next_event`'s goodbye branch has fired. Tells
4571
+ * `handleNextEvent` to refuse subsequent calls until the user explicitly
4572
+ * re-engages (which happens via `composer_create_room` / `composer_join_room`,
4573
+ * both of which build a fresh `RoomState` so this flag resets naturally).
4574
+ */
4575
+ goodbyeIssued = false;
4188
4576
  constructor(opts) {
4189
4577
  this.roomId = opts.roomId;
4190
4578
  this.actingAs = opts.actingAs;
4191
4579
  this.identity = opts.identity;
4580
+ this.doc = opts._docForTest ?? new Y5.Doc();
4192
4581
  this.watchMentions();
4193
4582
  attachRemoteActivityTracker(this.doc, {
4194
4583
  onActivity: (at) => {
4195
4584
  this._lastRemoteActivityAt = at;
4196
4585
  }
4197
4586
  });
4198
- this.provider = new YProvider(opts.serverHost, opts.roomId, this.doc, {
4199
- party: "composer-room",
4200
- connect: true,
4201
- WebSocketPolyfill: WebSocket
4202
- });
4203
- this.provider.awareness.setLocalStateField("user", {
4204
- name: opts.actingAs,
4205
- color: opts.identity.color,
4206
- userId: opts.identity.userId,
4207
- isAgent: true
4208
- });
4209
- this.installAwarenessHeartbeat();
4587
+ if (opts._providerForTest) {
4588
+ this.provider = opts._providerForTest;
4589
+ } else {
4590
+ this.provider = new YProvider(opts.serverHost, opts.roomId, this.doc, {
4591
+ party: "composer-room",
4592
+ connect: false,
4593
+ WebSocketPolyfill: WebSocket
4594
+ });
4595
+ }
4210
4596
  }
4211
4597
  /**
4212
- * Re-broadcast the MCP's awareness every 15s.
4213
- *
4214
- * y-partyserver's provider disables the y-protocols awareness
4215
- * `_checkInterval` (see `clearInterval(awareness._checkInterval)` in
4216
- * `y-partyserver/dist/provider/index.js`), so the MCP sends its awareness
4217
- * exactly once — on connect — and never heartbeats after that. Combined
4218
- * with Cloudflare Durable Object hibernation (which evicts the server's
4219
- * in-memory `document.awareness` Map on wake), this means a browser that
4220
- * connects more than ~60s after the MCP sees an empty awareness dump in
4221
- * `onConnect` and never learns the agent is there. The user's own
4222
- * awareness flows the other direction fine (they send on connect, server
4223
- * broadcasts to the MCP), which is why the failure is asymmetric.
4224
- *
4225
- * y-partyserver's provider listens to `awareness.on("change", ...)`, and
4226
- * y-protocols only fires `change` when the new state is deep-unequal to
4227
- * the previous one. Re-setting an identical state emits `update` but NOT
4228
- * `change`, so the provider never sends a wire frame. We bump a throwaway
4229
- * `_hb` field each tick to guarantee deep-inequality, forcing the change
4230
- * event and a broadcast. 15s is well under any realistic hibernation gap.
4598
+ * Test-only factory: build a fully-functional `RoomState` without opening
4599
+ * a WebSocket. Uses a standalone `Awareness` instance so local awareness
4600
+ * writes still work and can be observed via `awarenessForTest`.
4231
4601
  */
4232
- installAwarenessHeartbeat() {
4233
- const heartbeat = setInterval(() => {
4234
- const local = this.provider.awareness.getLocalState();
4235
- if (local !== null) {
4236
- this.provider.awareness.setLocalState({ ...local, _hb: Date.now() });
4602
+ static createForTest(opts) {
4603
+ const doc = new Y5.Doc();
4604
+ const awareness = new Awareness(doc);
4605
+ const fake = {
4606
+ awareness,
4607
+ synced: true,
4608
+ destroy() {
4609
+ awareness.destroy();
4237
4610
  }
4238
- }, 15e3);
4239
- heartbeat.unref?.();
4611
+ };
4612
+ return new _RoomState({
4613
+ roomId: opts.roomId,
4614
+ serverHost: "test://noop",
4615
+ actingAs: opts.actingAs,
4616
+ identity: opts.identity,
4617
+ _providerForTest: fake,
4618
+ _docForTest: doc
4619
+ });
4620
+ }
4621
+ /** Test-only accessor for the underlying awareness instance. */
4622
+ get awarenessForTest() {
4623
+ return this.provider.awareness;
4240
4624
  }
4241
4625
  /**
4242
4626
  * Resolves when the provider has completed its first sync handshake.
@@ -4246,24 +4630,142 @@ var RoomState = class {
4246
4630
  */
4247
4631
  async waitForInitialSync(timeoutMs = 15e3) {
4248
4632
  if (this.provider.synced) return;
4633
+ if (!this.provider.on || !this.provider.off) return;
4634
+ const provider = this.provider;
4635
+ const onSub = provider.on.bind(provider);
4636
+ const offSub = provider.off.bind(provider);
4249
4637
  await new Promise((resolve, reject) => {
4250
- const onSync = (synced) => {
4638
+ const onSync = (...args) => {
4639
+ const synced = args[0];
4251
4640
  if (!synced) return;
4252
4641
  clearTimeout(timer);
4253
- this.provider.off("sync", onSync);
4642
+ offSub("sync", onSync);
4254
4643
  resolve();
4255
4644
  };
4256
4645
  const timer = setTimeout(() => {
4257
- this.provider.off("sync", onSync);
4646
+ offSub("sync", onSync);
4258
4647
  reject(
4259
4648
  new Error(
4260
4649
  `timed out after ${timeoutMs}ms waiting for sync handshake on room "${this.roomId}"`
4261
4650
  )
4262
4651
  );
4263
4652
  }, timeoutMs);
4264
- this.provider.on("sync", onSync);
4653
+ onSub("sync", onSync);
4265
4654
  });
4266
4655
  }
4656
+ idleDisconnectTimer = null;
4657
+ /**
4658
+ * Open the WebSocket (if not already) and await the first sync handshake.
4659
+ * Idempotent — no-op if already connected and synced. Call at the top of
4660
+ * every tool handler that touches the doc. Cancels any pending idle
4661
+ * disconnect because we're back in active use.
4662
+ */
4663
+ async ensureConnected(timeoutMs = 15e3) {
4664
+ if (this.idleDisconnectTimer) {
4665
+ clearTimeout(this.idleDisconnectTimer);
4666
+ this.idleDisconnectTimer = null;
4667
+ }
4668
+ if (!this.provider.connect) return;
4669
+ if (this.provider.wsconnected && this.provider.synced) return;
4670
+ await this.provider.connect();
4671
+ await this.waitForInitialSync(timeoutMs);
4672
+ }
4673
+ /**
4674
+ * Arm a timer that turns monitoring off and closes the socket after `ms`
4675
+ * of no further tool-handler activity. Cancelled by `ensureConnected`.
4676
+ * Call at the end of every tool handler. Defaults to 30s — long enough
4677
+ * to bridge a normal multi-step monitor turn (mention → reply →
4678
+ * `composer_next_event` again), short enough that an abandoned monitor
4679
+ * loop fades the avatar within the "within a minute" window the user
4680
+ * requested.
4681
+ *
4682
+ * Both presence cleanup paths converge here: setMonitoring(false) clears
4683
+ * our local awareness state, then disconnect() closes the socket which
4684
+ * triggers y-partyserver's onClose → removeAwarenessStates. Either alone
4685
+ * would suffice; doing both keeps local and server state consistent.
4686
+ */
4687
+ scheduleIdleDisconnect(ms = 3e4) {
4688
+ if (this.idleDisconnectTimer) {
4689
+ clearTimeout(this.idleDisconnectTimer);
4690
+ this.idleDisconnectTimer = null;
4691
+ }
4692
+ if (!this.provider.disconnect) return;
4693
+ if (!this.provider.wsconnected) return;
4694
+ this.idleDisconnectTimer = setTimeout(() => {
4695
+ this.idleDisconnectTimer = null;
4696
+ this.setMonitoring(false);
4697
+ this.provider.disconnect?.();
4698
+ }, ms);
4699
+ this.idleDisconnectTimer.unref?.();
4700
+ }
4701
+ /**
4702
+ * Close the socket immediately. Available for explicit teardown
4703
+ * (e.g., destroy()). Does not touch monitoring state — the socket close
4704
+ * causes y-partyserver to broadcast the awareness removal, which is the
4705
+ * visual signal peers care about.
4706
+ */
4707
+ disconnect() {
4708
+ if (this.idleDisconnectTimer) {
4709
+ clearTimeout(this.idleDisconnectTimer);
4710
+ this.idleDisconnectTimer = null;
4711
+ }
4712
+ this.provider.disconnect?.();
4713
+ }
4714
+ /**
4715
+ * Toggle whether the agent is visibly monitoring the room. Broadcast via
4716
+ * the `user` awareness field — present ↔ avatar visible to peers. The
4717
+ * socket must already be open (caller should have ensured this).
4718
+ *
4719
+ * setMonitoring(true) — called once on first composer_next_event entry.
4720
+ * setMonitoring(false) — called on the goodbye branch; clears awareness
4721
+ * immediately so the avatar disappears as the
4722
+ * farewell lands.
4723
+ *
4724
+ * Subsequent calls with the same value are no-ops.
4725
+ */
4726
+ setMonitoring(on) {
4727
+ if (this.monitoringOn === on) return;
4728
+ this.monitoringOn = on;
4729
+ if (on) {
4730
+ const current = this.provider.awareness.getLocalState() ?? {};
4731
+ this.provider.awareness.setLocalState({
4732
+ ...current,
4733
+ user: {
4734
+ name: this.actingAs,
4735
+ color: this.identity.color,
4736
+ userId: this.identity.userId,
4737
+ isAgent: true
4738
+ }
4739
+ });
4740
+ } else {
4741
+ this.provider.awareness.setLocalState(null);
4742
+ }
4743
+ }
4744
+ get isMonitoring() {
4745
+ return this.monitoringOn;
4746
+ }
4747
+ /**
4748
+ * Latch flipped by `composer_next_event`'s goodbye branch. Once set, the
4749
+ * model is expected to stop calling `composer_next_event` until the user
4750
+ * explicitly asks to rejoin — which builds a fresh RoomState, resetting
4751
+ * the latch. The MCP refuses further next_event calls on this RoomState
4752
+ * instead of silently re-entering the monitor loop.
4753
+ */
4754
+ markGoodbyeIssued() {
4755
+ this.goodbyeIssued = true;
4756
+ }
4757
+ get hasIssuedGoodbye() {
4758
+ return this.goodbyeIssued;
4759
+ }
4760
+ /**
4761
+ * Read-only view of this client's local awareness state. Exposed for
4762
+ * tests/diagnostics only — production code should not depend on this.
4763
+ * Returns `null` when monitoring is off (local state cleared) or when
4764
+ * the provider hasn't written any fields yet.
4765
+ */
4766
+ getLocalAwareness() {
4767
+ return this.provider.awareness.getLocalState();
4768
+ }
4267
4769
  snapshot() {
4268
4770
  return {
4269
4771
  fullDoc: serializeDocAsMarkdown(this.doc),
@@ -4272,23 +4774,54 @@ var RoomState = class {
4272
4774
  threadsBacklog: []
4273
4775
  };
4274
4776
  }
4275
- async nextEvent(timeoutMs) {
4777
+ async nextEvent(timeoutMs, signal) {
4276
4778
  const pending = this.queue.shift();
4277
- if (pending) return pending;
4779
+ if (pending) {
4780
+ this.onEventDelivered(pending);
4781
+ return pending;
4782
+ }
4783
+ if (signal?.aborted) return { kind: "timeout" };
4278
4784
  return new Promise((resolve) => {
4279
- const timer = setTimeout(() => {
4785
+ const cleanup = () => {
4786
+ clearTimeout(timer);
4787
+ signal?.removeEventListener("abort", onAbort);
4280
4788
  const idx = this.waiters.indexOf(waiter);
4281
4789
  if (idx >= 0) this.waiters.splice(idx, 1);
4790
+ };
4791
+ const timer = setTimeout(() => {
4792
+ cleanup();
4282
4793
  resolve({ kind: "timeout" });
4283
4794
  }, timeoutMs);
4795
+ const onAbort = () => {
4796
+ cleanup();
4797
+ resolve({ kind: "timeout" });
4798
+ };
4799
+ signal?.addEventListener("abort", onAbort, { once: true });
4284
4800
  const waiter = (ev) => {
4285
- clearTimeout(timer);
4801
+ cleanup();
4802
+ this.onEventDelivered(ev);
4286
4803
  resolve(ev);
4287
4804
  };
4288
4805
  this.waiters.push(waiter);
4289
4806
  });
4290
4807
  }
4808
+ /**
4809
+ * Hook that runs at the moment an event is actually handed to the caller
4810
+ * of `composer_next_event`. For mentions this is where we publish the
4811
+ * `"thinking"` heartbeat — *not* at enqueue time — so that queued mentions
4812
+ * behind an in-flight one don't prematurely light up the indicator on the
4813
+ * user's client. The indicator only appears once the model has picked the
4814
+ * event up to process it.
4815
+ */
4816
+ onEventDelivered(ev) {
4817
+ if (ev.kind !== "mention") return;
4818
+ this.publishAgentWork(ev.threadId, ev.replyId, "thinking");
4819
+ }
4291
4820
  destroy() {
4821
+ if (this.idleDisconnectTimer) {
4822
+ clearTimeout(this.idleDisconnectTimer);
4823
+ this.idleDisconnectTimer = null;
4824
+ }
4292
4825
  this.provider.destroy();
4293
4826
  this.doc.destroy();
4294
4827
  }
@@ -4300,14 +4833,84 @@ var RoomState = class {
4300
4833
  markThreadActive(threadId) {
4301
4834
  this.activeThreads.add(threadId);
4302
4835
  }
4836
+ /**
4837
+ * Publish or update an `agentWork` entry on the local awareness state for an
4838
+ * in-flight agent record. Keyed on `{threadId, replyId}` — when `replyId` is
4839
+ * absent the entry targets the thread-head record (an agent-authored Comment
4840
+ * or Suggestion). Upserts: calling twice with the same key refreshes state /
4841
+ * note / updatedAt rather than appending a duplicate. Idempotent.
4842
+ *
4843
+ * Entries are transient — `clearAgentWork` prunes a specific entry on a
4844
+ * `ready` transition; the full array is auto-cleaned when the provider
4845
+ * disconnects (server-side `onClose` → `removeAwarenessStates`). The MCP
4846
+ * also self-heals on `composer_agent_status` re-entry by scanning the array
4847
+ * and dropping entries whose target record is already `ready`.
4848
+ */
4849
+ publishAgentWork(threadId, replyId, state, note) {
4850
+ const current = this.readAgentWork();
4851
+ const now = Date.now();
4852
+ const nextEntry = {
4853
+ threadId,
4854
+ ...replyId !== void 0 ? { replyId } : {},
4855
+ state,
4856
+ ...note !== void 0 ? { note } : {},
4857
+ startedAt: now,
4858
+ updatedAt: now
4859
+ };
4860
+ const existingIdx = current.findIndex(
4861
+ (e) => e.threadId === threadId && e.replyId === replyId
4862
+ );
4863
+ let nextArray;
4864
+ if (existingIdx >= 0) {
4865
+ const existing = current[existingIdx];
4866
+ nextArray = current.slice();
4867
+ nextArray[existingIdx] = {
4868
+ ...existing,
4869
+ state,
4870
+ ...note !== void 0 ? { note } : {},
4871
+ updatedAt: now
4872
+ };
4873
+ } else {
4874
+ nextArray = [...current, nextEntry];
4875
+ }
4876
+ setLocalAwareness(this.provider.awareness, "agentWork", nextArray);
4877
+ }
4878
+ /**
4879
+ * Remove the `agentWork` entry matching `{threadId, replyId}`. No-op if no
4880
+ * match exists (idempotent). When the result is empty, the field is cleared
4881
+ * entirely so remote peers see `agentWork` absent.
4882
+ */
4883
+ clearAgentWork(threadId, replyId) {
4884
+ const current = this.readAgentWork();
4885
+ const nextArray = current.filter(
4886
+ (e) => !(e.threadId === threadId && e.replyId === replyId)
4887
+ );
4888
+ if (nextArray.length === current.length) return;
4889
+ setLocalAwareness(
4890
+ this.provider.awareness,
4891
+ "agentWork",
4892
+ nextArray.length === 0 ? void 0 : nextArray
4893
+ );
4894
+ }
4895
+ /** Current local `agentWork` array, or `[]` if absent / malformed. */
4896
+ readAgentWork() {
4897
+ const local = this.provider.awareness.getLocalState();
4898
+ const work = local?.agentWork;
4899
+ if (!Array.isArray(work)) return [];
4900
+ return work;
4901
+ }
4303
4902
  /** Timestamp (ms) of the most recent non-local transaction on this doc. */
4304
4903
  get lastRemoteActivityAt() {
4305
4904
  return this._lastRemoteActivityAt;
4306
4905
  }
4307
4906
  enqueue(ev) {
4308
4907
  const waiter = this.waiters.shift();
4309
- if (waiter) waiter(ev);
4310
- else this.queue.push(ev);
4908
+ if (waiter) {
4909
+ this.onEventDelivered(ev);
4910
+ waiter(ev);
4911
+ } else {
4912
+ this.queue.push(ev);
4913
+ }
4311
4914
  }
4312
4915
  watchMentions() {
4313
4916
  attachMentionObserver(this.doc, {
@@ -4316,9 +4919,26 @@ var RoomState = class {
4316
4919
  activeThreads: this.activeThreads,
4317
4920
  identityUserId: this.identity.userId,
4318
4921
  actingAs: this.actingAs,
4319
- getSoloHumanAuthorId: () => this.computeSoloHumanAuthorId()
4922
+ getSoloHumanAuthorId: () => this.computeSoloHumanAuthorId(),
4923
+ resolveUserName: (userId) => this.resolveUserName(userId)
4320
4924
  });
4321
4925
  }
4926
+ /**
4927
+ * Look up a display name for `userId` from the awareness roster. Returns
4928
+ * `undefined` if nobody is currently advertising that userId — in which case
4929
+ * the observer falls back to whatever `authorName` the triggering record
4930
+ * carries (still a usable fallback).
4931
+ */
4932
+ resolveUserName(userId) {
4933
+ const states = this.provider.awareness.getStates();
4934
+ for (const state of states.values()) {
4935
+ const user = state?.user;
4936
+ if (!user || typeof user.userId !== "string") continue;
4937
+ if (user.userId !== userId) continue;
4938
+ if (typeof user.name === "string" && user.name.length > 0) return user.name;
4939
+ }
4940
+ return void 0;
4941
+ }
4322
4942
  /**
4323
4943
  * Read the provider's awareness map and decide whether the room is "solo"
4324
4944
  * right now — exactly one agent (us) and exactly one human. Returns the
@@ -4364,10 +4984,24 @@ function attachMentionObserver(doc, opts) {
4364
4984
  const identityUserId = opts.identityUserId;
4365
4985
  const hasActingAsMention = buildActingAsMatcher(opts.actingAs);
4366
4986
  const getSoloHumanAuthorId = opts.getSoloHumanAuthorId ?? (() => void 0);
4987
+ const resolveUserName = opts.resolveUserName ?? (() => void 0);
4988
+ const makeInvokerFields = (authorUserId, authorName) => {
4989
+ const fields = {};
4990
+ if (authorUserId) {
4991
+ fields.invokerUserId = authorUserId;
4992
+ const resolved = resolveUserName(authorUserId);
4993
+ if (resolved) fields.invokerName = resolved;
4994
+ else if (authorName) fields.invokerName = authorName;
4995
+ } else if (authorName) {
4996
+ fields.invokerName = authorName;
4997
+ }
4998
+ return fields;
4999
+ };
4367
5000
  const scan = (kind, threadId, entry, isLocal) => {
4368
5001
  if (!entry || typeof entry !== "object") return;
4369
5002
  const record = entry;
4370
5003
  const bodyAuthorUserId = typeof record.authorUserId === "string" ? record.authorUserId : void 0;
5004
+ const bodyAuthorName = typeof record.authorName === "string" ? record.authorName : void 0;
4371
5005
  const replies = Array.isArray(record.replies) ? record.replies : [];
4372
5006
  let lastAgentIdx = -1;
4373
5007
  if (identityUserId !== void 0) {
@@ -4392,7 +5026,8 @@ function attachMentionObserver(doc, opts) {
4392
5026
  threadKind: kind,
4393
5027
  threadText: body,
4394
5028
  reason: "direct_mention",
4395
- ...resolveAnchoredContext(doc, record.anchorFrom, record.anchorTo)
5029
+ ...resolveAnchoredContext(doc, record.anchorFrom, record.anchorTo),
5030
+ ...makeInvokerFields(bodyAuthorUserId, bodyAuthorName)
4396
5031
  });
4397
5032
  }
4398
5033
  } else if (!seen.has(threadId) && !isLocal && !bodyAnswered && bodySidecar !== "miss" && !ANY_AT_MENTION_RE.test(body)) {
@@ -4405,7 +5040,8 @@ function attachMentionObserver(doc, opts) {
4405
5040
  threadKind: kind,
4406
5041
  threadText: body,
4407
5042
  reason: "solo_room",
4408
- ...resolveAnchoredContext(doc, record.anchorFrom, record.anchorTo)
5043
+ ...resolveAnchoredContext(doc, record.anchorFrom, record.anchorTo),
5044
+ ...makeInvokerFields(bodyAuthorUserId, bodyAuthorName)
4409
5045
  });
4410
5046
  }
4411
5047
  }
@@ -4420,18 +5056,21 @@ function attachMentionObserver(doc, opts) {
4420
5056
  seen.add(key);
4421
5057
  if (isLocal) continue;
4422
5058
  if (i <= lastAgentIdx) continue;
5059
+ const replyAuthorUserId = typeof reply.authorUserId === "string" ? reply.authorUserId : void 0;
5060
+ const replyAuthorName = typeof reply.authorName === "string" ? reply.authorName : void 0;
5061
+ const replyAuthorIsAgent = reply.authorIsAgent === true;
4423
5062
  const replySidecar = checkMentionsSidecar(reply.mentions, identityUserId);
4424
5063
  const isDirect = replySidecar === "hit" || replySidecar === "absent" && hasActingAsMention(reply.text);
4425
5064
  const inActiveThread = activeThreads.has(threadId);
4426
5065
  let reason = isDirect ? "direct_mention" : inActiveThread ? "active_thread" : null;
4427
5066
  if (!reason) {
4428
- const replyAuthor = typeof reply.authorUserId === "string" ? reply.authorUserId : void 0;
4429
5067
  const soloHuman = getSoloHumanAuthorId();
4430
- if (replySidecar !== "miss" && !ANY_AT_MENTION_RE.test(reply.text) && soloHuman && replyAuthor === soloHuman) {
5068
+ if (replySidecar !== "miss" && !ANY_AT_MENTION_RE.test(reply.text) && soloHuman && replyAuthorUserId === soloHuman) {
4431
5069
  reason = "solo_room";
4432
5070
  }
4433
5071
  }
4434
5072
  if (!reason) continue;
5073
+ if (replyAuthorIsAgent && reason !== "direct_mention") continue;
4435
5074
  enqueue({
4436
5075
  kind: "mention",
4437
5076
  threadId,
@@ -4439,7 +5078,8 @@ function attachMentionObserver(doc, opts) {
4439
5078
  threadText: reply.text,
4440
5079
  replyId: reply.id,
4441
5080
  reason,
4442
- ...resolveAnchoredContext(doc, record.anchorFrom, record.anchorTo)
5081
+ ...resolveAnchoredContext(doc, record.anchorFrom, record.anchorTo),
5082
+ ...makeInvokerFields(replyAuthorUserId, replyAuthorName)
4443
5083
  });
4444
5084
  }
4445
5085
  };
@@ -4455,7 +5095,7 @@ function attachMentionObserver(doc, opts) {
4455
5095
  doc.getMap("suggestions").observe(onChange("suggestion"));
4456
5096
  }
4457
5097
  function hashState(doc) {
4458
- return Buffer.from(Y4.encodeStateVector(doc)).toString("base64");
5098
+ return Buffer.from(Y5.encodeStateVector(doc)).toString("base64");
4459
5099
  }
4460
5100
  function attachRemoteActivityTracker(doc, opts) {
4461
5101
  const now = opts.now ?? (() => Date.now());
@@ -4467,7 +5107,7 @@ function attachRemoteActivityTracker(doc, opts) {
4467
5107
  // src/identity.ts
4468
5108
  import * as fs from "fs/promises";
4469
5109
  import * as path from "path";
4470
- import { nanoid as nanoid2 } from "nanoid";
5110
+ import { nanoid as nanoid3 } from "nanoid";
4471
5111
  var PALETTE = [
4472
5112
  "#9333ea",
4473
5113
  // purple-600
@@ -4522,7 +5162,7 @@ async function loadOrCreateIdentity(dir) {
4522
5162
  }
4523
5163
  }
4524
5164
  const identity = {
4525
- userId: nanoid2(),
5165
+ userId: nanoid3(),
4526
5166
  color: pickColor()
4527
5167
  };
4528
5168
  await saveIdentity(dir, identity);
@@ -4605,6 +5245,18 @@ var SERVER_HOST = process.env.COMPOSER_SERVER_HOST ?? "usecomposer.app";
4605
5245
  var APP_BASE = process.env.COMPOSER_APP_BASE ?? "https://usecomposer.app";
4606
5246
  var rooms = /* @__PURE__ */ new Map();
4607
5247
  var identityCache = null;
5248
+ function teardownAllRooms() {
5249
+ for (const [roomId, state] of rooms) {
5250
+ try {
5251
+ state.setMonitoring(false);
5252
+ state.disconnect();
5253
+ state.destroy();
5254
+ } catch (err) {
5255
+ logError(`teardown error for room ${roomId}`, err);
5256
+ }
5257
+ }
5258
+ rooms.clear();
5259
+ }
4608
5260
  async function getIdentity() {
4609
5261
  if (!identityCache) {
4610
5262
  identityCache = await loadOrCreateIdentity(COMPOSER_DIR2);
@@ -4699,12 +5351,12 @@ var TOOL_DEFS = [
4699
5351
  },
4700
5352
  {
4701
5353
  name: "composer_next_event",
4702
- description: "Block for up to `timeoutSec` (default 600 / 10 min) waiting for a remote event. Returns `{ kind: 'mention' | 'timeout', ... }`. **The monitor loop is always-on** \u2014 every return carries a directive you must follow without waiting for user input. On `mention`, the `reason` is one of: `direct_mention` (sidecar or text named you \u2014 always reply), `active_thread` (plain reply on a thread you're already in \u2014 reply if the content invites one), or `solo_room` (you're alone with one human who didn't explicitly tag anyone \u2014 default to a helpful reply, but skip if the text reads like a note-to-self, acknowledgement, or aside). Handle the event, then execute the return's `requiredNextToolCall`. On `timeout`, `recentActivity` tells you whether to keep monitoring or exit with the goodbye line from `userMessage`.",
5354
+ description: "Block for up to `timeoutSec` (default 30s) waiting for a remote event. Returns `{ kind: 'mention' | 'timeout', ... }`. **The monitor loop is always-on** \u2014 every return carries a directive you must follow without waiting for user input. On `mention`, the `reason` is one of: `direct_mention` (sidecar or text named you \u2014 always reply), `active_thread` (plain reply on a thread you're already in \u2014 reply if the content invites one), or `solo_room` (you're alone with one human who didn't explicitly tag anyone \u2014 default to a helpful reply, but skip if the text reads like a note-to-self, acknowledgement, or aside). Handle the event, then execute the return's `requiredNextToolCall`. On `timeout`, `recentActivity` tells you whether to keep monitoring or exit with the goodbye line from `userMessage`.",
4703
5355
  inputSchema: {
4704
5356
  type: "object",
4705
5357
  properties: {
4706
5358
  roomId: { type: "string" },
4707
- timeoutSec: { type: "number", default: 600 }
5359
+ timeoutSec: { type: "number", default: 30 }
4708
5360
  },
4709
5361
  required: ["roomId"]
4710
5362
  }
@@ -4744,7 +5396,7 @@ var TOOL_DEFS = [
4744
5396
  },
4745
5397
  {
4746
5398
  name: "composer_add_comment",
4747
- description: "Post a new top-level comment anchored to a text span anywhere in the doc. Anchor is { headingId, textToFind, occurrence? }. Use this to flag something the user didn't ask about \u2014 cross-referencing related sections, raising a concern elsewhere in the doc, or seeding a thread on a new span. Use `composer_reply_comment` instead when continuing an existing thread. Returns { id } on success or an isError result if the anchor cannot be resolved.",
5399
+ description: "Post a new top-level comment anchored to a text span anywhere in the doc. Anchor is { headingId, textToFind, occurrence? }. Use this to flag something the user didn't ask about \u2014 cross-referencing related sections, raising a concern elsewhere in the doc, or seeding a thread on a new span. Use `composer_reply_comment` instead when continuing an existing thread. Accepts optional `state` (ack-first flow: post with `state: \"thinking\"` to start the live indicator immediately) and `mentions` (array of target userIds \u2014 the invoker's userId from the mention event payload, for the `@invoker` backlink). Returns { id } on success or an isError result if the anchor cannot be resolved.",
4748
5400
  inputSchema: {
4749
5401
  type: "object",
4750
5402
  properties: {
@@ -4758,20 +5410,40 @@ var TOOL_DEFS = [
4758
5410
  },
4759
5411
  required: ["headingId", "textToFind"]
4760
5412
  },
4761
- text: { type: "string" }
5413
+ text: { type: "string" },
5414
+ state: {
5415
+ type: "string",
5416
+ enum: ["thinking", "working", "replying", "ready"],
5417
+ description: 'Initial lifecycle state for the new comment. Set to "thinking" when posting an ack-first placeholder so the live indicator renders from the moment the record lands \u2014 no stateless flicker while a separate composer_agent_status call catches up.'
5418
+ },
5419
+ mentions: {
5420
+ type: "array",
5421
+ items: { type: "string" },
5422
+ description: "Target userIds of @-mentions in `text`, in order of occurrence. Typically a single entry: the invoker's userId from the triggering mention event. Rendered as bolded mention spans by the client."
5423
+ }
4762
5424
  },
4763
5425
  required: ["roomId", "anchor", "text"]
4764
5426
  }
4765
5427
  },
4766
5428
  {
4767
5429
  name: "composer_reply_comment",
4768
- description: "Append a reply to an existing comment thread.",
5430
+ description: 'Append a reply to an existing comment thread. Accepts optional `state` (ack-first flow: post with `state: "thinking"` to start the live indicator immediately \u2014 avoids the stateless flicker that occurs if state is added in a separate composer_agent_status call after the reply lands) and `mentions` (array of target userIds \u2014 typically the invoker\'s userId from the triggering mention event).',
4769
5431
  inputSchema: {
4770
5432
  type: "object",
4771
5433
  properties: {
4772
5434
  roomId: { type: "string" },
4773
5435
  threadId: { type: "string" },
4774
- text: { type: "string" }
5436
+ text: { type: "string" },
5437
+ state: {
5438
+ type: "string",
5439
+ enum: ["thinking", "working", "replying", "ready"],
5440
+ description: 'Initial lifecycle state for the new reply. Set to "thinking" when posting an ack-first placeholder.'
5441
+ },
5442
+ mentions: {
5443
+ type: "array",
5444
+ items: { type: "string" },
5445
+ description: "Target userIds of @-mentions in `text`. Typically the invoker's userId."
5446
+ }
4775
5447
  },
4776
5448
  required: ["roomId", "threadId", "text"]
4777
5449
  }
@@ -4796,20 +5468,40 @@ var TOOL_DEFS = [
4796
5468
  },
4797
5469
  required: ["headingId", "textToFind"]
4798
5470
  },
4799
- replacementText: { type: "string" }
5471
+ replacementText: { type: "string" },
5472
+ state: {
5473
+ type: "string",
5474
+ enum: ["thinking", "working", "replying", "ready"],
5475
+ description: `Initial lifecycle state for the new suggestion. Set to "working" when drafting a placeholder replacementText users shouldn't accept yet.`
5476
+ },
5477
+ mentions: {
5478
+ type: "array",
5479
+ items: { type: "string" },
5480
+ description: "Target userIds of @-mentions in `replacementText`."
5481
+ }
4800
5482
  },
4801
5483
  required: ["roomId", "replacementText"]
4802
5484
  }
4803
5485
  },
4804
5486
  {
4805
5487
  name: "composer_reply_suggestion",
4806
- description: "Append a reply to an existing suggestion thread.",
5488
+ description: "Append a reply to an existing suggestion thread. Accepts optional `state` and `mentions` with the same semantics as `composer_reply_comment`.",
4807
5489
  inputSchema: {
4808
5490
  type: "object",
4809
5491
  properties: {
4810
5492
  roomId: { type: "string" },
4811
5493
  threadId: { type: "string" },
4812
- text: { type: "string" }
5494
+ text: { type: "string" },
5495
+ state: {
5496
+ type: "string",
5497
+ enum: ["thinking", "working", "replying", "ready"],
5498
+ description: "Initial lifecycle state for the new reply."
5499
+ },
5500
+ mentions: {
5501
+ type: "array",
5502
+ items: { type: "string" },
5503
+ description: "Target userIds of @-mentions in `text`."
5504
+ }
4813
5505
  },
4814
5506
  required: ["roomId", "threadId", "text"]
4815
5507
  }
@@ -4825,6 +5517,56 @@ var TOOL_DEFS = [
4825
5517
  },
4826
5518
  required: ["roomId", "threadId"]
4827
5519
  }
5520
+ },
5521
+ {
5522
+ name: "composer_agent_status",
5523
+ description: 'Drive the live state indicator on an in-flight agent-authored record (a reply, or the thread-head Comment / Suggestion). Use AFTER posting an ack with `state: "thinking"` via the reply/add tools \u2014 this tool advances state mid-work without creating a new reply, and optionally rewrites text in place at completion. Transitions: thinking \u2192 working (tool use started) \u2192 replying (assembling final answer, optional) \u2192 ready (terminal; text now holds the final content OR a thin pointer to a standalone artifact). When `replyId` is set, updates that reply inside `threadId`; when absent, updates the thread-head record (use `kind` to disambiguate). Returns { replyId } for reply updates or { id } for thread-head updates.',
5524
+ inputSchema: {
5525
+ type: "object",
5526
+ properties: {
5527
+ roomId: { type: "string" },
5528
+ threadId: { type: "string" },
5529
+ replyId: {
5530
+ type: "string",
5531
+ description: "ID of the reply to update. Omit to update the thread-head record itself (an agent-authored Comment or Suggestion)."
5532
+ },
5533
+ state: {
5534
+ type: "string",
5535
+ enum: ["thinking", "working", "replying", "ready"],
5536
+ description: "New lifecycle state. `ready` is terminal \u2014 pair with `text` for the final content. Non-ready states update the heartbeat; `text` is optional."
5537
+ },
5538
+ text: {
5539
+ type: "string",
5540
+ description: "Optional new body text. On `ready` transitions this typically carries the final answer or a pointer to a standalone artifact. For suggestion thread-head updates this writes to `replacementText`."
5541
+ },
5542
+ note: {
5543
+ type: "string",
5544
+ description: 'Optional short label describing what the agent is currently doing, e.g. "reading section 3\u2026". Rides on the awareness heartbeat only; not persisted on the record.'
5545
+ },
5546
+ kind: {
5547
+ type: "string",
5548
+ enum: ["comment", "suggestion"],
5549
+ description: 'Disambiguates which map holds the thread-head record. Defaults to "comment"; optional when `replyId` is set.'
5550
+ }
5551
+ },
5552
+ required: ["roomId", "threadId", "state"]
5553
+ }
5554
+ },
5555
+ {
5556
+ name: "composer_done",
5557
+ description: 'Signal that the agent is finished with a thread WITHOUT posting a reply \u2014 clears the live indicator left over from `onEventDelivered` (which publishes a thread-head `agentWork(threadId, undefined, "thinking")` placeholder when a mention is dequeued). Call this when you decide to skip a mention (the skill\'s heuristic filters out chatter, your own mentions, etc.) or otherwise close out a thread without a `state: "ready"` reply landing. The normal reply-with-`ready` path already clears its own placeholder; this tool exists for the skip case where no reply is ever written. Idempotent \u2014 clearing a non-existent entry is a no-op. When `replyId` is set, also clears that per-reply entry (use if you abandoned a thinking-state ack mid-flight).',
5558
+ inputSchema: {
5559
+ type: "object",
5560
+ properties: {
5561
+ roomId: { type: "string" },
5562
+ threadId: { type: "string" },
5563
+ replyId: {
5564
+ type: "string",
5565
+ description: 'Optional reply ID. When set, also clears the per-reply `agentWork` entry \u2014 useful if you posted an ack with `state: "thinking"` and then abandoned the work without reaching `ready`.'
5566
+ }
5567
+ },
5568
+ required: ["roomId", "threadId"]
5569
+ }
4828
5570
  }
4829
5571
  ];
4830
5572
  function okResult(data) {
@@ -4857,6 +5599,33 @@ function asOptionalString(value, field) {
4857
5599
  }
4858
5600
  return value;
4859
5601
  }
5602
+ var AGENT_REPLY_STATES = [
5603
+ "thinking",
5604
+ "working",
5605
+ "replying",
5606
+ "ready"
5607
+ ];
5608
+ function asOptionalAgentState(value, field) {
5609
+ if (value === void 0) return void 0;
5610
+ if (typeof value !== "string") {
5611
+ throw new Error(`${field} must be a string`);
5612
+ }
5613
+ if (!AGENT_REPLY_STATES.includes(value)) {
5614
+ throw new Error(
5615
+ `${field} must be one of: ${AGENT_REPLY_STATES.join(", ")}`
5616
+ );
5617
+ }
5618
+ return value;
5619
+ }
5620
+ function asAgentState(value, field) {
5621
+ const s = asOptionalAgentState(value, field);
5622
+ if (s === void 0) {
5623
+ throw new Error(
5624
+ `${field} must be one of: ${AGENT_REPLY_STATES.join(", ")}`
5625
+ );
5626
+ }
5627
+ return s;
5628
+ }
4860
5629
  function asAnchor(value) {
4861
5630
  if (!value || typeof value !== "object") {
4862
5631
  throw new Error("anchor must be an object");
@@ -4919,7 +5688,7 @@ async function handleCreateRoom(args) {
4919
5688
  }
4920
5689
  const { actingAs, isFirstRun } = await resolveActingAs(actingAsArg);
4921
5690
  const identity = await getIdentity();
4922
- const roomId = nanoid3(10);
5691
+ const roomId = nanoid4(10);
4923
5692
  const browserUrl = browserUrlFor(roomId);
4924
5693
  const state = new RoomState({
4925
5694
  roomId,
@@ -4927,12 +5696,13 @@ async function handleCreateRoom(args) {
4927
5696
  actingAs,
4928
5697
  identity
4929
5698
  });
4930
- await state.waitForInitialSync();
5699
+ await state.ensureConnected();
4931
5700
  if (seedMarkdown) {
4932
5701
  writeMarkdownToFragment(state.doc.getXmlFragment("default"), seedMarkdown);
4933
5702
  }
4934
5703
  rooms.set(roomId, state);
4935
5704
  log(`composer room created \u2192 ${browserUrl}`, { roomId, actingAs });
5705
+ state.scheduleIdleDisconnect(3e4);
4936
5706
  return okResult({
4937
5707
  roomId,
4938
5708
  browserUrl,
@@ -4953,6 +5723,7 @@ async function handleJoinRoom(args) {
4953
5723
  roomId,
4954
5724
  actingAs: existing.actingAs
4955
5725
  });
5726
+ existing.scheduleIdleDisconnect(3e4);
4956
5727
  return okResult({
4957
5728
  roomId,
4958
5729
  browserUrl,
@@ -4974,9 +5745,10 @@ async function handleJoinRoom(args) {
4974
5745
  actingAs,
4975
5746
  identity
4976
5747
  });
4977
- await state.waitForInitialSync();
5748
+ await state.ensureConnected();
4978
5749
  rooms.set(roomId, state);
4979
5750
  log(`composer room joined \u2192 ${browserUrl}`, { roomId, actingAs });
5751
+ state.scheduleIdleDisconnect(3e4);
4980
5752
  return okResult({
4981
5753
  roomId,
4982
5754
  browserUrl,
@@ -4991,14 +5763,41 @@ function handleAttachRoom(args) {
4991
5763
  const state = getOrError(roomId);
4992
5764
  return okResult({ roomId, snapshot: state.snapshot() });
4993
5765
  }
4994
- var ACTIVITY_WINDOW_MS = 10 * 60 * 1e3;
5766
+ var ACTIVITY_WINDOW_MS = (() => {
5767
+ const raw = process.env.COMPOSER_ACTIVITY_WINDOW_MS;
5768
+ if (!raw) return 15 * 60 * 1e3;
5769
+ const n = Number(raw);
5770
+ return Number.isFinite(n) && n > 0 ? n : 15 * 60 * 1e3;
5771
+ })();
4995
5772
  var LEAVE_MESSAGE = "I've left the document. You can ask me to rejoin anytime and I'll continue replying.";
4996
- async function handleNextEvent(args) {
5773
+ async function handleNextEvent(args, signal) {
4997
5774
  const a = asObject(args);
4998
5775
  const roomId = asString(a.roomId, "roomId");
4999
- const timeoutSec = typeof a.timeoutSec === "number" && Number.isFinite(a.timeoutSec) ? a.timeoutSec : 600;
5776
+ const timeoutSec = typeof a.timeoutSec === "number" && Number.isFinite(a.timeoutSec) ? a.timeoutSec : 30;
5000
5777
  const state = getOrError(roomId);
5001
- const event = await state.nextEvent(timeoutSec * 1e3);
5778
+ if (state.hasIssuedGoodbye) {
5779
+ return errorResult(
5780
+ "Already said goodbye for this room. The user must explicitly ask you to rejoin (which will trigger a fresh `composer_join_room`). Do not call `composer_next_event` again on this roomId."
5781
+ );
5782
+ }
5783
+ state.setMonitoring(true);
5784
+ const onAbort = () => {
5785
+ state.setMonitoring(false);
5786
+ state.disconnect();
5787
+ };
5788
+ if (signal) {
5789
+ if (signal.aborted) onAbort();
5790
+ else signal.addEventListener("abort", onAbort, { once: true });
5791
+ }
5792
+ let event;
5793
+ try {
5794
+ event = await state.nextEvent(timeoutSec * 1e3, signal);
5795
+ } finally {
5796
+ signal?.removeEventListener("abort", onAbort);
5797
+ }
5798
+ if (signal?.aborted) {
5799
+ return okResult({ kind: "aborted" });
5800
+ }
5002
5801
  if (event.kind === "mention") {
5003
5802
  return okResult({
5004
5803
  ...event,
@@ -5024,6 +5823,9 @@ async function handleNextEvent(args) {
5024
5823
  }
5025
5824
  });
5026
5825
  }
5826
+ state.setMonitoring(false);
5827
+ state.disconnect();
5828
+ state.markGoodbyeIssued();
5027
5829
  return okResult({
5028
5830
  kind: "timeout",
5029
5831
  recentActivity: false,
@@ -5095,12 +5897,10 @@ function handleGetThread(args) {
5095
5897
  replies: shapedReplies
5096
5898
  });
5097
5899
  }
5098
- function handleAddComment(args) {
5099
- const a = asObject(args);
5100
- const roomId = asString(a.roomId, "roomId");
5900
+ function performAddComment(state, a) {
5101
5901
  const anchor = asAnchor(a.anchor);
5102
5902
  const text = asString(a.text, "text");
5103
- const state = getOrError(roomId);
5903
+ const agentState = asOptionalAgentState(a.state, "state");
5104
5904
  const resolved = resolveServerAnchor(state.doc, anchor);
5105
5905
  if (!resolved.ok) {
5106
5906
  return errorResult(
@@ -5108,36 +5908,59 @@ function handleAddComment(args) {
5108
5908
  ${resolved.currentSectionText}`
5109
5909
  );
5110
5910
  }
5111
- const id = nanoid3();
5112
- const comments = state.doc.getMap("comments");
5113
- comments.set(id, {
5911
+ const id = nanoid4();
5912
+ const createdAt = Date.now();
5913
+ const comment = {
5114
5914
  id,
5115
5915
  authorName: state.actingAs,
5116
5916
  authorColor: state.identity.color,
5117
5917
  authorUserId: state.identity.userId,
5118
5918
  authorIsAgent: true,
5119
5919
  text,
5120
- createdAt: Date.now(),
5920
+ createdAt,
5121
5921
  resolved: false,
5122
5922
  anchorFrom: resolved.from,
5123
5923
  anchorTo: resolved.to,
5124
- replies: []
5924
+ replies: [],
5925
+ ...agentState !== void 0 ? { state: agentState } : {}
5926
+ };
5927
+ Y6.transact(state.doc, () => {
5928
+ state.doc.getMap("comments").set(id, comment);
5929
+ emitActivity(state.doc, {
5930
+ type: "comment",
5931
+ threadId: id,
5932
+ authorName: state.actingAs,
5933
+ authorColor: state.identity.color,
5934
+ authorUserId: state.identity.userId,
5935
+ authorIsAgent: true,
5936
+ text: textPreview(text),
5937
+ participantUserIds: getParticipantUserIds(comment),
5938
+ createdAt
5939
+ });
5125
5940
  });
5126
5941
  state.markThreadActive(id);
5942
+ if (agentState !== void 0 && agentState !== "ready") {
5943
+ state.publishAgentWork(id, void 0, agentState);
5944
+ }
5127
5945
  return okResult({ id });
5128
5946
  }
5129
- function handleReplyComment(args) {
5947
+ function handleAddComment(args) {
5130
5948
  const a = asObject(args);
5131
5949
  const roomId = asString(a.roomId, "roomId");
5950
+ const state = getOrError(roomId);
5951
+ return performAddComment(state, a);
5952
+ }
5953
+ function performReplyComment(state, a) {
5132
5954
  const threadId = asString(a.threadId, "threadId");
5133
5955
  const text = asString(a.text, "text");
5134
- const state = getOrError(roomId);
5956
+ const agentState = asOptionalAgentState(a.state, "state");
5135
5957
  const comments = state.doc.getMap("comments");
5136
5958
  const existing = comments.get(threadId);
5137
5959
  if (!existing) {
5138
5960
  return errorResult(`comment not found: ${threadId}`);
5139
5961
  }
5140
- const replyId = nanoid3();
5962
+ const replyId = nanoid4();
5963
+ const createdAt = Date.now();
5141
5964
  const reply = {
5142
5965
  id: replyId,
5143
5966
  authorName: state.actingAs,
@@ -5145,21 +5968,49 @@ function handleReplyComment(args) {
5145
5968
  authorUserId: state.identity.userId,
5146
5969
  authorIsAgent: true,
5147
5970
  text,
5148
- createdAt: Date.now()
5971
+ createdAt,
5972
+ ...agentState !== void 0 ? { state: agentState } : {}
5149
5973
  };
5150
- comments.set(threadId, {
5974
+ const updated = {
5151
5975
  ...existing,
5152
5976
  replies: [...existing.replies ?? [], reply]
5977
+ };
5978
+ Y6.transact(state.doc, () => {
5979
+ comments.set(threadId, updated);
5980
+ emitActivity(state.doc, {
5981
+ type: "reply",
5982
+ threadId,
5983
+ replyId,
5984
+ threadKind: "comment",
5985
+ authorName: state.actingAs,
5986
+ authorColor: state.identity.color,
5987
+ authorUserId: state.identity.userId,
5988
+ authorIsAgent: true,
5989
+ text: textPreview(text),
5990
+ participantUserIds: getParticipantUserIds(
5991
+ updated
5992
+ ),
5993
+ createdAt
5994
+ });
5153
5995
  });
5154
5996
  state.markThreadActive(threadId);
5997
+ if (agentState !== void 0 && agentState !== "ready") {
5998
+ state.publishAgentWork(threadId, replyId, agentState);
5999
+ } else if (agentState === "ready") {
6000
+ state.clearAgentWork(threadId, void 0);
6001
+ }
5155
6002
  return okResult({ replyId });
5156
6003
  }
5157
- function handleAddSuggestion(args) {
6004
+ function handleReplyComment(args) {
5158
6005
  const a = asObject(args);
5159
6006
  const roomId = asString(a.roomId, "roomId");
6007
+ const state = getOrError(roomId);
6008
+ return performReplyComment(state, a);
6009
+ }
6010
+ function performAddSuggestion(state, a) {
5160
6011
  const replacementText = asString(a.replacementText, "replacementText");
5161
6012
  const fromThreadId = asOptionalString(a.fromThreadId, "fromThreadId");
5162
- const state = getOrError(roomId);
6013
+ const agentState = asOptionalAgentState(a.state, "state");
5163
6014
  let anchorFrom;
5164
6015
  let anchorTo;
5165
6016
  let originalText;
@@ -5195,38 +6046,61 @@ ${resolved.currentSectionText}`
5195
6046
  anchorTo = resolved.to;
5196
6047
  originalText = anchor.textToFind;
5197
6048
  }
5198
- const id = nanoid3();
5199
- const suggestions = state.doc.getMap("suggestions");
5200
- suggestions.set(id, {
6049
+ const id = nanoid4();
6050
+ const createdAt = Date.now();
6051
+ const suggestion = {
5201
6052
  id,
5202
6053
  authorName: state.actingAs,
5203
6054
  authorColor: state.identity.color,
5204
6055
  authorUserId: state.identity.userId,
5205
6056
  authorIsAgent: true,
5206
- createdAt: Date.now(),
6057
+ createdAt,
5207
6058
  status: "pending",
5208
6059
  anchorFrom,
5209
6060
  anchorTo,
5210
6061
  replacementText,
5211
6062
  originalText,
5212
- replies: []
6063
+ replies: [],
6064
+ ...agentState !== void 0 ? { state: agentState } : {}
6065
+ };
6066
+ Y6.transact(state.doc, () => {
6067
+ state.doc.getMap("suggestions").set(id, suggestion);
6068
+ emitActivity(state.doc, {
6069
+ type: "suggestion",
6070
+ threadId: id,
6071
+ authorName: state.actingAs,
6072
+ authorColor: state.identity.color,
6073
+ authorUserId: state.identity.userId,
6074
+ authorIsAgent: true,
6075
+ text: textPreview(replacementText),
6076
+ participantUserIds: getParticipantUserIds(suggestion),
6077
+ createdAt
6078
+ });
5213
6079
  });
5214
6080
  state.markThreadActive(id);
5215
6081
  if (fromThreadId) state.markThreadActive(fromThreadId);
6082
+ if (agentState !== void 0 && agentState !== "ready") {
6083
+ state.publishAgentWork(id, void 0, agentState);
6084
+ }
5216
6085
  return okResult({ id });
5217
6086
  }
5218
- function handleReplySuggestion(args) {
6087
+ function handleAddSuggestion(args) {
5219
6088
  const a = asObject(args);
5220
6089
  const roomId = asString(a.roomId, "roomId");
6090
+ const state = getOrError(roomId);
6091
+ return performAddSuggestion(state, a);
6092
+ }
6093
+ function performReplySuggestion(state, a) {
5221
6094
  const threadId = asString(a.threadId, "threadId");
5222
6095
  const text = asString(a.text, "text");
5223
- const state = getOrError(roomId);
6096
+ const agentState = asOptionalAgentState(a.state, "state");
5224
6097
  const suggestions = state.doc.getMap("suggestions");
5225
6098
  const existing = suggestions.get(threadId);
5226
6099
  if (!existing) {
5227
6100
  return errorResult(`suggestion not found: ${threadId}`);
5228
6101
  }
5229
- const replyId = nanoid3();
6102
+ const replyId = nanoid4();
6103
+ const createdAt = Date.now();
5230
6104
  const reply = {
5231
6105
  id: replyId,
5232
6106
  authorName: state.actingAs,
@@ -5234,56 +6108,237 @@ function handleReplySuggestion(args) {
5234
6108
  authorUserId: state.identity.userId,
5235
6109
  authorIsAgent: true,
5236
6110
  text,
5237
- createdAt: Date.now()
6111
+ createdAt,
6112
+ ...agentState !== void 0 ? { state: agentState } : {}
5238
6113
  };
5239
- suggestions.set(threadId, {
6114
+ const updated = {
5240
6115
  ...existing,
5241
6116
  replies: [...existing.replies ?? [], reply]
6117
+ };
6118
+ Y6.transact(state.doc, () => {
6119
+ suggestions.set(threadId, updated);
6120
+ emitActivity(state.doc, {
6121
+ type: "reply",
6122
+ threadId,
6123
+ replyId,
6124
+ threadKind: "suggestion",
6125
+ authorName: state.actingAs,
6126
+ authorColor: state.identity.color,
6127
+ authorUserId: state.identity.userId,
6128
+ authorIsAgent: true,
6129
+ text: textPreview(text),
6130
+ participantUserIds: getParticipantUserIds(
6131
+ updated
6132
+ ),
6133
+ createdAt
6134
+ });
5242
6135
  });
5243
6136
  state.markThreadActive(threadId);
6137
+ if (agentState !== void 0 && agentState !== "ready") {
6138
+ state.publishAgentWork(threadId, replyId, agentState);
6139
+ } else if (agentState === "ready") {
6140
+ state.clearAgentWork(threadId, void 0);
6141
+ }
5244
6142
  return okResult({ replyId });
5245
6143
  }
5246
- function handleResolveThread(args) {
6144
+ function handleReplySuggestion(args) {
5247
6145
  const a = asObject(args);
5248
6146
  const roomId = asString(a.roomId, "roomId");
5249
- const threadId = asString(a.threadId, "threadId");
5250
6147
  const state = getOrError(roomId);
6148
+ return performReplySuggestion(state, a);
6149
+ }
6150
+ function performResolveThread(state, a) {
6151
+ const threadId = asString(a.threadId, "threadId");
5251
6152
  const comments = state.doc.getMap("comments");
5252
6153
  const existing = comments.get(threadId);
5253
6154
  if (!existing) {
5254
6155
  return errorResult(`comment not found: ${threadId}`);
5255
6156
  }
5256
- comments.set(threadId, { ...existing, resolved: true });
6157
+ const createdAt = Date.now();
6158
+ Y6.transact(state.doc, () => {
6159
+ comments.set(threadId, { ...existing, resolved: true });
6160
+ emitActivity(state.doc, {
6161
+ type: "resolve",
6162
+ threadId,
6163
+ authorName: state.actingAs,
6164
+ authorColor: state.identity.color,
6165
+ authorUserId: state.identity.userId,
6166
+ authorIsAgent: true,
6167
+ participantUserIds: getParticipantUserIds(existing),
6168
+ createdAt
6169
+ });
6170
+ });
5257
6171
  return okResult({ threadId, resolved: true });
5258
6172
  }
5259
- async function dispatchTool(name, args) {
5260
- switch (name) {
5261
- case "composer_create_room":
5262
- return handleCreateRoom(args);
5263
- case "composer_join_room":
5264
- return handleJoinRoom(args);
5265
- case "composer_attach_room":
5266
- return handleAttachRoom(args);
5267
- case "composer_next_event":
5268
- return handleNextEvent(args);
5269
- case "composer_get_section":
5270
- return handleGetSection(args);
5271
- case "composer_get_full_doc":
5272
- return handleGetFullDoc(args);
5273
- case "composer_get_thread":
5274
- return handleGetThread(args);
5275
- case "composer_add_comment":
5276
- return handleAddComment(args);
5277
- case "composer_reply_comment":
5278
- return handleReplyComment(args);
5279
- case "composer_add_suggestion":
5280
- return handleAddSuggestion(args);
5281
- case "composer_reply_suggestion":
5282
- return handleReplySuggestion(args);
5283
- case "composer_resolve_thread":
5284
- return handleResolveThread(args);
5285
- default:
5286
- return errorResult(`unknown tool: ${name}`);
6173
+ function handleResolveThread(args) {
6174
+ const a = asObject(args);
6175
+ const roomId = asString(a.roomId, "roomId");
6176
+ const state = getOrError(roomId);
6177
+ return performResolveThread(state, a);
6178
+ }
6179
+ function performAgentStatus(state, a) {
6180
+ const threadId = asString(a.threadId, "threadId");
6181
+ const agentState = asAgentState(a.state, "state");
6182
+ const replyIdArg = asOptionalString(a.replyId, "replyId");
6183
+ const textArg = asOptionalString(a.text, "text");
6184
+ asOptionalString(a.note, "note");
6185
+ const note = typeof a.note === "string" && a.note.length > 0 ? a.note : void 0;
6186
+ const kindArg = a.kind === void 0 ? "comment" : a.kind === "comment" || a.kind === "suggestion" ? a.kind : null;
6187
+ if (kindArg === null) {
6188
+ return errorResult(`kind must be "comment" or "suggestion"`);
6189
+ }
6190
+ const commentsMap = state.doc.getMap("comments");
6191
+ const suggestionsMap = state.doc.getMap("suggestions");
6192
+ const preferredMap = kindArg === "comment" ? commentsMap : suggestionsMap;
6193
+ const fallbackMap = kindArg === "comment" ? suggestionsMap : commentsMap;
6194
+ let parent = preferredMap.get(threadId);
6195
+ let resolvedKind = kindArg;
6196
+ if (!parent) {
6197
+ const fallback = fallbackMap.get(threadId);
6198
+ if (fallback) {
6199
+ parent = fallback;
6200
+ resolvedKind = kindArg === "comment" ? "suggestion" : "comment";
6201
+ }
6202
+ }
6203
+ if (!parent) {
6204
+ return errorResult(`thread not found: ${threadId}`);
6205
+ }
6206
+ const parentMap = resolvedKind === "comment" ? commentsMap : suggestionsMap;
6207
+ const currentWork = state.readAgentWork();
6208
+ for (const entry of currentWork) {
6209
+ const p = commentsMap.get(entry.threadId) ?? suggestionsMap.get(entry.threadId);
6210
+ if (!p) continue;
6211
+ if (entry.replyId !== void 0) {
6212
+ const replies = Array.isArray(p.replies) ? p.replies : [];
6213
+ const targetReply = replies.find(
6214
+ (r) => !!r && typeof r === "object" && r.id === entry.replyId
6215
+ );
6216
+ if (targetReply && targetReply.state === "ready") {
6217
+ state.clearAgentWork(entry.threadId, entry.replyId);
6218
+ }
6219
+ } else {
6220
+ if (p.state === "ready") {
6221
+ state.clearAgentWork(entry.threadId, void 0);
6222
+ }
6223
+ }
6224
+ }
6225
+ if (replyIdArg !== void 0) {
6226
+ const replies = Array.isArray(parent.replies) ? parent.replies : [];
6227
+ const replyIdx = replies.findIndex(
6228
+ (r) => !!r && typeof r === "object" && r.id === replyIdArg
6229
+ );
6230
+ if (replyIdx < 0) {
6231
+ return errorResult(`reply not found: ${replyIdArg} on thread ${threadId}`);
6232
+ }
6233
+ if (agentState === "ready") {
6234
+ state.clearAgentWork(threadId, replyIdArg);
6235
+ state.clearAgentWork(threadId, void 0);
6236
+ }
6237
+ const existingReply = replies[replyIdx];
6238
+ const nextReply = {
6239
+ ...existingReply,
6240
+ state: agentState
6241
+ };
6242
+ if (textArg !== void 0) nextReply.text = textArg;
6243
+ const nextReplies = replies.slice();
6244
+ nextReplies[replyIdx] = nextReply;
6245
+ parentMap.set(threadId, { ...parent, replies: nextReplies });
6246
+ if (agentState !== "ready") {
6247
+ state.publishAgentWork(threadId, replyIdArg, agentState, note);
6248
+ }
6249
+ return okResult({ replyId: replyIdArg });
6250
+ }
6251
+ if (agentState === "ready") {
6252
+ state.clearAgentWork(threadId, void 0);
6253
+ }
6254
+ const nextRecord = { ...parent, state: agentState };
6255
+ if (textArg !== void 0) {
6256
+ if (resolvedKind === "suggestion") {
6257
+ nextRecord.replacementText = textArg;
6258
+ } else {
6259
+ nextRecord.text = textArg;
6260
+ }
6261
+ }
6262
+ parentMap.set(threadId, nextRecord);
6263
+ if (agentState !== "ready") {
6264
+ state.publishAgentWork(threadId, void 0, agentState, note);
6265
+ }
6266
+ return okResult({ id: threadId });
6267
+ }
6268
+ function handleAgentStatus(args) {
6269
+ const a = asObject(args);
6270
+ const roomId = asString(a.roomId, "roomId");
6271
+ const state = getOrError(roomId);
6272
+ return performAgentStatus(state, a);
6273
+ }
6274
+ function performDone(state, a) {
6275
+ const threadId = asString(a.threadId, "threadId");
6276
+ const replyIdArg = asOptionalString(a.replyId, "replyId");
6277
+ state.clearAgentWork(threadId, void 0);
6278
+ if (replyIdArg !== void 0) {
6279
+ state.clearAgentWork(threadId, replyIdArg);
6280
+ }
6281
+ return okResult({ threadId });
6282
+ }
6283
+ function handleDone(args) {
6284
+ const a = asObject(args);
6285
+ const roomId = asString(a.roomId, "roomId");
6286
+ const state = getOrError(roomId);
6287
+ return performDone(state, a);
6288
+ }
6289
+ function _registerRoomForTest(roomId, state) {
6290
+ rooms.set(roomId, state);
6291
+ }
6292
+ function _clearRoomsForTest() {
6293
+ rooms.clear();
6294
+ }
6295
+ async function dispatchTool(name, args, signal) {
6296
+ const roomId = args && typeof args === "object" && "roomId" in args ? args.roomId : void 0;
6297
+ const needsExistingRoom = name !== "composer_create_room" && name !== "composer_join_room";
6298
+ const state = needsExistingRoom && typeof roomId === "string" ? rooms.get(roomId) : void 0;
6299
+ if (state) {
6300
+ await state.ensureConnected();
6301
+ }
6302
+ try {
6303
+ try {
6304
+ switch (name) {
6305
+ case "composer_create_room":
6306
+ return await handleCreateRoom(args);
6307
+ case "composer_join_room":
6308
+ return await handleJoinRoom(args);
6309
+ case "composer_attach_room":
6310
+ return handleAttachRoom(args);
6311
+ case "composer_next_event":
6312
+ return await handleNextEvent(args, signal);
6313
+ case "composer_get_section":
6314
+ return handleGetSection(args);
6315
+ case "composer_get_full_doc":
6316
+ return handleGetFullDoc(args);
6317
+ case "composer_get_thread":
6318
+ return handleGetThread(args);
6319
+ case "composer_add_comment":
6320
+ return handleAddComment(args);
6321
+ case "composer_reply_comment":
6322
+ return handleReplyComment(args);
6323
+ case "composer_add_suggestion":
6324
+ return handleAddSuggestion(args);
6325
+ case "composer_reply_suggestion":
6326
+ return handleReplySuggestion(args);
6327
+ case "composer_resolve_thread":
6328
+ return handleResolveThread(args);
6329
+ case "composer_agent_status":
6330
+ return handleAgentStatus(args);
6331
+ case "composer_done":
6332
+ return handleDone(args);
6333
+ default:
6334
+ return errorResult(`unknown tool: ${name}`);
6335
+ }
6336
+ } catch (err) {
6337
+ const message = err instanceof Error ? err.message : String(err);
6338
+ return errorResult(message);
6339
+ }
6340
+ } finally {
6341
+ state?.scheduleIdleDisconnect(3e4);
5287
6342
  }
5288
6343
  }
5289
6344
  function buildServer() {
@@ -5294,13 +6349,13 @@ function buildServer() {
5294
6349
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
5295
6350
  tools: TOOL_DEFS
5296
6351
  }));
5297
- server.setRequestHandler(CallToolRequestSchema, async (req) => {
6352
+ server.setRequestHandler(CallToolRequestSchema, async (req, extra) => {
5298
6353
  const { name, arguments: args } = req.params;
5299
6354
  const roomId = args && typeof args === "object" && "roomId" in args ? args.roomId : void 0;
5300
6355
  const started = Date.now();
5301
6356
  log(`tool ${name} call`, { roomId });
5302
6357
  try {
5303
- const result = await dispatchTool(name, args);
6358
+ const result = await dispatchTool(name, args, extra.signal);
5304
6359
  const elapsedMs = Date.now() - started;
5305
6360
  if (result.isError) {
5306
6361
  const detail = result.content[0]?.text ?? "";
@@ -5327,7 +6382,7 @@ async function startMcpServer() {
5327
6382
  log("mcp server starting", {
5328
6383
  pid: process.pid,
5329
6384
  node: process.version,
5330
- build: "awareness-heartbeat-v1"
6385
+ build: "socket-driven-v1"
5331
6386
  });
5332
6387
  const server = buildServer();
5333
6388
  const transport = new StdioServerTransport();
@@ -5339,6 +6394,28 @@ async function startMcpServer() {
5339
6394
  logFile: LOG_FILE_PATH,
5340
6395
  pid: process.pid
5341
6396
  });
6397
+ installShutdownHandlers("stdio");
6398
+ }
6399
+ function installShutdownHandlers(transport) {
6400
+ let shuttingDown = false;
6401
+ const shutdown = (reason) => {
6402
+ if (shuttingDown) return;
6403
+ shuttingDown = true;
6404
+ log(`mcp shutdown \u2014 ${reason}`, { transport });
6405
+ try {
6406
+ teardownAllRooms();
6407
+ } catch (err) {
6408
+ logError("teardownAllRooms failed", err);
6409
+ }
6410
+ setTimeout(() => process.exit(0), 200).unref?.();
6411
+ };
6412
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
6413
+ process.on("SIGINT", () => shutdown("SIGINT"));
6414
+ process.on("SIGHUP", () => shutdown("SIGHUP"));
6415
+ if (transport === "stdio") {
6416
+ process.stdin.on("end", () => shutdown("stdin-end"));
6417
+ process.stdin.on("close", () => shutdown("stdin-close"));
6418
+ }
5342
6419
  }
5343
6420
  async function startMcpHttpServer(opts) {
5344
6421
  installCrashHandlers();
@@ -5346,7 +6423,7 @@ async function startMcpHttpServer(opts) {
5346
6423
  port: opts.port,
5347
6424
  pid: process.pid,
5348
6425
  node: process.version,
5349
- build: "awareness-heartbeat-v1"
6426
+ build: "socket-driven-v1"
5350
6427
  });
5351
6428
  const server = buildServer();
5352
6429
  const transport = new StreamableHTTPServerTransport({
@@ -5392,18 +6469,40 @@ async function startMcpHttpServer(opts) {
5392
6469
  appBase=${APP_BASE}`
5393
6470
  );
5394
6471
  });
5395
- const shutdown = () => {
5396
- log("mcp http server shutting down");
6472
+ installShutdownHandlers("http");
6473
+ const closeServer = () => {
5397
6474
  httpServer.close(() => process.exit(0));
5398
6475
  setTimeout(() => process.exit(0), 500).unref();
5399
6476
  };
5400
- process.on("SIGTERM", shutdown);
5401
- process.on("SIGINT", shutdown);
6477
+ process.on("SIGTERM", closeServer);
6478
+ process.on("SIGINT", closeServer);
6479
+ process.on("SIGHUP", closeServer);
5402
6480
  }
6481
+ var __test_dispatch = dispatchTool;
6482
+ var __test_setRoom = (id, state) => {
6483
+ rooms.set(id, state);
6484
+ };
6485
+ var __test_clearRooms = () => {
6486
+ rooms.clear();
6487
+ };
5403
6488
 
5404
6489
  export {
5405
6490
  loadOrCreateIdentity,
5406
6491
  logError,
6492
+ teardownAllRooms,
6493
+ performAddComment,
6494
+ performReplyComment,
6495
+ performAddSuggestion,
6496
+ performReplySuggestion,
6497
+ performResolveThread,
6498
+ performAgentStatus,
6499
+ performDone,
6500
+ _registerRoomForTest,
6501
+ _clearRoomsForTest,
6502
+ dispatchTool,
5407
6503
  startMcpServer,
5408
- startMcpHttpServer
6504
+ startMcpHttpServer,
6505
+ __test_dispatch,
6506
+ __test_setRoom,
6507
+ __test_clearRooms
5409
6508
  };