@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,18 +1,18 @@
|
|
|
1
1
|
import type { SessionId } from '../../shared/contracts.js';
|
|
2
|
-
import type { ParentComposerInput } from './
|
|
3
|
-
interface
|
|
2
|
+
import type { ParentComposerInput } from './composer-reference.js';
|
|
3
|
+
interface ReferenceStorage {
|
|
4
4
|
getItem(key: string): string | null;
|
|
5
5
|
setItem(key: string, value: string): void;
|
|
6
6
|
removeItem(key: string): void;
|
|
7
7
|
}
|
|
8
|
-
/**
|
|
8
|
+
/** Recover the complete reference-bearing draft, not only a leading annotation.
|
|
9
|
+
* DSH persists clipboard text, so conversation chips need recovery as well. */
|
|
9
10
|
export declare class ConversationAnnotationPersistence {
|
|
10
11
|
private readonly storage;
|
|
11
|
-
private readonly
|
|
12
|
-
|
|
12
|
+
private readonly observedInputs;
|
|
13
|
+
private reconciling;
|
|
14
|
+
constructor(storage?: ReferenceStorage | undefined);
|
|
13
15
|
reconcile(sessionId: SessionId, input: ParentComposerInput): void;
|
|
14
|
-
private
|
|
15
|
-
private write;
|
|
16
|
-
private remove;
|
|
16
|
+
private restore;
|
|
17
17
|
}
|
|
18
18
|
export {};
|
|
@@ -1,134 +1,138 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { SELECTION_REFERENCE_SOURCE } from './add-to-conversation.js';
|
|
2
|
+
import { SIDE_CHAT_CONVERSATION_REFERENCE_SOURCE } from './referenced-conversation.js';
|
|
3
|
+
const STORAGE_PREFIX = 'dsh-side-chat:composer-references:';
|
|
3
4
|
function browserSessionStorage() {
|
|
4
|
-
if (typeof window === 'undefined')
|
|
5
|
-
return;
|
|
6
5
|
try {
|
|
7
|
-
return window.sessionStorage;
|
|
6
|
+
return typeof window === 'undefined' ? undefined : window.sessionStorage;
|
|
8
7
|
}
|
|
9
8
|
catch {
|
|
10
9
|
return;
|
|
11
10
|
}
|
|
12
11
|
}
|
|
13
|
-
function
|
|
14
|
-
return
|
|
15
|
-
|
|
16
|
-
function recordOf(snapshot) {
|
|
17
|
-
const record = conversationAnnotationRecoveryRecord(snapshot);
|
|
18
|
-
return record === undefined ? undefined : { version: 2, ...record };
|
|
12
|
+
function ownsReference(snapshot) {
|
|
13
|
+
return snapshot.occurrences.some(occurrence => occurrence.source === SELECTION_REFERENCE_SOURCE
|
|
14
|
+
|| occurrence.source === SIDE_CHAT_CONVERSATION_REFERENCE_SOURCE);
|
|
19
15
|
}
|
|
20
|
-
function
|
|
21
|
-
|
|
16
|
+
function validOccurrences(draft, value) {
|
|
17
|
+
if (!Array.isArray(value) || value.length === 0)
|
|
18
|
+
return false;
|
|
19
|
+
let end = 0;
|
|
20
|
+
return value.every((candidate) => {
|
|
21
|
+
if (typeof candidate !== 'object' || candidate === null)
|
|
22
|
+
return false;
|
|
23
|
+
const item = candidate;
|
|
24
|
+
if (!Number.isSafeInteger(item.offset) || !Number.isSafeInteger(item.length)
|
|
25
|
+
|| item.offset < end || item.length < 0 || item.offset + item.length > draft.length
|
|
26
|
+
|| typeof item.source !== 'string' || typeof item.ref !== 'string'
|
|
27
|
+
|| typeof item.label !== 'string' || typeof item.clipboardText !== 'string'
|
|
28
|
+
|| draft.slice(item.offset, item.offset + item.length) !== item.clipboardText
|
|
29
|
+
|| (item.appearance !== undefined && !['session', 'file', 'folder'].includes(item.appearance)))
|
|
30
|
+
return false;
|
|
31
|
+
end = item.offset + item.length;
|
|
32
|
+
return true;
|
|
33
|
+
});
|
|
22
34
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
/** Tab-scoped recovery for drafts persisted as a reference clipboard projection. */
|
|
35
|
+
/** Recover the complete reference-bearing draft, not only a leading annotation.
|
|
36
|
+
* DSH persists clipboard text, so conversation chips need recovery as well. */
|
|
27
37
|
export class ConversationAnnotationPersistence {
|
|
28
38
|
storage;
|
|
29
|
-
|
|
39
|
+
observedInputs = new WeakSet();
|
|
40
|
+
reconciling = false;
|
|
30
41
|
constructor(storage = browserSessionStorage()) {
|
|
31
42
|
this.storage = storage;
|
|
32
43
|
}
|
|
33
44
|
reconcile(sessionId, input) {
|
|
34
|
-
|
|
35
|
-
const snapshot = input.state.getSnapshot();
|
|
36
|
-
const current = recordOf(snapshot);
|
|
37
|
-
if (current !== undefined) {
|
|
38
|
-
this.observedSessions.add(key);
|
|
39
|
-
this.write(key, current);
|
|
45
|
+
if (this.reconciling || this.storage === undefined)
|
|
40
46
|
return;
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if (storedMirrorDraft !== undefined
|
|
55
|
-
&& input.state.getSnapshot().draft === storedMirrorDraft)
|
|
56
|
-
return;
|
|
57
|
-
this.remove(key);
|
|
58
|
-
removeOrphanedConversationAnnotationPlaceholder(input);
|
|
47
|
+
this.reconciling = true;
|
|
48
|
+
const key = STORAGE_PREFIX + encodeURIComponent(sessionId);
|
|
49
|
+
try {
|
|
50
|
+
const snapshot = input.state.getSnapshot();
|
|
51
|
+
if (ownsReference(snapshot)) {
|
|
52
|
+
this.observedInputs.add(input);
|
|
53
|
+
this.storage.setItem(key, JSON.stringify({ version: 1, draft: snapshot.draft, occurrences: snapshot.occurrences }));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
// Deleting a chip in the same editor is authoritative. Never resurrect
|
|
57
|
+
// it just because its plain-text clipboard projection is unchanged.
|
|
58
|
+
if (this.observedInputs.has(input)) {
|
|
59
|
+
this.storage.removeItem(key);
|
|
59
60
|
return;
|
|
60
61
|
}
|
|
61
|
-
}
|
|
62
|
-
// A live occurrence disappeared in this plugin lifetime: removal or send
|
|
63
|
-
// is authoritative, including the transition to an empty draft.
|
|
64
|
-
if (this.observedSessions.has(key)) {
|
|
65
|
-
this.remove(key);
|
|
66
|
-
removeOrphanedConversationAnnotationPlaceholder(input);
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
// A new shell starts empty before ConversationSession seeds its persisted
|
|
70
|
-
// draft. Keep the tab record until that one mount-time adoption occurs.
|
|
71
|
-
if (stored !== undefined && snapshot.draft === '')
|
|
72
|
-
return;
|
|
73
|
-
if (stored !== undefined)
|
|
74
|
-
this.remove(key);
|
|
75
|
-
removeOrphanedConversationAnnotationPlaceholder(input);
|
|
76
|
-
}
|
|
77
|
-
read(key) {
|
|
78
|
-
if (this.storage === undefined)
|
|
79
|
-
return;
|
|
80
|
-
try {
|
|
81
62
|
const raw = this.storage.getItem(key);
|
|
82
63
|
if (raw === null)
|
|
83
64
|
return;
|
|
84
|
-
const
|
|
85
|
-
if (
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
return {
|
|
90
|
-
version: 1,
|
|
91
|
-
draft: value.draft,
|
|
92
|
-
...(value.projection === undefined ? {} : { projection: value.projection }),
|
|
93
|
-
ref: value.ref,
|
|
94
|
-
};
|
|
65
|
+
const stored = JSON.parse(raw);
|
|
66
|
+
if (stored === null || stored.version !== 1 || typeof stored.draft !== 'string'
|
|
67
|
+
|| !validOccurrences(stored.draft, stored.occurrences)) {
|
|
68
|
+
this.storage.removeItem(key);
|
|
69
|
+
return;
|
|
95
70
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
: undefined;
|
|
71
|
+
// A fresh Session shell starts empty before its persisted draft is seeded.
|
|
72
|
+
if (snapshot.draft === '')
|
|
73
|
+
return;
|
|
74
|
+
if (snapshot.draft !== stored.draft || snapshot.occurrences.length !== 0) {
|
|
75
|
+
this.storage.removeItem(key);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (snapshot.phase !== undefined && snapshot.phase !== 'plain')
|
|
79
|
+
return;
|
|
80
|
+
if (this.restore(input, stored))
|
|
81
|
+
this.observedInputs.add(input);
|
|
109
82
|
}
|
|
110
83
|
catch {
|
|
111
|
-
|
|
84
|
+
// Storage denial or a stale codec must not alter or erase the user's draft.
|
|
112
85
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if (this.storage === undefined)
|
|
116
|
-
return;
|
|
117
|
-
try {
|
|
118
|
-
this.storage.setItem(key, JSON.stringify(record));
|
|
119
|
-
}
|
|
120
|
-
catch {
|
|
121
|
-
// Draft recovery is best-effort; the orphan-prefix guard remains active.
|
|
86
|
+
finally {
|
|
87
|
+
this.reconciling = false;
|
|
122
88
|
}
|
|
123
89
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
90
|
+
restore(input, stored) {
|
|
91
|
+
let ownedSnapshot;
|
|
92
|
+
let complete = false;
|
|
127
93
|
try {
|
|
128
|
-
|
|
94
|
+
// Right-to-left edits leave every unprocessed clipboard offset unchanged.
|
|
95
|
+
for (const occurrence of [...stored.occurrences].reverse()) {
|
|
96
|
+
const before = input.state.getSnapshot();
|
|
97
|
+
if (before.draft !== stored.draft)
|
|
98
|
+
return false;
|
|
99
|
+
if (!input.insertReference({
|
|
100
|
+
source: occurrence.source, ref: occurrence.ref, label: occurrence.label,
|
|
101
|
+
clipboardText: occurrence.clipboardText,
|
|
102
|
+
...(occurrence.appearance === undefined ? {} : { appearance: occurrence.appearance }),
|
|
103
|
+
}, { start: occurrence.offset, end: occurrence.offset + occurrence.length, draftRev: before.draftRev }))
|
|
104
|
+
return false;
|
|
105
|
+
const after = input.state.getSnapshot();
|
|
106
|
+
const addedGap = stored.draft[occurrence.offset + occurrence.length] !== ' ';
|
|
107
|
+
const expected = addedGap
|
|
108
|
+
? stored.draft.slice(0, occurrence.offset + occurrence.length) + ' '
|
|
109
|
+
+ stored.draft.slice(occurrence.offset + occurrence.length)
|
|
110
|
+
: stored.draft;
|
|
111
|
+
if (after.draft !== expected)
|
|
112
|
+
return false;
|
|
113
|
+
ownedSnapshot = after;
|
|
114
|
+
if (addedGap) {
|
|
115
|
+
if (!input.replaceText('', {
|
|
116
|
+
start: occurrence.offset + 1, end: occurrence.offset + 2, draftRev: after.draftRev,
|
|
117
|
+
}))
|
|
118
|
+
return false;
|
|
119
|
+
const normalized = input.state.getSnapshot();
|
|
120
|
+
if (normalized.draft !== stored.draft)
|
|
121
|
+
return false;
|
|
122
|
+
ownedSnapshot = normalized;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
complete = true;
|
|
126
|
+
return true;
|
|
129
127
|
}
|
|
130
|
-
|
|
131
|
-
//
|
|
128
|
+
finally {
|
|
129
|
+
// Keep a refused multi-chip restoration retryable; don't save a partial
|
|
130
|
+
// projection, and don't roll back edits from synchronous subscribers.
|
|
131
|
+
const current = input.state.getSnapshot();
|
|
132
|
+
if (!complete && ownedSnapshot === current) {
|
|
133
|
+
const length = current.draft.length - current.occurrences.reduce((sum, item) => sum + item.length - 1, 0);
|
|
134
|
+
input.replaceText(stored.draft, { start: 0, end: length, draftRev: current.draftRev });
|
|
135
|
+
}
|
|
132
136
|
}
|
|
133
137
|
}
|
|
134
138
|
}
|
|
@@ -1,49 +1,23 @@
|
|
|
1
1
|
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
-
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
/** Occupied projection length (display text through 0.1.1, clipboard text in 0.1.2). */
|
|
9
|
-
readonly length?: number;
|
|
10
|
-
/** Insert-time display label, present in current DSH input snapshots. */
|
|
11
|
-
readonly label?: string;
|
|
12
|
-
/** Draft-persistence projection cached by current DSH input snapshots. */
|
|
13
|
-
readonly clipboardText?: string;
|
|
14
|
-
readonly appearance?: 'session' | 'file' | 'folder';
|
|
15
|
-
}
|
|
16
|
-
export interface ParentComposerInputSnapshot {
|
|
17
|
-
readonly draft: string;
|
|
18
|
-
readonly draftRev: number;
|
|
19
|
-
readonly occurrences: readonly ParentComposerOccurrence[];
|
|
20
|
-
}
|
|
21
|
-
export interface ParentComposerSpan {
|
|
22
|
-
readonly start: number;
|
|
23
|
-
readonly end: number;
|
|
24
|
-
readonly draftRev: number;
|
|
25
|
-
}
|
|
2
|
+
import type { InputState, ReferenceInsert, SessionInput, TokenSpan } from '@deepseek-ai/dsh-client-ui-conversation/client';
|
|
3
|
+
export type ParentComposerOccurrence = InputState['occurrences'][number];
|
|
4
|
+
export type ParentComposerSpan = TokenSpan;
|
|
5
|
+
export type ParentComposerInputSnapshot = Pick<InputState, 'draft' | 'draftRev' | 'occurrences'> & Partial<Pick<InputState, 'phase'>>;
|
|
6
|
+
/** DSH 0.2's Lexical editor: published offsets are clipboard coordinates,
|
|
7
|
+
* mutation spans count each reference chip as one character. */
|
|
26
8
|
export interface ParentComposerInput {
|
|
27
9
|
readonly state: {
|
|
28
10
|
getSnapshot(): ParentComposerInputSnapshot;
|
|
29
|
-
subscribe
|
|
11
|
+
subscribe(listener: () => void): () => void;
|
|
30
12
|
};
|
|
31
|
-
/** DSH 0.1.2's Lexical editor projects each reference as one mutation coordinate. */
|
|
32
|
-
readonly referenceMode?: 'text' | 'lexical';
|
|
33
13
|
setDraft(text: string): void;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
readonly source: string;
|
|
38
|
-
readonly ref: string;
|
|
39
|
-
readonly label: string;
|
|
40
|
-
readonly appearance?: 'session' | 'file' | 'folder';
|
|
41
|
-
readonly clipboardText: string;
|
|
42
|
-
}, span: ParentComposerSpan): boolean;
|
|
14
|
+
replaceText(text: string, span: ParentComposerSpan): boolean;
|
|
15
|
+
insertReference(reference: ReferenceInsert, span: ParentComposerSpan): boolean;
|
|
16
|
+
notify?: SessionInput['notify'];
|
|
43
17
|
}
|
|
44
18
|
export interface ParentConversationService {
|
|
45
19
|
readonly input: {
|
|
46
|
-
for(scope: Context):
|
|
20
|
+
for(scope: Context): SessionInput;
|
|
47
21
|
};
|
|
48
22
|
}
|
|
49
23
|
export interface SelectionReferenceSource {
|
|
@@ -57,25 +31,13 @@ export interface SelectionReferenceSource {
|
|
|
57
31
|
serialize(ref: string, signal: AbortSignal): Promise<string>;
|
|
58
32
|
};
|
|
59
33
|
}
|
|
60
|
-
export
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
export declare function referenceDisplayText(label: string): string;
|
|
65
|
-
/**
|
|
66
|
-
* Resolve one occurrence's occupied draft range across both DSH reference
|
|
67
|
-
* representations: current full `@label` text with `length`, and the legacy
|
|
68
|
-
* one-code-unit U+FFFC placeholder without it.
|
|
69
|
-
*/
|
|
70
|
-
export declare function occurrenceRange(snapshot: ParentComposerInputSnapshot, occurrence: ParentComposerOccurrence, expectedLabel: string): OccurrenceRange | undefined;
|
|
34
|
+
export declare function occurrenceRange(snapshot: ParentComposerInputSnapshot, occurrence: ParentComposerOccurrence, expectedLabel: string): {
|
|
35
|
+
start: number;
|
|
36
|
+
end: number;
|
|
37
|
+
} | undefined;
|
|
71
38
|
export declare function occurrenceMatchesDraft(snapshot: ParentComposerInputSnapshot, occurrence: ParentComposerOccurrence, expectedLabel: string): boolean;
|
|
72
|
-
/** Convert
|
|
73
|
-
export declare function occurrenceEditSpan(
|
|
39
|
+
/** Convert a reference's clipboard range to the editor's atomic coordinates. */
|
|
40
|
+
export declare function occurrenceEditSpan(_input: ParentComposerInput, snapshot: ParentComposerInputSnapshot, occurrence: ParentComposerOccurrence, options?: {
|
|
74
41
|
readonly consumeFollowingSeparator?: boolean;
|
|
75
42
|
}): ParentComposerSpan | undefined;
|
|
76
|
-
/** Find the exact occurrence minted by one synchronous insertReference call. */
|
|
77
43
|
export declare function newlyInsertedOccurrence(before: ParentComposerInputSnapshot, after: ParentComposerInputSnapshot, source: string, ref: string): ParentComposerOccurrence | undefined;
|
|
78
|
-
/** Remove one occurrence's display range and, optionally, its separating ASCII gap. */
|
|
79
|
-
export declare function draftWithoutOccurrence(snapshot: ParentComposerInputSnapshot, occurrence: ParentComposerOccurrence, expectedLabel: string, options?: {
|
|
80
|
-
readonly consumeAdjacentSpace?: boolean;
|
|
81
|
-
}): string | undefined;
|
|
@@ -1,89 +1,37 @@
|
|
|
1
|
-
export const LEGACY_REFERENCE_PLACEHOLDER = '\uFFFC';
|
|
2
|
-
export function referenceDisplayText(label) {
|
|
3
|
-
return `@${label}`;
|
|
4
|
-
}
|
|
5
|
-
/**
|
|
6
|
-
* Resolve one occurrence's occupied draft range across both DSH reference
|
|
7
|
-
* representations: current full `@label` text with `length`, and the legacy
|
|
8
|
-
* one-code-unit U+FFFC placeholder without it.
|
|
9
|
-
*/
|
|
10
1
|
export function occurrenceRange(snapshot, occurrence, expectedLabel) {
|
|
11
|
-
const
|
|
12
|
-
if (!Number.isSafeInteger(
|
|
2
|
+
const { offset, length } = occurrence;
|
|
3
|
+
if (!Number.isSafeInteger(offset) || !Number.isSafeInteger(length)
|
|
4
|
+
|| offset < 0 || length < 0 || offset + length > snapshot.draft.length
|
|
5
|
+
|| occurrence.label !== expectedLabel
|
|
6
|
+
|| snapshot.draft.slice(offset, offset + length) !== occurrence.clipboardText)
|
|
13
7
|
return;
|
|
14
|
-
|
|
15
|
-
&& Number.isSafeInteger(occurrence.length)
|
|
16
|
-
&& occurrence.length > 0
|
|
17
|
-
&& start + occurrence.length <= snapshot.draft.length) {
|
|
18
|
-
return { start, end: start + occurrence.length };
|
|
19
|
-
}
|
|
20
|
-
if (snapshot.draft.startsWith(LEGACY_REFERENCE_PLACEHOLDER, start)) {
|
|
21
|
-
return { start, end: start + LEGACY_REFERENCE_PLACEHOLDER.length };
|
|
22
|
-
}
|
|
23
|
-
const display = referenceDisplayText(expectedLabel);
|
|
24
|
-
if (snapshot.draft.startsWith(display, start))
|
|
25
|
-
return { start, end: start + display.length };
|
|
26
|
-
return;
|
|
8
|
+
return { start: offset, end: offset + length };
|
|
27
9
|
}
|
|
28
10
|
export function occurrenceMatchesDraft(snapshot, occurrence, expectedLabel) {
|
|
29
|
-
|
|
30
|
-
if (range === undefined)
|
|
31
|
-
return false;
|
|
32
|
-
const text = snapshot.draft.slice(range.start, range.end);
|
|
33
|
-
if (text === LEGACY_REFERENCE_PLACEHOLDER || text === referenceDisplayText(expectedLabel))
|
|
34
|
-
return true;
|
|
35
|
-
return occurrence.label === expectedLabel
|
|
36
|
-
&& occurrence.clipboardText !== undefined
|
|
37
|
-
&& text === occurrence.clipboardText;
|
|
11
|
+
return occurrenceRange(snapshot, occurrence, expectedLabel) !== undefined;
|
|
38
12
|
}
|
|
39
|
-
/** Convert
|
|
40
|
-
export function occurrenceEditSpan(
|
|
41
|
-
const range = occurrenceRange(snapshot, occurrence, occurrence.label
|
|
13
|
+
/** Convert a reference's clipboard range to the editor's atomic coordinates. */
|
|
14
|
+
export function occurrenceEditSpan(_input, snapshot, occurrence, options = {}) {
|
|
15
|
+
const range = occurrenceRange(snapshot, occurrence, occurrence.label);
|
|
42
16
|
if (range === undefined)
|
|
43
17
|
return;
|
|
44
18
|
let start = range.start;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
19
|
+
for (const previous of snapshot.occurrences) {
|
|
20
|
+
if (previous.offset >= occurrence.offset)
|
|
21
|
+
continue;
|
|
22
|
+
if (occurrenceRange(snapshot, previous, previous.label) === undefined
|
|
23
|
+
|| previous.offset + previous.length > occurrence.offset)
|
|
48
24
|
return;
|
|
49
|
-
start
|
|
50
|
-
for (const candidate of [...snapshot.occurrences].sort((left, right) => left.offset - right.offset)) {
|
|
51
|
-
if (candidate.occurrenceId === occurrence.occurrenceId)
|
|
52
|
-
break;
|
|
53
|
-
if (candidate.offset > occurrence.offset || candidate.length === undefined)
|
|
54
|
-
return;
|
|
55
|
-
start -= candidate.length - 1;
|
|
56
|
-
}
|
|
57
|
-
end = start + 1;
|
|
58
|
-
}
|
|
59
|
-
if (options.consumeFollowingSeparator === true) {
|
|
60
|
-
const tail = snapshot.draft.slice(range.end);
|
|
61
|
-
if (tail.startsWith('\n\n'))
|
|
62
|
-
end += 2;
|
|
63
|
-
else if (tail.startsWith('\n') || tail.startsWith(' '))
|
|
64
|
-
end += 1;
|
|
25
|
+
start -= previous.length - 1;
|
|
65
26
|
}
|
|
27
|
+
let end = start + 1;
|
|
28
|
+
if (options.consumeFollowingSeparator && snapshot.draft[range.end] === ' ')
|
|
29
|
+
end += 1;
|
|
66
30
|
return { start, end, draftRev: snapshot.draftRev };
|
|
67
31
|
}
|
|
68
|
-
/** Find the exact occurrence minted by one synchronous insertReference call. */
|
|
69
32
|
export function newlyInsertedOccurrence(before, after, source, ref) {
|
|
70
33
|
const previousIds = new Set(before.occurrences.map(occurrence => occurrence.occurrenceId));
|
|
71
34
|
const inserted = after.occurrences.filter(occurrence => !previousIds.has(occurrence.occurrenceId)
|
|
72
|
-
&& occurrence.source === source
|
|
73
|
-
&& occurrence.ref === ref);
|
|
35
|
+
&& occurrence.source === source && occurrence.ref === ref);
|
|
74
36
|
return inserted.length === 1 ? inserted[0] : undefined;
|
|
75
37
|
}
|
|
76
|
-
/** Remove one occurrence's display range and, optionally, its separating ASCII gap. */
|
|
77
|
-
export function draftWithoutOccurrence(snapshot, occurrence, expectedLabel, options = {}) {
|
|
78
|
-
const range = occurrenceRange(snapshot, occurrence, expectedLabel);
|
|
79
|
-
if (range === undefined)
|
|
80
|
-
return;
|
|
81
|
-
let { start, end } = range;
|
|
82
|
-
if (options.consumeAdjacentSpace === true) {
|
|
83
|
-
if (snapshot.draft[end] === ' ')
|
|
84
|
-
end += 1;
|
|
85
|
-
else if (start > 0 && snapshot.draft[start - 1] === ' ')
|
|
86
|
-
start -= 1;
|
|
87
|
-
}
|
|
88
|
-
return snapshot.draft.slice(0, start) + snapshot.draft.slice(end);
|
|
89
|
-
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type { SessionId } from '../../shared/contracts.js';
|
|
1
|
+
import type { SideChatId, SideChatMessage } from '../../shared/contracts.js';
|
|
3
2
|
import type { ParentComposerInput, SelectionReferenceSource } from './composer-reference.js';
|
|
4
3
|
export declare const SIDE_CHAT_CONVERSATION_REFERENCE_SOURCE = "dsh-side-chat-conversation";
|
|
5
4
|
export interface ReferencedConversationMessage {
|
|
@@ -9,7 +8,7 @@ export interface ReferencedConversationMessage {
|
|
|
9
8
|
/** One immutable snapshot carried by the parent composer's conversation label. */
|
|
10
9
|
export interface ReferencedSideChatConversation {
|
|
11
10
|
readonly version: 1;
|
|
12
|
-
readonly conversationId:
|
|
11
|
+
readonly conversationId: string;
|
|
13
12
|
readonly title: string;
|
|
14
13
|
readonly conversation: readonly ReferencedConversationMessage[];
|
|
15
14
|
}
|
|
@@ -17,12 +16,11 @@ export interface ParsedReferencedConversationPrompt {
|
|
|
17
16
|
readonly reference: ReferencedSideChatConversation;
|
|
18
17
|
readonly message: string;
|
|
19
18
|
}
|
|
20
|
-
/**
|
|
19
|
+
/** Capture only the plugin transcript, not the parent context or model reasoning. */
|
|
21
20
|
export declare function referencedSideChatConversation(input: {
|
|
22
|
-
readonly conversationId:
|
|
21
|
+
readonly conversationId: SideChatId;
|
|
23
22
|
readonly title: string;
|
|
24
|
-
readonly
|
|
25
|
-
readonly inheritedThroughSeq: number;
|
|
23
|
+
readonly messages: readonly SideChatMessage[];
|
|
26
24
|
}): ReferencedSideChatConversation;
|
|
27
25
|
/** Durable model-facing form of the referenced conversation label. */
|
|
28
26
|
export declare function serializeReferencedConversation(reference: ReferencedSideChatConversation): string;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { newlyInsertedOccurrence, occurrenceEditSpan, } from './composer-reference.js';
|
|
2
2
|
export const SIDE_CHAT_CONVERSATION_REFERENCE_SOURCE = 'dsh-side-chat-conversation';
|
|
3
3
|
const OPEN_TAG = '<referenced_conversation>';
|
|
4
4
|
const CLOSE_TAG = '</referenced_conversation>';
|
|
@@ -6,48 +6,16 @@ function normalizedTitle(title) {
|
|
|
6
6
|
const normalized = title.trim().replace(/\s+/gu, ' ');
|
|
7
7
|
return normalized.length === 0 ? 'Side Chat' : normalized;
|
|
8
8
|
}
|
|
9
|
-
|
|
10
|
-
return content.map((block) => {
|
|
11
|
-
if (typeof block !== 'object' || block === null)
|
|
12
|
-
return '';
|
|
13
|
-
const value = block;
|
|
14
|
-
if (value.type === 'text' && typeof value.text === 'string')
|
|
15
|
-
return value.text;
|
|
16
|
-
if (value.type === 'image')
|
|
17
|
-
return '[Image attachment]';
|
|
18
|
-
return '';
|
|
19
|
-
}).filter(Boolean).join('\n').trim();
|
|
20
|
-
}
|
|
21
|
-
function assistantText(node) {
|
|
22
|
-
return node.blocks
|
|
23
|
-
.flatMap((block) => block.kind === 'text' ? [block.text] : [])
|
|
24
|
-
.join('\n')
|
|
25
|
-
.trim();
|
|
26
|
-
}
|
|
27
|
-
/** Project exactly the user/assistant history visible in the Side Chat modal. */
|
|
9
|
+
/** Capture only the plugin transcript, not the parent context or model reasoning. */
|
|
28
10
|
export function referencedSideChatConversation(input) {
|
|
29
|
-
const conversation =
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
continue;
|
|
38
|
-
}
|
|
39
|
-
if (node.kind === 'assistant') {
|
|
40
|
-
const content = assistantText(node);
|
|
41
|
-
if (content.length > 0)
|
|
42
|
-
conversation.push({ role: 'assistant', content });
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
return {
|
|
46
|
-
version: 1,
|
|
47
|
-
conversationId: input.conversationId,
|
|
48
|
-
title: normalizedTitle(input.title),
|
|
49
|
-
conversation,
|
|
50
|
-
};
|
|
11
|
+
const conversation = input.messages.flatMap((message) => {
|
|
12
|
+
if (message.status === 'streaming' || message.text.trim().length === 0)
|
|
13
|
+
return [];
|
|
14
|
+
const content = message.selectedText === undefined ? message.text
|
|
15
|
+
: `Selected passage:\n${message.selectedText}\n\nQuestion:\n${message.text}`;
|
|
16
|
+
return [{ role: message.role, content }];
|
|
17
|
+
});
|
|
18
|
+
return { version: 1, conversationId: input.conversationId, title: normalizedTitle(input.title), conversation };
|
|
51
19
|
}
|
|
52
20
|
function isReferencedConversationMessage(value) {
|
|
53
21
|
if (typeof value !== 'object' || value === null)
|
|
@@ -144,24 +112,9 @@ function removeOccurrence(input, occurrenceId) {
|
|
|
144
112
|
const occurrence = snapshot.occurrences.find(candidate => candidate.occurrenceId === occurrenceId);
|
|
145
113
|
if (occurrence === undefined)
|
|
146
114
|
return;
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
catch {
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
if (input.referenceMode === 'lexical' && input.replaceText !== undefined) {
|
|
155
|
-
const span = occurrenceEditSpan(input, snapshot, occurrence, {
|
|
156
|
-
consumeFollowingSeparator: true,
|
|
157
|
-
});
|
|
158
|
-
if (span !== undefined)
|
|
159
|
-
input.replaceText('', span);
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
const draft = draftWithoutOccurrence(snapshot, occurrence, title, { consumeAdjacentSpace: true });
|
|
163
|
-
if (draft !== undefined)
|
|
164
|
-
input.setDraft(draft);
|
|
115
|
+
const span = occurrenceEditSpan(input, snapshot, occurrence);
|
|
116
|
+
if (span !== undefined)
|
|
117
|
+
input.replaceText('', span);
|
|
165
118
|
}
|
|
166
119
|
/** Insert or refresh one Side Chat label without duplicating an older snapshot. */
|
|
167
120
|
export function addReferencedSideChatToConversation(input, reference) {
|
|
@@ -172,7 +125,7 @@ export function addReferencedSideChatToConversation(input, reference) {
|
|
|
172
125
|
const encoded = encodeReferencedConversation(reference);
|
|
173
126
|
if (existing.some(occurrence => occurrence.ref === encoded))
|
|
174
127
|
return true;
|
|
175
|
-
const target =
|
|
128
|
+
const target = existing[0];
|
|
176
129
|
const span = target === undefined
|
|
177
130
|
? { start: 0, end: 0, draftRev: before.draftRev }
|
|
178
131
|
: occurrenceEditSpan(input, before, target);
|
|
@@ -187,18 +140,13 @@ export function addReferencedSideChatToConversation(input, reference) {
|
|
|
187
140
|
}, span);
|
|
188
141
|
if (!inserted)
|
|
189
142
|
return false;
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
return matchingOccurrences(input.state.getSnapshot(), reference).length === 1;
|
|
198
|
-
}
|
|
199
|
-
for (const occurrence of existing)
|
|
200
|
-
removeOccurrence(input, occurrence.occurrenceId);
|
|
201
|
-
return true;
|
|
143
|
+
const after = input.state.getSnapshot();
|
|
144
|
+
const occurrence = newlyInsertedOccurrence(before, after, SIDE_CHAT_CONVERSATION_REFERENCE_SOURCE, encoded);
|
|
145
|
+
if (occurrence === undefined)
|
|
146
|
+
return false;
|
|
147
|
+
for (const duplicate of existing.slice(1))
|
|
148
|
+
removeOccurrence(input, duplicate.occurrenceId);
|
|
149
|
+
return matchingOccurrences(input.state.getSnapshot(), reference).length === 1;
|
|
202
150
|
}
|
|
203
151
|
/** Reference codec used by the native composer chip and submit pipeline. */
|
|
204
152
|
export const sideChatConversationReferenceSource = {
|