@ahggg/dsh-side-chat 0.7.3 → 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.
- package/README.md +15 -12
- package/README.zh-CN.md +15 -12
- package/lib/client/apply.js +48 -32
- package/lib/client/contracts.d.ts +3 -58
- package/lib/client/dsh/ReadOnlyConversation.d.ts +10 -0
- package/lib/client/dsh/ReadOnlyConversation.js +91 -0
- package/lib/client/dsh/SideChatOverlay.d.ts +7 -0
- package/lib/client/{rc6/Rc6SideChatOverlay.js → dsh/SideChatOverlay.js} +26 -56
- package/lib/client/{rc6 → dsh}/context.d.ts +4 -6
- package/lib/client/dsh/conversation-store.d.ts +22 -0
- package/lib/client/dsh/conversation-store.js +19 -0
- package/lib/client/dsh/primitive-labels.d.ts +2 -0
- package/lib/client/dsh/primitive-labels.js +4 -0
- package/lib/client/{rc6 → dsh}/remote-adapter.d.ts +2 -2
- package/lib/client/dsh/remote-adapter.js +22 -0
- package/lib/client/dsh/sessions-adapter.d.ts +56 -0
- package/lib/client/dsh/sessions-adapter.js +185 -0
- package/lib/client/index.d.ts +2 -2
- package/lib/client/panel/SelectionQuote.js +8 -1
- package/lib/client/panel/SideChatErrorState.js +1 -1
- package/lib/client/panel/SideChatModelSelect.js +37 -18
- package/lib/client/panel/SideChatPanel.js +12 -5
- package/lib/client/panel/messages.d.ts +1 -1
- package/lib/client/panel/messages.js +8 -8
- package/lib/client/parent-composer/ParentConversationAnnotations.d.ts +3 -2
- package/lib/client/parent-composer/ParentConversationAnnotations.js +50 -30
- package/lib/client/parent-composer/add-to-conversation.d.ts +14 -28
- package/lib/client/parent-composer/add-to-conversation.js +131 -545
- package/lib/client/parent-composer/annotation-persistence.d.ts +8 -8
- package/lib/client/parent-composer/annotation-persistence.js +106 -102
- package/lib/client/parent-composer/composer-reference.d.ts +17 -55
- package/lib/client/parent-composer/composer-reference.js +20 -72
- package/lib/client/parent-composer/referenced-conversation.d.ts +5 -7
- package/lib/client/parent-composer/referenced-conversation.js +21 -73
- package/lib/client/selection/ConversationAnnotationMarkers.js +5 -1
- package/lib/client/selection/SelectionActions.js +2 -1
- package/lib/client/selection/conversation-dom.d.ts +5 -0
- package/lib/client/selection/conversation-dom.js +24 -0
- package/lib/client/selection/selection-controller.d.ts +1 -1
- package/lib/client/selection/selection-controller.js +7 -3
- package/lib/client/selection/selection-normalizer.js +1 -1
- package/lib/client/side-chat-controller.d.ts +28 -41
- package/lib/client/side-chat-controller.js +245 -337
- package/lib/client.js +81 -52
- package/lib/host/index.d.ts +1 -1
- package/lib/host/index.js +1 -1
- package/lib/host/read-only-chat-service.d.ts +30 -0
- package/lib/host/read-only-chat-service.js +314 -0
- package/lib/index.d.ts +16 -8
- package/lib/index.js +34 -13
- package/lib/remote.d.ts +23 -14
- package/lib/remote.js +2 -5
- package/lib/shared/constants.d.ts +0 -13
- package/lib/shared/constants.js +0 -13
- package/lib/shared/contracts.d.ts +52 -19
- package/lib/shared/contracts.js +2 -3
- package/lib/shared/error-codes.d.ts +1 -1
- package/lib/shared/error-codes.js +2 -2
- package/lib/shared/{archived-wire.d.ts → side-chat-wire.d.ts} +109 -20
- package/lib/shared/side-chat-wire.js +34 -0
- package/lib/typert.d.ts +3 -68
- package/lib/typert.js +60 -4
- package/package.json +76 -56
- package/lib/client/rc6/ArchivedConversation.d.ts +0 -14
- package/lib/client/rc6/ArchivedConversation.js +0 -231
- package/lib/client/rc6/Rc6SideChatOverlay.d.ts +0 -7
- package/lib/client/rc6/SideChatTool.d.ts +0 -22
- package/lib/client/rc6/SideChatTool.js +0 -404
- package/lib/client/rc6/primitive-labels.d.ts +0 -7
- package/lib/client/rc6/primitive-labels.js +0 -60
- package/lib/client/rc6/remote-adapter.js +0 -34
- package/lib/client/rc6/runtime-compat.d.ts +0 -120
- package/lib/client/rc6/runtime-compat.js +0 -187
- package/lib/client/rc6/sessions-adapter.d.ts +0 -64
- package/lib/client/rc6/sessions-adapter.js +0 -387
- package/lib/host/archived-fork-service.d.ts +0 -28
- package/lib/host/archived-fork-service.js +0 -309
- package/lib/shared/archived-wire.js +0 -47
- /package/lib/client/{rc6 → dsh}/context.js +0 -0
|
@@ -1,38 +1,19 @@
|
|
|
1
|
-
import {
|
|
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('<', '<')
|
|
8
|
-
.replaceAll('>', '>');
|
|
5
|
+
return value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>');
|
|
9
6
|
}
|
|
10
|
-
function
|
|
11
|
-
|
|
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('<', '<').replaceAll('>', '>').replaceAll('&', '&');
|
|
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
|
-
||
|
|
32
|
-
||
|
|
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.
|
|
47
|
-
&&
|
|
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
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
117
|
-
return selectionOccurrences(snapshot).flatMap(
|
|
56
|
+
function storedAnnotations(snapshot) {
|
|
57
|
+
return selectionOccurrences(snapshot).flatMap(occurrence => {
|
|
118
58
|
try {
|
|
119
59
|
return [...decodeStoredSelectionReference(occurrence.ref)];
|
|
120
60
|
}
|
|
@@ -123,497 +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
|
|
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
|
-
|
|
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;
|
|
143
|
-
}
|
|
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
|
-
const occurrences = selectionOccurrences(snapshot);
|
|
167
|
-
if (occurrences.length === 0)
|
|
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
|
-
}
|
|
183
|
-
input.setDraft(draftWithoutSelectionOccurrences(snapshot));
|
|
184
|
-
return true;
|
|
70
|
+
return storedAnnotations(snapshot).map((annotation, annotationIndex) => ({ ...annotation, annotationIndex }));
|
|
185
71
|
}
|
|
186
|
-
/**
|
|
187
|
-
|
|
188
|
-
trigger: '@',
|
|
189
|
-
name: SELECTION_REFERENCE_SOURCE,
|
|
190
|
-
order: 1_000,
|
|
191
|
-
candidates: async () => [],
|
|
192
|
-
onPick: () => undefined,
|
|
193
|
-
codec: {
|
|
194
|
-
clipboardText: ref => decodeSelectionReference(ref).map(annotation => annotation.text).join('\n\n'),
|
|
195
|
-
serialize: async (ref, signal) => {
|
|
196
|
-
if (signal.aborted)
|
|
197
|
-
throw signal.reason;
|
|
198
|
-
return selectedContext(decodeSelectionReference(ref));
|
|
199
|
-
},
|
|
200
|
-
},
|
|
201
|
-
};
|
|
202
|
-
function writeTextConversationAnnotations(input, before, annotations) {
|
|
203
|
-
const draft = draftWithoutSelectionOccurrences(before);
|
|
204
|
-
const insertionState = input.state.getSnapshot();
|
|
205
|
-
if (insertionState.draftRev !== before.draftRev || insertionState.draft !== before.draft)
|
|
206
|
-
return false;
|
|
207
|
-
const ref = encodeSelectionReference(annotations);
|
|
208
|
-
const inserted = input.insertReference({
|
|
209
|
-
source: SELECTION_REFERENCE_SOURCE,
|
|
210
|
-
ref,
|
|
211
|
-
label: SELECTION_REFERENCE_LABEL,
|
|
212
|
-
clipboardText: annotations.map(annotation => annotation.text).join('\n\n'),
|
|
213
|
-
}, {
|
|
214
|
-
start: 0,
|
|
215
|
-
end: 0,
|
|
216
|
-
draftRev: insertionState.draftRev,
|
|
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.
|
|
220
|
-
if (!inserted)
|
|
221
|
-
return false;
|
|
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();
|
|
243
|
-
return false;
|
|
244
|
-
}
|
|
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;
|
|
72
|
+
/** A single atomic chip replacement preserves other references and the user's draft. */
|
|
73
|
+
function writeAnnotations(input, before, annotations) {
|
|
290
74
|
const existing = selectionOccurrences(before);
|
|
291
75
|
if (existing.length > 1)
|
|
292
76
|
return false;
|
|
293
77
|
const previous = existing[0];
|
|
294
|
-
|
|
295
|
-
|
|
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);
|
|
78
|
+
const span = previous === undefined ? { start: 0, end: 0, draftRev: before.draftRev }
|
|
79
|
+
: occurrenceEditSpan(input, before, previous);
|
|
312
80
|
if (span === undefined)
|
|
313
81
|
return false;
|
|
314
|
-
const ref =
|
|
315
|
-
const clipboardText = annotations.map(annotation => annotation.text).join('\n\n');
|
|
82
|
+
const ref = JSON.stringify({ version: 2, annotations });
|
|
316
83
|
const inserted = input.insertReference({
|
|
317
|
-
source: SELECTION_REFERENCE_SOURCE,
|
|
318
|
-
|
|
319
|
-
label: SELECTION_REFERENCE_LABEL,
|
|
320
|
-
clipboardText,
|
|
84
|
+
source: SELECTION_REFERENCE_SOURCE, ref, label: SELECTION_REFERENCE_LABEL,
|
|
85
|
+
clipboardText: annotations.map(annotation => annotation.text).join('\n\n'),
|
|
321
86
|
}, 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
|
-
}
|
|
87
|
+
if (!inserted)
|
|
329
88
|
return false;
|
|
330
|
-
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
|
|
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);
|
|
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);
|
|
365
93
|
}
|
|
366
|
-
/** Add one passage to the parent composer's aggregated annotation occurrence. */
|
|
367
94
|
export function addSelectionToConversation(input, selection, comment) {
|
|
368
95
|
const before = input.state.getSnapshot();
|
|
369
|
-
const
|
|
370
|
-
return
|
|
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;
|
|
371
116
|
}
|
|
372
|
-
/** Remove one unsent selected-passage annotation, retaining the aggregate when needed. */
|
|
373
117
|
export function removeConversationAnnotation(input, annotationIndex) {
|
|
374
118
|
const before = input.state.getSnapshot();
|
|
375
|
-
const annotations = [...
|
|
119
|
+
const annotations = [...storedAnnotations(before)];
|
|
376
120
|
if (!Number.isSafeInteger(annotationIndex) || annotations[annotationIndex] === undefined)
|
|
377
121
|
return false;
|
|
378
122
|
annotations.splice(annotationIndex, 1);
|
|
379
|
-
|
|
380
|
-
return removeConversationAnnotations(input);
|
|
381
|
-
return writeConversationAnnotations(input, before, annotations);
|
|
123
|
+
return annotations.length === 0 ? removeConversationAnnotations(input) : writeAnnotations(input, before, annotations);
|
|
382
124
|
}
|
|
383
|
-
/** Replace the optional comment on one unsent selected-passage annotation. */
|
|
384
125
|
export function updateConversationAnnotation(input, annotationIndex, comment) {
|
|
385
126
|
const before = input.state.getSnapshot();
|
|
386
|
-
const annotations = [...
|
|
387
|
-
const
|
|
388
|
-
if (!Number.isSafeInteger(annotationIndex) ||
|
|
389
|
-
return false;
|
|
390
|
-
const trimmedComment = comment?.trim();
|
|
391
|
-
annotations[annotationIndex] = {
|
|
392
|
-
text: current.text,
|
|
393
|
-
...(current.selection === undefined ? {} : { selection: current.selection }),
|
|
394
|
-
...(trimmedComment === undefined || trimmedComment.length === 0
|
|
395
|
-
? {}
|
|
396
|
-
: { comment: trimmedComment }),
|
|
397
|
-
};
|
|
398
|
-
return writeConversationAnnotations(input, before, annotations);
|
|
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
|
-
}
|
|
479
|
-
/** Return the valid aggregated reference currently occupying the leading draft slot. */
|
|
480
|
-
export function conversationAnnotationReference(snapshot) {
|
|
481
|
-
const occurrence = selectionOccurrences(snapshot).find(candidate => candidate.offset === 0);
|
|
482
|
-
if (occurrence === undefined
|
|
483
|
-
|| !occurrenceMatchesDraft(snapshot, occurrence, SELECTION_REFERENCE_LABEL))
|
|
484
|
-
return;
|
|
485
|
-
try {
|
|
486
|
-
decodeStoredSelectionReference(occurrence.ref);
|
|
487
|
-
return occurrence.ref;
|
|
488
|
-
}
|
|
489
|
-
catch {
|
|
490
|
-
return;
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
function draftWithoutOrphanedAnnotationPrefix(snapshot) {
|
|
494
|
-
if (snapshot.occurrences.some(occurrence => occurrence.offset === 0))
|
|
495
|
-
return;
|
|
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;
|
|
510
|
-
}
|
|
511
|
-
/** Remove the plugin-owned prefix when rc.6 restored its draft without occurrences. */
|
|
512
|
-
export function removeOrphanedConversationAnnotationPlaceholder(input) {
|
|
513
|
-
const draft = draftWithoutOrphanedAnnotationPrefix(input.state.getSnapshot());
|
|
514
|
-
if (draft === undefined)
|
|
515
|
-
return false;
|
|
516
|
-
input.setDraft(draft);
|
|
517
|
-
return true;
|
|
518
|
-
}
|
|
519
|
-
/** Rehydrate a lost occurrence over either its display draft or exact mirror projection. */
|
|
520
|
-
export function restoreConversationAnnotationReference(input, ref, expectedMirrorDraft, expectedBaseDraft) {
|
|
521
|
-
let annotations;
|
|
522
|
-
try {
|
|
523
|
-
annotations = decodeStoredSelectionReference(ref);
|
|
524
|
-
}
|
|
525
|
-
catch {
|
|
127
|
+
const annotations = [...storedAnnotations(before)];
|
|
128
|
+
const annotation = annotations[annotationIndex];
|
|
129
|
+
if (!Number.isSafeInteger(annotationIndex) || annotation === undefined)
|
|
526
130
|
return false;
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
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);
|
|
541
|
-
if (draft === undefined)
|
|
542
|
-
return false;
|
|
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;
|
|
131
|
+
const trimmed = comment?.trim();
|
|
132
|
+
annotations[annotationIndex] = { text: annotation.text, selection: annotation.selection,
|
|
133
|
+
...(trimmed ? { comment: trimmed } : {}) };
|
|
134
|
+
return writeAnnotations(input, before, annotations);
|
|
557
135
|
}
|
|
558
|
-
function
|
|
559
|
-
return
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
const selectedText = /^<selected_text>\n([\s\S]*?)\n<\/selected_text>(?:\n<user_comment>\n([\s\S]*?)\n<\/user_comment>)?$/u.exec(body);
|
|
566
|
-
if (selectedText === null)
|
|
567
|
-
return { text: unescapeXmlText(body) };
|
|
568
|
-
const comment = selectedText[2];
|
|
569
|
-
return {
|
|
570
|
-
text: unescapeXmlText(selectedText[1] ?? ''),
|
|
571
|
-
...(comment === undefined ? {} : { comment: unescapeXmlText(comment) }),
|
|
572
|
-
};
|
|
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');
|
|
573
143
|
}
|
|
574
|
-
|
|
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
|
+
};
|
|
575
156
|
export function parseAnnotatedConversationPrompt(text) {
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
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]) }) });
|
|
585
172
|
}
|
|
586
173
|
}
|
|
587
|
-
|
|
588
|
-
if (legacyXml !== null) {
|
|
589
|
-
return {
|
|
590
|
-
annotations: [{ text: unescapeXmlText(legacyXml[2] ?? '') }],
|
|
591
|
-
message: text.slice(legacyXml[0].length).trim(),
|
|
592
|
-
};
|
|
593
|
-
}
|
|
594
|
-
const legacyText = /^Selected passage from the current conversation \(message (\d+)\):\n([\s\S]*?)\nEnd selected passage\.\s*/u.exec(text);
|
|
595
|
-
if (legacyText === null)
|
|
596
|
-
return undefined;
|
|
597
|
-
return {
|
|
598
|
-
annotations: [{ text: legacyText[2] ?? '' }],
|
|
599
|
-
message: text.slice(legacyText[0].length).trim(),
|
|
600
|
-
};
|
|
174
|
+
return annotations.length === 0 ? undefined : { annotations, message: text.slice(prefix[0].length).trim() };
|
|
601
175
|
}
|
|
602
|
-
/**
|
|
176
|
+
/** The first prompt uses the same durable annotation format as the main composer. */
|
|
603
177
|
export function buildSideChatPrompt(selection, question) {
|
|
604
178
|
const trimmed = question.trim();
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
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;
|
|
619
205
|
}
|