@volter/supercode-frontend-vscode 0.1.28 → 0.1.30
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 +25 -3
- package/dist/extension.js +262 -74
- package/dist/host-controls.d.ts +4 -0
- package/dist/host-controls.js +16 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -73,12 +73,29 @@ picker includes the official choice on supported hosts; with no choices it retur
|
|
|
73
73
|
to setup. Model
|
|
74
74
|
and reasoning are sent as empty values, leaving both at the harness's own defaults,
|
|
75
75
|
even if the previous conversation used explicit settings. Cancelling the agent
|
|
76
|
-
picker
|
|
76
|
+
picker starts no backend conversation.
|
|
77
|
+
|
|
78
|
+
The native blank chat opens before the picker appears. First send waits for the
|
|
79
|
+
choice and the host's new connection through Code-OSS's session-creation handler;
|
|
80
|
+
the composer keeps the draft until the request is accepted. There is no later
|
|
81
|
+
conversation swap that can hide a message sent while the agent starts. This follows
|
|
82
|
+
the pinned workbench's [first-send materialization](https://github.com/volter-ai/code-oss/blob/f16dc165c0dffe701a7bbf59aefb1c662b206cee/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts#L1272)
|
|
83
|
+
and [draft acceptance](https://github.com/volter-ai/code-oss/blob/f16dc165c0dffe701a7bbf59aefb1c662b206cee/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts#L3493)
|
|
84
|
+
paths. A failed startup leaves the draft available to retry.
|
|
85
|
+
|
|
86
|
+
The pending agent/model choice belongs to that blank chat's untitled resource
|
|
87
|
+
and is retired when another conversation opens. Cancelling or opening sign-in
|
|
88
|
+
does not choose an agent. Readiness polling and **Check again** still discover
|
|
89
|
+
agents while a draft is open, updating the welcome and model catalogue without
|
|
90
|
+
replacing its widget or input. First send refreshes readiness; after a native
|
|
91
|
+
sign-in choice it uses that agent once ready, otherwise it keeps the draft with
|
|
92
|
+
a visible sign-in error.
|
|
77
93
|
|
|
78
94
|
The composer's native **Model settings** control offers **Choose model and reasoning
|
|
79
95
|
for a new chat…** for Claude Code and Codex. This optional path retains the model
|
|
80
96
|
list, exact model ID entry and reasoning choices. Only completing the choices
|
|
81
|
-
|
|
97
|
+
configures the new blank chat; its backend starts on first send. Cancelling a
|
|
98
|
+
choice starts no backend and leaves the blank chat available.
|
|
82
99
|
A conversation's harness, model and reasoning remain fixed: this integration does
|
|
83
100
|
not advertise hot swapping. The built-in model picker still displays the selected
|
|
84
101
|
model and provider icon; nondelegating session providers cannot add its Manage
|
|
@@ -93,7 +110,12 @@ The host publishes durable conversation IDs and their exact harness history iden
|
|
|
93
110
|
reported native session ID to discovered history. Unknown or missing bindings fail explicitly,
|
|
94
111
|
never falling back to the currently active runtime. The host may provide an `openSessionCommand`
|
|
95
112
|
to reveal an exact resource through its native widget service. Cached native widgets should
|
|
96
|
-
notify `supercode.frontend.
|
|
113
|
+
notify `supercode.frontend.focusSession` on every native focused-resource change,
|
|
114
|
+
including untitled resources and leaving harness Chat (`undefined` when there is
|
|
115
|
+
no focused session). This retires abandoned draft choices even when returning to
|
|
116
|
+
the same cached conversation. Managed resources then notify
|
|
117
|
+
`supercode.frontend.activateSession` to restore their exact backend. The frontend
|
|
118
|
+
owns no processes.
|
|
97
119
|
|
|
98
120
|
The permission group offers **Ask for approval** and, on an answerable interactive attachment,
|
|
99
121
|
**Auto approve**. Policies are scoped by conversation and captured for each turn; automatic
|
package/dist/extension.js
CHANGED
|
@@ -321,6 +321,24 @@ export async function activate(context) {
|
|
|
321
321
|
const resourceBindings = new Map(context.workspaceState.get('supercode.sessionBindings', []));
|
|
322
322
|
const boundId = (resource) => resolveSessionBinding(resource, controls?.state.sessions ?? [], resourceBindings);
|
|
323
323
|
let applying = Promise.resolve();
|
|
324
|
+
let pendingDraft;
|
|
325
|
+
let focusedResource;
|
|
326
|
+
let focusGeneration = 0;
|
|
327
|
+
const activeDraft = () => pendingDraft?.resource && pendingDraft.resource.toString() === focusedResource?.toString() ? pendingDraft : undefined;
|
|
328
|
+
const draftSelection = () => {
|
|
329
|
+
const choice = activeDraft()?.choice;
|
|
330
|
+
return choice?.kind === 'agent' ? choice.selection : undefined;
|
|
331
|
+
};
|
|
332
|
+
const focusSession = (resource) => {
|
|
333
|
+
const changed = focusedResource?.toString() !== resource?.toString();
|
|
334
|
+
focusedResource = resource;
|
|
335
|
+
if (pendingDraft?.resource && pendingDraft.resource.toString() !== resource?.toString())
|
|
336
|
+
pendingDraft = undefined;
|
|
337
|
+
if (changed) {
|
|
338
|
+
focusGeneration++;
|
|
339
|
+
controlsChanged.fire();
|
|
340
|
+
}
|
|
341
|
+
};
|
|
324
342
|
const frontends = new SupercodeFrontends();
|
|
325
343
|
// Runtimes attached by address ("Set Runtime Token", "Attach to Runtime")
|
|
326
344
|
// beside a host's managed conversation. The host's catalogue does not know
|
|
@@ -343,6 +361,9 @@ export async function activate(context) {
|
|
|
343
361
|
if (focusedAttached)
|
|
344
362
|
return runtimeIdentity(focusedAttached.session.descriptor?.source_harness, focusedAttached.session.descriptor?.model);
|
|
345
363
|
const descriptor = managed()?.session.descriptor;
|
|
364
|
+
const selection = draftSelection();
|
|
365
|
+
if (selection)
|
|
366
|
+
return runtimeIdentity(selection.harness, selection.model);
|
|
346
367
|
return runtimeIdentity(controls?.state.selection.harness ?? descriptor?.source_harness, controls?.state.selection.model || controls?.state.actualModel || descriptor?.model);
|
|
347
368
|
};
|
|
348
369
|
const icon = () => {
|
|
@@ -357,7 +378,9 @@ export async function activate(context) {
|
|
|
357
378
|
provideLanguageModelChatInformation: () => {
|
|
358
379
|
if (controls.state.setup && !controls.state.setup.ready)
|
|
359
380
|
return [];
|
|
360
|
-
const
|
|
381
|
+
const draft = draftSelection();
|
|
382
|
+
const selection = draft ?? controls.state.selection;
|
|
383
|
+
const models = draft ? controls.state.modelsByHarness?.[selection.harness] ?? [] : controls.state.models;
|
|
361
384
|
// A running harness does not advertise hot model changes. Configuration
|
|
362
385
|
// belongs to the explicit new-conversation flow.
|
|
363
386
|
const choices = models.filter(model => model.id === selection.model);
|
|
@@ -386,11 +409,20 @@ export async function activate(context) {
|
|
|
386
409
|
const id = resource ? boundId(resource) : controls?.state.activeSession;
|
|
387
410
|
return { permissions: id ? permissions.get(id) ?? 'ask' : permission };
|
|
388
411
|
};
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
412
|
+
const savedConnections = new Map();
|
|
413
|
+
const saveManagedHistory = async (connection) => {
|
|
414
|
+
const state = await controls.open(connection.id);
|
|
415
|
+
if (state.activeSession !== connection.id)
|
|
416
|
+
throw new Error('The conversation changed before its history could be saved.');
|
|
417
|
+
connection.persistedHistory = state.history;
|
|
418
|
+
connection.historyTruncated = state.historyTruncated;
|
|
419
|
+
savedConnections.set(connection.id, connection);
|
|
420
|
+
};
|
|
421
|
+
const connectManaged = async (state = controls.state) => {
|
|
422
|
+
const env = state.connection;
|
|
423
|
+
if (!env || !state.activeSession)
|
|
392
424
|
throw new Error('The requested conversation has no runtime connection.');
|
|
393
|
-
const id =
|
|
425
|
+
const id = state.activeSession;
|
|
394
426
|
const existing = frontends.get(id);
|
|
395
427
|
if (existing && existing.baseUrl === env.SUPERCODE_FRONTEND_URL && existing.session.connection !== 'closed') {
|
|
396
428
|
if (existing.session.connection !== 'connected')
|
|
@@ -401,7 +433,7 @@ export async function activate(context) {
|
|
|
401
433
|
await frontends.disconnect(connection.id);
|
|
402
434
|
const connection = await frontends.connect({ connectionId: id, baseUrl: env.SUPERCODE_FRONTEND_URL,
|
|
403
435
|
token: await readCredentialFile(env.SUPERCODE_FRONTEND_CREDENTIAL_FILE), clientId: env.SUPERCODE_FRONTEND_CLIENT_ID,
|
|
404
|
-
permissions: normalizePermissions(env.SUPERCODE_FRONTEND_PERMISSIONS, ['observe']), label:
|
|
436
|
+
permissions: normalizePermissions(env.SUPERCODE_FRONTEND_PERMISSIONS, ['observe']), label: runtimeIdentity(state.selection.harness, state.selection.model || state.actualModel).name }, log);
|
|
405
437
|
if (!connection)
|
|
406
438
|
throw new Error('Could not connect to the requested conversation.');
|
|
407
439
|
return connection;
|
|
@@ -438,23 +470,33 @@ export async function activate(context) {
|
|
|
438
470
|
};
|
|
439
471
|
let refreshingSetup;
|
|
440
472
|
const refreshSetup = () => {
|
|
441
|
-
|
|
473
|
+
if (refreshingSetup)
|
|
474
|
+
return refreshingSetup;
|
|
475
|
+
const pending = applying.then(async () => {
|
|
442
476
|
if (!controls)
|
|
443
477
|
return;
|
|
478
|
+
// Discover and publish readiness even while a blank chat holds a draft.
|
|
479
|
+
// Only the automatic reveal is suppressed; discovery must never stall.
|
|
444
480
|
await controls.refresh();
|
|
445
|
-
|
|
481
|
+
const state = controls.state;
|
|
482
|
+
controlsChanged.fire();
|
|
483
|
+
const preserveDraft = focusedResource?.path.startsWith('/untitled-');
|
|
484
|
+
if (state.setup?.ready) {
|
|
446
485
|
setupRevealed = false;
|
|
447
|
-
await connectManaged();
|
|
486
|
+
await connectManaged(state);
|
|
448
487
|
controlsChanged.fire();
|
|
449
|
-
if (!setupHandoffComplete())
|
|
488
|
+
if (!preserveDraft && !setupHandoffComplete())
|
|
450
489
|
await revealManaged();
|
|
451
490
|
}
|
|
452
491
|
else {
|
|
453
492
|
setupHandoff = undefined;
|
|
454
493
|
controlsChanged.fire();
|
|
455
|
-
|
|
494
|
+
if (!preserveDraft)
|
|
495
|
+
await revealSetup();
|
|
456
496
|
}
|
|
457
|
-
})
|
|
497
|
+
});
|
|
498
|
+
applying = pending.then(() => undefined, () => undefined);
|
|
499
|
+
refreshingSetup = pending.finally(() => { refreshingSetup = undefined; });
|
|
458
500
|
return refreshingSetup;
|
|
459
501
|
};
|
|
460
502
|
const setupTerminals = new Map();
|
|
@@ -487,7 +529,8 @@ export async function activate(context) {
|
|
|
487
529
|
setupTerminals.set(key, terminal);
|
|
488
530
|
terminal.show();
|
|
489
531
|
}));
|
|
490
|
-
const
|
|
532
|
+
const restoring = new Map();
|
|
533
|
+
const connectionFor = async (resource, activate = true) => {
|
|
491
534
|
if (!controls) {
|
|
492
535
|
const connection = frontends.get(resource.authority);
|
|
493
536
|
if (!connection)
|
|
@@ -495,7 +538,7 @@ export async function activate(context) {
|
|
|
495
538
|
return connection;
|
|
496
539
|
}
|
|
497
540
|
const attached = attachedIds.has(resource.authority) ? frontends.get(resource.authority) : undefined;
|
|
498
|
-
if (focusedAttached !== attached) {
|
|
541
|
+
if (activate && focusedAttached !== attached) {
|
|
499
542
|
focusedAttached = attached;
|
|
500
543
|
controlsChanged.fire();
|
|
501
544
|
}
|
|
@@ -504,43 +547,124 @@ export async function activate(context) {
|
|
|
504
547
|
const id = boundId(resource);
|
|
505
548
|
if (!id)
|
|
506
549
|
throw new Error('This chat has no backend session binding. Start a new chat; no message was sent.');
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
550
|
+
// Content refresh is not a focus change. Replaying a previously connected
|
|
551
|
+
// conversation must not retire the runtime the person just selected.
|
|
552
|
+
if (!activate) {
|
|
553
|
+
const saved = frontends.get(id) ?? savedConnections.get(id);
|
|
554
|
+
if (saved)
|
|
555
|
+
return saved;
|
|
556
|
+
}
|
|
557
|
+
const existing = restoring.get(id);
|
|
558
|
+
// Coalesce adjacent requests only. A later A after a queued B must still
|
|
559
|
+
// activate A last, rather than borrowing an earlier A's completion.
|
|
560
|
+
if (existing?.completion === applying)
|
|
561
|
+
return existing.pending;
|
|
562
|
+
const pending = applying.then(async () => {
|
|
563
|
+
const active = managed();
|
|
564
|
+
if (active && active.id !== id && (active.session.descriptor?.turn_state === 'busy' || active.session.pendingRequests.size))
|
|
565
|
+
throw new Error('Finish or cancel the active turn before opening another conversation.');
|
|
566
|
+
if (active && active.id !== id) {
|
|
567
|
+
// Capture the exact saved transcript while its runtime still belongs
|
|
568
|
+
// to the host. Subsequent background content reads need no activation.
|
|
569
|
+
await saveManagedHistory(active);
|
|
570
|
+
}
|
|
571
|
+
let state;
|
|
572
|
+
try {
|
|
573
|
+
state = await controls.open(id);
|
|
574
|
+
}
|
|
575
|
+
catch (error) {
|
|
576
|
+
log.error(`Could not restore conversation ${id}: ${String(error)}`);
|
|
577
|
+
throw error;
|
|
578
|
+
}
|
|
579
|
+
if (state.activeSession !== id)
|
|
580
|
+
throw new Error('The host did not restore the requested conversation.');
|
|
581
|
+
const connection = await connectManaged(state);
|
|
582
|
+
connection.persistedHistory = state.history;
|
|
583
|
+
connection.historyTruncated = state.historyTruncated;
|
|
584
|
+
savedConnections.set(id, connection);
|
|
585
|
+
if (active !== connection)
|
|
586
|
+
controlsChanged.fire();
|
|
587
|
+
return connection;
|
|
588
|
+
});
|
|
589
|
+
const completion = pending.then(() => undefined, () => undefined);
|
|
590
|
+
restoring.set(id, { pending, completion });
|
|
591
|
+
applying = completion;
|
|
510
592
|
try {
|
|
511
|
-
await
|
|
593
|
+
return await pending;
|
|
512
594
|
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
595
|
+
finally {
|
|
596
|
+
if (restoring.get(id)?.pending === pending)
|
|
597
|
+
restoring.delete(id);
|
|
516
598
|
}
|
|
517
|
-
const history = controls.state.history;
|
|
518
|
-
const historyTruncated = controls.state.historyTruncated;
|
|
519
|
-
const connection = await connectManaged();
|
|
520
|
-
connection.persistedHistory = history;
|
|
521
|
-
connection.historyTruncated = historyTruncated;
|
|
522
|
-
// Resolving an already-focused session must not republish its catalogue
|
|
523
|
-
// while the native widget is still opening that same resource.
|
|
524
|
-
if (active !== connection)
|
|
525
|
-
controlsChanged.fire();
|
|
526
|
-
return connection;
|
|
527
599
|
};
|
|
528
|
-
async function applySelection(selection
|
|
600
|
+
async function applySelection(selection) {
|
|
529
601
|
if (!controls)
|
|
530
602
|
throw new Error('This host does not expose harness or model configuration.');
|
|
531
603
|
const active = managed();
|
|
532
604
|
if (active?.session.descriptor?.turn_state === 'busy' || active?.session.pendingRequests.size)
|
|
533
605
|
throw new Error('Finish or cancel the current turn before starting a new chat.');
|
|
534
|
-
|
|
535
|
-
|
|
606
|
+
if (active)
|
|
607
|
+
await saveManagedHistory(active);
|
|
608
|
+
const state = await controls.select(selection);
|
|
609
|
+
const connection = await connectManaged(state);
|
|
536
610
|
controlsChanged.fire();
|
|
537
|
-
|
|
538
|
-
await revealManaged();
|
|
611
|
+
return connection;
|
|
539
612
|
}
|
|
540
|
-
const
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
613
|
+
const openNewChat = async () => {
|
|
614
|
+
if (!controls || !granted.has('chatSessionsProvider'))
|
|
615
|
+
throw new Error('This host does not expose native conversation creation.');
|
|
616
|
+
const active = managed();
|
|
617
|
+
if (active?.session.descriptor?.turn_state === 'busy' || active?.session.pendingRequests.size)
|
|
618
|
+
throw new Error('Finish or cancel the current turn before starting a new chat.');
|
|
619
|
+
// Code-OSS materializes this untitled session on first send, awaiting our
|
|
620
|
+
// newChatSessionItemHandler before accepting/clearing the draft. Never boot
|
|
621
|
+
// beside the old widget and reveal another resource after it accepts input.
|
|
622
|
+
if (!controls.state.openSessionCommand)
|
|
623
|
+
throw new Error('The host cannot reveal a new conversation.');
|
|
624
|
+
// Use Code-OSS's untitled URI format and its existing native widget bridge.
|
|
625
|
+
// Blank sessions resolve content lazily, so a content-provider callback
|
|
626
|
+
// cannot be used to discover which resource owns the pending selection.
|
|
627
|
+
const draft = { resource: vscode.Uri.from({ scheme: SESSION_SCHEME, path: `/untitled-${randomUUID()}` }) };
|
|
628
|
+
pendingDraft = draft;
|
|
629
|
+
focusedResource = draft.resource;
|
|
630
|
+
focusGeneration++;
|
|
631
|
+
focusedAttached = undefined;
|
|
632
|
+
controlsChanged.fire();
|
|
633
|
+
await vscode.commands.executeCommand(controls.state.openSessionCommand, draft.resource.toString());
|
|
634
|
+
if (pendingDraft !== draft)
|
|
635
|
+
throw new vscode.CancellationError();
|
|
636
|
+
return draft;
|
|
637
|
+
};
|
|
638
|
+
const chooseNewChat = async (pick) => {
|
|
639
|
+
if (pendingDraft?.choosing)
|
|
640
|
+
throw new Error('Finish choosing an agent before starting another chat.');
|
|
641
|
+
// Open before the picker: a send can beat its result across the extension-
|
|
642
|
+
// host boundary. Materialization awaits this exact draft's choice too.
|
|
643
|
+
const draft = await openNewChat();
|
|
644
|
+
const picking = (async () => {
|
|
645
|
+
const choice = await pick();
|
|
646
|
+
if (activeDraft() !== draft)
|
|
647
|
+
throw new vscode.CancellationError();
|
|
648
|
+
draft.choice = choice;
|
|
649
|
+
controlsChanged.fire();
|
|
650
|
+
if (choice.kind !== 'agent')
|
|
651
|
+
throw new vscode.CancellationError();
|
|
652
|
+
})();
|
|
653
|
+
draft.choosing = picking;
|
|
654
|
+
try {
|
|
655
|
+
await picking;
|
|
656
|
+
}
|
|
657
|
+
catch (error) {
|
|
658
|
+
if (!(error instanceof vscode.CancellationError))
|
|
659
|
+
throw error;
|
|
660
|
+
}
|
|
661
|
+
finally {
|
|
662
|
+
if (draft.choosing === picking)
|
|
663
|
+
draft.choosing = undefined;
|
|
664
|
+
if (draft.choice?.kind === 'signIn') {
|
|
665
|
+
void refreshSetup().catch(error => log.warn(`Could not refresh sign-in: ${String(error)}`)).finally(pollSetup);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
544
668
|
};
|
|
545
669
|
// Publish capability-honest input groups only after the handoff descriptor is available.
|
|
546
670
|
if (controls?.state.connection)
|
|
@@ -561,7 +685,7 @@ export async function activate(context) {
|
|
|
561
685
|
if (selected && ['claude-code', 'codex'].includes(selected.selection.harness)) {
|
|
562
686
|
// Native session-option commands remain reachable for nondelegating providers,
|
|
563
687
|
// whose built-in model picker deliberately omits Manage Models. Do not advertise
|
|
564
|
-
// hot swapping: the command
|
|
688
|
+
// hot swapping: the command configures a separate native blank chat.
|
|
565
689
|
// A locked selected item hides the entire native dropdown, including commands.
|
|
566
690
|
const item = { id: 'settings', name: 'Model settings' };
|
|
567
691
|
groups.push({ id: 'modelSettings', name: 'Model', items: [item], selected: item,
|
|
@@ -581,19 +705,50 @@ export async function activate(context) {
|
|
|
581
705
|
create: async (inputState) => {
|
|
582
706
|
if (!controls)
|
|
583
707
|
throw new Error('The host does not expose conversation creation.');
|
|
584
|
-
|
|
585
|
-
|
|
708
|
+
const draft = activeDraft();
|
|
709
|
+
await draft?.choosing;
|
|
710
|
+
if (draft && activeDraft() !== draft)
|
|
711
|
+
throw new vscode.CancellationError();
|
|
712
|
+
await controls.refresh();
|
|
713
|
+
controlsChanged.fire();
|
|
714
|
+
const choice = draft?.choice;
|
|
715
|
+
const loginHarness = choice?.kind === 'signIn' ? choice.harness : undefined;
|
|
716
|
+
if (loginHarness && !controls.state.harnesses.some(h => h.id === loginHarness && h.autoStart)) {
|
|
717
|
+
throw new Error(`Finish signing in with ${harnessDisplayName(loginHarness)} before sending. Your draft is kept.`);
|
|
718
|
+
}
|
|
719
|
+
if (choice?.kind !== 'agent' && controls.state.setup && !controls.state.setup.ready) {
|
|
720
|
+
throw new Error('Sign in before sending. Your draft is kept; use Check again after signing in.');
|
|
721
|
+
}
|
|
722
|
+
const selection = choice?.kind === 'agent' ? choice.selection
|
|
723
|
+
: { harness: loginHarness ?? controls.state.selection.harness, model: '', effort: '' };
|
|
724
|
+
const pending = applying.then(() => applySelection(selection));
|
|
725
|
+
applying = pending.then(() => undefined, () => undefined);
|
|
726
|
+
const connection = await pending;
|
|
586
727
|
savePermission(connection.id, turnPermission(inputState.groups.find(group => group.id === 'permissions')?.selected?.id));
|
|
728
|
+
if (pendingDraft === draft) {
|
|
729
|
+
pendingDraft = undefined;
|
|
730
|
+
controlsChanged.fire();
|
|
731
|
+
}
|
|
587
732
|
return connection;
|
|
588
733
|
},
|
|
589
|
-
resolve: connectionFor,
|
|
734
|
+
resolve: (resource) => connectionFor(resource, false),
|
|
590
735
|
attached: (connection) => attachedIds.has(connection.id),
|
|
591
736
|
catalogue: () => controls?.state.sessions ?? [],
|
|
592
737
|
bindings: boundId,
|
|
593
738
|
};
|
|
594
739
|
context.subscriptions.push(vscode.commands.registerCommand('supercode.frontend.activateSession', async (resource) => { await connectionFor(vscode.Uri.parse(resource)); }));
|
|
740
|
+
// The host's native focus event is authoritative even when a cached model
|
|
741
|
+
// skips content resolution. Untitled/non-harness focus never starts a runtime.
|
|
742
|
+
context.subscriptions.push(vscode.commands.registerCommand('supercode.frontend.focusSession', (resource) => {
|
|
743
|
+
focusSession(resource ? vscode.Uri.parse(resource) : undefined);
|
|
744
|
+
}));
|
|
595
745
|
context.subscriptions.push(vscode.commands.registerCommand('supercode.frontend.openSession', async (id) => {
|
|
596
|
-
|
|
746
|
+
const resource = vscode.Uri.from({ scheme: SESSION_SCHEME, authority: id, path: '/conversation' });
|
|
747
|
+
focusSession(resource);
|
|
748
|
+
const generation = focusGeneration;
|
|
749
|
+
await connectionFor(resource);
|
|
750
|
+
if (generation !== focusGeneration)
|
|
751
|
+
return;
|
|
597
752
|
await revealManaged();
|
|
598
753
|
}));
|
|
599
754
|
context.subscriptions.push(vscode.commands.registerCommand('supercode.frontend.configure', async (supplied) => {
|
|
@@ -607,7 +762,7 @@ export async function activate(context) {
|
|
|
607
762
|
return currentOptions();
|
|
608
763
|
}
|
|
609
764
|
if (supplied && typeof supplied.harness === 'string') {
|
|
610
|
-
await
|
|
765
|
+
await chooseNewChat(async () => ({ kind: 'agent', selection: supplied }));
|
|
611
766
|
return currentOptions();
|
|
612
767
|
}
|
|
613
768
|
if (!controls) {
|
|
@@ -632,18 +787,20 @@ export async function activate(context) {
|
|
|
632
787
|
}
|
|
633
788
|
// A sole sign-in option still requires a deliberate click before opening its terminal.
|
|
634
789
|
// The official choice must not disable the existing sole-local-agent shortcut.
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
790
|
+
await chooseNewChat(async () => {
|
|
791
|
+
const harness = localChoices.length === 1 && localChoices[0].autoStart ? localChoices[0] : await vscode.window.showQuickPick(choices, { title: 'New chat · Agent', placeHolder: 'Choose your coding agent. Model and reasoning use its defaults.' });
|
|
792
|
+
if (!harness)
|
|
793
|
+
return { kind: 'cancelled' };
|
|
794
|
+
if (harness.id === CHATGPT_SIGN_IN) {
|
|
795
|
+
await vscode.commands.executeCommand(CHATGPT_SIGN_IN);
|
|
796
|
+
return { kind: 'signIn' };
|
|
797
|
+
}
|
|
798
|
+
if (harness.login) {
|
|
799
|
+
await vscode.commands.executeCommand('supercode.frontend.setup', 'login', harness.id);
|
|
800
|
+
return { kind: 'signIn', harness: harness.id };
|
|
801
|
+
}
|
|
802
|
+
return { kind: 'agent', selection: { harness: harness.id, model: '', effort: '' } };
|
|
803
|
+
});
|
|
647
804
|
}));
|
|
648
805
|
context.subscriptions.push(vscode.commands.registerCommand('supercode.frontend.configureModel', async (resource, sessionId) => {
|
|
649
806
|
if (!controls || focusedAttached)
|
|
@@ -660,25 +817,25 @@ export async function activate(context) {
|
|
|
660
817
|
const selection = { ...selected.selection };
|
|
661
818
|
if (!['claude-code', 'codex'].includes(selection.harness))
|
|
662
819
|
throw new Error('This harness uses its own model defaults; configuration is not advertised.');
|
|
663
|
-
|
|
820
|
+
await chooseNewChat(async () => {
|
|
664
821
|
const choices = controls.state.modelsByHarness?.[selection.harness] ?? [{ id: '', name: 'Harness default' }];
|
|
665
822
|
const picked = await vscode.window.showQuickPick([...choices.map(model => ({ label: model.name, id: model.id })), { label: 'Enter an exact model ID…', id: '__custom__' }], { title: 'New chat · Model', placeHolder: 'Model availability depends on your harness account.' });
|
|
666
823
|
if (!picked)
|
|
667
|
-
return;
|
|
824
|
+
return { kind: 'cancelled' };
|
|
668
825
|
if (picked.id === '__custom__') {
|
|
669
826
|
const model = await vscode.window.showInputBox({ title: 'New chat · Exact model', prompt: 'Model ID or CLI alias supported by this harness account.', value: selection.model });
|
|
670
827
|
if (model === undefined)
|
|
671
|
-
return;
|
|
828
|
+
return { kind: 'cancelled' };
|
|
672
829
|
selection.model = model.trim();
|
|
673
830
|
}
|
|
674
831
|
else
|
|
675
832
|
selection.model = picked.id;
|
|
676
833
|
const effort = await vscode.window.showQuickPick([{ label: 'Harness default', id: '' }, ...['low', 'medium', 'high'].map(id => ({ label: id, id }))], { title: 'New chat · Reasoning effort' });
|
|
677
834
|
if (!effort)
|
|
678
|
-
return;
|
|
835
|
+
return { kind: 'cancelled' };
|
|
679
836
|
selection.effort = effort.id;
|
|
680
|
-
|
|
681
|
-
|
|
837
|
+
return { kind: 'agent', selection };
|
|
838
|
+
});
|
|
682
839
|
}));
|
|
683
840
|
context.subscriptions.push({ dispose: () => frontends.dispose() });
|
|
684
841
|
const runTurnInto = async (connection, prompt, stream, token, approval) => {
|
|
@@ -804,8 +961,10 @@ export async function activate(context) {
|
|
|
804
961
|
if (welcomeValue === welcome?.value)
|
|
805
962
|
return;
|
|
806
963
|
welcomeValue = welcome?.value;
|
|
964
|
+
// Metadata updates merge across RPC; undefined can leave the previous
|
|
965
|
+
// sign-in message behind. An explicit empty value clears it in place.
|
|
807
966
|
for (const participant of participants)
|
|
808
|
-
participant.additionalWelcomeMessage = welcome;
|
|
967
|
+
participant.additionalWelcomeMessage = welcome ?? new vscode.MarkdownString('');
|
|
809
968
|
};
|
|
810
969
|
updateWelcome();
|
|
811
970
|
context.subscriptions.push(controlsChanged.event(updateWelcome), frontends.onDidChange(updateWelcome));
|
|
@@ -817,7 +976,7 @@ export async function activate(context) {
|
|
|
817
976
|
// canDelegate is disabled to avoid a generic workbench-owned registration.
|
|
818
977
|
// A participant names the harness; the native model picker names the model.
|
|
819
978
|
// Keep the author stable when a model changes during request dispatch.
|
|
820
|
-
const participantName = () => harnessDisplayName(focusedAttached?.session.descriptor?.source_harness ?? controls?.state.selection.harness ?? managed()?.session.descriptor?.source_harness);
|
|
979
|
+
const participantName = () => harnessDisplayName(focusedAttached?.session.descriptor?.source_harness ?? draftSelection()?.harness ?? controls?.state.selection.harness ?? managed()?.session.descriptor?.source_harness);
|
|
821
980
|
const participantKey = () => `${participantName()}/${identity().provider ?? ''}`;
|
|
822
981
|
const makeParticipant = () => {
|
|
823
982
|
const current = { name: participantName() };
|
|
@@ -839,7 +998,28 @@ export async function activate(context) {
|
|
|
839
998
|
p.iconPath = icon();
|
|
840
999
|
};
|
|
841
1000
|
context.subscriptions.push({ dispose: () => sessionParticipant.dispose() }, controlsChanged.event(updateIdentity), frontends.onDidChange(updateIdentity));
|
|
842
|
-
|
|
1001
|
+
// Replayed turns outlive the active composer participant. Keep their
|
|
1002
|
+
// author tied to the saved harness, including passive background loads.
|
|
1003
|
+
const historyParticipants = new Map();
|
|
1004
|
+
const historyParticipant = (connection) => {
|
|
1005
|
+
if (!granted.has('chatParticipantPrivate'))
|
|
1006
|
+
return sessionParticipant.id;
|
|
1007
|
+
const harness = connection.session.descriptor?.source_harness
|
|
1008
|
+
?? controls?.state.sessions.find(entry => entry.id === connection.id)?.selection.harness;
|
|
1009
|
+
if (!harness)
|
|
1010
|
+
return sessionParticipant.id;
|
|
1011
|
+
let author = historyParticipants.get(harness);
|
|
1012
|
+
if (!author) {
|
|
1013
|
+
const name = harnessDisplayName(harness);
|
|
1014
|
+
author = vscode.chat.createDynamicChatParticipant(`${SESSION_SCHEME}.history.${encodeURIComponent(harness)}`, { name: name.replace(/[^a-zA-Z0-9]/g, ''), fullName: name, publisherName: 'Volter' }, handler);
|
|
1015
|
+
const provider = runtimeIdentity(harness, '').provider;
|
|
1016
|
+
author.iconPath = provider ? vscode.Uri.joinPath(context.extensionUri, 'media', `${provider}.svg`) : new vscode.ThemeIcon('hubot');
|
|
1017
|
+
historyParticipants.set(harness, author);
|
|
1018
|
+
context.subscriptions.push(author);
|
|
1019
|
+
}
|
|
1020
|
+
return author.id;
|
|
1021
|
+
};
|
|
1022
|
+
registerSessions(context, frontends, sessionParticipant, granted, handler, historyParticipant, controls ? providerControls : undefined);
|
|
843
1023
|
}
|
|
844
1024
|
catch (error) {
|
|
845
1025
|
log.warn(`Volter Harness chat sessions were not registered: ${String(error?.message ?? error)}`);
|
|
@@ -1067,9 +1247,12 @@ export async function activate(context) {
|
|
|
1067
1247
|
let setupTimer;
|
|
1068
1248
|
let setupDisposed = false;
|
|
1069
1249
|
const pollSetup = () => {
|
|
1070
|
-
|
|
1250
|
+
const choice = activeDraft()?.choice;
|
|
1251
|
+
const awaitingLogin = choice?.kind === 'signIn' && choice.harness && !controls?.state.harnesses.some(h => h.id === choice.harness && h.autoStart);
|
|
1252
|
+
if (setupTimer || setupDisposed || !controls?.state.setup || (controls.state.setup.ready && setupHandoffComplete() && !awaitingLogin))
|
|
1071
1253
|
return;
|
|
1072
1254
|
setupTimer = setTimeout(() => {
|
|
1255
|
+
setupTimer = undefined;
|
|
1073
1256
|
void refreshSetup().catch(error => log.warn(`Could not refresh sign-in: ${String(error)}`)).finally(pollSetup);
|
|
1074
1257
|
}, 3000);
|
|
1075
1258
|
};
|
|
@@ -1206,7 +1389,7 @@ function applyTiming(item, connection) {
|
|
|
1206
1389
|
lastRequestEnded: connection.session.lastTurnEndedMs ?? undefined,
|
|
1207
1390
|
};
|
|
1208
1391
|
}
|
|
1209
|
-
function registerSessions(context, frontends, participant, granted, handler, controls) {
|
|
1392
|
+
function registerSessions(context, frontends, participant, granted, handler, historyParticipant, controls) {
|
|
1210
1393
|
const turns = vscode;
|
|
1211
1394
|
const items = () => {
|
|
1212
1395
|
return (controls?.catalogue() ?? []).filter(entry => !frontends.get(entry.id)).map(entry => {
|
|
@@ -1260,11 +1443,15 @@ function registerSessions(context, frontends, participant, granted, handler, con
|
|
|
1260
1443
|
}));
|
|
1261
1444
|
}
|
|
1262
1445
|
context.subscriptions.push(vscode.chat.registerChatSessionContentProvider(SESSION_SCHEME, {
|
|
1263
|
-
provideChatSessionContent: async (resource,
|
|
1446
|
+
provideChatSessionContent: async (resource, token, { inputState }) => {
|
|
1447
|
+
if (token.isCancellationRequested)
|
|
1448
|
+
throw new vscode.CancellationError();
|
|
1264
1449
|
const boundHandler = (request, chatContext, stream, token) => handler(request, { ...chatContext, chatSessionContext: { chatSessionItem: { resource, label: 'Conversation' } } }, stream, token);
|
|
1265
1450
|
// Only a native untitled resource can create a conversation. Existing
|
|
1266
1451
|
// resources must resolve their exact saved binding; there is no fallback.
|
|
1267
1452
|
const connection = controls ? (resource.path.startsWith('/untitled-') && !controls.bindings(resource) ? undefined : await controls.resolve(resource)) : frontends.get(resource.authority);
|
|
1453
|
+
if (token.isCancellationRequested)
|
|
1454
|
+
throw new vscode.CancellationError();
|
|
1268
1455
|
if (!connection) {
|
|
1269
1456
|
return { history: [], requestHandler: boundHandler };
|
|
1270
1457
|
}
|
|
@@ -1277,13 +1464,14 @@ function registerSessions(context, frontends, participant, granted, handler, con
|
|
|
1277
1464
|
// `state.history`), so the copy taken when this window attached is
|
|
1278
1465
|
// stale as soon as a turn runs. The generated client reuses the
|
|
1279
1466
|
// already-open event stream, so this is a snapshot, not a reconnect.
|
|
1280
|
-
const attached = await connection.session.attach().then(() => true).catch(() => {
|
|
1467
|
+
const attached = frontends.get(connection.id) === connection && await connection.session.attach().then(() => true).catch(() => {
|
|
1281
1468
|
// An unreachable runtime replays what this window last saw.
|
|
1282
1469
|
return false;
|
|
1283
1470
|
});
|
|
1284
1471
|
// The attach snapshot IS the session's history: the runtime's own
|
|
1285
1472
|
// canonical transcript, replayed as turns.
|
|
1286
1473
|
const history = [];
|
|
1474
|
+
const authorId = historyParticipant(connection);
|
|
1287
1475
|
// A hosted harness runtime has no canonical ChatMessage history —
|
|
1288
1476
|
// `HostedHarnessRuntime::attach` returns an empty one and puts the
|
|
1289
1477
|
// conversation in `replay`. Read whichever the runtime actually
|
|
@@ -1309,13 +1497,13 @@ function registerSessions(context, frontends, participant, granted, handler, con
|
|
|
1309
1497
|
canRespond: connection.session.descriptor?.actions.respond !== false,
|
|
1310
1498
|
});
|
|
1311
1499
|
if (connection.historyTruncated)
|
|
1312
|
-
history.push(new turns.ChatRequestTurn('[Earlier conversation history]', undefined, [],
|
|
1500
|
+
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));
|
|
1313
1501
|
for (const turn of projected) {
|
|
1314
1502
|
if (turn.role === 'user') {
|
|
1315
|
-
history.push(new turns.ChatRequestTurn(turn.prompt ?? '', undefined, [],
|
|
1503
|
+
history.push(new turns.ChatRequestTurn(turn.prompt ?? '', undefined, [], authorId, []));
|
|
1316
1504
|
continue;
|
|
1317
1505
|
}
|
|
1318
|
-
history.push(new turns.ChatResponseTurn(historyParts(oneRowPerToolCall(turn.parts ?? [])), {},
|
|
1506
|
+
history.push(new turns.ChatResponseTurn(historyParts(oneRowPerToolCall(turn.parts ?? [])), {}, authorId));
|
|
1319
1507
|
}
|
|
1320
1508
|
const busy = connection.session.descriptor?.turn_state === 'busy';
|
|
1321
1509
|
return {
|
package/dist/host-controls.d.ts
CHANGED
|
@@ -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. */
|
package/dist/host-controls.js
CHANGED
|
@@ -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() {
|
|
23
|
+
async refresh() { await this.update('/state'); }
|
|
23
24
|
async select(selection) {
|
|
24
|
-
|
|
25
|
-
this.state = result;
|
|
26
|
-
return result;
|
|
25
|
+
return this.update('/select', selection);
|
|
27
26
|
}
|
|
28
|
-
async remember(id, nativeId) {
|
|
27
|
+
async remember(id, nativeId) { await this.update('/remember', { id, nativeId }); }
|
|
29
28
|
async open(id) {
|
|
30
|
-
|
|
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.
|
|
4
|
+
"version": "0.1.30",
|
|
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",
|