@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.
Files changed (79) hide show
  1. package/README.md +15 -12
  2. package/README.zh-CN.md +15 -12
  3. package/lib/client/apply.js +48 -32
  4. package/lib/client/contracts.d.ts +3 -58
  5. package/lib/client/dsh/ReadOnlyConversation.d.ts +10 -0
  6. package/lib/client/dsh/ReadOnlyConversation.js +91 -0
  7. package/lib/client/dsh/SideChatOverlay.d.ts +7 -0
  8. package/lib/client/{rc6/Rc6SideChatOverlay.js → dsh/SideChatOverlay.js} +26 -56
  9. package/lib/client/{rc6 → dsh}/context.d.ts +4 -6
  10. package/lib/client/dsh/conversation-store.d.ts +22 -0
  11. package/lib/client/dsh/conversation-store.js +19 -0
  12. package/lib/client/dsh/primitive-labels.d.ts +2 -0
  13. package/lib/client/dsh/primitive-labels.js +4 -0
  14. package/lib/client/{rc6 → dsh}/remote-adapter.d.ts +2 -2
  15. package/lib/client/dsh/remote-adapter.js +22 -0
  16. package/lib/client/dsh/sessions-adapter.d.ts +56 -0
  17. package/lib/client/dsh/sessions-adapter.js +185 -0
  18. package/lib/client/index.d.ts +2 -2
  19. package/lib/client/panel/SelectionQuote.js +8 -1
  20. package/lib/client/panel/SideChatErrorState.js +1 -1
  21. package/lib/client/panel/SideChatModelSelect.js +37 -18
  22. package/lib/client/panel/SideChatPanel.js +12 -5
  23. package/lib/client/panel/messages.d.ts +1 -1
  24. package/lib/client/panel/messages.js +8 -8
  25. package/lib/client/parent-composer/ParentConversationAnnotations.d.ts +3 -2
  26. package/lib/client/parent-composer/ParentConversationAnnotations.js +50 -30
  27. package/lib/client/parent-composer/add-to-conversation.d.ts +14 -28
  28. package/lib/client/parent-composer/add-to-conversation.js +131 -545
  29. package/lib/client/parent-composer/annotation-persistence.d.ts +8 -8
  30. package/lib/client/parent-composer/annotation-persistence.js +106 -102
  31. package/lib/client/parent-composer/composer-reference.d.ts +17 -55
  32. package/lib/client/parent-composer/composer-reference.js +20 -72
  33. package/lib/client/parent-composer/referenced-conversation.d.ts +5 -7
  34. package/lib/client/parent-composer/referenced-conversation.js +21 -73
  35. package/lib/client/selection/ConversationAnnotationMarkers.js +5 -1
  36. package/lib/client/selection/SelectionActions.js +2 -1
  37. package/lib/client/selection/conversation-dom.d.ts +5 -0
  38. package/lib/client/selection/conversation-dom.js +24 -0
  39. package/lib/client/selection/selection-controller.d.ts +1 -1
  40. package/lib/client/selection/selection-controller.js +7 -3
  41. package/lib/client/selection/selection-normalizer.js +1 -1
  42. package/lib/client/side-chat-controller.d.ts +28 -41
  43. package/lib/client/side-chat-controller.js +245 -337
  44. package/lib/client.js +81 -52
  45. package/lib/host/index.d.ts +1 -1
  46. package/lib/host/index.js +1 -1
  47. package/lib/host/read-only-chat-service.d.ts +30 -0
  48. package/lib/host/read-only-chat-service.js +314 -0
  49. package/lib/index.d.ts +16 -8
  50. package/lib/index.js +34 -13
  51. package/lib/remote.d.ts +23 -14
  52. package/lib/remote.js +2 -5
  53. package/lib/shared/constants.d.ts +0 -13
  54. package/lib/shared/constants.js +0 -13
  55. package/lib/shared/contracts.d.ts +52 -19
  56. package/lib/shared/contracts.js +2 -3
  57. package/lib/shared/error-codes.d.ts +1 -1
  58. package/lib/shared/error-codes.js +2 -2
  59. package/lib/shared/{archived-wire.d.ts → side-chat-wire.d.ts} +109 -20
  60. package/lib/shared/side-chat-wire.js +34 -0
  61. package/lib/typert.d.ts +3 -68
  62. package/lib/typert.js +60 -4
  63. package/package.json +76 -56
  64. package/lib/client/rc6/ArchivedConversation.d.ts +0 -14
  65. package/lib/client/rc6/ArchivedConversation.js +0 -231
  66. package/lib/client/rc6/Rc6SideChatOverlay.d.ts +0 -7
  67. package/lib/client/rc6/SideChatTool.d.ts +0 -22
  68. package/lib/client/rc6/SideChatTool.js +0 -404
  69. package/lib/client/rc6/primitive-labels.d.ts +0 -7
  70. package/lib/client/rc6/primitive-labels.js +0 -60
  71. package/lib/client/rc6/remote-adapter.js +0 -34
  72. package/lib/client/rc6/runtime-compat.d.ts +0 -120
  73. package/lib/client/rc6/runtime-compat.js +0 -187
  74. package/lib/client/rc6/sessions-adapter.d.ts +0 -64
  75. package/lib/client/rc6/sessions-adapter.js +0 -387
  76. package/lib/host/archived-fork-service.d.ts +0 -28
  77. package/lib/host/archived-fork-service.js +0 -309
  78. package/lib/shared/archived-wire.js +0 -47
  79. /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 './add-to-conversation.js';
3
- interface AnnotationStorage {
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
- /** Tab-scoped recovery for drafts persisted as a reference clipboard projection. */
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 observedSessions;
12
- constructor(storage?: AnnotationStorage | undefined);
12
+ private readonly observedInputs;
13
+ private reconciling;
14
+ constructor(storage?: ReferenceStorage | undefined);
13
15
  reconcile(sessionId: SessionId, input: ParentComposerInput): void;
14
- private read;
15
- private write;
16
- private remove;
16
+ private restore;
17
17
  }
18
18
  export {};
@@ -1,134 +1,138 @@
1
- import { conversationAnnotationRecoveryRecord, removeOrphanedConversationAnnotationPlaceholder, restoreConversationAnnotationReference, } from './add-to-conversation.js';
2
- const STORAGE_PREFIX = 'dsh-side-chat:composer-annotations:';
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 storageKey(sessionId) {
14
- return `${STORAGE_PREFIX}${encodeURIComponent(sessionId)}`;
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 displayDraft(record) {
21
- return record.version === 1 ? record.draft : record.displayDraft;
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
- 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. */
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
- observedSessions = new Set();
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
- const key = storageKey(sessionId);
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 stored = this.read(key);
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);
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 value = JSON.parse(raw);
85
- if (value.version === 1
86
- && typeof value.draft === 'string'
87
- && typeof value.ref === 'string'
88
- && (value.projection === undefined || typeof value.projection === 'string')) {
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
- 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,
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
- return;
84
+ // Storage denial or a stale codec must not alter or erase the user's draft.
112
85
  }
113
- }
114
- write(key, record) {
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
- remove(key) {
125
- if (this.storage === undefined)
126
- return;
90
+ restore(input, stored) {
91
+ let ownedSnapshot;
92
+ let complete = false;
127
93
  try {
128
- this.storage.removeItem(key);
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
- catch {
131
- // Storage denial must not block composer cleanup.
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
- export declare const LEGACY_REFERENCE_PLACEHOLDER = "\uFFFC";
3
- export interface ParentComposerOccurrence {
4
- readonly occurrenceId: number;
5
- readonly source: string;
6
- readonly ref: string;
7
- readonly offset: number;
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?(listener: () => void): () => void;
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
- /** Atomic plain-text replacement supplied by Side Chat's scoped input-event adapter. */
35
- replaceText?(text: string, span: ParentComposerSpan): boolean;
36
- insertReference(reference: {
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): ParentComposerInput;
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 interface OccurrenceRange {
61
- readonly start: number;
62
- readonly end: number;
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 one published occurrence range to the host input machine's mutation coordinates. */
73
- export declare function occurrenceEditSpan(input: ParentComposerInput, snapshot: ParentComposerInputSnapshot, occurrence: ParentComposerOccurrence, options?: {
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 start = occurrence.offset;
12
- if (!Number.isSafeInteger(start) || start < 0 || start > snapshot.draft.length)
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
- if (occurrence.length !== undefined
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
- const range = occurrenceRange(snapshot, occurrence, expectedLabel);
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 one published occurrence range to the host input machine's mutation coordinates. */
40
- export function occurrenceEditSpan(input, snapshot, occurrence, options = {}) {
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
- let end = range.end;
46
- if (input.referenceMode === 'lexical') {
47
- if (occurrence.length === undefined)
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 = occurrence.offset;
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 { ConversationNode } from '@deepseek-ai/dsh-client-ui-conversation/client';
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: SessionId;
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
- /** Project exactly the user/assistant history visible in the Side Chat modal. */
19
+ /** Capture only the plugin transcript, not the parent context or model reasoning. */
21
20
  export declare function referencedSideChatConversation(input: {
22
- readonly conversationId: SessionId;
21
+ readonly conversationId: SideChatId;
23
22
  readonly title: string;
24
- readonly nodes: readonly ConversationNode[];
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 { draftWithoutOccurrence, newlyInsertedOccurrence, occurrenceEditSpan, } from './composer-reference.js';
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
- function visibleContentText(content) {
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
- for (const node of input.nodes) {
31
- if (node.seq <= input.inheritedThroughSeq)
32
- continue;
33
- if (node.kind === 'user' || node.kind === 'steering') {
34
- const content = visibleContentText(node.content);
35
- if (content.length > 0)
36
- conversation.push({ role: 'user', content });
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
- let title;
148
- try {
149
- title = decodeReferencedConversation(occurrence.ref).title;
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 = input.referenceMode === 'lexical' ? existing[0] : undefined;
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
- if (input.referenceMode === 'lexical') {
191
- const after = input.state.getSnapshot();
192
- const occurrence = newlyInsertedOccurrence(before, after, SIDE_CHAT_CONVERSATION_REFERENCE_SOURCE, encoded);
193
- if (occurrence === undefined)
194
- return false;
195
- for (const duplicate of existing.slice(1))
196
- removeOccurrence(input, duplicate.occurrenceId);
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 = {