@agent-link/agent 0.1.333 → 0.1.334
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/dist/backends/types.d.ts +11 -0
- package/dist/backends/types.js +10 -3
- package/dist/backends/types.js.map +1 -1
- package/dist/connection.js +91 -10
- package/dist/connection.js.map +1 -1
- package/dist/copilot-command-bridge.d.ts +102 -0
- package/dist/copilot-command-bridge.js +654 -0
- package/dist/copilot-command-bridge.js.map +1 -0
- package/dist/copilot.d.ts +45 -1
- package/dist/copilot.js +733 -14
- package/dist/copilot.js.map +1 -1
- package/dist/provider-command-discovery.d.ts +5 -0
- package/dist/provider-command-discovery.js +41 -1
- package/dist/provider-command-discovery.js.map +1 -1
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +1 -1
- package/dist/runtime.js.map +1 -1
- package/package.json +3 -1
package/dist/copilot.js
CHANGED
|
@@ -14,12 +14,14 @@ import { readdirSync, readFileSync, writeFileSync, existsSync, rmSync, mkdirSync
|
|
|
14
14
|
import { basename, dirname, extname, join, resolve } from 'path';
|
|
15
15
|
import { homedir, platform } from 'os';
|
|
16
16
|
import { randomUUID } from 'crypto';
|
|
17
|
+
import * as acp from '@agentclientprotocol/sdk';
|
|
17
18
|
import { processUserMessage } from './history-message-transform.js';
|
|
18
19
|
import { copilotCapabilities } from './backends/types.js';
|
|
19
20
|
import { CONFIG_DIR } from './config.js';
|
|
20
21
|
import { getCleanEnv, resolveExecutablePath } from './sdk.js';
|
|
21
22
|
import { getProvider, resolveProviderCommand } from './providers.js';
|
|
22
23
|
import { FileCache } from './file-cache.js';
|
|
24
|
+
import { CopilotCommandRuntime, } from './copilot-command-bridge.js';
|
|
23
25
|
// ── Module state ───────────────────────────────────────────────────────────
|
|
24
26
|
const conversations = new Map();
|
|
25
27
|
const sendFns = new Set();
|
|
@@ -27,6 +29,24 @@ let globalModel = null;
|
|
|
27
29
|
const MODEL_PATTERN = /^[a-zA-Z0-9._-]+$/;
|
|
28
30
|
const CONTEXT_TIERS = new Set(['default', 'long_context']);
|
|
29
31
|
const REASONING_EFFORTS = new Set(['none', 'low', 'medium', 'high', 'xhigh', 'max']);
|
|
32
|
+
const COPILOT_PLAN_INPUT_TIMEOUT_MS = 10 * 60_000;
|
|
33
|
+
const COPILOT_CONTINUE_PLAN_ANSWER = 'Continue developing the plan. In your final response, ask me to confirm the plan or describe any changes.';
|
|
34
|
+
const pendingCopilotDecisions = new Map();
|
|
35
|
+
class CopilotDecisionCancelledError extends Error {
|
|
36
|
+
constructor() {
|
|
37
|
+
super('Copilot decision request was cancelled');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function setCopilotModeState(state, mode) {
|
|
41
|
+
state.copilotMode = mode;
|
|
42
|
+
state.planMode = mode === 'plan';
|
|
43
|
+
sendFn({
|
|
44
|
+
type: 'plan_mode_changed',
|
|
45
|
+
enabled: state.planMode,
|
|
46
|
+
mode,
|
|
47
|
+
conversationId: state.conversationId,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
30
50
|
const COPILOT_NATIVE_ATTACHMENT_MIME_TYPES = new Set([
|
|
31
51
|
'application/pdf',
|
|
32
52
|
'image/gif',
|
|
@@ -60,6 +80,107 @@ export function addCopilotSendFn(fn) {
|
|
|
60
80
|
sendFns.add(fn);
|
|
61
81
|
return () => { sendFns.delete(fn); };
|
|
62
82
|
}
|
|
83
|
+
function emitCopilotDecisionRequest(pending) {
|
|
84
|
+
sendFn({
|
|
85
|
+
type: 'backend_event',
|
|
86
|
+
conversationId: pending.conversationId,
|
|
87
|
+
providerId: pending.providerId,
|
|
88
|
+
claudeSessionId: pending.sessionId,
|
|
89
|
+
event: {
|
|
90
|
+
type: 'user_input_requested',
|
|
91
|
+
session: sessionRef(pending.sessionId),
|
|
92
|
+
requestId: pending.requestId,
|
|
93
|
+
request: pending.request,
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
function settleCopilotDecision(pending, answer) {
|
|
98
|
+
if (pendingCopilotDecisions.get(pending.requestId) !== pending)
|
|
99
|
+
return false;
|
|
100
|
+
pendingCopilotDecisions.delete(pending.requestId);
|
|
101
|
+
if (pending.timer)
|
|
102
|
+
clearTimeout(pending.timer);
|
|
103
|
+
pending.timer = null;
|
|
104
|
+
sendFn({
|
|
105
|
+
type: 'backend_event',
|
|
106
|
+
conversationId: pending.conversationId,
|
|
107
|
+
providerId: pending.providerId,
|
|
108
|
+
claudeSessionId: pending.sessionId,
|
|
109
|
+
event: {
|
|
110
|
+
type: 'user_input_resolved',
|
|
111
|
+
session: sessionRef(pending.sessionId),
|
|
112
|
+
requestId: pending.requestId,
|
|
113
|
+
resolution: answer.action === 'cancel' ? 'cancelled' : 'answered',
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
pending.resolve(answer);
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
function waitForCopilotDecision(state, sessionId, request, choices = []) {
|
|
120
|
+
const requestId = randomUUID();
|
|
121
|
+
return new Promise(resolvePromise => {
|
|
122
|
+
const pending = {
|
|
123
|
+
requestId,
|
|
124
|
+
conversationId: state.conversationId,
|
|
125
|
+
providerId: state.providerId,
|
|
126
|
+
sessionId,
|
|
127
|
+
request,
|
|
128
|
+
choices,
|
|
129
|
+
resolve: resolvePromise,
|
|
130
|
+
timer: null,
|
|
131
|
+
};
|
|
132
|
+
pendingCopilotDecisions.set(requestId, pending);
|
|
133
|
+
if (request.expiresAt) {
|
|
134
|
+
pending.timer = setTimeout(() => {
|
|
135
|
+
if (choices.length > 0) {
|
|
136
|
+
settleCopilotDecision(pending, { action: 'answer', answer: choices[0] });
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
settleCopilotDecision(pending, {
|
|
140
|
+
action: 'answer',
|
|
141
|
+
answer: COPILOT_CONTINUE_PLAN_ANSWER,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}, Math.max(0, request.expiresAt - Date.now()));
|
|
145
|
+
}
|
|
146
|
+
emitCopilotDecisionRequest(pending);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
function cancelPendingCopilotDecisions(conversationId) {
|
|
150
|
+
for (const pending of [...pendingCopilotDecisions.values()]) {
|
|
151
|
+
if (pending.conversationId === conversationId) {
|
|
152
|
+
settleCopilotDecision(pending, { action: 'cancel' });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
export function getPendingCopilotDecisions(conversationId) {
|
|
157
|
+
return [...pendingCopilotDecisions.values()]
|
|
158
|
+
.filter(pending => pending.conversationId === conversationId)
|
|
159
|
+
.map(pending => ({
|
|
160
|
+
requestId: pending.requestId,
|
|
161
|
+
conversationId: pending.conversationId,
|
|
162
|
+
providerId: pending.providerId,
|
|
163
|
+
sessionId: pending.sessionId,
|
|
164
|
+
request: pending.request,
|
|
165
|
+
}));
|
|
166
|
+
}
|
|
167
|
+
export function handleCopilotUserAnswer(requestId, answers, scope = {}) {
|
|
168
|
+
const pending = pendingCopilotDecisions.get(requestId);
|
|
169
|
+
if (!pending)
|
|
170
|
+
return false;
|
|
171
|
+
if (scope.conversationId && pending.conversationId !== scope.conversationId)
|
|
172
|
+
return false;
|
|
173
|
+
if (scope.providerId && pending.providerId !== scope.providerId)
|
|
174
|
+
return false;
|
|
175
|
+
const action = typeof answers.__action === 'string' ? answers.__action : undefined;
|
|
176
|
+
const feedback = typeof answers.__feedback === 'string' ? answers.__feedback.trim() : undefined;
|
|
177
|
+
const firstAnswer = Object.entries(answers)
|
|
178
|
+
.find(([key, value]) => !key.startsWith('__') && typeof value === 'string')?.[1];
|
|
179
|
+
const answer = action || (typeof firstAnswer === 'string' ? firstAnswer : '');
|
|
180
|
+
if (!answer)
|
|
181
|
+
return false;
|
|
182
|
+
return settleCopilotDecision(pending, { action: 'answer', answer, feedback });
|
|
183
|
+
}
|
|
63
184
|
function resolveDirectExecution(cmdPath) {
|
|
64
185
|
if (platform() !== 'win32')
|
|
65
186
|
return null;
|
|
@@ -133,10 +254,18 @@ function finishCopilotTurn(state) {
|
|
|
133
254
|
state.heartbeatTimer = null;
|
|
134
255
|
}
|
|
135
256
|
}
|
|
257
|
+
function ensureCopilotCommandTurnActive(state, turnId) {
|
|
258
|
+
if (!state.turnActive || state.turnId !== turnId) {
|
|
259
|
+
throw new CopilotDecisionCancelledError();
|
|
260
|
+
}
|
|
261
|
+
}
|
|
136
262
|
function emitCopilotCancelled(conversationId, state) {
|
|
137
263
|
sendFn({
|
|
138
264
|
type: 'backend_event',
|
|
139
265
|
conversationId,
|
|
266
|
+
claudeSessionId: state?.copilotSessionId || null,
|
|
267
|
+
turnId: state?.turnId || null,
|
|
268
|
+
stateVersion: state?.stateVersion || 0,
|
|
140
269
|
event: { type: 'turn_cancelled', session: sessionRef(state?.copilotSessionId || '') },
|
|
141
270
|
});
|
|
142
271
|
sendFn({
|
|
@@ -205,6 +334,10 @@ export function rebindCopilotConversation(copilotSessionId, newConvId) {
|
|
|
205
334
|
conversations.delete(oldConvId);
|
|
206
335
|
state.conversationId = newConvId;
|
|
207
336
|
conversations.set(newConvId, state);
|
|
337
|
+
for (const pending of pendingCopilotDecisions.values()) {
|
|
338
|
+
if (pending.conversationId === oldConvId)
|
|
339
|
+
pending.conversationId = newConvId;
|
|
340
|
+
}
|
|
208
341
|
return true;
|
|
209
342
|
}
|
|
210
343
|
}
|
|
@@ -229,6 +362,13 @@ export function createCopilotPlaceholder(conversationId, opts) {
|
|
|
229
362
|
stateVersion: 0,
|
|
230
363
|
heartbeatTimer: null,
|
|
231
364
|
sessionNotified: false,
|
|
365
|
+
copilotMode: 'interactive',
|
|
366
|
+
planMode: false,
|
|
367
|
+
planModePending: false,
|
|
368
|
+
isCompacting: false,
|
|
369
|
+
commandRuntime: null,
|
|
370
|
+
commandSessionId: null,
|
|
371
|
+
deferredCommandUsage: null,
|
|
232
372
|
});
|
|
233
373
|
}
|
|
234
374
|
export function restartCopilotConversation(conversationId, opts) {
|
|
@@ -243,7 +383,7 @@ export function restartCopilotConversation(conversationId, opts) {
|
|
|
243
383
|
child: null,
|
|
244
384
|
abortController: null,
|
|
245
385
|
copilotSessionId: null,
|
|
246
|
-
lastCopilotSessionId: copilotSessionId,
|
|
386
|
+
lastCopilotSessionId: opts?.preserveSession === false ? null : copilotSessionId,
|
|
247
387
|
workDir: state?.workDir || process.cwd(),
|
|
248
388
|
turnActive: false,
|
|
249
389
|
providerId: opts?.providerId || state?.providerId || 'copilot',
|
|
@@ -254,6 +394,13 @@ export function restartCopilotConversation(conversationId, opts) {
|
|
|
254
394
|
stateVersion: (state?.stateVersion || 0) + 1,
|
|
255
395
|
heartbeatTimer: null,
|
|
256
396
|
sessionNotified: false,
|
|
397
|
+
copilotMode: state?.copilotMode || (state?.planMode ? 'plan' : 'interactive'),
|
|
398
|
+
planMode: state?.planMode || false,
|
|
399
|
+
planModePending: state?.planModePending || false,
|
|
400
|
+
isCompacting: false,
|
|
401
|
+
commandRuntime: null,
|
|
402
|
+
commandSessionId: null,
|
|
403
|
+
deferredCommandUsage: null,
|
|
257
404
|
});
|
|
258
405
|
return { wasTurnActive, copilotSessionId };
|
|
259
406
|
}
|
|
@@ -267,6 +414,459 @@ export function setModel(conversationId, model) {
|
|
|
267
414
|
globalModel = model;
|
|
268
415
|
}
|
|
269
416
|
}
|
|
417
|
+
function detachCopilotProviderSession(state, providerId) {
|
|
418
|
+
if (state.providerId === providerId)
|
|
419
|
+
return;
|
|
420
|
+
state.providerId = providerId;
|
|
421
|
+
state.copilotSessionId = null;
|
|
422
|
+
state.lastCopilotSessionId = null;
|
|
423
|
+
state.sessionNotified = false;
|
|
424
|
+
}
|
|
425
|
+
function isMissingCopilotSessionError(error) {
|
|
426
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
427
|
+
return /(?:session|resource).*not found|not found.*(?:session|resource)|no (?:session|task|name).*matched/i.test(message);
|
|
428
|
+
}
|
|
429
|
+
function emitCopilotCompactionStarted(state) {
|
|
430
|
+
if (state.isCompacting)
|
|
431
|
+
return false;
|
|
432
|
+
state.isCompacting = true;
|
|
433
|
+
const session = sessionRef(state.copilotSessionId || state.commandSessionId || 'pending');
|
|
434
|
+
sendFn({
|
|
435
|
+
type: 'context_compaction',
|
|
436
|
+
conversationId: state.conversationId,
|
|
437
|
+
turnId: state.turnId,
|
|
438
|
+
status: 'started',
|
|
439
|
+
});
|
|
440
|
+
sendFn({
|
|
441
|
+
type: 'backend_event',
|
|
442
|
+
conversationId: state.conversationId,
|
|
443
|
+
turnId: state.turnId,
|
|
444
|
+
event: { type: 'compact_started', session },
|
|
445
|
+
});
|
|
446
|
+
return true;
|
|
447
|
+
}
|
|
448
|
+
function emitCopilotCompactionCompleted(state) {
|
|
449
|
+
if (!state.isCompacting)
|
|
450
|
+
return false;
|
|
451
|
+
state.isCompacting = false;
|
|
452
|
+
const session = sessionRef(state.copilotSessionId || state.commandSessionId || '');
|
|
453
|
+
sendFn({
|
|
454
|
+
type: 'context_compaction',
|
|
455
|
+
conversationId: state.conversationId,
|
|
456
|
+
turnId: state.turnId,
|
|
457
|
+
status: 'completed',
|
|
458
|
+
});
|
|
459
|
+
sendFn({
|
|
460
|
+
type: 'backend_event',
|
|
461
|
+
conversationId: state.conversationId,
|
|
462
|
+
turnId: state.turnId,
|
|
463
|
+
event: { type: 'compact_completed', session },
|
|
464
|
+
});
|
|
465
|
+
return true;
|
|
466
|
+
}
|
|
467
|
+
function emitCopilotCommandOutput(state, content) {
|
|
468
|
+
if (!content.trim())
|
|
469
|
+
return;
|
|
470
|
+
sendFn({
|
|
471
|
+
type: 'command_output',
|
|
472
|
+
conversationId: state.conversationId,
|
|
473
|
+
content,
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
function emitCopilotCommandCompleted(state) {
|
|
477
|
+
if (!state.turnActive)
|
|
478
|
+
return;
|
|
479
|
+
finishCopilotTurn(state);
|
|
480
|
+
const session = sessionRef(state.copilotSessionId || '');
|
|
481
|
+
const usage = state.deferredCommandUsage || undefined;
|
|
482
|
+
state.deferredCommandUsage = null;
|
|
483
|
+
sendFn({
|
|
484
|
+
type: 'backend_event',
|
|
485
|
+
conversationId: state.conversationId,
|
|
486
|
+
event: { type: 'turn_completed', session, ...(usage ? { usage } : {}) },
|
|
487
|
+
});
|
|
488
|
+
sendFn({
|
|
489
|
+
type: 'turn_completed',
|
|
490
|
+
conversationId: state.conversationId,
|
|
491
|
+
workDir: state.workDir,
|
|
492
|
+
claudeSessionId: state.copilotSessionId,
|
|
493
|
+
turnId: state.turnId,
|
|
494
|
+
stateVersion: state.stateVersion,
|
|
495
|
+
...(usage ? { usage } : {}),
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
function commandInputForDisplay(command, input) {
|
|
499
|
+
return input.trim() ? `/${command} ${input}` : `/${command}`;
|
|
500
|
+
}
|
|
501
|
+
async function applyCopilotCommandResult(state, runtime, sessionId, command, input, result) {
|
|
502
|
+
switch (result.kind) {
|
|
503
|
+
case 'text':
|
|
504
|
+
emitCopilotCommandOutput(state, result.text);
|
|
505
|
+
return;
|
|
506
|
+
case 'completed':
|
|
507
|
+
if (result.message)
|
|
508
|
+
emitCopilotCommandOutput(state, result.message);
|
|
509
|
+
return;
|
|
510
|
+
case 'select-subcommand':
|
|
511
|
+
throw new Error(`${result.title}: ${result.options.map(option => option.name).join(', ')}`);
|
|
512
|
+
case 'agent-prompt':
|
|
513
|
+
if (result.notice)
|
|
514
|
+
emitCopilotCommandOutput(state, result.notice);
|
|
515
|
+
if (result.mode) {
|
|
516
|
+
await runtime.setMode(sessionId, result.mode);
|
|
517
|
+
setCopilotModeState(state, result.mode);
|
|
518
|
+
}
|
|
519
|
+
await runtime.send(sessionId, result.prompt, result.displayPrompt || commandInputForDisplay(command, input));
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
async function requestCopilotPlanInput(state, sessionId, request) {
|
|
523
|
+
const choices = request.choices || [];
|
|
524
|
+
const expiresAt = Date.now() + COPILOT_PLAN_INPUT_TIMEOUT_MS;
|
|
525
|
+
const decision = await waitForCopilotDecision(state, sessionId, {
|
|
526
|
+
questions: [{
|
|
527
|
+
question: request.question,
|
|
528
|
+
header: 'Plan question',
|
|
529
|
+
options: choices.map(choice => ({ label: choice, value: choice })),
|
|
530
|
+
}],
|
|
531
|
+
source: 'tool_request_user_input',
|
|
532
|
+
presentation: 'inline',
|
|
533
|
+
kind: 'plan_input',
|
|
534
|
+
allowFreeform: request.allowFreeform !== false,
|
|
535
|
+
expiresAt,
|
|
536
|
+
timeoutAction: choices.length > 0 ? 'first_option' : 'continue_plan',
|
|
537
|
+
}, choices);
|
|
538
|
+
if (decision.action === 'cancel')
|
|
539
|
+
throw new CopilotDecisionCancelledError();
|
|
540
|
+
const answer = decision.answer || '';
|
|
541
|
+
return {
|
|
542
|
+
answer,
|
|
543
|
+
wasFreeform: !choices.includes(answer),
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
async function requestCopilotAcpElicitation(state, request) {
|
|
547
|
+
if (!acp.CreateElicitationRequest.isForm(request) || !('sessionId' in request)) {
|
|
548
|
+
return { action: 'cancel' };
|
|
549
|
+
}
|
|
550
|
+
const properties = Object.entries(request.requestedSchema.properties || {});
|
|
551
|
+
const [fieldName, field] = properties[0] || [];
|
|
552
|
+
if (!fieldName || !field)
|
|
553
|
+
return { action: 'decline' };
|
|
554
|
+
let choices = [];
|
|
555
|
+
let multiSelect = false;
|
|
556
|
+
if (acp.ElicitationPropertySchema.isString(field)) {
|
|
557
|
+
choices = field.oneOf?.map(option => option.const) || field.enum || [];
|
|
558
|
+
}
|
|
559
|
+
else if (acp.ElicitationPropertySchema.isArray(field)) {
|
|
560
|
+
multiSelect = true;
|
|
561
|
+
choices = acp.MultiSelectItems.isTitled(field.items)
|
|
562
|
+
? field.items.anyOf.map(option => option.const)
|
|
563
|
+
: (acp.MultiSelectItems.isString(field.items) ? field.items.enum : []);
|
|
564
|
+
}
|
|
565
|
+
else if (acp.ElicitationPropertySchema.isBoolean(field)) {
|
|
566
|
+
choices = ['true', 'false'];
|
|
567
|
+
}
|
|
568
|
+
const fieldRecord = field;
|
|
569
|
+
const fieldDescription = typeof fieldRecord.description === 'string'
|
|
570
|
+
? fieldRecord.description
|
|
571
|
+
: request.message;
|
|
572
|
+
const fieldTitle = typeof fieldRecord.title === 'string'
|
|
573
|
+
? fieldRecord.title
|
|
574
|
+
: 'Plan question';
|
|
575
|
+
const expiresAt = Date.now() + COPILOT_PLAN_INPUT_TIMEOUT_MS;
|
|
576
|
+
const decision = await waitForCopilotDecision(state, request.sessionId, {
|
|
577
|
+
questions: [{
|
|
578
|
+
question: fieldDescription,
|
|
579
|
+
header: fieldTitle,
|
|
580
|
+
options: choices.map(choice => ({ label: choice, value: choice })),
|
|
581
|
+
multiSelect,
|
|
582
|
+
}],
|
|
583
|
+
source: 'tool_request_user_input',
|
|
584
|
+
presentation: 'inline',
|
|
585
|
+
kind: 'plan_input',
|
|
586
|
+
allowFreeform: choices.length === 0,
|
|
587
|
+
expiresAt,
|
|
588
|
+
timeoutAction: choices.length > 0 ? 'first_option' : 'continue_plan',
|
|
589
|
+
}, choices);
|
|
590
|
+
if (decision.action === 'cancel')
|
|
591
|
+
return { action: 'cancel' };
|
|
592
|
+
const raw = decision.answer || '';
|
|
593
|
+
let value = raw;
|
|
594
|
+
if (acp.ElicitationPropertySchema.isArray(field))
|
|
595
|
+
value = raw ? raw.split(/,\s*/) : [];
|
|
596
|
+
else if (acp.ElicitationPropertySchema.isBoolean(field))
|
|
597
|
+
value = raw === 'true';
|
|
598
|
+
else if (acp.ElicitationPropertySchema.isNumber(field)
|
|
599
|
+
|| acp.ElicitationPropertySchema.isInteger(field))
|
|
600
|
+
value = Number(raw);
|
|
601
|
+
return { action: 'accept', content: { [fieldName]: value } };
|
|
602
|
+
}
|
|
603
|
+
function copilotPlanConfirmationPrompt(content) {
|
|
604
|
+
return /[\u3400-\u9fff]/.test(content)
|
|
605
|
+
? '请确认以上计划,或直接回复需要修改的内容。'
|
|
606
|
+
: 'Please confirm the Plan above, or reply with any changes you want.';
|
|
607
|
+
}
|
|
608
|
+
function emitCopilotPlanReview(state, sessionId, content, languageHint = content || '') {
|
|
609
|
+
const plan = content?.trim() || '';
|
|
610
|
+
const prompt = copilotPlanConfirmationPrompt(languageHint);
|
|
611
|
+
sendFn({
|
|
612
|
+
type: 'backend_event',
|
|
613
|
+
conversationId: state.conversationId,
|
|
614
|
+
providerId: state.providerId,
|
|
615
|
+
claudeSessionId: sessionId,
|
|
616
|
+
event: {
|
|
617
|
+
type: 'text_delta',
|
|
618
|
+
session: sessionRef(sessionId),
|
|
619
|
+
text: `${plan ? `\n\n${plan}\n\n` : '\n\n'}> ${prompt}`,
|
|
620
|
+
},
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
async function acceptCopilotPlanExitWithoutBlocking(state, request) {
|
|
624
|
+
const selectedAction = request.actions.includes('exit_only')
|
|
625
|
+
? 'exit_only'
|
|
626
|
+
: request.actions.includes('interactive')
|
|
627
|
+
? 'interactive'
|
|
628
|
+
: null;
|
|
629
|
+
if (!selectedAction) {
|
|
630
|
+
return {
|
|
631
|
+
approved: false,
|
|
632
|
+
feedback: COPILOT_CONTINUE_PLAN_ANSWER,
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
state.planModePending = false;
|
|
636
|
+
return { approved: true, selectedAction };
|
|
637
|
+
}
|
|
638
|
+
export async function executeCopilotCommand(conversationId, command, input, workDir, options) {
|
|
639
|
+
let state = conversations.get(conversationId);
|
|
640
|
+
if (!state) {
|
|
641
|
+
createCopilotPlaceholder(conversationId, { providerId: options?.providerId });
|
|
642
|
+
state = conversations.get(conversationId);
|
|
643
|
+
}
|
|
644
|
+
if (state.turnActive || state.commandRuntime) {
|
|
645
|
+
sendFn({
|
|
646
|
+
type: 'backend_event',
|
|
647
|
+
conversationId,
|
|
648
|
+
event: { type: 'error', message: 'Copilot is already processing this conversation' },
|
|
649
|
+
});
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
state.workDir = workDir;
|
|
653
|
+
const providerChanged = !!options?.providerId && state.providerId !== options.providerId;
|
|
654
|
+
if (options?.providerId)
|
|
655
|
+
detachCopilotProviderSession(state, options.providerId);
|
|
656
|
+
const resumeSessionId = providerChanged
|
|
657
|
+
? undefined
|
|
658
|
+
: options?.resumeSessionId
|
|
659
|
+
|| state.copilotSessionId
|
|
660
|
+
|| state.lastCopilotSessionId
|
|
661
|
+
|| undefined;
|
|
662
|
+
const normalized = normalizeCopilotRunOptions(options?.copilotOptions);
|
|
663
|
+
if (normalized.error) {
|
|
664
|
+
sendCopilotSetupError(state, normalized.error);
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
if (normalized.options.model)
|
|
668
|
+
state.model = normalized.options.model;
|
|
669
|
+
if (normalized.options.context)
|
|
670
|
+
state.contextTier = normalized.options.context;
|
|
671
|
+
if (normalized.options.effort)
|
|
672
|
+
state.reasoningEffort = normalized.options.effort;
|
|
673
|
+
state.deferredCommandUsage = null;
|
|
674
|
+
let pendingPlanReview = null;
|
|
675
|
+
const runtime = new CopilotCommandRuntime({
|
|
676
|
+
providerId: state.providerId,
|
|
677
|
+
workDir,
|
|
678
|
+
onSessionEvent: (eventSessionId, event) => {
|
|
679
|
+
if (eventSessionId === state.commandSessionId)
|
|
680
|
+
handleCopilotEvent(state, event);
|
|
681
|
+
},
|
|
682
|
+
onUserInputRequest: command === 'plan'
|
|
683
|
+
? (eventSessionId, request) => requestCopilotPlanInput(state, eventSessionId, request)
|
|
684
|
+
: undefined,
|
|
685
|
+
onExitPlanModeRequest: command === 'plan'
|
|
686
|
+
? (eventSessionId, request) => {
|
|
687
|
+
pendingPlanReview = {
|
|
688
|
+
sessionId: eventSessionId,
|
|
689
|
+
content: request.planContent || request.summary,
|
|
690
|
+
languageHint: `${request.summary}\n${request.planContent || ''}`,
|
|
691
|
+
};
|
|
692
|
+
return acceptCopilotPlanExitWithoutBlocking(state, request);
|
|
693
|
+
}
|
|
694
|
+
: undefined,
|
|
695
|
+
onAcpElicitationRequest: command === 'plan'
|
|
696
|
+
? request => requestCopilotAcpElicitation(state, request)
|
|
697
|
+
: undefined,
|
|
698
|
+
});
|
|
699
|
+
state.commandRuntime = runtime;
|
|
700
|
+
const previousMode = state.copilotMode;
|
|
701
|
+
startCopilotTurn(state);
|
|
702
|
+
const commandTurnId = state.turnId;
|
|
703
|
+
sendFn({
|
|
704
|
+
type: 'backend_event',
|
|
705
|
+
conversationId,
|
|
706
|
+
event: {
|
|
707
|
+
type: 'turn_started',
|
|
708
|
+
session: sessionRef(resumeSessionId || 'pending'),
|
|
709
|
+
},
|
|
710
|
+
});
|
|
711
|
+
if (command === 'compact')
|
|
712
|
+
emitCopilotCompactionStarted(state);
|
|
713
|
+
let sessionId = null;
|
|
714
|
+
try {
|
|
715
|
+
await runtime.start();
|
|
716
|
+
ensureCopilotCommandTurnActive(state, commandTurnId);
|
|
717
|
+
const sessionOptions = {
|
|
718
|
+
model: state.model || globalModel || undefined,
|
|
719
|
+
contextTier: state.contextTier || undefined,
|
|
720
|
+
reasoningEffort: state.reasoningEffort || undefined,
|
|
721
|
+
};
|
|
722
|
+
try {
|
|
723
|
+
sessionId = await runtime.openSession(resumeSessionId, sessionOptions);
|
|
724
|
+
ensureCopilotCommandTurnActive(state, commandTurnId);
|
|
725
|
+
}
|
|
726
|
+
catch (error) {
|
|
727
|
+
if (!resumeSessionId || !isMissingCopilotSessionError(error))
|
|
728
|
+
throw error;
|
|
729
|
+
state.copilotSessionId = null;
|
|
730
|
+
state.lastCopilotSessionId = null;
|
|
731
|
+
state.sessionNotified = false;
|
|
732
|
+
sessionId = await runtime.openSession(undefined, sessionOptions);
|
|
733
|
+
ensureCopilotCommandTurnActive(state, commandTurnId);
|
|
734
|
+
}
|
|
735
|
+
state.commandSessionId = sessionId;
|
|
736
|
+
state.copilotSessionId = sessionId;
|
|
737
|
+
state.lastCopilotSessionId = sessionId;
|
|
738
|
+
await runtime.enableAllowAll(sessionId);
|
|
739
|
+
ensureCopilotCommandTurnActive(state, commandTurnId);
|
|
740
|
+
if (!state.sessionNotified) {
|
|
741
|
+
state.sessionNotified = true;
|
|
742
|
+
sendFn({
|
|
743
|
+
type: 'backend_event',
|
|
744
|
+
conversationId,
|
|
745
|
+
event: { type: 'session_started', session: sessionRef(sessionId) },
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
const commands = await runtime.listSessionCommands(sessionId);
|
|
749
|
+
ensureCopilotCommandTurnActive(state, commandTurnId);
|
|
750
|
+
if (!commands.some(value => value.name === command)) {
|
|
751
|
+
throw new Error(`Copilot provider ${state.providerId} does not expose /${command}`);
|
|
752
|
+
}
|
|
753
|
+
if (command === 'plan')
|
|
754
|
+
state.planModePending = true;
|
|
755
|
+
if (command === 'goal')
|
|
756
|
+
state.planModePending = false;
|
|
757
|
+
const result = await runtime.invokeCommand(sessionId, command, input);
|
|
758
|
+
ensureCopilotCommandTurnActive(state, commandTurnId);
|
|
759
|
+
await applyCopilotCommandResult(state, runtime, sessionId, command, input, result);
|
|
760
|
+
ensureCopilotCommandTurnActive(state, commandTurnId);
|
|
761
|
+
if (command === 'compact')
|
|
762
|
+
emitCopilotCompactionCompleted(state);
|
|
763
|
+
const reportedMode = await runtime.getMode(sessionId);
|
|
764
|
+
ensureCopilotCommandTurnActive(state, commandTurnId);
|
|
765
|
+
const mode = command === 'plan'
|
|
766
|
+
? (state.planModePending ? 'plan' : reportedMode)
|
|
767
|
+
: command === 'goal'
|
|
768
|
+
? 'autopilot'
|
|
769
|
+
: reportedMode;
|
|
770
|
+
state.planModePending = mode === 'plan';
|
|
771
|
+
setCopilotModeState(state, mode);
|
|
772
|
+
if (command === 'plan') {
|
|
773
|
+
const review = pendingPlanReview || {
|
|
774
|
+
sessionId,
|
|
775
|
+
languageHint: input,
|
|
776
|
+
};
|
|
777
|
+
emitCopilotPlanReview(state, review.sessionId, review.content, review.languageHint);
|
|
778
|
+
pendingPlanReview = null;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
catch (error) {
|
|
782
|
+
pendingPlanReview = null;
|
|
783
|
+
if (command === 'compact')
|
|
784
|
+
state.isCompacting = false;
|
|
785
|
+
if (!resumeSessionId && sessionId && state.copilotSessionId === sessionId) {
|
|
786
|
+
state.copilotSessionId = null;
|
|
787
|
+
state.lastCopilotSessionId = null;
|
|
788
|
+
state.sessionNotified = false;
|
|
789
|
+
}
|
|
790
|
+
if (command === 'plan') {
|
|
791
|
+
state.planModePending = true;
|
|
792
|
+
setCopilotModeState(state, 'plan');
|
|
793
|
+
}
|
|
794
|
+
else if (command === 'goal') {
|
|
795
|
+
setCopilotModeState(state, previousMode);
|
|
796
|
+
}
|
|
797
|
+
if (!(error instanceof CopilotDecisionCancelledError) && state.turnActive) {
|
|
798
|
+
sendFn({
|
|
799
|
+
type: 'backend_event',
|
|
800
|
+
conversationId,
|
|
801
|
+
event: {
|
|
802
|
+
type: 'error',
|
|
803
|
+
message: error instanceof Error ? error.message : String(error),
|
|
804
|
+
},
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
finally {
|
|
809
|
+
if (sessionId)
|
|
810
|
+
await runtime.destroySession(sessionId);
|
|
811
|
+
await runtime.stop();
|
|
812
|
+
state.commandRuntime = null;
|
|
813
|
+
state.commandSessionId = null;
|
|
814
|
+
state.isCompacting = false;
|
|
815
|
+
emitCopilotCommandCompleted(state);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
export async function setCopilotPlanMode(conversationId, enabled, workDir, providerId = 'copilot') {
|
|
819
|
+
let state = conversations.get(conversationId);
|
|
820
|
+
if (!state) {
|
|
821
|
+
createCopilotPlaceholder(conversationId, { providerId });
|
|
822
|
+
state = conversations.get(conversationId);
|
|
823
|
+
}
|
|
824
|
+
if (state.turnActive || state.commandRuntime) {
|
|
825
|
+
throw new Error('Copilot is already processing this conversation');
|
|
826
|
+
}
|
|
827
|
+
state.workDir = workDir;
|
|
828
|
+
detachCopilotProviderSession(state, providerId);
|
|
829
|
+
if (enabled) {
|
|
830
|
+
state.planModePending = true;
|
|
831
|
+
setCopilotModeState(state, 'plan');
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
state.planModePending = false;
|
|
835
|
+
const sessionId = state.copilotSessionId || state.lastCopilotSessionId;
|
|
836
|
+
if (!sessionId) {
|
|
837
|
+
setCopilotModeState(state, 'interactive');
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
const runtime = new CopilotCommandRuntime({ providerId, workDir });
|
|
841
|
+
state.commandRuntime = runtime;
|
|
842
|
+
state.commandSessionId = sessionId;
|
|
843
|
+
let activeSessionId = sessionId;
|
|
844
|
+
try {
|
|
845
|
+
await runtime.start();
|
|
846
|
+
try {
|
|
847
|
+
await runtime.openSession(sessionId);
|
|
848
|
+
}
|
|
849
|
+
catch (error) {
|
|
850
|
+
if (!isMissingCopilotSessionError(error))
|
|
851
|
+
throw error;
|
|
852
|
+
state.copilotSessionId = null;
|
|
853
|
+
state.lastCopilotSessionId = null;
|
|
854
|
+
state.sessionNotified = false;
|
|
855
|
+
activeSessionId = await runtime.openSession();
|
|
856
|
+
state.copilotSessionId = activeSessionId;
|
|
857
|
+
state.lastCopilotSessionId = activeSessionId;
|
|
858
|
+
state.commandSessionId = activeSessionId;
|
|
859
|
+
}
|
|
860
|
+
await runtime.setMode(activeSessionId, 'interactive');
|
|
861
|
+
setCopilotModeState(state, await runtime.getMode(activeSessionId));
|
|
862
|
+
}
|
|
863
|
+
finally {
|
|
864
|
+
await runtime.destroySession(activeSessionId);
|
|
865
|
+
await runtime.stop();
|
|
866
|
+
state.commandRuntime = null;
|
|
867
|
+
state.commandSessionId = null;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
270
870
|
export function normalizeCopilotRunOptions(input) {
|
|
271
871
|
const options = {};
|
|
272
872
|
if (!input)
|
|
@@ -343,26 +943,30 @@ export function handleCopilotChat(conversationId, text, workDir, options, files)
|
|
|
343
943
|
stateVersion: 0,
|
|
344
944
|
heartbeatTimer: null,
|
|
345
945
|
sessionNotified: false,
|
|
946
|
+
copilotMode: 'interactive',
|
|
947
|
+
planMode: false,
|
|
948
|
+
planModePending: false,
|
|
949
|
+
isCompacting: false,
|
|
950
|
+
commandRuntime: null,
|
|
951
|
+
commandSessionId: null,
|
|
952
|
+
deferredCommandUsage: null,
|
|
346
953
|
};
|
|
347
954
|
conversations.set(conversationId, state);
|
|
348
955
|
}
|
|
349
956
|
state.workDir = workDir;
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
957
|
+
const providerChanged = !!options?.providerId && state.providerId !== options.providerId;
|
|
958
|
+
if (options?.providerId)
|
|
959
|
+
detachCopilotProviderSession(state, options.providerId);
|
|
353
960
|
// Determine session to resume
|
|
354
|
-
const resumeId =
|
|
355
|
-
|
|
356
|
-
|
|
961
|
+
const resumeId = providerChanged
|
|
962
|
+
? null
|
|
963
|
+
: options?.resumeSessionId
|
|
964
|
+
|| state.copilotSessionId
|
|
965
|
+
|| state.lastCopilotSessionId;
|
|
357
966
|
if (resumeId) {
|
|
358
967
|
state.copilotSessionId = resumeId;
|
|
359
968
|
state.lastCopilotSessionId = resumeId;
|
|
360
969
|
}
|
|
361
|
-
// For new sessions, pre-assign a sessionId via --session-id so we know it
|
|
362
|
-
// immediately (don't have to wait for the `result` event).
|
|
363
|
-
if (!resumeId) {
|
|
364
|
-
state.copilotSessionId = randomUUID();
|
|
365
|
-
}
|
|
366
970
|
const normalized = normalizeCopilotRunOptions(options?.copilotOptions);
|
|
367
971
|
if (normalized.error) {
|
|
368
972
|
sendCopilotSetupError(state, normalized.error);
|
|
@@ -374,6 +978,19 @@ export function handleCopilotChat(conversationId, text, workDir, options, files)
|
|
|
374
978
|
state.contextTier = normalized.options.context;
|
|
375
979
|
if (normalized.options.effort)
|
|
376
980
|
state.reasoningEffort = normalized.options.effort;
|
|
981
|
+
if (state.planModePending) {
|
|
982
|
+
void executeCopilotCommand(conversationId, 'plan', text, workDir, {
|
|
983
|
+
resumeSessionId: resumeId || undefined,
|
|
984
|
+
providerId: state.providerId,
|
|
985
|
+
copilotOptions: options?.copilotOptions,
|
|
986
|
+
});
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
// For new one-shot sessions, pre-assign a sessionId via --session-id so we
|
|
990
|
+
// know it immediately (the SDK command path creates its own persisted ID).
|
|
991
|
+
if (!resumeId) {
|
|
992
|
+
state.copilotSessionId = randomUUID();
|
|
993
|
+
}
|
|
377
994
|
let processedFiles;
|
|
378
995
|
try {
|
|
379
996
|
processedFiles = processFilesForCopilot(files);
|
|
@@ -494,7 +1111,20 @@ export function handleCopilotChat(conversationId, text, workDir, options, files)
|
|
|
494
1111
|
state.child = null;
|
|
495
1112
|
state.abortController = null;
|
|
496
1113
|
if (state.turnActive) {
|
|
1114
|
+
const missingResumeSession = !!resumeId
|
|
1115
|
+
&& code !== 0
|
|
1116
|
+
&& isMissingCopilotSessionError(stderrBuf);
|
|
497
1117
|
finishCopilotTurn(state);
|
|
1118
|
+
if (missingResumeSession) {
|
|
1119
|
+
state.copilotSessionId = null;
|
|
1120
|
+
state.lastCopilotSessionId = null;
|
|
1121
|
+
state.sessionNotified = false;
|
|
1122
|
+
handleCopilotChat(conversationId, text, workDir, {
|
|
1123
|
+
providerId: state.providerId,
|
|
1124
|
+
copilotOptions: options?.copilotOptions,
|
|
1125
|
+
}, files);
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
498
1128
|
// If turn wasn't completed by a result event, emit error or cancellation
|
|
499
1129
|
if (code !== 0 && code !== null) {
|
|
500
1130
|
const errMsg = stderrBuf.trim() || `Copilot process exited with code ${code}`;
|
|
@@ -572,6 +1202,44 @@ function handleCopilotEvent(state, event) {
|
|
|
572
1202
|
}
|
|
573
1203
|
const session = sessionRef(state.copilotSessionId || 'pending');
|
|
574
1204
|
switch (event.type) {
|
|
1205
|
+
case 'session.mode_changed': {
|
|
1206
|
+
const data = event.data;
|
|
1207
|
+
if (data?.newMode) {
|
|
1208
|
+
const mode = data.newMode;
|
|
1209
|
+
if (state.planModePending && mode === 'interactive')
|
|
1210
|
+
break;
|
|
1211
|
+
setCopilotModeState(state, mode);
|
|
1212
|
+
}
|
|
1213
|
+
break;
|
|
1214
|
+
}
|
|
1215
|
+
case 'session.compaction_start': {
|
|
1216
|
+
emitCopilotCompactionStarted(state);
|
|
1217
|
+
break;
|
|
1218
|
+
}
|
|
1219
|
+
case 'session.compaction_complete': {
|
|
1220
|
+
// Native providers can report a compaction boundary before the command
|
|
1221
|
+
// request itself settles. Keep the synthesized row pending until
|
|
1222
|
+
// invokeCommand succeeds so a later rejection cannot leave a false
|
|
1223
|
+
// completed checkmark behind.
|
|
1224
|
+
if (state.commandRuntime && state.isCompacting)
|
|
1225
|
+
break;
|
|
1226
|
+
emitCopilotCompactionCompleted(state);
|
|
1227
|
+
break;
|
|
1228
|
+
}
|
|
1229
|
+
case 'session.compaction_failed': {
|
|
1230
|
+
state.isCompacting = false;
|
|
1231
|
+
const data = event.data;
|
|
1232
|
+
sendFn({
|
|
1233
|
+
type: 'backend_event',
|
|
1234
|
+
conversationId: convId,
|
|
1235
|
+
turnId: state.turnId,
|
|
1236
|
+
event: {
|
|
1237
|
+
type: 'error',
|
|
1238
|
+
message: data?.error || 'Context compaction failed',
|
|
1239
|
+
},
|
|
1240
|
+
});
|
|
1241
|
+
break;
|
|
1242
|
+
}
|
|
575
1243
|
case 'assistant.message_delta': {
|
|
576
1244
|
const data = event.data;
|
|
577
1245
|
if (data?.deltaContent) {
|
|
@@ -662,8 +1330,15 @@ function handleCopilotEvent(state, event) {
|
|
|
662
1330
|
state.copilotSessionId = sessionId;
|
|
663
1331
|
state.lastCopilotSessionId = sessionId;
|
|
664
1332
|
}
|
|
665
|
-
finishCopilotTurn(state);
|
|
666
1333
|
const usage = buildCopilotUsage(state, event.usage);
|
|
1334
|
+
// Native command turns can remain open after Copilot finishes generating
|
|
1335
|
+
// while AgentLink waits for a required final Plan decision. Their outer
|
|
1336
|
+
// command lifecycle emits the authoritative terminal event.
|
|
1337
|
+
if (state.commandRuntime) {
|
|
1338
|
+
state.deferredCommandUsage = usage;
|
|
1339
|
+
break;
|
|
1340
|
+
}
|
|
1341
|
+
finishCopilotTurn(state);
|
|
667
1342
|
// Emit backend_event turn_completed
|
|
668
1343
|
sendFn({
|
|
669
1344
|
type: 'backend_event',
|
|
@@ -691,10 +1366,23 @@ function handleCopilotEvent(state, event) {
|
|
|
691
1366
|
// ── Cancel ──────────────────────────────────────────────────────────────────
|
|
692
1367
|
export function cancelCopilotExecution(conversationId) {
|
|
693
1368
|
const state = conversations.get(conversationId);
|
|
1369
|
+
cancelPendingCopilotDecisions(conversationId);
|
|
694
1370
|
if (!state) {
|
|
695
1371
|
emitCopilotCancelled(conversationId);
|
|
696
1372
|
return;
|
|
697
1373
|
}
|
|
1374
|
+
state.deferredCommandUsage = null;
|
|
1375
|
+
if (state.commandRuntime && state.commandSessionId) {
|
|
1376
|
+
const cancellation = state.isCompacting
|
|
1377
|
+
? state.commandRuntime.abortManualCompaction(state.commandSessionId)
|
|
1378
|
+
: state.commandRuntime.abort(state.commandSessionId);
|
|
1379
|
+
state.isCompacting = false;
|
|
1380
|
+
void cancellation.catch(() => { });
|
|
1381
|
+
if (state.turnActive)
|
|
1382
|
+
finishCopilotTurn(state);
|
|
1383
|
+
emitCopilotCancelled(conversationId, state);
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
698
1386
|
if (!state.child) {
|
|
699
1387
|
if (state.turnActive) {
|
|
700
1388
|
finishCopilotTurn(state);
|
|
@@ -723,7 +1411,7 @@ function cleanupCopilotConversation(conversationId) {
|
|
|
723
1411
|
const state = conversations.get(conversationId);
|
|
724
1412
|
if (!state)
|
|
725
1413
|
return;
|
|
726
|
-
if (state.child) {
|
|
1414
|
+
if (state.child || state.commandRuntime) {
|
|
727
1415
|
cancelCopilotExecution(conversationId);
|
|
728
1416
|
}
|
|
729
1417
|
if (state.heartbeatTimer) {
|
|
@@ -1034,6 +1722,37 @@ export function readCopilotSessionMessages(_workDir, sessionId) {
|
|
|
1034
1722
|
}
|
|
1035
1723
|
return messages;
|
|
1036
1724
|
}
|
|
1725
|
+
export function readCopilotSessionMode(sessionId) {
|
|
1726
|
+
const sessionRoot = resolve(COPILOT_SESSION_DIR);
|
|
1727
|
+
const sessionDir = resolve(COPILOT_SESSION_DIR, sessionId);
|
|
1728
|
+
if (sessionDir === sessionRoot || !sessionDir.startsWith(`${sessionRoot}${process.platform === 'win32' ? '\\' : '/'}`)) {
|
|
1729
|
+
return 'interactive';
|
|
1730
|
+
}
|
|
1731
|
+
const eventsPath = join(sessionDir, 'events.jsonl');
|
|
1732
|
+
if (!existsSync(eventsPath))
|
|
1733
|
+
return 'interactive';
|
|
1734
|
+
let mode = 'interactive';
|
|
1735
|
+
try {
|
|
1736
|
+
for (const line of readFileSync(eventsPath, 'utf-8').split('\n')) {
|
|
1737
|
+
if (!line.trim())
|
|
1738
|
+
continue;
|
|
1739
|
+
try {
|
|
1740
|
+
const event = JSON.parse(line);
|
|
1741
|
+
const nextMode = event.type === 'session.mode_changed' ? event.data?.newMode : undefined;
|
|
1742
|
+
if (nextMode === 'interactive' || nextMode === 'plan' || nextMode === 'autopilot') {
|
|
1743
|
+
mode = nextMode;
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
catch {
|
|
1747
|
+
// Ignore partial or malformed history lines.
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
catch {
|
|
1752
|
+
return 'interactive';
|
|
1753
|
+
}
|
|
1754
|
+
return mode;
|
|
1755
|
+
}
|
|
1037
1756
|
export function getCopilotSessionWorkDir(sessionId) {
|
|
1038
1757
|
const sessionRoot = resolve(COPILOT_SESSION_DIR);
|
|
1039
1758
|
const sessionDir = resolve(COPILOT_SESSION_DIR, sessionId);
|