@volter/supercode-frontend-vscode 0.1.29 → 0.1.31

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
@@ -323,6 +323,7 @@ export async function activate(context) {
323
323
  let applying = Promise.resolve();
324
324
  let pendingDraft;
325
325
  let focusedResource;
326
+ let focusGeneration = 0;
326
327
  const activeDraft = () => pendingDraft?.resource && pendingDraft.resource.toString() === focusedResource?.toString() ? pendingDraft : undefined;
327
328
  const draftSelection = () => {
328
329
  const choice = activeDraft()?.choice;
@@ -333,8 +334,10 @@ export async function activate(context) {
333
334
  focusedResource = resource;
334
335
  if (pendingDraft?.resource && pendingDraft.resource.toString() !== resource?.toString())
335
336
  pendingDraft = undefined;
336
- if (changed)
337
+ if (changed) {
338
+ focusGeneration++;
337
339
  controlsChanged.fire();
340
+ }
338
341
  };
339
342
  const frontends = new SupercodeFrontends();
340
343
  // Runtimes attached by address ("Set Runtime Token", "Attach to Runtime")
@@ -388,11 +391,12 @@ export async function activate(context) {
388
391
  const id = `${selection.harness}/${encodeURIComponent(model.id || '__default__')}`;
389
392
  modelSelections.set(id, { ...selection, model: model.id });
390
393
  const provider = runtimeIdentity(selection.harness, selection.model).provider;
391
- return { id, name: model.name, family: selection.harness, version: '1',
394
+ const name = model.id ? model.name : runtimeIdentity(selection.harness, model.id).name;
395
+ return { id, name, family: selection.harness, version: '1',
392
396
  statusIcon: new vscode.ThemeIcon(provider === 'anthropic' ? 'claude' : provider === 'openai' ? 'openai' : provider === 'google' ? 'google-gemini' : 'sparkle'),
393
397
  maxInputTokens: 0, maxOutputTokens: 0, capabilities: {},
394
398
  targetChatSessionType: SESSION_SCHEME, isUserSelectable: true, isDefault: model.id === selection.model,
395
- tooltip: `${model.name} via ${harnessDisplayName(selection.harness)}${selection.effort ? ` · ${selection.effort} reasoning` : ''}. Model is fixed for this conversation. Use Model settings to choose model and reasoning for a new conversation.`,
399
+ tooltip: `${name} via ${harnessDisplayName(selection.harness)}${selection.effort ? ` · ${selection.effort} reasoning` : ''}. Model is fixed for this conversation. Use Model settings to choose model and reasoning for a new conversation.`,
396
400
  };
397
401
  });
398
402
  },
@@ -406,11 +410,20 @@ export async function activate(context) {
406
410
  const id = resource ? boundId(resource) : controls?.state.activeSession;
407
411
  return { permissions: id ? permissions.get(id) ?? 'ask' : permission };
408
412
  };
409
- const connectManaged = async () => {
410
- const env = controls.state.connection;
411
- if (!env || !controls.state.activeSession)
413
+ const savedConnections = new Map();
414
+ const saveManagedHistory = async (connection) => {
415
+ const state = await controls.open(connection.id);
416
+ if (state.activeSession !== connection.id)
417
+ throw new Error('The conversation changed before its history could be saved.');
418
+ connection.persistedHistory = state.history;
419
+ connection.historyTruncated = state.historyTruncated;
420
+ savedConnections.set(connection.id, connection);
421
+ };
422
+ const connectManaged = async (state = controls.state) => {
423
+ const env = state.connection;
424
+ if (!env || !state.activeSession)
412
425
  throw new Error('The requested conversation has no runtime connection.');
413
- const id = controls.state.activeSession;
426
+ const id = state.activeSession;
414
427
  const existing = frontends.get(id);
415
428
  if (existing && existing.baseUrl === env.SUPERCODE_FRONTEND_URL && existing.session.connection !== 'closed') {
416
429
  if (existing.session.connection !== 'connected')
@@ -421,7 +434,7 @@ export async function activate(context) {
421
434
  await frontends.disconnect(connection.id);
422
435
  const connection = await frontends.connect({ connectionId: id, baseUrl: env.SUPERCODE_FRONTEND_URL,
423
436
  token: await readCredentialFile(env.SUPERCODE_FRONTEND_CREDENTIAL_FILE), clientId: env.SUPERCODE_FRONTEND_CLIENT_ID,
424
- permissions: normalizePermissions(env.SUPERCODE_FRONTEND_PERMISSIONS, ['observe']), label: runtimeIdentity(controls.state.selection.harness, controls.state.selection.model || controls.state.actualModel).name }, log);
437
+ permissions: normalizePermissions(env.SUPERCODE_FRONTEND_PERMISSIONS, ['observe']), label: runtimeIdentity(state.selection.harness, state.selection.model || state.actualModel).name }, log);
425
438
  if (!connection)
426
439
  throw new Error('Could not connect to the requested conversation.');
427
440
  return connection;
@@ -458,17 +471,20 @@ export async function activate(context) {
458
471
  };
459
472
  let refreshingSetup;
460
473
  const refreshSetup = () => {
461
- refreshingSetup ??= (async () => {
474
+ if (refreshingSetup)
475
+ return refreshingSetup;
476
+ const pending = applying.then(async () => {
462
477
  if (!controls)
463
478
  return;
464
479
  // Discover and publish readiness even while a blank chat holds a draft.
465
480
  // Only the automatic reveal is suppressed; discovery must never stall.
466
481
  await controls.refresh();
482
+ const state = controls.state;
467
483
  controlsChanged.fire();
468
484
  const preserveDraft = focusedResource?.path.startsWith('/untitled-');
469
- if (controls.state.setup?.ready) {
485
+ if (state.setup?.ready) {
470
486
  setupRevealed = false;
471
- await connectManaged();
487
+ await connectManaged(state);
472
488
  controlsChanged.fire();
473
489
  if (!preserveDraft && !setupHandoffComplete())
474
490
  await revealManaged();
@@ -479,7 +495,9 @@ export async function activate(context) {
479
495
  if (!preserveDraft)
480
496
  await revealSetup();
481
497
  }
482
- })().finally(() => { refreshingSetup = undefined; });
498
+ });
499
+ applying = pending.then(() => undefined, () => undefined);
500
+ refreshingSetup = pending.finally(() => { refreshingSetup = undefined; });
483
501
  return refreshingSetup;
484
502
  };
485
503
  const setupTerminals = new Map();
@@ -512,8 +530,8 @@ export async function activate(context) {
512
530
  setupTerminals.set(key, terminal);
513
531
  terminal.show();
514
532
  }));
515
- const connectionFor = async (resource) => {
516
- focusSession(resource);
533
+ const restoring = new Map();
534
+ const connectionFor = async (resource, activate = true) => {
517
535
  if (!controls) {
518
536
  const connection = frontends.get(resource.authority);
519
537
  if (!connection)
@@ -521,7 +539,7 @@ export async function activate(context) {
521
539
  return connection;
522
540
  }
523
541
  const attached = attachedIds.has(resource.authority) ? frontends.get(resource.authority) : undefined;
524
- if (focusedAttached !== attached) {
542
+ if (activate && focusedAttached !== attached) {
525
543
  focusedAttached = attached;
526
544
  controlsChanged.fire();
527
545
  }
@@ -530,26 +548,55 @@ export async function activate(context) {
530
548
  const id = boundId(resource);
531
549
  if (!id)
532
550
  throw new Error('This chat has no backend session binding. Start a new chat; no message was sent.');
533
- const active = managed();
534
- if (active && active.id !== id && (active.session.descriptor?.turn_state === 'busy' || active.session.pendingRequests.size))
535
- throw new Error('Finish or cancel the active turn before opening another conversation.');
551
+ // Content refresh is not a focus change. Replaying a previously connected
552
+ // conversation must not retire the runtime the person just selected.
553
+ if (!activate) {
554
+ const saved = frontends.get(id) ?? savedConnections.get(id);
555
+ if (saved)
556
+ return saved;
557
+ }
558
+ const existing = restoring.get(id);
559
+ // Coalesce adjacent requests only. A later A after a queued B must still
560
+ // activate A last, rather than borrowing an earlier A's completion.
561
+ if (existing?.completion === applying)
562
+ return existing.pending;
563
+ const pending = applying.then(async () => {
564
+ const active = managed();
565
+ if (active && active.id !== id && (active.session.descriptor?.turn_state === 'busy' || active.session.pendingRequests.size))
566
+ throw new Error('Finish or cancel the active turn before opening another conversation.');
567
+ if (active && active.id !== id) {
568
+ // Capture the exact saved transcript while its runtime still belongs
569
+ // to the host. Subsequent background content reads need no activation.
570
+ await saveManagedHistory(active);
571
+ }
572
+ let state;
573
+ try {
574
+ state = await controls.open(id);
575
+ }
576
+ catch (error) {
577
+ log.error(`Could not restore conversation ${id}: ${String(error)}`);
578
+ throw error;
579
+ }
580
+ if (state.activeSession !== id)
581
+ throw new Error('The host did not restore the requested conversation.');
582
+ const connection = await connectManaged(state);
583
+ connection.persistedHistory = state.history;
584
+ connection.historyTruncated = state.historyTruncated;
585
+ savedConnections.set(id, connection);
586
+ if (active !== connection)
587
+ controlsChanged.fire();
588
+ return connection;
589
+ });
590
+ const completion = pending.then(() => undefined, () => undefined);
591
+ restoring.set(id, { pending, completion });
592
+ applying = completion;
536
593
  try {
537
- await controls.open(id);
594
+ return await pending;
538
595
  }
539
- catch (error) {
540
- log.error(`Could not restore conversation ${id}: ${String(error)}`);
541
- throw error;
596
+ finally {
597
+ if (restoring.get(id)?.pending === pending)
598
+ restoring.delete(id);
542
599
  }
543
- const history = controls.state.history;
544
- const historyTruncated = controls.state.historyTruncated;
545
- const connection = await connectManaged();
546
- connection.persistedHistory = history;
547
- connection.historyTruncated = historyTruncated;
548
- // Resolving an already-focused session must not republish its catalogue
549
- // while the native widget is still opening that same resource.
550
- if (active !== connection)
551
- controlsChanged.fire();
552
- return connection;
553
600
  };
554
601
  async function applySelection(selection) {
555
602
  if (!controls)
@@ -557,9 +604,12 @@ export async function activate(context) {
557
604
  const active = managed();
558
605
  if (active?.session.descriptor?.turn_state === 'busy' || active?.session.pendingRequests.size)
559
606
  throw new Error('Finish or cancel the current turn before starting a new chat.');
560
- await controls.select(selection);
561
- await connectManaged();
607
+ if (active)
608
+ await saveManagedHistory(active);
609
+ const state = await controls.select(selection);
610
+ const connection = await connectManaged(state);
562
611
  controlsChanged.fire();
612
+ return connection;
563
613
  }
564
614
  const openNewChat = async () => {
565
615
  if (!controls || !granted.has('chatSessionsProvider'))
@@ -578,6 +628,7 @@ export async function activate(context) {
578
628
  const draft = { resource: vscode.Uri.from({ scheme: SESSION_SCHEME, path: `/untitled-${randomUUID()}` }) };
579
629
  pendingDraft = draft;
580
630
  focusedResource = draft.resource;
631
+ focusGeneration++;
581
632
  focusedAttached = undefined;
582
633
  controlsChanged.fire();
583
634
  await vscode.commands.executeCommand(controls.state.openSessionCommand, draft.resource.toString());
@@ -672,9 +723,8 @@ export async function activate(context) {
672
723
  const selection = choice?.kind === 'agent' ? choice.selection
673
724
  : { harness: loginHarness ?? controls.state.selection.harness, model: '', effort: '' };
674
725
  const pending = applying.then(() => applySelection(selection));
675
- applying = pending.catch(() => undefined);
676
- await pending;
677
- const connection = frontends.get(controls.state.activeSession);
726
+ applying = pending.then(() => undefined, () => undefined);
727
+ const connection = await pending;
678
728
  savePermission(connection.id, turnPermission(inputState.groups.find(group => group.id === 'permissions')?.selected?.id));
679
729
  if (pendingDraft === draft) {
680
730
  pendingDraft = undefined;
@@ -682,8 +732,7 @@ export async function activate(context) {
682
732
  }
683
733
  return connection;
684
734
  },
685
- resolve: connectionFor,
686
- focus: focusSession,
735
+ resolve: (resource) => connectionFor(resource, false),
687
736
  attached: (connection) => attachedIds.has(connection.id),
688
737
  catalogue: () => controls?.state.sessions ?? [],
689
738
  bindings: boundId,
@@ -695,7 +744,12 @@ export async function activate(context) {
695
744
  focusSession(resource ? vscode.Uri.parse(resource) : undefined);
696
745
  }));
697
746
  context.subscriptions.push(vscode.commands.registerCommand('supercode.frontend.openSession', async (id) => {
698
- await connectionFor(vscode.Uri.from({ scheme: SESSION_SCHEME, authority: id, path: '/conversation' }));
747
+ const resource = vscode.Uri.from({ scheme: SESSION_SCHEME, authority: id, path: '/conversation' });
748
+ focusSession(resource);
749
+ const generation = focusGeneration;
750
+ await connectionFor(resource);
751
+ if (generation !== focusGeneration)
752
+ return;
699
753
  await revealManaged();
700
754
  }));
701
755
  context.subscriptions.push(vscode.commands.registerCommand('supercode.frontend.configure', async (supplied) => {
@@ -945,7 +999,28 @@ export async function activate(context) {
945
999
  p.iconPath = icon();
946
1000
  };
947
1001
  context.subscriptions.push({ dispose: () => sessionParticipant.dispose() }, controlsChanged.event(updateIdentity), frontends.onDidChange(updateIdentity));
948
- registerSessions(context, frontends, sessionParticipant, granted, handler, controls ? providerControls : undefined);
1002
+ // Replayed turns outlive the active composer participant. Keep their
1003
+ // author tied to the saved harness, including passive background loads.
1004
+ const historyParticipants = new Map();
1005
+ const historyParticipant = (connection) => {
1006
+ if (!granted.has('chatParticipantPrivate'))
1007
+ return sessionParticipant.id;
1008
+ const harness = connection.session.descriptor?.source_harness
1009
+ ?? controls?.state.sessions.find(entry => entry.id === connection.id)?.selection.harness;
1010
+ if (!harness)
1011
+ return sessionParticipant.id;
1012
+ let author = historyParticipants.get(harness);
1013
+ if (!author) {
1014
+ const name = harnessDisplayName(harness);
1015
+ author = vscode.chat.createDynamicChatParticipant(`${SESSION_SCHEME}.history.${encodeURIComponent(harness)}`, { name: name.replace(/[^a-zA-Z0-9]/g, ''), fullName: name, publisherName: 'Volter' }, handler);
1016
+ const provider = runtimeIdentity(harness, '').provider;
1017
+ author.iconPath = provider ? vscode.Uri.joinPath(context.extensionUri, 'media', `${provider}.svg`) : new vscode.ThemeIcon('hubot');
1018
+ historyParticipants.set(harness, author);
1019
+ context.subscriptions.push(author);
1020
+ }
1021
+ return author.id;
1022
+ };
1023
+ registerSessions(context, frontends, sessionParticipant, granted, handler, historyParticipant, controls ? providerControls : undefined);
949
1024
  }
950
1025
  catch (error) {
951
1026
  log.warn(`Volter Harness chat sessions were not registered: ${String(error?.message ?? error)}`);
@@ -1315,7 +1390,7 @@ function applyTiming(item, connection) {
1315
1390
  lastRequestEnded: connection.session.lastTurnEndedMs ?? undefined,
1316
1391
  };
1317
1392
  }
1318
- function registerSessions(context, frontends, participant, granted, handler, controls) {
1393
+ function registerSessions(context, frontends, participant, granted, handler, historyParticipant, controls) {
1319
1394
  const turns = vscode;
1320
1395
  const items = () => {
1321
1396
  return (controls?.catalogue() ?? []).filter(entry => !frontends.get(entry.id)).map(entry => {
@@ -1369,12 +1444,15 @@ function registerSessions(context, frontends, participant, granted, handler, con
1369
1444
  }));
1370
1445
  }
1371
1446
  context.subscriptions.push(vscode.chat.registerChatSessionContentProvider(SESSION_SCHEME, {
1372
- provideChatSessionContent: async (resource, _token, { inputState }) => {
1373
- controls?.focus(resource);
1447
+ provideChatSessionContent: async (resource, token, { inputState }) => {
1448
+ if (token.isCancellationRequested)
1449
+ throw new vscode.CancellationError();
1374
1450
  const boundHandler = (request, chatContext, stream, token) => handler(request, { ...chatContext, chatSessionContext: { chatSessionItem: { resource, label: 'Conversation' } } }, stream, token);
1375
1451
  // Only a native untitled resource can create a conversation. Existing
1376
1452
  // resources must resolve their exact saved binding; there is no fallback.
1377
1453
  const connection = controls ? (resource.path.startsWith('/untitled-') && !controls.bindings(resource) ? undefined : await controls.resolve(resource)) : frontends.get(resource.authority);
1454
+ if (token.isCancellationRequested)
1455
+ throw new vscode.CancellationError();
1378
1456
  if (!connection) {
1379
1457
  return { history: [], requestHandler: boundHandler };
1380
1458
  }
@@ -1387,13 +1465,14 @@ function registerSessions(context, frontends, participant, granted, handler, con
1387
1465
  // `state.history`), so the copy taken when this window attached is
1388
1466
  // stale as soon as a turn runs. The generated client reuses the
1389
1467
  // already-open event stream, so this is a snapshot, not a reconnect.
1390
- const attached = await connection.session.attach().then(() => true).catch(() => {
1468
+ const attached = frontends.get(connection.id) === connection && await connection.session.attach().then(() => true).catch(() => {
1391
1469
  // An unreachable runtime replays what this window last saw.
1392
1470
  return false;
1393
1471
  });
1394
1472
  // The attach snapshot IS the session's history: the runtime's own
1395
1473
  // canonical transcript, replayed as turns.
1396
1474
  const history = [];
1475
+ const authorId = historyParticipant(connection);
1397
1476
  // A hosted harness runtime has no canonical ChatMessage history —
1398
1477
  // `HostedHarnessRuntime::attach` returns an empty one and puts the
1399
1478
  // conversation in `replay`. Read whichever the runtime actually
@@ -1419,13 +1498,13 @@ function registerSessions(context, frontends, participant, granted, handler, con
1419
1498
  canRespond: connection.session.descriptor?.actions.respond !== false,
1420
1499
  });
1421
1500
  if (connection.historyTruncated)
1422
- history.push(new turns.ChatRequestTurn('[Earlier conversation history]', undefined, [], participant.id, []), new turns.ChatResponseTurn([new vscode.ChatResponseMarkdownPart('_Showing the latest 120 saved messages. Earlier context remains in the harness session._')], {}, participant.id));
1501
+ 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));
1423
1502
  for (const turn of projected) {
1424
1503
  if (turn.role === 'user') {
1425
- history.push(new turns.ChatRequestTurn(turn.prompt ?? '', undefined, [], participant.id, []));
1504
+ history.push(new turns.ChatRequestTurn(turn.prompt ?? '', undefined, [], authorId, []));
1426
1505
  continue;
1427
1506
  }
1428
- history.push(new turns.ChatResponseTurn(historyParts(oneRowPerToolCall(turn.parts ?? [])), {}, participant.id));
1507
+ history.push(new turns.ChatResponseTurn(historyParts(oneRowPerToolCall(turn.parts ?? [])), {}, authorId));
1429
1508
  }
1430
1509
  const busy = connection.session.descriptor?.turn_state === 'busy';
1431
1510
  return {
@@ -55,6 +55,7 @@ export declare class ChatHostControls {
55
55
  readonly baseUrl: string;
56
56
  private readonly credential;
57
57
  state: HostState;
58
+ private updating;
58
59
  private constructor();
59
60
  static fromEnvironment(env?: NodeJS.ProcessEnv): Promise<ChatHostControls | undefined>;
60
61
  refresh(): Promise<void>;
@@ -67,6 +68,9 @@ export declare class ChatHostControls {
67
68
  arguments: string[];
68
69
  env?: Record<string, string>;
69
70
  }>;
71
+ /** Publish complete host replies in request order; a late refresh must not
72
+ * overwrite the connection returned by a newer lifecycle operation. */
73
+ private update;
70
74
  private request;
71
75
  }
72
76
  /** Approval policy is captured for one turn, never inferred from connection privileges. */
@@ -3,6 +3,7 @@ export class ChatHostControls {
3
3
  baseUrl;
4
4
  credential;
5
5
  state;
6
+ updating = Promise.resolve();
6
7
  constructor(baseUrl, credential, state) {
7
8
  this.baseUrl = baseUrl;
8
9
  this.credential = credential;
@@ -19,21 +20,28 @@ export class ChatHostControls {
19
20
  host.state = await host.request('/state');
20
21
  return host;
21
22
  }
22
- async refresh() { this.state = await this.request('/state'); }
23
+ async refresh() { await this.update('/state'); }
23
24
  async select(selection) {
24
- const result = await this.request('/select', selection);
25
- this.state = result;
26
- return result;
25
+ return this.update('/select', selection);
27
26
  }
28
- async remember(id, nativeId) { this.state = await this.request('/remember', { id, nativeId }); }
27
+ async remember(id, nativeId) { await this.update('/remember', { id, nativeId }); }
29
28
  async open(id) {
30
- const result = await this.request('/open', { id });
31
- this.state = result;
32
- return result;
29
+ return this.update('/open', { id });
33
30
  }
34
31
  async setup(kind, harness) {
35
32
  return this.request('/setup', { kind, harness });
36
33
  }
34
+ /** Publish complete host replies in request order; a late refresh must not
35
+ * overwrite the connection returned by a newer lifecycle operation. */
36
+ update(path, selection) {
37
+ const pending = this.updating.then(async () => {
38
+ const result = await this.request(path, selection);
39
+ this.state = result;
40
+ return result;
41
+ });
42
+ this.updating = pending.then(() => undefined, () => undefined);
43
+ return pending;
44
+ }
37
45
  async request(path, selection) {
38
46
  const response = await fetch(this.baseUrl + path, {
39
47
  method: selection ? 'POST' : 'GET', headers: { Authorization: `Bearer ${this.credential}`, 'Content-Type': 'application/json' },
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.29",
4
+ "version": "0.1.31",
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",