@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/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.markdown(opaqueText(fallback));
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.answers[0].requestId))
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 was not shown because this watch was going to answer them.
1551
- const unshown = new Set();
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
- // A card this watch hid is shown when its automatic answer fails or does not settle: a typed reply cannot
1587
- // reach a running turn, its buttons can. With the response closed, the person is told an approval waits.
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 (!unshown.delete(pending.id) || !connection.session.pendingRequests.has(pending.id))
1596
+ if (redrawn.has(pending.id) || !connection.session.pendingRequests.has(pending.id))
1590
1597
  return;
1598
+ redrawn.add(pending.id);
1591
1599
  try {
1592
- writeProjected(stream, projectPendingRequests([pending], {
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.session.descriptor?.actions.respond !== false,
1595
- }), granted, { toolInputs: new Map(), toolNames: new Map(), toolTitles: new Map(), toolCommands: new Map(), toolKinds: new Map() });
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
- note(`\nAuto approve has had no answer for ${label} for ${AUTO_ANSWER_SETTLE_MS / 1000} s. Use the approval buttons to answer.\n`);
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
- note(`\nApproval failed: ${String(error.message || error)}. Use the approval buttons to answer.\n`);
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
- canRespond: connection.session.descriptor?.actions.respond !== false,
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
- // The harness's own background tasks (a Monitor, a background shell): each event one reports is a turn of its own,
1655
- // which this message waits behind, and nothing here would show it. Said once per change of the set, each with a
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; only an interruption from elsewhere is said.
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
- writeProjected(stream, projectEvent(event, { ...projection, liveUserMessages: turnStarted }), granted, state);
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
- return completed && ran && !token.isCancellationRequested ? `${opaqueText(ran, 120)}${effort ? ` · ${opaqueText(effort, 24)}` : ''}` : undefined;
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
- stream.markdown('Sign in before sending a message. Use **New Chat: Choose Agent** to get started.');
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
- stream.markdown(unavailableMarkdown(frontends.unavailable));
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
- const details = await runTurnInto(connection, prompt, stream, token, approval);
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
- await connection.session.respond(response);
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([new vscode.ChatResponseMarkdownPart(`**Conversation recovery failed**\n\n${message}`)], {}, 'supercode'),
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
- : part));
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
- if (usingSavedHistory && connection.historyTruncated)
2518
- history.push(new turns.ChatRequestTurn('[Earlier conversation history]', undefined, [], authorId, []), new turns.ChatResponseTurn([new vscode.ChatResponseMarkdownPart('_Showing the latest 120 saved messages. Earlier context remains in the harness session._')], {}, authorId));
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([new vscode.ChatResponseMarkdownPart(`**Conversation recovery failed**\n\n${opaqueText(connection.session.failure.message, 2048)}`)], {}, authorId));
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
- stream.markdown(`\n\n**Conversation stream failed**\n\n${opaqueText(String(error?.message ?? error), 2048)}\n\n`);
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;
@@ -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 participant-detection APIs a bundling host can use to route without a mention',
31
- without: 'nothing is lost in this package: it registers no dynamic participant and no detection provider',
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
- * The harness's own background tasks as it last listed them (`background_tasks`). Each event one reports starts a
103
- * turn of its own, so while any runs the session can be busy with turns no one here submitted.
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
- * The harness's own background tasks as it last listed them (`background_tasks`). Each event one reports starts a
64
- * turn of its own, so while any runs the session can be busy with turns no one here submitted.
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))