@ahggg/dsh-side-chat 0.7.3 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -12
- package/README.zh-CN.md +15 -12
- package/lib/client/apply.js +48 -32
- package/lib/client/contracts.d.ts +3 -58
- package/lib/client/dsh/ReadOnlyConversation.d.ts +10 -0
- package/lib/client/dsh/ReadOnlyConversation.js +91 -0
- package/lib/client/dsh/SideChatOverlay.d.ts +7 -0
- package/lib/client/{rc6/Rc6SideChatOverlay.js → dsh/SideChatOverlay.js} +26 -56
- package/lib/client/{rc6 → dsh}/context.d.ts +4 -6
- package/lib/client/dsh/conversation-store.d.ts +22 -0
- package/lib/client/dsh/conversation-store.js +19 -0
- package/lib/client/dsh/primitive-labels.d.ts +2 -0
- package/lib/client/dsh/primitive-labels.js +4 -0
- package/lib/client/{rc6 → dsh}/remote-adapter.d.ts +2 -2
- package/lib/client/dsh/remote-adapter.js +22 -0
- package/lib/client/dsh/sessions-adapter.d.ts +56 -0
- package/lib/client/dsh/sessions-adapter.js +185 -0
- package/lib/client/index.d.ts +2 -2
- package/lib/client/panel/SelectionQuote.js +8 -1
- package/lib/client/panel/SideChatErrorState.js +1 -1
- package/lib/client/panel/SideChatModelSelect.js +37 -18
- package/lib/client/panel/SideChatPanel.js +12 -5
- package/lib/client/panel/messages.d.ts +1 -1
- package/lib/client/panel/messages.js +8 -8
- package/lib/client/parent-composer/ParentConversationAnnotations.d.ts +3 -2
- package/lib/client/parent-composer/ParentConversationAnnotations.js +50 -30
- package/lib/client/parent-composer/add-to-conversation.d.ts +14 -28
- package/lib/client/parent-composer/add-to-conversation.js +131 -545
- package/lib/client/parent-composer/annotation-persistence.d.ts +8 -8
- package/lib/client/parent-composer/annotation-persistence.js +106 -102
- package/lib/client/parent-composer/composer-reference.d.ts +17 -55
- package/lib/client/parent-composer/composer-reference.js +20 -72
- package/lib/client/parent-composer/referenced-conversation.d.ts +5 -7
- package/lib/client/parent-composer/referenced-conversation.js +21 -73
- package/lib/client/selection/ConversationAnnotationMarkers.js +5 -1
- package/lib/client/selection/SelectionActions.js +2 -1
- package/lib/client/selection/conversation-dom.d.ts +5 -0
- package/lib/client/selection/conversation-dom.js +24 -0
- package/lib/client/selection/selection-controller.d.ts +1 -1
- package/lib/client/selection/selection-controller.js +7 -3
- package/lib/client/selection/selection-normalizer.js +1 -1
- package/lib/client/side-chat-controller.d.ts +28 -41
- package/lib/client/side-chat-controller.js +245 -337
- package/lib/client.js +81 -52
- package/lib/host/index.d.ts +1 -1
- package/lib/host/index.js +1 -1
- package/lib/host/read-only-chat-service.d.ts +30 -0
- package/lib/host/read-only-chat-service.js +314 -0
- package/lib/index.d.ts +16 -8
- package/lib/index.js +34 -13
- package/lib/remote.d.ts +23 -14
- package/lib/remote.js +2 -5
- package/lib/shared/constants.d.ts +0 -13
- package/lib/shared/constants.js +0 -13
- package/lib/shared/contracts.d.ts +52 -19
- package/lib/shared/contracts.js +2 -3
- package/lib/shared/error-codes.d.ts +1 -1
- package/lib/shared/error-codes.js +2 -2
- package/lib/shared/{archived-wire.d.ts → side-chat-wire.d.ts} +109 -20
- package/lib/shared/side-chat-wire.js +34 -0
- package/lib/typert.d.ts +3 -68
- package/lib/typert.js +60 -4
- package/package.json +76 -56
- package/lib/client/rc6/ArchivedConversation.d.ts +0 -14
- package/lib/client/rc6/ArchivedConversation.js +0 -231
- package/lib/client/rc6/Rc6SideChatOverlay.d.ts +0 -7
- package/lib/client/rc6/SideChatTool.d.ts +0 -22
- package/lib/client/rc6/SideChatTool.js +0 -404
- package/lib/client/rc6/primitive-labels.d.ts +0 -7
- package/lib/client/rc6/primitive-labels.js +0 -60
- package/lib/client/rc6/remote-adapter.js +0 -34
- package/lib/client/rc6/runtime-compat.d.ts +0 -120
- package/lib/client/rc6/runtime-compat.js +0 -187
- package/lib/client/rc6/sessions-adapter.d.ts +0 -64
- package/lib/client/rc6/sessions-adapter.js +0 -387
- package/lib/host/archived-fork-service.d.ts +0 -28
- package/lib/host/archived-fork-service.js +0 -309
- package/lib/shared/archived-wire.js +0 -47
- /package/lib/client/{rc6 → dsh}/context.js +0 -0
|
@@ -1,37 +1,26 @@
|
|
|
1
1
|
import { ObservableValue } from '../shared/observable.js';
|
|
2
|
-
import { buildSideChatPrompt } from './parent-composer/add-to-conversation.js';
|
|
3
2
|
import { assertSelectionCurrent } from './selection/selection-normalizer.js';
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
function failure(error) {
|
|
9
|
-
return { ok: false, error };
|
|
10
|
-
}
|
|
11
|
-
function localError(code, message, recoverable = true) {
|
|
3
|
+
const INITIAL = Object.freeze({ phase: 'closed', draft: '', messages: [] });
|
|
4
|
+
const success = (value) => ({ ok: true, value });
|
|
5
|
+
const failure = (error) => ({ ok: false, error });
|
|
6
|
+
function localError(message, code = 'invalid_request', recoverable = false) {
|
|
12
7
|
return { code, message, recoverable };
|
|
13
8
|
}
|
|
14
|
-
|
|
15
|
-
switch (snapshot.status) {
|
|
16
|
-
case 'idle': return 'ready';
|
|
17
|
-
case 'running': return 'running';
|
|
18
|
-
case 'needs-input': return 'needs-input';
|
|
19
|
-
case 'needs-approval': return 'needs-approval';
|
|
20
|
-
case 'failed': return 'error';
|
|
21
|
-
case 'interrupted': return 'ready';
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
/** Small controller for the stock rc.6 archived-fork path. */
|
|
9
|
+
/** Owns only plugin UI state and a cancellable model stream, never a child Session. */
|
|
25
10
|
export class SideChatController {
|
|
26
11
|
remote;
|
|
27
12
|
sessions;
|
|
28
|
-
observable = new ObservableValue(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
closing;
|
|
33
|
-
closeRequested = false;
|
|
13
|
+
observable = new ObservableValue(INITIAL, 'dsh-side-chat');
|
|
14
|
+
generation = 0;
|
|
15
|
+
modelGeneration = 0;
|
|
16
|
+
requestSequence = 0;
|
|
34
17
|
disposed = false;
|
|
18
|
+
releaseParent;
|
|
19
|
+
creating;
|
|
20
|
+
closing;
|
|
21
|
+
running;
|
|
22
|
+
cancelling = false;
|
|
23
|
+
pendingRequest;
|
|
35
24
|
constructor(remote, sessions) {
|
|
36
25
|
this.remote = remote;
|
|
37
26
|
this.sessions = sessions;
|
|
@@ -40,306 +29,158 @@ export class SideChatController {
|
|
|
40
29
|
subscribe = (listener) => this.observable.subscribe(listener);
|
|
41
30
|
openDraft(input = {}) {
|
|
42
31
|
if (this.disposed)
|
|
43
|
-
return failure(localError('
|
|
44
|
-
if (this.getSnapshot().phase !== 'closed')
|
|
45
|
-
return failure(localError('
|
|
46
|
-
}
|
|
32
|
+
return failure(localError('Side Chat has been disposed.'));
|
|
33
|
+
if (this.getSnapshot().phase !== 'closed')
|
|
34
|
+
return failure(localError('Close the current Side Chat first.', 'side_chat_already_open'));
|
|
47
35
|
const parentSessionId = input.parentSessionId ?? this.sessions.currentSessionId();
|
|
48
|
-
if (parentSessionId === undefined)
|
|
49
|
-
return failure(localError('
|
|
50
|
-
}
|
|
36
|
+
if (parentSessionId === undefined)
|
|
37
|
+
return failure(localError('Start the main conversation first.', 'parent_session_missing'));
|
|
51
38
|
try {
|
|
52
39
|
if (input.selection !== undefined)
|
|
53
40
|
assertSelectionCurrent(input.selection, parentSessionId);
|
|
41
|
+
this.releaseParent = this.sessions.retainParent(parentSessionId);
|
|
54
42
|
}
|
|
55
|
-
catch (
|
|
56
|
-
return failure(localError(
|
|
43
|
+
catch (cause) {
|
|
44
|
+
return failure(localError(cause instanceof Error ? cause.message : 'The parent conversation is unavailable.', 'selection_stale'));
|
|
57
45
|
}
|
|
58
|
-
|
|
59
|
-
this.publish({
|
|
60
|
-
|
|
61
|
-
parentSessionId,
|
|
62
|
-
...(input.selection === undefined ? {} : { selection: input.selection }),
|
|
63
|
-
...(modelSelection === undefined ? {} : { modelSelection }),
|
|
64
|
-
draft: input.draft ?? '',
|
|
65
|
-
});
|
|
46
|
+
++this.generation;
|
|
47
|
+
this.observable.publish({ phase: 'draft', parentSessionId, selection: input.selection,
|
|
48
|
+
modelSelection: this.sessions.sideChatModelPreference(), draft: input.draft ?? '', messages: [] });
|
|
66
49
|
return success(undefined);
|
|
67
50
|
}
|
|
68
51
|
setDraft(draft) {
|
|
69
52
|
const state = this.getSnapshot();
|
|
70
|
-
if (
|
|
71
|
-
|
|
72
|
-
return failure(localError('invalid_request', 'The draft is not editable right now.', false));
|
|
53
|
+
if (!['draft', 'error', 'ready'].includes(state.phase) || state.messages.length > 0 || state.error?.operation === 'close') {
|
|
54
|
+
return failure(localError('The draft is not editable right now.'));
|
|
73
55
|
}
|
|
74
|
-
this.publish({ ...state, draft });
|
|
56
|
+
this.observable.publish({ ...state, draft });
|
|
75
57
|
return success(undefined);
|
|
76
58
|
}
|
|
77
59
|
clearSelection() {
|
|
78
60
|
const state = this.getSnapshot();
|
|
79
|
-
if (state.
|
|
80
|
-
return
|
|
81
|
-
|
|
82
|
-
return failure(localError('invalid_request', 'The selected passage has already been sent.', false));
|
|
83
|
-
}
|
|
84
|
-
this.publish({ ...state, selection: undefined });
|
|
61
|
+
if (state.chatId !== undefined || !['draft', 'error'].includes(state.phase))
|
|
62
|
+
return failure(localError('The selected context is already captured.'));
|
|
63
|
+
this.observable.publish({ ...state, selection: undefined });
|
|
85
64
|
return success(undefined);
|
|
86
65
|
}
|
|
87
|
-
initializeModel(
|
|
66
|
+
initializeModel(model) {
|
|
88
67
|
const state = this.getSnapshot();
|
|
89
|
-
if (state.
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
const selected = { ...selection };
|
|
94
|
-
this.publish({ ...state, modelSelection: selected });
|
|
95
|
-
return success(selected);
|
|
68
|
+
if (state.chatId !== undefined || !['draft', 'error'].includes(state.phase))
|
|
69
|
+
return failure(localError('The model cannot be initialized now.'));
|
|
70
|
+
this.observable.publish({ ...state, modelSelection: { ...model } });
|
|
71
|
+
return success({ ...model });
|
|
96
72
|
}
|
|
97
|
-
async selectModel(
|
|
73
|
+
async selectModel(model) {
|
|
98
74
|
const state = this.getSnapshot();
|
|
99
|
-
if (
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
const result = await this.invoke(() => this.remote.selectModel({
|
|
112
|
-
childSessionId,
|
|
113
|
-
...selected,
|
|
114
|
-
}));
|
|
75
|
+
if (this.cancelling || !['draft', 'error', 'ready'].includes(state.phase) || state.error?.operation === 'close')
|
|
76
|
+
return failure(localError('Stop the reply before changing models.'));
|
|
77
|
+
if (state.chatId === undefined) {
|
|
78
|
+
const result = this.initializeModel(model);
|
|
79
|
+
if (result.ok)
|
|
80
|
+
this.sessions.rememberSideChatModelPreference(result.value);
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
const generation = this.generation;
|
|
84
|
+
const version = ++this.modelGeneration;
|
|
85
|
+
const result = await this.invoke(() => this.remote.selectModel({ chatId: state.chatId, ...model }));
|
|
115
86
|
if (!result.ok)
|
|
116
|
-
return
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
this.publish({ ...latest, modelSelection: result.value.selected });
|
|
87
|
+
return result;
|
|
88
|
+
if (generation === this.generation && version === this.modelGeneration && this.getSnapshot().phase !== 'running') {
|
|
89
|
+
this.observable.publish({ ...this.getSnapshot(), modelSelection: result.value.selected });
|
|
120
90
|
this.sessions.rememberSideChatModelPreference(result.value.selected);
|
|
121
91
|
}
|
|
122
92
|
return success(result.value.selected);
|
|
123
93
|
}
|
|
124
94
|
async sendFirst(question) {
|
|
125
|
-
if (this.opening !== undefined) {
|
|
126
|
-
return failure(localError('invalid_request', 'The Side Chat is already opening.', false));
|
|
127
|
-
}
|
|
128
95
|
const state = this.getSnapshot();
|
|
129
|
-
const
|
|
130
|
-
if (
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
this.
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
96
|
+
const text = question.trim();
|
|
97
|
+
if (text.length === 0 || state.parentSessionId === undefined || state.messages.length > 0
|
|
98
|
+
|| !['draft', 'error', 'ready'].includes(state.phase) || state.error?.operation === 'close') {
|
|
99
|
+
return failure(localError('Enter a question in an open Side Chat draft.'));
|
|
100
|
+
}
|
|
101
|
+
if (state.chatId === undefined) {
|
|
102
|
+
if (state.selection !== undefined && !this.sessions.selectionIsCurrent(state.selection)) {
|
|
103
|
+
return this.fail(localError('Select the passage again before sending.', 'selection_stale'), 'create');
|
|
104
|
+
}
|
|
105
|
+
const atSeq = state.selection?.atSeq ?? this.sessions.lastCompletedSeq(state.parentSessionId);
|
|
106
|
+
if (atSeq === undefined)
|
|
107
|
+
return this.fail(localError('Wait for a completed parent turn.', 'parent_session_not_ready', true), 'create');
|
|
108
|
+
const generation = this.generation;
|
|
109
|
+
this.observable.publish({ ...state, phase: 'creating', draft: text, error: undefined });
|
|
110
|
+
const operation = this.invoke(() => this.remote.create({
|
|
111
|
+
parentSessionId: state.parentSessionId, atSeq,
|
|
112
|
+
selectedText: state.selection?.text, modelSelection: state.modelSelection,
|
|
113
|
+
}));
|
|
114
|
+
this.creating = operation;
|
|
115
|
+
const result = await operation;
|
|
116
|
+
if (this.creating === operation)
|
|
117
|
+
this.creating = undefined;
|
|
118
|
+
if (generation !== this.generation)
|
|
119
|
+
return failure(localError('The Side Chat was closed.'));
|
|
120
|
+
if (!result.ok)
|
|
121
|
+
return this.fail(result.error, 'create');
|
|
122
|
+
this.observable.publish({ ...this.getSnapshot(), phase: 'ready', chatId: result.value.chatId,
|
|
123
|
+
boundarySeq: result.value.boundarySeq, modelSelection: result.value.modelSelection, error: undefined });
|
|
124
|
+
}
|
|
125
|
+
return await this.send(text);
|
|
126
|
+
}
|
|
127
|
+
/** Resolves at admission; the independently consumed stream continues to update the transcript. */
|
|
128
|
+
async send(question) {
|
|
129
|
+
const state = this.getSnapshot();
|
|
130
|
+
const text = question.trim();
|
|
131
|
+
if (text.length === 0 || state.chatId === undefined || this.running !== undefined || this.cancelling
|
|
132
|
+
|| !['ready', 'error'].includes(state.phase) || state.error?.operation === 'close') {
|
|
133
|
+
return failure(localError('Wait for the current reply or stop it before sending.'));
|
|
134
|
+
}
|
|
135
|
+
const request = this.pendingRequest?.chatId === state.chatId && this.pendingRequest.text === text
|
|
136
|
+
? this.pendingRequest : { chatId: state.chatId, requestId: `${state.chatId}:${++this.requestSequence}`, text };
|
|
137
|
+
this.pendingRequest = request;
|
|
138
|
+
let stream;
|
|
152
139
|
try {
|
|
153
|
-
|
|
140
|
+
stream = this.remote.stream(request);
|
|
154
141
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
this.opening = undefined;
|
|
142
|
+
catch (cause) {
|
|
143
|
+
return this.fail(localError(String(cause), 'transport_error', true), 'prompt');
|
|
158
144
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
return await this.sendParts([{ type: 'text', text: trimmed }], mode);
|
|
165
|
-
}
|
|
166
|
-
async sendParts(content, mode = 'queue') {
|
|
167
|
-
if (content.length === 0)
|
|
168
|
-
return failure(localError('invalid_request', 'Enter a message.', false));
|
|
169
|
-
const binding = this.bindingTarget();
|
|
170
|
-
if (!binding.ok)
|
|
171
|
-
return binding;
|
|
172
|
-
const result = await this.invoke(() => binding.value.prompt(content, mode));
|
|
173
|
-
if (this.getSnapshot().phase === 'closing' || this.getSnapshot().phase === 'closed') {
|
|
174
|
-
return failure(localError('transport_error', 'The Side Chat was closed.', false));
|
|
175
|
-
}
|
|
176
|
-
if (!result.ok)
|
|
177
|
-
this.fail(result.error, 'prompt');
|
|
178
|
-
return result.ok ? success(undefined) : failure(result.error);
|
|
179
|
-
}
|
|
180
|
-
async updateQueue(itemId, action) {
|
|
181
|
-
const binding = this.bindingTarget();
|
|
182
|
-
if (!binding.ok)
|
|
183
|
-
return binding;
|
|
184
|
-
const result = await this.invoke(() => binding.value.updateQueue(itemId, action));
|
|
185
|
-
return result.ok ? success(undefined) : failure(result.error);
|
|
145
|
+
const active = { stream, generation: this.generation, request, cancelled: false };
|
|
146
|
+
this.running = active;
|
|
147
|
+
this.observable.publish({ ...state, phase: state.messages.length === 0 ? 'creating' : 'running', error: undefined });
|
|
148
|
+
const admission = new Promise(resolve => { active.done = this.consume(active, resolve); });
|
|
149
|
+
return await admission;
|
|
186
150
|
}
|
|
187
151
|
async cancel() {
|
|
188
|
-
const
|
|
189
|
-
if (
|
|
190
|
-
return binding;
|
|
191
|
-
const result = await this.invoke(() => binding.value.cancel());
|
|
192
|
-
return result.ok ? success(undefined) : failure(result.error);
|
|
193
|
-
}
|
|
194
|
-
async respondApproval(interactionId, decision) {
|
|
195
|
-
const binding = this.bindingTarget();
|
|
196
|
-
if (!binding.ok)
|
|
197
|
-
return binding;
|
|
198
|
-
const result = await this.invoke(() => binding.value.respondApproval(interactionId, decision));
|
|
199
|
-
return result.ok ? success(undefined) : failure(result.error);
|
|
200
|
-
}
|
|
201
|
-
async respondQuestion(interactionId, answer) {
|
|
202
|
-
const binding = this.bindingTarget();
|
|
203
|
-
if (!binding.ok)
|
|
204
|
-
return binding;
|
|
205
|
-
const result = await this.invoke(() => binding.value.respondQuestion(interactionId, answer));
|
|
206
|
-
return result.ok ? success(undefined) : failure(result.error);
|
|
207
|
-
}
|
|
208
|
-
async close() {
|
|
209
|
-
const state = this.getSnapshot();
|
|
210
|
-
if (state.phase === 'closed')
|
|
211
|
-
return success(undefined);
|
|
212
|
-
if (state.childSessionId === undefined && this.opening === undefined) {
|
|
213
|
-
this.reset();
|
|
152
|
+
const chatId = this.getSnapshot().chatId;
|
|
153
|
+
if (chatId === undefined || this.running === undefined)
|
|
214
154
|
return success(undefined);
|
|
155
|
+
this.cancelling = true;
|
|
156
|
+
const active = this.running;
|
|
157
|
+
active.cancelled = true;
|
|
158
|
+
this.disposeStream(active.stream);
|
|
159
|
+
try {
|
|
160
|
+
const result = await this.invoke(() => this.remote.cancel({ chatId }));
|
|
161
|
+
await active.done;
|
|
162
|
+
return result.ok ? success(undefined) : result;
|
|
215
163
|
}
|
|
216
|
-
|
|
217
|
-
this.
|
|
218
|
-
this.publish({ ...state, phase: 'closing', error: undefined });
|
|
219
|
-
await this.opening;
|
|
220
|
-
const after = this.getSnapshot();
|
|
221
|
-
if (after.phase === 'closed')
|
|
222
|
-
return success(undefined);
|
|
223
|
-
if (after.error?.operation === 'close')
|
|
224
|
-
return failure(after.error);
|
|
225
|
-
if (after.childSessionId === undefined) {
|
|
226
|
-
this.reset();
|
|
227
|
-
return success(undefined);
|
|
228
|
-
}
|
|
229
|
-
return await this.closeChild(after.childSessionId);
|
|
230
|
-
}
|
|
231
|
-
if (state.childSessionId === undefined) {
|
|
232
|
-
this.reset();
|
|
233
|
-
return success(undefined);
|
|
164
|
+
finally {
|
|
165
|
+
this.cancelling = false;
|
|
234
166
|
}
|
|
235
|
-
return await this.closeChild(state.childSessionId);
|
|
236
167
|
}
|
|
237
168
|
async retry() {
|
|
238
169
|
const state = this.getSnapshot();
|
|
239
170
|
if (state.error?.operation === 'close')
|
|
240
171
|
return await this.close();
|
|
241
|
-
if (state.
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
return await this.
|
|
245
|
-
|
|
246
|
-
return failure(localError('invalid_request', 'There is no failed operation to retry.', false));
|
|
247
|
-
}
|
|
248
|
-
async dispose() {
|
|
249
|
-
if (this.disposed)
|
|
250
|
-
return;
|
|
251
|
-
if (this.getSnapshot().phase !== 'closed') {
|
|
252
|
-
const result = await this.close();
|
|
253
|
-
if (!result.ok) {
|
|
254
|
-
this.sessions.notify({ kind: 'warning', text: 'The Side Chat could not be closed cleanly.' });
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
this.disposed = true;
|
|
258
|
-
this.detachLease();
|
|
259
|
-
this.observable.dispose();
|
|
260
|
-
}
|
|
261
|
-
async createOpenAndPrompt(parentSessionId, atSeq, question) {
|
|
262
|
-
const original = this.getSnapshot();
|
|
263
|
-
let childSessionId = original.childSessionId;
|
|
264
|
-
if (childSessionId === undefined) {
|
|
265
|
-
this.publish({ ...original, phase: 'creating', draft: question, firstQuestion: question, error: undefined });
|
|
266
|
-
const created = await this.invoke(() => this.remote.create({
|
|
267
|
-
parentSessionId,
|
|
268
|
-
atSeq,
|
|
269
|
-
...(original.modelSelection === undefined ? {} : { modelSelection: original.modelSelection }),
|
|
270
|
-
}));
|
|
271
|
-
if (!created.ok) {
|
|
272
|
-
if (this.closeRequested)
|
|
273
|
-
this.reset();
|
|
274
|
-
else
|
|
275
|
-
this.fail(created.error, 'create', { draft: question, firstQuestion: question });
|
|
276
|
-
return failure(created.error);
|
|
277
|
-
}
|
|
278
|
-
childSessionId = created.value.childSessionId;
|
|
279
|
-
this.publishCreated(created.value, question);
|
|
280
|
-
if (this.closeRequested || this.disposed) {
|
|
281
|
-
const closed = await this.closeChild(childSessionId);
|
|
282
|
-
return closed.ok
|
|
283
|
-
? failure(localError('transport_error', 'The Side Chat was closed.', false))
|
|
284
|
-
: closed;
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
if (this.lease === undefined) {
|
|
288
|
-
this.publish({ ...this.getSnapshot(), phase: 'opening', error: undefined });
|
|
289
|
-
try {
|
|
290
|
-
const lease = await this.sessions.retain(childSessionId);
|
|
291
|
-
if (this.closeRequested || this.disposed) {
|
|
292
|
-
lease.release();
|
|
293
|
-
const closed = await this.closeChild(childSessionId);
|
|
294
|
-
return closed.ok
|
|
295
|
-
? failure(localError('transport_error', 'The Side Chat was closed.', false))
|
|
296
|
-
: closed;
|
|
297
|
-
}
|
|
298
|
-
this.attachLease(lease, childSessionId);
|
|
299
|
-
}
|
|
300
|
-
catch {
|
|
301
|
-
const error = localError('side_chat_open_failed', 'The child Session could not be opened.');
|
|
302
|
-
this.fail(error, 'open', { firstQuestion: question, draft: question });
|
|
303
|
-
return failure(error);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
const binding = this.lease?.binding;
|
|
307
|
-
if (binding === undefined) {
|
|
308
|
-
const error = localError('side_chat_open_failed', 'The child Session is unavailable.');
|
|
309
|
-
this.fail(error, 'open', { firstQuestion: question, draft: question });
|
|
310
|
-
return failure(error);
|
|
311
|
-
}
|
|
312
|
-
const prompted = await this.invoke(() => binding.prompt(buildSideChatPrompt(original.selection, question), 'queue'));
|
|
313
|
-
if (!prompted.ok) {
|
|
314
|
-
this.fail(prompted.error, 'prompt', { firstQuestion: question, draft: question });
|
|
315
|
-
return failure(prompted.error);
|
|
316
|
-
}
|
|
317
|
-
this.publish({
|
|
318
|
-
...this.getSnapshot(),
|
|
319
|
-
phase: phaseOf(binding.getSnapshot()),
|
|
320
|
-
draft: '',
|
|
321
|
-
firstQuestion: undefined,
|
|
322
|
-
error: undefined,
|
|
323
|
-
});
|
|
324
|
-
return success(undefined);
|
|
325
|
-
}
|
|
326
|
-
publishCreated(created, question) {
|
|
327
|
-
this.publish({
|
|
328
|
-
...this.getSnapshot(),
|
|
329
|
-
phase: 'opening',
|
|
330
|
-
parentSessionId: created.parentSessionId,
|
|
331
|
-
childSessionId: created.childSessionId,
|
|
332
|
-
boundarySeq: created.boundarySeq,
|
|
333
|
-
inheritedThroughSeq: created.inheritedThroughSeq,
|
|
334
|
-
...(created.modelSelection === undefined ? {} : { modelSelection: created.modelSelection }),
|
|
335
|
-
firstQuestion: question,
|
|
336
|
-
error: undefined,
|
|
337
|
-
});
|
|
172
|
+
if (state.messages.length === 0)
|
|
173
|
+
return await this.sendFirst(state.draft);
|
|
174
|
+
if (this.pendingRequest !== undefined)
|
|
175
|
+
return await this.send(this.pendingRequest.text);
|
|
176
|
+
return failure(localError('An admitted question is never replayed automatically. Send a follow-up to continue.'));
|
|
338
177
|
}
|
|
339
|
-
async
|
|
178
|
+
async close() {
|
|
340
179
|
if (this.closing !== undefined)
|
|
341
180
|
return await this.closing;
|
|
342
|
-
|
|
181
|
+
if (this.getSnapshot().phase === 'closed')
|
|
182
|
+
return success(undefined);
|
|
183
|
+
const operation = this.closeCurrent();
|
|
343
184
|
this.closing = operation;
|
|
344
185
|
try {
|
|
345
186
|
return await operation;
|
|
@@ -349,74 +190,141 @@ export class SideChatController {
|
|
|
349
190
|
this.closing = undefined;
|
|
350
191
|
}
|
|
351
192
|
}
|
|
352
|
-
async
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
193
|
+
async dispose() {
|
|
194
|
+
if (this.disposed)
|
|
195
|
+
return;
|
|
196
|
+
this.disposed = true;
|
|
197
|
+
try {
|
|
198
|
+
const result = await this.close();
|
|
199
|
+
if (!result.ok)
|
|
200
|
+
this.sessions.notify({ kind: 'warning', text: result.error.message });
|
|
358
201
|
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
if (binding === undefined || state.childSessionId !== binding.sessionId
|
|
366
|
-
|| !['ready', 'running', 'needs-input', 'needs-approval'].includes(state.phase)) {
|
|
367
|
-
return failure(localError('invalid_request', 'The Side Chat is not accepting messages.', false));
|
|
202
|
+
finally {
|
|
203
|
+
if (this.running !== undefined)
|
|
204
|
+
this.disposeStream(this.running.stream);
|
|
205
|
+
this.releaseParent?.();
|
|
206
|
+
this.releaseParent = undefined;
|
|
207
|
+
this.observable.dispose();
|
|
368
208
|
}
|
|
369
|
-
return success(binding);
|
|
370
209
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
210
|
+
async consume(active, resolve) {
|
|
211
|
+
let admitted = false;
|
|
212
|
+
let terminal = false;
|
|
213
|
+
const current = () => active.generation === this.generation && this.running === active;
|
|
214
|
+
const assistantId = `${active.request.requestId}:assistant`;
|
|
215
|
+
try {
|
|
216
|
+
for await (const event of active.stream) {
|
|
217
|
+
if (!current())
|
|
218
|
+
break;
|
|
219
|
+
const state = this.getSnapshot();
|
|
220
|
+
if (event.type === 'started') {
|
|
221
|
+
if (admitted || event.requestId !== active.request.requestId)
|
|
222
|
+
throw new Error('Invalid Side Chat admission response.');
|
|
223
|
+
admitted = true;
|
|
224
|
+
this.pendingRequest = undefined;
|
|
225
|
+
this.observable.publish({ ...state, phase: 'running', draft: '', modelSelection: event.modelSelection,
|
|
226
|
+
messages: [...state.messages,
|
|
227
|
+
{ id: `${active.request.requestId}:user`, role: 'user', text: active.request.text, status: 'complete',
|
|
228
|
+
selectedText: state.messages.length === 0 ? state.selection?.text : undefined },
|
|
229
|
+
{ id: assistantId, role: 'assistant', text: '', reasoning: '', status: 'streaming' },
|
|
230
|
+
], error: undefined });
|
|
231
|
+
this.sessions.rememberSideChatModelPreference(event.modelSelection);
|
|
232
|
+
resolve(success(undefined));
|
|
233
|
+
}
|
|
234
|
+
else if (event.type === 'content') {
|
|
235
|
+
if (!admitted)
|
|
236
|
+
throw new Error('Side Chat content arrived before admission.');
|
|
237
|
+
this.observable.publish({ ...state, messages: state.messages.map(message => message.id === assistantId
|
|
238
|
+
? { ...message, text: event.text, reasoning: event.reasoning } : message) });
|
|
239
|
+
}
|
|
240
|
+
else if (event.type === 'finished') {
|
|
241
|
+
if (!admitted)
|
|
242
|
+
throw new Error('Side Chat ended before admission.');
|
|
243
|
+
terminal = true;
|
|
244
|
+
this.observable.publish({ ...state, phase: 'ready', messages: state.messages.map(message => message.id === assistantId
|
|
245
|
+
? { ...message, status: event.status } : message) });
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
terminal = true;
|
|
250
|
+
const issue = { ...event.error, recoverable: !admitted && event.error.recoverable };
|
|
251
|
+
this.streamFailure(issue, assistantId);
|
|
252
|
+
resolve(failure(issue));
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (current() && !terminal && !active.cancelled)
|
|
257
|
+
throw new Error('The Side Chat connection ended before the reply completed.');
|
|
258
|
+
}
|
|
259
|
+
catch (cause) {
|
|
260
|
+
if (current() && !active.cancelled) {
|
|
261
|
+
const issue = localError(cause instanceof Error ? cause.message : String(cause), 'transport_error', !admitted);
|
|
262
|
+
this.streamFailure(issue, assistantId);
|
|
263
|
+
resolve(failure(issue));
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
finally {
|
|
267
|
+
if (current() && active.cancelled) {
|
|
268
|
+
this.pendingRequest = undefined;
|
|
269
|
+
const state = this.getSnapshot();
|
|
270
|
+
this.observable.publish({ ...state, phase: 'ready', error: undefined,
|
|
271
|
+
messages: state.messages.map(message => message.id === assistantId ? { ...message, status: 'stopped' } : message) });
|
|
272
|
+
}
|
|
273
|
+
this.disposeStream(active.stream);
|
|
274
|
+
if (this.running === active)
|
|
275
|
+
this.running = undefined;
|
|
276
|
+
resolve(failure(localError('The Side Chat was closed.')));
|
|
375
277
|
}
|
|
376
|
-
this.detachLease();
|
|
377
|
-
this.lease = lease;
|
|
378
|
-
this.childUnsubscribe = lease.binding.subscribe(() => { this.updateFromChild(); });
|
|
379
|
-
this.updateFromChild();
|
|
380
278
|
}
|
|
381
|
-
|
|
382
|
-
this.
|
|
383
|
-
this.
|
|
384
|
-
|
|
385
|
-
|
|
279
|
+
streamFailure(issue, assistantId) {
|
|
280
|
+
const state = this.getSnapshot();
|
|
281
|
+
this.observable.publish({ ...state, phase: state.messages.length === 0 ? 'error' : 'ready',
|
|
282
|
+
messages: state.messages.map(message => message.id === assistantId ? { ...message, status: 'error' } : message),
|
|
283
|
+
error: { ...issue, operation: 'prompt' } });
|
|
386
284
|
}
|
|
387
|
-
|
|
285
|
+
async closeCurrent() {
|
|
388
286
|
const state = this.getSnapshot();
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
287
|
+
const creating = this.creating;
|
|
288
|
+
++this.generation;
|
|
289
|
+
if (this.running !== undefined)
|
|
290
|
+
this.disposeStream(this.running.stream);
|
|
291
|
+
this.running = undefined;
|
|
292
|
+
this.observable.publish({ ...state, phase: 'closing', error: undefined });
|
|
293
|
+
let chatId = state.chatId;
|
|
294
|
+
if (creating !== undefined) {
|
|
295
|
+
const created = await creating;
|
|
296
|
+
if (created.ok)
|
|
297
|
+
chatId = created.value.chatId;
|
|
298
|
+
}
|
|
299
|
+
if (chatId !== undefined) {
|
|
300
|
+
const result = await this.invoke(() => this.remote.close({ chatId: chatId }));
|
|
301
|
+
if (!result.ok && result.error.code !== 'side_chat_not_found') {
|
|
302
|
+
this.observable.publish({ ...this.getSnapshot(), phase: 'error', chatId, error: { ...result.error, operation: 'close' } });
|
|
303
|
+
return result;
|
|
304
|
+
}
|
|
395
305
|
}
|
|
396
|
-
this.
|
|
306
|
+
this.pendingRequest = undefined;
|
|
307
|
+
this.releaseParent?.();
|
|
308
|
+
this.releaseParent = undefined;
|
|
309
|
+
this.observable.publish(INITIAL);
|
|
310
|
+
return success(undefined);
|
|
397
311
|
}
|
|
398
|
-
fail(
|
|
399
|
-
this.publish({
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
312
|
+
fail(issue, operation) {
|
|
313
|
+
this.observable.publish({ ...this.getSnapshot(), phase: 'error', error: { ...issue, operation } });
|
|
314
|
+
return failure(issue);
|
|
315
|
+
}
|
|
316
|
+
disposeStream(stream) {
|
|
317
|
+
try {
|
|
318
|
+
stream.dispose();
|
|
319
|
+
}
|
|
320
|
+
catch { /* A broken carrier must not prevent the explicit Host cancel/close RPC or parent release. */ }
|
|
405
321
|
}
|
|
406
322
|
async invoke(operation) {
|
|
407
323
|
try {
|
|
408
324
|
return await operation();
|
|
409
325
|
}
|
|
410
|
-
catch {
|
|
411
|
-
return
|
|
326
|
+
catch (cause) {
|
|
327
|
+
return failure(localError(cause instanceof Error ? cause.message : String(cause), 'transport_error', true));
|
|
412
328
|
}
|
|
413
329
|
}
|
|
414
|
-
reset() {
|
|
415
|
-
this.detachLease();
|
|
416
|
-
this.closeRequested = false;
|
|
417
|
-
this.publish(INITIAL_STATE);
|
|
418
|
-
}
|
|
419
|
-
publish(state) {
|
|
420
|
-
this.observable.publish(Object.freeze(state));
|
|
421
|
-
}
|
|
422
330
|
}
|