@volter/supercode-frontend-vscode 0.1.48 → 0.1.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/extension.js +135 -39
- package/dist/host-controls.d.ts +14 -1
- package/dist/host-controls.js +7 -4
- package/dist/session.js +25 -2
- package/dist/view.d.ts +22 -0
- package/dist/view.js +158 -17
- package/package.json +1 -1
package/dist/extension.js
CHANGED
|
@@ -267,17 +267,30 @@ function writePart(stream, part, state) {
|
|
|
267
267
|
const toolName = part.toolName || state.toolNames.get(part.toolCallId) || 'tool';
|
|
268
268
|
if (part.toolName)
|
|
269
269
|
state.toolNames.set(part.toolCallId, part.toolName);
|
|
270
|
+
// The titles a call started with stay its titles: a completion (which may not name its tool) never renames it.
|
|
271
|
+
const started = state.toolTitles.get(part.toolCallId);
|
|
272
|
+
const titles = started ?? { invocation: part.invocationMessage || `Running \`${toolName}\``, pastTense: part.pastTenseMessage || part.invocationMessage || `Ran \`${toolName}\`` };
|
|
273
|
+
if (!started)
|
|
274
|
+
state.toolTitles.set(part.toolCallId, titles);
|
|
275
|
+
if (part.commandLine)
|
|
276
|
+
state.toolCommands.set(part.toolCallId, part.commandLine);
|
|
277
|
+
const commandLine = state.toolCommands.get(part.toolCallId);
|
|
270
278
|
const invocation = new vscode.ChatToolInvocationPart(toolName, part.toolCallId);
|
|
271
279
|
invocation.enablePartialUpdate = true;
|
|
272
|
-
invocation.invocationMessage =
|
|
280
|
+
invocation.invocationMessage = new vscode.MarkdownString(titles.invocation);
|
|
273
281
|
invocation.isConfirmed = true;
|
|
274
282
|
invocation.isComplete = part.isComplete;
|
|
275
|
-
invocation.pastTenseMessage =
|
|
283
|
+
invocation.pastTenseMessage = new vscode.MarkdownString(titles.pastTense);
|
|
276
284
|
if (part.isError !== undefined)
|
|
277
285
|
invocation.isError = part.isError;
|
|
278
286
|
if (part.inputText !== undefined)
|
|
279
287
|
state.toolInputs.set(part.toolCallId, part.inputText);
|
|
280
|
-
if (
|
|
288
|
+
if (commandLine) {
|
|
289
|
+
// VS Code's own terminal block: the command, and its output once it has run.
|
|
290
|
+
invocation.toolSpecificData = { commandLine: { original: commandLine }, language: 'sh',
|
|
291
|
+
...(part.isComplete ? { output: { text: part.resultText ?? '' } } : {}) };
|
|
292
|
+
}
|
|
293
|
+
else if (part.isComplete) {
|
|
281
294
|
invocation.toolSpecificData = {
|
|
282
295
|
input: state.toolInputs.get(part.toolCallId) ?? '',
|
|
283
296
|
output: part.resultText ?? '',
|
|
@@ -494,14 +507,22 @@ export async function activate(context) {
|
|
|
494
507
|
controlsChanged.fire();
|
|
495
508
|
}
|
|
496
509
|
}));
|
|
510
|
+
// What a new chat of `selection` runs with: its own picks over what the harness's configuration names (host state).
|
|
511
|
+
const draftEffective = (selection) => {
|
|
512
|
+
const configured = controls?.state.effectiveByHarness?.[selection.harness];
|
|
513
|
+
if (!configured)
|
|
514
|
+
return null;
|
|
515
|
+
const picked = selection.model ? controls?.state.modelsByHarness?.[selection.harness]?.find(model => model.id === selection.model) : undefined;
|
|
516
|
+
return { model: selection.model || configured.model, modelName: selection.model ? picked?.name ?? selection.model : configured.modelName, effort: selection.effort || configured.effort };
|
|
517
|
+
};
|
|
497
518
|
const identity = () => {
|
|
498
519
|
if (focusedAttached)
|
|
499
520
|
return runtimeIdentity(focusedAttached.session.descriptor?.source_harness, focusedAttached.session.descriptor?.model);
|
|
500
521
|
const descriptor = managed()?.session.descriptor;
|
|
501
522
|
const selection = draftSelection();
|
|
502
523
|
if (selection)
|
|
503
|
-
return runtimeIdentity(selection.harness, selection.model);
|
|
504
|
-
return runtimeIdentity(controls?.state.selection.harness ?? descriptor?.source_harness, controls?.state.selection.model || controls?.state.actualModel || descriptor?.model);
|
|
524
|
+
return runtimeIdentity(selection.harness, selection.model, draftEffective(selection));
|
|
525
|
+
return runtimeIdentity(controls?.state.selection.harness ?? descriptor?.source_harness, controls?.state.selection.model || controls?.state.actualModel || descriptor?.model, controls?.state.effective);
|
|
505
526
|
};
|
|
506
527
|
const icon = () => {
|
|
507
528
|
const provider = identity().provider;
|
|
@@ -524,16 +545,18 @@ export async function activate(context) {
|
|
|
524
545
|
if (!choices.some(model => model.id === selection.model))
|
|
525
546
|
choices.push({ id: selection.model, name: selection.model });
|
|
526
547
|
modelSelections.clear();
|
|
548
|
+
// Named by what the conversation actually runs with, never "default model" (host `effective`).
|
|
549
|
+
const effective = draft ? draftEffective(selection) : controls.state.effective;
|
|
527
550
|
return choices.map(model => {
|
|
528
551
|
const id = `${selection.harness}/${encodeURIComponent(model.id || '__default__')}`;
|
|
529
552
|
modelSelections.set(id, { ...selection, model: model.id });
|
|
530
|
-
const provider = runtimeIdentity(selection.harness, selection.model).provider;
|
|
531
|
-
const name =
|
|
553
|
+
const provider = runtimeIdentity(selection.harness, selection.model, effective).provider;
|
|
554
|
+
const name = runtimeIdentity(selection.harness, model.id, effective).name;
|
|
532
555
|
return { id, name, family: selection.harness, version: '1',
|
|
533
556
|
statusIcon: new vscode.ThemeIcon(provider === 'anthropic' ? 'claude' : provider === 'openai' ? 'openai' : provider === 'google' ? 'google-gemini' : 'sparkle'),
|
|
534
557
|
maxInputTokens: 0, maxOutputTokens: 0, capabilities: {},
|
|
535
558
|
targetChatSessionType: SESSION_SCHEME, isUserSelectable: true, isDefault: model.id === selection.model,
|
|
536
|
-
tooltip: `${name}
|
|
559
|
+
tooltip: `${name}. Model is fixed for this conversation. Use Model settings to choose model and reasoning for a new conversation.`,
|
|
537
560
|
};
|
|
538
561
|
});
|
|
539
562
|
},
|
|
@@ -571,7 +594,7 @@ export async function activate(context) {
|
|
|
571
594
|
await frontends.disconnect(connection.id);
|
|
572
595
|
const connection = await frontends.connect({ connectionId: id, baseUrl: env.SUPERCODE_FRONTEND_URL,
|
|
573
596
|
token: await readCredentialFile(env.SUPERCODE_FRONTEND_CREDENTIAL_FILE), clientId: env.SUPERCODE_FRONTEND_CLIENT_ID,
|
|
574
|
-
permissions: normalizePermissions(env.SUPERCODE_FRONTEND_PERMISSIONS, ['observe']), label: runtimeIdentity(state.selection.harness, state.selection.model || state.actualModel).name }, log);
|
|
597
|
+
permissions: normalizePermissions(env.SUPERCODE_FRONTEND_PERMISSIONS, ['observe']), label: runtimeIdentity(state.selection.harness, state.selection.model || state.actualModel, state.effective).name }, log);
|
|
575
598
|
if (!connection)
|
|
576
599
|
throw new Error('Could not connect to the requested conversation.');
|
|
577
600
|
return connection;
|
|
@@ -842,7 +865,10 @@ export async function activate(context) {
|
|
|
842
865
|
const pickModelAndEffort = async (harness, current) => {
|
|
843
866
|
const choices = controls?.state.modelsByHarness?.[harness] ?? [{ id: '', name: 'Harness default' }];
|
|
844
867
|
const name = harnessDisplayName(harness);
|
|
845
|
-
|
|
868
|
+
// The harness's own choice is named by what it resolves to (host `effectiveByHarness`).
|
|
869
|
+
const configured = controls?.state.effectiveByHarness?.[harness];
|
|
870
|
+
const items = [...choices.map(model => ({ label: model.id === '' && configured?.modelName ? `${model.name}: ${configured.modelName}` : model.name,
|
|
871
|
+
description: [model.id, model.description].filter(Boolean).join(' · ') || undefined, id: model.id, efforts: model.efforts })),
|
|
846
872
|
{ label: 'Enter an exact model ID…', description: undefined, id: '__custom__', efforts: choices.find(model => model.id === '')?.efforts }];
|
|
847
873
|
const picked = await pickWithCurrent(items, `New chat · ${name} model`, 'Model availability depends on your harness account.', current?.model ?? '');
|
|
848
874
|
if (!picked)
|
|
@@ -855,8 +881,8 @@ export async function activate(context) {
|
|
|
855
881
|
model = typed.trim();
|
|
856
882
|
}
|
|
857
883
|
const levels = picked.efforts ?? ['low', 'medium', 'high'];
|
|
858
|
-
const efforts = [{ label: 'Harness default', id: '' }, ...levels.map(id => ({ label: id, id }))];
|
|
859
|
-
const effort = levels.length === 0 ? efforts[0] : await pickWithCurrent(efforts, `New chat · ${name} reasoning effort`,
|
|
884
|
+
const efforts = [{ label: configured?.effort && !model ? `Harness default: ${configured.effort}` : 'Harness default', id: '' }, ...levels.map(id => ({ label: id, id }))];
|
|
885
|
+
const effort = levels.length === 0 ? efforts[0] : await pickWithCurrent(efforts, `New chat · ${name} reasoning effort`, `For ${model || configured?.modelName || `${name}'s configured model`}`, current?.effort ?? '');
|
|
860
886
|
if (!effort)
|
|
861
887
|
return;
|
|
862
888
|
return { model, effort: effort.id };
|
|
@@ -958,7 +984,9 @@ export async function activate(context) {
|
|
|
958
984
|
// whose built-in model picker deliberately omits Manage Models. Do not advertise
|
|
959
985
|
// hot swapping: the command configures a separate native blank chat.
|
|
960
986
|
// A locked selected item hides the entire native dropdown, including commands.
|
|
961
|
-
|
|
987
|
+
// The model picker names the model and effort and the permissions picker the approval mode; this item is only
|
|
988
|
+
// the way to choose them for a new chat.
|
|
989
|
+
const item = { id: 'settings', name: 'Change model…', description: 'Choose model and reasoning for a new chat.' };
|
|
962
990
|
groups.push({ id: 'modelSettings', name: 'Model', items: [item], selected: item,
|
|
963
991
|
commands: [{ command: 'supercode.frontend.configureModel', title: 'Choose model and reasoning for a new chat…', arguments: [selected.id] }] });
|
|
964
992
|
}
|
|
@@ -990,6 +1018,7 @@ export async function activate(context) {
|
|
|
990
1018
|
resolve: (resource, token) => connectionFor(resource, false, token),
|
|
991
1019
|
attached: (connection) => attachedIds.has(connection.id),
|
|
992
1020
|
catalogue: () => controls?.state.sessions ?? [],
|
|
1021
|
+
effective: (entry) => controls && entry.id === controls.state.activeSession ? controls.state.effective ?? null : draftEffective(entry.selection),
|
|
993
1022
|
bindings: boundId,
|
|
994
1023
|
watchApprovals: (connection, stream, token) => watchApprovals(connection, stream, token, () => approvalFor(connection.id)),
|
|
995
1024
|
};
|
|
@@ -1121,6 +1150,66 @@ export async function activate(context) {
|
|
|
1121
1150
|
});
|
|
1122
1151
|
}));
|
|
1123
1152
|
context.subscriptions.push({ dispose: () => frontends.dispose() });
|
|
1153
|
+
// Auto approve holds for the conversation, not only while a response of this window is open. A harness also runs
|
|
1154
|
+
// turns of its own (a background task reporting back, a Monitor's event): an approval those raise had no response to
|
|
1155
|
+
// answer it or show its card, and the turn waited forever. With no turn of this window running, such an approval is
|
|
1156
|
+
// answered here under the conversation's mode; under Ask it keeps waiting for the person, as before.
|
|
1157
|
+
const answerOutsideTurns = () => {
|
|
1158
|
+
for (const connection of frontends.all) {
|
|
1159
|
+
if (attachedIds.has(connection.id) || runningTurns.get(connection.id)?.size || !canAnswer(connection))
|
|
1160
|
+
continue;
|
|
1161
|
+
if (approvalFor(connection.id) !== 'autoApprove')
|
|
1162
|
+
continue;
|
|
1163
|
+
const answered = answeredRequests.get(connection.id) ?? new Set();
|
|
1164
|
+
answeredRequests.set(connection.id, answered);
|
|
1165
|
+
for (const pending of connection.session.pendingRequests.values()) {
|
|
1166
|
+
if (pending.kind !== 'approval' || answered.has(pending.id))
|
|
1167
|
+
continue;
|
|
1168
|
+
const decisions = acceptedDecisions(pending);
|
|
1169
|
+
if (decisions && !decisions.includes('allow'))
|
|
1170
|
+
continue;
|
|
1171
|
+
answered.add(pending.id);
|
|
1172
|
+
void connection.session.respond({ kind: 'approval', request_id: pending.id, decision: 'allow' }).catch(error => {
|
|
1173
|
+
answered.delete(pending.id);
|
|
1174
|
+
log.warn(`Auto approve could not answer request #${pending.id} outside a turn: ${String(error?.message ?? error)}`);
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
};
|
|
1179
|
+
context.subscriptions.push(frontends.onDidChange(answerOutsideTurns));
|
|
1180
|
+
// STOP ANSWERS WHAT THE AGENT IS WAITING ON FIRST. An agent blocked on an approval (or a question) does not hear an
|
|
1181
|
+
// interrupt until that request is answered: Claude Code's permission prompt kept the turn alive, the request
|
|
1182
|
+
// vanished from view on reload, and every later prompt queued behind it (2026-10-07). So a Stop denies each waiting
|
|
1183
|
+
// approval and cancels each question, then interrupts.
|
|
1184
|
+
const stopConnection = async (connection) => {
|
|
1185
|
+
if (canAnswer(connection)) {
|
|
1186
|
+
for (const pending of [...connection.session.pendingRequests.values()]) {
|
|
1187
|
+
const answer = pending.kind === 'approval'
|
|
1188
|
+
? { kind: 'approval', request_id: pending.id, decision: 'deny' }
|
|
1189
|
+
: { kind: pending.kind === 'elicitation' ? 'elicitation' : 'other', request_id: pending.id, action: 'cancel' };
|
|
1190
|
+
await connection.session.respond(answer).catch(error => log.warn(`Stop could not answer request #${pending.id}: ${String(error?.message ?? error)}`));
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
await connection.session.interrupt();
|
|
1194
|
+
};
|
|
1195
|
+
// The same Stop for a turn no response of this window holds — one the harness started itself, after a background
|
|
1196
|
+
// task or a Monitor's event — which the Chat's own Stop (it cancels a response) cannot reach.
|
|
1197
|
+
context.subscriptions.push(vscode.commands.registerCommand('supercode.frontend.stopTurn', async (connectionId) => {
|
|
1198
|
+
const connection = connectionId ? frontends.get(connectionId) : managed();
|
|
1199
|
+
if (!connection)
|
|
1200
|
+
return { stopped: false, reason: 'no conversation' };
|
|
1201
|
+
try {
|
|
1202
|
+
await stopConnection(connection);
|
|
1203
|
+
return { stopped: true, connectionId: connection.id };
|
|
1204
|
+
}
|
|
1205
|
+
catch (error) {
|
|
1206
|
+
const reason = String(error?.message ?? error);
|
|
1207
|
+
log.warn(`Stop could not interrupt: ${reason}`);
|
|
1208
|
+
return { stopped: false, connectionId: connection.id, reason };
|
|
1209
|
+
}
|
|
1210
|
+
}));
|
|
1211
|
+
const outsideTurns = setInterval(answerOutsideTurns, 2_000);
|
|
1212
|
+
context.subscriptions.push({ dispose: () => clearInterval(outsideTurns) });
|
|
1124
1213
|
// Auto-answers a conversation's approvals while a response is written into `stream`: read at every event
|
|
1125
1214
|
// (`sweep`) and on every picker change for that conversation, so switching to Auto approve mid-turn answers
|
|
1126
1215
|
// the approvals already waiting and the ones after them. Used by a turn this window runs and by a turn it
|
|
@@ -1175,7 +1264,7 @@ export async function activate(context) {
|
|
|
1175
1264
|
writeProjected(stream, projectPendingRequests([pending], {
|
|
1176
1265
|
connectionId: connection.id,
|
|
1177
1266
|
canRespond: connection.session.descriptor?.actions.respond !== false,
|
|
1178
|
-
}), granted, { toolInputs: new Map(), toolNames: new Map() });
|
|
1267
|
+
}), granted, { toolInputs: new Map(), toolNames: new Map(), toolTitles: new Map(), toolCommands: new Map() });
|
|
1179
1268
|
}
|
|
1180
1269
|
catch {
|
|
1181
1270
|
void vscode.window.showWarningMessage(`${connection.label}: ${label} is waiting for approval. Open its chat to answer it.`);
|
|
@@ -1212,14 +1301,14 @@ export async function activate(context) {
|
|
|
1212
1301
|
if (approval() === 'autoApprove' && !canAnswer(connection)) {
|
|
1213
1302
|
throw new Error('This harness does not support automatic approval through Chat. Select Harness permissions.');
|
|
1214
1303
|
}
|
|
1215
|
-
const state = { toolInputs: new Map(), toolNames: new Map() };
|
|
1304
|
+
const state = { toolInputs: new Map(), toolNames: new Map(), toolTitles: new Map(), toolCommands: new Map() };
|
|
1216
1305
|
const projection = {
|
|
1217
1306
|
connectionId: connection.id,
|
|
1218
1307
|
pendingRequest: (requestId) => connection.session.pendingRequests.get(requestId) ?? connection.session.resolvedRequests.get(requestId),
|
|
1219
1308
|
canRespond: connection.session.descriptor?.actions.respond !== false,
|
|
1220
1309
|
};
|
|
1221
1310
|
const cancellation = token.onCancellationRequested(() => {
|
|
1222
|
-
void connection
|
|
1311
|
+
void stopConnection(connection).catch(() => {
|
|
1223
1312
|
// The runtime may already be idle; interrupting twice is not an error
|
|
1224
1313
|
// this view reports.
|
|
1225
1314
|
});
|
|
@@ -1250,6 +1339,9 @@ export async function activate(context) {
|
|
|
1250
1339
|
let completed = false;
|
|
1251
1340
|
try {
|
|
1252
1341
|
await connection.session.runTurn(prompt, (event) => {
|
|
1342
|
+
// A turn the person stopped is marked stopped by VS Code itself; only an interruption from elsewhere is said.
|
|
1343
|
+
if (event.kind === 'turn_interrupted' && token.isCancellationRequested)
|
|
1344
|
+
return;
|
|
1253
1345
|
writeProjected(stream, projectEvent(event, projection), granted, state);
|
|
1254
1346
|
watch.sweep();
|
|
1255
1347
|
});
|
|
@@ -1265,13 +1357,14 @@ export async function activate(context) {
|
|
|
1265
1357
|
await controls.refresh().catch(error => log.warn(`Could not refresh chat identity: ${String(error)}`));
|
|
1266
1358
|
controlsChanged.fire();
|
|
1267
1359
|
}
|
|
1268
|
-
// The model the turn ran on: what the harness's own replies name (the host watches them), else what the
|
|
1269
|
-
// runtime reports; and the effort it was given.
|
|
1270
|
-
const ran = (controls?.state.activeSession === connection.id ? controls.state.actualModel : undefined) || connection.session.descriptor?.model;
|
|
1271
|
-
const effort = controls?.state.sessions.find(session => session.id === connection.id)?.selection.effort;
|
|
1272
|
-
if (completed && ran && !token.isCancellationRequested)
|
|
1273
|
-
stream.markdown(`\n\n_Ran on ${opaqueText(ran, 120)}${effort ? ` · ${opaqueText(effort, 24)} effort` : ''}_`);
|
|
1274
1360
|
}
|
|
1361
|
+
// The model the turn ran on: what the harness's own replies name (the host watches them), else what the
|
|
1362
|
+
// runtime reports; and the effort it ran at (host `effective`).
|
|
1363
|
+
const active = controls?.state.activeSession === connection.id;
|
|
1364
|
+
const effective = active ? controls?.state.effective : undefined;
|
|
1365
|
+
const ran = effective?.modelName || (active ? controls?.state.actualModel : undefined) || connection.session.descriptor?.model;
|
|
1366
|
+
const effort = effective?.effort || controls?.state.sessions.find(session => session.id === connection.id)?.selection.effort;
|
|
1367
|
+
return completed && ran && !token.isCancellationRequested ? `${opaqueText(ran, 120)}${effort ? ` · ${opaqueText(effort, 24)}` : ''}` : undefined;
|
|
1275
1368
|
};
|
|
1276
1369
|
const handler = async (request, chatContext, stream, token) => {
|
|
1277
1370
|
await applying;
|
|
@@ -1302,25 +1395,24 @@ export async function activate(context) {
|
|
|
1302
1395
|
stream.markdown(unavailableMarkdown(frontends.unavailable));
|
|
1303
1396
|
return {};
|
|
1304
1397
|
}
|
|
1305
|
-
// While a request is pending, a typed message is its answer — this is the
|
|
1306
|
-
// path for the answers no button can carry (`allow for session`, `cancel`,
|
|
1307
|
-
// and an elicitation's free-text content).
|
|
1308
1398
|
const pending = pendingRequestOf(connection);
|
|
1399
|
+
// A message is never taken as an approval: one sent while an approval waits would otherwise allow the tool and
|
|
1400
|
+
// never reach the agent. It is refused, with what to do, like a send during any running turn.
|
|
1401
|
+
if (pending?.kind === 'approval') {
|
|
1402
|
+
throw new Error(`${busyRefusal(connection, 'sending another message') ?? 'The current chat is waiting for your answer.'} Nothing was sent.`);
|
|
1403
|
+
}
|
|
1309
1404
|
if (pending) {
|
|
1405
|
+
// A question that needs typed content: the message is its answer, and its resolution line says so.
|
|
1310
1406
|
try {
|
|
1311
|
-
|
|
1312
|
-
await connection.session.respond(answer);
|
|
1313
|
-
stream.markdown(`_answered request #${pending.id}: ${opaqueText(answer, 256)}_`);
|
|
1407
|
+
await connection.session.respond(typedAnswer(pending, request.prompt));
|
|
1314
1408
|
}
|
|
1315
1409
|
catch (error) {
|
|
1316
|
-
|
|
1410
|
+
throw new Error(`Could not answer the question: ${opaqueText(String(error.message), 512)}`);
|
|
1317
1411
|
}
|
|
1318
1412
|
return {};
|
|
1319
1413
|
}
|
|
1320
|
-
if (request.prompt.trim().length === 0)
|
|
1321
|
-
stream.markdown('_nothing to submit_');
|
|
1414
|
+
if (request.prompt.trim().length === 0)
|
|
1322
1415
|
return {};
|
|
1323
|
-
}
|
|
1324
1416
|
const native = controls ? undefined : turnPermission(request.permissionLevel);
|
|
1325
1417
|
const approval = () => native ?? approvalFor(connection.id);
|
|
1326
1418
|
const attached = [];
|
|
@@ -1334,8 +1426,9 @@ export async function activate(context) {
|
|
|
1334
1426
|
attached.push({ name: uri.toString(), text: document.getText(location?.range) });
|
|
1335
1427
|
}
|
|
1336
1428
|
const prompt = withTextContext(request.prompt, attached);
|
|
1337
|
-
await runTurnInto(connection, prompt, stream, token, approval);
|
|
1338
|
-
|
|
1429
|
+
const details = await runTurnInto(connection, prompt, stream, token, approval);
|
|
1430
|
+
// The model the turn ran on, in VS Code's own response footer rather than as a line of the conversation.
|
|
1431
|
+
return details ? { details } : {};
|
|
1339
1432
|
};
|
|
1340
1433
|
const participants = [];
|
|
1341
1434
|
for (const id of granted.has('defaultChatParticipant') ? [PARTICIPANT_ID, DEFAULT_PARTICIPANT_ID] : [PARTICIPANT_ID]) {
|
|
@@ -1766,13 +1859,16 @@ function historyParts(parts) {
|
|
|
1766
1859
|
flush();
|
|
1767
1860
|
const toolName = part.toolName || 'tool';
|
|
1768
1861
|
const invocation = new vscode.ChatToolInvocationPart(toolName, part.toolCallId);
|
|
1769
|
-
invocation.invocationMessage = part.invocationMessage || toolName;
|
|
1770
|
-
invocation.pastTenseMessage = part.pastTenseMessage || part.invocationMessage || toolName;
|
|
1862
|
+
invocation.invocationMessage = new vscode.MarkdownString(part.invocationMessage || `Ran \`${toolName}\``);
|
|
1863
|
+
invocation.pastTenseMessage = new vscode.MarkdownString(part.pastTenseMessage || part.invocationMessage || `Ran \`${toolName}\``);
|
|
1771
1864
|
invocation.isConfirmed = true;
|
|
1772
1865
|
invocation.isComplete = part.isComplete;
|
|
1773
1866
|
if (part.isError !== undefined)
|
|
1774
1867
|
invocation.isError = part.isError;
|
|
1775
|
-
if (part.
|
|
1868
|
+
if (part.commandLine) {
|
|
1869
|
+
invocation.toolSpecificData = { commandLine: { original: part.commandLine }, language: 'sh', output: { text: part.resultText ?? '' } };
|
|
1870
|
+
}
|
|
1871
|
+
else if (part.isComplete) {
|
|
1776
1872
|
invocation.toolSpecificData = { input: part.inputText ?? '', output: part.resultText ?? '' };
|
|
1777
1873
|
}
|
|
1778
1874
|
out.push(invocation);
|
|
@@ -1832,7 +1928,7 @@ function registerSessions(context, frontends, participant, granted, handler, his
|
|
|
1832
1928
|
const items = () => {
|
|
1833
1929
|
return (controls?.catalogue() ?? []).filter(entry => !frontends.get(entry.id)).map(entry => {
|
|
1834
1930
|
const item = controller.createChatSessionItem(vscode.Uri.from({ scheme: SESSION_SCHEME, authority: entry.id, path: '/conversation' }), entry.title);
|
|
1835
|
-
item.description = runtimeIdentity(entry.selection.harness, entry.selection.model).name;
|
|
1931
|
+
item.description = runtimeIdentity(entry.selection.harness, entry.selection.model, controls?.effective(entry)).name;
|
|
1836
1932
|
item.status = vscode.ChatSessionStatus.Completed;
|
|
1837
1933
|
item.timing = { created: entry.created };
|
|
1838
1934
|
return item;
|
|
@@ -1974,7 +2070,7 @@ function registerSessions(context, frontends, participant, granted, handler, his
|
|
|
1974
2070
|
// already waiting and later ones; Ask leaves them for the person, as before.
|
|
1975
2071
|
const watch = controls && !controls.attached(connection) ? controls.watchApprovals(connection, stream, token) : undefined;
|
|
1976
2072
|
try {
|
|
1977
|
-
const state = { toolInputs: new Map(), toolNames: new Map(), quiet: watch?.answers };
|
|
2073
|
+
const state = { toolInputs: new Map(), toolNames: new Map(), toolTitles: new Map(), toolCommands: new Map(), quiet: watch?.answers };
|
|
1978
2074
|
writeProjected(stream, projectPendingRequests(connection.session.pendingRequests.values(), {
|
|
1979
2075
|
connectionId: connection.id,
|
|
1980
2076
|
canRespond: connection.session.descriptor?.actions.respond !== false,
|
package/dist/host-controls.d.ts
CHANGED
|
@@ -5,6 +5,12 @@ export interface ChatSelection {
|
|
|
5
5
|
model: string;
|
|
6
6
|
effort: string;
|
|
7
7
|
}
|
|
8
|
+
/** `modelName`: how to name `model` ("Claude Opus 5.5"); `effort` is '' when the model takes none. */
|
|
9
|
+
export interface EffectiveChat {
|
|
10
|
+
model: string;
|
|
11
|
+
modelName: string;
|
|
12
|
+
effort: string;
|
|
13
|
+
}
|
|
8
14
|
/** `efforts`: the reasoning efforts a model takes, as the host read them from the harness (absent from older hosts). */
|
|
9
15
|
export interface ChatChoice {
|
|
10
16
|
id: string;
|
|
@@ -51,6 +57,11 @@ export interface HostState {
|
|
|
51
57
|
modelsByHarness?: Record<string, ChatChoice[]>;
|
|
52
58
|
busy: boolean;
|
|
53
59
|
actualModel?: string | null;
|
|
60
|
+
/** The model and effort this conversation actually runs with, read from the harness's own configuration when the
|
|
61
|
+
* selection names none (and from the runtime once a turn has run). Absent from older hosts. */
|
|
62
|
+
effective?: EffectiveChat | null;
|
|
63
|
+
/** The same for a new chat of each startable harness. */
|
|
64
|
+
effectiveByHarness?: Record<string, EffectiveChat | null>;
|
|
54
65
|
configurable: boolean;
|
|
55
66
|
setup?: ChatSetupState;
|
|
56
67
|
/** The mode a new chat's picker shows. A conversation's own default is its `ManagedChatSession.defaultPermission`;
|
|
@@ -85,7 +96,9 @@ export declare class ChatHostControls {
|
|
|
85
96
|
/** A picked approval mode, never inferred from connection privileges (those only keep a host default at ask). */
|
|
86
97
|
export declare function turnPermission(value: string | undefined): 'ask' | 'autoApprove';
|
|
87
98
|
export declare function harnessDisplayName(harness: string | null | undefined): string;
|
|
88
|
-
|
|
99
|
+
/** The Chat's name for a runtime: the model and effort it actually runs with (`effective`, from the host) and its
|
|
100
|
+
* harness. It never says "default model": a harness whose model nobody reports is said to choose its own. */
|
|
101
|
+
export declare function runtimeIdentity(harness: string | null | undefined, model: string | null | undefined, effective?: EffectiveChat | null): {
|
|
89
102
|
name: string;
|
|
90
103
|
provider: string | undefined;
|
|
91
104
|
};
|
package/dist/host-controls.js
CHANGED
|
@@ -64,14 +64,17 @@ export function turnPermission(value) {
|
|
|
64
64
|
export function harnessDisplayName(harness) {
|
|
65
65
|
return { 'claude-code': 'Claude Code', codex: 'Codex', gemini: 'Gemini CLI', opencode: 'OpenCode', pi: 'Pi', goose: 'Goose' }[harness || ''] || harness || 'Assistant';
|
|
66
66
|
}
|
|
67
|
-
|
|
67
|
+
/** The Chat's name for a runtime: the model and effort it actually runs with (`effective`, from the host) and its
|
|
68
|
+
* harness. It never says "default model": a harness whose model nobody reports is said to choose its own. */
|
|
69
|
+
export function runtimeIdentity(harness, model, effective) {
|
|
68
70
|
const harnessName = harnessDisplayName(harness);
|
|
69
|
-
const knownModel = model && model !== harness ? model : '';
|
|
70
|
-
const modelName = { sonnet: 'Claude Sonnet', opus: 'Claude Opus', haiku: 'Claude Haiku' }[knownModel] || knownModel;
|
|
71
|
+
const knownModel = effective?.model || (model && model !== harness ? model : '');
|
|
72
|
+
const modelName = effective?.modelName || { sonnet: 'Claude Sonnet', opus: 'Claude Opus', haiku: 'Claude Haiku' }[knownModel] || knownModel;
|
|
71
73
|
const provider = /claude|sonnet|opus|haiku/i.test(modelName) || (!modelName && harness === 'claude-code') ? 'anthropic'
|
|
72
74
|
: /^(gpt|o[134]|chatgpt)|\/gpt/i.test(modelName) || (!modelName && harness === 'codex') ? 'openai'
|
|
73
75
|
: /gemini/i.test(modelName) || (!modelName && harness === 'gemini') ? 'google' : undefined;
|
|
74
|
-
|
|
76
|
+
const effort = effective?.effort ? ` · ${effective.effort}` : '';
|
|
77
|
+
return { name: modelName ? `${modelName}${effort}` : `Model chosen by ${harnessName}`, provider };
|
|
75
78
|
}
|
|
76
79
|
/** Native pickers need a nonempty id; the host uses an empty model for its default. */
|
|
77
80
|
export function modelSelection(value) { return value === "__default__" ? "" : value; }
|
package/dist/session.js
CHANGED
|
@@ -128,8 +128,31 @@ export class VscodeFrontendSession {
|
|
|
128
128
|
// none no matter what its replay still carries.
|
|
129
129
|
// Hosted Codex also receives standalone MCP elicitations. Its host retains
|
|
130
130
|
// request/resolved pairs without the native broker's turn compaction.
|
|
131
|
-
|
|
132
|
-
|
|
131
|
+
// BUT A HARNESS ALSO RUNS TURNS OF ITS OWN (a background task reporting back, a Monitor's event): the runtime then
|
|
132
|
+
// reads idle while the agent loop is blocked on an approval it raised. Clearing that one left the agent waiting
|
|
133
|
+
// forever with nothing to answer it (2026-10-07). Compaction happens only at a turn boundary, so a request raised
|
|
134
|
+
// AFTER the replay's last boundary and not resolved since is still open, idle or not; only older ones are dropped.
|
|
135
|
+
if (staged.descriptor?.turn_state !== 'busy' && staged.descriptor?.source_harness !== 'codex') {
|
|
136
|
+
// A turn's END, not `turn_completed`: that one is a model round-trip and fires mid-turn.
|
|
137
|
+
const BOUNDARIES = new Set(['turn_succeeded', 'turn_failed', 'turn_interrupted']);
|
|
138
|
+
let lastBoundary = -1;
|
|
139
|
+
const raisedAt = new Map();
|
|
140
|
+
for (const event of snapshot.replay) {
|
|
141
|
+
if (BOUNDARIES.has(event.kind))
|
|
142
|
+
lastBoundary = Math.max(lastBoundary, event.sequence);
|
|
143
|
+
const payload = (event.payload ?? {});
|
|
144
|
+
const request = event.kind === 'request' ? payload.request : undefined;
|
|
145
|
+
if (request && typeof request.id === 'number')
|
|
146
|
+
raisedAt.set(request.id, event.sequence);
|
|
147
|
+
}
|
|
148
|
+
for (const id of [...staged.pendingRequests.keys()]) {
|
|
149
|
+
const at = raisedAt.get(id);
|
|
150
|
+
// A request known from before this attachment, with no boundary since, is still the open one it was.
|
|
151
|
+
const open = at !== undefined ? at > lastBoundary : lastBoundary < 0 && this.pendingRequests.has(id);
|
|
152
|
+
if (!open)
|
|
153
|
+
staged.pendingRequests.delete(id);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
133
156
|
this.descriptor = staged.descriptor;
|
|
134
157
|
this.history = structuredClone(snapshot.history);
|
|
135
158
|
this.historyCursor = snapshot.history_cursor;
|
package/dist/view.d.ts
CHANGED
|
@@ -60,6 +60,8 @@ export type ChatPart = {
|
|
|
60
60
|
isError?: boolean;
|
|
61
61
|
isComplete: boolean;
|
|
62
62
|
resultText?: string;
|
|
63
|
+
/** A shell call's command line: the row is then VS Code's own terminal block, command and output. */
|
|
64
|
+
commandLine?: string;
|
|
63
65
|
} | {
|
|
64
66
|
part: 'answers';
|
|
65
67
|
approval: boolean;
|
|
@@ -93,6 +95,18 @@ export declare function diffPaths(diff: string): string[];
|
|
|
93
95
|
* wasted row; `write_file · hello.txt` says what it did.
|
|
94
96
|
*/
|
|
95
97
|
export declare function toolSubject(argumentsText: string): string | undefined;
|
|
98
|
+
/**
|
|
99
|
+
* A tool row's titles the way VS Code's own agent writes them: what the call does, in the present while it runs
|
|
100
|
+
* ("Reading `view.ts`") and the past once done ("Read `view.ts`"), read from the call's own arguments. Shell calls
|
|
101
|
+
* use the description the harness gave them (Claude Code's `Bash.description`) when there is one. A tool this does
|
|
102
|
+
* not know is "Ran `name`", with its subject when the arguments name one.
|
|
103
|
+
*/
|
|
104
|
+
export declare function toolTitles(toolName: string, argumentsText: string): {
|
|
105
|
+
invocation: string;
|
|
106
|
+
pastTense: string;
|
|
107
|
+
};
|
|
108
|
+
/** The command line of a shell tool call, read from its own arguments; undefined for any other tool. */
|
|
109
|
+
export declare function shellCommand(toolName: string, argumentsText: string): string | undefined;
|
|
96
110
|
/** The summary line this view shows for a request payload. */
|
|
97
111
|
export declare function requestSummary(request: FrontendRequest): string;
|
|
98
112
|
/** The answers this view offers for a request, one button each. */
|
|
@@ -164,6 +178,14 @@ export interface ProjectedTurn {
|
|
|
164
178
|
* supercode's own agent loop, and a hosted harness has none. So a resumed
|
|
165
179
|
* Claude Code session's turns are here or nowhere.
|
|
166
180
|
*/
|
|
181
|
+
/**
|
|
182
|
+
* A "user" message no person typed: Claude Code's `<task-notification>` (a background task reporting back) or the
|
|
183
|
+
* editor's automatic tripwire nudge. Neither is shown as the person's words. A task notification is said as one
|
|
184
|
+
* short line ("Background task completed: <its summary>"); a nudge is not shown. Undefined for a person's message.
|
|
185
|
+
*/
|
|
186
|
+
export declare function automaticPrompt(body: string): {
|
|
187
|
+
notice?: string;
|
|
188
|
+
} | undefined;
|
|
167
189
|
export declare function projectReplayTurns(events: readonly FrontendEvent[], granted: ReadonlySet<string>, context: ProjectionContext): ProjectedTurn[];
|
|
168
190
|
/**
|
|
169
191
|
* The attach snapshot's canonical history as chat turns. Bounded: when older
|
package/dist/view.js
CHANGED
|
@@ -98,6 +98,103 @@ export function toolSubject(argumentsText) {
|
|
|
98
98
|
}
|
|
99
99
|
return undefined;
|
|
100
100
|
}
|
|
101
|
+
const code = (value) => `\`${value.replace(/`/g, "'")}\``;
|
|
102
|
+
/** Free text in a title, which the Chat renders as Markdown: its own `*`, `_`, `[` and the like stay characters. */
|
|
103
|
+
const plain = (value) => value.replace(/[\\`*_{}[\]()<>#+!|~]/g, '\\$&');
|
|
104
|
+
const basename = (path) => path.split(/[\\/]/).filter(Boolean).pop() ?? path;
|
|
105
|
+
/**
|
|
106
|
+
* A tool row's titles the way VS Code's own agent writes them: what the call does, in the present while it runs
|
|
107
|
+
* ("Reading `view.ts`") and the past once done ("Read `view.ts`"), read from the call's own arguments. Shell calls
|
|
108
|
+
* use the description the harness gave them (Claude Code's `Bash.description`) when there is one. A tool this does
|
|
109
|
+
* not know is "Ran `name`", with its subject when the arguments name one.
|
|
110
|
+
*/
|
|
111
|
+
export function toolTitles(toolName, argumentsText) {
|
|
112
|
+
let fields = {};
|
|
113
|
+
try {
|
|
114
|
+
const parsed = JSON.parse(argumentsText);
|
|
115
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
|
|
116
|
+
fields = parsed;
|
|
117
|
+
}
|
|
118
|
+
catch { /* not JSON: titled by name */ }
|
|
119
|
+
const field = (...names) => {
|
|
120
|
+
for (const name of names) {
|
|
121
|
+
const value = fields[name];
|
|
122
|
+
if (typeof value === 'string' && value.trim())
|
|
123
|
+
return clamp(stripControls(value.trim()), 120);
|
|
124
|
+
if (Array.isArray(value) && value.every(part => typeof part === 'string') && value.length)
|
|
125
|
+
return clamp(stripControls(value.join(' ')), 120);
|
|
126
|
+
}
|
|
127
|
+
return '';
|
|
128
|
+
};
|
|
129
|
+
const both = (present, past) => ({ invocation: clamp(present, VSCODE_CHAT_LIMITS.titleCharacters), pastTense: clamp(past, VSCODE_CHAT_LIMITS.titleCharacters) });
|
|
130
|
+
const file = field('file_path', 'path', 'file', 'notebook_path');
|
|
131
|
+
const name = toolName.toLowerCase();
|
|
132
|
+
if (['bash', 'shell', 'exec_command', 'local_shell', 'powershell'].includes(name)) {
|
|
133
|
+
const description = field('description');
|
|
134
|
+
if (description)
|
|
135
|
+
return both(plain(description), plain(description));
|
|
136
|
+
const command = field('command', 'cmd');
|
|
137
|
+
return command ? both(`Running ${code(command)}`, `Ran ${code(command)}`) : both(`Running a command`, `Ran a command`);
|
|
138
|
+
}
|
|
139
|
+
if (name === 'read' && file) {
|
|
140
|
+
const offset = typeof fields.offset === 'number' ? fields.offset : undefined;
|
|
141
|
+
const limit = typeof fields.limit === 'number' ? fields.limit : undefined;
|
|
142
|
+
const lines = offset !== undefined && limit !== undefined ? `, lines ${offset} to ${offset + limit - 1}` : '';
|
|
143
|
+
return both(`Reading ${code(basename(file))}${lines}`, `Read ${code(basename(file))}${lines}`);
|
|
144
|
+
}
|
|
145
|
+
if (name === 'write' && file)
|
|
146
|
+
return both(`Writing ${code(basename(file))}`, `Wrote ${code(basename(file))}`);
|
|
147
|
+
if (['edit', 'multiedit', 'notebookedit', 'apply_patch'].includes(name))
|
|
148
|
+
return file ? both(`Editing ${code(basename(file))}`, `Edited ${code(basename(file))}`) : both('Editing files', 'Edited files');
|
|
149
|
+
if (name === 'grep') {
|
|
150
|
+
const pattern = field('pattern');
|
|
151
|
+
if (pattern)
|
|
152
|
+
return both(`Searching for ${code(pattern)}`, `Searched for ${code(pattern)}`);
|
|
153
|
+
}
|
|
154
|
+
if (name === 'glob') {
|
|
155
|
+
const pattern = field('pattern');
|
|
156
|
+
if (pattern)
|
|
157
|
+
return both(`Finding files matching ${code(pattern)}`, `Found files matching ${code(pattern)}`);
|
|
158
|
+
}
|
|
159
|
+
if (name === 'webfetch') {
|
|
160
|
+
const url = field('url');
|
|
161
|
+
if (url)
|
|
162
|
+
return both(`Fetching ${plain(url)}`, `Fetched ${plain(url)}`);
|
|
163
|
+
}
|
|
164
|
+
if (name === 'websearch' || name === 'web_search') {
|
|
165
|
+
const query = field('query');
|
|
166
|
+
if (query)
|
|
167
|
+
return both(`Searching the web for ${code(query)}`, `Searched the web for ${code(query)}`);
|
|
168
|
+
}
|
|
169
|
+
if (['task', 'agent'].includes(name)) {
|
|
170
|
+
const description = field('description');
|
|
171
|
+
if (description)
|
|
172
|
+
return both(`Running a subagent: ${plain(description)}`, `Ran a subagent: ${plain(description)}`);
|
|
173
|
+
}
|
|
174
|
+
if (name === 'todowrite' || name === 'update_plan')
|
|
175
|
+
return both('Updating the plan', 'Updated the plan');
|
|
176
|
+
// Claude Code names an MCP tool `mcp__<server>__<tool>`; VS Code writes "Ran `tool` – server".
|
|
177
|
+
const mcp = /^mcp__(.+?)__(.+)$/.exec(toolName);
|
|
178
|
+
if (mcp)
|
|
179
|
+
return both(`Running ${code(mcp[2])} – ${plain(mcp[1])}`, `Ran ${code(mcp[2])} – ${plain(mcp[1])}`);
|
|
180
|
+
const subject = toolSubject(argumentsText);
|
|
181
|
+
return subject ? both(`Running ${code(toolName)} · ${plain(subject)}`, `Ran ${code(toolName)} · ${plain(subject)}`) : both(`Running ${code(toolName)}`, `Ran ${code(toolName)}`);
|
|
182
|
+
}
|
|
183
|
+
/** The command line of a shell tool call, read from its own arguments; undefined for any other tool. */
|
|
184
|
+
export function shellCommand(toolName, argumentsText) {
|
|
185
|
+
if (!['bash', 'shell', 'exec_command', 'local_shell', 'powershell'].includes(toolName.toLowerCase()))
|
|
186
|
+
return undefined;
|
|
187
|
+
try {
|
|
188
|
+
const fields = JSON.parse(argumentsText);
|
|
189
|
+
const command = fields?.command ?? fields?.cmd;
|
|
190
|
+
if (typeof command === 'string' && command.trim())
|
|
191
|
+
return stripControls(command);
|
|
192
|
+
if (Array.isArray(command) && command.every(part => typeof part === 'string') && command.length)
|
|
193
|
+
return stripControls(command.join(' '));
|
|
194
|
+
}
|
|
195
|
+
catch { /* not JSON */ }
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
101
198
|
/** The summary line this view shows for a request payload. */
|
|
102
199
|
export function requestSummary(request) {
|
|
103
200
|
const payload = request.payload;
|
|
@@ -302,21 +399,21 @@ export function projectEvent(event, context) {
|
|
|
302
399
|
const toolCallId = text(payload.id, `event-${event.sequence}`);
|
|
303
400
|
const rawArguments = text(payload.arguments);
|
|
304
401
|
const inputText = clamp(stripControls(parseArguments(rawArguments)), VSCODE_CHAT_LIMITS.partCharacters);
|
|
305
|
-
const
|
|
402
|
+
const titles = toolTitles(toolName, rawArguments);
|
|
403
|
+
const commandLine = shellCommand(toolName, rawArguments);
|
|
306
404
|
return [{
|
|
307
405
|
emit: {
|
|
308
406
|
part: 'tool',
|
|
309
407
|
toolName,
|
|
310
408
|
toolCallId,
|
|
311
|
-
invocationMessage:
|
|
409
|
+
invocationMessage: titles.invocation,
|
|
410
|
+
pastTenseMessage: titles.pastTense,
|
|
312
411
|
inputText,
|
|
313
412
|
isComplete: false,
|
|
413
|
+
...(commandLine ? { commandLine } : {}),
|
|
314
414
|
},
|
|
315
415
|
needs: 'chatParticipantAdditions',
|
|
316
|
-
fallback: [{
|
|
317
|
-
part: 'progress',
|
|
318
|
-
value: clamp(subject ? `${toolName} · ${subject}` : toolName, VSCODE_CHAT_LIMITS.titleCharacters),
|
|
319
|
-
}],
|
|
416
|
+
fallback: [{ part: 'progress', value: titles.invocation }],
|
|
320
417
|
}];
|
|
321
418
|
}
|
|
322
419
|
// crates/runtime/src/event.rs `AgentEvent::ToolCallCompleted`.
|
|
@@ -328,13 +425,16 @@ export function projectEvent(event, context) {
|
|
|
328
425
|
const toolCallId = text(payload.id, `event-${event.sequence}`);
|
|
329
426
|
const output = clamp(stripControls(text(payload.output)), VSCODE_CHAT_LIMITS.partCharacters);
|
|
330
427
|
const isError = payload.is_error === true;
|
|
428
|
+
// The titles stay the ones the call started with (the writer keeps them per call); a completion that names its
|
|
429
|
+
// tool without having started on this stream is titled by that name.
|
|
430
|
+
const titles = toolName ? toolTitles(toolName, text(payload.arguments)) : undefined;
|
|
331
431
|
return [{
|
|
332
432
|
emit: {
|
|
333
433
|
part: 'tool',
|
|
334
434
|
toolName,
|
|
335
435
|
toolCallId,
|
|
336
|
-
invocationMessage:
|
|
337
|
-
pastTenseMessage:
|
|
436
|
+
invocationMessage: titles?.invocation ?? '',
|
|
437
|
+
pastTenseMessage: titles?.pastTense ?? '',
|
|
338
438
|
isError,
|
|
339
439
|
isComplete: true,
|
|
340
440
|
resultText: output,
|
|
@@ -410,6 +510,10 @@ export function projectEvent(event, context) {
|
|
|
410
510
|
case 'request_resolved': {
|
|
411
511
|
const requestId = typeof payload.request_id === 'number' ? payload.request_id : Number.NaN;
|
|
412
512
|
const response = payload.response;
|
|
513
|
+
// An allowed approval says nothing more, as in VS Code's own chat: its tool row carries on and is the record.
|
|
514
|
+
// A denial, and an answer to any other request, is said once.
|
|
515
|
+
if (response?.kind === 'approval' && response.decision !== 'deny')
|
|
516
|
+
return [];
|
|
413
517
|
const sentence = resolutionSentence(response, context.pendingRequest?.(requestId), requestId);
|
|
414
518
|
return [stable({ part: 'markdown', value: `\n\n_${sentence}_\n\n` })];
|
|
415
519
|
}
|
|
@@ -423,11 +527,9 @@ export function projectEvent(event, context) {
|
|
|
423
527
|
}];
|
|
424
528
|
}
|
|
425
529
|
// crates/runtime/src/event.rs `AgentEvent::Usage`.
|
|
530
|
+
// Token counts are not conversation: VS Code's own chat keeps them out of the transcript.
|
|
426
531
|
case 'usage':
|
|
427
|
-
return [
|
|
428
|
-
part: 'progress',
|
|
429
|
-
value: `prompt ${text(payload.prompt_tokens, '?')} · completion ${text(payload.completion_tokens, '?')} · total ${text(payload.total_tokens, '?')} tokens`,
|
|
430
|
-
})];
|
|
532
|
+
return [];
|
|
431
533
|
// crates/runtime/src/event.rs `AgentEvent::BackgroundOutput`.
|
|
432
534
|
case 'background_output': {
|
|
433
535
|
const chunk = fence(clamp(stripControls(text(payload.chunk)), VSCODE_CHAT_LIMITS.partCharacters));
|
|
@@ -438,8 +540,9 @@ export function projectEvent(event, context) {
|
|
|
438
540
|
})];
|
|
439
541
|
}
|
|
440
542
|
// crates/harness/src/server.rs `submit_claimed`.
|
|
543
|
+
// The live writer drops it when the person stopped the turn (VS Code marks a stopped response itself).
|
|
441
544
|
case 'turn_interrupted':
|
|
442
|
-
return [stable({ part: '
|
|
545
|
+
return [stable({ part: 'warning', value: 'The turn was interrupted.' })];
|
|
443
546
|
// crates/harness/src/server.rs `submit_claimed` emits `turn_failed`. The
|
|
444
547
|
// other four are the error kinds the donor-neutral reader in
|
|
445
548
|
// crates/frontend-model/src/transcript.rs treats identically:
|
|
@@ -555,6 +658,23 @@ function messageText(message) {
|
|
|
555
658
|
* supercode's own agent loop, and a hosted harness has none. So a resumed
|
|
556
659
|
* Claude Code session's turns are here or nowhere.
|
|
557
660
|
*/
|
|
661
|
+
/**
|
|
662
|
+
* A "user" message no person typed: Claude Code's `<task-notification>` (a background task reporting back) or the
|
|
663
|
+
* editor's automatic tripwire nudge. Neither is shown as the person's words. A task notification is said as one
|
|
664
|
+
* short line ("Background task completed: <its summary>"); a nudge is not shown. Undefined for a person's message.
|
|
665
|
+
*/
|
|
666
|
+
export function automaticPrompt(body) {
|
|
667
|
+
const trimmed = body.trimStart();
|
|
668
|
+
if (trimmed.startsWith('<task-notification>')) {
|
|
669
|
+
const tag = (name) => clamp(stripControls(new RegExp(`<${name}>([\\s\\S]*?)</${name}>`).exec(trimmed)?.[1]?.trim() ?? ''), 200);
|
|
670
|
+
const summary = tag('summary');
|
|
671
|
+
const status = tag('status');
|
|
672
|
+
return { notice: `_Background task${status ? ` ${status}` : ''}${summary ? `: ${summary}` : ''}_` };
|
|
673
|
+
}
|
|
674
|
+
if (trimmed.startsWith('[Volter editor tripwire'))
|
|
675
|
+
return {};
|
|
676
|
+
return undefined;
|
|
677
|
+
}
|
|
558
678
|
export function projectReplayTurns(events, granted, context) {
|
|
559
679
|
const turns = [];
|
|
560
680
|
let streamedText;
|
|
@@ -562,6 +682,20 @@ export function projectReplayTurns(events, granted, context) {
|
|
|
562
682
|
if (event.kind === 'user_message') {
|
|
563
683
|
streamedText = undefined;
|
|
564
684
|
const payload = payloadOf(event);
|
|
685
|
+
const automatic = automaticPrompt(text(payload.text));
|
|
686
|
+
if (automatic) {
|
|
687
|
+
if (!automatic.notice)
|
|
688
|
+
continue;
|
|
689
|
+
if (turns.length === 0)
|
|
690
|
+
turns.push({ role: 'user', prompt: HISTORY_MARKER_PROMPT });
|
|
691
|
+
const last = turns[turns.length - 1];
|
|
692
|
+
const notice = { part: 'markdown', value: `\n\n${automatic.notice}\n\n` };
|
|
693
|
+
if (last.role === 'assistant' && last.parts)
|
|
694
|
+
last.parts.push(notice);
|
|
695
|
+
else
|
|
696
|
+
turns.push({ role: 'assistant', parts: [notice] });
|
|
697
|
+
continue;
|
|
698
|
+
}
|
|
565
699
|
turns.push({ role: 'user', prompt: clamp(stripControls(text(payload.text)), VSCODE_CHAT_LIMITS.partCharacters) });
|
|
566
700
|
continue;
|
|
567
701
|
}
|
|
@@ -652,6 +786,12 @@ export function projectHistory(history, granted, pendingToolNames = new Set()) {
|
|
|
652
786
|
pushAssistant([{ part: 'markdown', value: clamp(stripControls(text), VSCODE_CHAT_LIMITS.partCharacters) }]);
|
|
653
787
|
continue;
|
|
654
788
|
}
|
|
789
|
+
const automatic = automaticPrompt(body);
|
|
790
|
+
if (automatic) {
|
|
791
|
+
if (automatic.notice)
|
|
792
|
+
pushAssistant([{ part: 'markdown', value: `\n\n${automatic.notice}\n\n` }]);
|
|
793
|
+
continue;
|
|
794
|
+
}
|
|
655
795
|
toolRows.clear();
|
|
656
796
|
turns.push({ role: 'user', prompt: clamp(stripControls(body), VSCODE_CHAT_LIMITS.partCharacters) });
|
|
657
797
|
continue;
|
|
@@ -675,14 +815,15 @@ export function projectHistory(history, granted, pendingToolNames = new Set()) {
|
|
|
675
815
|
pushAssistant([{ part: 'markdown', value: clamp(stripControls(body), VSCODE_CHAT_LIMITS.partCharacters) }]);
|
|
676
816
|
}
|
|
677
817
|
for (const call of message.tool_calls ?? []) {
|
|
678
|
-
const
|
|
679
|
-
const
|
|
818
|
+
const titles = toolTitles(call.function.name, call.function.arguments);
|
|
819
|
+
const commandLine = shellCommand(call.function.name, call.function.arguments);
|
|
680
820
|
const row = {
|
|
681
821
|
part: 'tool',
|
|
682
822
|
toolName: call.function.name,
|
|
683
823
|
toolCallId: call.id,
|
|
684
|
-
invocationMessage:
|
|
685
|
-
pastTenseMessage:
|
|
824
|
+
invocationMessage: titles.invocation,
|
|
825
|
+
pastTenseMessage: titles.pastTense,
|
|
826
|
+
...(commandLine ? { commandLine } : {}),
|
|
686
827
|
inputText: clamp(stripControls(parseArguments(call.function.arguments)), VSCODE_CHAT_LIMITS.partCharacters),
|
|
687
828
|
isComplete: true,
|
|
688
829
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-frontend-vscode",
|
|
3
3
|
"displayName": "Volter Harness",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.50",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "VS Code chat-view frontend for an existing Volter Harness SDK runtime",
|
|
7
7
|
"publisher": "volter-ai-dev",
|