@ahggg/dsh-side-chat 0.7.2 → 1.0.0

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 (185) hide show
  1. package/README.md +16 -13
  2. package/README.zh-CN.md +16 -13
  3. package/lib/client/apply.d.ts +0 -1
  4. package/lib/client/apply.js +48 -33
  5. package/lib/client/contracts.d.ts +3 -59
  6. package/lib/client/contracts.js +0 -1
  7. package/lib/client/dsh/ReadOnlyConversation.d.ts +10 -0
  8. package/lib/client/dsh/ReadOnlyConversation.js +91 -0
  9. package/lib/client/dsh/SideChatOverlay.d.ts +7 -0
  10. package/lib/client/{rc6/Rc6SideChatOverlay.js → dsh/SideChatOverlay.js} +26 -57
  11. package/lib/client/{rc6 → dsh}/context.d.ts +7 -3
  12. package/lib/client/dsh/context.js +1 -0
  13. package/lib/client/dsh/conversation-store.d.ts +22 -0
  14. package/lib/client/dsh/conversation-store.js +19 -0
  15. package/lib/client/dsh/primitive-labels.d.ts +2 -0
  16. package/lib/client/dsh/primitive-labels.js +4 -0
  17. package/lib/client/{rc6 → dsh}/remote-adapter.d.ts +2 -3
  18. package/lib/client/dsh/remote-adapter.js +22 -0
  19. package/lib/client/dsh/sessions-adapter.d.ts +56 -0
  20. package/lib/client/dsh/sessions-adapter.js +185 -0
  21. package/lib/client/index.d.ts +2 -3
  22. package/lib/client/index.js +0 -1
  23. package/lib/client/model-preference.d.ts +0 -1
  24. package/lib/client/model-preference.js +0 -1
  25. package/lib/client/panel/SelectionQuote.d.ts +0 -1
  26. package/lib/client/panel/SelectionQuote.js +8 -2
  27. package/lib/client/panel/SendIcon.d.ts +0 -1
  28. package/lib/client/panel/SendIcon.js +0 -1
  29. package/lib/client/panel/SideChatBody.d.ts +0 -1
  30. package/lib/client/panel/SideChatBody.js +0 -1
  31. package/lib/client/panel/SideChatErrorState.d.ts +0 -1
  32. package/lib/client/panel/SideChatErrorState.js +1 -2
  33. package/lib/client/panel/SideChatHeader.d.ts +0 -1
  34. package/lib/client/panel/SideChatHeader.js +0 -1
  35. package/lib/client/panel/SideChatModelSelect.d.ts +0 -1
  36. package/lib/client/panel/SideChatModelSelect.js +37 -19
  37. package/lib/client/panel/SideChatPanel.d.ts +0 -1
  38. package/lib/client/panel/SideChatPanel.js +12 -6
  39. package/lib/client/panel/StopIcon.d.ts +0 -1
  40. package/lib/client/panel/StopIcon.js +0 -1
  41. package/lib/client/panel/messages.d.ts +1 -2
  42. package/lib/client/panel/messages.js +8 -9
  43. package/lib/client/panel/use-auto-growing-textarea.d.ts +0 -1
  44. package/lib/client/panel/use-auto-growing-textarea.js +0 -1
  45. package/lib/client/parent-composer/ParentConversationAnnotations.d.ts +3 -3
  46. package/lib/client/parent-composer/ParentConversationAnnotations.js +71 -28
  47. package/lib/client/parent-composer/add-to-conversation.d.ts +14 -29
  48. package/lib/client/parent-composer/add-to-conversation.js +134 -457
  49. package/lib/client/parent-composer/annotation-persistence.d.ts +8 -9
  50. package/lib/client/parent-composer/annotation-persistence.js +106 -103
  51. package/lib/client/parent-composer/composer-reference.d.ts +19 -48
  52. package/lib/client/parent-composer/composer-reference.js +26 -46
  53. package/lib/client/parent-composer/referenced-conversation.d.ts +5 -8
  54. package/lib/client/parent-composer/referenced-conversation.js +27 -61
  55. package/lib/client/selection/ConversationAnnotationMarkers.d.ts +0 -1
  56. package/lib/client/selection/ConversationAnnotationMarkers.js +5 -2
  57. package/lib/client/selection/SelectionActions.d.ts +0 -1
  58. package/lib/client/selection/SelectionActions.js +2 -2
  59. package/lib/client/selection/conversation-dom.d.ts +5 -0
  60. package/lib/client/selection/conversation-dom.js +24 -0
  61. package/lib/client/selection/selection-controller.d.ts +1 -2
  62. package/lib/client/selection/selection-controller.js +7 -4
  63. package/lib/client/selection/selection-limits.d.ts +0 -1
  64. package/lib/client/selection/selection-limits.js +0 -1
  65. package/lib/client/selection/selection-normalizer.d.ts +0 -1
  66. package/lib/client/selection/selection-normalizer.js +1 -2
  67. package/lib/client/side-chat-controller.d.ts +28 -42
  68. package/lib/client/side-chat-controller.js +245 -338
  69. package/lib/client.js +1202 -8951
  70. package/lib/host/index.d.ts +1 -2
  71. package/lib/host/index.js +1 -2
  72. package/lib/host/read-only-chat-service.d.ts +30 -0
  73. package/lib/host/read-only-chat-service.js +314 -0
  74. package/lib/index.d.ts +16 -9
  75. package/lib/index.js +34 -14
  76. package/lib/remote.d.ts +23 -15
  77. package/lib/remote.js +2 -6
  78. package/lib/shared/constants.d.ts +0 -14
  79. package/lib/shared/constants.js +0 -14
  80. package/lib/shared/contracts.d.ts +52 -20
  81. package/lib/shared/contracts.js +2 -4
  82. package/lib/shared/error-codes.d.ts +1 -2
  83. package/lib/shared/error-codes.js +2 -3
  84. package/lib/shared/observable.d.ts +0 -1
  85. package/lib/shared/observable.js +0 -1
  86. package/lib/shared/{archived-wire.d.ts → side-chat-wire.d.ts} +109 -21
  87. package/lib/shared/side-chat-wire.js +34 -0
  88. package/lib/typert.d.ts +3 -69
  89. package/lib/typert.js +120 -51
  90. package/package.json +80 -38
  91. package/lib/client/apply.d.ts.map +0 -1
  92. package/lib/client/apply.js.map +0 -1
  93. package/lib/client/contracts.d.ts.map +0 -1
  94. package/lib/client/contracts.js.map +0 -1
  95. package/lib/client/index.d.ts.map +0 -1
  96. package/lib/client/index.js.map +0 -1
  97. package/lib/client/model-preference.d.ts.map +0 -1
  98. package/lib/client/model-preference.js.map +0 -1
  99. package/lib/client/panel/SelectionQuote.d.ts.map +0 -1
  100. package/lib/client/panel/SelectionQuote.js.map +0 -1
  101. package/lib/client/panel/SendIcon.d.ts.map +0 -1
  102. package/lib/client/panel/SendIcon.js.map +0 -1
  103. package/lib/client/panel/SideChatBody.d.ts.map +0 -1
  104. package/lib/client/panel/SideChatBody.js.map +0 -1
  105. package/lib/client/panel/SideChatErrorState.d.ts.map +0 -1
  106. package/lib/client/panel/SideChatErrorState.js.map +0 -1
  107. package/lib/client/panel/SideChatHeader.d.ts.map +0 -1
  108. package/lib/client/panel/SideChatHeader.js.map +0 -1
  109. package/lib/client/panel/SideChatModelSelect.d.ts.map +0 -1
  110. package/lib/client/panel/SideChatModelSelect.js.map +0 -1
  111. package/lib/client/panel/SideChatPanel.d.ts.map +0 -1
  112. package/lib/client/panel/SideChatPanel.js.map +0 -1
  113. package/lib/client/panel/StopIcon.d.ts.map +0 -1
  114. package/lib/client/panel/StopIcon.js.map +0 -1
  115. package/lib/client/panel/messages.d.ts.map +0 -1
  116. package/lib/client/panel/messages.js.map +0 -1
  117. package/lib/client/panel/use-auto-growing-textarea.d.ts.map +0 -1
  118. package/lib/client/panel/use-auto-growing-textarea.js.map +0 -1
  119. package/lib/client/parent-composer/ParentConversationAnnotations.d.ts.map +0 -1
  120. package/lib/client/parent-composer/ParentConversationAnnotations.js.map +0 -1
  121. package/lib/client/parent-composer/add-to-conversation.d.ts.map +0 -1
  122. package/lib/client/parent-composer/add-to-conversation.js.map +0 -1
  123. package/lib/client/parent-composer/annotation-persistence.d.ts.map +0 -1
  124. package/lib/client/parent-composer/annotation-persistence.js.map +0 -1
  125. package/lib/client/parent-composer/composer-reference.d.ts.map +0 -1
  126. package/lib/client/parent-composer/composer-reference.js.map +0 -1
  127. package/lib/client/parent-composer/referenced-conversation.d.ts.map +0 -1
  128. package/lib/client/parent-composer/referenced-conversation.js.map +0 -1
  129. package/lib/client/rc6/ArchivedConversation.d.ts +0 -15
  130. package/lib/client/rc6/ArchivedConversation.d.ts.map +0 -1
  131. package/lib/client/rc6/ArchivedConversation.js +0 -230
  132. package/lib/client/rc6/ArchivedConversation.js.map +0 -1
  133. package/lib/client/rc6/Rc6SideChatOverlay.d.ts +0 -8
  134. package/lib/client/rc6/Rc6SideChatOverlay.d.ts.map +0 -1
  135. package/lib/client/rc6/Rc6SideChatOverlay.js.map +0 -1
  136. package/lib/client/rc6/SideChatTool.d.ts +0 -23
  137. package/lib/client/rc6/SideChatTool.d.ts.map +0 -1
  138. package/lib/client/rc6/SideChatTool.js +0 -395
  139. package/lib/client/rc6/SideChatTool.js.map +0 -1
  140. package/lib/client/rc6/context.d.ts.map +0 -1
  141. package/lib/client/rc6/context.js +0 -2
  142. package/lib/client/rc6/context.js.map +0 -1
  143. package/lib/client/rc6/remote-adapter.d.ts.map +0 -1
  144. package/lib/client/rc6/remote-adapter.js +0 -33
  145. package/lib/client/rc6/remote-adapter.js.map +0 -1
  146. package/lib/client/rc6/sessions-adapter.d.ts +0 -61
  147. package/lib/client/rc6/sessions-adapter.d.ts.map +0 -1
  148. package/lib/client/rc6/sessions-adapter.js +0 -370
  149. package/lib/client/rc6/sessions-adapter.js.map +0 -1
  150. package/lib/client/selection/ConversationAnnotationMarkers.d.ts.map +0 -1
  151. package/lib/client/selection/ConversationAnnotationMarkers.js.map +0 -1
  152. package/lib/client/selection/SelectionActions.d.ts.map +0 -1
  153. package/lib/client/selection/SelectionActions.js.map +0 -1
  154. package/lib/client/selection/selection-controller.d.ts.map +0 -1
  155. package/lib/client/selection/selection-controller.js.map +0 -1
  156. package/lib/client/selection/selection-limits.d.ts.map +0 -1
  157. package/lib/client/selection/selection-limits.js.map +0 -1
  158. package/lib/client/selection/selection-normalizer.d.ts.map +0 -1
  159. package/lib/client/selection/selection-normalizer.js.map +0 -1
  160. package/lib/client/side-chat-controller.d.ts.map +0 -1
  161. package/lib/client/side-chat-controller.js.map +0 -1
  162. package/lib/client.js.map +0 -1
  163. package/lib/host/archived-fork-service.d.ts +0 -29
  164. package/lib/host/archived-fork-service.d.ts.map +0 -1
  165. package/lib/host/archived-fork-service.js +0 -264
  166. package/lib/host/archived-fork-service.js.map +0 -1
  167. package/lib/host/index.d.ts.map +0 -1
  168. package/lib/host/index.js.map +0 -1
  169. package/lib/index.d.ts.map +0 -1
  170. package/lib/index.js.map +0 -1
  171. package/lib/remote.d.ts.map +0 -1
  172. package/lib/remote.js.map +0 -1
  173. package/lib/shared/archived-wire.d.ts.map +0 -1
  174. package/lib/shared/archived-wire.js +0 -48
  175. package/lib/shared/archived-wire.js.map +0 -1
  176. package/lib/shared/constants.d.ts.map +0 -1
  177. package/lib/shared/constants.js.map +0 -1
  178. package/lib/shared/contracts.d.ts.map +0 -1
  179. package/lib/shared/contracts.js.map +0 -1
  180. package/lib/shared/error-codes.d.ts.map +0 -1
  181. package/lib/shared/error-codes.js.map +0 -1
  182. package/lib/shared/observable.d.ts.map +0 -1
  183. package/lib/shared/observable.js.map +0 -1
  184. package/lib/typert.d.ts.map +0 -1
  185. package/lib/typert.js.map +0 -1
@@ -1,38 +1,19 @@
1
- import { draftWithoutOccurrence, LEGACY_REFERENCE_PLACEHOLDER, newlyInsertedOccurrence, occurrenceMatchesDraft, occurrenceRange, referenceDisplayText, } from './composer-reference.js';
2
- const SELECTION_REFERENCE_SOURCE = 'dsh-side-chat-selection';
1
+ import { newlyInsertedOccurrence, occurrenceEditSpan, occurrenceMatchesDraft } from './composer-reference.js';
2
+ export const SELECTION_REFERENCE_SOURCE = 'dsh-side-chat-selection';
3
3
  export const SELECTION_REFERENCE_LABEL = '__dsh_side_chat_annotations__';
4
4
  function escapeXmlText(value) {
5
- return value
6
- .replaceAll('&', '&')
7
- .replaceAll('<', '&lt;')
8
- .replaceAll('>', '&gt;');
5
+ return value.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;');
9
6
  }
10
- function annotationFromSelection(selection, comment) {
11
- const trimmedComment = comment?.trim();
12
- return {
13
- text: selection.text,
14
- selection,
15
- ...(trimmedComment === undefined || trimmedComment.length === 0
16
- ? {}
17
- : { comment: trimmedComment }),
18
- };
19
- }
20
- function encodeSelectionReference(annotations) {
21
- return JSON.stringify({
22
- version: 2,
23
- annotations,
24
- });
7
+ export function unescapeXmlText(value) {
8
+ return value.replaceAll('&lt;', '<').replaceAll('&gt;', '>').replaceAll('&amp;', '&');
25
9
  }
26
10
  function isSelection(value) {
27
11
  if (typeof value !== 'object' || value === null)
28
12
  return false;
29
13
  const selection = value;
30
- if (typeof selection.parentSessionId !== 'string'
31
- || typeof selection.text !== 'string'
32
- || !Number.isSafeInteger(selection.atSeq)
33
- || !Array.isArray(selection.fragments)
34
- || typeof selection.rect !== 'object'
35
- || selection.rect === null)
14
+ if (typeof selection.parentSessionId !== 'string' || typeof selection.text !== 'string'
15
+ || !Number.isSafeInteger(selection.atSeq) || !Array.isArray(selection.fragments)
16
+ || typeof selection.rect !== 'object' || selection.rect === null)
36
17
  return false;
37
18
  const rect = selection.rect;
38
19
  if (![rect.x, rect.y, rect.width, rect.height, rect.viewportWidth, rect.viewportHeight]
@@ -42,79 +23,38 @@ function isSelection(value) {
42
23
  if (typeof candidate !== 'object' || candidate === null)
43
24
  return false;
44
25
  const fragment = candidate;
45
- return typeof fragment.nodeKey === 'string'
46
- && typeof fragment.nodeKind === 'string'
47
- && typeof fragment.turnKey === 'string'
48
- && Number.isSafeInteger(fragment.seq)
49
- && Number.isSafeInteger(fragment.startOffset)
50
- && Number.isSafeInteger(fragment.endOffset)
26
+ return typeof fragment.nodeKey === 'string' && typeof fragment.nodeKind === 'string'
27
+ && typeof fragment.turnKey === 'string' && Number.isSafeInteger(fragment.seq)
28
+ && Number.isSafeInteger(fragment.startOffset) && Number.isSafeInteger(fragment.endOffset)
51
29
  && typeof fragment.text === 'string'
52
30
  && ['user', 'assistant', 'context', 'code'].includes(fragment.source ?? '')
53
- && typeof fragment.modelVisible === 'boolean'
54
- && typeof fragment.settled === 'boolean';
31
+ && typeof fragment.modelVisible === 'boolean' && typeof fragment.settled === 'boolean';
55
32
  });
56
33
  }
57
- function isStoredAnnotation(value) {
58
- if (typeof value !== 'object' || value === null)
59
- return false;
60
- const annotation = value;
61
- return typeof annotation.text === 'string'
62
- && (annotation.comment === undefined || typeof annotation.comment === 'string')
63
- && (annotation.selection === undefined || isSelection(annotation.selection));
64
- }
65
- function visibleAnnotation(annotation) {
66
- return {
67
- text: annotation.text,
68
- ...(annotation.comment === undefined ? {} : { comment: annotation.comment }),
69
- };
70
- }
71
34
  function decodeStoredSelectionReference(ref) {
72
- const value = JSON.parse(ref);
73
- // Accept the one-selection payload written by the first Add-to-chat build.
74
- if (isStoredAnnotation(value))
75
- return [value];
76
- if (typeof value !== 'object' || value === null) {
77
- throw new Error('The selected conversation annotation is no longer valid.');
78
- }
79
- const payload = value;
80
- if ((payload.version !== 1 && payload.version !== 2)
81
- || !Array.isArray(payload.annotations)
82
- || payload.annotations.length === 0
83
- || !payload.annotations.every(isStoredAnnotation)) {
35
+ const payload = JSON.parse(ref);
36
+ if (payload === null || payload.version !== 2 || !Array.isArray(payload.annotations)
37
+ || payload.annotations.length === 0 || !payload.annotations.every((value) => {
38
+ if (typeof value !== 'object' || value === null)
39
+ return false;
40
+ const annotation = value;
41
+ return typeof annotation.text === 'string' && isSelection(annotation.selection)
42
+ && (annotation.comment === undefined || typeof annotation.comment === 'string');
43
+ }))
84
44
  throw new Error('The selected conversation annotation is no longer valid.');
85
- }
86
45
  return payload.annotations;
87
46
  }
47
+ function visibleAnnotation(annotation) {
48
+ return { text: annotation.text, ...(annotation.comment === undefined ? {} : { comment: annotation.comment }) };
49
+ }
88
50
  export function decodeSelectionReference(ref) {
89
51
  return decodeStoredSelectionReference(ref).map(visibleAnnotation);
90
52
  }
91
- function selectedContext(annotations) {
92
- return [
93
- '<selected_context>',
94
- ...annotations.flatMap((annotation, index) => annotation.comment === undefined
95
- ? [
96
- `<annotation index="${String(index + 1)}">`,
97
- escapeXmlText(annotation.text),
98
- '</annotation>',
99
- ]
100
- : [
101
- `<annotation index="${String(index + 1)}">`,
102
- '<selected_text>',
103
- escapeXmlText(annotation.text),
104
- '</selected_text>',
105
- '<user_comment>',
106
- escapeXmlText(annotation.comment),
107
- '</user_comment>',
108
- '</annotation>',
109
- ]),
110
- '</selected_context>',
111
- ].join('\n');
112
- }
113
53
  function selectionOccurrences(snapshot) {
114
54
  return snapshot.occurrences.filter(occurrence => occurrence.source === SELECTION_REFERENCE_SOURCE);
115
55
  }
116
- function storedConversationAnnotations(snapshot) {
117
- return selectionOccurrences(snapshot).flatMap((occurrence) => {
56
+ function storedAnnotations(snapshot) {
57
+ return selectionOccurrences(snapshot).flatMap(occurrence => {
118
58
  try {
119
59
  return [...decodeStoredSelectionReference(occurrence.ref)];
120
60
  }
@@ -123,406 +63,143 @@ function storedConversationAnnotations(snapshot) {
123
63
  }
124
64
  });
125
65
  }
126
- /** Read all plugin annotations represented by the current DSH input occurrence. */
127
66
  export function conversationAnnotations(snapshot) {
128
- return storedConversationAnnotations(snapshot).map(visibleAnnotation);
67
+ return storedAnnotations(snapshot).map(visibleAnnotation);
129
68
  }
130
- /** Read annotations that retain an exact source-selection anchor. */
131
69
  export function conversationSelectionAnnotations(snapshot) {
132
- const anchored = [];
133
- storedConversationAnnotations(snapshot).forEach((annotation, annotationIndex) => {
134
- if (annotation.selection === undefined)
135
- return;
136
- anchored.push({
137
- ...visibleAnnotation(annotation),
138
- annotationIndex,
139
- selection: annotation.selection,
140
- });
141
- });
142
- return anchored;
70
+ return storedAnnotations(snapshot).map((annotation, annotationIndex) => ({ ...annotation, annotationIndex }));
143
71
  }
144
- function draftWithoutSelectionOccurrences(snapshot) {
145
- const occurrences = [...selectionOccurrences(snapshot)].sort((a, b) => b.offset - a.offset);
146
- let current = snapshot;
147
- for (const occurrence of occurrences) {
148
- const draft = draftWithoutOccurrence(current, occurrence, SELECTION_REFERENCE_LABEL);
149
- if (draft === undefined)
150
- continue;
151
- current = { ...current, draft };
152
- }
153
- let draft = current.draft;
154
- // Add to chat owns the separator immediately after its leading occurrence.
155
- if (occurrences.some(occurrence => occurrence.offset === 0)) {
156
- if (draft.startsWith('\n\n'))
157
- draft = draft.slice(2);
158
- else if (draft.startsWith('\n') || draft.startsWith(' '))
159
- draft = draft.slice(1);
160
- }
161
- return draft;
162
- }
163
- /** Remove every unsent plugin annotation while preserving the user's draft. */
164
- export function removeConversationAnnotations(input) {
165
- const snapshot = input.state.getSnapshot();
166
- if (selectionOccurrences(snapshot).length === 0)
72
+ /** A single atomic chip replacement preserves other references and the user's draft. */
73
+ function writeAnnotations(input, before, annotations) {
74
+ const existing = selectionOccurrences(before);
75
+ if (existing.length > 1)
167
76
  return false;
168
- input.setDraft(draftWithoutSelectionOccurrences(snapshot));
169
- return true;
170
- }
171
- /** Reference codec used by DSH's native composer chip and submit pipeline. */
172
- export const selectionReferenceSource = {
173
- trigger: '@',
174
- name: SELECTION_REFERENCE_SOURCE,
175
- order: 1_000,
176
- candidates: async () => [],
177
- onPick: () => undefined,
178
- codec: {
179
- clipboardText: ref => decodeSelectionReference(ref).map(annotation => annotation.text).join('\n\n'),
180
- serialize: async (ref, signal) => {
181
- if (signal.aborted)
182
- throw signal.reason;
183
- return selectedContext(decodeSelectionReference(ref));
184
- },
185
- },
186
- };
187
- function writeConversationAnnotations(input, before, annotations) {
188
- const draft = draftWithoutSelectionOccurrences(before);
189
- const insertionState = input.state.getSnapshot();
190
- if (insertionState.draftRev !== before.draftRev || insertionState.draft !== before.draft)
77
+ const previous = existing[0];
78
+ const span = previous === undefined ? { start: 0, end: 0, draftRev: before.draftRev }
79
+ : occurrenceEditSpan(input, before, previous);
80
+ if (span === undefined)
191
81
  return false;
192
- const ref = encodeSelectionReference(annotations);
82
+ const ref = JSON.stringify({ version: 2, annotations });
193
83
  const inserted = input.insertReference({
194
- source: SELECTION_REFERENCE_SOURCE,
195
- ref,
196
- label: SELECTION_REFERENCE_LABEL,
84
+ source: SELECTION_REFERENCE_SOURCE, ref, label: SELECTION_REFERENCE_LABEL,
197
85
  clipboardText: annotations.map(annotation => annotation.text).join('\n\n'),
198
- }, {
199
- start: 0,
200
- end: 0,
201
- draftRev: insertionState.draftRev,
202
- });
203
- // Existing annotations are intentionally still live here. If DSH rejects the
204
- // insertion, the operation is a no-op instead of deleting the previous chip.
86
+ }, span);
205
87
  if (!inserted)
206
88
  return false;
207
- const afterInsert = input.state.getSnapshot();
208
- const occurrence = newlyInsertedOccurrence(insertionState, afterInsert, SELECTION_REFERENCE_SOURCE, ref);
209
- const rollbackInsertion = () => {
210
- const current = input.state.getSnapshot();
211
- const insertedOccurrence = occurrence === undefined
212
- ? undefined
213
- : current.occurrences.find(candidate => candidate.occurrenceId === occurrence.occurrenceId);
214
- if (insertedOccurrence === undefined || insertedOccurrence.offset !== 0)
215
- return;
216
- const insertedRange = occurrenceRange(current, insertedOccurrence, SELECTION_REFERENCE_LABEL);
217
- if (insertedRange === undefined || insertedRange.start !== 0)
218
- return;
219
- let tail = current.draft.slice(insertedRange.end);
220
- if (tail.startsWith(' ') && tail.slice(1) === before.draft)
221
- tail = before.draft;
222
- else if (tail !== before.draft)
223
- return;
224
- input.setDraft(tail);
225
- };
226
- if (occurrence === undefined || occurrence.offset !== 0) {
227
- rollbackInsertion();
228
- return false;
229
- }
230
- const range = occurrenceRange(afterInsert, occurrence, SELECTION_REFERENCE_LABEL);
231
- if (range === undefined || range.start !== 0) {
232
- rollbackInsertion();
233
- return false;
234
- }
235
- // Current DSH inserts the complete `@label` display text followed by an
236
- // optional ASCII separating gap. Older releases insert a one-unit U+FFFC
237
- // placeholder. At this point the old occurrence is still in the tail; one
238
- // tail replacement preserves the new occurrence and removes the old one.
239
- let rest = afterInsert.draft.slice(range.end);
240
- if (rest.startsWith(' ') && rest.slice(1) === before.draft)
241
- rest = before.draft;
242
- else if (rest !== before.draft) {
243
- rollbackInsertion();
244
- return false;
245
- }
246
- const display = afterInsert.draft.slice(range.start, range.end);
247
- const normalizedDraft = `${display}\n\n${draft}`;
248
- // insertReference and setDraft are separate public transactions. Re-read the
249
- // exact occurrence before the second write so a synchronous subscriber cannot
250
- // make us normalize a stale insertion.
251
- const current = input.state.getSnapshot();
252
- const currentOccurrence = current.occurrences.find(candidate => candidate.occurrenceId === occurrence.occurrenceId);
253
- if (current.draftRev !== afterInsert.draftRev
254
- || current.draft !== afterInsert.draft
255
- || currentOccurrence === undefined
256
- || currentOccurrence.source !== SELECTION_REFERENCE_SOURCE
257
- || currentOccurrence.ref !== ref
258
- || !occurrenceMatchesDraft(current, currentOccurrence, SELECTION_REFERENCE_LABEL)) {
259
- rollbackInsertion();
260
- return false;
261
- }
262
- input.setDraft(normalizedDraft);
263
- const normalized = input.state.getSnapshot();
264
- const retained = normalized.occurrences.find(candidate => candidate.occurrenceId === occurrence.occurrenceId);
265
- return normalized.draft === normalizedDraft
266
- && retained !== undefined
267
- && retained.source === SELECTION_REFERENCE_SOURCE
268
- && retained.ref === ref
269
- && retained.offset === 0
270
- && occurrenceMatchesDraft(normalized, retained, SELECTION_REFERENCE_LABEL);
89
+ const after = input.state.getSnapshot();
90
+ const occurrence = newlyInsertedOccurrence(before, after, SELECTION_REFERENCE_SOURCE, ref);
91
+ return occurrence !== undefined && selectionOccurrences(after).length === 1
92
+ && occurrenceMatchesDraft(after, occurrence, SELECTION_REFERENCE_LABEL);
271
93
  }
272
- /** Add one passage to the parent composer's aggregated annotation occurrence. */
273
94
  export function addSelectionToConversation(input, selection, comment) {
274
95
  const before = input.state.getSnapshot();
275
- const annotations = [...storedConversationAnnotations(before), annotationFromSelection(selection, comment)];
276
- return writeConversationAnnotations(input, before, annotations);
96
+ const trimmed = comment?.trim();
97
+ return writeAnnotations(input, before, [...storedAnnotations(before), {
98
+ text: selection.text, selection, ...(trimmed ? { comment: trimmed } : {}),
99
+ }]);
100
+ }
101
+ export function removeConversationAnnotations(input) {
102
+ const occurrences = selectionOccurrences(input.state.getSnapshot());
103
+ if (occurrences.length === 0)
104
+ return false;
105
+ for (const occurrenceId of occurrences.map(occurrence => occurrence.occurrenceId).reverse()) {
106
+ const snapshot = input.state.getSnapshot();
107
+ const occurrence = snapshot.occurrences.find(candidate => candidate.occurrenceId === occurrenceId);
108
+ if (occurrence === undefined)
109
+ continue;
110
+ // Delete only the chip, never a possibly user-owned leading space/newline.
111
+ const span = occurrenceEditSpan(input, snapshot, occurrence);
112
+ if (span === undefined || !input.replaceText('', span))
113
+ return false;
114
+ }
115
+ return selectionOccurrences(input.state.getSnapshot()).length === 0;
277
116
  }
278
- /** Remove one unsent selected-passage annotation, retaining the aggregate when needed. */
279
117
  export function removeConversationAnnotation(input, annotationIndex) {
280
118
  const before = input.state.getSnapshot();
281
- const annotations = [...storedConversationAnnotations(before)];
119
+ const annotations = [...storedAnnotations(before)];
282
120
  if (!Number.isSafeInteger(annotationIndex) || annotations[annotationIndex] === undefined)
283
121
  return false;
284
122
  annotations.splice(annotationIndex, 1);
285
- if (annotations.length === 0) {
286
- input.setDraft(draftWithoutSelectionOccurrences(before));
287
- return selectionOccurrences(input.state.getSnapshot()).length === 0;
288
- }
289
- return writeConversationAnnotations(input, before, annotations);
123
+ return annotations.length === 0 ? removeConversationAnnotations(input) : writeAnnotations(input, before, annotations);
290
124
  }
291
- /** Replace the optional comment on one unsent selected-passage annotation. */
292
125
  export function updateConversationAnnotation(input, annotationIndex, comment) {
293
126
  const before = input.state.getSnapshot();
294
- const annotations = [...storedConversationAnnotations(before)];
295
- const current = annotations[annotationIndex];
296
- if (!Number.isSafeInteger(annotationIndex) || current === undefined)
297
- return false;
298
- const trimmedComment = comment?.trim();
299
- annotations[annotationIndex] = {
300
- text: current.text,
301
- ...(current.selection === undefined ? {} : { selection: current.selection }),
302
- ...(trimmedComment === undefined || trimmedComment.length === 0
303
- ? {}
304
- : { comment: trimmedComment }),
305
- };
306
- return writeConversationAnnotations(input, before, annotations);
307
- }
308
- function exactClipboardProjection(snapshot, annotationOccurrence, annotationClipboardText) {
309
- let projected = '';
310
- let cursor = 0;
311
- const occurrences = [...snapshot.occurrences].sort((left, right) => left.offset - right.offset);
312
- for (const occurrence of occurrences) {
313
- const expectedLabel = occurrence.occurrenceId === annotationOccurrence.occurrenceId
314
- ? SELECTION_REFERENCE_LABEL
315
- : occurrence.label;
316
- if (occurrence.length === undefined
317
- && !snapshot.draft.startsWith(LEGACY_REFERENCE_PLACEHOLDER, occurrence.offset)
318
- && expectedLabel === undefined)
319
- return;
320
- const range = occurrenceRange(snapshot, occurrence, expectedLabel ?? '');
321
- if (range === undefined || range.start < cursor)
322
- return;
323
- const clipboardText = occurrence.occurrenceId === annotationOccurrence.occurrenceId
324
- ? annotationClipboardText
325
- : occurrence.clipboardText;
326
- if (clipboardText === undefined)
327
- return;
328
- projected += snapshot.draft.slice(cursor, range.start) + clipboardText;
329
- cursor = range.end;
330
- }
331
- return projected + snapshot.draft.slice(cursor);
332
- }
333
- function annotationSeparator(draftTail) {
334
- if (draftTail.startsWith('\n\n'))
335
- return '\n\n';
336
- if (draftTail.startsWith('\n'))
337
- return '\n';
338
- if (draftTail.startsWith(' '))
339
- return ' ';
340
- return '';
341
- }
342
- function baseDraftFromMirror(mirrorDraft, clipboardText, expectedBaseDraft) {
343
- const separators = ['\n\n', '\n', ' ', ''];
344
- if (expectedBaseDraft !== undefined) {
345
- return separators.some(separator => mirrorDraft === clipboardText + separator + expectedBaseDraft)
346
- ? expectedBaseDraft
347
- : undefined;
348
- }
349
- for (const separator of separators.slice(0, -1)) {
350
- const prefix = clipboardText + separator;
351
- if (mirrorDraft.startsWith(prefix))
352
- return mirrorDraft.slice(prefix.length);
353
- }
354
- return mirrorDraft === clipboardText ? '' : undefined;
355
- }
356
- /** Describe both the display draft and DSH's persisted clipboard projection. */
357
- export function conversationAnnotationRecoveryRecord(snapshot) {
358
- const occurrence = selectionOccurrences(snapshot).find(candidate => candidate.offset === 0);
359
- const ref = conversationAnnotationReference(snapshot);
360
- if (occurrence === undefined || ref === undefined)
361
- return;
362
- const range = occurrenceRange(snapshot, occurrence, SELECTION_REFERENCE_LABEL);
363
- if (range === undefined || range.start !== 0)
364
- return;
365
- let annotations;
366
- try {
367
- annotations = decodeStoredSelectionReference(ref);
368
- }
369
- catch {
370
- return;
371
- }
372
- const clipboardText = annotations.map(annotation => annotation.text).join('\n\n');
373
- const mirrorDraft = exactClipboardProjection(snapshot, occurrence, clipboardText);
374
- if (mirrorDraft === undefined)
375
- return;
376
- const separator = annotationSeparator(snapshot.draft.slice(range.end));
377
- const mirrorPrefix = clipboardText + separator;
378
- if (!mirrorDraft.startsWith(mirrorPrefix))
379
- return;
380
- return {
381
- ref,
382
- displayDraft: snapshot.draft,
383
- mirrorDraft,
384
- baseDraft: mirrorDraft.slice(mirrorPrefix.length),
385
- };
386
- }
387
- /** Return the valid aggregated reference currently occupying the leading draft slot. */
388
- export function conversationAnnotationReference(snapshot) {
389
- const occurrence = selectionOccurrences(snapshot).find(candidate => candidate.offset === 0);
390
- if (occurrence === undefined
391
- || !occurrenceMatchesDraft(snapshot, occurrence, SELECTION_REFERENCE_LABEL))
392
- return;
393
- try {
394
- decodeStoredSelectionReference(occurrence.ref);
395
- return occurrence.ref;
396
- }
397
- catch {
398
- return;
399
- }
400
- }
401
- function draftWithoutOrphanedAnnotationPrefix(snapshot) {
402
- if (snapshot.occurrences.some(occurrence => occurrence.offset === 0))
403
- return;
404
- const legacyPrefix = `${LEGACY_REFERENCE_PLACEHOLDER}\n\n`;
405
- if (snapshot.draft.startsWith(legacyPrefix))
406
- return snapshot.draft.slice(legacyPrefix.length);
407
- const display = referenceDisplayText(SELECTION_REFERENCE_LABEL);
408
- if (!snapshot.draft.startsWith(display))
409
- return;
410
- let rest = snapshot.draft.slice(display.length);
411
- if (rest.startsWith('\n\n'))
412
- rest = rest.slice(2);
413
- else if (rest.startsWith(' '))
414
- rest = rest.slice(1);
415
- else
416
- return;
417
- return rest;
418
- }
419
- /** Remove the plugin-owned prefix when rc.6 restored its draft without occurrences. */
420
- export function removeOrphanedConversationAnnotationPlaceholder(input) {
421
- const draft = draftWithoutOrphanedAnnotationPrefix(input.state.getSnapshot());
422
- if (draft === undefined)
127
+ const annotations = [...storedAnnotations(before)];
128
+ const annotation = annotations[annotationIndex];
129
+ if (!Number.isSafeInteger(annotationIndex) || annotation === undefined)
423
130
  return false;
424
- input.setDraft(draft);
425
- return true;
131
+ const trimmed = comment?.trim();
132
+ annotations[annotationIndex] = { text: annotation.text, selection: annotation.selection,
133
+ ...(trimmed ? { comment: trimmed } : {}) };
134
+ return writeAnnotations(input, before, annotations);
426
135
  }
427
- /** Rehydrate a lost occurrence over either its display draft or exact mirror projection. */
428
- export function restoreConversationAnnotationReference(input, ref, expectedMirrorDraft, expectedBaseDraft) {
429
- let annotations;
430
- try {
431
- annotations = decodeStoredSelectionReference(ref);
432
- }
433
- catch {
434
- return false;
435
- }
436
- const snapshot = input.state.getSnapshot();
437
- const orphanDraft = draftWithoutOrphanedAnnotationPrefix(snapshot);
438
- const clipboardText = annotations.map(annotation => annotation.text).join('\n\n');
439
- const mirrorBaseDraft = expectedMirrorDraft === undefined
440
- ? undefined
441
- : baseDraftFromMirror(expectedMirrorDraft, clipboardText, expectedBaseDraft);
442
- const draft = orphanDraft
443
- ?? (expectedMirrorDraft !== undefined
444
- && snapshot.occurrences.length === 0
445
- && snapshot.draft === expectedMirrorDraft
446
- && mirrorBaseDraft !== undefined
447
- ? mirrorBaseDraft
448
- : undefined);
449
- if (draft === undefined)
450
- return false;
451
- if (draft !== snapshot.draft)
452
- input.setDraft(draft);
453
- const beforeWrite = input.state.getSnapshot();
454
- if (beforeWrite.draft !== draft || beforeWrite.occurrences.length !== 0)
455
- return false;
456
- if (writeConversationAnnotations(input, beforeWrite, annotations))
457
- return true;
458
- // Recovery must be content preserving. If DSH rejects insertion, put the
459
- // exact mirror projection back rather than leaving a partially stripped draft.
460
- const afterFailure = input.state.getSnapshot();
461
- if (afterFailure.occurrences.length === 0 && expectedMirrorDraft !== undefined) {
462
- input.setDraft(expectedMirrorDraft);
463
- }
464
- return false;
465
- }
466
- function unescapeXmlText(value) {
467
- return value
468
- .replaceAll('&lt;', '<')
469
- .replaceAll('&gt;', '>')
470
- .replaceAll('&amp;', '&');
471
- }
472
- function parseAnnotationBody(body) {
473
- const selectedText = /^<selected_text>\n([\s\S]*?)\n<\/selected_text>(?:\n<user_comment>\n([\s\S]*?)\n<\/user_comment>)?$/u.exec(body);
474
- if (selectedText === null)
475
- return { text: unescapeXmlText(body) };
476
- const comment = selectedText[2];
477
- return {
478
- text: unescapeXmlText(selectedText[1] ?? ''),
479
- ...(comment === undefined ? {} : { comment: unescapeXmlText(comment) }),
480
- };
136
+ function selectedContext(annotations) {
137
+ return ['<selected_context>', ...annotations.flatMap((annotation, index) => [
138
+ `<annotation index="${String(index + 1)}">`,
139
+ '<selected_text>', escapeXmlText(annotation.text), '</selected_text>',
140
+ ...(annotation.comment === undefined ? [] : ['<user_comment>', escapeXmlText(annotation.comment), '</user_comment>']),
141
+ '</annotation>',
142
+ ]), '</selected_context>'].join('\n');
481
143
  }
482
- /** Parse the durable model form back into the user-facing annotation capsule. */
144
+ export const selectionReferenceSource = {
145
+ trigger: '@', name: SELECTION_REFERENCE_SOURCE, order: 1_000,
146
+ candidates: async () => [], onPick: () => undefined,
147
+ codec: {
148
+ clipboardText: ref => decodeSelectionReference(ref).map(annotation => annotation.text).join('\n\n'),
149
+ serialize: async (ref, signal) => {
150
+ if (signal.aborted)
151
+ throw signal.reason;
152
+ return selectedContext(decodeSelectionReference(ref));
153
+ },
154
+ },
155
+ };
483
156
  export function parseAnnotatedConversationPrompt(text) {
484
- const current = /^<selected_context>\n([\s\S]*?)\n<\/selected_context>\s*/u.exec(text);
485
- if (current !== null) {
486
- const annotations = [];
487
- const annotationPattern = /<annotation(?: index="\d+")?>\n([\s\S]*?)\n<\/annotation>/gu;
488
- for (const match of current[1]?.matchAll(annotationPattern) ?? []) {
489
- annotations.push(parseAnnotationBody(match[1] ?? ''));
490
- }
491
- if (annotations.length > 0) {
492
- return { annotations, message: text.slice(current[0].length).trim() };
157
+ // Stored messages outlive a plugin update. Reading their durable text does
158
+ // not require an old DSH runtime or an old composer implementation.
159
+ const prefix = /^\s*<selected_context(?: source="current-conversation" event-seq="\d+")?>\r?\n([\s\S]*?)\r?\n<\/selected_context>\s*/u.exec(text);
160
+ if (prefix === null)
161
+ return;
162
+ const annotations = [];
163
+ if (prefix[0].trimStart().startsWith('<selected_context source=')) {
164
+ annotations.push({ text: unescapeXmlText(prefix[1] ?? '') });
165
+ }
166
+ else {
167
+ for (const match of prefix[1]?.matchAll(/<annotation(?: index="\d+")?>\r?\n([\s\S]*?)\r?\n<\/annotation>/gu) ?? []) {
168
+ const body = match[1] ?? '';
169
+ const parts = /^<selected_text>\r?\n([\s\S]*?)\r?\n<\/selected_text>(?:\r?\n<user_comment>\r?\n([\s\S]*?)\r?\n<\/user_comment>)?$/u.exec(body);
170
+ annotations.push({ text: unescapeXmlText(parts?.[1] ?? body),
171
+ ...(parts?.[2] === undefined ? {} : { comment: unescapeXmlText(parts[2]) }) });
493
172
  }
494
173
  }
495
- const legacyXml = /^<selected_context source="current-conversation" event-seq="(\d+)">\n?([\s\S]*?)\n?<\/selected_context>\s*/u.exec(text);
496
- if (legacyXml !== null) {
497
- return {
498
- annotations: [{ text: unescapeXmlText(legacyXml[2] ?? '') }],
499
- message: text.slice(legacyXml[0].length).trim(),
500
- };
501
- }
502
- const legacyText = /^Selected passage from the current conversation \(message (\d+)\):\n([\s\S]*?)\nEnd selected passage\.\s*/u.exec(text);
503
- if (legacyText === null)
504
- return undefined;
505
- return {
506
- annotations: [{ text: legacyText[2] ?? '' }],
507
- message: text.slice(legacyText[0].length).trim(),
508
- };
174
+ return annotations.length === 0 ? undefined : { annotations, message: text.slice(prefix[0].length).trim() };
509
175
  }
510
- /** Add the selected quote to the first child prompt. */
176
+ /** The first prompt uses the same durable annotation format as the main composer. */
511
177
  export function buildSideChatPrompt(selection, question) {
512
178
  const trimmed = question.trim();
513
- if (selection === undefined)
514
- return [{ type: 'text', text: trimmed }];
515
- return [{
516
- type: 'text',
517
- text: [
518
- `<selected_context source="current-conversation" event-seq="${selection.atSeq}">`,
519
- escapeXmlText(selection.text),
520
- '</selected_context>',
521
- '',
522
- '<user_question>',
523
- escapeXmlText(trimmed),
524
- '</user_question>',
525
- ].join('\n'),
526
- }];
179
+ return [{ type: 'text', text: selection === undefined ? trimmed : [
180
+ selectedContext([{ text: selection.text }]), '',
181
+ '<user_question>', escapeXmlText(trimmed), '</user_question>',
182
+ ].join('\n') }];
183
+ }
184
+ /** Decode a question only after recognizing this plugin's annotation prefix. */
185
+ export function annotationMessageText(message) {
186
+ const question = /^<user_question>(?:\r?\n)?([\s\S]*?)(?:\r?\n)?<\/user_question>(?=$|\s)/u.exec(message);
187
+ if (question === null)
188
+ return message;
189
+ const text = unescapeXmlText((question[1] ?? '').trim());
190
+ // Attachments or additional text blocks must not make the question wrapper
191
+ // reappear, nor be discarded while projecting the model-facing payload.
192
+ const suffix = message.slice(question[0].length).trim();
193
+ return [text, suffix].filter(part => part.length > 0).join('\n');
194
+ }
195
+ /** One projection supplies both the annotation capsule and the visible question. */
196
+ export function parseSideChatPrompt(text) {
197
+ const annotated = parseAnnotatedConversationPrompt(text);
198
+ return annotated === undefined ? undefined : {
199
+ annotations: annotated.annotations,
200
+ message: annotationMessageText(annotated.message),
201
+ };
202
+ }
203
+ export function sideChatQuestionText(text) {
204
+ return parseSideChatPrompt(text)?.message ?? text;
527
205
  }
528
- //# sourceMappingURL=add-to-conversation.js.map