@ahggg/dsh-side-chat 0.7.3 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -12
- package/README.zh-CN.md +15 -12
- package/lib/client/apply.js +48 -32
- package/lib/client/contracts.d.ts +3 -58
- package/lib/client/dsh/ReadOnlyConversation.d.ts +10 -0
- package/lib/client/dsh/ReadOnlyConversation.js +91 -0
- package/lib/client/dsh/SideChatOverlay.d.ts +7 -0
- package/lib/client/{rc6/Rc6SideChatOverlay.js → dsh/SideChatOverlay.js} +26 -56
- package/lib/client/{rc6 → dsh}/context.d.ts +4 -6
- package/lib/client/dsh/conversation-store.d.ts +22 -0
- package/lib/client/dsh/conversation-store.js +19 -0
- package/lib/client/dsh/primitive-labels.d.ts +2 -0
- package/lib/client/dsh/primitive-labels.js +4 -0
- package/lib/client/{rc6 → dsh}/remote-adapter.d.ts +2 -2
- package/lib/client/dsh/remote-adapter.js +22 -0
- package/lib/client/dsh/sessions-adapter.d.ts +56 -0
- package/lib/client/dsh/sessions-adapter.js +185 -0
- package/lib/client/index.d.ts +2 -2
- package/lib/client/panel/SelectionQuote.js +8 -1
- package/lib/client/panel/SideChatErrorState.js +1 -1
- package/lib/client/panel/SideChatModelSelect.js +37 -18
- package/lib/client/panel/SideChatPanel.js +12 -5
- package/lib/client/panel/messages.d.ts +1 -1
- package/lib/client/panel/messages.js +8 -8
- package/lib/client/parent-composer/ParentConversationAnnotations.d.ts +3 -2
- package/lib/client/parent-composer/ParentConversationAnnotations.js +50 -30
- package/lib/client/parent-composer/add-to-conversation.d.ts +14 -28
- package/lib/client/parent-composer/add-to-conversation.js +131 -545
- package/lib/client/parent-composer/annotation-persistence.d.ts +8 -8
- package/lib/client/parent-composer/annotation-persistence.js +106 -102
- package/lib/client/parent-composer/composer-reference.d.ts +17 -55
- package/lib/client/parent-composer/composer-reference.js +20 -72
- package/lib/client/parent-composer/referenced-conversation.d.ts +5 -7
- package/lib/client/parent-composer/referenced-conversation.js +21 -73
- package/lib/client/selection/ConversationAnnotationMarkers.js +5 -1
- package/lib/client/selection/SelectionActions.js +2 -1
- package/lib/client/selection/conversation-dom.d.ts +5 -0
- package/lib/client/selection/conversation-dom.js +24 -0
- package/lib/client/selection/selection-controller.d.ts +1 -1
- package/lib/client/selection/selection-controller.js +7 -3
- package/lib/client/selection/selection-normalizer.js +1 -1
- package/lib/client/side-chat-controller.d.ts +28 -41
- package/lib/client/side-chat-controller.js +245 -337
- package/lib/client.js +81 -52
- package/lib/host/index.d.ts +1 -1
- package/lib/host/index.js +1 -1
- package/lib/host/read-only-chat-service.d.ts +30 -0
- package/lib/host/read-only-chat-service.js +314 -0
- package/lib/index.d.ts +16 -8
- package/lib/index.js +34 -13
- package/lib/remote.d.ts +23 -14
- package/lib/remote.js +2 -5
- package/lib/shared/constants.d.ts +0 -13
- package/lib/shared/constants.js +0 -13
- package/lib/shared/contracts.d.ts +52 -19
- package/lib/shared/contracts.js +2 -3
- package/lib/shared/error-codes.d.ts +1 -1
- package/lib/shared/error-codes.js +2 -2
- package/lib/shared/{archived-wire.d.ts → side-chat-wire.d.ts} +109 -20
- package/lib/shared/side-chat-wire.js +34 -0
- package/lib/typert.d.ts +3 -68
- package/lib/typert.js +60 -4
- package/package.json +76 -56
- package/lib/client/rc6/ArchivedConversation.d.ts +0 -14
- package/lib/client/rc6/ArchivedConversation.js +0 -231
- package/lib/client/rc6/Rc6SideChatOverlay.d.ts +0 -7
- package/lib/client/rc6/SideChatTool.d.ts +0 -22
- package/lib/client/rc6/SideChatTool.js +0 -404
- package/lib/client/rc6/primitive-labels.d.ts +0 -7
- package/lib/client/rc6/primitive-labels.js +0 -60
- package/lib/client/rc6/remote-adapter.js +0 -34
- package/lib/client/rc6/runtime-compat.d.ts +0 -120
- package/lib/client/rc6/runtime-compat.js +0 -187
- package/lib/client/rc6/sessions-adapter.d.ts +0 -64
- package/lib/client/rc6/sessions-adapter.js +0 -387
- package/lib/host/archived-fork-service.d.ts +0 -28
- package/lib/host/archived-fork-service.js +0 -309
- package/lib/shared/archived-wire.js +0 -47
- /package/lib/client/{rc6 → dsh}/context.js +0 -0
|
@@ -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
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { SideChatRemote } from '../../shared/contracts.js';
|
|
2
|
-
import type {
|
|
3
|
-
export declare function
|
|
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
|
+
}
|
package/lib/client/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { apply, inject, name } from './apply.js';
|
|
2
|
-
export type { SideChatActionResult, SideChatClientSessions,
|
|
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,
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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(
|
|
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
|
-
|
|
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', '
|
|
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', '
|
|
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.
|
|
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
|
|
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: '
|
|
16
|
-
referenceOnly: '
|
|
17
|
-
cannotReopen: '
|
|
18
|
-
|
|
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
|
-
|
|
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 {
|
|
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:
|
|
28
|
+
export declare function mountParentConversationAnnotations(ctx: DshClientContext, removeAnnotations: (sessionId: SessionId) => void): () => void;
|
|
28
29
|
export {};
|