@composer-app/mcp 0.0.4-beta.1 → 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;
@@ -363,24 +380,1182 @@ var removeAwarenessStates = (awareness, clients, origin) => {
363
380
  lastUpdated: getUnixTime()
364
381
  });
365
382
  }
366
- removed.push(clientID);
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;
1506
+ }
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
 
@@ -4371,22 +5748,22 @@ function buildFlatMap(fragment) {
4371
5748
  walk(node);
4372
5749
  }
4373
5750
  if (idx < topLevel.length - 1) {
4374
- flat += "\n";
5751
+ flat += "\n\n";
4375
5752
  }
4376
5753
  });
4377
5754
  return { flat, map, blockFlatStarts };
4378
5755
  }
4379
- function findNthOccurrence(flat, needle, n) {
4380
- if (needle.length === 0) return null;
4381
- let searchFrom = 0;
4382
- let found2 = -1;
4383
- for (let i = 0; i < n; i++) {
4384
- const idx = flat.indexOf(needle, searchFrom);
4385
- if (idx < 0) return null;
4386
- found2 = idx;
4387
- searchFrom = idx + 1;
5756
+ function findAllOccurrences(haystack, needle) {
5757
+ if (needle.length === 0) return [];
5758
+ const out = [];
5759
+ let from2 = 0;
5760
+ while (true) {
5761
+ const idx = haystack.indexOf(needle, from2);
5762
+ if (idx < 0) break;
5763
+ out.push(idx);
5764
+ from2 = idx + 1;
4388
5765
  }
4389
- return found2;
5766
+ return out;
4390
5767
  }
4391
5768
  function lookupFlatIndex(map, flatIndex) {
4392
5769
  for (const entry of map) {
@@ -4484,13 +5861,11 @@ function resolveServerAnchor(doc, spec) {
4484
5861
  return {
4485
5862
  ok: false,
4486
5863
  error: "section_not_found",
4487
- currentSectionText: ""
5864
+ currentSectionText: "",
5865
+ currentSectionFlat: ""
4488
5866
  };
4489
5867
  }
4490
5868
  const currentSectionText = getSection(doc, spec.headingId);
4491
- if (!currentSectionText.includes(spec.textToFind)) {
4492
- return { ok: false, error: "text_not_found", currentSectionText };
4493
- }
4494
5869
  const fragment = doc.getXmlFragment("default");
4495
5870
  const { flat, map, blockFlatStarts } = buildFlatMap(fragment);
4496
5871
  const range = getSectionBlockRange(doc, spec.headingId);
@@ -4498,25 +5873,68 @@ function resolveServerAnchor(doc, spec) {
4498
5873
  return {
4499
5874
  ok: false,
4500
5875
  error: "section_not_found",
4501
- currentSectionText
5876
+ currentSectionText,
5877
+ currentSectionFlat: ""
4502
5878
  };
4503
5879
  }
4504
5880
  const sectionFlatStart = blockFlatStarts[range.start] ?? 0;
4505
5881
  const sectionFlatEnd = range.end < blockFlatStarts.length ? blockFlatStarts[range.end] : flat.length;
4506
5882
  const sectionFlat = flat.slice(sectionFlatStart, sectionFlatEnd);
4507
- const sectionRelStart = findNthOccurrence(sectionFlat, spec.textToFind, occurrence);
4508
- if (sectionRelStart === null) {
4509
- return { ok: false, error: "text_not_found", currentSectionText };
5883
+ const allMatches = findAllOccurrences(sectionFlat, spec.textToFind);
5884
+ if (allMatches.length === 0) {
5885
+ return {
5886
+ ok: false,
5887
+ error: "text_not_found",
5888
+ currentSectionText,
5889
+ currentSectionFlat: sectionFlat
5890
+ };
5891
+ }
5892
+ const topLevel = fragment.toArray();
5893
+ const isHeadingBlockAt = (absFlatIndex) => {
5894
+ let blockIdx = 0;
5895
+ for (let i = 0; i < blockFlatStarts.length; i++) {
5896
+ if (blockFlatStarts[i] <= absFlatIndex) blockIdx = i;
5897
+ else break;
5898
+ }
5899
+ const block = topLevel[blockIdx];
5900
+ return block instanceof Y4.XmlElement && block.nodeName === "heading";
5901
+ };
5902
+ const ranked = allMatches.map((sectionRelStart) => ({
5903
+ sectionRelStart,
5904
+ isHeading: isHeadingBlockAt(sectionFlatStart + sectionRelStart)
5905
+ }));
5906
+ ranked.sort((a, b) => {
5907
+ if (a.isHeading !== b.isHeading) return a.isHeading ? 1 : -1;
5908
+ return a.sectionRelStart - b.sectionRelStart;
5909
+ });
5910
+ const pick = ranked[occurrence - 1];
5911
+ if (!pick) {
5912
+ return {
5913
+ ok: false,
5914
+ error: "text_not_found",
5915
+ currentSectionText,
5916
+ currentSectionFlat: sectionFlat
5917
+ };
4510
5918
  }
4511
- const flatStart = sectionFlatStart + sectionRelStart;
5919
+ const flatStart = sectionFlatStart + pick.sectionRelStart;
4512
5920
  const flatEnd = flatStart + spec.textToFind.length;
4513
5921
  const startEntry = lookupFlatIndex(map, flatStart);
4514
5922
  if (!startEntry) {
4515
- return { ok: false, error: "text_not_found", currentSectionText };
5923
+ return {
5924
+ ok: false,
5925
+ error: "text_not_found",
5926
+ currentSectionText,
5927
+ currentSectionFlat: sectionFlat
5928
+ };
4516
5929
  }
4517
5930
  const lastCharEntry = lookupFlatIndexEnd(map, flatEnd - 1);
4518
5931
  if (!lastCharEntry) {
4519
- return { ok: false, error: "text_not_found", currentSectionText };
5932
+ return {
5933
+ ok: false,
5934
+ error: "text_not_found",
5935
+ currentSectionText,
5936
+ currentSectionFlat: sectionFlat
5937
+ };
4520
5938
  }
4521
5939
  const fromRelPos = Y4.createRelativePositionFromTypeIndex(
4522
5940
  startEntry.xmlText,
@@ -5574,9 +6992,30 @@ var TOOL_DEFS = [
5574
6992
  required: ["roomId", "threadId"]
5575
6993
  }
5576
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
+ },
5577
7016
  {
5578
7017
  name: "composer_add_comment",
5579
- 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.",
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.",
5580
7019
  inputSchema: {
5581
7020
  type: "object",
5582
7021
  properties: {
@@ -5630,7 +7069,7 @@ var TOOL_DEFS = [
5630
7069
  },
5631
7070
  {
5632
7071
  name: "composer_add_suggestion",
5633
- description: "Post a text replacement suggestion. A suggestion can target ANY span anywhere in the doc \u2014 not just the span of the thread that triggered you. Pick exactly one of:\n - `fromThreadId` \u2014 inherit the source thread's exact stored anchor. Right when the user's request is scoped to what they selected (the common case: 'rewrite this', 'make this shorter').\n - `anchor` \u2014 specify a span yourself via `{ headingId, textToFind, occurrence? }`. Use this when the user's request targets different text ('also update the intro', 'the bullet list in Section 3 is stale') OR for proactive suggestions with no source thread.\n**Anchor = what gets deleted.** Your `textToFind` is literally cut when the user accepts and `replacementText` is inserted in its place. Anchor the whole unit you're changing (full sentence including terminal punctuation; full list item text; full paragraph), match your replacementText's shape (inline for mid-paragraph edits, full markdown block for block replacements), end replacement at the same boundary as the anchor, and include any formatting you want preserved in the replacement itself \u2014 the anchor's bold / link / heading level is gone on accept. A too-narrow or mid-token anchor leaves broken spacing or smashed-together words. See SKILL.md 'Pick the right span' for the full rubric.\n**Ripple coverage is your responsibility.** If the change requires edits nearby or elsewhere (enumeration counts, cross-references, subject/verb agreement, restated facts), call this tool MULTIPLE times in the same turn \u2014 one suggestion per span \u2014 so accepting leaves the doc correct. If you're unsure whether ripples exist elsewhere in the doc, call `composer_get_full_doc` first. Returns { id } on success or an isError result if the anchor cannot be resolved.",
7072
+ description: "Post a text replacement suggestion. A suggestion can target ANY span anywhere in the doc \u2014 not just the span of the thread that triggered you. Pick exactly one of:\n - `fromThreadId` \u2014 inherit the source thread's exact stored anchor. Right when the user's request is scoped to what they selected (the common case: 'rewrite this', 'make this shorter').\n - `anchor` \u2014 specify a span yourself via `{ headingId, textToFind, occurrence? }`. Use this when the user's request targets different text ('also update the intro', 'the bullet list in Section 3 is stale') OR for proactive suggestions with no source thread.\n**Anchor = what gets deleted.** Your `textToFind` is literally cut when the user accepts and `replacementText` is inserted in its place. Anchor the whole unit you're changing (full sentence including terminal punctuation; full list item text; full paragraph), match your replacementText's shape (inline for mid-paragraph edits, full markdown block for block replacements), end replacement at the same boundary as the anchor, and include any formatting you want preserved in the replacement itself \u2014 the anchor's bold / link / heading level is gone on accept. A too-narrow or mid-token anchor leaves broken spacing or smashed-together words. See SKILL.md 'Pick the right span' for the full rubric.\n**`textToFind` matches the doc's stored text, not the markdown source.** Three rules govern the shape:\n 1. **Inline marks are stripped.** `**bold**`, `*italic*`, `` `code` ``, `_emphasis_`, `~~strike~~` are stored as Y.Marks, not as literal characters. Write the plain text \u2014 `the file back` not `` `the file back` ``.\n 2. **Top-level blocks separate with `\\n\\n`.** Heading + body, or two sibling paragraphs, are joined by a blank line. Soft line breaks WITHIN a single paragraph are preserved as single `\\n`.\n 3. **List items have NO separator.** Three bullets `- a / - b / - c` are stored as `abc` (smashed). Same for ordered list items.\n When in doubt, copy from the `Section as the matcher sees it` block in any `text_not_found` error.\n**Ripple coverage is your responsibility.** If the change requires edits nearby or elsewhere (enumeration counts, cross-references, subject/verb agreement, restated facts), call this tool MULTIPLE times in the same turn \u2014 one suggestion per span \u2014 so accepting leaves the doc correct. If you're unsure whether ripples exist elsewhere in the doc, call `composer_get_full_doc` first. Returns { id } on success or an isError result if the anchor cannot be resolved.",
5634
7073
  inputSchema: {
5635
7074
  type: "object",
5636
7075
  properties: {
@@ -5787,6 +7226,12 @@ function asString(value, field) {
5787
7226
  }
5788
7227
  return value;
5789
7228
  }
7229
+ function asStringAllowEmpty(value, field) {
7230
+ if (typeof value !== "string") {
7231
+ throw new Error(`${field} must be a string`);
7232
+ }
7233
+ return value;
7234
+ }
5790
7235
  function asOptionalString(value, field) {
5791
7236
  if (value === void 0) return void 0;
5792
7237
  if (typeof value !== "string") {
@@ -6083,23 +7528,8 @@ function handleGetFullDoc(args) {
6083
7528
  const state = getOrError(roomId);
6084
7529
  return okResult({ markdown: serializeDocAsMarkdown(state.doc) });
6085
7530
  }
6086
- function handleGetThread(args) {
6087
- const a = asObject(args);
6088
- const roomId = asString(a.roomId, "roomId");
6089
- const threadId = asString(a.threadId, "threadId");
6090
- const state = getOrError(roomId);
6091
- const commentRaw = state.doc.getMap("comments").get(threadId);
6092
- const suggestionRaw = state.doc.getMap("suggestions").get(threadId);
6093
- const raw = commentRaw ?? suggestionRaw;
6094
- if (!raw) {
6095
- return errorResult(`thread not found: ${threadId}`);
6096
- }
6097
- const kind = commentRaw ? "comment" : "suggestion";
6098
- const anchoredContext = resolveAnchoredContext(
6099
- state.doc,
6100
- raw.anchorFrom,
6101
- raw.anchorTo
6102
- );
7531
+ function shapeThread(doc, threadId, raw, kind, opts) {
7532
+ const anchoredContext = resolveAnchoredContext(doc, raw.anchorFrom, raw.anchorTo);
6103
7533
  const replies = Array.isArray(raw.replies) ? raw.replies : [];
6104
7534
  const shapedReplies = replies.filter(
6105
7535
  (r) => !!r && typeof r === "object" && typeof r.id === "string" && typeof r.text === "string"
@@ -6112,7 +7542,7 @@ function handleGetThread(args) {
6112
7542
  mentions: Array.isArray(r.mentions) ? r.mentions.filter((m) => typeof m === "string") : void 0,
6113
7543
  createdAt: typeof r.createdAt === "number" ? r.createdAt : void 0
6114
7544
  }));
6115
- return okResult({
7545
+ return {
6116
7546
  threadId,
6117
7547
  kind,
6118
7548
  body: typeof raw.text === "string" ? raw.text : void 0,
@@ -6128,9 +7558,72 @@ function handleGetThread(args) {
6128
7558
  anchoredText: anchoredContext.anchoredText,
6129
7559
  headingId: anchoredContext.headingId,
6130
7560
  headingText: anchoredContext.headingText,
6131
- sectionMarkdown: anchoredContext.sectionMarkdown,
7561
+ sectionMarkdown: opts.includeSectionMarkdown ? anchoredContext.sectionMarkdown : void 0,
6132
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;
6133
7625
  });
7626
+ return okResult({ threads: out });
6134
7627
  }
6135
7628
  function performAddComment(state, a) {
6136
7629
  const anchor = asAnchor(a.anchor);
@@ -6139,8 +7632,11 @@ function performAddComment(state, a) {
6139
7632
  const resolved = resolveServerAnchor(state.doc, anchor);
6140
7633
  if (!resolved.ok) {
6141
7634
  return errorResult(
6142
- `anchor ${resolved.error}. Current section:
6143
- ${resolved.currentSectionText}`
7635
+ `anchor ${resolved.error}. Section as rendered (markdown form, with markers):
7636
+ ${resolved.currentSectionText}
7637
+
7638
+ Section as the matcher sees it (use this exact form for textToFind \u2014 inline marks like \`code\`, **bold**, *italic* are stored as marks not characters, so they're absent here; soft line breaks are preserved; list items have no separators):
7639
+ ${resolved.currentSectionFlat}`
6144
7640
  );
6145
7641
  }
6146
7642
  const id = nanoid4();
@@ -6243,7 +7739,7 @@ function handleReplyComment(args) {
6243
7739
  return performReplyComment(state, a);
6244
7740
  }
6245
7741
  function performAddSuggestion(state, a) {
6246
- const replacementText = asString(a.replacementText, "replacementText");
7742
+ const replacementText = asStringAllowEmpty(a.replacementText, "replacementText");
6247
7743
  const fromThreadId = asOptionalString(a.fromThreadId, "fromThreadId");
6248
7744
  const agentState = asOptionalAgentState(a.state, "state");
6249
7745
  let anchorFrom;
@@ -6402,6 +7898,7 @@ function performResolveThread(state, a) {
6402
7898
  participantUserIds: getParticipantUserIds(existing),
6403
7899
  createdAt
6404
7900
  });
7901
+ markActivityReadForThread(state.doc, threadId, state.identity.userId);
6405
7902
  });
6406
7903
  return okResult({ threadId, resolved: true });
6407
7904
  }
@@ -6574,6 +8071,8 @@ async function dispatchTool(name, args, signal) {
6574
8071
  return handleGetFullDoc(args);
6575
8072
  case "composer_get_thread":
6576
8073
  return handleGetThread(args);
8074
+ case "composer_list_threads":
8075
+ return handleListThreads(args);
6577
8076
  case "composer_add_comment":
6578
8077
  return handleAddComment(args);
6579
8078
  case "composer_reply_comment":