@volter/supercode-frontend-vscode 0.1.44 → 0.1.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -430,3 +430,21 @@ it never submits a new chat prompt. The host guards the addressed conversation,
430
430
  disables the choices during dispatch and retires them only after an acknowledged
431
431
  response or an already-resolved result. Hosts without this capability retain
432
432
  stable command buttons. Other request kinds retain their existing presentation.
433
+
434
+ ## External runtime handoff
435
+
436
+ `supercode.frontend.connect(handoff)` accepts a `SupercodeRuntimeHandoff` with
437
+ `baseUrl`, `clientId`, `connectionId`, `credentialFile`, optional permissions and
438
+ label. Credentials are read from the private file; command arguments contain no
439
+ raw token. External attachments retain their exact identity across reloads.
440
+ `supercode.frontend.openSession(connectionId)` opens that conversation;
441
+ `supercode.frontend.status(true)` reports connection state, conversation view
442
+ readiness/resource and rendered event cursor. View readiness follows history
443
+ restoration and live-response subscription, rather than a socket alone.
444
+ `supercode.frontend.disconnect(connectionId)` also forgets the saved attachment.
445
+ IDs belonging to managed conversations cannot be reused for external attachments.
446
+ Calling `connect` without arguments retains the interactive settings flow.
447
+
448
+ These are ordinary connection/view APIs for any external runtime. Playback or
449
+ recording policy belongs to its runtime/controller; a host needs no replay-specific
450
+ commands. [Replay recording](../../docs/guides/replay-recording.md) illustrates a consumer.
@@ -15,6 +15,12 @@ export interface SupercodeConnectOptions {
15
15
  */
16
16
  unavailable?: string;
17
17
  }
18
+ /** An external runtime handoff carries a private credential file, never a token in arguments. */
19
+ export interface SupercodeRuntimeHandoff extends Omit<SupercodeConnectOptions, 'token' | 'unavailable'> {
20
+ baseUrl: string;
21
+ clientId: string;
22
+ credentialFile: string;
23
+ }
18
24
  export interface SupercodeConnectionInfo {
19
25
  readonly id: string;
20
26
  readonly baseUrl: string;
@@ -22,6 +28,10 @@ export interface SupercodeConnectionInfo {
22
28
  readonly sessionId: string | null;
23
29
  readonly model: string | null;
24
30
  readonly turnState: 'idle' | 'busy' | null;
31
+ readonly connectionState: 'disconnected' | 'connected' | 'closed';
32
+ readonly viewReady: boolean;
33
+ readonly viewResource: string | null;
34
+ readonly cursor: number;
25
35
  }
26
36
  /** What `activate` returns: the door a host uses to hand over a connection. */
27
37
  export interface SupercodeFrontendApi {
package/dist/extension.js CHANGED
@@ -87,6 +87,10 @@ class SupercodeFrontends {
87
87
  sessionId: connection.session.descriptor?.session_id ?? null,
88
88
  model: connection.session.descriptor?.model ?? null,
89
89
  turnState: connection.session.descriptor?.turn_state ?? null,
90
+ connectionState: connection.session.connection,
91
+ viewReady: connection.viewReady === true && connection.session.connection === 'connected',
92
+ viewResource: connection.viewResource ?? null,
93
+ cursor: connection.session.cursor,
90
94
  };
91
95
  }
92
96
  async connect(options, log) {
@@ -411,7 +415,7 @@ export async function activate(context) {
411
415
  const boundId = (resource) => resolveSessionBinding(resource, controls?.state.sessions ?? [], resourceBindings);
412
416
  let applying = Promise.resolve();
413
417
  // the reason the last send from a new chat made no conversation, said by that draft's request handler
414
- let refusedSend;
418
+ const refusedSends = new Map(); // by the draft's resource ('' when the send named none)
415
419
  let pendingDraft;
416
420
  let focusedResource;
417
421
  let focusGeneration = 0;
@@ -436,6 +440,37 @@ export async function activate(context) {
436
440
  // them, so they open as what they are: attachments, shown as their runtime
437
441
  // reports them, and never taken for the conversation the host manages.
438
442
  const attachedIds = new Set();
443
+ const savedAttachments = new Map(context.workspaceState.get('supercode.externalAttachments', [])
444
+ .map(entry => [entry.connectionId, entry]));
445
+ const restoringAttachments = new Map();
446
+ const attachmentSecretKey = (id) => `supercode.frontend.attachment:${id}`;
447
+ const saveAttachments = () => context.workspaceState.update('supercode.externalAttachments', [...savedAttachments.values()]);
448
+ const restoreAttachment = (id) => {
449
+ const existing = frontends.get(id);
450
+ if (existing)
451
+ return Promise.resolve(existing);
452
+ const pending = restoringAttachments.get(id);
453
+ if (pending)
454
+ return pending;
455
+ const saved = savedAttachments.get(id);
456
+ if (!saved)
457
+ return Promise.resolve(undefined);
458
+ const restoration = (async () => {
459
+ const token = saved.credentialFile ? await readCredentialFile(saved.credentialFile) : await context.secrets.get(attachmentSecretKey(id));
460
+ if (!token)
461
+ throw new Error('The saved runtime credential is unavailable. Attach to the runtime again.');
462
+ const connection = await frontends.connect({ ...saved, token }, log);
463
+ if (connection)
464
+ attachedIds.add(id);
465
+ return connection;
466
+ })().finally(() => restoringAttachments.delete(id));
467
+ restoringAttachments.set(id, restoration);
468
+ return restoration;
469
+ };
470
+ for (const id of savedAttachments.keys()) {
471
+ attachedIds.add(id);
472
+ await restoreAttachment(id).catch(error => log.warn(`Could not restore runtime attachment ${id}: ${String(error)}`));
473
+ }
439
474
  const managed = () => frontends.all.find((connection) => !attachedIds.has(connection.id));
440
475
  // The host runs one conversation at a time, so a running or waiting turn refuses a new or another chat. This is
441
476
  // the sentence that refusal says to the person; each caller picks the one place it is shown (see its uses).
@@ -448,7 +483,8 @@ export async function activate(context) {
448
483
  return `A turn is still running in the current chat. Let it finish or cancel it before ${action}.`;
449
484
  };
450
485
  // The attached runtime the person is looking at: the Chat names its harness, not the host's.
451
- let focusedAttached;
486
+ const restoredFocus = context.workspaceState.get('supercode.focusedAttachment');
487
+ let focusedAttached = restoredFocus ? frontends.get(restoredFocus) : undefined;
452
488
  let approvalAvailable = false;
453
489
  context.subscriptions.push(frontends.onDidChange(() => {
454
490
  const active = managed();
@@ -661,7 +697,25 @@ export async function activate(context) {
661
697
  terminal.show();
662
698
  }));
663
699
  const restoring = new Map();
664
- const connectionFor = async (resource, activate = true) => {
700
+ const connectionFor = async (resource, activate = true, token) => {
701
+ if (savedAttachments.has(resource.authority)) {
702
+ let attached;
703
+ while (!attached) {
704
+ if (token?.isCancellationRequested)
705
+ throw new vscode.CancellationError();
706
+ try {
707
+ attached = await restoreAttachment(resource.authority);
708
+ if (!attached)
709
+ throw new Error('This runtime attachment is no longer saved.');
710
+ }
711
+ catch (error) {
712
+ log.warn(`Waiting for runtime attachment ${resource.authority}: ${String(error)}`);
713
+ if (!token)
714
+ throw error;
715
+ await new Promise(resolve => setTimeout(resolve, 500));
716
+ }
717
+ }
718
+ }
665
719
  if (!controls) {
666
720
  const connection = frontends.get(resource.authority);
667
721
  if (!connection)
@@ -671,6 +725,7 @@ export async function activate(context) {
671
725
  const attached = attachedIds.has(resource.authority) ? frontends.get(resource.authority) : undefined;
672
726
  if (activate && focusedAttached !== attached) {
673
727
  focusedAttached = attached;
728
+ await context.workspaceState.update('supercode.focusedAttachment', attached?.id);
674
729
  controlsChanged.fire();
675
730
  }
676
731
  if (attached)
@@ -734,12 +789,10 @@ export async function activate(context) {
734
789
  if (!controls)
735
790
  throw new Error('This host does not expose harness or model configuration.');
736
791
  const active = managed();
737
- // A send from a blank chat lands here, and the workbench does not show its error: say it, then refuse.
792
+ // its caller (a new chat's first send) says the refusal in that chat
738
793
  const refusal = busyRefusal(active, 'starting a new chat');
739
- if (refusal) {
740
- void vscode.window.showWarningMessage(refusal);
794
+ if (refusal)
741
795
  throw new Error(refusal);
742
- }
743
796
  if (active)
744
797
  await saveManagedHistory(active);
745
798
  const state = await controls.select(selection);
@@ -851,7 +904,9 @@ export async function activate(context) {
851
904
  if (busy)
852
905
  throw new Error(busy);
853
906
  const draft = activeDraft();
854
- await draft?.choosing;
907
+ // the picker ends in a cancellation for a sign-in choice too: the draft's choice says which (sign-in is refused
908
+ // below, in the chat); a draft that went away is the real cancellation
909
+ await draft?.choosing?.catch(() => undefined);
855
910
  if (draft && activeDraft() !== draft)
856
911
  throw new vscode.CancellationError();
857
912
  await controls.refresh();
@@ -928,11 +983,11 @@ export async function activate(context) {
928
983
  catch (error) {
929
984
  if (error instanceof vscode.CancellationError)
930
985
  throw error;
931
- refusedSend = String(error?.message ?? error);
986
+ refusedSends.set(activeDraft()?.resource.toString() ?? '', String(error?.message ?? error));
932
987
  return undefined;
933
988
  }
934
989
  },
935
- resolve: (resource) => connectionFor(resource, false),
990
+ resolve: (resource, token) => connectionFor(resource, false, token),
936
991
  attached: (connection) => attachedIds.has(connection.id),
937
992
  catalogue: () => controls?.state.sessions ?? [],
938
993
  bindings: boundId,
@@ -951,7 +1006,13 @@ export async function activate(context) {
951
1006
  await connectionFor(resource);
952
1007
  if (generation !== focusGeneration)
953
1008
  return;
954
- await revealManaged();
1009
+ if (attachedIds.has(id)) {
1010
+ if (!controls?.state.openSessionCommand)
1011
+ throw new Error('The host cannot reveal this conversation.');
1012
+ await vscode.commands.executeCommand(controls.state.openSessionCommand, resource.toString());
1013
+ }
1014
+ else
1015
+ await revealManaged();
955
1016
  }));
956
1017
  context.subscriptions.push(vscode.commands.registerCommand('supercode.frontend.configure', async (supplied) => {
957
1018
  if (supplied?.permissions) {
@@ -1177,9 +1238,11 @@ export async function activate(context) {
1177
1238
  const handler = async (request, chatContext, stream, token) => {
1178
1239
  await applying;
1179
1240
  const requestedResource = chatContext.chatSessionContext?.chatSessionItem?.resource;
1180
- if (refusedSend && requestedResource?.path.startsWith('/untitled-') && !boundId(requestedResource)) {
1181
- const refusal = refusedSend;
1182
- refusedSend = undefined;
1241
+ const refusedKey = requestedResource?.path.startsWith('/untitled-') && !boundId(requestedResource)
1242
+ ? [requestedResource.toString(), ''].find(key => refusedSends.has(key)) : undefined;
1243
+ if (refusedKey !== undefined) {
1244
+ const refusal = refusedSends.get(refusedKey);
1245
+ refusedSends.delete(refusedKey);
1183
1246
  // thrown as is: the workbench writes a handler's error into the chat
1184
1247
  throw new Error(`${refusal} Nothing was sent; send it again in a new chat.`);
1185
1248
  }
@@ -1389,7 +1452,17 @@ export async function activate(context) {
1389
1452
  await settings.update('baseUrl', baseUrl, vscode.ConfigurationTarget.Global);
1390
1453
  await context.secrets.store(secretKey(baseUrl), token);
1391
1454
  await connectFromSettings();
1392
- }), vscode.commands.registerCommand('supercode.frontend.connect', () => connectFromSettings()),
1455
+ }), vscode.commands.registerCommand('supercode.frontend.connect', async (handoff) => {
1456
+ if (handoff === undefined)
1457
+ return connectFromSettings();
1458
+ if (!handoff || typeof handoff.baseUrl !== 'string' || typeof handoff.clientId !== 'string' || !handoff.clientId.trim() || typeof handoff.credentialFile !== 'string') {
1459
+ throw new TypeError('A runtime handoff needs baseUrl, clientId and credentialFile.');
1460
+ }
1461
+ const token = await readCredentialFile(handoff.credentialFile);
1462
+ const connection = await connectExternal({ baseUrl: handoff.baseUrl, token, clientId: handoff.clientId,
1463
+ connectionId: handoff.connectionId, permissions: handoff.permissions, label: handoff.label }, handoff.credentialFile);
1464
+ return connection ? frontends.info(connection) : undefined;
1465
+ }),
1393
1466
  // The create command the core's own `chatSessions/newSession` menu invokes
1394
1467
  // for this session type (`chatSessions.contribution.ts`
1395
1468
  // `_resolveCreateSubMenuCommandId`). It opens a chat on the supercode
@@ -1402,7 +1475,7 @@ export async function activate(context) {
1402
1475
  }
1403
1476
  const where = position === 'editor' ? 'editor' : 'sidebar';
1404
1477
  await vscode.commands.executeCommand(`workbench.action.chat.openNewChatSessionInPlace.${SESSION_SCHEME}`, where);
1405
- }), vscode.commands.registerCommand('supercode.frontend.disconnect', () => frontends.disconnect()),
1478
+ }), vscode.commands.registerCommand('supercode.frontend.disconnect', (id) => disconnectExternal(id)),
1406
1479
  // The one door an answer button uses. It answers the runtime's pending
1407
1480
  // request through `frontend.v2.respond` and narrates itself, because a
1408
1481
  // click whose effect is invisible is a click nobody can trust.
@@ -1432,6 +1505,40 @@ export async function activate(context) {
1432
1505
  : REQUESTED_PROPOSALS.map((name) => `${name} not granted: ${PROPOSAL_EFFECTS[name].without}`).join('\n'),
1433
1506
  });
1434
1507
  }));
1508
+ async function connectExternal(options, credentialFile) {
1509
+ const id = options.connectionId ?? options.clientId;
1510
+ if (id && controls?.state.sessions.some(session => session.id === id))
1511
+ throw new Error('An external runtime cannot replace a host-managed conversation.');
1512
+ const existing = id ? frontends.get(id) : undefined;
1513
+ if (existing && existing.baseUrl !== options.baseUrl)
1514
+ throw new Error('This connection identity already belongs to another runtime.');
1515
+ const connection = await frontends.connect(options, log);
1516
+ if (connection && options.token) {
1517
+ if (!credentialFile)
1518
+ await context.secrets.store(attachmentSecretKey(connection.id), options.token);
1519
+ savedAttachments.set(connection.id, { baseUrl: connection.baseUrl, clientId: options.clientId ?? connection.id,
1520
+ connectionId: connection.id, permissions: [...connection.session.permissions], label: connection.label, credentialFile });
1521
+ attachedIds.add(connection.id);
1522
+ await saveAttachments();
1523
+ controlsChanged.fire();
1524
+ }
1525
+ return connection;
1526
+ }
1527
+ async function disconnectExternal(id) {
1528
+ const ids = id ? [id] : [...savedAttachments.keys()];
1529
+ for (const target of ids) {
1530
+ savedAttachments.delete(target);
1531
+ attachedIds.delete(target);
1532
+ await context.secrets.delete(attachmentSecretKey(target));
1533
+ if (focusedAttached?.id === target)
1534
+ focusedAttached = undefined;
1535
+ }
1536
+ await saveAttachments();
1537
+ if (!focusedAttached)
1538
+ await context.workspaceState.update('supercode.focusedAttachment', undefined);
1539
+ await frontends.disconnect(id);
1540
+ controlsChanged.fire();
1541
+ }
1435
1542
  async function connectFromSettings() {
1436
1543
  const settings = vscode.workspace.getConfiguration('supercode.frontend');
1437
1544
  const baseUrl = settings.get('baseUrl')?.trim() || context.globalState.get(HANDED_OVER_KEY);
@@ -1442,16 +1549,11 @@ export async function activate(context) {
1442
1549
  log.info('Volter Harness: a base URL is configured but no token is stored; run Volter Harness: Set Runtime Token.');
1443
1550
  return;
1444
1551
  }
1445
- const connection = await frontends
1446
- .connect({ baseUrl, token, permissions: normalizePermissions(settings.get('permissions'), ['observe']) }, log)
1552
+ await connectExternal({ baseUrl, token, permissions: normalizePermissions(settings.get('permissions'), ['observe']) })
1447
1553
  .catch((error) => {
1448
1554
  log.error(`Volter Harness could not attach to ${baseUrl}: ${String(error?.message ?? error)}`);
1449
1555
  return undefined;
1450
1556
  });
1451
- if (controls && connection) {
1452
- attachedIds.add(connection.id);
1453
- controlsChanged.fire();
1454
- }
1455
1557
  }
1456
1558
  /**
1457
1559
  * The handover path: a runtime (or a product that launched this editor's
@@ -1505,6 +1607,12 @@ export async function activate(context) {
1505
1607
  async function openResumedSession() {
1506
1608
  if (!granted.has('chatSessionsProvider'))
1507
1609
  return;
1610
+ const focusedId = context.workspaceState.get('supercode.focusedAttachment');
1611
+ const attached = focusedId ? frontends.get(focusedId) : undefined;
1612
+ if (attached && controls?.state.openSessionCommand) {
1613
+ await vscode.commands.executeCommand('supercode.frontend.openSession', attached.id);
1614
+ return;
1615
+ }
1508
1616
  if (controls) {
1509
1617
  await refreshSetup();
1510
1618
  return;
@@ -1552,10 +1660,10 @@ export async function activate(context) {
1552
1660
  context.subscriptions.push({ dispose: () => { setupDisposed = true; clearTimeout(setupTimer); } });
1553
1661
  return {
1554
1662
  connect: async (options) => {
1555
- const connection = await frontends.connect(options, log);
1663
+ const connection = await connectExternal(options);
1556
1664
  return connection ? frontends.info(connection) : undefined;
1557
1665
  },
1558
- disconnect: (connectionId) => frontends.disconnect(connectionId),
1666
+ disconnect: (connectionId) => disconnectExternal(connectionId),
1559
1667
  get connections() {
1560
1668
  return frontends.all.map((connection) => frontends.info(connection));
1561
1669
  },
@@ -1754,12 +1862,14 @@ function registerSessions(context, frontends, participant, granted, handler, his
1754
1862
  const boundHandler = (request, chatContext, stream, token) => handler(request, { ...chatContext, chatSessionContext: { chatSessionItem: { resource, label: 'Conversation' } } }, stream, token);
1755
1863
  // Only a native untitled resource can create a conversation. Existing
1756
1864
  // resources must resolve their exact saved binding; there is no fallback.
1757
- const connection = controls ? (resource.path.startsWith('/untitled-') && !controls.bindings(resource) ? undefined : await controls.resolve(resource)) : frontends.get(resource.authority);
1865
+ const connection = controls ? (resource.path.startsWith('/untitled-') && !controls.bindings(resource) ? undefined : await controls.resolve(resource, token)) : frontends.get(resource.authority);
1758
1866
  if (token.isCancellationRequested)
1759
1867
  throw new vscode.CancellationError();
1760
1868
  if (!connection) {
1761
1869
  return { history: [], requestHandler: boundHandler };
1762
1870
  }
1871
+ connection.viewReady = false;
1872
+ connection.viewResource = resource.toString();
1763
1873
  if (controls && !controls.attached(connection)) {
1764
1874
  controls.setPermission(resource, inputState.groups.find(group => group.id === 'permissions')?.selected?.id);
1765
1875
  inputState.groups = controls.options(resource);
@@ -1811,6 +1921,9 @@ function registerSessions(context, frontends, participant, granted, handler, his
1811
1921
  history.push(new turns.ChatResponseTurn(historyParts(oneRowPerToolCall(turn.parts ?? [])), {}, authorId));
1812
1922
  }
1813
1923
  const busy = connection.session.descriptor?.turn_state === 'busy';
1924
+ const renderedCursor = connection.session.cursor;
1925
+ if (!busy)
1926
+ connection.viewReady = attached;
1814
1927
  return {
1815
1928
  title: connection.label,
1816
1929
  options: controls?.attached(connection) ? undefined : controls?.currentOptions(resource),
@@ -1836,9 +1949,10 @@ function registerSessions(context, frontends, participant, granted, handler, his
1836
1949
  canRespond: connection.session.descriptor?.actions.respond !== false,
1837
1950
  }), granted, state);
1838
1951
  watch?.sweep();
1839
- });
1952
+ }, () => { connection.viewReady = true; }, renderedCursor);
1840
1953
  }
1841
1954
  finally {
1955
+ connection.viewReady = connection.session.descriptor?.turn_state !== 'busy';
1842
1956
  watch?.dispose();
1843
1957
  }
1844
1958
  }
package/dist/index.d.ts CHANGED
@@ -4,4 +4,4 @@ export type { ChatGPTSignInWorkPhase, ChatGPTSignInOperation } from './chat-setu
4
4
  export { grantedProposals, PROPOSAL_EFFECTS, REQUESTED_PROPOSALS, type ProposalName, } from './proposals.js';
5
5
  export { EventSequenceGapError, LEASE_ERROR_NAMES, RequestNotPendingError, TURN_TERMINAL_KINDS, VscodeFrontendSession, type VscodeFrontendActions, type VscodeRuntimeClient, } from './session.js';
6
6
  export { HISTORY_MARKER_PROMPT, VSCODE_CHAT_LIMITS, acceptedDecisions, diffPaths, fence, opaqueText, projectEvent, projectPendingRequests, projectHistory, projectReplayTurns, requestAnswers, requestNeedsTypedReply, requestSubject, requestSummary, resolutionSentence, resolveParts, toolSubject, typedAnswer, type ChatAnswer, type ChatPart, type ProjectedPart, type ProjectedTurn, type ProjectionContext, } from './view.js';
7
- export type { SupercodeConnectOptions, SupercodeConnectionInfo, SupercodeFrontendApi, } from './extension.js';
7
+ export type { SupercodeConnectOptions, SupercodeRuntimeHandoff, SupercodeConnectionInfo, SupercodeFrontendApi, } from './extension.js';
@@ -3,4 +3,4 @@ import type { FrontendEvent } from '../vendor/supercode-frontend/generated.mjs';
3
3
  import { type VscodeFrontendSession } from './session.js';
4
4
  export declare function sessionStatus(session: Pick<VscodeFrontendSession, 'descriptor' | 'pendingRequests'>): 'NeedsInput' | 'InProgress' | 'Completed';
5
5
  /** Follow a restored turn, including the interval between snapshot and callback. */
6
- export declare function observeActiveResponse(session: Pick<VscodeFrontendSession, 'descriptor' | 'onEvent' | 'interrupt'>, token: CancellationToken, write: (event: FrontendEvent) => void): Promise<void>;
6
+ export declare function observeActiveResponse(session: Pick<VscodeFrontendSession, 'descriptor' | 'onEvent' | 'interrupt' | 'actions' | 'replay'>, token: CancellationToken, write: (event: FrontendEvent) => void, onSubscribed?: () => void, afterSequence?: number): Promise<void>;
@@ -3,7 +3,7 @@ export function sessionStatus(session) {
3
3
  return session.pendingRequests.size ? 'NeedsInput' : session.descriptor?.turn_state === 'busy' ? 'InProgress' : 'Completed';
4
4
  }
5
5
  /** Follow a restored turn, including the interval between snapshot and callback. */
6
- export async function observeActiveResponse(session, token, write) {
6
+ export async function observeActiveResponse(session, token, write, onSubscribed, afterSequence) {
7
7
  let unsubscribe = () => { };
8
8
  let cancellation;
9
9
  try {
@@ -13,10 +13,20 @@ export async function observeActiveResponse(session, token, write) {
13
13
  if (cancelling)
14
14
  return;
15
15
  cancelling = true;
16
+ // Closing an observed view cannot interrupt a runtime that grants no
17
+ // interrupt action. Stop observing without inventing control authority.
18
+ if (!session.actions().interrupt) {
19
+ resolve();
20
+ return;
21
+ }
16
22
  // Wait for the runtime acknowledgement; a stopped UI is not a stopped harness.
17
23
  void session.interrupt().then(resolve, reject);
18
24
  };
19
- unsubscribe = session.onEvent(event => {
25
+ let delivered = afterSequence ?? 0;
26
+ const consume = (event) => {
27
+ if (afterSequence !== undefined && event.sequence <= delivered)
28
+ return;
29
+ delivered = event.sequence;
20
30
  try {
21
31
  write(event);
22
32
  }
@@ -26,7 +36,14 @@ export async function observeActiveResponse(session, token, write) {
26
36
  }
27
37
  if (!cancelling && TURN_TERMINAL_KINDS.includes(event.kind))
28
38
  resolve();
29
- });
39
+ };
40
+ unsubscribe = session.onEvent(consume);
41
+ if (afterSequence !== undefined) {
42
+ for (const event of session.replay)
43
+ if (event.sequence > afterSequence)
44
+ consume(event);
45
+ }
46
+ onSubscribed?.();
30
47
  cancellation = token.onCancellationRequested(cancel);
31
48
  if (token.isCancellationRequested)
32
49
  cancel();
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.44",
4
+ "version": "0.1.46",
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",
@@ -190,7 +190,7 @@
190
190
  },
191
191
  "supercodeFrontendContract": {
192
192
  "name": "@volter/supercode-frontend",
193
- "version": "0.2.7",
193
+ "version": "0.2.8",
194
194
  "mode": "generated-artifact"
195
195
  },
196
196
  "license": "MIT OR Apache-2.0"
@@ -73,7 +73,7 @@
73
73
  "argument_hint": {"type": "string", "optional": true, "nullable": true}
74
74
  }
75
75
  },
76
- "FrontendOperationKind": {"kind": "enum", "values": ["prompt", "file", "model", "session", "subagent", "image", "reduction"]},
76
+ "FrontendOperationKind": {"kind": "enum", "values": ["prompt", "file", "model", "session", "subagent", "image", "reduction", "replay"]},
77
77
  "FrontendOperationDescriptor": {
78
78
  "kind": "object",
79
79
  "fields": {
@@ -175,6 +175,10 @@
175
175
  "prompt": {
176
176
  "operation_id": {"type": "string"},
177
177
  "arguments": {"type": "string"}
178
+ },
179
+ "replay": {
180
+ "operation_id": {"type": "string"},
181
+ "control": {"type": "string"}
178
182
  }
179
183
  }
180
184
  },
@@ -182,7 +186,8 @@
182
186
  "kind": "tagged_union",
183
187
  "tag": "kind",
184
188
  "variants": {
185
- "prompt": {"reply": {"type": "string"}}
189
+ "prompt": {"reply": {"type": "string"}},
190
+ "replay": {"status": {"type": "JsonValue"}}
186
191
  }
187
192
  },
188
193
  "RuntimeControllerLease": {
@@ -32,7 +32,7 @@ export interface FrontendCommandDescriptor {
32
32
  description: string | null;
33
33
  argument_hint?: string | null;
34
34
  }
35
- export type FrontendOperationKind = "prompt" | "file" | "model" | "session" | "subagent" | "image" | "reduction";
35
+ export type FrontendOperationKind = "prompt" | "file" | "model" | "session" | "subagent" | "image" | "reduction" | "replay";
36
36
  export interface FrontendOperationDescriptor {
37
37
  id: string;
38
38
  kind: FrontendOperationKind;
@@ -84,8 +84,8 @@ export interface FrontendAttachSnapshot {
84
84
  export type FrontendApprovalDecision = "deny" | "allow" | "allow_for_session";
85
85
  export type FrontendElicitationAction = "accept" | "decline" | "cancel";
86
86
  export type FrontendResponse = { kind: "approval"; request_id: number; decision: FrontendApprovalDecision } | { kind: "elicitation"; request_id: number; action: FrontendElicitationAction; content?: JsonValue | null } | { kind: "other"; request_id: number; action: FrontendElicitationAction; content?: JsonValue | null };
87
- export type FrontendOperationInvocation = { kind: "prompt"; operation_id: string; arguments: string };
88
- export type FrontendOperationResult = { kind: "prompt"; reply: string };
87
+ export type FrontendOperationInvocation = { kind: "prompt"; operation_id: string; arguments: string } | { kind: "replay"; operation_id: string; control: string };
88
+ export type FrontendOperationResult = { kind: "prompt"; reply: string } | { kind: "replay"; status: JsonValue };
89
89
  export interface RuntimeControllerLease {
90
90
  client_id: string;
91
91
  expires_at_ms: number;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-frontend",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
4
4
  "type": "module",
5
5
  "description": "Generated language-neutral attachment client for Volter Harness SDK runtimes",
6
6
  "exports": {