@volter/supercode-frontend-vscode 0.1.63 → 0.1.65

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