@ahggg/dsh-side-chat 0.7.3 → 1.0.1

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 (80) hide show
  1. package/README.md +52 -20
  2. package/README.zh-CN.md +52 -20
  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 +99 -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.d.ts +2 -1
  22. package/lib/client/panel/SideChatModelSelect.js +43 -24
  23. package/lib/client/panel/SideChatPanel.js +12 -5
  24. package/lib/client/panel/messages.d.ts +1 -1
  25. package/lib/client/panel/messages.js +8 -8
  26. package/lib/client/parent-composer/ParentConversationAnnotations.d.ts +3 -2
  27. package/lib/client/parent-composer/ParentConversationAnnotations.js +50 -30
  28. package/lib/client/parent-composer/add-to-conversation.d.ts +14 -28
  29. package/lib/client/parent-composer/add-to-conversation.js +131 -545
  30. package/lib/client/parent-composer/annotation-persistence.d.ts +8 -8
  31. package/lib/client/parent-composer/annotation-persistence.js +106 -102
  32. package/lib/client/parent-composer/composer-reference.d.ts +17 -55
  33. package/lib/client/parent-composer/composer-reference.js +20 -72
  34. package/lib/client/parent-composer/referenced-conversation.d.ts +5 -7
  35. package/lib/client/parent-composer/referenced-conversation.js +21 -73
  36. package/lib/client/selection/ConversationAnnotationMarkers.js +5 -1
  37. package/lib/client/selection/SelectionActions.js +2 -1
  38. package/lib/client/selection/conversation-dom.d.ts +5 -0
  39. package/lib/client/selection/conversation-dom.js +24 -0
  40. package/lib/client/selection/selection-controller.d.ts +1 -1
  41. package/lib/client/selection/selection-controller.js +7 -3
  42. package/lib/client/selection/selection-normalizer.js +1 -1
  43. package/lib/client/side-chat-controller.d.ts +28 -41
  44. package/lib/client/side-chat-controller.js +247 -337
  45. package/lib/client.js +84 -54
  46. package/lib/host/index.d.ts +1 -1
  47. package/lib/host/index.js +1 -1
  48. package/lib/host/read-only-chat-service.d.ts +30 -0
  49. package/lib/host/read-only-chat-service.js +315 -0
  50. package/lib/index.d.ts +16 -8
  51. package/lib/index.js +34 -13
  52. package/lib/remote.d.ts +23 -14
  53. package/lib/remote.js +2 -5
  54. package/lib/shared/constants.d.ts +0 -13
  55. package/lib/shared/constants.js +0 -13
  56. package/lib/shared/contracts.d.ts +52 -19
  57. package/lib/shared/contracts.js +2 -3
  58. package/lib/shared/error-codes.d.ts +1 -1
  59. package/lib/shared/error-codes.js +2 -2
  60. package/lib/shared/{archived-wire.d.ts → side-chat-wire.d.ts} +109 -20
  61. package/lib/shared/side-chat-wire.js +34 -0
  62. package/lib/typert.d.ts +3 -68
  63. package/lib/typert.js +60 -4
  64. package/package.json +76 -56
  65. package/lib/client/rc6/ArchivedConversation.d.ts +0 -14
  66. package/lib/client/rc6/ArchivedConversation.js +0 -231
  67. package/lib/client/rc6/Rc6SideChatOverlay.d.ts +0 -7
  68. package/lib/client/rc6/SideChatTool.d.ts +0 -22
  69. package/lib/client/rc6/SideChatTool.js +0 -404
  70. package/lib/client/rc6/primitive-labels.d.ts +0 -7
  71. package/lib/client/rc6/primitive-labels.js +0 -60
  72. package/lib/client/rc6/remote-adapter.js +0 -34
  73. package/lib/client/rc6/runtime-compat.d.ts +0 -120
  74. package/lib/client/rc6/runtime-compat.js +0 -187
  75. package/lib/client/rc6/sessions-adapter.d.ts +0 -64
  76. package/lib/client/rc6/sessions-adapter.js +0 -387
  77. package/lib/host/archived-fork-service.d.ts +0 -28
  78. package/lib/host/archived-fork-service.js +0 -309
  79. package/lib/shared/archived-wire.js +0 -47
  80. /package/lib/client/{rc6 → dsh}/context.js +0 -0
@@ -7,8 +7,9 @@ import { SideChatPanel } from '../panel/SideChatPanel.js';
7
7
  import { ConversationAnnotationMarkers } from '../selection/ConversationAnnotationMarkers.js';
8
8
  import { captureDomConversationSelection } from '../selection/selection-controller.js';
9
9
  import { SelectionActions } from '../selection/SelectionActions.js';
10
- import { ArchivedConversation } from './ArchivedConversation.js';
11
- import { Rc6SideChatSessions, selectionDescriptor } from './sessions-adapter.js';
10
+ import { conversationChatRoot, focusConversationComposer } from '../selection/conversation-dom.js';
11
+ import { ReadOnlyConversation } from './ReadOnlyConversation.js';
12
+ import { DshSideChatSessions, selectionDescriptor } from './sessions-adapter.js';
12
13
  const MORE_DETAILS_PROMPT = 'Please explain the selected passage in more detail.';
13
14
  const TOUCH_SELECTION_SETTLE_MS = 300;
14
15
  const TOUCH_ACTIVATION_SUPPRESS_MS = 750;
@@ -24,28 +25,8 @@ function captureEvent(event) {
24
25
  '.dsh-side-chat-annotation-marker',
25
26
  ].join(', ')) !== null);
26
27
  }
27
- function focusParentComposer() {
28
- const input = document.querySelector([
29
- '[data-composer-seat] textarea',
30
- '[data-composer-seat] [role="textbox"]',
31
- '[data-composer-seat] [contenteditable="true"]',
32
- ].join(', '));
33
- if (input === null)
34
- return;
35
- input.focus();
36
- if (input instanceof HTMLTextAreaElement) {
37
- input.setSelectionRange(input.value.length, input.value.length);
38
- return;
39
- }
40
- const range = document.createRange();
41
- range.selectNodeContents(input);
42
- range.collapse(false);
43
- const selection = window.getSelection();
44
- selection?.removeAllRanges();
45
- selection?.addRange(range);
46
- }
47
- /** rc.6 compatibility surface: selection toolbar plus a non-current child conversation panel. */
48
- export function Rc6SideChatOverlay({ controller, sessions, }) {
28
+ /** Selection toolbar and plugin-owned read-only discussion panel. */
29
+ export function SideChatOverlay({ controller, sessions, }) {
49
30
  const state = useSyncExternalStore(controller.subscribe, controller.getSnapshot, controller.getSnapshot);
50
31
  const currentSessionId = useSyncExternalStore(sessions.subscribeList, () => sessions.currentSessionId(), () => sessions.currentSessionId());
51
32
  const composerInput = useSyncExternalStore(sessions.subscribeConversationInput, sessions.currentConversationInputSnapshot, sessions.currentConversationInputSnapshot);
@@ -62,11 +43,11 @@ export function Rc6SideChatOverlay({ controller, sessions, }) {
62
43
  const generation = ++captureGeneration.current;
63
44
  const parentSessionId = sessions.currentSessionId();
64
45
  const face = parentSessionId === undefined ? undefined : sessions.face(parentSessionId);
65
- const conversationRoot = document.querySelector('[data-chat-flow]');
46
+ const conversationRoot = parentSessionId === undefined ? undefined : conversationChatRoot(parentSessionId);
66
47
  const browserSelection = window.getSelection();
67
48
  if (parentSessionId === undefined
68
49
  || face === undefined
69
- || conversationRoot === null
50
+ || conversationRoot === undefined
70
51
  || browserSelection === null
71
52
  || browserSelection.isCollapsed) {
72
53
  if (generation === captureGeneration.current)
@@ -86,7 +67,7 @@ export function Rc6SideChatOverlay({ controller, sessions, }) {
86
67
  },
87
68
  },
88
69
  });
89
- if (generation === captureGeneration.current) {
70
+ if (generation === captureGeneration.current && sessions.currentSessionId() === parentSessionId) {
90
71
  setActiveSelection({ value: captured, touch });
91
72
  }
92
73
  }
@@ -193,11 +174,13 @@ export function Rc6SideChatOverlay({ controller, sessions, }) {
193
174
  touchInteraction.current = false;
194
175
  cancelScheduledCapture();
195
176
  if (event.key === 'Escape') {
177
+ // Escape in a menu, annotation editor, or main composer belongs to
178
+ // that control. Closing discards the temporary discussion, so it must
179
+ // not fire from every keyup in the document.
196
180
  ++captureGeneration.current;
197
181
  annotationEditing.current = false;
198
182
  setActiveSelection(null);
199
183
  setEditingAnnotation(null);
200
- void controller.close();
201
184
  return;
202
185
  }
203
186
  if (captureEvent(event))
@@ -253,34 +236,25 @@ export function Rc6SideChatOverlay({ controller, sessions, }) {
253
236
  const askDisabledReason = state.phase === 'closed'
254
237
  ? undefined
255
238
  : 'Close the current Side Chat before starting another one.';
256
- const childFace = state.childSessionId === undefined ? undefined : sessions.face(state.childSessionId);
257
- const childCwd = state.childSessionId === undefined ? undefined : sessions.cwd(state.childSessionId);
258
- const inheritedThroughSeq = state.inheritedThroughSeq;
259
239
  const locale = navigator.language.toLowerCase().startsWith('zh') ? 'zh-CN' : 'en';
260
240
  const modelDirectory = state.parentSessionId === undefined
261
241
  ? undefined
262
242
  : sessions.modelDirectory?.(state.parentSessionId);
263
- const addToConversationDisabled = childFace === undefined
264
- || inheritedThroughSeq === undefined
265
- || state.parentSessionId === undefined
266
- || state.childSessionId === undefined
267
- || state.phase !== 'ready';
243
+ const addToConversationDisabled = state.parentSessionId === undefined
244
+ || state.chatId === undefined || state.phase !== 'ready'
245
+ || !state.messages.some(message => message.role === 'assistant' && message.text.trim().length > 0);
268
246
  const addToConversation = () => {
269
247
  const parentSessionId = state.parentSessionId;
270
- const childSessionId = state.childSessionId;
271
- const face = childSessionId === undefined ? undefined : sessions.face(childSessionId);
272
- if (parentSessionId === undefined
273
- || childSessionId === undefined
274
- || inheritedThroughSeq === undefined
275
- || face === undefined) {
248
+ const chatId = state.chatId;
249
+ if (parentSessionId === undefined || chatId === undefined || state.phase !== 'ready') {
276
250
  sessions.notify({ kind: 'warning', text: 'The Side Chat conversation is not ready to add yet.' });
277
251
  return;
278
252
  }
253
+ const parentTitle = sessions.title(parentSessionId);
279
254
  const reference = referencedSideChatConversation({
280
- conversationId: childSessionId,
281
- title: sessions.title(childSessionId) ?? (locale === 'zh-CN' ? '侧边对话' : 'Side Chat'),
282
- nodes: face.getSnapshot().nodes,
283
- inheritedThroughSeq,
255
+ conversationId: chatId,
256
+ title: [locale === 'zh-CN' ? '侧边对话' : 'Side Chat', parentTitle].filter(Boolean).join(' · '),
257
+ messages: state.messages,
284
258
  });
285
259
  if (reference.conversation.length === 0) {
286
260
  sessions.notify({ kind: 'warning', text: 'The Side Chat does not have any conversation history to add yet.' });
@@ -291,7 +265,7 @@ export function Rc6SideChatOverlay({ controller, sessions, }) {
291
265
  sessions.notify({ kind: 'warning', text: 'Could not add the Side Chat to the main conversation.' });
292
266
  return;
293
267
  }
294
- void sessions.openSession(parentSessionId).then(focusParentComposer, () => {
268
+ void sessions.openSession(parentSessionId).then(() => focusConversationComposer(parentSessionId), () => {
295
269
  sessions.notify({ kind: 'warning', text: 'The Side Chat was added, but its parent conversation could not be opened.' });
296
270
  });
297
271
  }
@@ -301,12 +275,12 @@ export function Rc6SideChatOverlay({ controller, sessions, }) {
301
275
  };
302
276
  const modelControl = modelDirectory === undefined
303
277
  ? undefined
304
- : (_jsx(SideChatModelSelect, { directory: modelDirectory, selection: state.modelSelection, locked: ['creating', 'opening', 'closing'].includes(state.phase) || state.error?.operation === 'close', validateInitialSelection: state.childSessionId === undefined, locale: locale, onInitialize: (selection, options) => {
278
+ : (_jsx(SideChatModelSelect, { directory: modelDirectory, selection: state.modelSelection, locked: ['creating', 'closing'].includes(state.phase) || state.error?.operation === 'close', running: state.phase === 'running', validateInitialSelection: state.chatId === undefined, locale: locale, onInitialize: (selection, options) => {
305
279
  if (options.remember)
306
280
  void controller.selectModel(selection);
307
281
  else
308
282
  controller.initializeModel(selection);
309
- }, onSelect: (selection) => controller.selectModel(selection) }, `${state.parentSessionId}:${state.childSessionId ?? 'draft'}`));
283
+ }, onSelect: (selection) => controller.selectModel(selection) }, `${state.parentSessionId}:${state.chatId ?? 'draft'}`));
310
284
  return (_jsxs("div", { className: "dsh-side-chat-overlay", children: [_jsx(ConversationAnnotationMarkers, { annotations: annotations, ...editingAnnotation === null
311
285
  ? {}
312
286
  : { activeAnnotationIndex: editingAnnotation.annotationIndex }, onEdit: (annotation, restoredSelection) => {
@@ -318,7 +292,7 @@ export function Rc6SideChatOverlay({ controller, sessions, }) {
318
292
  } }), activeSelection !== null && (_jsx(SelectionActions, { selection: activeSelection.value, touchInteraction: activeSelection.touch, annotationNumber: sessions.nextConversationAnnotationNumber(), ...askDisabledReason === undefined ? {} : { askDisabledReason }, onAddToChat: (captured, comment) => {
319
293
  try {
320
294
  if (sessions.addSelectionToConversation(captured, comment))
321
- focusParentComposer();
295
+ focusConversationComposer(captured.parentSessionId);
322
296
  else
323
297
  sessions.notify({ kind: 'warning', text: 'Could not add the selection to the current chat.' });
324
298
  }
@@ -373,12 +347,8 @@ export function Rc6SideChatOverlay({ controller, sessions, }) {
373
347
  }, onMoreDetails: () => { }, onAskInSideChat: () => { }, onDismiss: () => {
374
348
  annotationEditing.current = false;
375
349
  setEditingAnnotation(null);
376
- } }, `annotation:${String(editingAnnotation.annotationIndex)}`)), state.phase !== 'closed' && (_jsx(SideChatPanel, { state: state, locale: locale, ...childFace === undefined || inheritedThroughSeq === undefined
377
- ? {}
378
- : {
379
- embeddedConversation: (_jsx(ArchivedConversation, { face: childFace, inheritedThroughSeq: inheritedThroughSeq, controller: controller, cwd: childCwd, ...state.selection === undefined ? {} : { selection: state.selection }, locale: locale, modelControl: modelControl })),
380
- }, modelControl: modelControl, onDraftChange: (draft) => { controller.setDraft(draft); }, onFirstSend: (question) => controller.sendFirst(question), onClose: () => controller.close(), onRetry: () => controller.retry(), onFocusParent: () => {
350
+ } }, `annotation:${String(editingAnnotation.annotationIndex)}`)), state.phase !== 'closed' && (_jsx(SideChatPanel, { state: state, locale: locale, embeddedConversation: _jsx(ReadOnlyConversation, { state: state, controller: controller, locale: locale, modelControl: modelControl }, state.chatId), modelControl: modelControl, onDraftChange: (draft) => { controller.setDraft(draft); }, onFirstSend: (question) => controller.sendFirst(question), onClose: () => controller.close(), onRetry: () => controller.retry(), onFocusParent: () => {
381
351
  if (state.parentSessionId !== undefined)
382
352
  void sessions.openSession(state.parentSessionId);
383
- }, onAddToConversation: addToConversation, addToConversationDisabled: addToConversationDisabled, onRemoveSelection: () => { controller.clearSelection(); } }))] }));
353
+ }, onAddToConversation: addToConversation, addToConversationDisabled: addToConversationDisabled, ...state.chatId === undefined ? { onRemoveSelection: () => { controller.clearSelection(); } } : {} }))] }));
384
354
  }
@@ -4,10 +4,10 @@ import type { ISessions } from '@deepseek-ai/dsh-api-session-controller/client';
4
4
  import type { UiConversation } from '@deepseek-ai/dsh-client-ui-conversation/client';
5
5
  import type { ModelDirectoryResolver } from '@deepseek-ai/dsh-client-ui-model-selection/client';
6
6
  import type { SlotRegistry } from '@deepseek-ai/dsh-client-ui-renderer/client';
7
- import type { UiSession } from '@deepseek-ai/dsh-client-ui-session/client';
7
+ import type { UiWorkspace } from '@deepseek-ai/dsh-client-ui-workspace/client';
8
8
  import type { ParentConversationService, SelectionReferenceSource } from '../parent-composer/add-to-conversation.js';
9
9
  /** Browser-only Cordis view for a package that also compiles the Host half. */
10
- export type Rc6ClientContext = Omit<Context, 'conversation' | 'inputTriggers' | 'modelDirectories' | 'remote' | 'sessions' | 'slots' | 'uiConversation' | 'uiSession'> & {
10
+ export type DshClientContext = Omit<Context, 'conversation' | 'inputTriggers' | 'modelDirectories' | 'remote' | 'sessions' | 'slots' | 'uiConversation' | 'uiWorkspace'> & {
11
11
  readonly conversation: ParentConversationService;
12
12
  readonly inputTriggers: {
13
13
  registerSource(source: SelectionReferenceSource): () => void;
@@ -16,8 +16,6 @@ export type Rc6ClientContext = Omit<Context, 'conversation' | 'inputTriggers' |
16
16
  readonly remote: ClientRemote;
17
17
  readonly sessions: ISessions;
18
18
  readonly slots: SlotRegistry;
19
- /** Added by DSH 0.1.2; absent on the monolithic 0.1.0/0.1.1 runtime. */
20
- readonly uiConversation?: Pick<UiConversation, 'binding'>;
21
- /** Added by DSH 0.1.2; absent on the monolithic 0.1.0/0.1.1 runtime. */
22
- readonly uiSession?: Pick<UiSession, 'pendingInteractions'>;
19
+ readonly uiWorkspace: UiWorkspace;
20
+ readonly uiConversation: Pick<UiConversation, 'binding'>;
23
21
  };
@@ -0,0 +1,22 @@
1
+ import type { ChatSnapshot } from '@deepseek-ai/dsh-client-ui-chat/client';
2
+ export type SideChatNodeStore = ChatSnapshot['nodes'];
3
+ export interface ParentConversationSnapshot {
4
+ readonly turnEnds: ReadonlyMap<number, number>;
5
+ readonly chatNodes: Pick<SideChatNodeStore, 'get'>;
6
+ }
7
+ export interface ParentConversationFace {
8
+ getSnapshot(): ParentConversationSnapshot;
9
+ subscribe(listener: () => void): () => void;
10
+ }
11
+ /** Read-only parent Chat projection, used only for selection anchors and turn boundaries. */
12
+ export declare class DshParentConversation implements ParentConversationFace {
13
+ private readonly chat;
14
+ private source;
15
+ private snapshot;
16
+ constructor(chat: {
17
+ getSnapshot(): ChatSnapshot | undefined;
18
+ subscribe(listener: () => void): () => void;
19
+ });
20
+ subscribe: (listener: () => void) => (() => void);
21
+ getSnapshot: () => ParentConversationSnapshot;
22
+ }
@@ -0,0 +1,19 @@
1
+ const EMPTY = { turnEnds: new Map(), chatNodes: { get: () => undefined } };
2
+ /** Read-only parent Chat projection, used only for selection anchors and turn boundaries. */
3
+ export class DshParentConversation {
4
+ chat;
5
+ source;
6
+ snapshot = EMPTY;
7
+ constructor(chat) {
8
+ this.chat = chat;
9
+ }
10
+ subscribe = (listener) => this.chat.subscribe(listener);
11
+ getSnapshot = () => {
12
+ const source = this.chat.getSnapshot();
13
+ if (source !== this.source) {
14
+ this.source = source;
15
+ this.snapshot = source === undefined ? EMPTY : { turnEnds: source.legacy.turnEnds, chatNodes: source.nodes };
16
+ }
17
+ return this.snapshot;
18
+ };
19
+ }
@@ -0,0 +1,2 @@
1
+ import type { MarkdownLabels } from '@deepseek-ai/dsh-client-ui-primitives';
2
+ export declare const MARKDOWN_LABELS: MarkdownLabels;
@@ -0,0 +1,4 @@
1
+ export const MARKDOWN_LABELS = {
2
+ code: { copyLabel: 'Copy', copiedLabel: 'Copied' },
3
+ footnotes: 'Footnotes',
4
+ };
@@ -1,6 +1,6 @@
1
1
  import type { SideChatRemote } from '../../shared/contracts.js';
2
- import type { Rc6ClientContext } from './context.js';
3
- export declare function mountArchivedRemote(ctx: Rc6ClientContext): Promise<{
2
+ import type { DshClientContext } from './context.js';
3
+ export declare function mountSideChatRemote(ctx: DshClientContext): Promise<{
4
4
  readonly remote: SideChatRemote;
5
5
  readonly dispose: () => Promise<void>;
6
6
  }>;
@@ -0,0 +1,22 @@
1
+ import TYPERT_REMOTE from '../../remote.js';
2
+ function remoteFailure(error) {
3
+ const code = error.code;
4
+ const badRequest = code === 'bad-request' || code === 'gateway/bad-request';
5
+ return { code: badRequest ? 'invalid_request' : 'transport_error',
6
+ message: error.message || 'The Side Chat RPC failed.', recoverable: !badRequest };
7
+ }
8
+ async function unwrap(result) {
9
+ const value = await result;
10
+ return value.ok ? value.value : { ok: false, error: remoteFailure(value.error) };
11
+ }
12
+ export async function mountSideChatRemote(ctx) {
13
+ const dispose = await ctx.remote.$mount(TYPERT_REMOTE);
14
+ const rpc = ctx.get('remote.sideChat');
15
+ return { remote: {
16
+ create: request => unwrap(rpc.create(request)),
17
+ selectModel: request => unwrap(rpc.selectModel(request)),
18
+ stream: request => rpc.stream(request),
19
+ cancel: request => unwrap(rpc.cancel(request)),
20
+ close: request => unwrap(rpc.close(request)),
21
+ }, dispose };
22
+ }
@@ -0,0 +1,56 @@
1
+ import type { ModelDirectory } from '@deepseek-ai/dsh-client-ui-model-selection/client';
2
+ import type { SideChatClientSessions } from '../contracts.js';
3
+ import { SessionId, type ConversationSelection, type SideChatModelSelection } from '../../shared/contracts.js';
4
+ import { type ParentComposerInputSnapshot } from '../parent-composer/add-to-conversation.js';
5
+ import { type ReferencedSideChatConversation } from '../parent-composer/referenced-conversation.js';
6
+ import type { DshClientContext } from './context.js';
7
+ import { type ParentConversationFace, type ParentConversationSnapshot } from './conversation-store.js';
8
+ declare module '@deepseek-ai/dsh-api-session-controller/client' {
9
+ interface SessionReferenceSourceMap {
10
+ sideChat: unknown;
11
+ }
12
+ }
13
+ /** The only Session touched by the plugin is the parent: read its Chat, retain its editor, and insert explicit references. */
14
+ export declare class DshSideChatSessions implements SideChatClientSessions {
15
+ private readonly ctx;
16
+ private readonly faces;
17
+ private readonly parentInputs;
18
+ private readonly annotationPersistence;
19
+ private readonly modelPreferences;
20
+ constructor(ctx: DshClientContext);
21
+ readonly subscribeList: (listener: () => void) => (() => void);
22
+ readonly subscribeConversationInput: (listener: () => void) => (() => void);
23
+ readonly currentConversationInputSnapshot: () => ParentComposerInputSnapshot | undefined;
24
+ currentSessionId(): SessionId | undefined;
25
+ lastCompletedSeq(parentSessionId: SessionId): number | undefined;
26
+ selectionIsCurrent(selection: ConversationSelection): boolean;
27
+ addSelectionToConversation(selection: ConversationSelection, comment?: string): boolean;
28
+ addSideChatToConversation(parentSessionId: SessionId, reference: ReferencedSideChatConversation): boolean;
29
+ removeConversationAnnotation(index: number): boolean;
30
+ updateConversationAnnotation(index: number, comment?: string): boolean;
31
+ reconcileConversationAnnotationPersistence(): void;
32
+ nextConversationAnnotationNumber(): number;
33
+ removeConversationAnnotations(id?: SessionId | undefined): boolean;
34
+ retainParent(id: SessionId): () => void;
35
+ openSession(id: SessionId): Promise<void>;
36
+ notify(message: {
37
+ readonly kind: 'status' | 'warning';
38
+ readonly text: string;
39
+ }): void;
40
+ face(id: SessionId): ParentConversationFace | undefined;
41
+ title(id: SessionId): string | undefined;
42
+ modelDirectory(id: SessionId): ModelDirectory | undefined;
43
+ sideChatModelPreference(): SideChatModelSelection | undefined;
44
+ rememberSideChatModelPreference(model: SideChatModelSelection): void;
45
+ private currentParentInput;
46
+ private parentInput;
47
+ }
48
+ export declare function selectionDescriptor(snapshot: ParentConversationSnapshot, anchorKey: string): {
49
+ readonly nodeKey: string;
50
+ readonly nodeKind: string;
51
+ readonly turnKey: string;
52
+ readonly seq: number;
53
+ readonly source: 'user' | 'assistant' | 'context' | 'code';
54
+ readonly modelVisible: boolean;
55
+ readonly settled: boolean;
56
+ } | undefined;
@@ -0,0 +1,185 @@
1
+ import { SessionId } from '../../shared/contracts.js';
2
+ import { addSelectionToConversation as addSelection, conversationAnnotations, removeConversationAnnotation as removeAnnotation, removeConversationAnnotations as removeAnnotations, updateConversationAnnotation as updateAnnotation, } from '../parent-composer/add-to-conversation.js';
3
+ import { ConversationAnnotationPersistence } from '../parent-composer/annotation-persistence.js';
4
+ import { addReferencedSideChatToConversation } from '../parent-composer/referenced-conversation.js';
5
+ import { SideChatModelPreferences } from '../model-preference.js';
6
+ import { DshParentConversation } from './conversation-store.js';
7
+ function dshId(id) { return id; }
8
+ function locationSettled(location) {
9
+ if (location.kind === 'turn')
10
+ return location.turn.status === 'closed';
11
+ if (location.kind === 'step')
12
+ return location.turn.status === 'closed' && location.step.status === 'closed';
13
+ return false;
14
+ }
15
+ /** The only Session touched by the plugin is the parent: read its Chat, retain its editor, and insert explicit references. */
16
+ export class DshSideChatSessions {
17
+ ctx;
18
+ faces = new WeakMap();
19
+ parentInputs = new WeakMap();
20
+ annotationPersistence = new ConversationAnnotationPersistence();
21
+ modelPreferences = new SideChatModelPreferences();
22
+ constructor(ctx) {
23
+ this.ctx = ctx;
24
+ }
25
+ subscribeList = (listener) => this.ctx.sessions.list.subscribe(listener);
26
+ subscribeConversationInput = (listener) => {
27
+ let input;
28
+ let removeInput = () => { };
29
+ const bind = () => {
30
+ const next = this.currentParentInput();
31
+ if (next === input)
32
+ return;
33
+ removeInput();
34
+ input = next;
35
+ removeInput = input?.state.subscribe(listener) ?? (() => { });
36
+ };
37
+ bind();
38
+ const removeList = this.ctx.sessions.list.subscribe(() => { bind(); listener(); });
39
+ return () => { removeList(); removeInput(); };
40
+ };
41
+ currentConversationInputSnapshot = () => this.currentParentInput()?.state.getSnapshot();
42
+ currentSessionId() {
43
+ const current = Object.entries(this.ctx.sessions.list.getSnapshot().byId)
44
+ .find(([, summary]) => (summary.retainedBy.mainView ?? 0) > 0)?.[0];
45
+ return current === undefined ? undefined : SessionId(current);
46
+ }
47
+ lastCompletedSeq(parentSessionId) {
48
+ const snapshot = this.face(parentSessionId)?.getSnapshot();
49
+ let latest;
50
+ for (const seq of snapshot?.turnEnds.values() ?? [])
51
+ if (latest === undefined || seq > latest)
52
+ latest = seq;
53
+ return latest;
54
+ }
55
+ selectionIsCurrent(selection) {
56
+ const snapshot = this.face(selection.parentSessionId)?.getSnapshot();
57
+ if (snapshot === undefined)
58
+ return false;
59
+ return selection.fragments.every(fragment => {
60
+ const node = snapshot.chatNodes.get(fragment.nodeKey);
61
+ return node !== undefined && node.visibility === 'visible' && node.kind === fragment.nodeKind
62
+ && node.anchorSeq === fragment.seq && locationSettled(node.location);
63
+ });
64
+ }
65
+ addSelectionToConversation(selection, comment) {
66
+ if (this.currentSessionId() !== selection.parentSessionId)
67
+ return false;
68
+ const input = this.currentParentInput();
69
+ if (input === undefined || !addSelection(input, selection, comment))
70
+ return false;
71
+ this.annotationPersistence.reconcile(selection.parentSessionId, input);
72
+ return true;
73
+ }
74
+ addSideChatToConversation(parentSessionId, reference) {
75
+ const scope = this.ctx.sessions.scope(dshId(parentSessionId));
76
+ const input = scope === undefined ? undefined : this.parentInput(scope);
77
+ if (input === undefined || !addReferencedSideChatToConversation(input, reference))
78
+ return false;
79
+ this.annotationPersistence.reconcile(parentSessionId, input);
80
+ return true;
81
+ }
82
+ removeConversationAnnotation(index) {
83
+ const id = this.currentSessionId();
84
+ const input = this.currentParentInput();
85
+ if (id === undefined || input === undefined || !removeAnnotation(input, index))
86
+ return false;
87
+ this.annotationPersistence.reconcile(id, input);
88
+ return true;
89
+ }
90
+ updateConversationAnnotation(index, comment) {
91
+ const id = this.currentSessionId();
92
+ const input = this.currentParentInput();
93
+ if (id === undefined || input === undefined || !updateAnnotation(input, index, comment))
94
+ return false;
95
+ this.annotationPersistence.reconcile(id, input);
96
+ return true;
97
+ }
98
+ reconcileConversationAnnotationPersistence() {
99
+ const id = this.currentSessionId();
100
+ const input = this.currentParentInput();
101
+ if (id !== undefined && input !== undefined)
102
+ this.annotationPersistence.reconcile(id, input);
103
+ }
104
+ nextConversationAnnotationNumber() {
105
+ const snapshot = this.currentConversationInputSnapshot();
106
+ return snapshot === undefined ? 1 : conversationAnnotations(snapshot).length + 1;
107
+ }
108
+ removeConversationAnnotations(id = this.currentSessionId()) {
109
+ const scope = id === undefined ? undefined : this.ctx.sessions.scope(dshId(id));
110
+ const input = scope === undefined ? undefined : this.parentInput(scope);
111
+ if (id === undefined || input === undefined || !removeAnnotations(input))
112
+ return false;
113
+ this.annotationPersistence.reconcile(id, input);
114
+ return true;
115
+ }
116
+ retainParent(id) {
117
+ const reference = this.ctx.sessions.retain(dshId(id), { source: 'sideChat' });
118
+ let active = true;
119
+ void reference.ready.catch(() => { if (active)
120
+ this.notify({ kind: 'warning', text: 'The parent conversation could not be loaded.' }); });
121
+ return () => { active = false; reference.release(); };
122
+ }
123
+ async openSession(id) { this.ctx.uiWorkspace.openSession(dshId(id)); }
124
+ notify(message) {
125
+ console[message.kind === 'warning' ? 'warn' : 'info'](`[dsh-side-chat] ${message.text}`);
126
+ this.currentParentInput()?.notify?.(message.kind === 'warning' ? 'error' : 'info', message.text);
127
+ }
128
+ face(id) {
129
+ const source = this.ctx.sessions.binding(dshId(id))?.session;
130
+ if (source === undefined)
131
+ return;
132
+ const existing = this.faces.get(source);
133
+ if (existing !== undefined)
134
+ return existing;
135
+ const conversation = this.ctx.uiConversation.binding(source.sessionId);
136
+ conversation.activate('chat');
137
+ const face = new DshParentConversation(conversation.target('chat'));
138
+ this.faces.set(source, face);
139
+ return face;
140
+ }
141
+ title(id) { return this.ctx.sessions.list.getSnapshot().byId[dshId(id)]?.displayTitle; }
142
+ modelDirectory(id) {
143
+ try {
144
+ return this.ctx.modelDirectories.directoryFor(dshId(id));
145
+ }
146
+ catch {
147
+ return undefined;
148
+ }
149
+ }
150
+ sideChatModelPreference() { return this.modelPreferences.get(); }
151
+ rememberSideChatModelPreference(model) { this.modelPreferences.set(model); }
152
+ currentParentInput() {
153
+ const id = this.currentSessionId();
154
+ const scope = id === undefined ? undefined : this.ctx.sessions.scope(dshId(id));
155
+ return scope === undefined ? undefined : this.parentInput(scope);
156
+ }
157
+ parentInput(scope) {
158
+ const source = this.ctx.conversation.input.for(scope);
159
+ if (source === undefined)
160
+ return;
161
+ const existing = this.parentInputs.get(source);
162
+ if (existing !== undefined)
163
+ return existing;
164
+ const input = {
165
+ state: source.state, setDraft: text => { source.setDraft(text); },
166
+ notify: (level, text) => source.notify?.(level, text),
167
+ insertReference: (reference, span) => source.insertReference(reference, span),
168
+ replaceText: (text, span) => scope.bail(scope, 'slash/input-insert-text', { text, span }) === true,
169
+ };
170
+ this.parentInputs.set(source, input);
171
+ return input;
172
+ }
173
+ }
174
+ export function selectionDescriptor(snapshot, anchorKey) {
175
+ const node = snapshot.chatNodes.get(anchorKey);
176
+ if (node === undefined || node.visibility !== 'visible')
177
+ return;
178
+ const source = node.kind === 'user' || node.kind === 'steering' ? 'user'
179
+ : node.kind === 'assistant-step' ? 'assistant' : node.kind === 'context' ? 'context' : undefined;
180
+ const turn = node.location.kind === 'turn' || node.location.kind === 'step' ? node.location.turn.turn : undefined;
181
+ if (source === undefined || turn === undefined)
182
+ return;
183
+ return { nodeKey: node.key, nodeKind: node.kind, turnKey: `turn:${String(turn)}`, seq: node.anchorSeq,
184
+ source, modelVisible: true, settled: locationSettled(node.location) };
185
+ }
@@ -1,5 +1,5 @@
1
1
  export { apply, inject, name } from './apply.js';
2
- export type { SideChatActionResult, SideChatClientSessions, SideChatSessionBinding, SideChatSessionLease, SideChatSessionSnapshot, } from './contracts.js';
2
+ export type { SideChatActionResult, SideChatClientSessions, } from './contracts.js';
3
3
  export { SideChatController } from './side-chat-controller.js';
4
4
  export { buildSideChatPrompt } from './parent-composer/add-to-conversation.js';
5
5
  export { SideChatPanel, type SideChatPanelProps } from './panel/SideChatPanel.js';
@@ -7,4 +7,4 @@ export { SelectionActions, type SelectionActionsProps } from './selection/Select
7
7
  export { captureDomConversationSelection, restoreDomConversationSelection, type RestoredConversationSelection, type SelectionAnchorResolver, type SelectionNodeDescriptor, } from './selection/selection-controller.js';
8
8
  export { selectionFitsLimit, summarizeSelection, utf8ByteLength } from './selection/selection-limits.js';
9
9
  export { assertSelectionCurrent, finalizeConversationSelection, normalizeSelectedText, SelectionValidationError, type ConversationSelectionInput, } from './selection/selection-normalizer.js';
10
- export type { ConversationSelection, SelectionFragment, SelectionRect, SideChatModelSelection, SideChatState, } from '../shared/contracts.js';
10
+ export type { ConversationSelection, SelectionFragment, SelectionRect, SideChatModelSelection, SideChatMessage, SideChatId, SideChatState, } from '../shared/contracts.js';
@@ -60,7 +60,14 @@ export function SelectionQuote({ selections, messages, onRemove, }) {
60
60
  return (_jsxs("section", { ref: quoteRef, className: "dsh-side-chat-quote", "aria-label": messages.selectedPassage, "data-expanded": expanded || undefined, "data-hovered": hovered || undefined, onMouseEnter: () => {
61
61
  keepHoverOpen();
62
62
  constrainDetailsToBoundary();
63
- }, onMouseLeave: closeHoverAfterGrace, onFocusCapture: constrainDetailsToBoundary, children: [_jsxs("div", { className: "dsh-side-chat-quote-chip", children: [_jsxs("button", { type: "button", className: "dsh-side-chat-quote-trigger", "aria-expanded": expanded, "aria-label": `${expanded ? messages.collapse : messages.expand}: ${messages.selectedPassage}`, onClick: () => {
63
+ }, onMouseLeave: closeHoverAfterGrace, onFocusCapture: constrainDetailsToBoundary, onKeyDown: (event) => {
64
+ if (event.key !== 'Escape' || (!expanded && !hovered))
65
+ return;
66
+ event.preventDefault();
67
+ event.stopPropagation();
68
+ setExpanded(false);
69
+ setHovered(false);
70
+ }, children: [_jsxs("div", { className: "dsh-side-chat-quote-chip", children: [_jsxs("button", { type: "button", className: "dsh-side-chat-quote-trigger", "aria-expanded": expanded, "aria-label": `${expanded ? messages.collapse : messages.expand}: ${messages.selectedPassage}`, onClick: () => {
64
71
  if (expanded) {
65
72
  if (leaveTimerRef.current !== undefined)
66
73
  window.clearTimeout(leaveTimerRef.current);
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /** Error strip with one direct retry action. */
3
3
  export function SideChatErrorState({ error, messages, onRetry, }) {
4
- return (_jsxs("section", { className: "dsh-side-chat-error", role: "alert", children: [_jsx("strong", { children: error.code === 'side_chat_destroy_failed' ? messages.closeError : messages.genericError }), _jsx("p", { children: error.message }), error.recoverable && _jsx("button", { type: "button", onClick: onRetry, children: messages.retry })] }));
4
+ return (_jsxs("section", { className: "dsh-side-chat-error", role: "alert", children: [_jsx("strong", { children: error.operation === 'close' ? messages.closeError : messages.genericError }), _jsx("p", { children: error.message }), error.recoverable && _jsx("button", { type: "button", onClick: onRetry, children: messages.retry })] }));
5
5
  }
@@ -5,6 +5,7 @@ export interface SideChatModelSelectProps {
5
5
  readonly directory: ModelDirectory;
6
6
  readonly selection?: SideChatModelSelection | undefined;
7
7
  readonly locked: boolean;
8
+ readonly running?: boolean;
8
9
  readonly validateInitialSelection?: boolean;
9
10
  readonly locale?: 'en' | 'zh-CN';
10
11
  readonly onInitialize: (selection: SideChatModelSelection, options: {
@@ -13,4 +14,4 @@ export interface SideChatModelSelectProps {
13
14
  readonly onSelect: (selection: SideChatModelSelection) => Promise<SideChatActionResult<SideChatModelSelection>>;
14
15
  }
15
16
  /** Side Chat projection of DSH Web's native composer model selector. */
16
- export declare function SideChatModelSelect({ directory, selection, locked, validateInitialSelection, locale, onInitialize, onSelect, }: SideChatModelSelectProps): import("react/jsx-runtime").JSX.Element;
17
+ export declare function SideChatModelSelect({ directory, selection, locked, running, validateInitialSelection, locale, onInitialize, onSelect, }: SideChatModelSelectProps): import("react/jsx-runtime").JSX.Element;