@ahggg/dsh-side-chat 0.7.1 → 0.7.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 (171) hide show
  1. package/README.md +3 -3
  2. package/README.zh-CN.md +3 -3
  3. package/lib/client/apply.d.ts +0 -1
  4. package/lib/client/apply.js +0 -1
  5. package/lib/client/contracts.d.ts +0 -1
  6. package/lib/client/contracts.js +0 -1
  7. package/lib/client/index.d.ts +0 -1
  8. package/lib/client/index.js +0 -1
  9. package/lib/client/model-preference.d.ts +0 -1
  10. package/lib/client/model-preference.js +0 -1
  11. package/lib/client/panel/SelectionQuote.d.ts +0 -1
  12. package/lib/client/panel/SelectionQuote.js +0 -1
  13. package/lib/client/panel/SendIcon.d.ts +0 -1
  14. package/lib/client/panel/SendIcon.js +0 -1
  15. package/lib/client/panel/SideChatBody.d.ts +0 -1
  16. package/lib/client/panel/SideChatBody.js +0 -1
  17. package/lib/client/panel/SideChatErrorState.d.ts +0 -1
  18. package/lib/client/panel/SideChatErrorState.js +0 -1
  19. package/lib/client/panel/SideChatHeader.d.ts +0 -1
  20. package/lib/client/panel/SideChatHeader.js +0 -1
  21. package/lib/client/panel/SideChatModelSelect.d.ts +0 -1
  22. package/lib/client/panel/SideChatModelSelect.js +0 -1
  23. package/lib/client/panel/SideChatPanel.d.ts +0 -1
  24. package/lib/client/panel/SideChatPanel.js +0 -1
  25. package/lib/client/panel/StopIcon.d.ts +0 -1
  26. package/lib/client/panel/StopIcon.js +0 -1
  27. package/lib/client/panel/messages.d.ts +0 -1
  28. package/lib/client/panel/messages.js +0 -1
  29. package/lib/client/panel/use-auto-growing-textarea.d.ts +0 -1
  30. package/lib/client/panel/use-auto-growing-textarea.js +0 -1
  31. package/lib/client/parent-composer/ParentConversationAnnotations.d.ts +0 -1
  32. package/lib/client/parent-composer/ParentConversationAnnotations.js +26 -3
  33. package/lib/client/parent-composer/add-to-conversation.d.ts +15 -54
  34. package/lib/client/parent-composer/add-to-conversation.js +308 -63
  35. package/lib/client/parent-composer/annotation-persistence.d.ts +1 -2
  36. package/lib/client/parent-composer/annotation-persistence.js +43 -41
  37. package/lib/client/parent-composer/composer-reference.d.ts +81 -0
  38. package/lib/client/parent-composer/composer-reference.js +89 -0
  39. package/lib/client/parent-composer/referenced-conversation.d.ts +2 -3
  40. package/lib/client/parent-composer/referenced-conversation.js +36 -25
  41. package/lib/client/rc6/ArchivedConversation.d.ts +2 -3
  42. package/lib/client/rc6/ArchivedConversation.js +6 -5
  43. package/lib/client/rc6/Rc6SideChatOverlay.d.ts +0 -1
  44. package/lib/client/rc6/Rc6SideChatOverlay.js +12 -2
  45. package/lib/client/rc6/SideChatTool.d.ts +1 -2
  46. package/lib/client/rc6/SideChatTool.js +18 -9
  47. package/lib/client/rc6/context.d.ts +9 -3
  48. package/lib/client/rc6/context.js +0 -1
  49. package/lib/client/rc6/primitive-labels.d.ts +7 -0
  50. package/lib/client/rc6/primitive-labels.js +60 -0
  51. package/lib/client/rc6/remote-adapter.d.ts +0 -1
  52. package/lib/client/rc6/remote-adapter.js +4 -3
  53. package/lib/client/rc6/runtime-compat.d.ts +120 -0
  54. package/lib/client/rc6/runtime-compat.js +187 -0
  55. package/lib/client/rc6/sessions-adapter.d.ts +9 -4
  56. package/lib/client/rc6/sessions-adapter.js +74 -46
  57. package/lib/client/selection/ConversationAnnotationMarkers.d.ts +0 -1
  58. package/lib/client/selection/ConversationAnnotationMarkers.js +0 -1
  59. package/lib/client/selection/SelectionActions.d.ts +5 -2
  60. package/lib/client/selection/SelectionActions.js +66 -50
  61. package/lib/client/selection/selection-controller.d.ts +0 -1
  62. package/lib/client/selection/selection-controller.js +0 -1
  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 +0 -1
  67. package/lib/client/side-chat-controller.d.ts +0 -1
  68. package/lib/client/side-chat-controller.js +0 -1
  69. package/lib/client.js +1173 -8744
  70. package/lib/host/archived-fork-service.d.ts +0 -1
  71. package/lib/host/archived-fork-service.js +71 -26
  72. package/lib/host/index.d.ts +0 -1
  73. package/lib/host/index.js +0 -1
  74. package/lib/index.d.ts +0 -1
  75. package/lib/index.js +0 -1
  76. package/lib/remote.d.ts +0 -1
  77. package/lib/remote.js +0 -1
  78. package/lib/shared/archived-wire.d.ts +0 -1
  79. package/lib/shared/archived-wire.js +0 -1
  80. package/lib/shared/constants.d.ts +0 -1
  81. package/lib/shared/constants.js +0 -1
  82. package/lib/shared/contracts.d.ts +0 -1
  83. package/lib/shared/contracts.js +0 -1
  84. package/lib/shared/error-codes.d.ts +0 -1
  85. package/lib/shared/error-codes.js +0 -1
  86. package/lib/shared/observable.d.ts +0 -1
  87. package/lib/shared/observable.js +0 -1
  88. package/lib/typert.d.ts +0 -1
  89. package/lib/typert.js +64 -51
  90. package/package.json +60 -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/referenced-conversation.d.ts.map +0 -1
  126. package/lib/client/parent-composer/referenced-conversation.js.map +0 -1
  127. package/lib/client/rc6/ArchivedConversation.d.ts.map +0 -1
  128. package/lib/client/rc6/ArchivedConversation.js.map +0 -1
  129. package/lib/client/rc6/Rc6SideChatOverlay.d.ts.map +0 -1
  130. package/lib/client/rc6/Rc6SideChatOverlay.js.map +0 -1
  131. package/lib/client/rc6/SideChatTool.d.ts.map +0 -1
  132. package/lib/client/rc6/SideChatTool.js.map +0 -1
  133. package/lib/client/rc6/context.d.ts.map +0 -1
  134. package/lib/client/rc6/context.js.map +0 -1
  135. package/lib/client/rc6/remote-adapter.d.ts.map +0 -1
  136. package/lib/client/rc6/remote-adapter.js.map +0 -1
  137. package/lib/client/rc6/sessions-adapter.d.ts.map +0 -1
  138. package/lib/client/rc6/sessions-adapter.js.map +0 -1
  139. package/lib/client/selection/ConversationAnnotationMarkers.d.ts.map +0 -1
  140. package/lib/client/selection/ConversationAnnotationMarkers.js.map +0 -1
  141. package/lib/client/selection/SelectionActions.d.ts.map +0 -1
  142. package/lib/client/selection/SelectionActions.js.map +0 -1
  143. package/lib/client/selection/selection-controller.d.ts.map +0 -1
  144. package/lib/client/selection/selection-controller.js.map +0 -1
  145. package/lib/client/selection/selection-limits.d.ts.map +0 -1
  146. package/lib/client/selection/selection-limits.js.map +0 -1
  147. package/lib/client/selection/selection-normalizer.d.ts.map +0 -1
  148. package/lib/client/selection/selection-normalizer.js.map +0 -1
  149. package/lib/client/side-chat-controller.d.ts.map +0 -1
  150. package/lib/client/side-chat-controller.js.map +0 -1
  151. package/lib/client.js.map +0 -1
  152. package/lib/host/archived-fork-service.d.ts.map +0 -1
  153. package/lib/host/archived-fork-service.js.map +0 -1
  154. package/lib/host/index.d.ts.map +0 -1
  155. package/lib/host/index.js.map +0 -1
  156. package/lib/index.d.ts.map +0 -1
  157. package/lib/index.js.map +0 -1
  158. package/lib/remote.d.ts.map +0 -1
  159. package/lib/remote.js.map +0 -1
  160. package/lib/shared/archived-wire.d.ts.map +0 -1
  161. package/lib/shared/archived-wire.js.map +0 -1
  162. package/lib/shared/constants.d.ts.map +0 -1
  163. package/lib/shared/constants.js.map +0 -1
  164. package/lib/shared/contracts.d.ts.map +0 -1
  165. package/lib/shared/contracts.js.map +0 -1
  166. package/lib/shared/error-codes.d.ts.map +0 -1
  167. package/lib/shared/error-codes.js.map +0 -1
  168. package/lib/shared/observable.d.ts.map +0 -1
  169. package/lib/shared/observable.js.map +0 -1
  170. package/lib/typert.d.ts.map +0 -1
  171. package/lib/typert.js.map +0 -1
@@ -1,5 +1,5 @@
1
+ import { draftWithoutOccurrence, LEGACY_REFERENCE_PLACEHOLDER, newlyInsertedOccurrence, occurrenceEditSpan, occurrenceMatchesDraft, occurrenceRange, referenceDisplayText, } from './composer-reference.js';
1
2
  const SELECTION_REFERENCE_SOURCE = 'dsh-side-chat-selection';
2
- const OBJECT_REPLACEMENT_CHARACTER = '\uFFFC';
3
3
  export const SELECTION_REFERENCE_LABEL = '__dsh_side_chat_annotations__';
4
4
  function escapeXmlText(value) {
5
5
  return value
@@ -113,13 +113,6 @@ function selectedContext(annotations) {
113
113
  function selectionOccurrences(snapshot) {
114
114
  return snapshot.occurrences.filter(occurrence => occurrence.source === SELECTION_REFERENCE_SOURCE);
115
115
  }
116
- function occurrenceLength(occurrence) {
117
- return occurrence.length !== undefined
118
- && Number.isSafeInteger(occurrence.length)
119
- && occurrence.length > 0
120
- ? occurrence.length
121
- : 1;
122
- }
123
116
  function storedConversationAnnotations(snapshot) {
124
117
  return selectionOccurrences(snapshot).flatMap((occurrence) => {
125
118
  try {
@@ -150,16 +143,19 @@ export function conversationSelectionAnnotations(snapshot) {
150
143
  }
151
144
  function draftWithoutSelectionOccurrences(snapshot) {
152
145
  const occurrences = [...selectionOccurrences(snapshot)].sort((a, b) => b.offset - a.offset);
153
- let draft = snapshot.draft;
146
+ let current = snapshot;
154
147
  for (const occurrence of occurrences) {
155
- draft = draft.slice(0, occurrence.offset)
156
- + draft.slice(occurrence.offset + occurrenceLength(occurrence));
148
+ const draft = draftWithoutOccurrence(current, occurrence, SELECTION_REFERENCE_LABEL);
149
+ if (draft === undefined)
150
+ continue;
151
+ current = { ...current, draft };
157
152
  }
158
- // Add to chat owns the blank lines immediately after its leading occurrence.
153
+ let draft = current.draft;
154
+ // Add to chat owns the separator immediately after its leading occurrence.
159
155
  if (occurrences.some(occurrence => occurrence.offset === 0)) {
160
156
  if (draft.startsWith('\n\n'))
161
157
  draft = draft.slice(2);
162
- else if (draft.startsWith('\n'))
158
+ else if (draft.startsWith('\n') || draft.startsWith(' '))
163
159
  draft = draft.slice(1);
164
160
  }
165
161
  return draft;
@@ -167,8 +163,23 @@ function draftWithoutSelectionOccurrences(snapshot) {
167
163
  /** Remove every unsent plugin annotation while preserving the user's draft. */
168
164
  export function removeConversationAnnotations(input) {
169
165
  const snapshot = input.state.getSnapshot();
170
- if (selectionOccurrences(snapshot).length === 0)
166
+ const occurrences = selectionOccurrences(snapshot);
167
+ if (occurrences.length === 0)
171
168
  return false;
169
+ if (input.referenceMode === 'lexical' && input.replaceText !== undefined) {
170
+ for (const occurrenceId of occurrences.map(occurrence => occurrence.occurrenceId).reverse()) {
171
+ const current = input.state.getSnapshot();
172
+ const occurrence = current.occurrences.find(candidate => candidate.occurrenceId === occurrenceId);
173
+ if (occurrence === undefined)
174
+ continue;
175
+ const span = occurrenceEditSpan(input, current, occurrence, {
176
+ consumeFollowingSeparator: true,
177
+ });
178
+ if (span === undefined || !input.replaceText('', span))
179
+ return false;
180
+ }
181
+ return selectionOccurrences(input.state.getSnapshot()).length === 0;
182
+ }
172
183
  input.setDraft(draftWithoutSelectionOccurrences(snapshot));
173
184
  return true;
174
185
  }
@@ -188,46 +199,169 @@ export const selectionReferenceSource = {
188
199
  },
189
200
  },
190
201
  };
191
- function annotationClipboardText(annotations) {
192
- return annotations.map(annotation => annotation.text).join('\n\n');
193
- }
194
- function writeConversationAnnotations(input, before, annotations) {
202
+ function writeTextConversationAnnotations(input, before, annotations) {
195
203
  const draft = draftWithoutSelectionOccurrences(before);
196
- if (selectionOccurrences(before).length > 0)
197
- input.setDraft(draft);
198
204
  const insertionState = input.state.getSnapshot();
205
+ if (insertionState.draftRev !== before.draftRev || insertionState.draft !== before.draft)
206
+ return false;
199
207
  const ref = encodeSelectionReference(annotations);
200
208
  const inserted = input.insertReference({
201
209
  source: SELECTION_REFERENCE_SOURCE,
202
210
  ref,
203
211
  label: SELECTION_REFERENCE_LABEL,
204
- clipboardText: annotationClipboardText(annotations),
212
+ clipboardText: annotations.map(annotation => annotation.text).join('\n\n'),
205
213
  }, {
206
214
  start: 0,
207
215
  end: 0,
208
216
  draftRev: insertionState.draftRev,
209
217
  });
218
+ // Existing annotations are intentionally still live here. If DSH rejects the
219
+ // insertion, the operation is a no-op instead of deleting the previous chip.
210
220
  if (!inserted)
211
221
  return false;
212
- // rc.7 inserts one U+FFFC placeholder, while newer DSH versions insert the
213
- // full @label range. Preserve whichever range DSH minted and only replace
214
- // its trailing separator so the first visual row remains available to the
215
- // plugin's interactive capsule.
216
- const afterInsertion = input.state.getSnapshot();
217
- const occurrence = selectionOccurrences(afterInsertion)
218
- .find(candidate => candidate.offset === 0 && candidate.ref === ref);
219
- const displayLength = occurrence === undefined ? 0 : occurrenceLength(occurrence);
220
- if (occurrence === undefined || displayLength > afterInsertion.draft.length) {
221
- input.setDraft(draft);
222
+ const afterInsert = input.state.getSnapshot();
223
+ const occurrence = newlyInsertedOccurrence(insertionState, afterInsert, SELECTION_REFERENCE_SOURCE, ref);
224
+ const rollbackInsertion = () => {
225
+ const current = input.state.getSnapshot();
226
+ const insertedOccurrence = occurrence === undefined
227
+ ? undefined
228
+ : current.occurrences.find(candidate => candidate.occurrenceId === occurrence.occurrenceId);
229
+ if (insertedOccurrence === undefined || insertedOccurrence.offset !== 0)
230
+ return;
231
+ const insertedRange = occurrenceRange(current, insertedOccurrence, SELECTION_REFERENCE_LABEL);
232
+ if (insertedRange === undefined || insertedRange.start !== 0)
233
+ return;
234
+ let tail = current.draft.slice(insertedRange.end);
235
+ if (tail.startsWith(' ') && tail.slice(1) === before.draft)
236
+ tail = before.draft;
237
+ else if (tail !== before.draft)
238
+ return;
239
+ input.setDraft(tail);
240
+ };
241
+ if (occurrence === undefined || occurrence.offset !== 0) {
242
+ rollbackInsertion();
222
243
  return false;
223
244
  }
224
- const display = afterInsertion.draft.slice(0, displayLength);
225
- input.setDraft(`${display}\n\n${draft}`);
226
- const preserved = selectionOccurrences(input.state.getSnapshot())
227
- .some(candidate => candidate.offset === 0 && candidate.ref === ref);
228
- if (!preserved)
229
- input.setDraft(draft);
230
- return preserved;
245
+ const range = occurrenceRange(afterInsert, occurrence, SELECTION_REFERENCE_LABEL);
246
+ if (range === undefined || range.start !== 0) {
247
+ rollbackInsertion();
248
+ return false;
249
+ }
250
+ // Current DSH inserts the complete `@label` display text followed by an
251
+ // optional ASCII separating gap. Older releases insert a one-unit U+FFFC
252
+ // placeholder. At this point the old occurrence is still in the tail; one
253
+ // tail replacement preserves the new occurrence and removes the old one.
254
+ let rest = afterInsert.draft.slice(range.end);
255
+ if (rest.startsWith(' ') && rest.slice(1) === before.draft)
256
+ rest = before.draft;
257
+ else if (rest !== before.draft) {
258
+ rollbackInsertion();
259
+ return false;
260
+ }
261
+ const display = afterInsert.draft.slice(range.start, range.end);
262
+ const normalizedDraft = `${display}\n\n${draft}`;
263
+ // insertReference and setDraft are separate public transactions. Re-read the
264
+ // exact occurrence before the second write so a synchronous subscriber cannot
265
+ // make us normalize a stale insertion.
266
+ const current = input.state.getSnapshot();
267
+ const currentOccurrence = current.occurrences.find(candidate => candidate.occurrenceId === occurrence.occurrenceId);
268
+ if (current.draftRev !== afterInsert.draftRev
269
+ || current.draft !== afterInsert.draft
270
+ || currentOccurrence === undefined
271
+ || currentOccurrence.source !== SELECTION_REFERENCE_SOURCE
272
+ || currentOccurrence.ref !== ref
273
+ || !occurrenceMatchesDraft(current, currentOccurrence, SELECTION_REFERENCE_LABEL)) {
274
+ rollbackInsertion();
275
+ return false;
276
+ }
277
+ input.setDraft(normalizedDraft);
278
+ const normalized = input.state.getSnapshot();
279
+ const retained = normalized.occurrences.find(candidate => candidate.occurrenceId === occurrence.occurrenceId);
280
+ return normalized.draft === normalizedDraft
281
+ && retained !== undefined
282
+ && retained.source === SELECTION_REFERENCE_SOURCE
283
+ && retained.ref === ref
284
+ && retained.offset === 0
285
+ && occurrenceMatchesDraft(normalized, retained, SELECTION_REFERENCE_LABEL);
286
+ }
287
+ function writeLexicalConversationAnnotations(input, before, annotations) {
288
+ if (input.replaceText === undefined)
289
+ return false;
290
+ const existing = selectionOccurrences(before);
291
+ if (existing.length > 1)
292
+ return false;
293
+ const previous = existing[0];
294
+ if (previous !== undefined && previous.length === undefined)
295
+ return false;
296
+ let insertionState = before;
297
+ let preparedGap = false;
298
+ if (previous === undefined) {
299
+ // A gap inserted before the chip is unambiguously plugin-owned. DSH's
300
+ // automatic gap cannot otherwise be distinguished from a user's leading
301
+ // space when the reference is later removed.
302
+ if (!input.replaceText(' ', { start: 0, end: 0, draftRev: before.draftRev }))
303
+ return false;
304
+ insertionState = input.state.getSnapshot();
305
+ if (insertionState.draft !== ` ${before.draft}`)
306
+ return false;
307
+ preparedGap = true;
308
+ }
309
+ const span = previous === undefined
310
+ ? { start: 0, end: 0, draftRev: insertionState.draftRev }
311
+ : occurrenceEditSpan(input, insertionState, previous);
312
+ if (span === undefined)
313
+ return false;
314
+ const ref = encodeSelectionReference(annotations);
315
+ const clipboardText = annotations.map(annotation => annotation.text).join('\n\n');
316
+ const inserted = input.insertReference({
317
+ source: SELECTION_REFERENCE_SOURCE,
318
+ ref,
319
+ label: SELECTION_REFERENCE_LABEL,
320
+ clipboardText,
321
+ }, span);
322
+ if (!inserted) {
323
+ if (preparedGap) {
324
+ const current = input.state.getSnapshot();
325
+ if (current.draft === ` ${before.draft}`) {
326
+ input.replaceText('', { start: 0, end: 1, draftRev: current.draftRev });
327
+ }
328
+ }
329
+ return false;
330
+ }
331
+ const afterInsert = input.state.getSnapshot();
332
+ const occurrence = newlyInsertedOccurrence(insertionState, afterInsert, SELECTION_REFERENCE_SOURCE, ref);
333
+ const previousEnd = previous === undefined ? 0 : previous.offset + (previous.length ?? 0);
334
+ const previousTail = previous === undefined
335
+ ? insertionState.draft
336
+ : insertionState.draft.slice(previousEnd);
337
+ const automaticGap = previousTail.startsWith(' ') ? '' : ' ';
338
+ const expectedDraft = previous === undefined
339
+ ? `${clipboardText}${insertionState.draft}`
340
+ : insertionState.draft.slice(0, previous.offset) + clipboardText + automaticGap + previousTail;
341
+ const valid = occurrence !== undefined
342
+ && selectionOccurrences(afterInsert).length === 1
343
+ && occurrence.offset === (previous?.offset ?? 0)
344
+ && occurrence.label === SELECTION_REFERENCE_LABEL
345
+ && occurrence.clipboardText === clipboardText
346
+ && occurrenceMatchesDraft(afterInsert, occurrence, SELECTION_REFERENCE_LABEL)
347
+ && afterInsert.draft === expectedDraft;
348
+ if (valid)
349
+ return true;
350
+ // Best-effort rollback for a first insertion. Replacing an existing chip is
351
+ // atomic and should never reach this path on a conforming DSH input facade.
352
+ if (previous === undefined && occurrence !== undefined) {
353
+ const rollback = occurrenceEditSpan(input, afterInsert, occurrence, {
354
+ consumeFollowingSeparator: true,
355
+ });
356
+ if (rollback !== undefined)
357
+ input.replaceText('', rollback);
358
+ }
359
+ return false;
360
+ }
361
+ function writeConversationAnnotations(input, before, annotations) {
362
+ return input.referenceMode === 'lexical'
363
+ ? writeLexicalConversationAnnotations(input, before, annotations)
364
+ : writeTextConversationAnnotations(input, before, annotations);
231
365
  }
232
366
  /** Add one passage to the parent composer's aggregated annotation occurrence. */
233
367
  export function addSelectionToConversation(input, selection, comment) {
@@ -235,6 +369,17 @@ export function addSelectionToConversation(input, selection, comment) {
235
369
  const annotations = [...storedConversationAnnotations(before), annotationFromSelection(selection, comment)];
236
370
  return writeConversationAnnotations(input, before, annotations);
237
371
  }
372
+ /** Remove one unsent selected-passage annotation, retaining the aggregate when needed. */
373
+ export function removeConversationAnnotation(input, annotationIndex) {
374
+ const before = input.state.getSnapshot();
375
+ const annotations = [...storedConversationAnnotations(before)];
376
+ if (!Number.isSafeInteger(annotationIndex) || annotations[annotationIndex] === undefined)
377
+ return false;
378
+ annotations.splice(annotationIndex, 1);
379
+ if (annotations.length === 0)
380
+ return removeConversationAnnotations(input);
381
+ return writeConversationAnnotations(input, before, annotations);
382
+ }
238
383
  /** Replace the optional comment on one unsent selected-passage annotation. */
239
384
  export function updateConversationAnnotation(input, annotationIndex, comment) {
240
385
  const before = input.state.getSnapshot();
@@ -252,13 +397,90 @@ export function updateConversationAnnotation(input, annotationIndex, comment) {
252
397
  };
253
398
  return writeConversationAnnotations(input, before, annotations);
254
399
  }
400
+ function exactClipboardProjection(snapshot, annotationOccurrence, annotationClipboardText) {
401
+ let projected = '';
402
+ let cursor = 0;
403
+ const occurrences = [...snapshot.occurrences].sort((left, right) => left.offset - right.offset);
404
+ for (const occurrence of occurrences) {
405
+ const expectedLabel = occurrence.occurrenceId === annotationOccurrence.occurrenceId
406
+ ? SELECTION_REFERENCE_LABEL
407
+ : occurrence.label;
408
+ if (occurrence.length === undefined
409
+ && !snapshot.draft.startsWith(LEGACY_REFERENCE_PLACEHOLDER, occurrence.offset)
410
+ && expectedLabel === undefined)
411
+ return;
412
+ const range = occurrenceRange(snapshot, occurrence, expectedLabel ?? '');
413
+ if (range === undefined || range.start < cursor)
414
+ return;
415
+ const clipboardText = occurrence.occurrenceId === annotationOccurrence.occurrenceId
416
+ ? annotationClipboardText
417
+ : occurrence.clipboardText;
418
+ if (clipboardText === undefined)
419
+ return;
420
+ projected += snapshot.draft.slice(cursor, range.start) + clipboardText;
421
+ cursor = range.end;
422
+ }
423
+ return projected + snapshot.draft.slice(cursor);
424
+ }
425
+ function annotationSeparator(draftTail) {
426
+ if (draftTail.startsWith('\n\n'))
427
+ return '\n\n';
428
+ if (draftTail.startsWith('\n'))
429
+ return '\n';
430
+ if (draftTail.startsWith(' '))
431
+ return ' ';
432
+ return '';
433
+ }
434
+ function baseDraftFromMirror(mirrorDraft, clipboardText, expectedBaseDraft) {
435
+ const separators = ['\n\n', '\n', ' ', ''];
436
+ if (expectedBaseDraft !== undefined) {
437
+ return separators.some(separator => mirrorDraft === clipboardText + separator + expectedBaseDraft)
438
+ ? expectedBaseDraft
439
+ : undefined;
440
+ }
441
+ for (const separator of separators.slice(0, -1)) {
442
+ const prefix = clipboardText + separator;
443
+ if (mirrorDraft.startsWith(prefix))
444
+ return mirrorDraft.slice(prefix.length);
445
+ }
446
+ return mirrorDraft === clipboardText ? '' : undefined;
447
+ }
448
+ /** Describe both the display draft and DSH's persisted clipboard projection. */
449
+ export function conversationAnnotationRecoveryRecord(snapshot) {
450
+ const occurrence = selectionOccurrences(snapshot).find(candidate => candidate.offset === 0);
451
+ const ref = conversationAnnotationReference(snapshot);
452
+ if (occurrence === undefined || ref === undefined)
453
+ return;
454
+ const range = occurrenceRange(snapshot, occurrence, SELECTION_REFERENCE_LABEL);
455
+ if (range === undefined || range.start !== 0)
456
+ return;
457
+ let annotations;
458
+ try {
459
+ annotations = decodeStoredSelectionReference(ref);
460
+ }
461
+ catch {
462
+ return;
463
+ }
464
+ const clipboardText = annotations.map(annotation => annotation.text).join('\n\n');
465
+ const mirrorDraft = exactClipboardProjection(snapshot, occurrence, clipboardText);
466
+ if (mirrorDraft === undefined)
467
+ return;
468
+ const separator = annotationSeparator(snapshot.draft.slice(range.end));
469
+ const mirrorPrefix = clipboardText + separator;
470
+ if (!mirrorDraft.startsWith(mirrorPrefix))
471
+ return;
472
+ return {
473
+ ref,
474
+ displayDraft: snapshot.draft,
475
+ mirrorDraft,
476
+ baseDraft: mirrorDraft.slice(mirrorPrefix.length),
477
+ };
478
+ }
255
479
  /** Return the valid aggregated reference currently occupying the leading draft slot. */
256
480
  export function conversationAnnotationReference(snapshot) {
257
481
  const occurrence = selectionOccurrences(snapshot).find(candidate => candidate.offset === 0);
258
482
  if (occurrence === undefined
259
- || occurrenceLength(occurrence) > snapshot.draft.length
260
- || (occurrence.length === undefined
261
- && !snapshot.draft.startsWith(OBJECT_REPLACEMENT_CHARACTER)))
483
+ || !occurrenceMatchesDraft(snapshot, occurrence, SELECTION_REFERENCE_LABEL))
262
484
  return;
263
485
  try {
264
486
  decodeStoredSelectionReference(occurrence.ref);
@@ -268,25 +490,25 @@ export function conversationAnnotationReference(snapshot) {
268
490
  return;
269
491
  }
270
492
  }
271
- function draftWithoutOrphanedAnnotationPrefix(snapshot, ref) {
493
+ function draftWithoutOrphanedAnnotationPrefix(snapshot) {
272
494
  if (snapshot.occurrences.some(occurrence => occurrence.offset === 0))
273
495
  return;
274
- const prefixes = [
275
- `${OBJECT_REPLACEMENT_CHARACTER}\n\n`,
276
- `@${SELECTION_REFERENCE_LABEL}\n\n`,
277
- ];
278
- if (ref !== undefined) {
279
- try {
280
- prefixes.push(`${annotationClipboardText(decodeStoredSelectionReference(ref))}\n\n`);
281
- }
282
- catch {
283
- // Invalid stored references cannot authorize removing plain draft text.
284
- }
285
- }
286
- const prefix = prefixes.find(candidate => snapshot.draft.startsWith(candidate));
287
- return prefix === undefined ? undefined : snapshot.draft.slice(prefix.length);
496
+ const legacyPrefix = `${LEGACY_REFERENCE_PLACEHOLDER}\n\n`;
497
+ if (snapshot.draft.startsWith(legacyPrefix))
498
+ return snapshot.draft.slice(legacyPrefix.length);
499
+ const display = referenceDisplayText(SELECTION_REFERENCE_LABEL);
500
+ if (!snapshot.draft.startsWith(display))
501
+ return;
502
+ let rest = snapshot.draft.slice(display.length);
503
+ if (rest.startsWith('\n\n'))
504
+ rest = rest.slice(2);
505
+ else if (rest.startsWith(' '))
506
+ rest = rest.slice(1);
507
+ else
508
+ return;
509
+ return rest;
288
510
  }
289
- /** Remove a recognized plugin-owned prefix restored without its occurrence. */
511
+ /** Remove the plugin-owned prefix when rc.6 restored its draft without occurrences. */
290
512
  export function removeOrphanedConversationAnnotationPlaceholder(input) {
291
513
  const draft = draftWithoutOrphanedAnnotationPrefix(input.state.getSnapshot());
292
514
  if (draft === undefined)
@@ -294,8 +516,8 @@ export function removeOrphanedConversationAnnotationPlaceholder(input) {
294
516
  input.setDraft(draft);
295
517
  return true;
296
518
  }
297
- /** Rehydrate a lost occurrence only over an exact stored plugin projection. */
298
- export function restoreConversationAnnotationReference(input, ref) {
519
+ /** Rehydrate a lost occurrence over either its display draft or exact mirror projection. */
520
+ export function restoreConversationAnnotationReference(input, ref, expectedMirrorDraft, expectedBaseDraft) {
299
521
  let annotations;
300
522
  try {
301
523
  annotations = decodeStoredSelectionReference(ref);
@@ -303,11 +525,35 @@ export function restoreConversationAnnotationReference(input, ref) {
303
525
  catch {
304
526
  return false;
305
527
  }
306
- const draft = draftWithoutOrphanedAnnotationPrefix(input.state.getSnapshot(), ref);
528
+ const snapshot = input.state.getSnapshot();
529
+ const orphanDraft = draftWithoutOrphanedAnnotationPrefix(snapshot);
530
+ const clipboardText = annotations.map(annotation => annotation.text).join('\n\n');
531
+ const mirrorBaseDraft = expectedMirrorDraft === undefined
532
+ ? undefined
533
+ : baseDraftFromMirror(expectedMirrorDraft, clipboardText, expectedBaseDraft);
534
+ const draft = orphanDraft
535
+ ?? (expectedMirrorDraft !== undefined
536
+ && snapshot.occurrences.length === 0
537
+ && snapshot.draft === expectedMirrorDraft
538
+ && mirrorBaseDraft !== undefined
539
+ ? mirrorBaseDraft
540
+ : undefined);
307
541
  if (draft === undefined)
308
542
  return false;
309
- input.setDraft(draft);
310
- return writeConversationAnnotations(input, input.state.getSnapshot(), annotations);
543
+ if (draft !== snapshot.draft)
544
+ input.setDraft(draft);
545
+ const beforeWrite = input.state.getSnapshot();
546
+ if (beforeWrite.draft !== draft || beforeWrite.occurrences.length !== 0)
547
+ return false;
548
+ if (writeConversationAnnotations(input, beforeWrite, annotations))
549
+ return true;
550
+ // Recovery must be content preserving. If DSH rejects insertion, put the
551
+ // exact mirror projection back rather than leaving a partially stripped draft.
552
+ const afterFailure = input.state.getSnapshot();
553
+ if (afterFailure.occurrences.length === 0 && expectedMirrorDraft !== undefined) {
554
+ input.setDraft(expectedMirrorDraft);
555
+ }
556
+ return false;
311
557
  }
312
558
  function unescapeXmlText(value) {
313
559
  return value
@@ -371,4 +617,3 @@ export function buildSideChatPrompt(selection, question) {
371
617
  ].join('\n'),
372
618
  }];
373
619
  }
374
- //# sourceMappingURL=add-to-conversation.js.map
@@ -5,7 +5,7 @@ interface AnnotationStorage {
5
5
  setItem(key: string, value: string): void;
6
6
  removeItem(key: string): void;
7
7
  }
8
- /** Tab-scoped recovery for drafts persisted without their runtime occurrences. */
8
+ /** Tab-scoped recovery for drafts persisted as a reference clipboard projection. */
9
9
  export declare class ConversationAnnotationPersistence {
10
10
  private readonly storage;
11
11
  private readonly observedSessions;
@@ -16,4 +16,3 @@ export declare class ConversationAnnotationPersistence {
16
16
  private remove;
17
17
  }
18
18
  export {};
19
- //# sourceMappingURL=annotation-persistence.d.ts.map
@@ -1,4 +1,4 @@
1
- import { conversationAnnotationReference, removeOrphanedConversationAnnotationPlaceholder, restoreConversationAnnotationReference, } from './add-to-conversation.js';
1
+ import { conversationAnnotationRecoveryRecord, removeOrphanedConversationAnnotationPlaceholder, restoreConversationAnnotationReference, } from './add-to-conversation.js';
2
2
  const STORAGE_PREFIX = 'dsh-side-chat:composer-annotations:';
3
3
  function browserSessionStorage() {
4
4
  if (typeof window === 'undefined')
@@ -13,37 +13,17 @@ function browserSessionStorage() {
13
13
  function storageKey(sessionId) {
14
14
  return `${STORAGE_PREFIX}${encodeURIComponent(sessionId)}`;
15
15
  }
16
- function clipboardProjection(snapshot) {
17
- let draft = snapshot.draft;
18
- const occurrences = [...snapshot.occurrences].sort((a, b) => b.offset - a.offset);
19
- for (const occurrence of occurrences) {
20
- const length = occurrence.length ?? 1;
21
- if (!Number.isSafeInteger(occurrence.offset)
22
- || occurrence.offset < 0
23
- || !Number.isSafeInteger(length)
24
- || length <= 0
25
- || occurrence.offset + length > draft.length
26
- || occurrence.clipboardText === undefined)
27
- return;
28
- draft = draft.slice(0, occurrence.offset)
29
- + occurrence.clipboardText
30
- + draft.slice(occurrence.offset + length);
31
- }
32
- return draft;
33
- }
34
16
  function recordOf(snapshot) {
35
- const ref = conversationAnnotationReference(snapshot);
36
- if (ref === undefined)
37
- return;
38
- const projection = clipboardProjection(snapshot);
39
- return {
40
- version: 1,
41
- draft: snapshot.draft,
42
- ...(projection === undefined ? {} : { projection }),
43
- ref,
44
- };
17
+ const record = conversationAnnotationRecoveryRecord(snapshot);
18
+ return record === undefined ? undefined : { version: 2, ...record };
45
19
  }
46
- /** Tab-scoped recovery for drafts persisted without their runtime occurrences. */
20
+ function displayDraft(record) {
21
+ return record.version === 1 ? record.draft : record.displayDraft;
22
+ }
23
+ function mirrorDraft(record) {
24
+ return record.version === 1 ? record.projection : record.mirrorDraft;
25
+ }
26
+ /** Tab-scoped recovery for drafts persisted as a reference clipboard projection. */
47
27
  export class ConversationAnnotationPersistence {
48
28
  storage;
49
29
  observedSessions = new Set();
@@ -60,14 +40,24 @@ export class ConversationAnnotationPersistence {
60
40
  return;
61
41
  }
62
42
  const stored = this.read(key);
63
- if (stored !== undefined
64
- && (snapshot.draft === stored.draft || snapshot.draft === stored.projection)) {
65
- this.observedSessions.add(key);
66
- if (restoreConversationAnnotationReference(input, stored.ref))
43
+ if (stored !== undefined) {
44
+ const exactDisplayDraft = snapshot.draft === displayDraft(stored);
45
+ const storedMirrorDraft = mirrorDraft(stored);
46
+ const exactMirrorDraft = storedMirrorDraft !== undefined && snapshot.draft === storedMirrorDraft;
47
+ if (exactDisplayDraft || exactMirrorDraft) {
48
+ this.observedSessions.add(key);
49
+ const restored = restoreConversationAnnotationReference(input, stored.ref, storedMirrorDraft, stored.version === 2 ? stored.baseDraft : undefined);
50
+ if (restored)
51
+ return;
52
+ // A refused insertion may move an exact display record to its exact
53
+ // mirror. Keep either v1 or v2 storage retryable from that safe state.
54
+ if (storedMirrorDraft !== undefined
55
+ && input.state.getSnapshot().draft === storedMirrorDraft)
56
+ return;
57
+ this.remove(key);
58
+ removeOrphanedConversationAnnotationPlaceholder(input);
67
59
  return;
68
- this.remove(key);
69
- removeOrphanedConversationAnnotationPlaceholder(input);
70
- return;
60
+ }
71
61
  }
72
62
  // A live occurrence disappeared in this plugin lifetime: removal or send
73
63
  // is authoritative, including the transition to an empty draft.
@@ -92,15 +82,28 @@ export class ConversationAnnotationPersistence {
92
82
  if (raw === null)
93
83
  return;
94
84
  const value = JSON.parse(raw);
95
- return value.version === 1
85
+ if (value.version === 1
96
86
  && typeof value.draft === 'string'
97
87
  && typeof value.ref === 'string'
98
- && (value.projection === undefined || typeof value.projection === 'string')
99
- ? {
88
+ && (value.projection === undefined || typeof value.projection === 'string')) {
89
+ return {
100
90
  version: 1,
101
91
  draft: value.draft,
102
92
  ...(value.projection === undefined ? {} : { projection: value.projection }),
103
93
  ref: value.ref,
94
+ };
95
+ }
96
+ return value.version === 2
97
+ && typeof value.displayDraft === 'string'
98
+ && typeof value.mirrorDraft === 'string'
99
+ && typeof value.baseDraft === 'string'
100
+ && typeof value.ref === 'string'
101
+ ? {
102
+ version: 2,
103
+ displayDraft: value.displayDraft,
104
+ mirrorDraft: value.mirrorDraft,
105
+ baseDraft: value.baseDraft,
106
+ ref: value.ref,
104
107
  }
105
108
  : undefined;
106
109
  }
@@ -129,4 +132,3 @@ export class ConversationAnnotationPersistence {
129
132
  }
130
133
  }
131
134
  }
132
- //# sourceMappingURL=annotation-persistence.js.map