@yeaft/webchat-agent 0.1.937 → 0.1.938
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/conversation.js +30 -12
- package/package.json +1 -1
- package/providers/copilot.js +108 -57
package/conversation.js
CHANGED
|
@@ -457,11 +457,19 @@ export async function resumeConversation(msg) {
|
|
|
457
457
|
if (id === conversationId || (claudeSessionId && conv.claudeSessionId === claudeSessionId)) {
|
|
458
458
|
console.log(`[Resume] Cleaning up old conversation: ${id} (claudeSessionId: ${conv.claudeSessionId})`);
|
|
459
459
|
if (conv.providerOptions && !priorProviderOptions) priorProviderOptions = conv.providerOptions;
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
460
|
+
let cleanupDriver = null;
|
|
461
|
+
try {
|
|
462
|
+
cleanupDriver = getProvider(conv.providerName || provider || DEFAULT_PROVIDER);
|
|
463
|
+
} catch { /* fallback to legacy cleanup below */ }
|
|
464
|
+
if (typeof cleanupDriver?.dispose === 'function') {
|
|
465
|
+
cleanupDriver.dispose(conv, 'resume cleanup');
|
|
466
|
+
} else {
|
|
467
|
+
if (conv.abortController) {
|
|
468
|
+
conv.abortController.abort();
|
|
469
|
+
}
|
|
470
|
+
if (conv.inputStream) {
|
|
471
|
+
try { conv.inputStream.done(); } catch {}
|
|
472
|
+
}
|
|
465
473
|
}
|
|
466
474
|
ctx.conversations.delete(id);
|
|
467
475
|
}
|
|
@@ -783,17 +791,23 @@ export async function handleUserInput(msg) {
|
|
|
783
791
|
|
|
784
792
|
// /clear for capable providers — reset session in-place without spawning new turn
|
|
785
793
|
if (slashCommand.type === 'slash' && slashCommand.command === '/clear') {
|
|
794
|
+
state.turnActive = true;
|
|
795
|
+
state.turnCompletedEmitted = false;
|
|
796
|
+
state.turnErrorEmitted = false;
|
|
786
797
|
if (typeof driver.clear === 'function' && driver.capabilities?.clear) {
|
|
787
798
|
try { await driver.clear(state); } catch (err) {
|
|
788
799
|
console.warn(`[${conversationId}] driver.clear failed:`, err?.message || err);
|
|
789
800
|
}
|
|
790
801
|
}
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
802
|
+
if (!state.turnCompletedEmitted) {
|
|
803
|
+
ctx.sendToServer({
|
|
804
|
+
type: 'turn_completed',
|
|
805
|
+
conversationId,
|
|
806
|
+
claudeSessionId: state.sessionId || state.claudeSessionId,
|
|
807
|
+
workDir: state.workDir
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
state.turnActive = false;
|
|
797
811
|
return;
|
|
798
812
|
}
|
|
799
813
|
|
|
@@ -812,6 +826,10 @@ export async function handleUserInput(msg) {
|
|
|
812
826
|
});
|
|
813
827
|
} finally {
|
|
814
828
|
state.turnActive = false;
|
|
829
|
+
if (state._abortKillTimer) {
|
|
830
|
+
clearTimeout(state._abortKillTimer);
|
|
831
|
+
state._abortKillTimer = null;
|
|
832
|
+
}
|
|
815
833
|
sendConversationList();
|
|
816
834
|
}
|
|
817
835
|
return;
|
|
@@ -976,7 +994,7 @@ export function handleAskUserAnswer(msg) {
|
|
|
976
994
|
if (typeof driver.respondToPermissionRequest === 'function') {
|
|
977
995
|
const ans = msg.answers || {};
|
|
978
996
|
const optionId = typeof ans === 'string' ? ans
|
|
979
|
-
: ans.optionId || ans.option || Object.values(ans)[0];
|
|
997
|
+
: ans.optionId || ans.option || ans['Copilot permission'] || Object.values(ans)[0];
|
|
980
998
|
driver.respondToPermissionRequest(state, msg.requestId, optionId);
|
|
981
999
|
return;
|
|
982
1000
|
}
|
package/package.json
CHANGED
package/providers/copilot.js
CHANGED
|
@@ -38,12 +38,7 @@ export async function start(opts) {
|
|
|
38
38
|
const conversationId = opts.conversationId;
|
|
39
39
|
// Tear down any prior entry so we don't leak children.
|
|
40
40
|
const prior = ctx.conversations.get(conversationId);
|
|
41
|
-
|
|
42
|
-
try { prior.copilotChild.kill('SIGTERM'); } catch { /* noop */ }
|
|
43
|
-
}
|
|
44
|
-
if (prior?.acpClient) {
|
|
45
|
-
try { prior.acpClient.close('replaced'); } catch { /* noop */ }
|
|
46
|
-
}
|
|
41
|
+
dispose(prior, 'replaced');
|
|
47
42
|
|
|
48
43
|
const providerOptions = opts.providerOptions || prior?.providerOptions || {};
|
|
49
44
|
const model = providerOptions.model || DEFAULT_COPILOT_MODEL;
|
|
@@ -116,31 +111,16 @@ async function _bootAcp(state, resumeSessionId, model) {
|
|
|
116
111
|
}
|
|
117
112
|
});
|
|
118
113
|
child.on('error', (err) => {
|
|
119
|
-
|
|
120
|
-
type: 'result',
|
|
121
|
-
subtype: 'error',
|
|
122
|
-
session_id: state.sessionId,
|
|
123
|
-
is_error: true,
|
|
124
|
-
error: `copilot process error: ${err?.message || err}`,
|
|
125
|
-
});
|
|
114
|
+
_sendTurnError(state, `copilot process error: ${err?.message || err}`);
|
|
126
115
|
});
|
|
127
116
|
child.on('close', (code) => {
|
|
128
117
|
if (state.turnActive) {
|
|
129
118
|
const tail = stderrBuf.trim().slice(-2000);
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
error: tail || `copilot exited mid-turn (code ${code})`,
|
|
136
|
-
});
|
|
137
|
-
ctx.sendToServer({
|
|
138
|
-
type: 'turn_completed',
|
|
139
|
-
conversationId: state.conversationId,
|
|
140
|
-
claudeSessionId: state.sessionId,
|
|
141
|
-
workDir: state.workDir,
|
|
142
|
-
});
|
|
143
|
-
state.turnActive = false;
|
|
119
|
+
_sendTurnError(state, tail || `copilot exited mid-turn (code ${code})`);
|
|
120
|
+
_completeTurn(state);
|
|
121
|
+
}
|
|
122
|
+
if (state.acpClient) {
|
|
123
|
+
try { state.acpClient.close(`copilot exited (code ${code})`); } catch { /* noop */ }
|
|
144
124
|
}
|
|
145
125
|
// Drain any in-flight permission prompts so the frontend dialog unwedges
|
|
146
126
|
// and the Promise GC roots release.
|
|
@@ -180,6 +160,7 @@ async function _bootAcp(state, resumeSessionId, model) {
|
|
|
180
160
|
state.claudeSessionId = resumeSessionId;
|
|
181
161
|
if (Array.isArray(r?.modes?.availableModes)) state.acpModes = r.modes.availableModes;
|
|
182
162
|
if (Array.isArray(r?.models?.availableModels)) cacheCopilotModelsFromAcp(r.models.availableModels);
|
|
163
|
+
_sendSessionIdUpdate(state);
|
|
183
164
|
} else {
|
|
184
165
|
if (resumeSessionId && !state.acpCapabilities.loadSession) {
|
|
185
166
|
// Surface the downgrade — silently handing back a fresh session would
|
|
@@ -198,6 +179,7 @@ async function _bootAcp(state, resumeSessionId, model) {
|
|
|
198
179
|
state.claudeSessionId = state.sessionId;
|
|
199
180
|
if (Array.isArray(r?.modes?.availableModes)) state.acpModes = r.modes.availableModes;
|
|
200
181
|
if (Array.isArray(r?.models?.availableModels)) cacheCopilotModelsFromAcp(r.models.availableModels);
|
|
182
|
+
_sendSessionIdUpdate(state);
|
|
201
183
|
}
|
|
202
184
|
|
|
203
185
|
// 3) Emit a system_init envelope so the UI populates tools / model panels.
|
|
@@ -224,14 +206,8 @@ export async function sendInput(state, prompt, opts = {}) {
|
|
|
224
206
|
try {
|
|
225
207
|
await _bootAcp(state, state.sessionId || null, state.model);
|
|
226
208
|
} catch (err) {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
subtype: 'error',
|
|
230
|
-
session_id: state.sessionId,
|
|
231
|
-
is_error: true,
|
|
232
|
-
error: `copilot ACP reinit failed: ${err?.message || err}`,
|
|
233
|
-
});
|
|
234
|
-
ctx.sendToServer({ type: 'turn_completed', conversationId, claudeSessionId: state.sessionId, workDir: state.workDir });
|
|
209
|
+
_sendTurnError(state, `copilot ACP reinit failed: ${err?.message || err}`);
|
|
210
|
+
_completeTurn(state, conversationId);
|
|
235
211
|
return;
|
|
236
212
|
}
|
|
237
213
|
}
|
|
@@ -247,6 +223,8 @@ export async function sendInput(state, prompt, opts = {}) {
|
|
|
247
223
|
const abortController = new AbortController();
|
|
248
224
|
state.abortController = abortController;
|
|
249
225
|
state.turnActive = true;
|
|
226
|
+
state.turnCompletedEmitted = false;
|
|
227
|
+
state.turnErrorEmitted = false;
|
|
250
228
|
state.turnResultReceived = false;
|
|
251
229
|
|
|
252
230
|
// Build prompt content blocks. ACP ContentBlock variants: text, image,
|
|
@@ -289,21 +267,9 @@ export async function sendInput(state, prompt, opts = {}) {
|
|
|
289
267
|
error: isErr ? `copilot stop_reason=${stopReason}` : undefined,
|
|
290
268
|
});
|
|
291
269
|
} catch (err) {
|
|
292
|
-
|
|
293
|
-
type: 'result',
|
|
294
|
-
subtype: 'error',
|
|
295
|
-
session_id: state.sessionId,
|
|
296
|
-
is_error: true,
|
|
297
|
-
error: err?.message || String(err),
|
|
298
|
-
});
|
|
270
|
+
_sendTurnError(state, err?.message || String(err));
|
|
299
271
|
} finally {
|
|
300
|
-
state
|
|
301
|
-
ctx.sendToServer({
|
|
302
|
-
type: 'turn_completed',
|
|
303
|
-
conversationId,
|
|
304
|
-
claudeSessionId: state.sessionId,
|
|
305
|
-
workDir: state.workDir,
|
|
306
|
-
});
|
|
272
|
+
_completeTurn(state, conversationId);
|
|
307
273
|
}
|
|
308
274
|
}
|
|
309
275
|
|
|
@@ -330,12 +296,44 @@ export function abort(state) {
|
|
|
330
296
|
}
|
|
331
297
|
}
|
|
332
298
|
|
|
299
|
+
export function dispose(state, reason = 'disposed') {
|
|
300
|
+
if (!state) return;
|
|
301
|
+
if (state.abortController) {
|
|
302
|
+
try { state.abortController.abort(); } catch { /* noop */ }
|
|
303
|
+
state.abortController = null;
|
|
304
|
+
}
|
|
305
|
+
if (state._abortKillTimer) {
|
|
306
|
+
clearTimeout(state._abortKillTimer);
|
|
307
|
+
state._abortKillTimer = null;
|
|
308
|
+
}
|
|
309
|
+
_drainPendingPermissions(state, reason);
|
|
310
|
+
if (state.acpClient) {
|
|
311
|
+
try { state.acpClient.close(reason); } catch { /* noop */ }
|
|
312
|
+
state.acpClient = null;
|
|
313
|
+
}
|
|
314
|
+
if (state.copilotChild) {
|
|
315
|
+
try { state.copilotChild.kill('SIGTERM'); } catch { /* noop */ }
|
|
316
|
+
state.copilotChild = null;
|
|
317
|
+
}
|
|
318
|
+
state.initialized = false;
|
|
319
|
+
state.turnActive = false;
|
|
320
|
+
}
|
|
321
|
+
|
|
333
322
|
/**
|
|
334
323
|
* /clear support: ask ACP for a brand-new session under the same
|
|
335
324
|
* conversationId. Keeps the child alive — no spawn cost.
|
|
336
325
|
*/
|
|
337
326
|
export async function clear(state) {
|
|
338
|
-
if (!state
|
|
327
|
+
if (!state) return;
|
|
328
|
+
if (!state.initialized || !state.acpClient) {
|
|
329
|
+
try {
|
|
330
|
+
await _bootAcp(state, null, state.model);
|
|
331
|
+
return;
|
|
332
|
+
} catch (err) {
|
|
333
|
+
_sendTurnError(state, `copilot ACP reinit failed during clear: ${err?.message || err}`);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
339
337
|
// A fresh session invalidates any in-flight permission prompts.
|
|
340
338
|
_drainPendingPermissions(state, 'session cleared');
|
|
341
339
|
try {
|
|
@@ -345,6 +343,7 @@ export async function clear(state) {
|
|
|
345
343
|
});
|
|
346
344
|
state.sessionId = r?.sessionId || randomUUID();
|
|
347
345
|
state.claudeSessionId = state.sessionId;
|
|
346
|
+
_sendSessionIdUpdate(state);
|
|
348
347
|
sendOutput(state.conversationId, {
|
|
349
348
|
type: 'system',
|
|
350
349
|
subtype: 'init',
|
|
@@ -355,6 +354,7 @@ export async function clear(state) {
|
|
|
355
354
|
permissionMode: state.allowAllTools ? 'bypassPermissions' : 'default',
|
|
356
355
|
});
|
|
357
356
|
} catch (err) {
|
|
357
|
+
_sendTurnError(state, `copilot clear failed: ${err?.message || err}`);
|
|
358
358
|
if (ctx?.CONFIG?.debug) console.warn('[copilot] clear failed:', err?.message || err);
|
|
359
359
|
}
|
|
360
360
|
}
|
|
@@ -365,6 +365,40 @@ function sendOutput(conversationId, data) {
|
|
|
365
365
|
ctx.sendToServer({ type: 'claude_output', conversationId, data });
|
|
366
366
|
}
|
|
367
367
|
|
|
368
|
+
function _sendSessionIdUpdate(state) {
|
|
369
|
+
if (!state?.conversationId || !state.sessionId) return;
|
|
370
|
+
ctx.sendToServer({
|
|
371
|
+
type: 'session_id_update',
|
|
372
|
+
conversationId: state.conversationId,
|
|
373
|
+
claudeSessionId: state.sessionId,
|
|
374
|
+
workDir: state.workDir,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function _sendTurnError(state, error) {
|
|
379
|
+
if (!state || state.turnErrorEmitted) return;
|
|
380
|
+
state.turnErrorEmitted = true;
|
|
381
|
+
sendOutput(state.conversationId, {
|
|
382
|
+
type: 'result',
|
|
383
|
+
subtype: 'error',
|
|
384
|
+
session_id: state.sessionId,
|
|
385
|
+
is_error: true,
|
|
386
|
+
error,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function _completeTurn(state, conversationId = state?.conversationId) {
|
|
391
|
+
if (!state || state.turnCompletedEmitted) return;
|
|
392
|
+
state.turnCompletedEmitted = true;
|
|
393
|
+
state.turnActive = false;
|
|
394
|
+
ctx.sendToServer({
|
|
395
|
+
type: 'turn_completed',
|
|
396
|
+
conversationId,
|
|
397
|
+
claudeSessionId: state.sessionId,
|
|
398
|
+
workDir: state.workDir,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
368
402
|
function _handleAcpNotification(state, method, params) {
|
|
369
403
|
if (method === 'session/update') {
|
|
370
404
|
_handleSessionUpdate(state, params);
|
|
@@ -474,19 +508,36 @@ async function _handlePermissionRequest(state, params) {
|
|
|
474
508
|
const allow = opt.find(o => o.kind === 'allow_always' || o.kind === 'allow_once') || opt[0];
|
|
475
509
|
return { outcome: { outcome: 'selected', optionId: allow.optionId } };
|
|
476
510
|
}
|
|
477
|
-
// Otherwise route through the existing
|
|
478
|
-
//
|
|
479
|
-
//
|
|
480
|
-
// driver via `respondToPermissionRequest(state, requestId, optionId)`.
|
|
511
|
+
// Otherwise route through the existing AskUserQuestion UI. Emit the same
|
|
512
|
+
// Claude-style tool_use first, then link it with ask_user_question so the
|
|
513
|
+
// regular card renders and answer routing can stay provider-agnostic.
|
|
481
514
|
const requestId = `copilot-perm-${randomUUID()}`;
|
|
515
|
+
const question = _formatPermissionPrompt(params);
|
|
516
|
+
const questions = [{
|
|
517
|
+
header: 'Copilot permission',
|
|
518
|
+
question,
|
|
519
|
+
options: opt.map(o => ({ label: o.name || o.optionId })),
|
|
520
|
+
multiSelect: false,
|
|
521
|
+
}];
|
|
522
|
+
sendOutput(state.conversationId, {
|
|
523
|
+
type: 'assistant',
|
|
524
|
+
message: {
|
|
525
|
+
role: 'assistant',
|
|
526
|
+
content: [{
|
|
527
|
+
type: 'tool_use',
|
|
528
|
+
id: requestId,
|
|
529
|
+
name: 'AskUserQuestion',
|
|
530
|
+
input: { questions },
|
|
531
|
+
}],
|
|
532
|
+
},
|
|
533
|
+
});
|
|
482
534
|
return new Promise((resolve) => {
|
|
483
535
|
state.pendingPermissions.set(requestId, { resolve, options: opt });
|
|
484
536
|
ctx.sendToServer({
|
|
485
537
|
type: 'ask_user_question',
|
|
486
538
|
conversationId: state.conversationId,
|
|
487
539
|
requestId,
|
|
488
|
-
|
|
489
|
-
options: opt.map(o => ({ id: o.optionId, label: o.name || o.optionId, kind: o.kind })),
|
|
540
|
+
questions,
|
|
490
541
|
});
|
|
491
542
|
});
|
|
492
543
|
}
|
|
@@ -518,7 +569,7 @@ export function respondToPermissionRequest(state, requestId, optionId) {
|
|
|
518
569
|
return false;
|
|
519
570
|
}
|
|
520
571
|
state.pendingPermissions.delete(requestId);
|
|
521
|
-
const opt = slot.options.find(o => o.optionId === optionId) || slot.options[0];
|
|
572
|
+
const opt = slot.options.find(o => o.optionId === optionId || o.name === optionId) || slot.options[0];
|
|
522
573
|
slot.resolve({ outcome: { outcome: 'selected', optionId: opt?.optionId || optionId } });
|
|
523
574
|
return true;
|
|
524
575
|
}
|