@volter/supercode-frontend-vscode 0.1.63 → 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 +85 -66
- package/dist/extension.js +127 -91
- 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 +42 -5
- package/dist/view.js +179 -107
- 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, PlanTracker, 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,27 @@ 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
|
+
};
|
|
207
228
|
/**
|
|
208
229
|
* A harness's questions as VS Code's own carousel, inside the running turn; the person's answers are the
|
|
209
230
|
* elicitation's `{action:'accept', content:{<key>: label | [labels] | text}}`, and Skip is a decline. The answer
|
|
@@ -301,7 +322,7 @@ function writeProjected(stream, projected, granted, state) {
|
|
|
301
322
|
writePart(stream, fallback, state);
|
|
302
323
|
}
|
|
303
324
|
catch {
|
|
304
|
-
stream
|
|
325
|
+
// (a stream that refuses even the stable part has closed; a part's JSON is never conversation)
|
|
305
326
|
}
|
|
306
327
|
}
|
|
307
328
|
}
|
|
@@ -330,6 +351,7 @@ function writePart(stream, part, state) {
|
|
|
330
351
|
stream.push(hookPart(part));
|
|
331
352
|
return;
|
|
332
353
|
case 'questions':
|
|
354
|
+
drew(stream, part.requestId);
|
|
333
355
|
if (state.carousel)
|
|
334
356
|
askQuestions(stream, part, state.pending, state.cancelled);
|
|
335
357
|
else
|
|
@@ -345,9 +367,18 @@ function writePart(stream, part, state) {
|
|
|
345
367
|
stream.markdown(`\`${part.path}\``);
|
|
346
368
|
return;
|
|
347
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;
|
|
348
378
|
case 'answers':
|
|
349
|
-
if (part.approval && part.answers.length > 0 && state.quiet?.(part.
|
|
379
|
+
if (part.approval && part.answers.length > 0 && state.quiet?.(part.requestId))
|
|
350
380
|
return;
|
|
381
|
+
drew(stream, part.requestId);
|
|
351
382
|
if (part.approval && nativeInTurnApprovalCards && part.answers.length > 0) {
|
|
352
383
|
stream.confirmation(part.title, part.message, {
|
|
353
384
|
kind: 'supercode.inTurnApproval', version: 1, answers: part.answers,
|
|
@@ -434,6 +465,7 @@ function unavailableMarkdown(reason) {
|
|
|
434
465
|
}
|
|
435
466
|
export async function activate(context) {
|
|
436
467
|
const log = vscode.window.createOutputChannel('Volter Harness', { log: true });
|
|
468
|
+
outputLog = log;
|
|
437
469
|
context.subscriptions.push(log);
|
|
438
470
|
// The HOST decides which proposals this extension got; its own manifest only
|
|
439
471
|
// asks. `context.extension.packageJSON` is the filtered extension
|
|
@@ -770,7 +802,6 @@ export async function activate(context) {
|
|
|
770
802
|
if (state.activeSession !== connection.id)
|
|
771
803
|
throw new Error('The conversation changed before its history could be saved.');
|
|
772
804
|
connection.persistedHistory = state.history;
|
|
773
|
-
connection.historyTruncated = state.historyTruncated;
|
|
774
805
|
savedConnections.set(connection.id, connection);
|
|
775
806
|
};
|
|
776
807
|
const connectManaged = async (state = controls.state) => {
|
|
@@ -1087,7 +1118,6 @@ export async function activate(context) {
|
|
|
1087
1118
|
throw new Error('The host did not restore the requested conversation.');
|
|
1088
1119
|
const connection = await connectManaged(state);
|
|
1089
1120
|
connection.persistedHistory = state.history;
|
|
1090
|
-
connection.historyTruncated = state.historyTruncated;
|
|
1091
1121
|
savedConnections.set(id, connection);
|
|
1092
1122
|
if (active !== connection)
|
|
1093
1123
|
controlsChanged.fire();
|
|
@@ -1347,22 +1377,6 @@ export async function activate(context) {
|
|
|
1347
1377
|
// prevents the submit command from reaching session materialization at all.
|
|
1348
1378
|
context.subscriptions.push(vscode.commands.registerCommand('supercode.frontend.sendRefusal', () => draftChoiceRefusal()
|
|
1349
1379
|
?? (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
1380
|
// The host's native focus event is authoritative even when a cached model
|
|
1367
1381
|
// skips content resolution. Untitled/non-harness focus never starts a runtime.
|
|
1368
1382
|
context.subscriptions.push(vscode.commands.registerCommand('supercode.frontend.focusSession', (resource) => {
|
|
@@ -1547,8 +1561,8 @@ export async function activate(context) {
|
|
|
1547
1561
|
// never both answer the same request (the second answer would be refused and noted as a failure).
|
|
1548
1562
|
const answered = answeredRequests.get(connection.id) ?? new Set();
|
|
1549
1563
|
answeredRequests.set(connection.id, answered);
|
|
1550
|
-
// Requests whose card
|
|
1551
|
-
const
|
|
1564
|
+
// Requests whose card an automatic answer that failed or did not settle has drawn again: once each.
|
|
1565
|
+
const redrawn = new Set();
|
|
1552
1566
|
const answers = (requestId) => {
|
|
1553
1567
|
if (approval() !== 'autoApprove' || token.isCancellationRequested || !canAnswer(connection))
|
|
1554
1568
|
return false;
|
|
@@ -1559,7 +1573,6 @@ export async function activate(context) {
|
|
|
1559
1573
|
const decisions = acceptedDecisions(pending);
|
|
1560
1574
|
if (decisions && !decisions.includes('allow'))
|
|
1561
1575
|
return false;
|
|
1562
|
-
unshown.add(requestId);
|
|
1563
1576
|
return true;
|
|
1564
1577
|
};
|
|
1565
1578
|
const sweep = () => {
|
|
@@ -1572,45 +1585,40 @@ export async function activate(context) {
|
|
|
1572
1585
|
if (decisions && !decisions.includes('allow'))
|
|
1573
1586
|
continue;
|
|
1574
1587
|
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
1588
|
const payload = pending.payload && typeof pending.payload === 'object' && !Array.isArray(pending.payload) ? pending.payload : {};
|
|
1577
1589
|
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
1590
|
const label = opaqueText(named ?? `request #${pending.id}`, 200);
|
|
1586
|
-
//
|
|
1587
|
-
//
|
|
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.
|
|
1588
1595
|
const showCard = () => {
|
|
1589
|
-
if (
|
|
1596
|
+
if (redrawn.has(pending.id) || !connection.session.pendingRequests.has(pending.id))
|
|
1590
1597
|
return;
|
|
1598
|
+
redrawn.add(pending.id);
|
|
1591
1599
|
try {
|
|
1592
|
-
|
|
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], {
|
|
1593
1602
|
connectionId: connection.id,
|
|
1594
|
-
canRespond: connection
|
|
1595
|
-
}), granted
|
|
1603
|
+
canRespond: canAnswer(connection),
|
|
1604
|
+
}), granted))
|
|
1605
|
+
writePart(stream, part, state);
|
|
1596
1606
|
}
|
|
1597
1607
|
catch {
|
|
1598
1608
|
void vscode.window.showWarningMessage(`${connection.label}: ${label} is waiting for approval. Open its chat to answer it.`);
|
|
1599
1609
|
}
|
|
1600
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.
|
|
1601
1613
|
const unsettled = setTimeout(() => {
|
|
1602
|
-
|
|
1614
|
+
log.warn(`Auto approve has had no answer for ${label} for ${AUTO_ANSWER_SETTLE_MS / 1000} s.`);
|
|
1603
1615
|
showCard();
|
|
1604
1616
|
}, AUTO_ANSWER_SETTLE_MS);
|
|
1605
1617
|
void connection.session.respond({ kind: 'approval', request_id: pending.id, decision: 'allow' }).then(() => {
|
|
1606
1618
|
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
1619
|
}, error => {
|
|
1612
1620
|
clearTimeout(unsettled);
|
|
1613
|
-
|
|
1621
|
+
log.warn(`Auto approve could not answer ${label}: ${String(error.message || error)}`);
|
|
1614
1622
|
showCard();
|
|
1615
1623
|
});
|
|
1616
1624
|
}
|
|
@@ -1640,8 +1648,10 @@ export async function activate(context) {
|
|
|
1640
1648
|
const projection = {
|
|
1641
1649
|
connectionId: connection.id,
|
|
1642
1650
|
pendingRequest: (requestId) => connection.session.pendingRequests.get(requestId) ?? connection.session.resolvedRequests.get(requestId),
|
|
1643
|
-
|
|
1651
|
+
// (as history: a control appears only where the runtime offers `respond` and this attachment may approve)
|
|
1652
|
+
canRespond: canAnswer(connection),
|
|
1644
1653
|
plan: connection.plan ??= new PlanTracker(),
|
|
1654
|
+
notices: new Set(),
|
|
1645
1655
|
};
|
|
1646
1656
|
const cancellation = token.onCancellationRequested(() => {
|
|
1647
1657
|
void stopConnection(connection).catch(() => {
|
|
@@ -1651,27 +1661,8 @@ export async function activate(context) {
|
|
|
1651
1661
|
});
|
|
1652
1662
|
const watch = watchApprovals(connection, stream, token, approval);
|
|
1653
1663
|
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);
|
|
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;
|
|
1675
1666
|
let completed = false;
|
|
1676
1667
|
let turnStarted = false;
|
|
1677
1668
|
try {
|
|
@@ -1680,14 +1671,18 @@ export async function activate(context) {
|
|
|
1680
1671
|
// events arrive; no save refusal submits input again or aborts its turn.
|
|
1681
1672
|
throwIfCancelled();
|
|
1682
1673
|
await connection.session.runTurn(prompt, (event) => {
|
|
1683
|
-
// 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.
|
|
1684
1675
|
if (event.kind === 'turn_interrupted' && token.isCancellationRequested)
|
|
1685
1676
|
return;
|
|
1686
1677
|
// Native submit publishes its initial user echo before `turn_started`.
|
|
1687
1678
|
// Subsequent input belongs in the response while this turn is visible.
|
|
1688
1679
|
if (event.kind === 'turn_started')
|
|
1689
1680
|
turnStarted = true;
|
|
1690
|
-
|
|
1681
|
+
const ended = turnOutcome(event);
|
|
1682
|
+
if (ended)
|
|
1683
|
+
outcome = ended;
|
|
1684
|
+
else
|
|
1685
|
+
writeProjected(stream, projectEvent(event, { ...projection, liveUserMessages: turnStarted }), granted, state);
|
|
1691
1686
|
watch.sweep();
|
|
1692
1687
|
rememberIdentity(connection);
|
|
1693
1688
|
}, { throwIfCancelled, onSubmitted: () => {
|
|
@@ -1696,7 +1691,6 @@ export async function activate(context) {
|
|
|
1696
1691
|
completed = true;
|
|
1697
1692
|
}
|
|
1698
1693
|
finally {
|
|
1699
|
-
unwatchTasks();
|
|
1700
1694
|
watch.dispose();
|
|
1701
1695
|
cancellation.dispose();
|
|
1702
1696
|
await finishIdentitySave(connection);
|
|
@@ -1707,7 +1701,14 @@ export async function activate(context) {
|
|
|
1707
1701
|
const effective = active ? controls?.state.effective : undefined;
|
|
1708
1702
|
const ran = effective?.modelName || (active ? controls?.state.actualModel : undefined) || connection.session.descriptor?.model;
|
|
1709
1703
|
const effort = effective?.effort || controls?.state.sessions.find(session => session.id === connection.id)?.selection.effort;
|
|
1710
|
-
|
|
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 } : {}) };
|
|
1711
1712
|
};
|
|
1712
1713
|
const handler = async (request, chatContext, stream, token) => {
|
|
1713
1714
|
await applying;
|
|
@@ -1720,9 +1721,10 @@ export async function activate(context) {
|
|
|
1720
1721
|
// thrown as is: the workbench writes a handler's error into the chat
|
|
1721
1722
|
throw new Error(`${refusal} Nothing was sent; send it again in a new chat.`);
|
|
1722
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.
|
|
1723
1726
|
if (controls?.state.setup && !controls.state.setup.ready) {
|
|
1724
|
-
|
|
1725
|
-
return {};
|
|
1727
|
+
throw new Error('Sign in before sending a message. Use **New Chat: Choose Agent** to get started.');
|
|
1726
1728
|
}
|
|
1727
1729
|
const connection = requestedResource
|
|
1728
1730
|
? await connectionFor(requestedResource)
|
|
@@ -1734,10 +1736,8 @@ export async function activate(context) {
|
|
|
1734
1736
|
throw new Error('This conversation has fixed model settings. Start a new chat to choose different settings; no message was sent.');
|
|
1735
1737
|
}
|
|
1736
1738
|
}
|
|
1737
|
-
if (!connection)
|
|
1738
|
-
|
|
1739
|
-
return {};
|
|
1740
|
-
}
|
|
1739
|
+
if (!connection)
|
|
1740
|
+
throw new Error(unavailableMarkdown(frontends.unavailable));
|
|
1741
1741
|
const pending = pendingRequestOf(connection);
|
|
1742
1742
|
// A message is never taken as an approval: one sent while an approval waits would otherwise allow the tool and
|
|
1743
1743
|
// never reach the agent. It is refused, with what to do, like a send during any running turn.
|
|
@@ -1769,9 +1769,7 @@ export async function activate(context) {
|
|
|
1769
1769
|
attached.push({ name: uri.toString(), text: document.getText(location?.range) });
|
|
1770
1770
|
}
|
|
1771
1771
|
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 } : {};
|
|
1772
|
+
return runTurnInto(connection, prompt, stream, token, approval);
|
|
1775
1773
|
};
|
|
1776
1774
|
const participants = [];
|
|
1777
1775
|
for (const id of granted.has('defaultChatParticipant') ? [PARTICIPANT_ID, DEFAULT_PARTICIPANT_ID] : [PARTICIPANT_ID]) {
|
|
@@ -1891,7 +1889,17 @@ export async function activate(context) {
|
|
|
1891
1889
|
return { status: 'cancelled' };
|
|
1892
1890
|
response = { ...response, content };
|
|
1893
1891
|
}
|
|
1894
|
-
|
|
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
|
+
}
|
|
1895
1903
|
void vscode.window.setStatusBarMessage(`Volter Harness: ${resolutionSentence(response, request, answer.requestId)}`, 4000);
|
|
1896
1904
|
return { status: 'responded' };
|
|
1897
1905
|
}
|
|
@@ -2223,7 +2231,7 @@ function oneRowPerToolCall(parts) {
|
|
|
2223
2231
|
* between tools is markdown, consecutive text joined into one part. A turn
|
|
2224
2232
|
* looks the same however late the Chat was opened.
|
|
2225
2233
|
*/
|
|
2226
|
-
function historyParts(parts) {
|
|
2234
|
+
function historyParts(parts, granted) {
|
|
2227
2235
|
const out = [];
|
|
2228
2236
|
let text = [];
|
|
2229
2237
|
const flush = () => {
|
|
@@ -2251,6 +2259,12 @@ function historyParts(parts) {
|
|
|
2251
2259
|
out.push(hookPart(part));
|
|
2252
2260
|
continue;
|
|
2253
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
|
+
}
|
|
2254
2268
|
const value = historyText(part);
|
|
2255
2269
|
if (value)
|
|
2256
2270
|
text.push(value);
|
|
@@ -2258,6 +2272,11 @@ function historyParts(parts) {
|
|
|
2258
2272
|
flush();
|
|
2259
2273
|
return out.length ? out : [new vscode.ChatResponseMarkdownPart('')];
|
|
2260
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
|
+
}
|
|
2261
2280
|
/**
|
|
2262
2281
|
* One text part as the markdown a HISTORY turn carries (tool calls are drawn
|
|
2263
2282
|
* as tool rows by `historyParts`).
|
|
@@ -2285,6 +2304,12 @@ function historyText(part) {
|
|
|
2285
2304
|
return `**${part.title}**\n\n${part.message}`;
|
|
2286
2305
|
case 'questions':
|
|
2287
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 '';
|
|
2288
2313
|
}
|
|
2289
2314
|
}
|
|
2290
2315
|
function registerSessions(context, frontends, participant, granted, handler, historyParticipant, controls) {
|
|
@@ -2402,7 +2427,7 @@ function registerSessions(context, frontends, participant, granted, handler, his
|
|
|
2402
2427
|
return {
|
|
2403
2428
|
history: [
|
|
2404
2429
|
new turns.ChatRequestTurn(HISTORY_MARKER_PROMPT, undefined, [], 'supercode', []),
|
|
2405
|
-
new turns.ChatResponseTurn([
|
|
2430
|
+
new turns.ChatResponseTurn([failurePart(message, granted)], {}, 'supercode'),
|
|
2406
2431
|
],
|
|
2407
2432
|
requestHandler: boundHandler,
|
|
2408
2433
|
};
|
|
@@ -2470,8 +2495,10 @@ function registerSessions(context, frontends, participant, granted, handler, his
|
|
|
2470
2495
|
pendingToolNames.add(name);
|
|
2471
2496
|
}
|
|
2472
2497
|
}
|
|
2473
|
-
// The task list is replayed with the history it was built in, and live turns carry on from it
|
|
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.
|
|
2474
2500
|
const plan = new PlanTracker();
|
|
2501
|
+
const notices = new Set();
|
|
2475
2502
|
const projected = projectReplayTurns(replay.filter(event => {
|
|
2476
2503
|
if (event.kind !== 'request')
|
|
2477
2504
|
return true;
|
|
@@ -2482,6 +2509,8 @@ function registerSessions(context, frontends, participant, granted, handler, his
|
|
|
2482
2509
|
pendingRequest: (id) => connection.session.pendingRequests.get(id) ?? connection.session.resolvedRequests.get(id),
|
|
2483
2510
|
canRespond: Boolean(connection.session.descriptor?.actions.respond && connection.session.permissions.has('approve')),
|
|
2484
2511
|
plan,
|
|
2512
|
+
notices,
|
|
2513
|
+
unshown: logRecord,
|
|
2485
2514
|
}, projectHistory(canonicalHistory, granted, pendingToolNames, plan));
|
|
2486
2515
|
connection.plan = plan;
|
|
2487
2516
|
const live = connection.session.descriptor?.turn_state === 'busy' || connection.session.pendingRequests.size > 0;
|
|
@@ -2511,24 +2540,24 @@ function registerSessions(context, frontends, participant, granted, handler, his
|
|
|
2511
2540
|
if (last?.role === 'assistant') {
|
|
2512
2541
|
activeParts.push(...(projected.pop()?.parts ?? []).map(part => part.part === 'answers' || part.part === 'questions'
|
|
2513
2542
|
? { part: 'markdown', value: `**${(part.part === 'questions' ? part.fallback : part).title}**\n\n${(part.part === 'questions' ? part.fallback : part).message}` }
|
|
2514
|
-
|
|
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));
|
|
2515
2546
|
}
|
|
2516
2547
|
}
|
|
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));
|
|
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)
|
|
2521
2550
|
for (const turn of projected) {
|
|
2522
2551
|
if (turn.role === 'user') {
|
|
2523
2552
|
history.push(new turns.ChatRequestTurn(turn.prompt ?? '', undefined, [], authorId, []));
|
|
2524
2553
|
continue;
|
|
2525
2554
|
}
|
|
2526
|
-
history.push(new turns.ChatResponseTurn(historyParts(oneRowPerToolCall(turn.parts ?? [])), {}, authorId));
|
|
2555
|
+
history.push(new turns.ChatResponseTurn(historyParts(oneRowPerToolCall(turn.parts ?? []), granted), {}, authorId));
|
|
2527
2556
|
}
|
|
2528
2557
|
if (connection.session.failure) {
|
|
2529
2558
|
// An explicit runtime marker pairs this notice without presenting
|
|
2530
2559
|
// it as a person's prompt. The native busy state stays unchanged.
|
|
2531
|
-
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));
|
|
2532
2561
|
}
|
|
2533
2562
|
const renderedCursor = connection.session.cursor;
|
|
2534
2563
|
const pendingAtCursor = [...connection.session.pendingRequests.values()];
|
|
@@ -2560,6 +2589,7 @@ function registerSessions(context, frontends, participant, granted, handler, his
|
|
|
2560
2589
|
connectionId: connection.id,
|
|
2561
2590
|
liveUserMessages: true,
|
|
2562
2591
|
plan,
|
|
2592
|
+
notices,
|
|
2563
2593
|
pendingRequest: (id) => connection.session.pendingRequests.get(id) ?? connection.session.resolvedRequests.get(id),
|
|
2564
2594
|
canRespond: Boolean(connection.session.descriptor?.actions.respond && connection.session.permissions.has('approve')),
|
|
2565
2595
|
}), granted, state);
|
|
@@ -2570,7 +2600,13 @@ function registerSessions(context, frontends, participant, granted, handler, his
|
|
|
2570
2600
|
catch (error) {
|
|
2571
2601
|
if (token.isCancellationRequested || error instanceof vscode.CancellationError)
|
|
2572
2602
|
throw error;
|
|
2573
|
-
|
|
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`);
|
|
2574
2610
|
}
|
|
2575
2611
|
finally {
|
|
2576
2612
|
connection.viewReady = connection.session.descriptor?.turn_state !== 'busy' && !connection.session.pendingRequests.size;
|
package/dist/host-controls.d.ts
CHANGED
|
@@ -52,7 +52,6 @@ export interface ChatSetupState {
|
|
|
52
52
|
}
|
|
53
53
|
export interface HostState {
|
|
54
54
|
history?: ChatMessage[];
|
|
55
|
-
historyTruncated?: boolean;
|
|
56
55
|
activeSession: string | null;
|
|
57
56
|
openSessionCommand?: string;
|
|
58
57
|
/** Automatic ready-session reveal checks the observed native focus through its queued commit. */
|
package/dist/proposals.js
CHANGED
|
@@ -27,8 +27,8 @@ export const PROPOSAL_EFFECTS = Object.freeze({
|
|
|
27
27
|
without: 'the ordinary `@supercode` participant answers; a mention is required',
|
|
28
28
|
},
|
|
29
29
|
chatParticipantPrivate: {
|
|
30
|
-
grants: 'the dynamic-participant and
|
|
31
|
-
without: '
|
|
30
|
+
grants: 'the dynamic-participant identity, and an interrupted turn ending with a warning-level, expected error',
|
|
31
|
+
without: 'a static participant identity; an interrupted turn ends with an ordinary error',
|
|
32
32
|
},
|
|
33
33
|
chatParticipantAdditions: {
|
|
34
34
|
grants: 'tool-invocation, confirmation, warning and codeblock-uri response parts',
|
package/dist/session.d.ts
CHANGED
|
@@ -16,9 +16,6 @@ export interface VscodeRuntimeClient {
|
|
|
16
16
|
steer(prompt: string): Promise<{
|
|
17
17
|
queued: boolean;
|
|
18
18
|
}>;
|
|
19
|
-
stopTask(taskId: string): Promise<{
|
|
20
|
-
stopped: boolean;
|
|
21
|
-
}>;
|
|
22
19
|
interrupt(): Promise<{
|
|
23
20
|
interrupted: boolean;
|
|
24
21
|
}>;
|
|
@@ -66,12 +63,6 @@ export declare const TURN_TERMINAL_KINDS: readonly ["turn_succeeded", "turn_fail
|
|
|
66
63
|
* the view reports.
|
|
67
64
|
*/
|
|
68
65
|
export declare const LEASE_ERROR_NAMES: readonly string[];
|
|
69
|
-
/** One of the harness's own background tasks (a Monitor, a background shell), as it last listed them. */
|
|
70
|
-
export interface BackgroundTask {
|
|
71
|
-
id: string;
|
|
72
|
-
kind: string;
|
|
73
|
-
description: string;
|
|
74
|
-
}
|
|
75
66
|
/**
|
|
76
67
|
* One attachment to one already-running Volter Harness SDK runtime: cursor-safe
|
|
77
68
|
* event projection, permission narrowing, and the semantic actions the runtime
|
|
@@ -87,7 +78,6 @@ export declare class VscodeFrontendSession {
|
|
|
87
78
|
history: ChatMessage[];
|
|
88
79
|
historyCursor: number;
|
|
89
80
|
historyEvents: FrontendEvent[];
|
|
90
|
-
historyEventsDropped: number;
|
|
91
81
|
cursor: number;
|
|
92
82
|
/**
|
|
93
83
|
* Every event this attachment has seen, in order — NOT just the last
|
|
@@ -99,15 +89,8 @@ export declare class VscodeFrontendSession {
|
|
|
99
89
|
replay: FrontendEvent[];
|
|
100
90
|
pendingRequests: Map<number, FrontendRequest>;
|
|
101
91
|
/**
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*/
|
|
105
|
-
backgroundTasks: BackgroundTask[];
|
|
106
|
-
/**
|
|
107
|
-
* Requests this attachment has seen resolved. `request_resolved` deletes the
|
|
108
|
-
* pending entry while it is being projected, so without this the resolution
|
|
109
|
-
* could only say "request #1" — it would have lost the very thing it needs to
|
|
110
|
-
* name. Bounded, because a long session resolves a lot of them.
|
|
92
|
+
* Requests this attachment has seen resolved, so a resolution can still name what it answered once the pending
|
|
93
|
+
* entry is gone. Bounded, because a long session resolves a lot of them.
|
|
111
94
|
*/
|
|
112
95
|
resolvedRequests: Map<number, FrontendRequest>;
|
|
113
96
|
connection: 'disconnected' | 'connected' | 'closed';
|
|
@@ -158,8 +141,6 @@ export declare class VscodeFrontendSession {
|
|
|
158
141
|
submit(prompt: string, throwIfCancelled?: () => void): Promise<void>;
|
|
159
142
|
steer(prompt: string): Promise<void>;
|
|
160
143
|
interrupt(): Promise<void>;
|
|
161
|
-
/** Stop one of the harness's own background tasks, by the id it listed it with. */
|
|
162
|
-
stopTask(taskId: string): Promise<boolean>;
|
|
163
144
|
respond(response: FrontendResponse): Promise<void>;
|
|
164
145
|
detach(): Promise<void>;
|
|
165
146
|
close(): Promise<void>;
|
package/dist/session.js
CHANGED
|
@@ -48,7 +48,6 @@ export class VscodeFrontendSession {
|
|
|
48
48
|
history = [];
|
|
49
49
|
historyCursor = 0;
|
|
50
50
|
historyEvents = [];
|
|
51
|
-
historyEventsDropped = 0;
|
|
52
51
|
cursor = 0;
|
|
53
52
|
/**
|
|
54
53
|
* Every event this attachment has seen, in order — NOT just the last
|
|
@@ -60,15 +59,8 @@ export class VscodeFrontendSession {
|
|
|
60
59
|
replay = [];
|
|
61
60
|
pendingRequests = new Map();
|
|
62
61
|
/**
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*/
|
|
66
|
-
backgroundTasks = [];
|
|
67
|
-
/**
|
|
68
|
-
* Requests this attachment has seen resolved. `request_resolved` deletes the
|
|
69
|
-
* pending entry while it is being projected, so without this the resolution
|
|
70
|
-
* could only say "request #1" — it would have lost the very thing it needs to
|
|
71
|
-
* name. Bounded, because a long session resolves a lot of them.
|
|
62
|
+
* Requests this attachment has seen resolved, so a resolution can still name what it answered once the pending
|
|
63
|
+
* entry is gone. Bounded, because a long session resolves a lot of them.
|
|
72
64
|
*/
|
|
73
65
|
resolvedRequests = new Map();
|
|
74
66
|
connection = 'disconnected';
|
|
@@ -130,23 +122,12 @@ export class VscodeFrontendSession {
|
|
|
130
122
|
descriptor: structuredClone(snapshot.descriptor),
|
|
131
123
|
pendingRequests: new Map((authoritative ? snapshot.pending_requests : [...this.pendingRequests.values()])
|
|
132
124
|
.map(request => [request.id, structuredClone(request)])),
|
|
133
|
-
backgroundTasks: structuredClone(this.backgroundTasks),
|
|
134
125
|
cursor: authoritative ? recovery.captureCursor : Math.max(this.cursor, snapshot.history_cursor),
|
|
135
126
|
};
|
|
136
127
|
// Validate and project the whole attachment before any observable field
|
|
137
128
|
// moves or a live listener is registered.
|
|
138
129
|
if (authoritative) {
|
|
139
130
|
// Snapshot state is authoritative at capture, before any later events.
|
|
140
|
-
// Background listings have no separate snapshot field; retain their
|
|
141
|
-
// latest original event without replaying historical request state.
|
|
142
|
-
for (const event of recovery.replay) {
|
|
143
|
-
const payload = (event.payload ?? {});
|
|
144
|
-
if (event.kind === 'background_tasks' && Array.isArray(payload.tasks)) {
|
|
145
|
-
staged.backgroundTasks = payload.tasks
|
|
146
|
-
.filter(task => task && typeof task.id === 'string')
|
|
147
|
-
.map(task => ({ id: task.id, kind: String(task.kind ?? 'task'), description: String(task.description ?? '') }));
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
131
|
for (const event of recovery.later)
|
|
151
132
|
this.projectEvent(staged, event);
|
|
152
133
|
}
|
|
@@ -197,10 +178,8 @@ export class VscodeFrontendSession {
|
|
|
197
178
|
this.history = structuredClone(snapshot.history);
|
|
198
179
|
this.historyCursor = snapshot.history_cursor;
|
|
199
180
|
this.historyEvents = structuredClone(snapshot.history_events ?? []);
|
|
200
|
-
this.historyEventsDropped = snapshot.history_events_dropped ?? 0;
|
|
201
181
|
this.appendTranscript(structuredClone(recovery.replay));
|
|
202
182
|
this.pendingRequests = staged.pendingRequests;
|
|
203
|
-
this.backgroundTasks = staged.backgroundTasks;
|
|
204
183
|
const previousCursor = this.cursor;
|
|
205
184
|
this.cursor = staged.cursor;
|
|
206
185
|
this.connection = 'connected';
|
|
@@ -392,18 +371,12 @@ export class VscodeFrontendSession {
|
|
|
392
371
|
if (request && typeof request.id === 'number')
|
|
393
372
|
state.pendingRequests.set(request.id, structuredClone(request));
|
|
394
373
|
}
|
|
395
|
-
if (event.kind === 'background_tasks' && Array.isArray(payload.tasks)) {
|
|
396
|
-
state.backgroundTasks = payload.tasks
|
|
397
|
-
.filter(task => task && typeof task.id === 'string')
|
|
398
|
-
.map(task => ({ id: task.id, kind: String(task.kind ?? 'task'), description: String(task.description ?? '') }));
|
|
399
|
-
}
|
|
400
374
|
if (event.kind === 'request_resolved' && typeof payload.request_id === 'number') {
|
|
401
375
|
const resolved = state.pendingRequests.get(payload.request_id);
|
|
402
376
|
if (resolved) {
|
|
403
377
|
this.resolvedRequests.set(payload.request_id, resolved);
|
|
404
|
-
while (this.resolvedRequests.size > 50)
|
|
378
|
+
while (this.resolvedRequests.size > 50)
|
|
405
379
|
this.resolvedRequests.delete(this.resolvedRequests.keys().next().value);
|
|
406
|
-
}
|
|
407
380
|
}
|
|
408
381
|
state.pendingRequests.delete(payload.request_id);
|
|
409
382
|
}
|
|
@@ -480,11 +453,6 @@ export class VscodeFrontendSession {
|
|
|
480
453
|
this.require('interrupt');
|
|
481
454
|
await this.withControl(() => this.client.interrupt());
|
|
482
455
|
}
|
|
483
|
-
/** Stop one of the harness's own background tasks, by the id it listed it with. */
|
|
484
|
-
async stopTask(taskId) {
|
|
485
|
-
const result = await this.withControl(() => this.client.stopTask(taskId));
|
|
486
|
-
return result?.stopped === true;
|
|
487
|
-
}
|
|
488
456
|
async respond(response) {
|
|
489
457
|
this.require('respond');
|
|
490
458
|
if (!this.pendingRequests.has(response.request_id))
|