@volter/supercode-frontend-vscode 0.1.63 → 0.1.65
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 +110 -72
- package/dist/extension.js +245 -104
- package/dist/host-controls.d.ts +0 -1
- package/dist/proposals.d.ts +1 -1
- package/dist/proposals.js +18 -2
- package/dist/session.d.ts +2 -21
- package/dist/session.js +3 -35
- package/dist/view.d.ts +58 -5
- package/dist/view.js +234 -109
- package/package.json +5 -2
package/dist/extension.js
CHANGED
|
@@ -10,8 +10,8 @@ import { createFrontendClient } from './client.js';
|
|
|
10
10
|
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
|
-
import { RequestNotPendingError, VscodeFrontendSession } from './session.js';
|
|
14
|
-
import { acceptedDecisions, opaqueText, projectEvent, projectPendingRequests, projectHistory, projectReplayTurns, HISTORY_MARKER_PROMPT, PlanTracker, resolveParts, resolutionSentence, typedAnswer, } from './view.js';
|
|
13
|
+
import { RequestNotPendingError, TURN_TERMINAL_KINDS, VscodeFrontendSession } from './session.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,43 @@ class SupercodeFrontends {
|
|
|
204
204
|
}
|
|
205
205
|
}
|
|
206
206
|
let nativeInTurnApprovalCards = false;
|
|
207
|
+
/** The host draws a harness notice as its system notification row (`chatSystemNotification`). */
|
|
208
|
+
let systemNotifications = false;
|
|
209
|
+
/** The host retires a confirmation the extension marks answered (`chatConfirmationResolution`). */
|
|
210
|
+
let confirmationResolution = false;
|
|
211
|
+
/** The output channel, for records that are not conversation (`unshown` parts). */
|
|
212
|
+
let outputLog;
|
|
213
|
+
/** Each such record once, however often a history is rebuilt from the same events. */
|
|
214
|
+
const loggedRecords = new Set();
|
|
215
|
+
const logRecord = (value) => {
|
|
216
|
+
if (loggedRecords.has(value))
|
|
217
|
+
return;
|
|
218
|
+
loggedRecords.add(value);
|
|
219
|
+
if (loggedRecords.size > 1000)
|
|
220
|
+
loggedRecords.delete(loggedRecords.values().next().value);
|
|
221
|
+
outputLog?.warn(value);
|
|
222
|
+
};
|
|
223
|
+
/** The requests each response stream drew a card for, so a resolution is said only beneath a card it drew. */
|
|
224
|
+
const drawnRequests = new WeakMap();
|
|
225
|
+
/** Requests answered through their own card in this window (`answerRequest`): such a card shows its answer itself. */
|
|
226
|
+
const answeredByCard = new Set();
|
|
227
|
+
const cardKey = (connectionId, requestId) => JSON.stringify([connectionId, requestId]);
|
|
228
|
+
const drew = (stream, requestId) => {
|
|
229
|
+
const drawn = drawnRequests.get(stream) ?? new Set();
|
|
230
|
+
drawnRequests.set(stream, drawn.add(requestId));
|
|
231
|
+
};
|
|
232
|
+
/** The confirmation cards each response stream drew natively, so one answered elsewhere can be retired. */
|
|
233
|
+
const drawnCards = new WeakMap();
|
|
234
|
+
/** Retires a native card drawn in `stream`, as one answered through itself is; false where the host cannot. */
|
|
235
|
+
function retireCard(stream, requestId) {
|
|
236
|
+
const card = confirmationResolution ? drawnCards.get(stream)?.get(requestId) : undefined;
|
|
237
|
+
if (!card)
|
|
238
|
+
return false;
|
|
239
|
+
const retired = new vscode.ChatResponseConfirmationPart(card.title, card.message, card.data, card.buttons);
|
|
240
|
+
retired.isUsed = true;
|
|
241
|
+
stream.push(retired);
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
207
244
|
/**
|
|
208
245
|
* A harness's questions as VS Code's own carousel, inside the running turn; the person's answers are the
|
|
209
246
|
* elicitation's `{action:'accept', content:{<key>: label | [labels] | text}}`, and Skip is a decline. The answer
|
|
@@ -301,7 +338,7 @@ function writeProjected(stream, projected, granted, state) {
|
|
|
301
338
|
writePart(stream, fallback, state);
|
|
302
339
|
}
|
|
303
340
|
catch {
|
|
304
|
-
stream
|
|
341
|
+
// (a stream that refuses even the stable part has closed; a part's JSON is never conversation)
|
|
305
342
|
}
|
|
306
343
|
}
|
|
307
344
|
}
|
|
@@ -316,6 +353,12 @@ function writePart(stream, part, state) {
|
|
|
316
353
|
case 'progress':
|
|
317
354
|
stream.progress(part.value);
|
|
318
355
|
return;
|
|
356
|
+
case 'notice':
|
|
357
|
+
if (systemNotifications)
|
|
358
|
+
stream.push(new vscode.ChatResponseSystemNotificationPart(part.value, part.icon ? new vscode.ThemeIcon(part.icon) : undefined));
|
|
359
|
+
else
|
|
360
|
+
stream.markdown(`\n\n${part.value}\n\n`);
|
|
361
|
+
return;
|
|
319
362
|
case 'warning':
|
|
320
363
|
stream.warning(part.value);
|
|
321
364
|
return;
|
|
@@ -330,6 +373,7 @@ function writePart(stream, part, state) {
|
|
|
330
373
|
stream.push(hookPart(part));
|
|
331
374
|
return;
|
|
332
375
|
case 'questions':
|
|
376
|
+
drew(stream, part.requestId);
|
|
333
377
|
if (state.carousel)
|
|
334
378
|
askQuestions(stream, part, state.pending, state.cancelled);
|
|
335
379
|
else
|
|
@@ -345,13 +389,26 @@ function writePart(stream, part, state) {
|
|
|
345
389
|
stream.markdown(`\`${part.path}\``);
|
|
346
390
|
return;
|
|
347
391
|
}
|
|
392
|
+
case 'resolution':
|
|
393
|
+
if (!drawnRequests.get(stream)?.has(part.requestId) || answeredByCard.has(cardKey(part.connectionId, part.requestId)))
|
|
394
|
+
return;
|
|
395
|
+
// A native card answered elsewhere retires itself, as Code-OSS's does; the call's row carries the outcome.
|
|
396
|
+
if (retireCard(stream, part.requestId))
|
|
397
|
+
return;
|
|
398
|
+
stream.markdown(`\n\n${part.value}\n\n`);
|
|
399
|
+
return;
|
|
400
|
+
case 'unshown':
|
|
401
|
+
logRecord(part.value);
|
|
402
|
+
return;
|
|
348
403
|
case 'answers':
|
|
349
|
-
if (part.approval && part.answers.length > 0 && state.quiet?.(part.
|
|
404
|
+
if (part.approval && part.answers.length > 0 && state.quiet?.(part.requestId))
|
|
350
405
|
return;
|
|
406
|
+
drew(stream, part.requestId);
|
|
351
407
|
if (part.approval && nativeInTurnApprovalCards && part.answers.length > 0) {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
408
|
+
const card = { title: part.title, message: part.message, data: { kind: 'supercode.inTurnApproval', version: 1, answers: part.answers }, buttons: part.answers.map(answer => answer.label) };
|
|
409
|
+
stream.confirmation(card.title, card.message, card.data, card.buttons);
|
|
410
|
+
const cards = drawnCards.get(stream) ?? new Map();
|
|
411
|
+
drawnCards.set(stream, cards.set(part.requestId, card));
|
|
355
412
|
return;
|
|
356
413
|
}
|
|
357
414
|
// Stable API on purpose: a command button runs INSIDE the live turn,
|
|
@@ -434,12 +491,15 @@ function unavailableMarkdown(reason) {
|
|
|
434
491
|
}
|
|
435
492
|
export async function activate(context) {
|
|
436
493
|
const log = vscode.window.createOutputChannel('Volter Harness', { log: true });
|
|
494
|
+
outputLog = log;
|
|
437
495
|
context.subscriptions.push(log);
|
|
438
496
|
// The HOST decides which proposals this extension got; its own manifest only
|
|
439
497
|
// asks. `context.extension.packageJSON` is the filtered extension
|
|
440
498
|
// description, so this is the grant, not the request.
|
|
441
499
|
const granted = grantedProposals(context.extension.packageJSON);
|
|
442
500
|
log.info(`Volter Harness: proposals requested ${REQUESTED_PROPOSALS.join(', ')}; granted ${granted.size ? [...granted].join(', ') : 'none (stable chat parts only)'}`);
|
|
501
|
+
systemNotifications = granted.has('chatSystemNotification');
|
|
502
|
+
confirmationResolution = granted.has('chatConfirmationResolution');
|
|
443
503
|
// Explicit workbench support is required: stock confirmation clicks send
|
|
444
504
|
// a new prompt. A missing capability keeps the stable in-turn buttons.
|
|
445
505
|
if (granted.has('chatParticipantAdditions')) {
|
|
@@ -770,7 +830,6 @@ export async function activate(context) {
|
|
|
770
830
|
if (state.activeSession !== connection.id)
|
|
771
831
|
throw new Error('The conversation changed before its history could be saved.');
|
|
772
832
|
connection.persistedHistory = state.history;
|
|
773
|
-
connection.historyTruncated = state.historyTruncated;
|
|
774
833
|
savedConnections.set(connection.id, connection);
|
|
775
834
|
};
|
|
776
835
|
const connectManaged = async (state = controls.state) => {
|
|
@@ -1087,7 +1146,6 @@ export async function activate(context) {
|
|
|
1087
1146
|
throw new Error('The host did not restore the requested conversation.');
|
|
1088
1147
|
const connection = await connectManaged(state);
|
|
1089
1148
|
connection.persistedHistory = state.history;
|
|
1090
|
-
connection.historyTruncated = state.historyTruncated;
|
|
1091
1149
|
savedConnections.set(id, connection);
|
|
1092
1150
|
if (active !== connection)
|
|
1093
1151
|
controlsChanged.fire();
|
|
@@ -1347,22 +1405,6 @@ export async function activate(context) {
|
|
|
1347
1405
|
// prevents the submit command from reaching session materialization at all.
|
|
1348
1406
|
context.subscriptions.push(vscode.commands.registerCommand('supercode.frontend.sendRefusal', () => draftChoiceRefusal()
|
|
1349
1407
|
?? (controls?.state.setup?.ready === false ? 'Finish setting up your coding agent before sending. Your draft is saved.' : undefined)));
|
|
1350
|
-
// A background task the chat named (sayTasks in runTurnInto), stopped through the harness's own control.
|
|
1351
|
-
context.subscriptions.push(vscode.commands.registerCommand('supercode.frontend.stopBackgroundTask', async (connectionId, taskId) => {
|
|
1352
|
-
const connection = frontends.get(connectionId);
|
|
1353
|
-
const task = connection?.session.backgroundTasks.find(candidate => candidate.id === taskId);
|
|
1354
|
-
if (!connection || !task) {
|
|
1355
|
-
void vscode.window.showInformationMessage('That background task is no longer running.');
|
|
1356
|
-
return;
|
|
1357
|
-
}
|
|
1358
|
-
try {
|
|
1359
|
-
const stopped = await connection.session.stopTask(taskId);
|
|
1360
|
-
void vscode.window.showInformationMessage(stopped ? `Stopped ${task.description || taskId}.` : `The harness did not stop ${task.description || taskId}.`);
|
|
1361
|
-
}
|
|
1362
|
-
catch (error) {
|
|
1363
|
-
void vscode.window.showErrorMessage(`Could not stop ${task.description || taskId}: ${String(error?.message ?? error)}`);
|
|
1364
|
-
}
|
|
1365
|
-
}));
|
|
1366
1408
|
// The host's native focus event is authoritative even when a cached model
|
|
1367
1409
|
// skips content resolution. Untitled/non-harness focus never starts a runtime.
|
|
1368
1410
|
context.subscriptions.push(vscode.commands.registerCommand('supercode.frontend.focusSession', (resource) => {
|
|
@@ -1547,8 +1589,8 @@ export async function activate(context) {
|
|
|
1547
1589
|
// never both answer the same request (the second answer would be refused and noted as a failure).
|
|
1548
1590
|
const answered = answeredRequests.get(connection.id) ?? new Set();
|
|
1549
1591
|
answeredRequests.set(connection.id, answered);
|
|
1550
|
-
// Requests whose card
|
|
1551
|
-
const
|
|
1592
|
+
// Requests whose card an automatic answer that failed or did not settle has drawn again: once each.
|
|
1593
|
+
const redrawn = new Set();
|
|
1552
1594
|
const answers = (requestId) => {
|
|
1553
1595
|
if (approval() !== 'autoApprove' || token.isCancellationRequested || !canAnswer(connection))
|
|
1554
1596
|
return false;
|
|
@@ -1559,7 +1601,6 @@ export async function activate(context) {
|
|
|
1559
1601
|
const decisions = acceptedDecisions(pending);
|
|
1560
1602
|
if (decisions && !decisions.includes('allow'))
|
|
1561
1603
|
return false;
|
|
1562
|
-
unshown.add(requestId);
|
|
1563
1604
|
return true;
|
|
1564
1605
|
};
|
|
1565
1606
|
const sweep = () => {
|
|
@@ -1572,45 +1613,41 @@ export async function activate(context) {
|
|
|
1572
1613
|
if (decisions && !decisions.includes('allow'))
|
|
1573
1614
|
continue;
|
|
1574
1615
|
answered.add(pending.id);
|
|
1575
|
-
// Its card is already in the stream (the event that raised it was written first), so say it was answered.
|
|
1576
1616
|
const payload = pending.payload && typeof pending.payload === 'object' && !Array.isArray(pending.payload) ? pending.payload : {};
|
|
1577
1617
|
const named = [payload.title, payload.tool, payload.tool_name].find((value) => typeof value === 'string');
|
|
1578
|
-
// The answer can settle after the turn's response has closed, when writing to it throws: the note is then dropped.
|
|
1579
|
-
const note = (text) => { try {
|
|
1580
|
-
stream.markdown(text);
|
|
1581
|
-
}
|
|
1582
|
-
catch {
|
|
1583
|
-
log.info(text.trim());
|
|
1584
|
-
} };
|
|
1585
1618
|
const label = opaqueText(named ?? `request #${pending.id}`, 200);
|
|
1586
|
-
//
|
|
1587
|
-
//
|
|
1619
|
+
// An automatic answer that fails or does not settle leaves the approval waiting: its card is drawn (again,
|
|
1620
|
+
// when it was already shown) as the one that still needs an answer, once: the settle timer and a later
|
|
1621
|
+
// failure of the same answer draw it one time between them. With the response closed, writing to it throws,
|
|
1622
|
+
// and the person is told an approval waits.
|
|
1588
1623
|
const showCard = () => {
|
|
1589
|
-
if (
|
|
1624
|
+
if (redrawn.has(pending.id) || !connection.session.pendingRequests.has(pending.id))
|
|
1590
1625
|
return;
|
|
1626
|
+
redrawn.add(pending.id);
|
|
1591
1627
|
try {
|
|
1592
|
-
|
|
1628
|
+
const state = { toolInputs: new Map(), toolNames: new Map(), toolTitles: new Map(), toolCommands: new Map(), toolKinds: new Map() };
|
|
1629
|
+
for (const part of resolveParts(projectPendingRequests([pending], {
|
|
1593
1630
|
connectionId: connection.id,
|
|
1594
|
-
canRespond: connection
|
|
1595
|
-
}), granted
|
|
1631
|
+
canRespond: canAnswer(connection),
|
|
1632
|
+
}), granted))
|
|
1633
|
+
writePart(stream, part, state);
|
|
1596
1634
|
}
|
|
1597
1635
|
catch {
|
|
1598
1636
|
void vscode.window.showWarningMessage(`${connection.label}: ${label} is waiting for approval. Open its chat to answer it.`);
|
|
1599
1637
|
}
|
|
1600
1638
|
};
|
|
1639
|
+
// An approval answered automatically adds no line of its own: Code-OSS's own agent host draws a call its Auto
|
|
1640
|
+
// approve answered as its plain row (`sessionPermissions.ts` `createToolReadyAction`, `confirmed: NotNeeded`).
|
|
1641
|
+
// A card it answered after the card was shown gets its resolution.
|
|
1601
1642
|
const unsettled = setTimeout(() => {
|
|
1602
|
-
|
|
1643
|
+
log.warn(`Auto approve has had no answer for ${label} for ${AUTO_ANSWER_SETTLE_MS / 1000} s.`);
|
|
1603
1644
|
showCard();
|
|
1604
1645
|
}, AUTO_ANSWER_SETTLE_MS);
|
|
1605
1646
|
void connection.session.respond({ kind: 'approval', request_id: pending.id, decision: 'allow' }).then(() => {
|
|
1606
1647
|
clearTimeout(unsettled);
|
|
1607
|
-
// A card that was never shown needs no note: its resolution line says it was allowed.
|
|
1608
|
-
if (unshown.has(pending.id))
|
|
1609
|
-
return;
|
|
1610
|
-
note(`\n_auto-approved: ${label}_\n`);
|
|
1611
1648
|
}, error => {
|
|
1612
1649
|
clearTimeout(unsettled);
|
|
1613
|
-
|
|
1650
|
+
log.warn(`Auto approve could not answer ${label}: ${String(error.message || error)}`);
|
|
1614
1651
|
showCard();
|
|
1615
1652
|
});
|
|
1616
1653
|
}
|
|
@@ -1640,8 +1677,10 @@ export async function activate(context) {
|
|
|
1640
1677
|
const projection = {
|
|
1641
1678
|
connectionId: connection.id,
|
|
1642
1679
|
pendingRequest: (requestId) => connection.session.pendingRequests.get(requestId) ?? connection.session.resolvedRequests.get(requestId),
|
|
1643
|
-
|
|
1680
|
+
// (as history: a control appears only where the runtime offers `respond` and this attachment may approve)
|
|
1681
|
+
canRespond: canAnswer(connection),
|
|
1644
1682
|
plan: connection.plan ??= new PlanTracker(),
|
|
1683
|
+
notices: new Set(),
|
|
1645
1684
|
};
|
|
1646
1685
|
const cancellation = token.onCancellationRequested(() => {
|
|
1647
1686
|
void stopConnection(connection).catch(() => {
|
|
@@ -1651,27 +1690,8 @@ export async function activate(context) {
|
|
|
1651
1690
|
});
|
|
1652
1691
|
const watch = watchApprovals(connection, stream, token, approval);
|
|
1653
1692
|
state.quiet = watch.answers;
|
|
1654
|
-
//
|
|
1655
|
-
|
|
1656
|
-
// way to stop it.
|
|
1657
|
-
let saidTasks = '';
|
|
1658
|
-
const sayTasks = () => {
|
|
1659
|
-
const tasks = connection.session.backgroundTasks;
|
|
1660
|
-
const key = tasks.map(task => task.id).join(',');
|
|
1661
|
-
if (key === saidTasks)
|
|
1662
|
-
return;
|
|
1663
|
-
saidTasks = key;
|
|
1664
|
-
if (!tasks.length) {
|
|
1665
|
-
stream.markdown('\n\n_No background task is running any more._\n\n');
|
|
1666
|
-
return;
|
|
1667
|
-
}
|
|
1668
|
-
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`);
|
|
1669
|
-
for (const task of tasks) {
|
|
1670
|
-
stream.button({ command: 'supercode.frontend.stopBackgroundTask', title: `Stop ${opaqueText(task.description || task.id, 80)}`, arguments: [connection.id, task.id] });
|
|
1671
|
-
}
|
|
1672
|
-
};
|
|
1673
|
-
sayTasks();
|
|
1674
|
-
const unwatchTasks = connection.session.onChange(sayTasks);
|
|
1693
|
+
// How the turn ended when it did not succeed: the response's own error, as VS Code's agent host ends one.
|
|
1694
|
+
let outcome;
|
|
1675
1695
|
let completed = false;
|
|
1676
1696
|
let turnStarted = false;
|
|
1677
1697
|
try {
|
|
@@ -1680,14 +1700,18 @@ export async function activate(context) {
|
|
|
1680
1700
|
// events arrive; no save refusal submits input again or aborts its turn.
|
|
1681
1701
|
throwIfCancelled();
|
|
1682
1702
|
await connection.session.runTurn(prompt, (event) => {
|
|
1683
|
-
// A turn the person stopped is marked stopped by VS Code itself;
|
|
1703
|
+
// A turn the person stopped is marked stopped by VS Code itself; any other end is this response's result.
|
|
1684
1704
|
if (event.kind === 'turn_interrupted' && token.isCancellationRequested)
|
|
1685
1705
|
return;
|
|
1686
1706
|
// Native submit publishes its initial user echo before `turn_started`.
|
|
1687
1707
|
// Subsequent input belongs in the response while this turn is visible.
|
|
1688
1708
|
if (event.kind === 'turn_started')
|
|
1689
1709
|
turnStarted = true;
|
|
1690
|
-
|
|
1710
|
+
const ended = turnOutcome(event);
|
|
1711
|
+
if (ended)
|
|
1712
|
+
outcome = ended;
|
|
1713
|
+
else
|
|
1714
|
+
writeProjected(stream, projectEvent(event, { ...projection, liveUserMessages: turnStarted }), granted, state);
|
|
1691
1715
|
watch.sweep();
|
|
1692
1716
|
rememberIdentity(connection);
|
|
1693
1717
|
}, { throwIfCancelled, onSubmitted: () => {
|
|
@@ -1696,7 +1720,6 @@ export async function activate(context) {
|
|
|
1696
1720
|
completed = true;
|
|
1697
1721
|
}
|
|
1698
1722
|
finally {
|
|
1699
|
-
unwatchTasks();
|
|
1700
1723
|
watch.dispose();
|
|
1701
1724
|
cancellation.dispose();
|
|
1702
1725
|
await finishIdentitySave(connection);
|
|
@@ -1707,7 +1730,14 @@ export async function activate(context) {
|
|
|
1707
1730
|
const effective = active ? controls?.state.effective : undefined;
|
|
1708
1731
|
const ran = effective?.modelName || (active ? controls?.state.actualModel : undefined) || connection.session.descriptor?.model;
|
|
1709
1732
|
const effort = effective?.effort || controls?.state.sessions.find(session => session.id === connection.id)?.selection.effort;
|
|
1710
|
-
|
|
1733
|
+
const details = completed && ran && !token.isCancellationRequested ? `${opaqueText(ran, 120)}${effort ? ` · ${opaqueText(effort, 24)}` : ''}` : undefined;
|
|
1734
|
+
// (an interruption is the expected, warning-level error: `isExpectedError` and `level` are
|
|
1735
|
+
// `chatParticipantPrivate`'s, which the ext host checks for `isExpectedError`, so a host without it gets the message)
|
|
1736
|
+
const errorDetails = outcome && (outcome.interrupted && granted.has('chatParticipantPrivate')
|
|
1737
|
+
? { message: outcome.message, isExpectedError: true, level: 1 /* ChatErrorLevel.Warning */ }
|
|
1738
|
+
: { message: outcome.message });
|
|
1739
|
+
// The model the turn ran on, in VS Code's own response footer rather than as a line of the conversation.
|
|
1740
|
+
return { ...(details ? { details } : {}), ...(errorDetails ? { errorDetails } : {}) };
|
|
1711
1741
|
};
|
|
1712
1742
|
const handler = async (request, chatContext, stream, token) => {
|
|
1713
1743
|
await applying;
|
|
@@ -1720,9 +1750,10 @@ export async function activate(context) {
|
|
|
1720
1750
|
// thrown as is: the workbench writes a handler's error into the chat
|
|
1721
1751
|
throw new Error(`${refusal} Nothing was sent; send it again in a new chat.`);
|
|
1722
1752
|
}
|
|
1753
|
+
// A request the handler cannot run is refused as VS Code refuses one: the request's own error (thrown, which
|
|
1754
|
+
// the workbench writes as the response's errorDetails), not an answer in the transcript.
|
|
1723
1755
|
if (controls?.state.setup && !controls.state.setup.ready) {
|
|
1724
|
-
|
|
1725
|
-
return {};
|
|
1756
|
+
throw new Error('Sign in before sending a message. Use **New Chat: Choose Agent** to get started.');
|
|
1726
1757
|
}
|
|
1727
1758
|
const connection = requestedResource
|
|
1728
1759
|
? await connectionFor(requestedResource)
|
|
@@ -1734,10 +1765,8 @@ export async function activate(context) {
|
|
|
1734
1765
|
throw new Error('This conversation has fixed model settings. Start a new chat to choose different settings; no message was sent.');
|
|
1735
1766
|
}
|
|
1736
1767
|
}
|
|
1737
|
-
if (!connection)
|
|
1738
|
-
|
|
1739
|
-
return {};
|
|
1740
|
-
}
|
|
1768
|
+
if (!connection)
|
|
1769
|
+
throw new Error(unavailableMarkdown(frontends.unavailable));
|
|
1741
1770
|
const pending = pendingRequestOf(connection);
|
|
1742
1771
|
// A message is never taken as an approval: one sent while an approval waits would otherwise allow the tool and
|
|
1743
1772
|
// never reach the agent. It is refused, with what to do, like a send during any running turn.
|
|
@@ -1769,9 +1798,7 @@ export async function activate(context) {
|
|
|
1769
1798
|
attached.push({ name: uri.toString(), text: document.getText(location?.range) });
|
|
1770
1799
|
}
|
|
1771
1800
|
const prompt = withTextContext(request.prompt, attached);
|
|
1772
|
-
|
|
1773
|
-
// The model the turn ran on, in VS Code's own response footer rather than as a line of the conversation.
|
|
1774
|
-
return details ? { details } : {};
|
|
1801
|
+
return runTurnInto(connection, prompt, stream, token, approval);
|
|
1775
1802
|
};
|
|
1776
1803
|
const participants = [];
|
|
1777
1804
|
for (const id of granted.has('defaultChatParticipant') ? [PARTICIPANT_ID, DEFAULT_PARTICIPANT_ID] : [PARTICIPANT_ID]) {
|
|
@@ -1891,7 +1918,17 @@ export async function activate(context) {
|
|
|
1891
1918
|
return { status: 'cancelled' };
|
|
1892
1919
|
response = { ...response, content };
|
|
1893
1920
|
}
|
|
1894
|
-
|
|
1921
|
+
const key = cardKey(connection.id, answer.requestId);
|
|
1922
|
+
answeredByCard.add(key);
|
|
1923
|
+
if (answeredByCard.size > 500)
|
|
1924
|
+
answeredByCard.delete(answeredByCard.values().next().value);
|
|
1925
|
+
try {
|
|
1926
|
+
await connection.session.respond(response);
|
|
1927
|
+
}
|
|
1928
|
+
catch (error) {
|
|
1929
|
+
answeredByCard.delete(key);
|
|
1930
|
+
throw error;
|
|
1931
|
+
}
|
|
1895
1932
|
void vscode.window.setStatusBarMessage(`Volter Harness: ${resolutionSentence(response, request, answer.requestId)}`, 4000);
|
|
1896
1933
|
return { status: 'responded' };
|
|
1897
1934
|
}
|
|
@@ -2223,7 +2260,7 @@ function oneRowPerToolCall(parts) {
|
|
|
2223
2260
|
* between tools is markdown, consecutive text joined into one part. A turn
|
|
2224
2261
|
* looks the same however late the Chat was opened.
|
|
2225
2262
|
*/
|
|
2226
|
-
function historyParts(parts) {
|
|
2263
|
+
function historyParts(parts, granted) {
|
|
2227
2264
|
const out = [];
|
|
2228
2265
|
let text = [];
|
|
2229
2266
|
const flush = () => {
|
|
@@ -2251,6 +2288,18 @@ function historyParts(parts) {
|
|
|
2251
2288
|
out.push(hookPart(part));
|
|
2252
2289
|
continue;
|
|
2253
2290
|
}
|
|
2291
|
+
// A harness notice is Code-OSS's system notification row in history too, where the host has it.
|
|
2292
|
+
if (part.part === 'notice' && granted.has('chatSystemNotification')) {
|
|
2293
|
+
flush();
|
|
2294
|
+
out.push(new vscode.ChatResponseSystemNotificationPart(part.value, part.icon ? new vscode.ThemeIcon(part.icon) : undefined));
|
|
2295
|
+
continue;
|
|
2296
|
+
}
|
|
2297
|
+
// A warning and the agent's reasoning are VS Code's own parts in history too, where the host granted them.
|
|
2298
|
+
if ((part.part === 'warning' || part.part === 'thinking') && granted.has('chatParticipantAdditions')) {
|
|
2299
|
+
flush();
|
|
2300
|
+
out.push(part.part === 'warning' ? new vscode.ChatResponseWarningPart(part.value) : new vscode.ChatResponseThinkingProgressPart(part.value));
|
|
2301
|
+
continue;
|
|
2302
|
+
}
|
|
2254
2303
|
const value = historyText(part);
|
|
2255
2304
|
if (value)
|
|
2256
2305
|
text.push(value);
|
|
@@ -2258,6 +2307,30 @@ function historyParts(parts) {
|
|
|
2258
2307
|
flush();
|
|
2259
2308
|
return out.length ? out : [new vscode.ChatResponseMarkdownPart('')];
|
|
2260
2309
|
}
|
|
2310
|
+
/** How a turn that did not succeed ends its response, as VS Code's agent host ends one (`_getTurnErrorDetails`). */
|
|
2311
|
+
function outcomeErrorDetails(outcome, granted) {
|
|
2312
|
+
// (an interruption is the expected, warning-level error: `isExpectedError` and `level` are
|
|
2313
|
+
// `chatParticipantPrivate`'s, which the ext host checks for `isExpectedError`, so a host without it gets the message)
|
|
2314
|
+
return outcome.interrupted && granted.has('chatParticipantPrivate')
|
|
2315
|
+
? { message: outcome.message, isExpectedError: true, level: 1 /* ChatErrorLevel.Warning */ }
|
|
2316
|
+
: { message: outcome.message };
|
|
2317
|
+
}
|
|
2318
|
+
/** A conversation that failed, as a history response shows it: the response's own error where the host keeps a saved
|
|
2319
|
+
* response's result (`chatSessionResponses`), as Code-OSS shows a session that could not be loaded; else VS Code's
|
|
2320
|
+
* warning part where the host granted it, else the words alone. */
|
|
2321
|
+
function failureResponse(message, granted) {
|
|
2322
|
+
if (granted.has('chatSessionResponses'))
|
|
2323
|
+
return { parts: [], result: { errorDetails: { message } } };
|
|
2324
|
+
return { parts: [granted.has('chatParticipantAdditions') ? new vscode.ChatResponseWarningPart(message) : new vscode.ChatResponseMarkdownPart(message)], result: {} };
|
|
2325
|
+
}
|
|
2326
|
+
/** The request a failure's response answers: the marker turn, which Code-OSS anchors as the system-initiated request
|
|
2327
|
+
* "Couldn't open session" (`agentHostSessionHandler.ts` `sessionLoadFailedLabel`) where the host keeps that label. */
|
|
2328
|
+
function failureRequest(turns, participant, granted) {
|
|
2329
|
+
const request = new turns.ChatRequestTurn(HISTORY_MARKER_PROMPT, undefined, [], participant, []);
|
|
2330
|
+
if (granted.has('chatSessionResponses'))
|
|
2331
|
+
request.systemInitiatedLabel = "Couldn't open session";
|
|
2332
|
+
return request;
|
|
2333
|
+
}
|
|
2261
2334
|
/**
|
|
2262
2335
|
* One text part as the markdown a HISTORY turn carries (tool calls are drawn
|
|
2263
2336
|
* as tool rows by `historyParts`).
|
|
@@ -2266,6 +2339,7 @@ function historyText(part) {
|
|
|
2266
2339
|
switch (part.part) {
|
|
2267
2340
|
case 'markdown':
|
|
2268
2341
|
case 'progress':
|
|
2342
|
+
case 'notice':
|
|
2269
2343
|
return part.value;
|
|
2270
2344
|
case 'warning':
|
|
2271
2345
|
return `> ⚠ ${part.value}`;
|
|
@@ -2285,6 +2359,12 @@ function historyText(part) {
|
|
|
2285
2359
|
return `**${part.title}**\n\n${part.message}`;
|
|
2286
2360
|
case 'questions':
|
|
2287
2361
|
return `**${part.fallback.title}**\n\n${part.fallback.message}`;
|
|
2362
|
+
case 'resolution':
|
|
2363
|
+
// (a saved card is words, which nothing retires, so its answer is said beneath it)
|
|
2364
|
+
return part.value;
|
|
2365
|
+
case 'unshown':
|
|
2366
|
+
// (`projectReplayTurns` hands these to `unshown`; none reaches a history turn)
|
|
2367
|
+
return '';
|
|
2288
2368
|
}
|
|
2289
2369
|
}
|
|
2290
2370
|
function registerSessions(context, frontends, participant, granted, handler, historyParticipant, controls) {
|
|
@@ -2399,10 +2479,11 @@ function registerSessions(context, frontends, participant, granted, handler, his
|
|
|
2399
2479
|
failedResources.set(resource.authority, message);
|
|
2400
2480
|
refreshItems();
|
|
2401
2481
|
}
|
|
2482
|
+
const failure = failureResponse(message, granted);
|
|
2402
2483
|
return {
|
|
2403
2484
|
history: [
|
|
2404
|
-
|
|
2405
|
-
new turns.ChatResponseTurn(
|
|
2485
|
+
failureRequest(turns, 'supercode', granted),
|
|
2486
|
+
new turns.ChatResponseTurn(failure.parts, failure.result, 'supercode'),
|
|
2406
2487
|
],
|
|
2407
2488
|
requestHandler: boundHandler,
|
|
2408
2489
|
};
|
|
@@ -2470,8 +2551,10 @@ function registerSessions(context, frontends, participant, granted, handler, his
|
|
|
2470
2551
|
pendingToolNames.add(name);
|
|
2471
2552
|
}
|
|
2472
2553
|
}
|
|
2473
|
-
// The task list is replayed with the history it was built in, and live turns carry on from it
|
|
2554
|
+
// The task list is replayed with the history it was built in, and live turns carry on from it; so is the
|
|
2555
|
+
// set of background tasks whose finishing has been drawn.
|
|
2474
2556
|
const plan = new PlanTracker();
|
|
2557
|
+
const notices = new Set();
|
|
2475
2558
|
const projected = projectReplayTurns(replay.filter(event => {
|
|
2476
2559
|
if (event.kind !== 'request')
|
|
2477
2560
|
return true;
|
|
@@ -2482,6 +2565,8 @@ function registerSessions(context, frontends, participant, granted, handler, his
|
|
|
2482
2565
|
pendingRequest: (id) => connection.session.pendingRequests.get(id) ?? connection.session.resolvedRequests.get(id),
|
|
2483
2566
|
canRespond: Boolean(connection.session.descriptor?.actions.respond && connection.session.permissions.has('approve')),
|
|
2484
2567
|
plan,
|
|
2568
|
+
notices,
|
|
2569
|
+
unshown: logRecord,
|
|
2485
2570
|
}, projectHistory(canonicalHistory, granted, pendingToolNames, plan));
|
|
2486
2571
|
connection.plan = plan;
|
|
2487
2572
|
const live = connection.session.descriptor?.turn_state === 'busy' || connection.session.pendingRequests.size > 0;
|
|
@@ -2489,6 +2574,7 @@ function registerSessions(context, frontends, participant, granted, handler, his
|
|
|
2489
2574
|
if (!live)
|
|
2490
2575
|
void controls?.finishIdentitySave(connection);
|
|
2491
2576
|
const activeParts = [];
|
|
2577
|
+
let heldFailure;
|
|
2492
2578
|
if (live) {
|
|
2493
2579
|
// Standalone requests have no user turn. Give their live response
|
|
2494
2580
|
// an explicit runtime marker instead of reopening an old answer.
|
|
@@ -2509,26 +2595,30 @@ function registerSessions(context, frontends, participant, granted, handler, his
|
|
|
2509
2595
|
// responses can be stale; their command buttons are disabled.
|
|
2510
2596
|
const last = projected.at(-1);
|
|
2511
2597
|
if (last?.role === 'assistant') {
|
|
2598
|
+
// (a failure that ended that turn stays held for the live response, as it was in history)
|
|
2599
|
+
heldFailure = last.errorEvent;
|
|
2512
2600
|
activeParts.push(...(projected.pop()?.parts ?? []).map(part => part.part === 'answers' || part.part === 'questions'
|
|
2513
2601
|
? { part: 'markdown', value: `**${(part.part === 'questions' ? part.fallback : part).title}**\n\n${(part.part === 'questions' ? part.fallback : part).message}` }
|
|
2514
|
-
|
|
2602
|
+
// (its card is words here, so its answer is said beneath it)
|
|
2603
|
+
: part.part === 'resolution' ? { part: 'markdown', value: `\n\n${part.value}\n\n` }
|
|
2604
|
+
: part));
|
|
2515
2605
|
}
|
|
2516
2606
|
}
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
if (connection.session.historyEventsDropped > 0)
|
|
2520
|
-
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));
|
|
2607
|
+
// (saved history and retained events past their bounds are left out without a word, as VS Code's own agent
|
|
2608
|
+
// host shows a transcript its harness truncated)
|
|
2521
2609
|
for (const turn of projected) {
|
|
2522
2610
|
if (turn.role === 'user') {
|
|
2523
2611
|
history.push(new turns.ChatRequestTurn(turn.prompt ?? '', undefined, [], authorId, []));
|
|
2524
2612
|
continue;
|
|
2525
2613
|
}
|
|
2526
|
-
|
|
2614
|
+
// (a turn that failed keeps its error as the saved response's own, where the host shows one)
|
|
2615
|
+
history.push(new turns.ChatResponseTurn(historyParts(oneRowPerToolCall(turn.parts ?? []), granted), turn.error ? { errorDetails: outcomeErrorDetails(turn.error, granted) } : {}, authorId));
|
|
2527
2616
|
}
|
|
2528
2617
|
if (connection.session.failure) {
|
|
2529
2618
|
// An explicit runtime marker pairs this notice without presenting
|
|
2530
2619
|
// it as a person's prompt. The native busy state stays unchanged.
|
|
2531
|
-
|
|
2620
|
+
const failure = failureResponse(opaqueText(connection.session.failure.message, 2048), granted);
|
|
2621
|
+
history.push(failureRequest(turns, authorId, granted), new turns.ChatResponseTurn(failure.parts, failure.result, authorId));
|
|
2532
2622
|
}
|
|
2533
2623
|
const renderedCursor = connection.session.cursor;
|
|
2534
2624
|
const pendingAtCursor = [...connection.session.pendingRequests.values()];
|
|
@@ -2545,8 +2635,31 @@ function registerSessions(context, frontends, participant, granted, handler, his
|
|
|
2545
2635
|
// answered here under the conversation's mode (t_39dac582): Auto approve answers the cards
|
|
2546
2636
|
// already waiting and later ones; Ask leaves them for the person, as before.
|
|
2547
2637
|
const watch = controls && !controls.attached(connection) ? controls.watchApprovals(connection, stream, token) : undefined;
|
|
2638
|
+
// Where the host keeps a re-joined response's result and answers its questions (`chatSessionResponses`),
|
|
2639
|
+
// how the turn it ends on did not succeed is that result, as on a turn this window runs, and its
|
|
2640
|
+
// questions are the carousel. It follows to idle, as it always did, so an earlier turn's failure it
|
|
2641
|
+
// passes is still drawn, as its warning part.
|
|
2642
|
+
const results = granted.has('chatSessionResponses');
|
|
2643
|
+
let failure = results ? heldFailure : undefined;
|
|
2644
|
+
let result;
|
|
2645
|
+
// Draws the held failure where it stood, once anything follows it, as history draws one.
|
|
2646
|
+
let release = () => { };
|
|
2548
2647
|
try {
|
|
2549
|
-
const state = { toolInputs: new Map(), toolNames: new Map(), toolTitles: new Map(), toolCommands: new Map(), toolKinds: new Map(), quiet: watch?.answers
|
|
2648
|
+
const state = { toolInputs: new Map(), toolNames: new Map(), toolTitles: new Map(), toolCommands: new Map(), toolKinds: new Map(), quiet: watch?.answers,
|
|
2649
|
+
...(results ? { carousel: true, pending: (requestId) => connection.session.pendingRequests.has(requestId), cancelled: () => token.isCancellationRequested } : {}) };
|
|
2650
|
+
const project = (event) => projectEvent(event, {
|
|
2651
|
+
connectionId: connection.id,
|
|
2652
|
+
liveUserMessages: true,
|
|
2653
|
+
plan,
|
|
2654
|
+
notices,
|
|
2655
|
+
pendingRequest: (id) => connection.session.pendingRequests.get(id) ?? connection.session.resolvedRequests.get(id),
|
|
2656
|
+
canRespond: Boolean(connection.session.descriptor?.actions.respond && connection.session.permissions.has('approve')),
|
|
2657
|
+
});
|
|
2658
|
+
release = () => {
|
|
2659
|
+
if (failure)
|
|
2660
|
+
writeProjected(stream, project(failure), granted, state);
|
|
2661
|
+
failure = undefined;
|
|
2662
|
+
};
|
|
2550
2663
|
for (const part of oneRowPerToolCall(activeParts))
|
|
2551
2664
|
writePart(stream, part, state);
|
|
2552
2665
|
writeProjected(stream, projectPendingRequests(pendingAtCursor.filter(request => connection.session.pendingRequests.has(request.id)), {
|
|
@@ -2556,27 +2669,55 @@ function registerSessions(context, frontends, participant, granted, handler, his
|
|
|
2556
2669
|
watch?.sweep();
|
|
2557
2670
|
controls?.rememberIdentity(connection);
|
|
2558
2671
|
await observeActiveResponse(connection.session, token, (event) => {
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2672
|
+
// (a failure the next turn follows is that turn's past: drawn where it stood as soon as the next
|
|
2673
|
+
// turn starts or anything is drawn after it, so neither a later Stop nor a disconnect makes it
|
|
2674
|
+
// this response's error)
|
|
2675
|
+
const terminal = TURN_TERMINAL_KINDS.includes(event.kind);
|
|
2676
|
+
if (event.kind === 'turn_started' || terminal)
|
|
2677
|
+
release();
|
|
2678
|
+
if (results && terminal) {
|
|
2679
|
+
failure = turnOutcome(event) && !token.isCancellationRequested ? event : undefined;
|
|
2680
|
+
}
|
|
2681
|
+
else {
|
|
2682
|
+
const projected = project(event);
|
|
2683
|
+
if (resolveParts(projected, granted).some(part => part.part !== 'unshown'))
|
|
2684
|
+
release();
|
|
2685
|
+
writeProjected(stream, projected, granted, state);
|
|
2686
|
+
}
|
|
2566
2687
|
watch?.sweep();
|
|
2567
2688
|
controls?.rememberIdentity(connection);
|
|
2568
2689
|
}, () => { connection.viewReady = true; }, renderedCursor);
|
|
2690
|
+
const outcome = failure && turnOutcome(failure);
|
|
2691
|
+
if (outcome)
|
|
2692
|
+
result = { errorDetails: outcomeErrorDetails(outcome, granted) };
|
|
2569
2693
|
}
|
|
2570
2694
|
catch (error) {
|
|
2571
2695
|
if (token.isCancellationRequested || error instanceof vscode.CancellationError)
|
|
2572
2696
|
throw error;
|
|
2573
|
-
|
|
2697
|
+
// The response's own error is what VS Code shows; a host that does not keep a re-joined
|
|
2698
|
+
// response's result gets the warning part.
|
|
2699
|
+
const message = opaqueText(String(error?.message ?? error), 2048);
|
|
2700
|
+
// (a failure held when the connection drops is drawn; the drop is the result)
|
|
2701
|
+
if (results) {
|
|
2702
|
+
try {
|
|
2703
|
+
release();
|
|
2704
|
+
}
|
|
2705
|
+
catch {
|
|
2706
|
+
// (a stream that refuses a part has closed)
|
|
2707
|
+
}
|
|
2708
|
+
result = { errorDetails: { message } };
|
|
2709
|
+
}
|
|
2710
|
+
else if (granted.has('chatParticipantAdditions'))
|
|
2711
|
+
stream.warning(message);
|
|
2712
|
+
else
|
|
2713
|
+
stream.markdown(`\n\n${message}\n\n`);
|
|
2574
2714
|
}
|
|
2575
2715
|
finally {
|
|
2576
2716
|
connection.viewReady = connection.session.descriptor?.turn_state !== 'busy' && !connection.session.pendingRequests.size;
|
|
2577
2717
|
watch?.dispose();
|
|
2578
2718
|
await controls?.finishIdentitySave(connection);
|
|
2579
2719
|
}
|
|
2720
|
+
return result;
|
|
2580
2721
|
}
|
|
2581
2722
|
: undefined,
|
|
2582
2723
|
};
|