@composer-app/mcp 0.0.4-beta.2 → 0.0.5

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.
@@ -33,6 +33,9 @@ function getActivityMap(doc) {
33
33
  function getActivityStateMap(doc) {
34
34
  return doc.getMap("activityState");
35
35
  }
36
+ function stateKey(activityId, userId) {
37
+ return `${activityId}:${userId}`;
38
+ }
36
39
  function emitActivity(doc, event, opts) {
37
40
  if (opts?.silent) return;
38
41
  const activity = getActivityMap(doc);
@@ -65,6 +68,20 @@ function pruneIfOverCap(doc) {
65
68
  });
66
69
  }
67
70
  }
71
+ function markActivityReadForThread(doc, threadId, userId) {
72
+ const activity = getActivityMap(doc);
73
+ const stateMap = getActivityStateMap(doc);
74
+ activity.forEach((value) => {
75
+ const event = value;
76
+ if (event.threadId === threadId) {
77
+ const key = stateKey(event.id, userId);
78
+ const existing = stateMap.get(key);
79
+ if (!existing?.read) {
80
+ stateMap.set(key, { read: true, dismissed: existing?.dismissed ?? false });
81
+ }
82
+ }
83
+ });
84
+ }
68
85
  function textPreview(text, maxLen = 80) {
69
86
  if (!text) return void 0;
70
87
  return text.length > maxLen ? text.slice(0, maxLen) + "\u2026" : text;
@@ -358,29 +375,1187 @@ var removeAwarenessStates = (awareness, clients, origin) => {
358
375
  /** @type {MetaClientState} */
359
376
  awareness.meta.get(clientID)
360
377
  );
361
- awareness.meta.set(clientID, {
362
- clock: curMeta.clock + 1,
363
- lastUpdated: getUnixTime()
364
- });
378
+ awareness.meta.set(clientID, {
379
+ clock: curMeta.clock + 1,
380
+ lastUpdated: getUnixTime()
381
+ });
382
+ }
383
+ removed.push(clientID);
384
+ }
385
+ }
386
+ if (removed.length > 0) {
387
+ awareness.emit("change", [{ added: [], updated: [], removed }, origin]);
388
+ awareness.emit("update", [{ added: [], updated: [], removed }, origin]);
389
+ }
390
+ };
391
+
392
+ // src/roomState.ts
393
+ import WebSocket from "ws";
394
+
395
+ // ../shared/src/editor-extensions.ts
396
+ import StarterKit from "@tiptap/starter-kit";
397
+ import { Code } from "@tiptap/extension-code";
398
+ import CodeBlock from "@tiptap/extension-code-block";
399
+
400
+ // ../node_modules/@tiptap/extension-blockquote/dist/index.js
401
+ import { mergeAttributes, Node, wrappingInputRule } from "@tiptap/core";
402
+ import { jsx } from "@tiptap/core/jsx-runtime";
403
+ var inputRegex = /^\s*>\s$/;
404
+ var Blockquote = Node.create({
405
+ name: "blockquote",
406
+ addOptions() {
407
+ return {
408
+ HTMLAttributes: {}
409
+ };
410
+ },
411
+ content: "block+",
412
+ group: "block",
413
+ defining: true,
414
+ parseHTML() {
415
+ return [{ tag: "blockquote" }];
416
+ },
417
+ renderHTML({ HTMLAttributes }) {
418
+ return /* @__PURE__ */ jsx("blockquote", { ...mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), children: /* @__PURE__ */ jsx("slot", {}) });
419
+ },
420
+ parseMarkdown: (token, helpers) => {
421
+ var _a;
422
+ const parseBlockChildren = (_a = helpers.parseBlockChildren) != null ? _a : helpers.parseChildren;
423
+ return helpers.createNode("blockquote", void 0, parseBlockChildren(token.tokens || []));
424
+ },
425
+ renderMarkdown: (node, h) => {
426
+ if (!node.content) {
427
+ return "";
428
+ }
429
+ const prefix = ">";
430
+ const result = [];
431
+ node.content.forEach((child, index) => {
432
+ var _a, _b;
433
+ const childContent = (_b = (_a = h.renderChild) == null ? void 0 : _a.call(h, child, index)) != null ? _b : h.renderChildren([child]);
434
+ const lines = childContent.split("\n");
435
+ const linesWithPrefix = lines.map((line) => {
436
+ if (line.trim() === "") {
437
+ return prefix;
438
+ }
439
+ return `${prefix} ${line}`;
440
+ });
441
+ result.push(linesWithPrefix.join("\n"));
442
+ });
443
+ return result.join(`
444
+ ${prefix}
445
+ `);
446
+ },
447
+ addCommands() {
448
+ return {
449
+ setBlockquote: () => ({ commands }) => {
450
+ return commands.wrapIn(this.name);
451
+ },
452
+ toggleBlockquote: () => ({ commands }) => {
453
+ return commands.toggleWrap(this.name);
454
+ },
455
+ unsetBlockquote: () => ({ commands }) => {
456
+ return commands.lift(this.name);
457
+ }
458
+ };
459
+ },
460
+ addKeyboardShortcuts() {
461
+ return {
462
+ "Mod-Shift-b": () => this.editor.commands.toggleBlockquote()
463
+ };
464
+ },
465
+ addInputRules() {
466
+ return [
467
+ wrappingInputRule({
468
+ find: inputRegex,
469
+ type: this.type
470
+ })
471
+ ];
472
+ }
473
+ });
474
+
475
+ // ../node_modules/@tiptap/extension-list/dist/index.js
476
+ import { mergeAttributes as mergeAttributes2, Node as Node2, wrappingInputRule as wrappingInputRule2 } from "@tiptap/core";
477
+ import { mergeAttributes as mergeAttributes22, Node as Node22, renderNestedMarkdownContent } from "@tiptap/core";
478
+ import { Extension } from "@tiptap/core";
479
+ import { getNodeType } from "@tiptap/core";
480
+ import { getNodeAtPosition } from "@tiptap/core";
481
+ import { isAtStartOfNode, isNodeActive } from "@tiptap/core";
482
+ import { getNodeType as getNodeType2 } from "@tiptap/core";
483
+ import { isAtEndOfNode, isNodeActive as isNodeActive2 } from "@tiptap/core";
484
+ import { Extension as Extension2 } from "@tiptap/core";
485
+ import { mergeAttributes as mergeAttributes3, Node as Node3, wrappingInputRule as wrappingInputRule22 } from "@tiptap/core";
486
+ import {
487
+ getRenderedAttributes,
488
+ mergeAttributes as mergeAttributes4,
489
+ Node as Node4,
490
+ renderNestedMarkdownContent as renderNestedMarkdownContent2,
491
+ wrappingInputRule as wrappingInputRule3
492
+ } from "@tiptap/core";
493
+ import { mergeAttributes as mergeAttributes5, Node as Node5, parseIndentedBlocks } from "@tiptap/core";
494
+ var __defProp = Object.defineProperty;
495
+ var __export = (target, all) => {
496
+ for (var name in all)
497
+ __defProp(target, name, { get: all[name], enumerable: true });
498
+ };
499
+ var ListItemName = "listItem";
500
+ var TextStyleName = "textStyle";
501
+ var bulletListInputRegex = /^\s*([-+*])\s$/;
502
+ var BulletList = Node2.create({
503
+ name: "bulletList",
504
+ addOptions() {
505
+ return {
506
+ itemTypeName: "listItem",
507
+ HTMLAttributes: {},
508
+ keepMarks: false,
509
+ keepAttributes: false
510
+ };
511
+ },
512
+ group: "block list",
513
+ content() {
514
+ return `${this.options.itemTypeName}+`;
515
+ },
516
+ parseHTML() {
517
+ return [{ tag: "ul" }];
518
+ },
519
+ renderHTML({ HTMLAttributes }) {
520
+ return ["ul", mergeAttributes2(this.options.HTMLAttributes, HTMLAttributes), 0];
521
+ },
522
+ markdownTokenName: "list",
523
+ parseMarkdown: (token, helpers) => {
524
+ if (token.type !== "list" || token.ordered) {
525
+ return [];
526
+ }
527
+ return {
528
+ type: "bulletList",
529
+ content: token.items ? helpers.parseChildren(token.items) : []
530
+ };
531
+ },
532
+ renderMarkdown: (node, h) => {
533
+ if (!node.content) {
534
+ return "";
535
+ }
536
+ return h.renderChildren(node.content, "\n");
537
+ },
538
+ markdownOptions: {
539
+ indentsContent: true
540
+ },
541
+ addCommands() {
542
+ return {
543
+ toggleBulletList: () => ({ commands, chain }) => {
544
+ if (this.options.keepAttributes) {
545
+ return chain().toggleList(this.name, this.options.itemTypeName, this.options.keepMarks).updateAttributes(ListItemName, this.editor.getAttributes(TextStyleName)).run();
546
+ }
547
+ return commands.toggleList(this.name, this.options.itemTypeName, this.options.keepMarks);
548
+ }
549
+ };
550
+ },
551
+ addKeyboardShortcuts() {
552
+ return {
553
+ "Mod-Shift-8": () => this.editor.commands.toggleBulletList()
554
+ };
555
+ },
556
+ addInputRules() {
557
+ let inputRule = wrappingInputRule2({
558
+ find: bulletListInputRegex,
559
+ type: this.type
560
+ });
561
+ if (this.options.keepMarks || this.options.keepAttributes) {
562
+ inputRule = wrappingInputRule2({
563
+ find: bulletListInputRegex,
564
+ type: this.type,
565
+ keepMarks: this.options.keepMarks,
566
+ keepAttributes: this.options.keepAttributes,
567
+ getAttributes: () => {
568
+ return this.editor.getAttributes(TextStyleName);
569
+ },
570
+ editor: this.editor
571
+ });
572
+ }
573
+ return [inputRule];
574
+ }
575
+ });
576
+ var ListItem = Node22.create({
577
+ name: "listItem",
578
+ addOptions() {
579
+ return {
580
+ HTMLAttributes: {},
581
+ bulletListTypeName: "bulletList",
582
+ orderedListTypeName: "orderedList"
583
+ };
584
+ },
585
+ content: "paragraph block*",
586
+ defining: true,
587
+ parseHTML() {
588
+ return [
589
+ {
590
+ tag: "li"
591
+ }
592
+ ];
593
+ },
594
+ renderHTML({ HTMLAttributes }) {
595
+ return ["li", mergeAttributes22(this.options.HTMLAttributes, HTMLAttributes), 0];
596
+ },
597
+ markdownTokenName: "list_item",
598
+ parseMarkdown: (token, helpers) => {
599
+ var _a;
600
+ if (token.type !== "list_item") {
601
+ return [];
602
+ }
603
+ const parseBlockChildren = (_a = helpers.parseBlockChildren) != null ? _a : helpers.parseChildren;
604
+ let content = [];
605
+ if (token.tokens && token.tokens.length > 0) {
606
+ const hasParagraphTokens = token.tokens.some((t) => t.type === "paragraph");
607
+ if (hasParagraphTokens) {
608
+ content = parseBlockChildren(token.tokens);
609
+ } else {
610
+ const firstToken = token.tokens[0];
611
+ if (firstToken && firstToken.type === "text" && firstToken.tokens && firstToken.tokens.length > 0) {
612
+ const inlineContent = helpers.parseInline(firstToken.tokens);
613
+ content = [
614
+ {
615
+ type: "paragraph",
616
+ content: inlineContent
617
+ }
618
+ ];
619
+ if (token.tokens.length > 1) {
620
+ const remainingTokens = token.tokens.slice(1);
621
+ const additionalContent = parseBlockChildren(remainingTokens);
622
+ content.push(...additionalContent);
623
+ }
624
+ } else {
625
+ content = parseBlockChildren(token.tokens);
626
+ }
627
+ }
628
+ }
629
+ if (content.length === 0) {
630
+ content = [
631
+ {
632
+ type: "paragraph",
633
+ content: []
634
+ }
635
+ ];
636
+ }
637
+ return {
638
+ type: "listItem",
639
+ content
640
+ };
641
+ },
642
+ renderMarkdown: (node, h, ctx) => {
643
+ return renderNestedMarkdownContent(
644
+ node,
645
+ h,
646
+ (context) => {
647
+ var _a, _b;
648
+ if (context.parentType === "bulletList") {
649
+ return "- ";
650
+ }
651
+ if (context.parentType === "orderedList") {
652
+ const start = ((_b = (_a = context.meta) == null ? void 0 : _a.parentAttrs) == null ? void 0 : _b.start) || 1;
653
+ return `${start + context.index}. `;
654
+ }
655
+ return "- ";
656
+ },
657
+ ctx
658
+ );
659
+ },
660
+ addKeyboardShortcuts() {
661
+ return {
662
+ Enter: () => this.editor.commands.splitListItem(this.name),
663
+ Tab: () => this.editor.commands.sinkListItem(this.name),
664
+ "Shift-Tab": () => this.editor.commands.liftListItem(this.name)
665
+ };
666
+ }
667
+ });
668
+ var listHelpers_exports = {};
669
+ __export(listHelpers_exports, {
670
+ findListItemPos: () => findListItemPos,
671
+ getNextListDepth: () => getNextListDepth,
672
+ handleBackspace: () => handleBackspace,
673
+ handleDelete: () => handleDelete,
674
+ hasListBefore: () => hasListBefore,
675
+ hasListItemAfter: () => hasListItemAfter,
676
+ hasListItemBefore: () => hasListItemBefore,
677
+ listItemHasSubList: () => listItemHasSubList,
678
+ nextListIsDeeper: () => nextListIsDeeper,
679
+ nextListIsHigher: () => nextListIsHigher
680
+ });
681
+ var findListItemPos = (typeOrName, state) => {
682
+ const { $from } = state.selection;
683
+ const nodeType = getNodeType(typeOrName, state.schema);
684
+ let currentNode = null;
685
+ let currentDepth = $from.depth;
686
+ let currentPos = $from.pos;
687
+ let targetDepth = null;
688
+ while (currentDepth > 0 && targetDepth === null) {
689
+ currentNode = $from.node(currentDepth);
690
+ if (currentNode.type === nodeType) {
691
+ targetDepth = currentDepth;
692
+ } else {
693
+ currentDepth -= 1;
694
+ currentPos -= 1;
695
+ }
696
+ }
697
+ if (targetDepth === null) {
698
+ return null;
699
+ }
700
+ return { $pos: state.doc.resolve(currentPos), depth: targetDepth };
701
+ };
702
+ var getNextListDepth = (typeOrName, state) => {
703
+ const listItemPos = findListItemPos(typeOrName, state);
704
+ if (!listItemPos) {
705
+ return false;
706
+ }
707
+ const [, depth] = getNodeAtPosition(state, typeOrName, listItemPos.$pos.pos + 4);
708
+ return depth;
709
+ };
710
+ var hasListBefore = (editorState, name, parentListTypes) => {
711
+ const { $anchor } = editorState.selection;
712
+ const previousNodePos = Math.max(0, $anchor.pos - 2);
713
+ const previousNode = editorState.doc.resolve(previousNodePos).node();
714
+ if (!previousNode || !parentListTypes.includes(previousNode.type.name)) {
715
+ return false;
716
+ }
717
+ return true;
718
+ };
719
+ var hasListItemBefore = (typeOrName, state) => {
720
+ var _a;
721
+ const { $anchor } = state.selection;
722
+ const $targetPos = state.doc.resolve($anchor.pos - 2);
723
+ if ($targetPos.index() === 0) {
724
+ return false;
725
+ }
726
+ if (((_a = $targetPos.nodeBefore) == null ? void 0 : _a.type.name) !== typeOrName) {
727
+ return false;
728
+ }
729
+ return true;
730
+ };
731
+ var listItemHasSubList = (typeOrName, state, node) => {
732
+ if (!node) {
733
+ return false;
734
+ }
735
+ const nodeType = getNodeType2(typeOrName, state.schema);
736
+ let hasSubList = false;
737
+ node.descendants((child) => {
738
+ if (child.type === nodeType) {
739
+ hasSubList = true;
740
+ }
741
+ });
742
+ return hasSubList;
743
+ };
744
+ var handleBackspace = (editor, name, parentListTypes) => {
745
+ if (editor.commands.undoInputRule()) {
746
+ return true;
747
+ }
748
+ if (editor.state.selection.from !== editor.state.selection.to) {
749
+ return false;
750
+ }
751
+ if (!isNodeActive(editor.state, name) && hasListBefore(editor.state, name, parentListTypes)) {
752
+ const { $anchor } = editor.state.selection;
753
+ const $listPos = editor.state.doc.resolve($anchor.before() - 1);
754
+ const listDescendants = [];
755
+ $listPos.node().descendants((node, pos) => {
756
+ if (node.type.name === name) {
757
+ listDescendants.push({ node, pos });
758
+ }
759
+ });
760
+ const lastItem = listDescendants.at(-1);
761
+ if (!lastItem) {
762
+ return false;
763
+ }
764
+ const $lastItemPos = editor.state.doc.resolve($listPos.start() + lastItem.pos + 1);
765
+ return editor.chain().cut({ from: $anchor.start() - 1, to: $anchor.end() + 1 }, $lastItemPos.end()).joinForward().run();
766
+ }
767
+ if (!isNodeActive(editor.state, name)) {
768
+ return false;
769
+ }
770
+ if (!isAtStartOfNode(editor.state)) {
771
+ return false;
772
+ }
773
+ const listItemPos = findListItemPos(name, editor.state);
774
+ if (!listItemPos) {
775
+ return false;
776
+ }
777
+ const $prev = editor.state.doc.resolve(listItemPos.$pos.pos - 2);
778
+ const prevNode = $prev.node(listItemPos.depth);
779
+ const previousListItemHasSubList = listItemHasSubList(name, editor.state, prevNode);
780
+ if (hasListItemBefore(name, editor.state) && !previousListItemHasSubList) {
781
+ return editor.commands.joinItemBackward();
782
+ }
783
+ return editor.chain().liftListItem(name).run();
784
+ };
785
+ var nextListIsDeeper = (typeOrName, state) => {
786
+ const listDepth = getNextListDepth(typeOrName, state);
787
+ const listItemPos = findListItemPos(typeOrName, state);
788
+ if (!listItemPos || !listDepth) {
789
+ return false;
790
+ }
791
+ if (listDepth > listItemPos.depth) {
792
+ return true;
793
+ }
794
+ return false;
795
+ };
796
+ var nextListIsHigher = (typeOrName, state) => {
797
+ const listDepth = getNextListDepth(typeOrName, state);
798
+ const listItemPos = findListItemPos(typeOrName, state);
799
+ if (!listItemPos || !listDepth) {
800
+ return false;
801
+ }
802
+ if (listDepth < listItemPos.depth) {
803
+ return true;
804
+ }
805
+ return false;
806
+ };
807
+ var handleDelete = (editor, name) => {
808
+ if (!isNodeActive2(editor.state, name)) {
809
+ return false;
810
+ }
811
+ if (!isAtEndOfNode(editor.state, name)) {
812
+ return false;
813
+ }
814
+ const { selection } = editor.state;
815
+ const { $from, $to } = selection;
816
+ if (!selection.empty && $from.sameParent($to)) {
817
+ return false;
818
+ }
819
+ if (nextListIsDeeper(name, editor.state)) {
820
+ return editor.chain().focus(editor.state.selection.from + 4).lift(name).joinBackward().run();
821
+ }
822
+ if (nextListIsHigher(name, editor.state)) {
823
+ return editor.chain().joinForward().joinBackward().run();
824
+ }
825
+ return editor.commands.joinItemForward();
826
+ };
827
+ var hasListItemAfter = (typeOrName, state) => {
828
+ var _a;
829
+ const { $anchor } = state.selection;
830
+ const $targetPos = state.doc.resolve($anchor.pos - $anchor.parentOffset - 2);
831
+ if ($targetPos.index() === $targetPos.parent.childCount - 1) {
832
+ return false;
833
+ }
834
+ if (((_a = $targetPos.nodeAfter) == null ? void 0 : _a.type.name) !== typeOrName) {
835
+ return false;
836
+ }
837
+ return true;
838
+ };
839
+ var ListKeymap = Extension.create({
840
+ name: "listKeymap",
841
+ addOptions() {
842
+ return {
843
+ listTypes: [
844
+ {
845
+ itemName: "listItem",
846
+ wrapperNames: ["bulletList", "orderedList"]
847
+ },
848
+ {
849
+ itemName: "taskItem",
850
+ wrapperNames: ["taskList"]
851
+ }
852
+ ]
853
+ };
854
+ },
855
+ addKeyboardShortcuts() {
856
+ return {
857
+ Delete: ({ editor }) => {
858
+ let handled = false;
859
+ this.options.listTypes.forEach(({ itemName }) => {
860
+ if (editor.state.schema.nodes[itemName] === void 0) {
861
+ return;
862
+ }
863
+ if (handleDelete(editor, itemName)) {
864
+ handled = true;
865
+ }
866
+ });
867
+ return handled;
868
+ },
869
+ "Mod-Delete": ({ editor }) => {
870
+ let handled = false;
871
+ this.options.listTypes.forEach(({ itemName }) => {
872
+ if (editor.state.schema.nodes[itemName] === void 0) {
873
+ return;
874
+ }
875
+ if (handleDelete(editor, itemName)) {
876
+ handled = true;
877
+ }
878
+ });
879
+ return handled;
880
+ },
881
+ Backspace: ({ editor }) => {
882
+ let handled = false;
883
+ this.options.listTypes.forEach(({ itemName, wrapperNames }) => {
884
+ if (editor.state.schema.nodes[itemName] === void 0) {
885
+ return;
886
+ }
887
+ if (handleBackspace(editor, itemName, wrapperNames)) {
888
+ handled = true;
889
+ }
890
+ });
891
+ return handled;
892
+ },
893
+ "Mod-Backspace": ({ editor }) => {
894
+ let handled = false;
895
+ this.options.listTypes.forEach(({ itemName, wrapperNames }) => {
896
+ if (editor.state.schema.nodes[itemName] === void 0) {
897
+ return;
898
+ }
899
+ if (handleBackspace(editor, itemName, wrapperNames)) {
900
+ handled = true;
901
+ }
902
+ });
903
+ return handled;
904
+ }
905
+ };
906
+ }
907
+ });
908
+ var ORDERED_LIST_ITEM_REGEX = /^(\s*)(\d+)\.\s+(.*)$/;
909
+ var INDENTED_LINE_REGEX = /^\s/;
910
+ function isBlockContentLine(line) {
911
+ const trimmedLine = line.trimStart();
912
+ return /^[-+*]\s+/.test(trimmedLine) || /^\d+\.\s+/.test(trimmedLine) || /^>\s?/.test(trimmedLine) || /^```/.test(trimmedLine) || /^~~~/.test(trimmedLine);
913
+ }
914
+ function splitItemContent(contentLines) {
915
+ const paragraphLines = [];
916
+ const blockLines = [];
917
+ let reachedBlockBoundary = false;
918
+ contentLines.forEach((line) => {
919
+ if (reachedBlockBoundary) {
920
+ blockLines.push(line);
921
+ return;
922
+ }
923
+ if (line.trim() === "") {
924
+ reachedBlockBoundary = true;
925
+ blockLines.push(line);
926
+ return;
927
+ }
928
+ if (paragraphLines.length > 0 && isBlockContentLine(line)) {
929
+ reachedBlockBoundary = true;
930
+ blockLines.push(line);
931
+ return;
932
+ }
933
+ paragraphLines.push(line);
934
+ });
935
+ return {
936
+ paragraphLines,
937
+ blockLines
938
+ };
939
+ }
940
+ function collectOrderedListItems(lines) {
941
+ const listItems = [];
942
+ let currentLineIndex = 0;
943
+ let consumed = 0;
944
+ while (currentLineIndex < lines.length) {
945
+ const line = lines[currentLineIndex];
946
+ const match = line.match(ORDERED_LIST_ITEM_REGEX);
947
+ if (!match) {
948
+ break;
949
+ }
950
+ const [, indent, number, content] = match;
951
+ const indentLevel = indent.length;
952
+ const itemContentLines = [content];
953
+ let nextLineIndex = currentLineIndex + 1;
954
+ const itemLines = [line];
955
+ let sawBlankLine = false;
956
+ while (nextLineIndex < lines.length) {
957
+ const nextLine = lines[nextLineIndex];
958
+ const nextMatch = nextLine.match(ORDERED_LIST_ITEM_REGEX);
959
+ if (nextMatch) {
960
+ break;
961
+ }
962
+ if (nextLine.trim() === "") {
963
+ itemLines.push(nextLine);
964
+ itemContentLines.push("");
965
+ sawBlankLine = true;
966
+ nextLineIndex += 1;
967
+ } else if (nextLine.match(INDENTED_LINE_REGEX)) {
968
+ itemLines.push(nextLine);
969
+ itemContentLines.push(nextLine.slice(indentLevel + 2));
970
+ nextLineIndex += 1;
971
+ } else {
972
+ if (sawBlankLine) {
973
+ break;
974
+ }
975
+ itemLines.push(nextLine);
976
+ itemContentLines.push(nextLine);
977
+ nextLineIndex += 1;
978
+ }
979
+ }
980
+ listItems.push({
981
+ indent: indentLevel,
982
+ number: parseInt(number, 10),
983
+ content: itemContentLines.join("\n").trim(),
984
+ contentLines: itemContentLines,
985
+ raw: itemLines.join("\n")
986
+ });
987
+ consumed = nextLineIndex;
988
+ currentLineIndex = nextLineIndex;
989
+ }
990
+ return [listItems, consumed];
991
+ }
992
+ function buildNestedStructure(items, baseIndent, lexer) {
993
+ const result = [];
994
+ let currentIndex = 0;
995
+ while (currentIndex < items.length) {
996
+ const item = items[currentIndex];
997
+ if (item.indent === baseIndent) {
998
+ const { paragraphLines, blockLines } = splitItemContent(item.contentLines);
999
+ const mainText = paragraphLines.join("\n").trim();
1000
+ const tokens = [];
1001
+ if (mainText) {
1002
+ tokens.push({
1003
+ type: "paragraph",
1004
+ raw: mainText,
1005
+ tokens: lexer.inlineTokens(mainText)
1006
+ });
1007
+ }
1008
+ const additionalContent = blockLines.join("\n").trim();
1009
+ if (additionalContent) {
1010
+ const blockTokens = lexer.blockTokens(additionalContent);
1011
+ tokens.push(...blockTokens);
1012
+ }
1013
+ let lookAheadIndex = currentIndex + 1;
1014
+ const nestedItems = [];
1015
+ while (lookAheadIndex < items.length && items[lookAheadIndex].indent > baseIndent) {
1016
+ nestedItems.push(items[lookAheadIndex]);
1017
+ lookAheadIndex += 1;
1018
+ }
1019
+ if (nestedItems.length > 0) {
1020
+ const nextIndent = Math.min(...nestedItems.map((nestedItem) => nestedItem.indent));
1021
+ const nestedListItems = buildNestedStructure(nestedItems, nextIndent, lexer);
1022
+ tokens.push({
1023
+ type: "list",
1024
+ ordered: true,
1025
+ start: nestedItems[0].number,
1026
+ items: nestedListItems,
1027
+ raw: nestedItems.map((nestedItem) => nestedItem.raw).join("\n")
1028
+ });
1029
+ }
1030
+ result.push({
1031
+ type: "list_item",
1032
+ raw: item.raw,
1033
+ tokens
1034
+ });
1035
+ currentIndex = lookAheadIndex;
1036
+ } else {
1037
+ currentIndex += 1;
1038
+ }
1039
+ }
1040
+ return result;
1041
+ }
1042
+ function parseListItems(items, helpers) {
1043
+ return items.map((item) => {
1044
+ if (item.type !== "list_item") {
1045
+ return helpers.parseChildren([item])[0];
1046
+ }
1047
+ const content = [];
1048
+ if (item.tokens && item.tokens.length > 0) {
1049
+ item.tokens.forEach((itemToken) => {
1050
+ if (itemToken.type === "paragraph" || itemToken.type === "list" || itemToken.type === "blockquote" || itemToken.type === "code") {
1051
+ content.push(...helpers.parseChildren([itemToken]));
1052
+ } else if (itemToken.type === "text" && itemToken.tokens) {
1053
+ const inlineContent = helpers.parseChildren([itemToken]);
1054
+ content.push({
1055
+ type: "paragraph",
1056
+ content: inlineContent
1057
+ });
1058
+ } else {
1059
+ const parsed = helpers.parseChildren([itemToken]);
1060
+ if (parsed.length > 0) {
1061
+ content.push(...parsed);
1062
+ }
1063
+ }
1064
+ });
1065
+ }
1066
+ return {
1067
+ type: "listItem",
1068
+ content
1069
+ };
1070
+ });
1071
+ }
1072
+ var ListItemName2 = "listItem";
1073
+ var TextStyleName2 = "textStyle";
1074
+ var orderedListInputRegex = /^(\d+)\.\s$/;
1075
+ var OrderedList = Node3.create({
1076
+ name: "orderedList",
1077
+ addOptions() {
1078
+ return {
1079
+ itemTypeName: "listItem",
1080
+ HTMLAttributes: {},
1081
+ keepMarks: false,
1082
+ keepAttributes: false
1083
+ };
1084
+ },
1085
+ group: "block list",
1086
+ content() {
1087
+ return `${this.options.itemTypeName}+`;
1088
+ },
1089
+ addAttributes() {
1090
+ return {
1091
+ start: {
1092
+ default: 1,
1093
+ parseHTML: (element) => {
1094
+ return element.hasAttribute("start") ? parseInt(element.getAttribute("start") || "", 10) : 1;
1095
+ }
1096
+ },
1097
+ type: {
1098
+ default: null,
1099
+ parseHTML: (element) => element.getAttribute("type")
1100
+ }
1101
+ };
1102
+ },
1103
+ parseHTML() {
1104
+ return [
1105
+ {
1106
+ tag: "ol"
1107
+ }
1108
+ ];
1109
+ },
1110
+ renderHTML({ HTMLAttributes }) {
1111
+ const { start, ...attributesWithoutStart } = HTMLAttributes;
1112
+ return start === 1 ? ["ol", mergeAttributes3(this.options.HTMLAttributes, attributesWithoutStart), 0] : ["ol", mergeAttributes3(this.options.HTMLAttributes, HTMLAttributes), 0];
1113
+ },
1114
+ markdownTokenName: "list",
1115
+ parseMarkdown: (token, helpers) => {
1116
+ if (token.type !== "list" || !token.ordered) {
1117
+ return [];
1118
+ }
1119
+ const startValue = token.start || 1;
1120
+ const content = token.items ? parseListItems(token.items, helpers) : [];
1121
+ if (startValue !== 1) {
1122
+ return {
1123
+ type: "orderedList",
1124
+ attrs: { start: startValue },
1125
+ content
1126
+ };
1127
+ }
1128
+ return {
1129
+ type: "orderedList",
1130
+ content
1131
+ };
1132
+ },
1133
+ renderMarkdown: (node, h) => {
1134
+ if (!node.content) {
1135
+ return "";
1136
+ }
1137
+ return h.renderChildren(node.content, "\n");
1138
+ },
1139
+ markdownTokenizer: {
1140
+ name: "orderedList",
1141
+ level: "block",
1142
+ start: (src) => {
1143
+ const match = src.match(/^(\s*)(\d+)\.\s+/);
1144
+ const index = match == null ? void 0 : match.index;
1145
+ return index !== void 0 ? index : -1;
1146
+ },
1147
+ tokenize: (src, _tokens, lexer) => {
1148
+ var _a;
1149
+ const lines = src.split("\n");
1150
+ const [listItems, consumed] = collectOrderedListItems(lines);
1151
+ if (listItems.length === 0) {
1152
+ return void 0;
1153
+ }
1154
+ const items = buildNestedStructure(listItems, 0, lexer);
1155
+ if (items.length === 0) {
1156
+ return void 0;
1157
+ }
1158
+ const startValue = ((_a = listItems[0]) == null ? void 0 : _a.number) || 1;
1159
+ return {
1160
+ type: "list",
1161
+ ordered: true,
1162
+ start: startValue,
1163
+ items,
1164
+ raw: lines.slice(0, consumed).join("\n")
1165
+ };
1166
+ }
1167
+ },
1168
+ markdownOptions: {
1169
+ indentsContent: true
1170
+ },
1171
+ addCommands() {
1172
+ return {
1173
+ toggleOrderedList: () => ({ commands, chain }) => {
1174
+ if (this.options.keepAttributes) {
1175
+ return chain().toggleList(this.name, this.options.itemTypeName, this.options.keepMarks).updateAttributes(ListItemName2, this.editor.getAttributes(TextStyleName2)).run();
1176
+ }
1177
+ return commands.toggleList(this.name, this.options.itemTypeName, this.options.keepMarks);
1178
+ }
1179
+ };
1180
+ },
1181
+ addKeyboardShortcuts() {
1182
+ return {
1183
+ "Mod-Shift-7": () => this.editor.commands.toggleOrderedList()
1184
+ };
1185
+ },
1186
+ addInputRules() {
1187
+ let inputRule = wrappingInputRule22({
1188
+ find: orderedListInputRegex,
1189
+ type: this.type,
1190
+ getAttributes: (match) => ({ start: +match[1] }),
1191
+ joinPredicate: (match, node) => node.childCount + node.attrs.start === +match[1]
1192
+ });
1193
+ if (this.options.keepMarks || this.options.keepAttributes) {
1194
+ inputRule = wrappingInputRule22({
1195
+ find: orderedListInputRegex,
1196
+ type: this.type,
1197
+ keepMarks: this.options.keepMarks,
1198
+ keepAttributes: this.options.keepAttributes,
1199
+ getAttributes: (match) => ({ start: +match[1], ...this.editor.getAttributes(TextStyleName2) }),
1200
+ joinPredicate: (match, node) => node.childCount + node.attrs.start === +match[1],
1201
+ editor: this.editor
1202
+ });
1203
+ }
1204
+ return [inputRule];
1205
+ }
1206
+ });
1207
+ var inputRegex2 = /^\s*(\[([( |x])?\])\s$/;
1208
+ var TaskItem = Node4.create({
1209
+ name: "taskItem",
1210
+ addOptions() {
1211
+ return {
1212
+ nested: false,
1213
+ HTMLAttributes: {},
1214
+ taskListTypeName: "taskList",
1215
+ a11y: void 0
1216
+ };
1217
+ },
1218
+ content() {
1219
+ return this.options.nested ? "paragraph block*" : "paragraph+";
1220
+ },
1221
+ defining: true,
1222
+ addAttributes() {
1223
+ return {
1224
+ checked: {
1225
+ default: false,
1226
+ keepOnSplit: false,
1227
+ parseHTML: (element) => {
1228
+ const dataChecked = element.getAttribute("data-checked");
1229
+ return dataChecked === "" || dataChecked === "true";
1230
+ },
1231
+ renderHTML: (attributes) => ({
1232
+ "data-checked": attributes.checked
1233
+ })
1234
+ }
1235
+ };
1236
+ },
1237
+ parseHTML() {
1238
+ return [
1239
+ {
1240
+ tag: `li[data-type="${this.name}"]`,
1241
+ priority: 51
1242
+ }
1243
+ ];
1244
+ },
1245
+ renderHTML({ node, HTMLAttributes }) {
1246
+ return [
1247
+ "li",
1248
+ mergeAttributes4(this.options.HTMLAttributes, HTMLAttributes, {
1249
+ "data-type": this.name
1250
+ }),
1251
+ [
1252
+ "label",
1253
+ [
1254
+ "input",
1255
+ {
1256
+ type: "checkbox",
1257
+ checked: node.attrs.checked ? "checked" : null
1258
+ }
1259
+ ],
1260
+ ["span"]
1261
+ ],
1262
+ ["div", 0]
1263
+ ];
1264
+ },
1265
+ parseMarkdown: (token, h) => {
1266
+ const content = [];
1267
+ if (token.tokens && token.tokens.length > 0) {
1268
+ content.push(h.createNode("paragraph", {}, h.parseInline(token.tokens)));
1269
+ } else if (token.text) {
1270
+ content.push(h.createNode("paragraph", {}, [h.createNode("text", { text: token.text })]));
1271
+ } else {
1272
+ content.push(h.createNode("paragraph", {}, []));
1273
+ }
1274
+ if (token.nestedTokens && token.nestedTokens.length > 0) {
1275
+ const nestedContent = h.parseChildren(token.nestedTokens);
1276
+ content.push(...nestedContent);
1277
+ }
1278
+ return h.createNode("taskItem", { checked: token.checked || false }, content);
1279
+ },
1280
+ renderMarkdown: (node, h) => {
1281
+ var _a;
1282
+ const checkedChar = ((_a = node.attrs) == null ? void 0 : _a.checked) ? "x" : " ";
1283
+ const prefix = `- [${checkedChar}] `;
1284
+ return renderNestedMarkdownContent2(node, h, prefix);
1285
+ },
1286
+ addKeyboardShortcuts() {
1287
+ const shortcuts = {
1288
+ Enter: () => this.editor.commands.splitListItem(this.name),
1289
+ "Shift-Tab": () => this.editor.commands.liftListItem(this.name)
1290
+ };
1291
+ if (!this.options.nested) {
1292
+ return shortcuts;
1293
+ }
1294
+ return {
1295
+ ...shortcuts,
1296
+ Tab: () => this.editor.commands.sinkListItem(this.name)
1297
+ };
1298
+ },
1299
+ addNodeView() {
1300
+ return ({ node, HTMLAttributes, getPos, editor }) => {
1301
+ const listItem = document.createElement("li");
1302
+ const checkboxWrapper = document.createElement("label");
1303
+ const checkboxStyler = document.createElement("span");
1304
+ const checkbox = document.createElement("input");
1305
+ const content = document.createElement("div");
1306
+ const updateA11Y = (currentNode) => {
1307
+ var _a, _b;
1308
+ checkbox.ariaLabel = ((_b = (_a = this.options.a11y) == null ? void 0 : _a.checkboxLabel) == null ? void 0 : _b.call(_a, currentNode, checkbox.checked)) || `Task item checkbox for ${currentNode.textContent || "empty task item"}`;
1309
+ };
1310
+ updateA11Y(node);
1311
+ checkboxWrapper.contentEditable = "false";
1312
+ checkbox.type = "checkbox";
1313
+ checkbox.addEventListener("mousedown", (event) => event.preventDefault());
1314
+ checkbox.addEventListener("change", (event) => {
1315
+ if (!editor.isEditable && !this.options.onReadOnlyChecked) {
1316
+ checkbox.checked = !checkbox.checked;
1317
+ return;
1318
+ }
1319
+ const { checked } = event.target;
1320
+ if (editor.isEditable && typeof getPos === "function") {
1321
+ editor.chain().focus(void 0, { scrollIntoView: false }).command(({ tr }) => {
1322
+ const position = getPos();
1323
+ if (typeof position !== "number") {
1324
+ return false;
1325
+ }
1326
+ const currentNode = tr.doc.nodeAt(position);
1327
+ tr.setNodeMarkup(position, void 0, {
1328
+ ...currentNode == null ? void 0 : currentNode.attrs,
1329
+ checked
1330
+ });
1331
+ return true;
1332
+ }).run();
1333
+ }
1334
+ if (!editor.isEditable && this.options.onReadOnlyChecked) {
1335
+ if (!this.options.onReadOnlyChecked(node, checked)) {
1336
+ checkbox.checked = !checkbox.checked;
1337
+ }
1338
+ }
1339
+ });
1340
+ Object.entries(this.options.HTMLAttributes).forEach(([key, value]) => {
1341
+ listItem.setAttribute(key, value);
1342
+ });
1343
+ listItem.dataset.checked = node.attrs.checked;
1344
+ checkbox.checked = node.attrs.checked;
1345
+ checkboxWrapper.append(checkbox, checkboxStyler);
1346
+ listItem.append(checkboxWrapper, content);
1347
+ Object.entries(HTMLAttributes).forEach(([key, value]) => {
1348
+ listItem.setAttribute(key, value);
1349
+ });
1350
+ let prevRenderedAttributeKeys = new Set(Object.keys(HTMLAttributes));
1351
+ return {
1352
+ dom: listItem,
1353
+ contentDOM: content,
1354
+ update: (updatedNode) => {
1355
+ if (updatedNode.type !== this.type) {
1356
+ return false;
1357
+ }
1358
+ listItem.dataset.checked = updatedNode.attrs.checked;
1359
+ checkbox.checked = updatedNode.attrs.checked;
1360
+ updateA11Y(updatedNode);
1361
+ const extensionAttributes = editor.extensionManager.attributes;
1362
+ const newHTMLAttributes = getRenderedAttributes(updatedNode, extensionAttributes);
1363
+ const newKeys = new Set(Object.keys(newHTMLAttributes));
1364
+ const staticAttrs = this.options.HTMLAttributes;
1365
+ prevRenderedAttributeKeys.forEach((key) => {
1366
+ if (!newKeys.has(key)) {
1367
+ if (key in staticAttrs) {
1368
+ listItem.setAttribute(key, staticAttrs[key]);
1369
+ } else {
1370
+ listItem.removeAttribute(key);
1371
+ }
1372
+ }
1373
+ });
1374
+ Object.entries(newHTMLAttributes).forEach(([key, value]) => {
1375
+ if (value === null || value === void 0) {
1376
+ if (key in staticAttrs) {
1377
+ listItem.setAttribute(key, staticAttrs[key]);
1378
+ } else {
1379
+ listItem.removeAttribute(key);
1380
+ }
1381
+ } else {
1382
+ listItem.setAttribute(key, value);
1383
+ }
1384
+ });
1385
+ prevRenderedAttributeKeys = newKeys;
1386
+ return true;
1387
+ }
1388
+ };
1389
+ };
1390
+ },
1391
+ addInputRules() {
1392
+ return [
1393
+ wrappingInputRule3({
1394
+ find: inputRegex2,
1395
+ type: this.type,
1396
+ getAttributes: (match) => ({
1397
+ checked: match[match.length - 1] === "x"
1398
+ })
1399
+ })
1400
+ ];
1401
+ }
1402
+ });
1403
+ var TaskList = Node5.create({
1404
+ name: "taskList",
1405
+ addOptions() {
1406
+ return {
1407
+ itemTypeName: "taskItem",
1408
+ HTMLAttributes: {}
1409
+ };
1410
+ },
1411
+ group: "block list",
1412
+ content() {
1413
+ return `${this.options.itemTypeName}+`;
1414
+ },
1415
+ parseHTML() {
1416
+ return [
1417
+ {
1418
+ tag: `ul[data-type="${this.name}"]`,
1419
+ priority: 51
1420
+ }
1421
+ ];
1422
+ },
1423
+ renderHTML({ HTMLAttributes }) {
1424
+ return ["ul", mergeAttributes5(this.options.HTMLAttributes, HTMLAttributes, { "data-type": this.name }), 0];
1425
+ },
1426
+ parseMarkdown: (token, h) => {
1427
+ return h.createNode("taskList", {}, h.parseChildren(token.items || []));
1428
+ },
1429
+ renderMarkdown: (node, h) => {
1430
+ if (!node.content) {
1431
+ return "";
1432
+ }
1433
+ return h.renderChildren(node.content, "\n");
1434
+ },
1435
+ markdownTokenizer: {
1436
+ name: "taskList",
1437
+ level: "block",
1438
+ start(src) {
1439
+ var _a;
1440
+ const index = (_a = src.match(/^\s*[-+*]\s+\[([ xX])\]\s+/)) == null ? void 0 : _a.index;
1441
+ return index !== void 0 ? index : -1;
1442
+ },
1443
+ tokenize(src, tokens, lexer) {
1444
+ const parseTaskListContent = (content) => {
1445
+ const nestedResult = parseIndentedBlocks(
1446
+ content,
1447
+ {
1448
+ itemPattern: /^(\s*)([-+*])\s+\[([ xX])\]\s+(.*)$/,
1449
+ extractItemData: (match) => ({
1450
+ indentLevel: match[1].length,
1451
+ mainContent: match[4],
1452
+ checked: match[3].toLowerCase() === "x"
1453
+ }),
1454
+ createToken: (data, nestedTokens) => ({
1455
+ type: "taskItem",
1456
+ raw: "",
1457
+ mainContent: data.mainContent,
1458
+ indentLevel: data.indentLevel,
1459
+ checked: data.checked,
1460
+ text: data.mainContent,
1461
+ tokens: lexer.inlineTokens(data.mainContent),
1462
+ nestedTokens
1463
+ }),
1464
+ // Allow recursive nesting
1465
+ customNestedParser: parseTaskListContent
1466
+ },
1467
+ lexer
1468
+ );
1469
+ if (nestedResult) {
1470
+ return [
1471
+ {
1472
+ type: "taskList",
1473
+ raw: nestedResult.raw,
1474
+ items: nestedResult.items
1475
+ }
1476
+ ];
1477
+ }
1478
+ return lexer.blockTokens(content);
1479
+ };
1480
+ const result = parseIndentedBlocks(
1481
+ src,
1482
+ {
1483
+ itemPattern: /^(\s*)([-+*])\s+\[([ xX])\]\s+(.*)$/,
1484
+ extractItemData: (match) => ({
1485
+ indentLevel: match[1].length,
1486
+ mainContent: match[4],
1487
+ checked: match[3].toLowerCase() === "x"
1488
+ }),
1489
+ createToken: (data, nestedTokens) => ({
1490
+ type: "taskItem",
1491
+ raw: "",
1492
+ mainContent: data.mainContent,
1493
+ indentLevel: data.indentLevel,
1494
+ checked: data.checked,
1495
+ text: data.mainContent,
1496
+ tokens: lexer.inlineTokens(data.mainContent),
1497
+ nestedTokens
1498
+ }),
1499
+ // Use the recursive parser for nested content
1500
+ customNestedParser: parseTaskListContent
1501
+ },
1502
+ lexer
1503
+ );
1504
+ if (!result) {
1505
+ return void 0;
365
1506
  }
366
- removed.push(clientID);
1507
+ return {
1508
+ type: "taskList",
1509
+ raw: result.raw,
1510
+ items: result.items
1511
+ };
367
1512
  }
1513
+ },
1514
+ markdownOptions: {
1515
+ indentsContent: true
1516
+ },
1517
+ addCommands() {
1518
+ return {
1519
+ toggleTaskList: () => ({ commands }) => {
1520
+ return commands.toggleList(this.name, this.options.itemTypeName);
1521
+ }
1522
+ };
1523
+ },
1524
+ addKeyboardShortcuts() {
1525
+ return {
1526
+ "Mod-Shift-9": () => this.editor.commands.toggleTaskList()
1527
+ };
368
1528
  }
369
- if (removed.length > 0) {
370
- awareness.emit("change", [{ added: [], updated: [], removed }, origin]);
371
- awareness.emit("update", [{ added: [], updated: [], removed }, origin]);
1529
+ });
1530
+ var ListKit = Extension2.create({
1531
+ name: "listKit",
1532
+ addExtensions() {
1533
+ const extensions = [];
1534
+ if (this.options.bulletList !== false) {
1535
+ extensions.push(BulletList.configure(this.options.bulletList));
1536
+ }
1537
+ if (this.options.listItem !== false) {
1538
+ extensions.push(ListItem.configure(this.options.listItem));
1539
+ }
1540
+ if (this.options.listKeymap !== false) {
1541
+ extensions.push(ListKeymap.configure(this.options.listKeymap));
1542
+ }
1543
+ if (this.options.orderedList !== false) {
1544
+ extensions.push(OrderedList.configure(this.options.orderedList));
1545
+ }
1546
+ if (this.options.taskItem !== false) {
1547
+ extensions.push(TaskItem.configure(this.options.taskItem));
1548
+ }
1549
+ if (this.options.taskList !== false) {
1550
+ extensions.push(TaskList.configure(this.options.taskList));
1551
+ }
1552
+ return extensions;
372
1553
  }
373
- };
374
-
375
- // src/roomState.ts
376
- import WebSocket from "ws";
1554
+ });
377
1555
 
378
1556
  // ../shared/src/editor-extensions.ts
379
- import StarterKit from "@tiptap/starter-kit";
380
- import { Code } from "@tiptap/extension-code";
381
- import CodeBlock from "@tiptap/extension-code-block";
382
- import TaskList from "@tiptap/extension-task-list";
383
- import TaskItem from "@tiptap/extension-task-item";
1557
+ import TaskList2 from "@tiptap/extension-task-list";
1558
+ import TaskItem2 from "@tiptap/extension-task-item";
384
1559
  import Highlight from "@tiptap/extension-highlight";
385
1560
  import Subscript from "@tiptap/extension-subscript";
386
1561
  import Superscript from "@tiptap/extension-superscript";
@@ -479,7 +1654,7 @@ var ImageBlockExtension = Image.extend({
479
1654
  var imageBlockExtensions_default = ImageBlockExtension;
480
1655
 
481
1656
  // ../shared/src/videoBlockNode.ts
482
- import { Node, mergeAttributes } from "@tiptap/core";
1657
+ import { Node as Node6, mergeAttributes as mergeAttributes6 } from "@tiptap/core";
483
1658
 
484
1659
  // ../shared/src/embedParser.ts
485
1660
  var YOUTUBE_HOSTS = /* @__PURE__ */ new Set([
@@ -581,7 +1756,7 @@ function detectVideoProvider(url) {
581
1756
  }
582
1757
  }
583
1758
  var IFRAME_SANDBOX = "allow-scripts allow-same-origin allow-presentation allow-popups";
584
- var VideoBlockNode = Node.create({
1759
+ var VideoBlockNode = Node6.create({
585
1760
  name: "videoBlock",
586
1761
  group: "block",
587
1762
  addOptions() {
@@ -696,7 +1871,7 @@ var VideoBlockNode = Node.create({
696
1871
  renderHTML({ node, HTMLAttributes }) {
697
1872
  const attrs = node.attrs;
698
1873
  if (attrs.src) {
699
- const merged = mergeAttributes(HTMLAttributes, {
1874
+ const merged = mergeAttributes6(HTMLAttributes, {
700
1875
  src: attrs.src,
701
1876
  controls: ""
702
1877
  });
@@ -707,7 +1882,7 @@ var VideoBlockNode = Node.create({
707
1882
  return ["video", merged];
708
1883
  }
709
1884
  if (attrs.embedUrl) {
710
- const merged = mergeAttributes(HTMLAttributes, {
1885
+ const merged = mergeAttributes6(HTMLAttributes, {
711
1886
  src: attrs.embedUrl,
712
1887
  sandbox: IFRAME_SANDBOX,
713
1888
  frameborder: "0",
@@ -720,7 +1895,7 @@ var VideoBlockNode = Node.create({
720
1895
  }
721
1896
  return ["iframe", merged];
722
1897
  }
723
- return ["div", mergeAttributes(HTMLAttributes, { "data-video-error": "missing-src" })];
1898
+ return ["div", mergeAttributes6(HTMLAttributes, { "data-video-error": "missing-src" })];
724
1899
  },
725
1900
  /**
726
1901
  * Link-first markdown serialization (Phase 2 Unit 3):
@@ -771,7 +1946,7 @@ function filenameFromUrl(url) {
771
1946
  var videoBlockNode_default = VideoBlockNode;
772
1947
 
773
1948
  // ../shared/src/mediaKeyboardNav.ts
774
- import { Extension } from "@tiptap/core";
1949
+ import { Extension as Extension3 } from "@tiptap/core";
775
1950
 
776
1951
  // ../node_modules/prosemirror-model/dist/index.js
777
1952
  function findDiffStart(a, b, pos) {
@@ -1805,7 +2980,7 @@ var NodeRange = class {
1805
2980
  }
1806
2981
  };
1807
2982
  var emptyAttrs = /* @__PURE__ */ Object.create(null);
1808
- var Node2 = class _Node {
2983
+ var Node7 = class _Node {
1809
2984
  /**
1810
2985
  @internal
1811
2986
  */
@@ -2206,7 +3381,7 @@ var Node2 = class _Node {
2206
3381
  return node;
2207
3382
  }
2208
3383
  };
2209
- Node2.prototype.text = void 0;
3384
+ Node7.prototype.text = void 0;
2210
3385
  function wrapMarks(marks, str) {
2211
3386
  for (let i = marks.length - 1; i >= 0; i--)
2212
3387
  str = marks[i].type.name + "(" + str + ")";
@@ -3846,7 +5021,7 @@ var MEDIA_NODE_TYPES = /* @__PURE__ */ new Set(["image", "videoBlock"]);
3846
5021
  function isMediaNodeSelection(sel) {
3847
5022
  return sel instanceof NodeSelection && MEDIA_NODE_TYPES.has(sel.node.type.name);
3848
5023
  }
3849
- var MediaKeyboardNav = Extension.create({
5024
+ var MediaKeyboardNav = Extension3.create({
3850
5025
  name: "mediaKeyboardNav",
3851
5026
  addProseMirrorPlugins() {
3852
5027
  return [
@@ -3890,8 +5065,251 @@ var MediaKeyboardNav = Extension.create({
3890
5065
  });
3891
5066
  var mediaKeyboardNav_default = MediaKeyboardNav;
3892
5067
 
5068
+ // ../shared/src/inlineIconNode.ts
5069
+ import { Node as Node8, mergeAttributes as mergeAttributes7 } from "@tiptap/core";
5070
+ var INLINE_ICON_ALLOWLIST = [
5071
+ "message-square-plus",
5072
+ "pen-line"
5073
+ ];
5074
+ function isInlineIconName(value) {
5075
+ return INLINE_ICON_ALLOWLIST.includes(value);
5076
+ }
5077
+ var INLINE_ICON_NODE_NAME = "inlineIcon";
5078
+ var InlineIconNode = Node8.create({
5079
+ name: INLINE_ICON_NODE_NAME,
5080
+ group: "inline",
5081
+ inline: true,
5082
+ atom: true,
5083
+ selectable: false,
5084
+ addAttributes() {
5085
+ return {
5086
+ name: {
5087
+ default: null,
5088
+ parseHTML: (el) => {
5089
+ const raw = el.getAttribute("data-icon");
5090
+ return raw && isInlineIconName(raw) ? raw : null;
5091
+ },
5092
+ renderHTML: (attrs) => attrs.name ? { "data-icon": attrs.name } : {}
5093
+ }
5094
+ };
5095
+ },
5096
+ parseHTML() {
5097
+ return [{ tag: "span[data-inline-icon]" }];
5098
+ },
5099
+ renderHTML({ HTMLAttributes }) {
5100
+ return [
5101
+ "span",
5102
+ mergeAttributes7(HTMLAttributes, {
5103
+ "data-inline-icon": "true",
5104
+ class: "inline-icon"
5105
+ })
5106
+ ];
5107
+ },
5108
+ renderMarkdown(node) {
5109
+ const name = node.attrs?.name;
5110
+ return name && isInlineIconName(name) ? `:icon[${name}]` : "";
5111
+ }
5112
+ });
5113
+
5114
+ // ../shared/src/markdown.ts
5115
+ import { getSchema } from "@tiptap/core";
5116
+ import { MarkdownManager } from "@tiptap/markdown";
5117
+ import {
5118
+ prosemirrorJSONToYXmlFragment,
5119
+ yXmlFragmentToProsemirrorJSON
5120
+ } from "@tiptap/y-tiptap";
5121
+
5122
+ // ../shared/src/frontmatter.ts
5123
+ function extractFrontmatter(text) {
5124
+ const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
5125
+ if (!match) return null;
5126
+ return { yaml: match[1].trim(), body: match[2] };
5127
+ }
5128
+
5129
+ // ../shared/src/markdown.ts
5130
+ var EMPTY_LIST_ITEM_SENTINEL = "<!-- composer-empty-list-item -->";
5131
+ function markdownToDocJSON(markdown, extensions = editorExtensions) {
5132
+ const manager = new MarkdownManager({ extensions });
5133
+ const parsed = manager.parse(markdown);
5134
+ const doc = parsed && parsed.type === "doc" ? parsed : { type: "doc", content: parsed?.content ?? [] };
5135
+ repairListItemSchema(doc);
5136
+ return doc;
5137
+ }
5138
+ function repairListItemSchema(node) {
5139
+ if (node.type === "listItem" || node.type === "taskItem") {
5140
+ if (!node.content || node.content.length === 0) {
5141
+ node.content = [{ type: "paragraph" }];
5142
+ return;
5143
+ }
5144
+ if (node.content[0]?.type !== "paragraph") {
5145
+ node.content = [{ type: "paragraph" }, ...node.content];
5146
+ }
5147
+ const first = node.content[0];
5148
+ if (first?.type === "paragraph" && paragraphIsEmptyListItemSentinel(first)) {
5149
+ node.content = [{ type: "paragraph" }, ...node.content.slice(1)];
5150
+ }
5151
+ }
5152
+ if (node.content) {
5153
+ for (const child of node.content) repairListItemSchema(child);
5154
+ }
5155
+ }
5156
+ function paragraphIsEmptyListItemSentinel(paragraph) {
5157
+ if (!paragraph.content || paragraph.content.length === 0) return false;
5158
+ let sawSentinel = false;
5159
+ for (const child of paragraph.content) {
5160
+ if (child.type === "paragraph" && (!child.content || child.content.length === 0)) {
5161
+ sawSentinel = true;
5162
+ continue;
5163
+ }
5164
+ if (child.type !== "text") return false;
5165
+ if (child.text === EMPTY_LIST_ITEM_SENTINEL) {
5166
+ sawSentinel = true;
5167
+ continue;
5168
+ }
5169
+ if ((child.text ?? "").trim() !== "") return false;
5170
+ }
5171
+ return sawSentinel;
5172
+ }
5173
+ function writeMarkdownToYFragment(fragment, markdown, extensions = editorExtensions) {
5174
+ const doc = fragment.doc;
5175
+ if (!doc) throw new Error("fragment must be attached to a Y.Doc");
5176
+ const fm = extractFrontmatter(markdown);
5177
+ const body = fm ? fm.body : markdown;
5178
+ const schema = getSchema(extensions);
5179
+ const bodyDoc = markdownToDocJSON(body, extensions);
5180
+ const children = [];
5181
+ if (fm) {
5182
+ children.push({
5183
+ type: "frontmatter",
5184
+ attrs: { language: "yaml" },
5185
+ content: fm.yaml ? [{ type: "text", text: fm.yaml }] : void 0
5186
+ });
5187
+ }
5188
+ if (bodyDoc.content) children.push(...bodyDoc.content);
5189
+ const fullDoc = { type: "doc", content: children };
5190
+ doc.transact(() => {
5191
+ prosemirrorJSONToYXmlFragment(schema, fullDoc, fragment);
5192
+ });
5193
+ }
5194
+ function markdownFromYFragment(fragment, extensions = editorExtensions) {
5195
+ const json = yXmlFragmentToProsemirrorJSON(fragment);
5196
+ if (!json || !json.content || json.content.length === 0) return "";
5197
+ const manager = new MarkdownManager({ extensions });
5198
+ return manager.serialize(json);
5199
+ }
5200
+
3893
5201
  // ../shared/src/editor-extensions.ts
3894
5202
  var CodeWithCombinableMarks = Code.extend({ excludes: "" });
5203
+ var markText = (node) => node.content?.[0]?.text ?? "";
5204
+ var SubscriptWithMarkdown = Subscript.extend({
5205
+ renderMarkdown: (node) => `<sub>${markText(node)}</sub>`
5206
+ });
5207
+ var SuperscriptWithMarkdown = Superscript.extend({
5208
+ renderMarkdown: (node) => `<sup>${markText(node)}</sup>`
5209
+ });
5210
+ var BlockquoteWithStableEmptyMarkdown = Blockquote.extend({
5211
+ renderMarkdown: (node, h) => {
5212
+ if (!node.content) return "";
5213
+ const rendered = node.content.map((child, index) => {
5214
+ const childContent = h.renderChild?.(child, index) ?? h.renderChildren([child]);
5215
+ return childContent.split("\n").map((line) => line.trim() === "" ? ">" : `> ${line}`).join("\n");
5216
+ });
5217
+ const nonEmpty = rendered.filter((part) => part.replace(/^>\s*/gm, "").trim() !== "");
5218
+ if (nonEmpty.length === 0) return "";
5219
+ return nonEmpty.join("\n>\n");
5220
+ }
5221
+ });
5222
+ function renderStableListItemMarkdown(node, h, ctx = {}) {
5223
+ if (!node.content) return "";
5224
+ const prefix = ctx.itemPrefix ?? (ctx.parentType === "orderedList" ? `${(ctx.meta?.parentAttrs?.start ?? 1) + (ctx.index ?? 0)}. ` : "- ");
5225
+ const [content, ...children] = node.content;
5226
+ const renderedMain = content ? h.renderChildren([content]) : "";
5227
+ const mainContent = renderedMain.trim() === "" ? EMPTY_LIST_ITEM_SENTINEL : renderedMain;
5228
+ let output = `${prefix}${mainContent}`;
5229
+ for (const [index, child] of children.entries()) {
5230
+ const childContent = h.renderChild?.(child, index + 1) ?? h.renderChildren([child]);
5231
+ const indentedChild = childContent.split("\n").map((line) => h.indent(line || "")).join("\n");
5232
+ output += child.type === "paragraph" ? `
5233
+
5234
+ ${indentedChild}` : `
5235
+ ${indentedChild}`;
5236
+ }
5237
+ return output;
5238
+ }
5239
+ var ListItemWithStableEmptyMarkdown = ListItem.extend({
5240
+ renderMarkdown: renderStableListItemMarkdown
5241
+ });
5242
+ var TaskItemWithStableEmptyMarkdown = TaskItem2.extend({
5243
+ renderMarkdown: (node, h) => renderStableListItemMarkdown(node, h, {
5244
+ itemPrefix: `- [${node.attrs?.checked ? "x" : " "}] `
5245
+ })
5246
+ });
5247
+ function normalizeAlign(value) {
5248
+ return value === "left" || value === "right" || value === "center" ? value : null;
5249
+ }
5250
+ function sanitizeCellText(raw) {
5251
+ return raw.replace(/\r?\n/g, "<br>").replace(/\s+/g, " ").trim().replace(/\|/g, "\\|");
5252
+ }
5253
+ function renderTableCellSafeMarkdown(node, h) {
5254
+ if (!node?.content || node.content.length === 0) return "";
5255
+ const rows = [];
5256
+ for (const rowNode of node.content) {
5257
+ const cells = [];
5258
+ for (const cellNode of rowNode.content ?? []) {
5259
+ const children = cellNode.content;
5260
+ let raw = "";
5261
+ if (Array.isArray(children) && children.length > 1) {
5262
+ raw = children.map((c) => sanitizeCellText(h.renderChildren(c))).join("<br>");
5263
+ } else {
5264
+ raw = sanitizeCellText(children ? h.renderChildren(children) : "");
5265
+ }
5266
+ cells.push({
5267
+ text: raw,
5268
+ isHeader: cellNode.type === "tableHeader",
5269
+ align: normalizeAlign(cellNode.attrs?.align)
5270
+ });
5271
+ }
5272
+ rows.push(cells);
5273
+ }
5274
+ const columnCount = rows.reduce((max, r) => Math.max(max, r.length), 0);
5275
+ if (columnCount === 0) return "";
5276
+ const colWidths = new Array(columnCount).fill(0);
5277
+ for (const r of rows) {
5278
+ for (let i = 0; i < columnCount; i += 1) {
5279
+ const len = (r[i]?.text || "").length;
5280
+ if (len > colWidths[i]) colWidths[i] = len;
5281
+ if (colWidths[i] < 3) colWidths[i] = 3;
5282
+ }
5283
+ }
5284
+ const pad = (s, width) => s + " ".repeat(Math.max(0, width - s.length));
5285
+ const headerRow = rows[0];
5286
+ const hasHeader = headerRow.some((c) => c.isHeader);
5287
+ const colAlignments = new Array(columnCount).fill(null);
5288
+ for (const r of rows) {
5289
+ for (let i = 0; i < columnCount; i += 1) {
5290
+ if (!colAlignments[i] && r[i]?.align) colAlignments[i] = r[i].align;
5291
+ }
5292
+ }
5293
+ let out = "\n";
5294
+ const headerTexts = new Array(columnCount).fill(0).map((_, i) => hasHeader ? headerRow[i]?.text || "" : "");
5295
+ out += `| ${headerTexts.map((t, i) => pad(t, colWidths[i])).join(" | ")} |
5296
+ `;
5297
+ out += `| ${colWidths.map((w, index) => {
5298
+ const dashCount = Math.max(3, w);
5299
+ const a = colAlignments[index];
5300
+ if (a === "left") return `:${"-".repeat(dashCount)}`;
5301
+ if (a === "right") return `${"-".repeat(dashCount)}:`;
5302
+ if (a === "center") return `:${"-".repeat(dashCount)}:`;
5303
+ return "-".repeat(dashCount);
5304
+ }).join(" | ")} |
5305
+ `;
5306
+ const body = hasHeader ? rows.slice(1) : rows;
5307
+ for (const r of body) {
5308
+ out += `| ${new Array(columnCount).fill(0).map((_, i) => pad(r[i]?.text || "", colWidths[i])).join(" | ")} |
5309
+ `;
5310
+ }
5311
+ return out;
5312
+ }
3895
5313
  var TableWithId = Table.extend({
3896
5314
  addAttributes() {
3897
5315
  return {
@@ -3902,7 +5320,8 @@ var TableWithId = Table.extend({
3902
5320
  renderHTML: (attrs) => attrs.tableId ? { "data-table-id": attrs.tableId } : {}
3903
5321
  }
3904
5322
  };
3905
- }
5323
+ },
5324
+ renderMarkdown: renderTableCellSafeMarkdown
3906
5325
  });
3907
5326
  var FrontmatterSchema = CodeBlock.extend({
3908
5327
  name: "frontmatter",
@@ -3920,10 +5339,13 @@ ${text}
3920
5339
  function buildEditorExtensions(opts = {}) {
3921
5340
  const table = opts.table ?? TableWithId;
3922
5341
  const frontmatter = opts.frontmatter ?? FrontmatterSchema;
5342
+ const inlineIcon = opts.inlineIcon ?? InlineIconNode;
3923
5343
  return [
3924
5344
  StarterKit.configure({
3925
5345
  undoRedo: false,
5346
+ blockquote: false,
3926
5347
  code: false,
5348
+ listItem: false,
3927
5349
  link: {
3928
5350
  openOnClick: true,
3929
5351
  HTMLAttributes: { target: "_blank", rel: "noopener noreferrer" },
@@ -3931,14 +5353,16 @@ function buildEditorExtensions(opts = {}) {
3931
5353
  }
3932
5354
  }),
3933
5355
  CodeWithCombinableMarks,
5356
+ BlockquoteWithStableEmptyMarkdown,
5357
+ ListItemWithStableEmptyMarkdown,
3934
5358
  imageBlockExtensions_default.configure({ roomId: opts.roomId, awareness: opts.awareness }),
3935
5359
  videoBlockNode_default.configure({ roomId: opts.roomId, awareness: opts.awareness }),
3936
5360
  mediaKeyboardNav_default,
3937
- TaskList,
3938
- TaskItem.configure({ nested: true }),
5361
+ TaskList2,
5362
+ TaskItemWithStableEmptyMarkdown.configure({ nested: true }),
3939
5363
  Highlight,
3940
- Subscript,
3941
- Superscript,
5364
+ SubscriptWithMarkdown,
5365
+ SuperscriptWithMarkdown,
3942
5366
  table.configure({
3943
5367
  resizable: true,
3944
5368
  handleWidth: 5,
@@ -3949,59 +5373,12 @@ function buildEditorExtensions(opts = {}) {
3949
5373
  TableRow,
3950
5374
  TableCell,
3951
5375
  TableHeader,
3952
- frontmatter
5376
+ frontmatter,
5377
+ inlineIcon
3953
5378
  ];
3954
5379
  }
3955
5380
  var editorExtensions = buildEditorExtensions();
3956
5381
 
3957
- // ../shared/src/frontmatter.ts
3958
- function extractFrontmatter(text) {
3959
- const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
3960
- if (!match) return null;
3961
- return { yaml: match[1].trim(), body: match[2] };
3962
- }
3963
-
3964
- // ../shared/src/markdown.ts
3965
- import { getSchema } from "@tiptap/core";
3966
- import { MarkdownManager } from "@tiptap/markdown";
3967
- import {
3968
- prosemirrorJSONToYXmlFragment,
3969
- yXmlFragmentToProsemirrorJSON
3970
- } from "@tiptap/y-tiptap";
3971
- function markdownToDocJSON(markdown, extensions = editorExtensions) {
3972
- const manager = new MarkdownManager({ extensions });
3973
- const parsed = manager.parse(markdown);
3974
- if (parsed && parsed.type === "doc") return parsed;
3975
- return { type: "doc", content: parsed?.content ?? [] };
3976
- }
3977
- function writeMarkdownToYFragment(fragment, markdown, extensions = editorExtensions) {
3978
- const doc = fragment.doc;
3979
- if (!doc) throw new Error("fragment must be attached to a Y.Doc");
3980
- const fm = extractFrontmatter(markdown);
3981
- const body = fm ? fm.body : markdown;
3982
- const schema = getSchema(extensions);
3983
- const bodyDoc = markdownToDocJSON(body, extensions);
3984
- const children = [];
3985
- if (fm) {
3986
- children.push({
3987
- type: "frontmatter",
3988
- attrs: { language: "yaml" },
3989
- content: fm.yaml ? [{ type: "text", text: fm.yaml }] : void 0
3990
- });
3991
- }
3992
- if (bodyDoc.content) children.push(...bodyDoc.content);
3993
- const fullDoc = { type: "doc", content: children };
3994
- doc.transact(() => {
3995
- prosemirrorJSONToYXmlFragment(schema, fullDoc, fragment);
3996
- });
3997
- }
3998
- function markdownFromYFragment(fragment, extensions = editorExtensions) {
3999
- const json = yXmlFragmentToProsemirrorJSON(fragment);
4000
- if (!json || !json.content || json.content.length === 0) return "";
4001
- const manager = new MarkdownManager({ extensions });
4002
- return manager.serialize(json);
4003
- }
4004
-
4005
5382
  // ../shared/src/insertMedia.ts
4006
5383
  import { nanoid as nanoid2 } from "nanoid";
4007
5384
 
@@ -5615,6 +6992,27 @@ var TOOL_DEFS = [
5615
6992
  required: ["roomId", "threadId"]
5616
6993
  }
5617
6994
  },
6995
+ {
6996
+ name: "composer_list_threads",
6997
+ description: 'List comment and suggestion threads in the room, with full reply history. Same per-thread shape as `composer_get_thread` minus `sectionMarkdown` (omitted so the payload stays tractable across many threads \u2014 call `composer_get_full_doc` or `composer_get_thread` for surrounding context). Sorted by `createdAt` ascending so the agent reads them in the order they were posted. Use when you need to harvest discussion across the whole doc \u2014 e.g. "the user asked questions in the doc and other people answered in comment threads, summarize the answers". Optional `kind` filter narrows to one type when suggestion churn or comment chatter would be noise. Optional `status` filter drives the "which threads are still open" audit AND is the primary lever for shrinking the payload: `"open"` excludes resolved comments and accepted/rejected suggestions, which accumulate for the room\'s whole lifetime (they are never deleted), so on a long-lived doc the default `"all"` can be large.',
6998
+ inputSchema: {
6999
+ type: "object",
7000
+ properties: {
7001
+ roomId: { type: "string" },
7002
+ kind: {
7003
+ type: "string",
7004
+ enum: ["comment", "suggestion", "all"],
7005
+ description: 'Which thread kind to return. `"comment"` = Q&A / discussion threads only; `"suggestion"` = text-replacement proposals only; `"all"` (default) = both, with the `kind` field on each entry as the discriminator.'
7006
+ },
7007
+ status: {
7008
+ type: "string",
7009
+ enum: ["open", "resolved", "all"],
7010
+ description: 'Lifecycle filter. `"open"` = unresolved comments + pending suggestions (a thread with no terminal marker counts as open). `"resolved"` = resolved comments + accepted/rejected suggestions. `"all"` (default) = no filter. Use `"open"` for the unresolved-threads audit and to keep the response small on busy docs.'
7011
+ }
7012
+ },
7013
+ required: ["roomId"]
7014
+ }
7015
+ },
5618
7016
  {
5619
7017
  name: "composer_add_comment",
5620
7018
  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).\n**`textToFind` matches the doc's stored text, not the markdown source.** Inline marks (`**bold**`, `*italic*`, `` `code` ``, `_emphasis_`, `~~strike~~`) are stored as Y.Marks \u2014 write plain text, no markers. Top-level blocks join with `\\n\\n`; list items have NO separator (three bullets `- a / - b / - c` are stored as `abc`). Copy from `Section as the matcher sees it` in any `text_not_found` error if unsure. Returns { id } on success or an isError result if the anchor cannot be resolved.",
@@ -6130,23 +7528,8 @@ function handleGetFullDoc(args) {
6130
7528
  const state = getOrError(roomId);
6131
7529
  return okResult({ markdown: serializeDocAsMarkdown(state.doc) });
6132
7530
  }
6133
- function handleGetThread(args) {
6134
- const a = asObject(args);
6135
- const roomId = asString(a.roomId, "roomId");
6136
- const threadId = asString(a.threadId, "threadId");
6137
- const state = getOrError(roomId);
6138
- const commentRaw = state.doc.getMap("comments").get(threadId);
6139
- const suggestionRaw = state.doc.getMap("suggestions").get(threadId);
6140
- const raw = commentRaw ?? suggestionRaw;
6141
- if (!raw) {
6142
- return errorResult(`thread not found: ${threadId}`);
6143
- }
6144
- const kind = commentRaw ? "comment" : "suggestion";
6145
- const anchoredContext = resolveAnchoredContext(
6146
- state.doc,
6147
- raw.anchorFrom,
6148
- raw.anchorTo
6149
- );
7531
+ function shapeThread(doc, threadId, raw, kind, opts) {
7532
+ const anchoredContext = resolveAnchoredContext(doc, raw.anchorFrom, raw.anchorTo);
6150
7533
  const replies = Array.isArray(raw.replies) ? raw.replies : [];
6151
7534
  const shapedReplies = replies.filter(
6152
7535
  (r) => !!r && typeof r === "object" && typeof r.id === "string" && typeof r.text === "string"
@@ -6159,7 +7542,7 @@ function handleGetThread(args) {
6159
7542
  mentions: Array.isArray(r.mentions) ? r.mentions.filter((m) => typeof m === "string") : void 0,
6160
7543
  createdAt: typeof r.createdAt === "number" ? r.createdAt : void 0
6161
7544
  }));
6162
- return okResult({
7545
+ return {
6163
7546
  threadId,
6164
7547
  kind,
6165
7548
  body: typeof raw.text === "string" ? raw.text : void 0,
@@ -6175,9 +7558,72 @@ function handleGetThread(args) {
6175
7558
  anchoredText: anchoredContext.anchoredText,
6176
7559
  headingId: anchoredContext.headingId,
6177
7560
  headingText: anchoredContext.headingText,
6178
- sectionMarkdown: anchoredContext.sectionMarkdown,
7561
+ sectionMarkdown: opts.includeSectionMarkdown ? anchoredContext.sectionMarkdown : void 0,
6179
7562
  replies: shapedReplies
7563
+ };
7564
+ }
7565
+ function handleGetThread(args) {
7566
+ const a = asObject(args);
7567
+ const roomId = asString(a.roomId, "roomId");
7568
+ const threadId = asString(a.threadId, "threadId");
7569
+ const state = getOrError(roomId);
7570
+ const commentRaw = state.doc.getMap("comments").get(threadId);
7571
+ const suggestionRaw = state.doc.getMap("suggestions").get(threadId);
7572
+ const raw = commentRaw ?? suggestionRaw;
7573
+ if (!raw) {
7574
+ return errorResult(`thread not found: ${threadId}`);
7575
+ }
7576
+ const kind = commentRaw ? "comment" : "suggestion";
7577
+ return okResult(
7578
+ shapeThread(state.doc, threadId, raw, kind, { includeSectionMarkdown: true })
7579
+ );
7580
+ }
7581
+ function handleListThreads(args) {
7582
+ const a = asObject(args);
7583
+ const roomId = asString(a.roomId, "roomId");
7584
+ const state = getOrError(roomId);
7585
+ const kindArg = asOptionalString(a.kind, "kind") ?? "all";
7586
+ if (kindArg !== "all" && kindArg !== "comment" && kindArg !== "suggestion") {
7587
+ return errorResult(`kind must be one of "all", "comment", "suggestion" (got "${kindArg}")`);
7588
+ }
7589
+ const statusArg = asOptionalString(a.status, "status") ?? "all";
7590
+ if (statusArg !== "all" && statusArg !== "open" && statusArg !== "resolved") {
7591
+ return errorResult(`status must be one of "all", "open", "resolved" (got "${statusArg}")`);
7592
+ }
7593
+ const out = [];
7594
+ if (kindArg === "all" || kindArg === "comment") {
7595
+ const comments = state.doc.getMap("comments");
7596
+ comments.forEach((raw, id) => {
7597
+ if (raw && typeof raw === "object") {
7598
+ out.push(shapeThread(state.doc, id, raw, "comment", {
7599
+ includeSectionMarkdown: false
7600
+ }));
7601
+ }
7602
+ });
7603
+ }
7604
+ if (kindArg === "all" || kindArg === "suggestion") {
7605
+ const suggestions = state.doc.getMap("suggestions");
7606
+ suggestions.forEach((raw, id) => {
7607
+ if (raw && typeof raw === "object") {
7608
+ out.push(shapeThread(state.doc, id, raw, "suggestion", {
7609
+ includeSectionMarkdown: false
7610
+ }));
7611
+ }
7612
+ });
7613
+ }
7614
+ if (statusArg !== "all") {
7615
+ const isOpen = (t) => t.kind === "comment" ? t.resolved !== true : t.status === void 0 || t.status === "pending";
7616
+ const wantOpen = statusArg === "open";
7617
+ for (let i = out.length - 1; i >= 0; i--) {
7618
+ if (isOpen(out[i]) !== wantOpen) out.splice(i, 1);
7619
+ }
7620
+ }
7621
+ out.sort((a2, b) => {
7622
+ const aT = a2.createdAt ?? Number.POSITIVE_INFINITY;
7623
+ const bT = b.createdAt ?? Number.POSITIVE_INFINITY;
7624
+ return aT - bT;
6180
7625
  });
7626
+ return okResult({ threads: out });
6181
7627
  }
6182
7628
  function performAddComment(state, a) {
6183
7629
  const anchor = asAnchor(a.anchor);
@@ -6452,6 +7898,7 @@ function performResolveThread(state, a) {
6452
7898
  participantUserIds: getParticipantUserIds(existing),
6453
7899
  createdAt
6454
7900
  });
7901
+ markActivityReadForThread(state.doc, threadId, state.identity.userId);
6455
7902
  });
6456
7903
  return okResult({ threadId, resolved: true });
6457
7904
  }
@@ -6624,6 +8071,8 @@ async function dispatchTool(name, args, signal) {
6624
8071
  return handleGetFullDoc(args);
6625
8072
  case "composer_get_thread":
6626
8073
  return handleGetThread(args);
8074
+ case "composer_list_threads":
8075
+ return handleListThreads(args);
6627
8076
  case "composer_add_comment":
6628
8077
  return handleAddComment(args);
6629
8078
  case "composer_reply_comment":