@volter/supercode-frontend-vscode 0.1.45 → 0.1.47
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 +18 -0
- package/dist/extension.d.ts +10 -0
- package/dist/extension.js +128 -16
- package/dist/index.d.ts +1 -1
- package/dist/native-session.d.ts +1 -1
- package/dist/native-session.js +20 -3
- package/dist/session.js +22 -0
- package/package.json +2 -2
- package/vendor/supercode-frontend/contract.json +7 -2
- package/vendor/supercode-frontend/generated.d.mts +3 -3
- package/vendor/supercode-frontend/package.json +1 -1
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.
|
package/dist/extension.d.ts
CHANGED
|
@@ -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) {
|
|
@@ -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
|
-
|
|
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)
|
|
@@ -932,7 +987,7 @@ export async function activate(context) {
|
|
|
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
|
-
|
|
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) {
|
|
@@ -1391,7 +1452,17 @@ export async function activate(context) {
|
|
|
1391
1452
|
await settings.update('baseUrl', baseUrl, vscode.ConfigurationTarget.Global);
|
|
1392
1453
|
await context.secrets.store(secretKey(baseUrl), token);
|
|
1393
1454
|
await connectFromSettings();
|
|
1394
|
-
}), vscode.commands.registerCommand('supercode.frontend.connect', () =>
|
|
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
|
+
}),
|
|
1395
1466
|
// The create command the core's own `chatSessions/newSession` menu invokes
|
|
1396
1467
|
// for this session type (`chatSessions.contribution.ts`
|
|
1397
1468
|
// `_resolveCreateSubMenuCommandId`). It opens a chat on the supercode
|
|
@@ -1404,7 +1475,7 @@ export async function activate(context) {
|
|
|
1404
1475
|
}
|
|
1405
1476
|
const where = position === 'editor' ? 'editor' : 'sidebar';
|
|
1406
1477
|
await vscode.commands.executeCommand(`workbench.action.chat.openNewChatSessionInPlace.${SESSION_SCHEME}`, where);
|
|
1407
|
-
}), vscode.commands.registerCommand('supercode.frontend.disconnect', () =>
|
|
1478
|
+
}), vscode.commands.registerCommand('supercode.frontend.disconnect', (id) => disconnectExternal(id)),
|
|
1408
1479
|
// The one door an answer button uses. It answers the runtime's pending
|
|
1409
1480
|
// request through `frontend.v2.respond` and narrates itself, because a
|
|
1410
1481
|
// click whose effect is invisible is a click nobody can trust.
|
|
@@ -1434,6 +1505,40 @@ export async function activate(context) {
|
|
|
1434
1505
|
: REQUESTED_PROPOSALS.map((name) => `${name} not granted: ${PROPOSAL_EFFECTS[name].without}`).join('\n'),
|
|
1435
1506
|
});
|
|
1436
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
|
+
}
|
|
1437
1542
|
async function connectFromSettings() {
|
|
1438
1543
|
const settings = vscode.workspace.getConfiguration('supercode.frontend');
|
|
1439
1544
|
const baseUrl = settings.get('baseUrl')?.trim() || context.globalState.get(HANDED_OVER_KEY);
|
|
@@ -1444,16 +1549,11 @@ export async function activate(context) {
|
|
|
1444
1549
|
log.info('Volter Harness: a base URL is configured but no token is stored; run Volter Harness: Set Runtime Token.');
|
|
1445
1550
|
return;
|
|
1446
1551
|
}
|
|
1447
|
-
|
|
1448
|
-
.connect({ baseUrl, token, permissions: normalizePermissions(settings.get('permissions'), ['observe']) }, log)
|
|
1552
|
+
await connectExternal({ baseUrl, token, permissions: normalizePermissions(settings.get('permissions'), ['observe']) })
|
|
1449
1553
|
.catch((error) => {
|
|
1450
1554
|
log.error(`Volter Harness could not attach to ${baseUrl}: ${String(error?.message ?? error)}`);
|
|
1451
1555
|
return undefined;
|
|
1452
1556
|
});
|
|
1453
|
-
if (controls && connection) {
|
|
1454
|
-
attachedIds.add(connection.id);
|
|
1455
|
-
controlsChanged.fire();
|
|
1456
|
-
}
|
|
1457
1557
|
}
|
|
1458
1558
|
/**
|
|
1459
1559
|
* The handover path: a runtime (or a product that launched this editor's
|
|
@@ -1507,6 +1607,12 @@ export async function activate(context) {
|
|
|
1507
1607
|
async function openResumedSession() {
|
|
1508
1608
|
if (!granted.has('chatSessionsProvider'))
|
|
1509
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
|
+
}
|
|
1510
1616
|
if (controls) {
|
|
1511
1617
|
await refreshSetup();
|
|
1512
1618
|
return;
|
|
@@ -1554,10 +1660,10 @@ export async function activate(context) {
|
|
|
1554
1660
|
context.subscriptions.push({ dispose: () => { setupDisposed = true; clearTimeout(setupTimer); } });
|
|
1555
1661
|
return {
|
|
1556
1662
|
connect: async (options) => {
|
|
1557
|
-
const connection = await
|
|
1663
|
+
const connection = await connectExternal(options);
|
|
1558
1664
|
return connection ? frontends.info(connection) : undefined;
|
|
1559
1665
|
},
|
|
1560
|
-
disconnect: (connectionId) =>
|
|
1666
|
+
disconnect: (connectionId) => disconnectExternal(connectionId),
|
|
1561
1667
|
get connections() {
|
|
1562
1668
|
return frontends.all.map((connection) => frontends.info(connection));
|
|
1563
1669
|
},
|
|
@@ -1756,12 +1862,14 @@ function registerSessions(context, frontends, participant, granted, handler, his
|
|
|
1756
1862
|
const boundHandler = (request, chatContext, stream, token) => handler(request, { ...chatContext, chatSessionContext: { chatSessionItem: { resource, label: 'Conversation' } } }, stream, token);
|
|
1757
1863
|
// Only a native untitled resource can create a conversation. Existing
|
|
1758
1864
|
// resources must resolve their exact saved binding; there is no fallback.
|
|
1759
|
-
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);
|
|
1760
1866
|
if (token.isCancellationRequested)
|
|
1761
1867
|
throw new vscode.CancellationError();
|
|
1762
1868
|
if (!connection) {
|
|
1763
1869
|
return { history: [], requestHandler: boundHandler };
|
|
1764
1870
|
}
|
|
1871
|
+
connection.viewReady = false;
|
|
1872
|
+
connection.viewResource = resource.toString();
|
|
1765
1873
|
if (controls && !controls.attached(connection)) {
|
|
1766
1874
|
controls.setPermission(resource, inputState.groups.find(group => group.id === 'permissions')?.selected?.id);
|
|
1767
1875
|
inputState.groups = controls.options(resource);
|
|
@@ -1813,6 +1921,9 @@ function registerSessions(context, frontends, participant, granted, handler, his
|
|
|
1813
1921
|
history.push(new turns.ChatResponseTurn(historyParts(oneRowPerToolCall(turn.parts ?? [])), {}, authorId));
|
|
1814
1922
|
}
|
|
1815
1923
|
const busy = connection.session.descriptor?.turn_state === 'busy';
|
|
1924
|
+
const renderedCursor = connection.session.cursor;
|
|
1925
|
+
if (!busy)
|
|
1926
|
+
connection.viewReady = attached;
|
|
1816
1927
|
return {
|
|
1817
1928
|
title: connection.label,
|
|
1818
1929
|
options: controls?.attached(connection) ? undefined : controls?.currentOptions(resource),
|
|
@@ -1838,9 +1949,10 @@ function registerSessions(context, frontends, participant, granted, handler, his
|
|
|
1838
1949
|
canRespond: connection.session.descriptor?.actions.respond !== false,
|
|
1839
1950
|
}), granted, state);
|
|
1840
1951
|
watch?.sweep();
|
|
1841
|
-
});
|
|
1952
|
+
}, () => { connection.viewReady = true; }, renderedCursor);
|
|
1842
1953
|
}
|
|
1843
1954
|
finally {
|
|
1955
|
+
connection.viewReady = connection.session.descriptor?.turn_state !== 'busy';
|
|
1844
1956
|
watch?.dispose();
|
|
1845
1957
|
}
|
|
1846
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';
|
package/dist/native-session.d.ts
CHANGED
|
@@ -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>;
|
package/dist/native-session.js
CHANGED
|
@@ -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
|
-
|
|
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/dist/session.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
/** How long a turn waits on a connection that dropped before saying it was lost (the follow loop's re-attach backs
|
|
2
|
+
* off to 15 s; this outlasts a few of its attempts). */
|
|
3
|
+
const DISCONNECT_GRACE_MS = 60_000;
|
|
1
4
|
/** The request this attachment tried to answer is no longer open. */
|
|
2
5
|
export class RequestNotPendingError extends Error {
|
|
3
6
|
requestId;
|
|
@@ -402,7 +405,25 @@ export class VscodeFrontendSession {
|
|
|
402
405
|
// that hangs one is worse than one that ends it a beat early. The
|
|
403
406
|
// descriptor going idle after the submit is the second, independent signal.
|
|
404
407
|
let sawBusy = false;
|
|
408
|
+
// A stream that ends is re-attached from its cursor (the extension's follow loop), and the replay ends the turn;
|
|
409
|
+
// only a connection closed, or one still down after DISCONNECT_GRACE_MS, ends the wait, said: a response held open
|
|
410
|
+
// with nothing in it is the silent failure.
|
|
411
|
+
let down;
|
|
412
|
+
const lost = () => fail?.(new Error('The connection to this conversation\'s runtime was lost before its turn ended; the turn may still be running. Reopen the chat to see where it stands.'));
|
|
413
|
+
finished.catch(() => undefined); // a loss while the submit is still in flight is answered by the submit's own error
|
|
405
414
|
const unwatch = this.onChange(() => {
|
|
415
|
+
if (this.connection === 'closed') {
|
|
416
|
+
lost();
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
if (this.connection === 'connected') {
|
|
420
|
+
clearTimeout(down);
|
|
421
|
+
down = undefined;
|
|
422
|
+
}
|
|
423
|
+
else {
|
|
424
|
+
down ??= setTimeout(lost, DISCONNECT_GRACE_MS);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
406
427
|
const state = this.descriptor?.turn_state;
|
|
407
428
|
if (state === 'busy')
|
|
408
429
|
sawBusy = true;
|
|
@@ -417,6 +438,7 @@ export class VscodeFrontendSession {
|
|
|
417
438
|
return await finished;
|
|
418
439
|
}
|
|
419
440
|
finally {
|
|
441
|
+
clearTimeout(down);
|
|
420
442
|
unsubscribe();
|
|
421
443
|
unwatch();
|
|
422
444
|
}
|
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.47",
|
|
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.
|
|
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;
|