@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
@@ -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
  }
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useEffect, useId, useMemo, useRef, useState, useSyncExternalStore, } from 'react';
3
- import { IconCheckOutline16, IconChevronDownOutline14, IconChevronRightOutline14, IconWarningOutline16, Toast, } from '@deepseek-ai/dsh-client-ui-primitives';
3
+ import { IconCheckOutlineRegular as IconCheckOutline16, IconChevronDownOutlineRegular as IconChevronDownOutline14, IconChevronRightOutlineRegular as IconChevronRightOutline14, IconWarningOutlineRegular as IconWarningOutline16, Toast, } from '@deepseek-ai/dsh-client-ui-primitives';
4
4
  const MODEL_MESSAGES = {
5
5
  en: {
6
6
  selectModel: 'Select model',
@@ -74,6 +74,7 @@ export function SideChatModelSelect({ directory, selection, locked, validateInit
74
74
  const initialized = useRef(false);
75
75
  const operation = useRef(0);
76
76
  const toastSequence = useRef(0);
77
+ const selectionPending = useRef(false);
77
78
  const id = useId();
78
79
  const choices = useMemo(() => state.groups.flatMap(group => group.models.map(model => ({ group, model }))), [state.groups]);
79
80
  const current = selection ?? state.current ?? undefined;
@@ -98,14 +99,20 @@ export function SideChatModelSelect({ directory, selection, locked, validateInit
98
99
  ...(effort.description === undefined ? {} : { description: effort.description }),
99
100
  })),
100
101
  ], [messages.providerDefault, reasoning]);
101
- const modelLabel = currentChoice?.model.name ?? messages.selectModel;
102
+ const modelLabel = currentChoice?.model.name ?? current?.model ?? messages.selectModel;
102
103
  const triggerLabel = effortLabel === undefined ? modelLabel : `${modelLabel} · ${effortLabel}`;
103
104
  const reload = () => {
104
105
  void directory.load().catch(() => undefined);
105
106
  };
106
- useEffect(() => { reload(); }, [directory]);
107
107
  useEffect(() => {
108
- if (initialized.current)
108
+ initialized.current = false;
109
+ reload();
110
+ return () => { ++operation.current; };
111
+ }, [directory]);
112
+ useEffect(() => { if (locked)
113
+ setOpen(false); }, [locked]);
114
+ useEffect(() => {
115
+ if (initialized.current || locked)
109
116
  return;
110
117
  if (selection !== undefined && !validateInitialSelection) {
111
118
  initialized.current = true;
@@ -125,13 +132,13 @@ export function SideChatModelSelect({ directory, selection, locked, validateInit
125
132
  onInitialize(directoryCurrent, { remember: false });
126
133
  return;
127
134
  }
128
- if (state.status !== 'ready')
135
+ if (state.status !== 'ready' || state.error !== null || state.failures.length > 0)
129
136
  return;
130
137
  initialized.current = true;
131
138
  const repaired = repairSelection(selection, choices, directoryCurrent);
132
139
  if (!sameSelection(selection, repaired))
133
140
  onInitialize(repaired, { remember: true });
134
- }, [choices, onInitialize, selection, state.current, state.status, validateInitialSelection]);
141
+ }, [choices, locked, onInitialize, selection, state.current, state.status, state.error, state.failures, validateInitialSelection]);
135
142
  useEffect(() => {
136
143
  if (!open)
137
144
  return;
@@ -163,6 +170,7 @@ export function SideChatModelSelect({ directory, selection, locked, validateInit
163
170
  const onKeyDown = (event) => {
164
171
  if (event.key === 'Escape' && open) {
165
172
  event.preventDefault();
173
+ event.stopPropagation();
166
174
  if (pane !== 'root')
167
175
  setPane('root');
168
176
  else
@@ -180,21 +188,32 @@ export function SideChatModelSelect({ directory, selection, locked, validateInit
180
188
  close();
181
189
  };
182
190
  const submitSelection = async (next) => {
191
+ if (locked || selectionPending.current)
192
+ return;
183
193
  const generation = ++operation.current;
194
+ selectionPending.current = true;
184
195
  setSelecting(true);
185
- const result = await onSelect(next);
186
- if (generation !== operation.current)
187
- return;
188
- setSelecting(false);
189
- if (!result.ok) {
190
- toastSequence.current += 1;
196
+ try {
197
+ const result = await onSelect(next);
198
+ if (generation !== operation.current)
199
+ return;
200
+ if (!result.ok)
201
+ throw new Error(result.error.message);
202
+ close(true);
203
+ }
204
+ catch (error) {
205
+ if (generation !== operation.current)
206
+ return;
191
207
  setToast({
192
- seq: toastSequence.current,
193
- text: messages.operationFailed(result.error.message),
208
+ seq: ++toastSequence.current,
209
+ text: messages.operationFailed(error instanceof Error ? error.message : 'Could not select the model.'),
194
210
  });
195
- return;
196
211
  }
197
- close(true);
212
+ finally {
213
+ selectionPending.current = false;
214
+ if (generation === operation.current)
215
+ setSelecting(false);
216
+ }
198
217
  };
199
218
  const chooseModel = (group, model) => {
200
219
  if (sameModel(current, group.id, model.id)) {
@@ -235,12 +254,12 @@ export function SideChatModelSelect({ directory, selection, locked, validateInit
235
254
  const headingId = `${id}-${group.id}`;
236
255
  return (_jsxs("section", { role: "group", "aria-labelledby": headingId, className: "dsh-side-chat-model-group", children: [_jsx("div", { id: headingId, className: "dsh-side-chat-model-group-title", children: group.name }), group.models.map(model => {
237
256
  const selected = sameModel(current, group.id, model.id);
238
- return (_jsxs("button", { ref: itemRef(), type: "button", role: "menuitemradio", "aria-checked": selected, className: `dsh-side-chat-model-option${selected ? ' dsh-side-chat-model-selected' : ''}`, title: model.name, disabled: selecting, onClick: () => { chooseModel(group, model); }, children: [_jsxs("span", { className: "dsh-side-chat-model-option-copy", children: [_jsx("span", { className: "dsh-side-chat-model-name", children: model.name }), model.description !== undefined && (_jsx("span", { className: "dsh-side-chat-model-description", children: model.description }))] }), _jsx("span", { className: "dsh-side-chat-model-check", children: selected ? _jsx(IconCheckOutline16, {}) : null })] }, model.id));
257
+ return (_jsxs("button", { ref: itemRef(), type: "button", role: "menuitemradio", "aria-checked": selected, className: `dsh-side-chat-model-option${selected ? ' dsh-side-chat-model-selected' : ''}`, title: model.name, disabled: locked || selecting, onClick: () => { chooseModel(group, model); }, children: [_jsxs("span", { className: "dsh-side-chat-model-option-copy", children: [_jsx("span", { className: "dsh-side-chat-model-name", children: model.name }), model.description !== undefined && (_jsx("span", { className: "dsh-side-chat-model-description", children: model.description }))] }), _jsx("span", { className: "dsh-side-chat-model-check", children: selected ? _jsx(IconCheckOutline16, {}) : null })] }, model.id));
239
258
  })] }, group.id));
240
259
  }) }), state.status === 'ready' && choices.length === 0 && (_jsx("div", { className: "dsh-side-chat-model-empty", children: messages.emptyModels }))] })), pane === 'effort' && (effortChoices.length === 0
241
260
  ? _jsx("div", { className: "dsh-side-chat-model-empty", children: messages.emptyEfforts })
242
261
  : effortChoices.map(level => {
243
262
  const selected = effectiveEffort === level.effort;
244
- return (_jsxs("button", { ref: itemRef(), type: "button", role: "menuitemradio", "aria-checked": selected, className: `dsh-side-chat-model-option${selected ? ' dsh-side-chat-model-selected' : ''}`, disabled: selecting, onClick: () => { chooseEffort(level.effort); }, children: [_jsxs("span", { className: "dsh-side-chat-model-option-copy", children: [_jsx("span", { className: "dsh-side-chat-model-name", children: level.label }), level.description !== undefined && (_jsx("span", { className: "dsh-side-chat-model-description", children: level.description }))] }), _jsx("span", { className: "dsh-side-chat-model-check", children: selected ? _jsx(IconCheckOutline16, {}) : null })] }, level.key));
263
+ return (_jsxs("button", { ref: itemRef(), type: "button", role: "menuitemradio", "aria-checked": selected, className: `dsh-side-chat-model-option${selected ? ' dsh-side-chat-model-selected' : ''}`, disabled: locked || selecting, onClick: () => { chooseEffort(level.effort); }, children: [_jsxs("span", { className: "dsh-side-chat-model-option-copy", children: [_jsx("span", { className: "dsh-side-chat-model-name", children: level.label }), level.description !== undefined && (_jsx("span", { className: "dsh-side-chat-model-description", children: level.description }))] }), _jsx("span", { className: "dsh-side-chat-model-check", children: selected ? _jsx(IconCheckOutline16, {}) : null })] }, level.key));
245
264
  }))] })), toast !== null && (_jsx(Toast, { text: toast.text, icon: _jsx(IconWarningOutline16, {}), anchor: rootRef.current?.closest('[data-composer-card]') ?? null, onDone: () => { setToast(null); } }, toast.seq))] }));
246
265
  }
@@ -13,7 +13,7 @@ export function SideChatPanel({ state, locale = 'en', embeddedConversation, mode
13
13
  const draftRef = useAutoGrowingTextarea(state.draft);
14
14
  const submit = async (event) => {
15
15
  event.preventDefault();
16
- if (submitting)
16
+ if (submitting || ['creating', 'running', 'closing'].includes(state.phase))
17
17
  return;
18
18
  setSubmitting(true);
19
19
  try {
@@ -23,12 +23,19 @@ export function SideChatPanel({ state, locale = 'en', embeddedConversation, mode
23
23
  setSubmitting(false);
24
24
  }
25
25
  };
26
- return (_jsxs("aside", { className: "dsh-side-chat-panel", "data-side-chat-panel": "", "aria-label": messages.title, "aria-busy": ['creating', 'opening', 'closing'].includes(state.phase) || undefined, children: [_jsx(SideChatHeader, { phase: state.phase, messages: messages, ...onAddToConversation === undefined ? {} : { onAddToConversation }, ...addToConversationDisabled === undefined ? {} : { addToConversationDisabled }, onFocusParent: onFocusParent, onClose: () => { void onClose(); } }), state.error !== undefined && (_jsx(SideChatErrorState, { error: state.error, messages: messages, onRetry: () => { void onRetry(); } })), state.childSessionId !== undefined && embeddedConversation !== undefined
26
+ return (_jsxs("aside", { className: "dsh-side-chat-panel", "data-side-chat-panel": "", "aria-label": messages.title, "aria-busy": ['creating', 'closing'].includes(state.phase) || undefined, onKeyDown: (event) => {
27
+ if (event.key === 'Escape' && !event.defaultPrevented && !event.nativeEvent.isComposing) {
28
+ event.preventDefault();
29
+ event.stopPropagation();
30
+ void onClose();
31
+ }
32
+ }, children: [_jsx(SideChatHeader, { phase: state.phase, messages: messages, ...onAddToConversation === undefined ? {} : { onAddToConversation }, ...addToConversationDisabled === undefined ? {} : { addToConversationDisabled }, onFocusParent: onFocusParent, onClose: () => { void onClose(); } }), state.error !== undefined && (_jsx(SideChatErrorState, { error: state.error, messages: messages, onRetry: () => { void onRetry(); } })), state.messages.length > 0 && embeddedConversation !== undefined
27
33
  ? _jsx(SideChatBody, { children: embeddedConversation })
28
- : (_jsxs("form", { className: "dsh-side-chat-draft", "data-composer-card": "", onSubmit: (event) => { void submit(event); }, children: [state.selection !== undefined && (_jsx(SelectionQuote, { selections: [state.selection], messages: messages, ...onRemoveSelection === undefined ? {} : { onRemove: onRemoveSelection } })), _jsx("label", { htmlFor: "dsh-side-chat-draft-input", children: messages.placeholder }), _jsx("textarea", { id: "dsh-side-chat-draft-input", ref: draftRef, autoFocus: true, rows: 1, value: state.draft, disabled: ['creating', 'opening', 'closing'].includes(state.phase), placeholder: messages.placeholder, onChange: (event) => { onDraftChange(event.target.value); }, onKeyDown: (event) => {
29
- if (event.key === 'Enter' && !event.shiftKey) {
34
+ : (_jsxs("form", { className: "dsh-side-chat-draft", "data-composer-card": "", onSubmit: (event) => { void submit(event); }, children: [state.selection !== undefined && (_jsx(SelectionQuote, { selections: [state.selection], messages: messages, ...onRemoveSelection === undefined ? {} : { onRemove: onRemoveSelection } })), _jsx("label", { htmlFor: "dsh-side-chat-draft-input", children: messages.placeholder }), _jsx("textarea", { id: "dsh-side-chat-draft-input", ref: draftRef, autoFocus: true, rows: 1, value: state.draft, disabled: ['creating', 'running', 'closing'].includes(state.phase), placeholder: messages.placeholder, onChange: (event) => { onDraftChange(event.target.value); }, onKeyDown: (event) => {
35
+ if (event.key === 'Enter' && !event.shiftKey && !event.nativeEvent.isComposing
36
+ && event.keyCode !== 229) {
30
37
  event.preventDefault();
31
38
  event.currentTarget.form?.requestSubmit();
32
39
  }
33
- } }), _jsxs("div", { className: "dsh-side-chat-draft-actions", children: [modelControl, _jsx("button", { type: "submit", className: "dsh-side-chat-send-button", "aria-label": messages.send, disabled: submitting || state.draft.trim().length === 0, children: _jsx(SendIcon, {}) })] })] })), _jsxs("footer", { className: "dsh-side-chat-footer", children: [_jsx("span", { children: messages.temporary }), _jsx("span", { children: messages.referenceOnly }), _jsx("span", { children: messages.cannotReopen }), _jsx("span", { children: messages.sharedWorkspace })] }), _jsx("div", { className: "dsh-side-chat-announcer", "aria-live": "polite", children: state.phase === 'running' ? 'Side Chat running' : `Side Chat ${state.phase}` })] }));
40
+ } }), _jsxs("div", { className: "dsh-side-chat-draft-actions", children: [modelControl, _jsx("button", { type: "submit", className: "dsh-side-chat-send-button", "aria-label": messages.send, disabled: submitting || ['creating', 'running', 'closing'].includes(state.phase) || state.draft.trim().length === 0, children: _jsx(SendIcon, {}) })] })] })), _jsxs("footer", { className: "dsh-side-chat-footer", children: [_jsx("span", { children: messages.temporary }), _jsx("span", { children: messages.referenceOnly }), _jsx("span", { children: messages.cannotReopen }), _jsx("span", { children: messages.readOnly })] }), _jsx("div", { className: "dsh-side-chat-announcer", "aria-live": "polite", children: state.phase === 'running' ? 'Side Chat running' : `Side Chat ${state.phase}` })] }));
34
41
  }
@@ -14,7 +14,7 @@ export interface SideChatMessages {
14
14
  readonly temporary: string;
15
15
  readonly referenceOnly: string;
16
16
  readonly cannotReopen: string;
17
- readonly sharedWorkspace: string;
17
+ readonly readOnly: string;
18
18
  readonly retry: string;
19
19
  readonly genericError: string;
20
20
  readonly closeError: string;
@@ -12,10 +12,10 @@ export const SIDE_CHAT_MESSAGES = Object.freeze({
12
12
  removeSelection: 'Remove annotation',
13
13
  expand: 'Expand',
14
14
  collapse: 'Collapse',
15
- temporary: 'Archived when closed; history remains on disk',
16
- referenceOnly: 'Inherits the complete parent conversation prefix',
17
- cannotReopen: 'No reopen action',
18
- sharedWorkspace: 'Shares the parent workspace',
15
+ temporary: 'Temporary; discarded when closed or reloaded',
16
+ referenceOnly: 'Read-only parent context; no Session copy',
17
+ cannotReopen: 'Add to conversation to keep this discussion',
18
+ readOnly: 'No tools or file changes',
19
19
  retry: 'Retry',
20
20
  genericError: 'Side Chat error',
21
21
  closeError: 'Could not close the Side Chat',
@@ -33,10 +33,10 @@ export const SIDE_CHAT_MESSAGES = Object.freeze({
33
33
  removeSelection: '移除引用',
34
34
  expand: '展开',
35
35
  collapse: '收起',
36
- temporary: '关闭时归档;历史仍保存在磁盘上',
37
- referenceOnly: '完整继承父会话对话前缀',
38
- cannotReopen: '不提供重新打开操作',
39
- sharedWorkspace: '与父会话共享工作区',
36
+ temporary: '临时对话;关闭或刷新后丢弃',
37
+ referenceOnly: '只读父会话上下文;不复制 Session',
38
+ cannotReopen: '添加到主对话以保留讨论内容',
39
+ readOnly: '不使用工具,不修改文件',
40
40
  retry: '重试',
41
41
  genericError: '侧边对话错误',
42
42
  closeError: '无法关闭侧边对话',
@@ -1,5 +1,6 @@
1
1
  import { type ElementType, type ReactNode } from 'react';
2
- import type { Rc6ClientContext } from '../rc6/context.js';
2
+ import type { DshClientContext } from '../dsh/context.js';
3
+ import { SessionId } from '../../shared/contracts.js';
3
4
  import { SIDE_CHAT_MESSAGES } from '../panel/messages.js';
4
5
  import { type ParentComposerInputSnapshot } from './add-to-conversation.js';
5
6
  type Locale = keyof typeof SIDE_CHAT_MESSAGES;
@@ -24,5 +25,5 @@ export declare function ParentComposerAnnotations({ input, onRemove, locale, }:
24
25
  /** Wrap DSH's own user renderer only when this plugin's durable prefix exists. */
25
26
  export declare function annotatedUserMessageRenderer(Original: UserNodeRenderer): (props: UserNodeProps) => ReactNode;
26
27
  /** Mount the composer capsule and a thin wrapper around DSH's user renderers. */
27
- export declare function mountParentConversationAnnotations(ctx: Rc6ClientContext, removeAnnotations: () => void): () => void;
28
+ export declare function mountParentConversationAnnotations(ctx: DshClientContext, removeAnnotations: (sessionId: SessionId) => void): () => void;
28
29
  export {};