@volter/supercode-frontend-vscode 0.1.62 → 0.1.64
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 +119 -22
- package/dist/extension.js +256 -111
- package/dist/host-controls.d.ts +0 -1
- package/dist/proposals.js +2 -2
- package/dist/session.d.ts +2 -21
- package/dist/session.js +3 -35
- package/dist/view.d.ts +135 -6
- package/dist/view.js +470 -110
- package/package.json +1 -1
package/dist/extension.js
CHANGED
|
@@ -11,7 +11,7 @@ import { readCredentialFile } from './credential.js';
|
|
|
11
11
|
import { grantedProposals, PROPOSAL_EFFECTS, REQUESTED_PROPOSALS } from './proposals.js';
|
|
12
12
|
import { observeActiveResponse, sessionStatus } from './native-session.js';
|
|
13
13
|
import { RequestNotPendingError, VscodeFrontendSession } from './session.js';
|
|
14
|
-
import { acceptedDecisions, opaqueText, projectEvent, projectPendingRequests, projectHistory, projectReplayTurns, HISTORY_MARKER_PROMPT, resolveParts, resolutionSentence, typedAnswer, } from './view.js';
|
|
14
|
+
import { acceptedDecisions, opaqueText, projectEvent, projectPendingRequests, projectHistory, projectReplayTurns, HISTORY_MARKER_PROMPT, PlanTracker, resolveParts, resolutionSentence, turnOutcome, typedAnswer, } from './view.js';
|
|
15
15
|
const SESSION_SCHEME = 'supercode';
|
|
16
16
|
/** The harnesses whose model and reasoning effort a new chat can set (the host passes them to the CLI). */
|
|
17
17
|
const CONFIGURABLE_HARNESSES = ['claude-code', 'codex'];
|
|
@@ -204,6 +204,109 @@ class SupercodeFrontends {
|
|
|
204
204
|
}
|
|
205
205
|
}
|
|
206
206
|
let nativeInTurnApprovalCards = false;
|
|
207
|
+
/** The output channel, for records that are not conversation (`unshown` parts). */
|
|
208
|
+
let outputLog;
|
|
209
|
+
/** Each such record once, however often a history is rebuilt from the same events. */
|
|
210
|
+
const loggedRecords = new Set();
|
|
211
|
+
const logRecord = (value) => {
|
|
212
|
+
if (loggedRecords.has(value))
|
|
213
|
+
return;
|
|
214
|
+
loggedRecords.add(value);
|
|
215
|
+
if (loggedRecords.size > 1000)
|
|
216
|
+
loggedRecords.delete(loggedRecords.values().next().value);
|
|
217
|
+
outputLog?.warn(value);
|
|
218
|
+
};
|
|
219
|
+
/** The requests each response stream drew a card for, so a resolution is said only beneath a card it drew. */
|
|
220
|
+
const drawnRequests = new WeakMap();
|
|
221
|
+
/** Requests answered through their own card in this window (`answerRequest`): such a card shows its answer itself. */
|
|
222
|
+
const answeredByCard = new Set();
|
|
223
|
+
const cardKey = (connectionId, requestId) => JSON.stringify([connectionId, requestId]);
|
|
224
|
+
const drew = (stream, requestId) => {
|
|
225
|
+
const drawn = drawnRequests.get(stream) ?? new Set();
|
|
226
|
+
drawnRequests.set(stream, drawn.add(requestId));
|
|
227
|
+
};
|
|
228
|
+
/**
|
|
229
|
+
* A harness's questions as VS Code's own carousel, inside the running turn; the person's answers are the
|
|
230
|
+
* elicitation's `{action:'accept', content:{<key>: label | [labels] | text}}`, and Skip is a decline. The answer
|
|
231
|
+
* takes the same door as a button's (`supercode.frontend.respond`).
|
|
232
|
+
*/
|
|
233
|
+
function askQuestions(stream, part, pending, cancelled) {
|
|
234
|
+
const questions = part.questions.map(field => new vscode.ChatQuestion(field.key, field.options.length === 0 ? vscode.ChatQuestionType.Text
|
|
235
|
+
: field.multiSelect ? vscode.ChatQuestionType.MultiSelect : vscode.ChatQuestionType.SingleSelect, field.title, {
|
|
236
|
+
...(field.message ? { message: field.message } : {}),
|
|
237
|
+
...(field.options.length ? {
|
|
238
|
+
options: field.options.map((option, index) => ({ id: `${field.key}-${index}`, label: option.description ? `${option.label} — ${option.description}` : option.label, value: option.label })),
|
|
239
|
+
allowFreeformInput: true,
|
|
240
|
+
} : {}),
|
|
241
|
+
}));
|
|
242
|
+
const respond = (response) => vscode.commands.executeCommand('supercode.frontend.respond', { connectionId: part.connectionId, requestId: part.requestId, response, label: 'Answer' });
|
|
243
|
+
void Promise.resolve(stream.questionCarousel(questions, true)).then(answers => {
|
|
244
|
+
// (the editor also resolves an unanswered carousel when its response ends: a request answered by another door
|
|
245
|
+
// by then is left alone)
|
|
246
|
+
// (and a stopped turn: Stop already cancels its requests)
|
|
247
|
+
if ((pending && !pending(part.requestId)) || cancelled?.())
|
|
248
|
+
return;
|
|
249
|
+
const content = {};
|
|
250
|
+
for (const field of part.questions) {
|
|
251
|
+
const answer = carouselAnswer(answers?.[field.key]);
|
|
252
|
+
if (answer !== undefined)
|
|
253
|
+
content[field.key] = answer;
|
|
254
|
+
}
|
|
255
|
+
// Skip, and a carousel submitted with nothing chosen, decline: a question is never answered with nothing.
|
|
256
|
+
if (Object.keys(content).length === 0)
|
|
257
|
+
return respond({ kind: 'elicitation', request_id: part.requestId, action: 'decline' });
|
|
258
|
+
return respond({ kind: 'elicitation', request_id: part.requestId, action: 'accept', content });
|
|
259
|
+
}, () => undefined);
|
|
260
|
+
}
|
|
261
|
+
/** One carousel answer as the elicitation's value: the text, the selected label (or the person's own words), or the
|
|
262
|
+
* selected labels with any words of their own after them. */
|
|
263
|
+
function carouselAnswer(value) {
|
|
264
|
+
if (typeof value === 'string')
|
|
265
|
+
return value.trim() || undefined;
|
|
266
|
+
if (!value || typeof value !== 'object')
|
|
267
|
+
return undefined;
|
|
268
|
+
const answer = value;
|
|
269
|
+
const own = typeof answer.freeformValue === 'string' && answer.freeformValue.trim() ? answer.freeformValue.trim() : undefined;
|
|
270
|
+
if (Array.isArray(answer.selectedValues)) {
|
|
271
|
+
const picked = [...answer.selectedValues.filter((item) => typeof item === 'string'), ...(own ? [own] : [])];
|
|
272
|
+
return picked.length ? picked : undefined;
|
|
273
|
+
}
|
|
274
|
+
return typeof answer.selectedValue === 'string' && answer.selectedValue ? answer.selectedValue : own;
|
|
275
|
+
}
|
|
276
|
+
/** VS Code's own hook row: which hook, and what it said — a stop reason where it blocked, a warning otherwise. */
|
|
277
|
+
function hookPart(part) {
|
|
278
|
+
return part.blocked
|
|
279
|
+
? new vscode.ChatResponseHookPart(part.hookType, part.reason || 'The hook gave no reason.')
|
|
280
|
+
: new vscode.ChatResponseHookPart(part.hookType, undefined, part.reason || 'The hook gave no reason.');
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* What a tool row shows inside it, the way VS Code's own rows do, live and in history alike: a plan call is the todo
|
|
284
|
+
* list as the call left it; a subagent call is the subagent row, with its answer once it has one (none for a subagent
|
|
285
|
+
* started in the background, whose call returns only a launch notice); a shell call is the terminal block; any other
|
|
286
|
+
* finished call its input and output. A subagent's own calls are grouped under the call they were made inside.
|
|
287
|
+
*/
|
|
288
|
+
function setToolDetails(invocation, row) {
|
|
289
|
+
if (row.parentToolCallId)
|
|
290
|
+
invocation.subAgentInvocationId = row.parentToolCallId;
|
|
291
|
+
if (row.plan) {
|
|
292
|
+
invocation.toolSpecificData = { todoList: row.plan.map((item, index) => ({
|
|
293
|
+
id: index + 1,
|
|
294
|
+
title: item.title,
|
|
295
|
+
status: item.status === 'completed' ? vscode.ChatTodoStatus.Completed
|
|
296
|
+
: item.status === 'in-progress' ? vscode.ChatTodoStatus.InProgress : vscode.ChatTodoStatus.NotStarted,
|
|
297
|
+
})) };
|
|
298
|
+
}
|
|
299
|
+
else if (row.subagent) {
|
|
300
|
+
invocation.toolSpecificData = new vscode.ChatSubagentToolInvocationData(row.subagent.description, row.subagent.agentName, row.subagent.prompt, row.isComplete && !row.background ? row.resultText ?? '' : undefined);
|
|
301
|
+
}
|
|
302
|
+
else if (row.commandLine) {
|
|
303
|
+
invocation.toolSpecificData = { commandLine: { original: row.commandLine }, language: 'sh',
|
|
304
|
+
...(row.isComplete ? { output: { text: row.resultText ?? '' } } : {}) };
|
|
305
|
+
}
|
|
306
|
+
else if (row.isComplete) {
|
|
307
|
+
invocation.toolSpecificData = { input: row.inputText ?? '', output: row.resultText ?? '' };
|
|
308
|
+
}
|
|
309
|
+
}
|
|
207
310
|
function writeProjected(stream, projected, granted, state) {
|
|
208
311
|
for (const candidate of projected) {
|
|
209
312
|
const parts = resolveParts([candidate], granted);
|
|
@@ -219,7 +322,7 @@ function writeProjected(stream, projected, granted, state) {
|
|
|
219
322
|
writePart(stream, fallback, state);
|
|
220
323
|
}
|
|
221
324
|
catch {
|
|
222
|
-
stream
|
|
325
|
+
// (a stream that refuses even the stable part has closed; a part's JSON is never conversation)
|
|
223
326
|
}
|
|
224
327
|
}
|
|
225
328
|
}
|
|
@@ -244,6 +347,16 @@ function writePart(stream, part, state) {
|
|
|
244
347
|
case 'thinking':
|
|
245
348
|
stream.push(new vscode.ChatResponseThinkingProgressPart(part.value));
|
|
246
349
|
return;
|
|
350
|
+
case 'hook':
|
|
351
|
+
stream.push(hookPart(part));
|
|
352
|
+
return;
|
|
353
|
+
case 'questions':
|
|
354
|
+
drew(stream, part.requestId);
|
|
355
|
+
if (state.carousel)
|
|
356
|
+
askQuestions(stream, part, state.pending, state.cancelled);
|
|
357
|
+
else
|
|
358
|
+
writePart(stream, part.fallback, state);
|
|
359
|
+
return;
|
|
247
360
|
case 'reference': {
|
|
248
361
|
// Only a path the runtime's own payload named, resolved against the
|
|
249
362
|
// workspace this window already has. Nothing is created or fabricated.
|
|
@@ -254,9 +367,18 @@ function writePart(stream, part, state) {
|
|
|
254
367
|
stream.markdown(`\`${part.path}\``);
|
|
255
368
|
return;
|
|
256
369
|
}
|
|
370
|
+
case 'resolution':
|
|
371
|
+
if (!drawnRequests.get(stream)?.has(part.requestId) || answeredByCard.has(cardKey(part.connectionId, part.requestId)))
|
|
372
|
+
return;
|
|
373
|
+
stream.markdown(`\n\n${part.value}\n\n`);
|
|
374
|
+
return;
|
|
375
|
+
case 'unshown':
|
|
376
|
+
logRecord(part.value);
|
|
377
|
+
return;
|
|
257
378
|
case 'answers':
|
|
258
|
-
if (part.approval && part.answers.length > 0 && state.quiet?.(part.
|
|
379
|
+
if (part.approval && part.answers.length > 0 && state.quiet?.(part.requestId))
|
|
259
380
|
return;
|
|
381
|
+
drew(stream, part.requestId);
|
|
260
382
|
if (part.approval && nativeInTurnApprovalCards && part.answers.length > 0) {
|
|
261
383
|
stream.confirmation(part.title, part.message, {
|
|
262
384
|
kind: 'supercode.inTurnApproval', version: 1, answers: part.answers,
|
|
@@ -299,17 +421,22 @@ function writePart(stream, part, state) {
|
|
|
299
421
|
invocation.isError = part.isError;
|
|
300
422
|
if (part.inputText !== undefined)
|
|
301
423
|
state.toolInputs.set(part.toolCallId, part.inputText);
|
|
302
|
-
if (
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
...
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
};
|
|
424
|
+
if (part.plan || part.subagent || part.parentToolCallId || part.background) {
|
|
425
|
+
const kept = state.toolKinds.get(part.toolCallId) ?? {};
|
|
426
|
+
state.toolKinds.set(part.toolCallId, {
|
|
427
|
+
...kept,
|
|
428
|
+
...(part.plan ? { plan: part.plan } : {}),
|
|
429
|
+
...(part.subagent ? { subagent: part.subagent } : {}),
|
|
430
|
+
...(part.parentToolCallId ? { parentToolCallId: part.parentToolCallId } : {}),
|
|
431
|
+
...(part.background ? { background: true } : {}),
|
|
432
|
+
});
|
|
312
433
|
}
|
|
434
|
+
setToolDetails(invocation, {
|
|
435
|
+
...part,
|
|
436
|
+
...state.toolKinds.get(part.toolCallId),
|
|
437
|
+
commandLine,
|
|
438
|
+
inputText: state.toolInputs.get(part.toolCallId),
|
|
439
|
+
});
|
|
313
440
|
stream.push(invocation);
|
|
314
441
|
return;
|
|
315
442
|
}
|
|
@@ -338,6 +465,7 @@ function unavailableMarkdown(reason) {
|
|
|
338
465
|
}
|
|
339
466
|
export async function activate(context) {
|
|
340
467
|
const log = vscode.window.createOutputChannel('Volter Harness', { log: true });
|
|
468
|
+
outputLog = log;
|
|
341
469
|
context.subscriptions.push(log);
|
|
342
470
|
// The HOST decides which proposals this extension got; its own manifest only
|
|
343
471
|
// asks. `context.extension.packageJSON` is the filtered extension
|
|
@@ -674,7 +802,6 @@ export async function activate(context) {
|
|
|
674
802
|
if (state.activeSession !== connection.id)
|
|
675
803
|
throw new Error('The conversation changed before its history could be saved.');
|
|
676
804
|
connection.persistedHistory = state.history;
|
|
677
|
-
connection.historyTruncated = state.historyTruncated;
|
|
678
805
|
savedConnections.set(connection.id, connection);
|
|
679
806
|
};
|
|
680
807
|
const connectManaged = async (state = controls.state) => {
|
|
@@ -991,7 +1118,6 @@ export async function activate(context) {
|
|
|
991
1118
|
throw new Error('The host did not restore the requested conversation.');
|
|
992
1119
|
const connection = await connectManaged(state);
|
|
993
1120
|
connection.persistedHistory = state.history;
|
|
994
|
-
connection.historyTruncated = state.historyTruncated;
|
|
995
1121
|
savedConnections.set(id, connection);
|
|
996
1122
|
if (active !== connection)
|
|
997
1123
|
controlsChanged.fire();
|
|
@@ -1251,22 +1377,6 @@ export async function activate(context) {
|
|
|
1251
1377
|
// prevents the submit command from reaching session materialization at all.
|
|
1252
1378
|
context.subscriptions.push(vscode.commands.registerCommand('supercode.frontend.sendRefusal', () => draftChoiceRefusal()
|
|
1253
1379
|
?? (controls?.state.setup?.ready === false ? 'Finish setting up your coding agent before sending. Your draft is saved.' : undefined)));
|
|
1254
|
-
// A background task the chat named (sayTasks in runTurnInto), stopped through the harness's own control.
|
|
1255
|
-
context.subscriptions.push(vscode.commands.registerCommand('supercode.frontend.stopBackgroundTask', async (connectionId, taskId) => {
|
|
1256
|
-
const connection = frontends.get(connectionId);
|
|
1257
|
-
const task = connection?.session.backgroundTasks.find(candidate => candidate.id === taskId);
|
|
1258
|
-
if (!connection || !task) {
|
|
1259
|
-
void vscode.window.showInformationMessage('That background task is no longer running.');
|
|
1260
|
-
return;
|
|
1261
|
-
}
|
|
1262
|
-
try {
|
|
1263
|
-
const stopped = await connection.session.stopTask(taskId);
|
|
1264
|
-
void vscode.window.showInformationMessage(stopped ? `Stopped ${task.description || taskId}.` : `The harness did not stop ${task.description || taskId}.`);
|
|
1265
|
-
}
|
|
1266
|
-
catch (error) {
|
|
1267
|
-
void vscode.window.showErrorMessage(`Could not stop ${task.description || taskId}: ${String(error?.message ?? error)}`);
|
|
1268
|
-
}
|
|
1269
|
-
}));
|
|
1270
1380
|
// The host's native focus event is authoritative even when a cached model
|
|
1271
1381
|
// skips content resolution. Untitled/non-harness focus never starts a runtime.
|
|
1272
1382
|
context.subscriptions.push(vscode.commands.registerCommand('supercode.frontend.focusSession', (resource) => {
|
|
@@ -1451,8 +1561,8 @@ export async function activate(context) {
|
|
|
1451
1561
|
// never both answer the same request (the second answer would be refused and noted as a failure).
|
|
1452
1562
|
const answered = answeredRequests.get(connection.id) ?? new Set();
|
|
1453
1563
|
answeredRequests.set(connection.id, answered);
|
|
1454
|
-
// Requests whose card
|
|
1455
|
-
const
|
|
1564
|
+
// Requests whose card an automatic answer that failed or did not settle has drawn again: once each.
|
|
1565
|
+
const redrawn = new Set();
|
|
1456
1566
|
const answers = (requestId) => {
|
|
1457
1567
|
if (approval() !== 'autoApprove' || token.isCancellationRequested || !canAnswer(connection))
|
|
1458
1568
|
return false;
|
|
@@ -1463,7 +1573,6 @@ export async function activate(context) {
|
|
|
1463
1573
|
const decisions = acceptedDecisions(pending);
|
|
1464
1574
|
if (decisions && !decisions.includes('allow'))
|
|
1465
1575
|
return false;
|
|
1466
|
-
unshown.add(requestId);
|
|
1467
1576
|
return true;
|
|
1468
1577
|
};
|
|
1469
1578
|
const sweep = () => {
|
|
@@ -1476,45 +1585,40 @@ export async function activate(context) {
|
|
|
1476
1585
|
if (decisions && !decisions.includes('allow'))
|
|
1477
1586
|
continue;
|
|
1478
1587
|
answered.add(pending.id);
|
|
1479
|
-
// Its card is already in the stream (the event that raised it was written first), so say it was answered.
|
|
1480
1588
|
const payload = pending.payload && typeof pending.payload === 'object' && !Array.isArray(pending.payload) ? pending.payload : {};
|
|
1481
1589
|
const named = [payload.title, payload.tool, payload.tool_name].find((value) => typeof value === 'string');
|
|
1482
|
-
// The answer can settle after the turn's response has closed, when writing to it throws: the note is then dropped.
|
|
1483
|
-
const note = (text) => { try {
|
|
1484
|
-
stream.markdown(text);
|
|
1485
|
-
}
|
|
1486
|
-
catch {
|
|
1487
|
-
log.info(text.trim());
|
|
1488
|
-
} };
|
|
1489
1590
|
const label = opaqueText(named ?? `request #${pending.id}`, 200);
|
|
1490
|
-
//
|
|
1491
|
-
//
|
|
1591
|
+
// An automatic answer that fails or does not settle leaves the approval waiting: its card is drawn (again,
|
|
1592
|
+
// when it was already shown) as the one that still needs an answer, once: the settle timer and a later
|
|
1593
|
+
// failure of the same answer draw it one time between them. With the response closed, writing to it throws,
|
|
1594
|
+
// and the person is told an approval waits.
|
|
1492
1595
|
const showCard = () => {
|
|
1493
|
-
if (
|
|
1596
|
+
if (redrawn.has(pending.id) || !connection.session.pendingRequests.has(pending.id))
|
|
1494
1597
|
return;
|
|
1598
|
+
redrawn.add(pending.id);
|
|
1495
1599
|
try {
|
|
1496
|
-
|
|
1600
|
+
const state = { toolInputs: new Map(), toolNames: new Map(), toolTitles: new Map(), toolCommands: new Map(), toolKinds: new Map() };
|
|
1601
|
+
for (const part of resolveParts(projectPendingRequests([pending], {
|
|
1497
1602
|
connectionId: connection.id,
|
|
1498
|
-
canRespond: connection
|
|
1499
|
-
}), granted
|
|
1603
|
+
canRespond: canAnswer(connection),
|
|
1604
|
+
}), granted))
|
|
1605
|
+
writePart(stream, part, state);
|
|
1500
1606
|
}
|
|
1501
1607
|
catch {
|
|
1502
1608
|
void vscode.window.showWarningMessage(`${connection.label}: ${label} is waiting for approval. Open its chat to answer it.`);
|
|
1503
1609
|
}
|
|
1504
1610
|
};
|
|
1611
|
+
// An approval answered automatically adds no line of its own (Code-OSS says "Auto approved" on the call's row,
|
|
1612
|
+
// which the extension API cannot), and a card it answered after the card was shown gets its resolution line.
|
|
1505
1613
|
const unsettled = setTimeout(() => {
|
|
1506
|
-
|
|
1614
|
+
log.warn(`Auto approve has had no answer for ${label} for ${AUTO_ANSWER_SETTLE_MS / 1000} s.`);
|
|
1507
1615
|
showCard();
|
|
1508
1616
|
}, AUTO_ANSWER_SETTLE_MS);
|
|
1509
1617
|
void connection.session.respond({ kind: 'approval', request_id: pending.id, decision: 'allow' }).then(() => {
|
|
1510
1618
|
clearTimeout(unsettled);
|
|
1511
|
-
// A card that was never shown needs no note: its resolution line says it was allowed.
|
|
1512
|
-
if (unshown.has(pending.id))
|
|
1513
|
-
return;
|
|
1514
|
-
note(`\n_auto-approved: ${label}_\n`);
|
|
1515
1619
|
}, error => {
|
|
1516
1620
|
clearTimeout(unsettled);
|
|
1517
|
-
|
|
1621
|
+
log.warn(`Auto approve could not answer ${label}: ${String(error.message || error)}`);
|
|
1518
1622
|
showCard();
|
|
1519
1623
|
});
|
|
1520
1624
|
}
|
|
@@ -1538,11 +1642,16 @@ export async function activate(context) {
|
|
|
1538
1642
|
if (approval() === 'autoApprove' && !canAnswer(connection)) {
|
|
1539
1643
|
throw new Error('This harness does not support automatic approval through Chat. Select Harness permissions.');
|
|
1540
1644
|
}
|
|
1541
|
-
const state = { toolInputs: new Map(), toolNames: new Map(), toolTitles: new Map(), toolCommands: new Map()
|
|
1645
|
+
const state = { toolInputs: new Map(), toolNames: new Map(), toolTitles: new Map(), toolCommands: new Map(), toolKinds: new Map(),
|
|
1646
|
+
carousel: true, pending: (requestId) => connection.session.pendingRequests.has(requestId),
|
|
1647
|
+
cancelled: () => token.isCancellationRequested };
|
|
1542
1648
|
const projection = {
|
|
1543
1649
|
connectionId: connection.id,
|
|
1544
1650
|
pendingRequest: (requestId) => connection.session.pendingRequests.get(requestId) ?? connection.session.resolvedRequests.get(requestId),
|
|
1545
|
-
|
|
1651
|
+
// (as history: a control appears only where the runtime offers `respond` and this attachment may approve)
|
|
1652
|
+
canRespond: canAnswer(connection),
|
|
1653
|
+
plan: connection.plan ??= new PlanTracker(),
|
|
1654
|
+
notices: new Set(),
|
|
1546
1655
|
};
|
|
1547
1656
|
const cancellation = token.onCancellationRequested(() => {
|
|
1548
1657
|
void stopConnection(connection).catch(() => {
|
|
@@ -1552,27 +1661,8 @@ export async function activate(context) {
|
|
|
1552
1661
|
});
|
|
1553
1662
|
const watch = watchApprovals(connection, stream, token, approval);
|
|
1554
1663
|
state.quiet = watch.answers;
|
|
1555
|
-
//
|
|
1556
|
-
|
|
1557
|
-
// way to stop it.
|
|
1558
|
-
let saidTasks = '';
|
|
1559
|
-
const sayTasks = () => {
|
|
1560
|
-
const tasks = connection.session.backgroundTasks;
|
|
1561
|
-
const key = tasks.map(task => task.id).join(',');
|
|
1562
|
-
if (key === saidTasks)
|
|
1563
|
-
return;
|
|
1564
|
-
saidTasks = key;
|
|
1565
|
-
if (!tasks.length) {
|
|
1566
|
-
stream.markdown('\n\n_No background task is running any more._\n\n');
|
|
1567
|
-
return;
|
|
1568
|
-
}
|
|
1569
|
-
stream.markdown(`\n\n_${tasks.length === 1 ? 'A background task is' : `${tasks.length} background tasks are`} still running in this session. The harness answers each of its events as a turn of its own, so this message can wait behind them:_\n\n`);
|
|
1570
|
-
for (const task of tasks) {
|
|
1571
|
-
stream.button({ command: 'supercode.frontend.stopBackgroundTask', title: `Stop ${opaqueText(task.description || task.id, 80)}`, arguments: [connection.id, task.id] });
|
|
1572
|
-
}
|
|
1573
|
-
};
|
|
1574
|
-
sayTasks();
|
|
1575
|
-
const unwatchTasks = connection.session.onChange(sayTasks);
|
|
1664
|
+
// How the turn ended when it did not succeed: the response's own error, as VS Code's agent host ends one.
|
|
1665
|
+
let outcome;
|
|
1576
1666
|
let completed = false;
|
|
1577
1667
|
let turnStarted = false;
|
|
1578
1668
|
try {
|
|
@@ -1581,14 +1671,18 @@ export async function activate(context) {
|
|
|
1581
1671
|
// events arrive; no save refusal submits input again or aborts its turn.
|
|
1582
1672
|
throwIfCancelled();
|
|
1583
1673
|
await connection.session.runTurn(prompt, (event) => {
|
|
1584
|
-
// A turn the person stopped is marked stopped by VS Code itself;
|
|
1674
|
+
// A turn the person stopped is marked stopped by VS Code itself; any other end is this response's result.
|
|
1585
1675
|
if (event.kind === 'turn_interrupted' && token.isCancellationRequested)
|
|
1586
1676
|
return;
|
|
1587
1677
|
// Native submit publishes its initial user echo before `turn_started`.
|
|
1588
1678
|
// Subsequent input belongs in the response while this turn is visible.
|
|
1589
1679
|
if (event.kind === 'turn_started')
|
|
1590
1680
|
turnStarted = true;
|
|
1591
|
-
|
|
1681
|
+
const ended = turnOutcome(event);
|
|
1682
|
+
if (ended)
|
|
1683
|
+
outcome = ended;
|
|
1684
|
+
else
|
|
1685
|
+
writeProjected(stream, projectEvent(event, { ...projection, liveUserMessages: turnStarted }), granted, state);
|
|
1592
1686
|
watch.sweep();
|
|
1593
1687
|
rememberIdentity(connection);
|
|
1594
1688
|
}, { throwIfCancelled, onSubmitted: () => {
|
|
@@ -1597,7 +1691,6 @@ export async function activate(context) {
|
|
|
1597
1691
|
completed = true;
|
|
1598
1692
|
}
|
|
1599
1693
|
finally {
|
|
1600
|
-
unwatchTasks();
|
|
1601
1694
|
watch.dispose();
|
|
1602
1695
|
cancellation.dispose();
|
|
1603
1696
|
await finishIdentitySave(connection);
|
|
@@ -1608,7 +1701,14 @@ export async function activate(context) {
|
|
|
1608
1701
|
const effective = active ? controls?.state.effective : undefined;
|
|
1609
1702
|
const ran = effective?.modelName || (active ? controls?.state.actualModel : undefined) || connection.session.descriptor?.model;
|
|
1610
1703
|
const effort = effective?.effort || controls?.state.sessions.find(session => session.id === connection.id)?.selection.effort;
|
|
1611
|
-
|
|
1704
|
+
const details = completed && ran && !token.isCancellationRequested ? `${opaqueText(ran, 120)}${effort ? ` · ${opaqueText(effort, 24)}` : ''}` : undefined;
|
|
1705
|
+
// (an interruption is the expected, warning-level error: `isExpectedError` and `level` are
|
|
1706
|
+
// `chatParticipantPrivate`'s, which the ext host checks for `isExpectedError`, so a host without it gets the message)
|
|
1707
|
+
const errorDetails = outcome && (outcome.interrupted && granted.has('chatParticipantPrivate')
|
|
1708
|
+
? { message: outcome.message, isExpectedError: true, level: 1 /* ChatErrorLevel.Warning */ }
|
|
1709
|
+
: { message: outcome.message });
|
|
1710
|
+
// The model the turn ran on, in VS Code's own response footer rather than as a line of the conversation.
|
|
1711
|
+
return { ...(details ? { details } : {}), ...(errorDetails ? { errorDetails } : {}) };
|
|
1612
1712
|
};
|
|
1613
1713
|
const handler = async (request, chatContext, stream, token) => {
|
|
1614
1714
|
await applying;
|
|
@@ -1621,9 +1721,10 @@ export async function activate(context) {
|
|
|
1621
1721
|
// thrown as is: the workbench writes a handler's error into the chat
|
|
1622
1722
|
throw new Error(`${refusal} Nothing was sent; send it again in a new chat.`);
|
|
1623
1723
|
}
|
|
1724
|
+
// A request the handler cannot run is refused as VS Code refuses one: the request's own error (thrown, which
|
|
1725
|
+
// the workbench writes as the response's errorDetails), not an answer in the transcript.
|
|
1624
1726
|
if (controls?.state.setup && !controls.state.setup.ready) {
|
|
1625
|
-
|
|
1626
|
-
return {};
|
|
1727
|
+
throw new Error('Sign in before sending a message. Use **New Chat: Choose Agent** to get started.');
|
|
1627
1728
|
}
|
|
1628
1729
|
const connection = requestedResource
|
|
1629
1730
|
? await connectionFor(requestedResource)
|
|
@@ -1635,10 +1736,8 @@ export async function activate(context) {
|
|
|
1635
1736
|
throw new Error('This conversation has fixed model settings. Start a new chat to choose different settings; no message was sent.');
|
|
1636
1737
|
}
|
|
1637
1738
|
}
|
|
1638
|
-
if (!connection)
|
|
1639
|
-
|
|
1640
|
-
return {};
|
|
1641
|
-
}
|
|
1739
|
+
if (!connection)
|
|
1740
|
+
throw new Error(unavailableMarkdown(frontends.unavailable));
|
|
1642
1741
|
const pending = pendingRequestOf(connection);
|
|
1643
1742
|
// A message is never taken as an approval: one sent while an approval waits would otherwise allow the tool and
|
|
1644
1743
|
// never reach the agent. It is refused, with what to do, like a send during any running turn.
|
|
@@ -1670,9 +1769,7 @@ export async function activate(context) {
|
|
|
1670
1769
|
attached.push({ name: uri.toString(), text: document.getText(location?.range) });
|
|
1671
1770
|
}
|
|
1672
1771
|
const prompt = withTextContext(request.prompt, attached);
|
|
1673
|
-
|
|
1674
|
-
// The model the turn ran on, in VS Code's own response footer rather than as a line of the conversation.
|
|
1675
|
-
return details ? { details } : {};
|
|
1772
|
+
return runTurnInto(connection, prompt, stream, token, approval);
|
|
1676
1773
|
};
|
|
1677
1774
|
const participants = [];
|
|
1678
1775
|
for (const id of granted.has('defaultChatParticipant') ? [PARTICIPANT_ID, DEFAULT_PARTICIPANT_ID] : [PARTICIPANT_ID]) {
|
|
@@ -1792,7 +1889,17 @@ export async function activate(context) {
|
|
|
1792
1889
|
return { status: 'cancelled' };
|
|
1793
1890
|
response = { ...response, content };
|
|
1794
1891
|
}
|
|
1795
|
-
|
|
1892
|
+
const key = cardKey(connection.id, answer.requestId);
|
|
1893
|
+
answeredByCard.add(key);
|
|
1894
|
+
if (answeredByCard.size > 500)
|
|
1895
|
+
answeredByCard.delete(answeredByCard.values().next().value);
|
|
1896
|
+
try {
|
|
1897
|
+
await connection.session.respond(response);
|
|
1898
|
+
}
|
|
1899
|
+
catch (error) {
|
|
1900
|
+
answeredByCard.delete(key);
|
|
1901
|
+
throw error;
|
|
1902
|
+
}
|
|
1796
1903
|
void vscode.window.setStatusBarMessage(`Volter Harness: ${resolutionSentence(response, request, answer.requestId)}`, 4000);
|
|
1797
1904
|
return { status: 'responded' };
|
|
1798
1905
|
}
|
|
@@ -2124,7 +2231,7 @@ function oneRowPerToolCall(parts) {
|
|
|
2124
2231
|
* between tools is markdown, consecutive text joined into one part. A turn
|
|
2125
2232
|
* looks the same however late the Chat was opened.
|
|
2126
2233
|
*/
|
|
2127
|
-
function historyParts(parts) {
|
|
2234
|
+
function historyParts(parts, granted) {
|
|
2128
2235
|
const out = [];
|
|
2129
2236
|
let text = [];
|
|
2130
2237
|
const flush = () => {
|
|
@@ -2143,15 +2250,21 @@ function historyParts(parts) {
|
|
|
2143
2250
|
invocation.isComplete = part.isComplete;
|
|
2144
2251
|
if (part.isError !== undefined)
|
|
2145
2252
|
invocation.isError = part.isError;
|
|
2146
|
-
|
|
2147
|
-
invocation.toolSpecificData = { commandLine: { original: part.commandLine }, language: 'sh', output: { text: part.resultText ?? '' } };
|
|
2148
|
-
}
|
|
2149
|
-
else if (part.isComplete) {
|
|
2150
|
-
invocation.toolSpecificData = { input: part.inputText ?? '', output: part.resultText ?? '' };
|
|
2151
|
-
}
|
|
2253
|
+
setToolDetails(invocation, part);
|
|
2152
2254
|
out.push(invocation);
|
|
2153
2255
|
continue;
|
|
2154
2256
|
}
|
|
2257
|
+
if (part.part === 'hook') {
|
|
2258
|
+
flush();
|
|
2259
|
+
out.push(hookPart(part));
|
|
2260
|
+
continue;
|
|
2261
|
+
}
|
|
2262
|
+
// A warning and the agent's reasoning are VS Code's own parts in history too, where the host granted them.
|
|
2263
|
+
if ((part.part === 'warning' || part.part === 'thinking') && granted.has('chatParticipantAdditions')) {
|
|
2264
|
+
flush();
|
|
2265
|
+
out.push(part.part === 'warning' ? new vscode.ChatResponseWarningPart(part.value) : new vscode.ChatResponseThinkingProgressPart(part.value));
|
|
2266
|
+
continue;
|
|
2267
|
+
}
|
|
2155
2268
|
const value = historyText(part);
|
|
2156
2269
|
if (value)
|
|
2157
2270
|
text.push(value);
|
|
@@ -2159,6 +2272,11 @@ function historyParts(parts) {
|
|
|
2159
2272
|
flush();
|
|
2160
2273
|
return out.length ? out : [new vscode.ChatResponseMarkdownPart('')];
|
|
2161
2274
|
}
|
|
2275
|
+
/** A conversation that failed, as a history response shows it: VS Code's warning part where the host granted it (the
|
|
2276
|
+
* extension API does not carry a history response's own error), else the words alone. */
|
|
2277
|
+
function failurePart(message, granted) {
|
|
2278
|
+
return granted.has('chatParticipantAdditions') ? new vscode.ChatResponseWarningPart(message) : new vscode.ChatResponseMarkdownPart(message);
|
|
2279
|
+
}
|
|
2162
2280
|
/**
|
|
2163
2281
|
* One text part as the markdown a HISTORY turn carries (tool calls are drawn
|
|
2164
2282
|
* as tool rows by `historyParts`).
|
|
@@ -2172,6 +2290,9 @@ function historyText(part) {
|
|
|
2172
2290
|
return `> ⚠ ${part.value}`;
|
|
2173
2291
|
case 'thinking':
|
|
2174
2292
|
return `> ${part.value}`;
|
|
2293
|
+
case 'hook':
|
|
2294
|
+
// Drawn as VS Code's own hook row by `historyParts`.
|
|
2295
|
+
return '';
|
|
2175
2296
|
case 'codeblock':
|
|
2176
2297
|
return part.value;
|
|
2177
2298
|
case 'reference':
|
|
@@ -2181,6 +2302,14 @@ function historyText(part) {
|
|
|
2181
2302
|
return '';
|
|
2182
2303
|
case 'answers':
|
|
2183
2304
|
return `**${part.title}**\n\n${part.message}`;
|
|
2305
|
+
case 'questions':
|
|
2306
|
+
return `**${part.fallback.title}**\n\n${part.fallback.message}`;
|
|
2307
|
+
case 'resolution':
|
|
2308
|
+
// (a saved card is words, which nothing retires, so its answer is said beneath it)
|
|
2309
|
+
return part.value;
|
|
2310
|
+
case 'unshown':
|
|
2311
|
+
// (`projectReplayTurns` hands these to `unshown`; none reaches a history turn)
|
|
2312
|
+
return '';
|
|
2184
2313
|
}
|
|
2185
2314
|
}
|
|
2186
2315
|
function registerSessions(context, frontends, participant, granted, handler, historyParticipant, controls) {
|
|
@@ -2298,7 +2427,7 @@ function registerSessions(context, frontends, participant, granted, handler, his
|
|
|
2298
2427
|
return {
|
|
2299
2428
|
history: [
|
|
2300
2429
|
new turns.ChatRequestTurn(HISTORY_MARKER_PROMPT, undefined, [], 'supercode', []),
|
|
2301
|
-
new turns.ChatResponseTurn([
|
|
2430
|
+
new turns.ChatResponseTurn([failurePart(message, granted)], {}, 'supercode'),
|
|
2302
2431
|
],
|
|
2303
2432
|
requestHandler: boundHandler,
|
|
2304
2433
|
};
|
|
@@ -2366,6 +2495,10 @@ function registerSessions(context, frontends, participant, granted, handler, his
|
|
|
2366
2495
|
pendingToolNames.add(name);
|
|
2367
2496
|
}
|
|
2368
2497
|
}
|
|
2498
|
+
// The task list is replayed with the history it was built in, and live turns carry on from it; so is the
|
|
2499
|
+
// set of background tasks whose finishing has been drawn.
|
|
2500
|
+
const plan = new PlanTracker();
|
|
2501
|
+
const notices = new Set();
|
|
2369
2502
|
const projected = projectReplayTurns(replay.filter(event => {
|
|
2370
2503
|
if (event.kind !== 'request')
|
|
2371
2504
|
return true;
|
|
@@ -2375,7 +2508,11 @@ function registerSessions(context, frontends, participant, granted, handler, his
|
|
|
2375
2508
|
connectionId: connection.id,
|
|
2376
2509
|
pendingRequest: (id) => connection.session.pendingRequests.get(id) ?? connection.session.resolvedRequests.get(id),
|
|
2377
2510
|
canRespond: Boolean(connection.session.descriptor?.actions.respond && connection.session.permissions.has('approve')),
|
|
2378
|
-
|
|
2511
|
+
plan,
|
|
2512
|
+
notices,
|
|
2513
|
+
unshown: logRecord,
|
|
2514
|
+
}, projectHistory(canonicalHistory, granted, pendingToolNames, plan));
|
|
2515
|
+
connection.plan = plan;
|
|
2379
2516
|
const live = connection.session.descriptor?.turn_state === 'busy' || connection.session.pendingRequests.size > 0;
|
|
2380
2517
|
controls?.rememberIdentity(connection);
|
|
2381
2518
|
if (!live)
|
|
@@ -2401,26 +2538,26 @@ function registerSessions(context, frontends, participant, granted, handler, his
|
|
|
2401
2538
|
// responses can be stale; their command buttons are disabled.
|
|
2402
2539
|
const last = projected.at(-1);
|
|
2403
2540
|
if (last?.role === 'assistant') {
|
|
2404
|
-
activeParts.push(...(projected.pop()?.parts ?? []).map(part => part.part === 'answers'
|
|
2405
|
-
? { part: 'markdown', value: `**${part.title}**\n\n${part.message}` }
|
|
2406
|
-
|
|
2541
|
+
activeParts.push(...(projected.pop()?.parts ?? []).map(part => part.part === 'answers' || part.part === 'questions'
|
|
2542
|
+
? { part: 'markdown', value: `**${(part.part === 'questions' ? part.fallback : part).title}**\n\n${(part.part === 'questions' ? part.fallback : part).message}` }
|
|
2543
|
+
// (its card is words here, so its answer is said beneath it)
|
|
2544
|
+
: part.part === 'resolution' ? { part: 'markdown', value: `\n\n${part.value}\n\n` }
|
|
2545
|
+
: part));
|
|
2407
2546
|
}
|
|
2408
2547
|
}
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
if (connection.session.historyEventsDropped > 0)
|
|
2412
|
-
history.push(new turns.ChatRequestTurn(HISTORY_MARKER_PROMPT, undefined, [], authorId, []), new turns.ChatResponseTurn([new vscode.ChatResponseMarkdownPart(`_${connection.session.historyEventsDropped} earlier runtime events are outside this display snapshot._`)], {}, authorId));
|
|
2548
|
+
// (saved history and retained events past their bounds are left out without a word, as VS Code's own agent
|
|
2549
|
+
// host shows a transcript its harness truncated)
|
|
2413
2550
|
for (const turn of projected) {
|
|
2414
2551
|
if (turn.role === 'user') {
|
|
2415
2552
|
history.push(new turns.ChatRequestTurn(turn.prompt ?? '', undefined, [], authorId, []));
|
|
2416
2553
|
continue;
|
|
2417
2554
|
}
|
|
2418
|
-
history.push(new turns.ChatResponseTurn(historyParts(oneRowPerToolCall(turn.parts ?? [])), {}, authorId));
|
|
2555
|
+
history.push(new turns.ChatResponseTurn(historyParts(oneRowPerToolCall(turn.parts ?? []), granted), {}, authorId));
|
|
2419
2556
|
}
|
|
2420
2557
|
if (connection.session.failure) {
|
|
2421
2558
|
// An explicit runtime marker pairs this notice without presenting
|
|
2422
2559
|
// it as a person's prompt. The native busy state stays unchanged.
|
|
2423
|
-
history.push(new turns.ChatRequestTurn(HISTORY_MARKER_PROMPT, undefined, [], authorId, []), new turns.ChatResponseTurn([
|
|
2560
|
+
history.push(new turns.ChatRequestTurn(HISTORY_MARKER_PROMPT, undefined, [], authorId, []), new turns.ChatResponseTurn([failurePart(opaqueText(connection.session.failure.message, 2048), granted)], {}, authorId));
|
|
2424
2561
|
}
|
|
2425
2562
|
const renderedCursor = connection.session.cursor;
|
|
2426
2563
|
const pendingAtCursor = [...connection.session.pendingRequests.values()];
|
|
@@ -2438,7 +2575,7 @@ function registerSessions(context, frontends, participant, granted, handler, his
|
|
|
2438
2575
|
// already waiting and later ones; Ask leaves them for the person, as before.
|
|
2439
2576
|
const watch = controls && !controls.attached(connection) ? controls.watchApprovals(connection, stream, token) : undefined;
|
|
2440
2577
|
try {
|
|
2441
|
-
const state = { toolInputs: new Map(), toolNames: new Map(), toolTitles: new Map(), toolCommands: new Map(), quiet: watch?.answers };
|
|
2578
|
+
const state = { toolInputs: new Map(), toolNames: new Map(), toolTitles: new Map(), toolCommands: new Map(), toolKinds: new Map(), quiet: watch?.answers };
|
|
2442
2579
|
for (const part of oneRowPerToolCall(activeParts))
|
|
2443
2580
|
writePart(stream, part, state);
|
|
2444
2581
|
writeProjected(stream, projectPendingRequests(pendingAtCursor.filter(request => connection.session.pendingRequests.has(request.id)), {
|
|
@@ -2451,6 +2588,8 @@ function registerSessions(context, frontends, participant, granted, handler, his
|
|
|
2451
2588
|
writeProjected(stream, projectEvent(event, {
|
|
2452
2589
|
connectionId: connection.id,
|
|
2453
2590
|
liveUserMessages: true,
|
|
2591
|
+
plan,
|
|
2592
|
+
notices,
|
|
2454
2593
|
pendingRequest: (id) => connection.session.pendingRequests.get(id) ?? connection.session.resolvedRequests.get(id),
|
|
2455
2594
|
canRespond: Boolean(connection.session.descriptor?.actions.respond && connection.session.permissions.has('approve')),
|
|
2456
2595
|
}), granted, state);
|
|
@@ -2461,7 +2600,13 @@ function registerSessions(context, frontends, participant, granted, handler, his
|
|
|
2461
2600
|
catch (error) {
|
|
2462
2601
|
if (token.isCancellationRequested || error instanceof vscode.CancellationError)
|
|
2463
2602
|
throw error;
|
|
2464
|
-
|
|
2603
|
+
// The response's own error is what VS Code shows, and a re-joined response cannot set one (the
|
|
2604
|
+
// ext host ignores what `activeResponseCallback` returns), so it is the warning part.
|
|
2605
|
+
const message = opaqueText(String(error?.message ?? error), 2048);
|
|
2606
|
+
if (granted.has('chatParticipantAdditions'))
|
|
2607
|
+
stream.warning(message);
|
|
2608
|
+
else
|
|
2609
|
+
stream.markdown(`\n\n${message}\n\n`);
|
|
2465
2610
|
}
|
|
2466
2611
|
finally {
|
|
2467
2612
|
connection.viewReady = connection.session.descriptor?.turn_state !== 'busy' && !connection.session.pendingRequests.size;
|