@adecore/agents-react 0.37.2 → 0.37.3

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.
Files changed (98) hide show
  1. package/dist/chat/chat-client.d.ts +1 -0
  2. package/dist/chat/chat-client.js +2 -1
  3. package/dist/chat/chat-references.d.ts +16 -1
  4. package/dist/chat/chat-references.js +29 -3
  5. package/dist/chat/drafts.d.ts +2 -0
  6. package/dist/chat/drafts.js +21 -5
  7. package/dist/chat/index.d.ts +1 -1
  8. package/dist/chat/mentions.d.ts +31 -14
  9. package/dist/chat/mentions.js +186 -41
  10. package/dist/chat/stash.d.ts +1 -0
  11. package/dist/chat/stash.js +4 -2
  12. package/dist/chat/ui/Composer.js +438 -429
  13. package/dist/chat/ui/ComposerInput.d.ts +1 -0
  14. package/dist/chat/ui/ComposerInput.js +2 -0
  15. package/dist/chat/ui/Markdown.d.ts +3 -1
  16. package/dist/chat/ui/Markdown.js +194 -120
  17. package/dist/chat/ui/MessageActions.js +70 -66
  18. package/dist/chat/ui/ReferenceChip.d.ts +15 -0
  19. package/dist/chat/ui/ReferenceChip.js +255 -0
  20. package/dist/chat/ui/Timeline.js +52 -29
  21. package/dist/chat/ui/TimelineMenu.js +84 -78
  22. package/dist/chat/ui/chip-clipboard.d.ts +9 -0
  23. package/dist/chat/ui/chip-clipboard.js +105 -0
  24. package/dist/chat/ui/chips.d.ts +5 -0
  25. package/dist/chat/ui/chips.js +15 -1
  26. package/dist/chat/ui/composer/ComposerMenu.js +140 -79
  27. package/dist/chat/ui/composer/DraftParts.d.ts +0 -4
  28. package/dist/chat/ui/composer/DraftParts.js +0 -41
  29. package/dist/chat/ui/composer/chip-paste.d.ts +15 -0
  30. package/dist/chat/ui/composer/chip-paste.js +104 -0
  31. package/dist/chat/ui/composer/chips.d.ts +5 -0
  32. package/dist/chat/ui/composer/chips.js +59 -9
  33. package/dist/chat/ui/composer/key-handler.js +9 -0
  34. package/dist/chat/ui/composer/use-composer-draft.d.ts +0 -1
  35. package/dist/chat/ui/composer/use-composer-draft.js +2 -10
  36. package/dist/chat/ui/composer/use-composer-menus.d.ts +12 -2
  37. package/dist/chat/ui/composer/use-composer-menus.js +192 -149
  38. package/dist/chat/ui/composer/use-composer-submit.js +17 -12
  39. package/dist/chat/ui/composer/use-draft-files.d.ts +7 -1
  40. package/dist/chat/ui/composer/use-draft-files.js +90 -58
  41. package/dist/chat/ui/composer/use-take-back.js +2 -1
  42. package/dist/chat/ui/index.d.ts +0 -1
  43. package/dist/chat/ui/index.js +0 -1
  44. package/dist/chat/ui/reference-icons.d.ts +3 -0
  45. package/dist/chat/ui/reference-icons.js +15 -0
  46. package/dist/chat/ui/reference-lookup.d.ts +7 -0
  47. package/dist/chat/ui/reference-lookup.js +6 -0
  48. package/dist/chat/ui/rehype-chips.d.ts +8 -1
  49. package/dist/chat/ui/rehype-chips.js +35 -11
  50. package/dist/chat/ui/rows/MessageRows.js +218 -158
  51. package/dist/chat/ui/thread-references.d.ts +4 -0
  52. package/dist/chat/ui/thread-references.js +78 -0
  53. package/dist/host-hooks.d.ts +2 -0
  54. package/dist/host-hooks.js +7 -0
  55. package/dist/host.d.ts +3 -0
  56. package/dist/locales/en/agent-chat.d.ts +20 -3
  57. package/dist/locales/en/agent-chat.js +24 -7
  58. package/dist/locales/nl/agent-chat.d.ts +20 -3
  59. package/dist/locales/nl/agent-chat.js +24 -7
  60. package/package.json +8 -8
  61. package/src/chat/chat-client.ts +4 -1
  62. package/src/chat/chat-references.ts +44 -3
  63. package/src/chat/drafts.ts +24 -6
  64. package/src/chat/index.ts +1 -1
  65. package/src/chat/mentions.ts +202 -36
  66. package/src/chat/stash.ts +6 -2
  67. package/src/chat/ui/Composer.tsx +14 -4
  68. package/src/chat/ui/ComposerInput.tsx +4 -0
  69. package/src/chat/ui/Markdown.tsx +57 -25
  70. package/src/chat/ui/MessageActions.tsx +5 -2
  71. package/src/chat/ui/ReferenceChip.tsx +74 -0
  72. package/src/chat/ui/Timeline.tsx +24 -2
  73. package/src/chat/ui/TimelineMenu.tsx +7 -3
  74. package/src/chat/ui/chip-clipboard.ts +115 -0
  75. package/src/chat/ui/chips.ts +20 -1
  76. package/src/chat/ui/composer/ComposerMenu.tsx +53 -37
  77. package/src/chat/ui/composer/DraftParts.tsx +0 -12
  78. package/src/chat/ui/composer/chip-paste.ts +122 -0
  79. package/src/chat/ui/composer/chips.ts +60 -10
  80. package/src/chat/ui/composer/key-handler.ts +9 -0
  81. package/src/chat/ui/composer/use-composer-draft.ts +1 -7
  82. package/src/chat/ui/composer/use-composer-menus.ts +68 -29
  83. package/src/chat/ui/composer/use-composer-submit.ts +13 -6
  84. package/src/chat/ui/composer/use-draft-files.ts +39 -5
  85. package/src/chat/ui/composer/use-take-back.ts +2 -1
  86. package/src/chat/ui/index.ts +0 -1
  87. package/src/chat/ui/reference-icons.ts +17 -0
  88. package/src/chat/ui/reference-lookup.ts +13 -0
  89. package/src/chat/ui/rehype-chips.ts +38 -13
  90. package/src/chat/ui/rows/MessageRows.tsx +88 -64
  91. package/src/chat/ui/thread-references.ts +47 -0
  92. package/src/host-hooks.ts +9 -0
  93. package/src/host.ts +6 -1
  94. package/src/locales/en/agent-chat.ts +24 -7
  95. package/src/locales/nl/agent-chat.ts +24 -7
  96. package/dist/chat/ui/ChatReferenceChip.d.ts +0 -4
  97. package/dist/chat/ui/ChatReferenceChip.js +0 -110
  98. package/src/chat/ui/ChatReferenceChip.tsx +0 -26
@@ -457,6 +457,8 @@ export function Timeline({
457
457
  });
458
458
  const [loadingEarlier, setLoadingEarlier] = useState(false);
459
459
  const loadingEarlierRef = useRef(false);
460
+ const anchorRef = useRef(null);
461
+ const headRef = useRef(undefined);
460
462
  const loadEarlier = useCallback(() => {
461
463
  if (cursor === null || loadingEarlierRef.current) {
462
464
  return;
@@ -559,7 +561,7 @@ export function Timeline({
559
561
  const [, settleLayout] = useReducer(count => count + 1, 0);
560
562
  // Changing this callback invalidates every cached measurement, including on a scroll.
561
563
  const getItemKey = useCallback(index => rows[index].id, [rows]);
562
- // Nothing here builds with the React Compiler, so its memoization rule has nothing to break.
564
+ // Follow the browser's actual end below. Virtual end anchoring drifts when growth still fits without scrolling.
563
565
  // oxlint-disable-next-line react/incompatible-library
564
566
  const virtualizer = useVirtualizer({
565
567
  count: loadingHistory ? 0 : rows.length,
@@ -567,7 +569,6 @@ export function Timeline({
567
569
  estimateSize: () => ESTIMATED_ROW_PX,
568
570
  getItemKey,
569
571
  overscan: 8,
570
- anchorTo: 'end',
571
572
  // Include the end before the first pin, so its actual heights are known before revealing it.
572
573
  rangeExtractor: range => {
573
574
  const indexes = defaultRangeExtractor(range);
@@ -617,10 +618,32 @@ export function Timeline({
617
618
  viewport: element_2.clientHeight
618
619
  };
619
620
  }, [rows.length, totalSize, composerHeight, frame.height]);
621
+ // Prepending history moves rows; keep the reader on the same row at the same height.
622
+ useLayoutEffect(() => {
623
+ const element_3 = scrollRef.current;
624
+ const head = order?.[0];
625
+ const moved = head !== headRef.current;
626
+ headRef.current = head;
627
+ if (element_3 === null) {
628
+ return;
629
+ }
630
+ const anchor = anchorRef.current;
631
+ const index_2 = moved && anchor !== null && !followRef.current ? rows.findIndex(row_8 => row_8.id === anchor.id) : -1;
632
+ if (anchor !== null && index_2 !== -1) {
633
+ virtualizer.getTotalSize();
634
+ element_3.scrollTop = virtualizer.measurementsCache[index_2].start - anchor.offset;
635
+ return;
636
+ }
637
+ const row_9 = virtualizer.getVirtualItemForOffset(element_3.scrollTop);
638
+ anchorRef.current = row_9 === undefined ? null : {
639
+ id: String(row_9.key),
640
+ offset: row_9.start - element_3.scrollTop
641
+ };
642
+ });
620
643
  // Close to the top, the page before goes in; again after each page that left too little above the reader.
621
644
  useEffect(() => {
622
- const element_3 = scrollRef.current;
623
- if (element_3 !== null && wantsEarlier(element_3)) {
645
+ const element_4 = scrollRef.current;
646
+ if (element_4 !== null && wantsEarlier(element_4)) {
624
647
  loadEarlier();
625
648
  }
626
649
  }, [rows, loadEarlier, frame.height]);
@@ -634,8 +657,8 @@ export function Timeline({
634
657
  const measurements = virtualizer.measurementsCache;
635
658
  const starts = {
636
659
  length: ticks.length,
637
- at: index_2 => {
638
- const tick = ticks.at(index_2);
660
+ at: index_3 => {
661
+ const tick = ticks.at(index_3);
639
662
  return tick === undefined ? undefined : measurements[tick.rowIndex]?.start ?? tick.rowIndex * ESTIMATED_ROW_PX;
640
663
  }
641
664
  };
@@ -662,7 +685,7 @@ export function Timeline({
662
685
  starts,
663
686
  inView
664
687
  };
665
- const followStrip = element_4 => {
688
+ const followStrip = element_5 => {
666
689
  const {
667
690
  starts: current_0,
668
691
  inView: shown
@@ -672,7 +695,7 @@ export function Timeline({
672
695
  }
673
696
  const next = messagesInView({
674
697
  starts: current_0,
675
- scrollTop: element_4.scrollTop,
698
+ scrollTop: element_5.scrollTop,
676
699
  visibleHeight: visibleHeightRef.current
677
700
  });
678
701
  if (next?.first !== shown.first || next?.last !== shown.last) {
@@ -697,11 +720,11 @@ export function Timeline({
697
720
  openGroup: groups.add,
698
721
  openSubagent: subagents.add,
699
722
  // A row already on screen stays put; the find scrolls to the match itself once it is drawn.
700
- scrollToRow: index_3 => {
723
+ scrollToRow: index_4 => {
701
724
  const top_0 = scrollRef.current?.scrollTop ?? 0;
702
725
  const bottom = top_0 + visibleHeightRef.current;
703
- if (!virtualizer.getVirtualItems().some(virtualRow_0 => virtualRow_0.index === index_3 && virtualRow_0.end > top_0 && virtualRow_0.start < bottom)) {
704
- virtualizer.scrollToIndex(index_3, {
726
+ if (!virtualizer.getVirtualItems().some(virtualRow_0 => virtualRow_0.index === index_4 && virtualRow_0.end > top_0 && virtualRow_0.start < bottom)) {
727
+ virtualizer.scrollToIndex(index_4, {
705
728
  align: 'center'
706
729
  });
707
730
  }
@@ -719,8 +742,8 @@ export function Timeline({
719
742
  }, [searching, loadEarlier]);
720
743
  const found = useMemo(() => searching ? ticksWithHits(ticks, chatFind.hitRows) : [], [searching, ticks, chatFind.hitRows]);
721
744
  const foundCurrent = chatFind.currentRow === null ? null : tickOfRow(ticks, chatFind.currentRow);
722
- const jumpTo = index_4 => {
723
- const tick_0 = ticks[index_4];
745
+ const jumpTo = index_5 => {
746
+ const tick_0 = ticks[index_5];
724
747
  if (!tick_0) {
725
748
  return;
726
749
  }
@@ -763,10 +786,10 @@ export function Timeline({
763
786
  }
764
787
  return;
765
788
  }
766
- const index_5 = rows.findIndex(row_8 => row_8.id === seeking.itemId);
767
- if (index_5 !== -1) {
789
+ const index_6 = rows.findIndex(row_10 => row_10.id === seeking.itemId);
790
+ if (index_6 !== -1) {
768
791
  setSeeking(null);
769
- showRowAtTop(virtualizer, followRef, index_5);
792
+ showRowAtTop(virtualizer, followRef, index_6);
770
793
  if (seeking.options?.flash) {
771
794
  useTimelineFlash.getState().flash(seeking.key, seeking.itemId, false);
772
795
  }
@@ -837,9 +860,9 @@ export function Timeline({
837
860
  turnId: turnId_0,
838
861
  target: target_0
839
862
  } = revealing;
840
- const changes = target_0 === 'changes' ? rows.findIndex(row_9 => row_9.kind === 'changed-files' && row_9.turnId === turnId_0) : -1;
863
+ const changes = target_0 === 'changes' ? rows.findIndex(row_11 => row_11.kind === 'changed-files' && row_11.turnId === turnId_0) : -1;
841
864
  const opening = order?.map(id_0 => items?.[id_0]).find(item_0 => item_0?.kind === 'user' && item_0.turnId === turnId_0);
842
- const found_0 = changes !== -1 ? changes : rows.findIndex(row_10 => row_10.id === opening?.id || row_10.id === `start-${turnId_0}` || row_10.id === `fold-${turnId_0}`);
865
+ const found_0 = changes !== -1 ? changes : rows.findIndex(row_12 => row_12.id === opening?.id || row_12.id === `start-${turnId_0}` || row_12.id === `fold-${turnId_0}`);
843
866
  if (found_0 !== -1) {
844
867
  setRevealing(null);
845
868
  showRowAtTop(virtualizer, followRef, found_0);
@@ -860,7 +883,7 @@ export function Timeline({
860
883
  // The strip is memoized and draws on every scroll, so it gets one callback for its life.
861
884
  const jumpRef = useRef(jumpTo);
862
885
  jumpRef.current = jumpTo;
863
- const pick = useCallback(index_6 => jumpRef.current(index_6), []);
886
+ const pick = useCallback(index_7 => jumpRef.current(index_7), []);
864
887
  // Read when the card asks rather than on every render, so the strip's memo holds while the thread streams.
865
888
  const cardChat = useMemo(() => ({
866
889
  chatId,
@@ -868,8 +891,8 @@ export function Timeline({
868
891
  if (fork === null) {
869
892
  return false;
870
893
  }
871
- const row_11 = useChats.getState().byKey[keyOf(chatId)];
872
- return forkRefusal(row_11?.info ?? null, row_11?.structure[turnId_2]) === null;
894
+ const row_13 = useChats.getState().byKey[keyOf(chatId)];
895
+ return forkRefusal(row_13?.info ?? null, row_13?.structure[turnId_2]) === null;
873
896
  },
874
897
  fork: turnId_3 => fork?.(chatId, turnId_3)
875
898
  }), [keyOf, chatId, fork]);
@@ -878,11 +901,11 @@ export function Timeline({
878
901
  if (!onMainAgent) {
879
902
  return false;
880
903
  }
881
- const index_7 = stepMessage(starts, i => ticks[i].kind === 'person', scrollTop, direction);
882
- if (index_7 === null) {
904
+ const index_8 = stepMessage(starts, i => ticks[i].kind === 'person', scrollTop, direction);
905
+ if (index_8 === null) {
883
906
  return false;
884
907
  }
885
- jumpTo(index_7);
908
+ jumpTo(index_8);
886
909
  return true;
887
910
  };
888
911
  useEffect(() => registerMessageStepper(keyOf(chatId), direction_0 => stepRef.current(direction_0)), [keyOf, chatId]);
@@ -993,22 +1016,22 @@ export function Timeline({
993
1016
  children: /*#__PURE__*/_jsx(FindRevealContext.Provider, {
994
1017
  value: chatFind.reveal,
995
1018
  children: virtualRows.map(virtualRow_1 => {
996
- const row_12 = rows[virtualRow_1.index];
1019
+ const row_14 = rows[virtualRow_1.index];
997
1020
  return /*#__PURE__*/_jsx(ThreadRow, {
998
- row: row_12,
1021
+ row: row_14,
999
1022
  previous: virtualRow_1.index > 0 ? rows[virtualRow_1.index - 1] : null,
1000
1023
  index: virtualRow_1.index,
1001
1024
  top: virtualRow_1.start,
1002
1025
  measure: virtualizer.measureElement,
1003
1026
  chatId: chatId,
1004
- flashNonce: flash !== null && flashedRow === row_12.id ? flash.nonce : null,
1005
- bookmark: marks.get(row_12.id) ?? null,
1027
+ flashNonce: flash !== null && flashedRow === row_14.id ? flash.nonce : null,
1028
+ bookmark: marks.get(row_14.id) ?? null,
1006
1029
  toggleGroup: groups.toggle,
1007
1030
  toggleTurn: turns.toggle,
1008
1031
  toggleSubagent: subagents.toggle,
1009
1032
  openSubagent: openSubagent,
1010
1033
  openConversation: openConversation
1011
- }, row_12.id);
1034
+ }, row_14.id);
1012
1035
  })
1013
1036
  })
1014
1037
  })
@@ -9,12 +9,14 @@ import { markdownOf, messageTextOf } from '../logic/timeline-copy.js';
9
9
  import { useChatHost } from '../../host.js';
10
10
  import { useChatScope } from '../../scope.js';
11
11
  import { useChatRow } from '../../state/chats.js';
12
+ import { copyWithChips, rowClipboardChips } from './chip-clipboard.js';
12
13
  import { useFileLinkCwd, useFileLinkScopeId, useOpenFileLink } from './file-links.js';
14
+ import { useReferenceLookup } from './thread-references.js';
13
15
  import { ContextMenu, copyText, DisabledReason, EDIT_SHORTCUTS, Icon, Kbd, selectAllWithin } from '@adecore/ui';
14
16
  /* The menu behind a right-click in a thread, mostly for copying: a whole message, a code block or an answer's markdown. */
15
17
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
16
18
  export function TimelineMenuPopup(t0) {
17
- const $ = _c(70);
19
+ const $ = _c(72);
18
20
  const {
19
21
  target,
20
22
  thread,
@@ -26,10 +28,11 @@ export function TimelineMenuPopup(t0) {
26
28
  const {
27
29
  t
28
30
  } = useTranslation("agent-chat");
31
+ const row = target.row;
29
32
  let t3;
30
- if ($[0] !== target.row) {
31
- t3 = target.row === null ? null : messageTextOf(target.row);
32
- $[0] = target.row;
33
+ if ($[0] !== row) {
34
+ t3 = row === null ? null : messageTextOf(row);
35
+ $[0] = row;
33
36
  $[1] = t3;
34
37
  } else {
35
38
  t3 = $[1];
@@ -47,7 +50,7 @@ export function TimelineMenuPopup(t0) {
47
50
  const turnId = t4;
48
51
  let t5;
49
52
  if ($[5] !== turnId) {
50
- t5 = row => turnId === null ? null : forkRefusal(row?.info ?? null, row?.structure[turnId]);
53
+ t5 = row_0 => turnId === null ? null : forkRefusal(row_0?.info ?? null, row_0?.structure[turnId]);
51
54
  $[5] = turnId;
52
55
  $[6] = t5;
53
56
  } else {
@@ -66,6 +69,7 @@ export function TimelineMenuPopup(t0) {
66
69
  const markable = chatId !== null && (target.row?.kind === "user" || target.row?.kind === "assistant") ? target.row.id : null;
67
70
  const cwd = useFileLinkCwd();
68
71
  const scopeId = useFileLinkScopeId();
72
+ const lookup = useReferenceLookup(chatId ?? "");
69
73
  const {
70
74
  fork,
71
75
  fileLinks
@@ -150,22 +154,24 @@ export function TimelineMenuPopup(t0) {
150
154
  t13 = $[25];
151
155
  }
152
156
  let t14;
153
- if ($[26] !== message || $[27] !== t) {
154
- t14 = message !== null && /*#__PURE__*/_jsxs(ContextMenu.Item, {
155
- onClick: () => copyText(message),
157
+ if ($[26] !== lookup || $[27] !== message || $[28] !== row || $[29] !== t) {
158
+ t14 = row !== null && message !== null && /*#__PURE__*/_jsxs(ContextMenu.Item, {
159
+ onClick: () => copyWithChips(rowClipboardChips(row, message, lookup)),
156
160
  children: [/*#__PURE__*/_jsx(Icon, {
157
161
  icon: MessageSquare,
158
162
  size: 14
159
163
  }), " ", t("timeline.menu.copyMessage")]
160
164
  });
161
- $[26] = message;
162
- $[27] = t;
163
- $[28] = t14;
165
+ $[26] = lookup;
166
+ $[27] = message;
167
+ $[28] = row;
168
+ $[29] = t;
169
+ $[30] = t14;
164
170
  } else {
165
- t14 = $[28];
171
+ t14 = $[30];
166
172
  }
167
173
  let t15;
168
- if ($[29] !== t || $[30] !== target.code) {
174
+ if ($[31] !== t || $[32] !== target.code) {
169
175
  t15 = target.code !== null && /*#__PURE__*/_jsxs(ContextMenu.Item, {
170
176
  onClick: () => copyText(target.code ?? ""),
171
177
  children: [/*#__PURE__*/_jsx(Icon, {
@@ -173,14 +179,14 @@ export function TimelineMenuPopup(t0) {
173
179
  size: 14
174
180
  }), " ", t("timeline.menu.copyCode")]
175
181
  });
176
- $[29] = t;
177
- $[30] = target.code;
178
- $[31] = t15;
182
+ $[31] = t;
183
+ $[32] = target.code;
184
+ $[33] = t15;
179
185
  } else {
180
- t15 = $[31];
186
+ t15 = $[33];
181
187
  }
182
188
  let t16;
183
- if ($[32] !== markdown || $[33] !== t) {
189
+ if ($[34] !== markdown || $[35] !== t) {
184
190
  t16 = markdown !== null && /*#__PURE__*/_jsxs(ContextMenu.Item, {
185
191
  onClick: () => copyText(markdown),
186
192
  children: [/*#__PURE__*/_jsx(Icon, {
@@ -188,14 +194,14 @@ export function TimelineMenuPopup(t0) {
188
194
  size: 14
189
195
  }), " ", t("timeline.menu.copyMarkdown")]
190
196
  });
191
- $[32] = markdown;
192
- $[33] = t;
193
- $[34] = t16;
197
+ $[34] = markdown;
198
+ $[35] = t;
199
+ $[36] = t16;
194
200
  } else {
195
- t16 = $[34];
201
+ t16 = $[36];
196
202
  }
197
203
  let t17;
198
- if ($[35] !== chatId || $[36] !== fork || $[37] !== forkBlocked || $[38] !== t || $[39] !== turnId) {
204
+ if ($[37] !== chatId || $[38] !== fork || $[39] !== forkBlocked || $[40] !== t || $[41] !== turnId) {
199
205
  t17 = chatId !== null && turnId !== null && fork !== null && /*#__PURE__*/_jsx(DisabledReason, {
200
206
  reason: forkBlocked,
201
207
  children: /*#__PURE__*/_jsxs(ContextMenu.Item, {
@@ -207,74 +213,74 @@ export function TimelineMenuPopup(t0) {
207
213
  }), " ", t("timeline.menu.forkFromHere")]
208
214
  })
209
215
  });
210
- $[35] = chatId;
211
- $[36] = fork;
212
- $[37] = forkBlocked;
213
- $[38] = t;
214
- $[39] = turnId;
215
- $[40] = t17;
216
+ $[37] = chatId;
217
+ $[38] = fork;
218
+ $[39] = forkBlocked;
219
+ $[40] = t;
220
+ $[41] = turnId;
221
+ $[42] = t17;
216
222
  } else {
217
- t17 = $[40];
223
+ t17 = $[42];
218
224
  }
219
225
  let t18;
220
- if ($[41] !== chatId || $[42] !== markable) {
226
+ if ($[43] !== chatId || $[44] !== markable) {
221
227
  t18 = chatId !== null && markable !== null && /*#__PURE__*/_jsx(BookmarkMenuItems, {
222
228
  chatId: chatId,
223
229
  itemId: markable
224
230
  });
225
- $[41] = chatId;
226
- $[42] = markable;
227
- $[43] = t18;
231
+ $[43] = chatId;
232
+ $[44] = markable;
233
+ $[45] = t18;
228
234
  } else {
229
- t18 = $[43];
235
+ t18 = $[45];
230
236
  }
231
237
  let t19;
232
- if ($[44] === Symbol.for("react.memo_cache_sentinel")) {
238
+ if ($[46] === Symbol.for("react.memo_cache_sentinel")) {
233
239
  t19 = /*#__PURE__*/_jsx(ContextMenu.Separator, {});
234
- $[44] = t19;
240
+ $[46] = t19;
235
241
  } else {
236
- t19 = $[44];
242
+ t19 = $[46];
237
243
  }
238
244
  let t20;
239
- if ($[45] !== thread.current) {
245
+ if ($[47] !== thread.current) {
240
246
  t20 = () => selectAllWithin(thread.current);
241
- $[45] = thread.current;
242
- $[46] = t20;
247
+ $[47] = thread.current;
248
+ $[48] = t20;
243
249
  } else {
244
- t20 = $[46];
250
+ t20 = $[48];
245
251
  }
246
252
  let t21;
247
- if ($[47] === Symbol.for("react.memo_cache_sentinel")) {
253
+ if ($[49] === Symbol.for("react.memo_cache_sentinel")) {
248
254
  t21 = /*#__PURE__*/_jsx(Icon, {
249
255
  icon: Scan,
250
256
  size: 14
251
257
  });
252
- $[47] = t21;
258
+ $[49] = t21;
253
259
  } else {
254
- t21 = $[47];
260
+ t21 = $[49];
255
261
  }
256
262
  let t22;
257
- if ($[48] !== t) {
263
+ if ($[50] !== t) {
258
264
  t22 = t("common.action.selectAll");
259
- $[48] = t;
260
- $[49] = t22;
265
+ $[50] = t;
266
+ $[51] = t22;
261
267
  } else {
262
- t22 = $[49];
268
+ t22 = $[51];
263
269
  }
264
270
  let t23;
265
- if ($[50] !== t20 || $[51] !== t22) {
271
+ if ($[52] !== t20 || $[53] !== t22) {
266
272
  t23 = /*#__PURE__*/_jsxs(ContextMenu.Item, {
267
273
  onClick: t20,
268
274
  children: [t21, " ", t22]
269
275
  });
270
- $[50] = t20;
271
- $[51] = t22;
272
- $[52] = t23;
276
+ $[52] = t20;
277
+ $[53] = t22;
278
+ $[54] = t23;
273
279
  } else {
274
- t23 = $[52];
280
+ t23 = $[54];
275
281
  }
276
282
  let t24;
277
- if ($[53] !== fileLinks || $[54] !== openInPreview || $[55] !== t || $[56] !== target.path) {
283
+ if ($[55] !== fileLinks || $[56] !== openInPreview || $[57] !== t || $[58] !== target.path) {
278
284
  t24 = target.path !== null && fileLinks !== null && /*#__PURE__*/_jsxs(_Fragment, {
279
285
  children: [/*#__PURE__*/_jsx(ContextMenu.Separator, {}), /*#__PURE__*/_jsxs(ContextMenu.Item, {
280
286
  onClick: openInPreview,
@@ -284,41 +290,41 @@ export function TimelineMenuPopup(t0) {
284
290
  }), " ", t("timeline.menu.openInPreview")]
285
291
  })]
286
292
  });
287
- $[53] = fileLinks;
288
- $[54] = openInPreview;
289
- $[55] = t;
290
- $[56] = target.path;
291
- $[57] = t24;
293
+ $[55] = fileLinks;
294
+ $[56] = openInPreview;
295
+ $[57] = t;
296
+ $[58] = target.path;
297
+ $[59] = t24;
292
298
  } else {
293
- t24 = $[57];
299
+ t24 = $[59];
294
300
  }
295
301
  let t25;
296
- if ($[58] !== column) {
302
+ if ($[60] !== column) {
297
303
  t25 = column && /*#__PURE__*/_jsxs(_Fragment, {
298
304
  children: [/*#__PURE__*/_jsx(ContextMenu.Separator, {}), /*#__PURE__*/_jsx(ReadingWidthSubmenu, {})]
299
305
  });
300
- $[58] = column;
301
- $[59] = t25;
306
+ $[60] = column;
307
+ $[61] = t25;
302
308
  } else {
303
- t25 = $[59];
309
+ t25 = $[61];
304
310
  }
305
311
  let t26;
306
- if ($[60] !== t13 || $[61] !== t14 || $[62] !== t15 || $[63] !== t16 || $[64] !== t17 || $[65] !== t18 || $[66] !== t23 || $[67] !== t24 || $[68] !== t25) {
312
+ if ($[62] !== t13 || $[63] !== t14 || $[64] !== t15 || $[65] !== t16 || $[66] !== t17 || $[67] !== t18 || $[68] !== t23 || $[69] !== t24 || $[70] !== t25) {
307
313
  t26 = /*#__PURE__*/_jsxs(ContextMenu.Popup, {
308
314
  children: [t13, t14, t15, t16, t17, t18, t19, t23, t24, t25]
309
315
  });
310
- $[60] = t13;
311
- $[61] = t14;
312
- $[62] = t15;
313
- $[63] = t16;
314
- $[64] = t17;
315
- $[65] = t18;
316
- $[66] = t23;
317
- $[67] = t24;
318
- $[68] = t25;
319
- $[69] = t26;
320
- } else {
321
- t26 = $[69];
316
+ $[62] = t13;
317
+ $[63] = t14;
318
+ $[64] = t15;
319
+ $[65] = t16;
320
+ $[66] = t17;
321
+ $[67] = t18;
322
+ $[68] = t23;
323
+ $[69] = t24;
324
+ $[70] = t25;
325
+ $[71] = t26;
326
+ } else {
327
+ t26 = $[71];
322
328
  }
323
329
  return t26;
324
330
  }
@@ -0,0 +1,9 @@
1
+ import type { ChatAssistantItem, ChatUserItem } from '@adecore/agent-contracts';
2
+ import { type ClipboardChips } from '../mentions.js';
3
+ import type { TimelineRow } from '../logic/timeline.js';
4
+ import type { ReferenceLookup } from './reference-lookup.js';
5
+ export declare function messageClipboardChips(item: ChatUserItem | ChatAssistantItem, text: string, lookup: Pick<ReferenceLookup, 'items' | 'referenced'>): ClipboardChips;
6
+ export declare function rowClipboardChips(row: TimelineRow, text: string, lookup: Pick<ReferenceLookup, 'items' | 'referenced'>): ClipboardChips;
7
+ export declare function selectionChips(selection: Selection): ClipboardChips;
8
+ export declare function copySelectionWithChips(event: ClipboardEvent): void;
9
+ export declare function copyWithChips(chips: ClipboardChips): void;
@@ -0,0 +1,105 @@
1
+ import { copyText } from '@adecore/ui';
2
+ import { referenceTokens } from '../chat-references.js';
3
+ import { clipboardChipsOf, hasClipboardChips, tokenizeChips, tokenizeNamedReferences, writeClipboardChips } from '../mentions.js';
4
+ /* The chips a copy of a whole message carries: what the person picked, or what a reply names. */
5
+ export function messageClipboardChips(item, text, lookup) {
6
+ if (item.kind === 'assistant') {
7
+ return clipboardChipsOf(text, tokenizeNamedReferences(text, lookup.items, lookup.referenced));
8
+ }
9
+ const tokens = referenceTokens([...(item.chats ?? []), ...(item.references ?? [])], lookup.items);
10
+ return clipboardChipsOf(text, tokenizeChips(text, item.mentions ?? [], item.skills ?? [], tokens));
11
+ }
12
+ /* The chips a copy of a row of the thread carries; only a message has any. */
13
+ export function rowClipboardChips(row, text, lookup) {
14
+ if (row.kind === 'user' || row.kind === 'assistant') {
15
+ return messageClipboardChips(row.item, text, lookup);
16
+ }
17
+ return clipboardChipsOf(text, []);
18
+ }
19
+ /* What a chip the transcript drew stands for, read back from the attributes `TokenChip` writes. */
20
+ function segmentOf(chip) {
21
+ const value = chip.getAttribute('data-chip-value');
22
+ if (value === null) {
23
+ return null;
24
+ }
25
+ switch (chip.getAttribute('data-chip')) {
26
+ case 'mention':
27
+ return {
28
+ kind: 'mention',
29
+ path: value
30
+ };
31
+ case 'skill':
32
+ return {
33
+ kind: 'skill',
34
+ name: value
35
+ };
36
+ case 'reference':
37
+ {
38
+ const name = chip.getAttribute('data-chip-name');
39
+ return name === null || name === '' ? null : {
40
+ kind: 'reference',
41
+ id: value,
42
+ name
43
+ };
44
+ }
45
+ default:
46
+ return null;
47
+ }
48
+ }
49
+ /* The chips a selection holds whole, beside the text it copies; a chip cut in half is only text. */
50
+ export function selectionChips(selection) {
51
+ const segments = [];
52
+ for (let i = 0; i < selection.rangeCount; i++) {
53
+ const container = selection.getRangeAt(i).commonAncestorContainer;
54
+ const root = container instanceof Element ? container : container.parentElement;
55
+ const own = root?.closest('[data-chip]');
56
+ const candidates = own ? [own] : [...(root?.querySelectorAll('[data-chip]') ?? [])];
57
+ for (const chip of candidates) {
58
+ const segment = selection.containsNode(chip, false) ? segmentOf(chip) : null;
59
+ if (segment !== null) {
60
+ segments.push(segment);
61
+ }
62
+ }
63
+ }
64
+ return clipboardChipsOf(selection.toString(), segments);
65
+ }
66
+ /* A copy of a selection in the thread, which carries the chips in it as well when it holds any. */
67
+ export function copySelectionWithChips(event) {
68
+ const selection = document.getSelection();
69
+ if (event.defaultPrevented || event.clipboardData === null || selection === null || selection.isCollapsed) {
70
+ return;
71
+ }
72
+ const chips = selectionChips(selection);
73
+ if (!hasClipboardChips(chips)) {
74
+ return;
75
+ }
76
+ writeClipboardChips(event.clipboardData, chips);
77
+ event.preventDefault();
78
+ }
79
+ /*
80
+ * Copies a text and its chips. The asynchronous clipboard writes no type of an app's own that a paste
81
+ * can read, so the copy goes through a copy event, which only a page with the user's gesture gets.
82
+ */
83
+ export function copyWithChips(chips) {
84
+ if (!hasClipboardChips(chips) || typeof document.execCommand !== 'function') {
85
+ void copyText(chips.text);
86
+ return;
87
+ }
88
+ let written = false;
89
+ const write = event => {
90
+ if (event.clipboardData !== null) {
91
+ writeClipboardChips(event.clipboardData, chips);
92
+ event.preventDefault();
93
+ written = true;
94
+ }
95
+ };
96
+ document.addEventListener('copy', write, true);
97
+ try {
98
+ document.execCommand('copy');
99
+ } finally {
100
+ document.removeEventListener('copy', write, true);
101
+ }
102
+ if (!written) {
103
+ void copyText(chips.text);
104
+ }
105
+ }
@@ -1,4 +1,9 @@
1
1
  export declare const MENTION_TONE = "bg-accent-soft text-accent shadow-[inset_0_0_0_1px_color-mix(in_srgb,var(--accent)_25%,transparent)]";
2
2
  export declare const SKILL_TONE = "bg-skill-soft text-skill shadow-[inset_0_0_0_1px_color-mix(in_srgb,var(--skill)_25%,transparent)]";
3
+ export declare const REFERENCE_TONE = "bg-text/8 text-text shadow-[inset_0_0_0_1px_var(--border-strong)]";
4
+ export declare const REFERENCE_GONE_TONE = "bg-text/4 text-text-faint shadow-[inset_0_0_0_1px_var(--border)]";
3
5
  export declare const CHIP_IN_EDITOR = "box-decoration-clone rounded-md px-1 py-0.5";
6
+ export declare const TOKEN_IN_MESSAGE = "box-decoration-clone rounded-md px-1.5 py-0.5 text-xs font-medium";
7
+ export declare const TOKEN_ICON = "mr-1 inline align-[calc(0.5cap-7px)]!";
8
+ export declare const TOKEN_ICON_IN_EDITOR = "mr-1 inline-block align-[calc(0.5cap-6px)]";
4
9
  export declare const CHIP_IN_MESSAGE = "inline-flex h-5 max-w-full items-center gap-1 rounded-md px-1.5 align-[-3px] text-xs/none font-medium";
@@ -1,11 +1,25 @@
1
- /* An `@file` or `$skill` token drawn as a pill: the tone says which of the two it is, the shape
1
+ /* An `@file`, `$skill` or `#reference` token drawn as a pill: the tone says which it is, the shape
2
2
  says where it is drawn. */
3
3
  export const MENTION_TONE = 'bg-accent-soft text-accent shadow-[inset_0_0_0_1px_color-mix(in_srgb,var(--accent)_25%,transparent)]';
4
4
  /* A skill is the same token shape as a file mention, in its own hue. */
5
5
  export const SKILL_TONE = 'bg-skill-soft text-skill shadow-[inset_0_0_0_1px_color-mix(in_srgb,var(--skill)_25%,transparent)]';
6
+ /* A `#` to a view, node or chat is neutral, so it never reads as a file or a skill. */
7
+ export const REFERENCE_TONE = 'bg-text/8 text-text shadow-[inset_0_0_0_1px_var(--border-strong)]';
8
+ /* A `#` whose view or node is no longer in the project. */
9
+ export const REFERENCE_GONE_TONE = 'bg-text/4 text-text-faint shadow-[inset_0_0_0_1px_var(--border)]';
6
10
  /* The pill in the composer's editor, around the token's own characters: the editor places the caret
7
11
  from what it drew, so padding is safe, and a chip that wraps keeps it on every fragment. */
8
12
  export const CHIP_IN_EDITOR = 'box-decoration-clone rounded-md px-1 py-0.5';
13
+ /* A picked or named token in a message or a reply, a shade smaller than the text around it. Inline like
14
+ the editor's pill, so its name sits on the baseline of the sentence, and a selection copies it on the
15
+ same line as the words around it, where a flex box would put it on lines of its own. */
16
+ export const TOKEN_IN_MESSAGE = 'box-decoration-clone rounded-md px-1.5 py-0.5 text-xs font-medium';
17
+ /* The mark in place of the sigil, a 14px icon. Inline, since the base styles make an svg a block of its
18
+ own. Its middle goes on the middle of the name's capitals, where a mark reads as centered; the middle
19
+ of the x-height `Icon` aligns on sits low next to a capital. */
20
+ export const TOKEN_ICON = 'mr-1 inline align-[calc(0.5cap-7px)]!';
21
+ /* The same middle for the 12px mark the composer draws, in a box whose bottom edge is its baseline. */
22
+ export const TOKEN_ICON_IN_EDITOR = 'mr-1 inline-block align-[calc(0.5cap-6px)]';
9
23
  /* The same pill in the transcript: a real inline box a shade smaller than the text around it, with
10
24
  the icon in place of the sigil. */
11
25
  export const CHIP_IN_MESSAGE = 'inline-flex h-5 max-w-full items-center gap-1 rounded-md px-1.5 align-[-3px] text-xs/none font-medium';