@volter/supercode-frontend-vscode 0.1.11

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.
Files changed (39) hide show
  1. package/NOTICE.md +19 -0
  2. package/README.md +293 -0
  3. package/dist/bindings.d.ts +7 -0
  4. package/dist/bindings.js +5 -0
  5. package/dist/client.d.ts +11 -0
  6. package/dist/client.js +5 -0
  7. package/dist/context.d.ts +6 -0
  8. package/dist/context.js +14 -0
  9. package/dist/credential.d.ts +17 -0
  10. package/dist/credential.js +53 -0
  11. package/dist/extension.d.ts +39 -0
  12. package/dist/extension.js +1131 -0
  13. package/dist/host-controls.d.ts +58 -0
  14. package/dist/host-controls.js +66 -0
  15. package/dist/index.d.ts +6 -0
  16. package/dist/index.js +5 -0
  17. package/dist/native-session.d.ts +6 -0
  18. package/dist/native-session.js +41 -0
  19. package/dist/proposals.d.ts +24 -0
  20. package/dist/proposals.js +54 -0
  21. package/dist/session.d.ts +154 -0
  22. package/dist/session.js +431 -0
  23. package/dist/view.d.ts +176 -0
  24. package/dist/view.js +717 -0
  25. package/media/anthropic.svg +1 -0
  26. package/media/google.svg +1 -0
  27. package/media/icon.png +0 -0
  28. package/media/openai.svg +1 -0
  29. package/package.json +200 -0
  30. package/scripts/pack-extension.mjs +63 -0
  31. package/vendor/supercode-frontend/README.md +22 -0
  32. package/vendor/supercode-frontend/client.d.mts +106 -0
  33. package/vendor/supercode-frontend/client.mjs +300 -0
  34. package/vendor/supercode-frontend/contract.json +289 -0
  35. package/vendor/supercode-frontend/generated-client.d.mts +18 -0
  36. package/vendor/supercode-frontend/generated-client.mjs +20 -0
  37. package/vendor/supercode-frontend/generated.d.mts +146 -0
  38. package/vendor/supercode-frontend/generated.mjs +19 -0
  39. package/vendor/supercode-frontend/package.json +20 -0
@@ -0,0 +1,1131 @@
1
+ import { resolveSessionBinding } from './bindings.js';
2
+ import { withTextContext } from './context.js';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { ChatHostControls, runtimeIdentity, harnessDisplayName, turnPermission } from './host-controls.js';
5
+ import * as vscode from 'vscode';
6
+ import { createFrontendClient } from './client.js';
7
+ import { readCredentialFile } from './credential.js';
8
+ import { grantedProposals, PROPOSAL_EFFECTS, REQUESTED_PROPOSALS } from './proposals.js';
9
+ import { observeActiveResponse, sessionStatus } from './native-session.js';
10
+ import { RequestNotPendingError, VscodeFrontendSession } from './session.js';
11
+ import { fence, acceptedDecisions, opaqueText, projectEvent, projectPendingRequests, projectHistory, projectReplayTurns, resolveParts, resolutionSentence, typedAnswer, } from './view.js';
12
+ const SESSION_SCHEME = 'supercode';
13
+ const PARTICIPANT_ID = 'supercode.chat';
14
+ const DEFAULT_PARTICIPANT_ID = 'supercode.chat.default';
15
+ /** `package.json` gates the `isDefault` participant on this key. */
16
+ const DEFAULT_PARTICIPANT_CONTEXT_KEY = 'supercodeChatDefaultParticipant';
17
+ /** `chatViewsWelcome` shows the empty state until this is true. */
18
+ const ATTACHED_CONTEXT_KEY = 'supercodeAttached';
19
+ const PERMISSIONS = ['observe', 'interact', 'approve', 'terminate'];
20
+ /** The environment a runtime uses to hand this frontend a live connection. */
21
+ const ENV = Object.freeze({
22
+ url: 'SUPERCODE_FRONTEND_URL',
23
+ clientId: 'SUPERCODE_FRONTEND_CLIENT_ID',
24
+ credentialFile: 'SUPERCODE_FRONTEND_CREDENTIAL_FILE',
25
+ permissions: 'SUPERCODE_FRONTEND_PERMISSIONS',
26
+ /**
27
+ * Why there is no runtime to attach to, in the host's OWN words — "Install
28
+ * Claude Code and ensure `claude` is on PATH", "sign in with `codex login`".
29
+ * A host that could not start one knows exactly why and what to type; this
30
+ * frontend does not, and a chat panel that looks identical to the working
31
+ * case is how a person ends up typing into nothing.
32
+ */
33
+ unavailable: 'SUPERCODE_FRONTEND_UNAVAILABLE',
34
+ });
35
+ function normalizePermissions(value, fallback) {
36
+ const raw = Array.isArray(value)
37
+ ? value.map(String)
38
+ : typeof value === 'string'
39
+ ? value.split(',')
40
+ : [];
41
+ const permissions = raw
42
+ .map((entry) => entry.trim())
43
+ .filter((entry) => PERMISSIONS.includes(entry));
44
+ if (!permissions.includes('observe'))
45
+ permissions.unshift('observe');
46
+ return value === undefined || value === null ? fallback : permissions;
47
+ }
48
+ /** The one session item that stands for "there is no runtime, and here is why". */
49
+ const UNAVAILABLE_PATH = '/unavailable';
50
+ /**
51
+ * The prompt of the turn that carries a status this frontend is reporting.
52
+ * It names itself; nothing here is presented as a person's words.
53
+ */
54
+ const STATUS_MARKER_PROMPT = '⟨supercode⟩';
55
+ function unavailableResource() {
56
+ return vscode.Uri.from({ scheme: SESSION_SCHEME, authority: 'supercode', path: UNAVAILABLE_PATH });
57
+ }
58
+ function sessionResource(connection) {
59
+ const sessionId = 'conversation';
60
+ return vscode.Uri.from({ scheme: SESSION_SCHEME, authority: connection.id, path: `/${encodeURIComponent(sessionId)}` });
61
+ }
62
+ /**
63
+ * Every attachment this window holds. It owns no agent, no model and no
64
+ * session file: one generated client per runtime and nothing else.
65
+ */
66
+ class SupercodeFrontends {
67
+ #connections = new Map();
68
+ #changed = new vscode.EventEmitter();
69
+ onDidChange = this.#changed.event;
70
+ /**
71
+ * Why there is no runtime, in the host's own words. Held here because it is
72
+ * shown the same way an attached runtime is — as a session in the picker
73
+ * whose content is that sentence — so a person reading the panel learns
74
+ * what to do instead of typing into nothing.
75
+ */
76
+ unavailable;
77
+ setUnavailable(reason) {
78
+ this.unavailable = reason?.trim() || undefined;
79
+ this.#changed.fire();
80
+ }
81
+ get all() {
82
+ return [...this.#connections.values()];
83
+ }
84
+ get(id) {
85
+ if (id)
86
+ return this.#connections.get(id);
87
+ return this.#connections.values().next().value;
88
+ }
89
+ info(connection) {
90
+ return {
91
+ id: connection.id,
92
+ baseUrl: connection.baseUrl,
93
+ label: connection.label,
94
+ sessionId: connection.session.descriptor?.session_id ?? null,
95
+ model: connection.session.descriptor?.model ?? null,
96
+ turnState: connection.session.descriptor?.turn_state ?? null,
97
+ };
98
+ }
99
+ async connect(options, log) {
100
+ // `{unavailable}` and no runtime: the host is telling this frontend why
101
+ // there is nothing to attach to. Record it and show it; do not attach.
102
+ if (!options.baseUrl || !options.token) {
103
+ if (options.unavailable) {
104
+ this.setUnavailable(options.unavailable);
105
+ log.info(`Volter Harness has no runtime: ${options.unavailable}`);
106
+ return undefined;
107
+ }
108
+ throw new TypeError('connect needs a baseUrl and token, or an `unavailable` reason');
109
+ }
110
+ const { baseUrl, token } = options;
111
+ const id = options.connectionId ?? options.clientId ?? `vscode-${randomUUID()}`;
112
+ const existing = this.#connections.get(id);
113
+ if (existing)
114
+ return existing;
115
+ const permissions = normalizePermissions(options.permissions, ['observe']);
116
+ const client = createFrontendClient({
117
+ baseUrl,
118
+ token,
119
+ clientId: options.clientId ?? id,
120
+ permissions,
121
+ });
122
+ const session = new VscodeFrontendSession(id, baseUrl, client, permissions);
123
+ // The generated client opens SSE before the snapshot RPC; `attach` is
124
+ // therefore the whole race-free attachment and `follow` only drains it.
125
+ await session.attach();
126
+ // An interactive attachment holds the runtime's controller lease; without
127
+ // it the first submit comes back `lease_expired`.
128
+ await session.takeControlAndHold().catch((error) => {
129
+ log.warn(`Volter Harness could not take control of ${baseUrl}: ${String(error?.message ?? error)}`);
130
+ });
131
+ // The runtime's stream can end under a live attachment: an HTTP client's
132
+ // body timeout ended an idle one at 300 s (the runtime sent no keep-alive),
133
+ // and the view went on showing a turn it would never see finish, with the
134
+ // next prompt queued behind it. While this connection is still the
135
+ // registered one, it re-attaches from its cursor and follows again.
136
+ let connection;
137
+ const stillWanted = () => connection !== undefined && this.#connections.get(id) === connection && session.connection !== 'closed';
138
+ const following = (async () => {
139
+ for (let attempt = 0;; attempt += 1) {
140
+ try {
141
+ await session.follow();
142
+ }
143
+ catch (error) {
144
+ log.warn(`Volter Harness event stream ended: ${String(error?.message ?? error)}`);
145
+ }
146
+ if (!stillWanted())
147
+ return;
148
+ await new Promise((resolve) => setTimeout(resolve, Math.min(15_000, 500 * 2 ** Math.min(attempt, 5))));
149
+ if (!stillWanted())
150
+ return;
151
+ try {
152
+ await session.attach();
153
+ attempt = -1;
154
+ log.info(`Volter Harness re-attached to ${baseUrl} at event ${session.cursor}`);
155
+ }
156
+ catch (error) {
157
+ log.warn(`Volter Harness could not re-attach to ${baseUrl}: ${String(error?.message ?? error)}`);
158
+ }
159
+ }
160
+ })();
161
+ connection = {
162
+ id,
163
+ label: options.label ?? session.descriptor?.session_id ?? baseUrl,
164
+ baseUrl,
165
+ session,
166
+ following,
167
+ };
168
+ session.onChange(() => this.#changed.fire());
169
+ this.#connections.set(id, connection);
170
+ this.#changed.fire();
171
+ this.setUnavailable(undefined);
172
+ log.info(`Volter Harness attached: ${connection.label} (${baseUrl}) as ${permissions.join(',')}` +
173
+ ` · ${session.history.length} history messages`);
174
+ return connection;
175
+ }
176
+ async disconnect(id) {
177
+ const targets = id ? [this.#connections.get(id)].filter(Boolean) : this.all;
178
+ for (const connection of targets) {
179
+ try {
180
+ if (connection.session.actions().detach)
181
+ await connection.session.detach();
182
+ }
183
+ catch {
184
+ // Detaching is best effort; dropping the client never closes a runtime.
185
+ }
186
+ connection.session.dispose();
187
+ this.#connections.delete(connection.id);
188
+ }
189
+ this.#changed.fire();
190
+ }
191
+ dispose() {
192
+ for (const connection of this.all)
193
+ connection.session.dispose();
194
+ this.#connections.clear();
195
+ this.#changed.dispose();
196
+ }
197
+ }
198
+ function writeProjected(stream, projected, granted, state) {
199
+ for (const candidate of projected) {
200
+ const parts = resolveParts([candidate], granted);
201
+ for (const part of parts) {
202
+ try {
203
+ writePart(stream, part, state);
204
+ }
205
+ catch {
206
+ // A host that advertised the proposal but refuses the call still gets
207
+ // the stable rendering rather than a dropped event.
208
+ for (const fallback of candidate.fallback ?? []) {
209
+ try {
210
+ writePart(stream, fallback, state);
211
+ }
212
+ catch {
213
+ stream.markdown(opaqueText(fallback));
214
+ }
215
+ }
216
+ }
217
+ }
218
+ }
219
+ }
220
+ function writePart(stream, part, state) {
221
+ switch (part.part) {
222
+ case 'markdown':
223
+ stream.markdown(part.value);
224
+ return;
225
+ case 'progress':
226
+ stream.progress(part.value);
227
+ return;
228
+ case 'warning':
229
+ stream.warning(part.value);
230
+ return;
231
+ case 'codeblock':
232
+ stream.codeblockUri(vscode.Uri.parse(part.uri));
233
+ stream.markdown(part.value);
234
+ return;
235
+ case 'thinking':
236
+ stream.push(new vscode.ChatResponseThinkingProgressPart(part.value));
237
+ return;
238
+ case 'reference': {
239
+ // Only a path the runtime's own payload named, resolved against the
240
+ // workspace this window already has. Nothing is created or fabricated.
241
+ const folder = vscode.workspace.workspaceFolders?.[0];
242
+ if (folder)
243
+ stream.reference(vscode.Uri.joinPath(folder.uri, part.path));
244
+ else
245
+ stream.markdown(`\`${part.path}\``);
246
+ return;
247
+ }
248
+ case 'answers':
249
+ // Stable API on purpose: a command button runs INSIDE the live turn,
250
+ // which is when a supercode approval is raised. See `view.ts`.
251
+ stream.markdown(`\n\n**${part.title}**\n\n${part.message}\n\n`);
252
+ for (const answer of part.answers) {
253
+ stream.button({
254
+ title: answer.label,
255
+ command: 'supercode.frontend.respond',
256
+ arguments: [answer],
257
+ });
258
+ }
259
+ return;
260
+ case 'tool': {
261
+ const toolName = part.toolName || state.toolNames.get(part.toolCallId) || 'tool';
262
+ if (part.toolName)
263
+ state.toolNames.set(part.toolCallId, part.toolName);
264
+ const invocation = new vscode.ChatToolInvocationPart(toolName, part.toolCallId);
265
+ invocation.enablePartialUpdate = true;
266
+ invocation.invocationMessage = part.invocationMessage || toolName;
267
+ invocation.isConfirmed = true;
268
+ invocation.isComplete = part.isComplete;
269
+ invocation.pastTenseMessage = part.pastTenseMessage || toolName;
270
+ if (part.isError !== undefined)
271
+ invocation.isError = part.isError;
272
+ if (part.inputText !== undefined)
273
+ state.toolInputs.set(part.toolCallId, part.inputText);
274
+ if (part.isComplete) {
275
+ invocation.toolSpecificData = {
276
+ input: state.toolInputs.get(part.toolCallId) ?? '',
277
+ output: part.resultText ?? '',
278
+ };
279
+ }
280
+ stream.push(invocation);
281
+ return;
282
+ }
283
+ }
284
+ }
285
+ function pendingRequestOf(connection) {
286
+ return connection.session.pendingRequests.values().next().value;
287
+ }
288
+ /**
289
+ * What the person reads when there is no runtime. A HOST-SUPPLIED reason is
290
+ * printed verbatim, because the host is the only thing that knows which
291
+ * harness it tried to start and what the person must type; this package would
292
+ * only be guessing. Without one it falls back to its own configuration
293
+ * pointer.
294
+ */
295
+ function unavailableMarkdown(reason) {
296
+ if (reason) {
297
+ return ['**Volter Harness has no runtime attached.**', '', reason, ''].join('\n');
298
+ }
299
+ return [
300
+ '**Volter Harness is not attached to a runtime.**',
301
+ '',
302
+ `Set \`supercode.frontend.baseUrl\`, then run **Volter Harness: Set Runtime Token** — or start the editor from a runtime that exports \`${ENV.url}\`.`,
303
+ '',
304
+ ].join('\n');
305
+ }
306
+ export async function activate(context) {
307
+ const log = vscode.window.createOutputChannel('Volter Harness', { log: true });
308
+ context.subscriptions.push(log);
309
+ // The HOST decides which proposals this extension got; its own manifest only
310
+ // asks. `context.extension.packageJSON` is the filtered extension
311
+ // description, so this is the grant, not the request.
312
+ const granted = grantedProposals(context.extension.packageJSON);
313
+ log.info(`Volter Harness: proposals requested ${REQUESTED_PROPOSALS.join(', ')}; granted ${granted.size ? [...granted].join(', ') : 'none (stable chat parts only)'}`);
314
+ const controls = await ChatHostControls.fromEnvironment();
315
+ const controlsChanged = new vscode.EventEmitter();
316
+ context.subscriptions.push(controlsChanged);
317
+ let permission = 'ask';
318
+ const permissions = new Map(context.workspaceState.get('supercode.sessionPermissions', []));
319
+ const savePermission = (id, value) => {
320
+ permissions.set(id, value);
321
+ void Promise.resolve(context.workspaceState.update('supercode.sessionPermissions', [...permissions])).catch(error => log.error(`Could not persist conversation permissions: ${String(error)}`));
322
+ };
323
+ const resourceBindings = new Map(context.workspaceState.get('supercode.sessionBindings', []));
324
+ const boundId = (resource) => resolveSessionBinding(resource, controls?.state.sessions ?? [], resourceBindings);
325
+ let applying = Promise.resolve();
326
+ const frontends = new SupercodeFrontends();
327
+ let approvalAvailable = false;
328
+ context.subscriptions.push(frontends.onDidChange(() => {
329
+ const active = frontends.get(undefined);
330
+ const available = Boolean(active?.session.descriptor?.actions.respond && active.session.permissions.has('approve'));
331
+ if (available !== approvalAvailable) {
332
+ approvalAvailable = available;
333
+ controlsChanged.fire();
334
+ }
335
+ }));
336
+ const identity = () => {
337
+ const descriptor = frontends.get(undefined)?.session.descriptor;
338
+ return runtimeIdentity(controls?.state.selection.harness ?? descriptor?.source_harness, controls?.state.selection.model || controls?.state.actualModel || descriptor?.model);
339
+ };
340
+ const icon = () => {
341
+ const provider = identity().provider;
342
+ return provider ? vscode.Uri.joinPath(context.extensionUri, 'media', `${provider}.svg`) : new vscode.ThemeIcon('hubot');
343
+ };
344
+ const nativeModels = Boolean(controls && granted.has('chatProvider'));
345
+ const modelSelections = new Map();
346
+ if (nativeModels && controls) {
347
+ context.subscriptions.push(vscode.lm.registerLanguageModelChatProvider('supercode-runtime', {
348
+ onDidChangeLanguageModelChatInformation: controlsChanged.event,
349
+ provideLanguageModelChatInformation: () => {
350
+ const { selection, models } = controls.state;
351
+ // A running harness does not advertise hot model changes. Configuration
352
+ // belongs to the explicit new-conversation flow.
353
+ const choices = models.filter(model => model.id === selection.model);
354
+ if (!choices.some(model => model.id === selection.model))
355
+ choices.push({ id: selection.model, name: selection.model });
356
+ modelSelections.clear();
357
+ return choices.map(model => {
358
+ const id = `${selection.harness}/${encodeURIComponent(model.id || '__default__')}`;
359
+ modelSelections.set(id, { ...selection, model: model.id });
360
+ const provider = runtimeIdentity(selection.harness, selection.model).provider;
361
+ return { id, name: model.name, family: selection.harness, version: '1',
362
+ statusIcon: new vscode.ThemeIcon(provider === 'anthropic' ? 'claude' : provider === 'openai' ? 'openai' : provider === 'google' ? 'google-gemini' : 'sparkle'),
363
+ maxInputTokens: 0, maxOutputTokens: 0, capabilities: {},
364
+ targetChatSessionType: SESSION_SCHEME, isUserSelectable: true, isDefault: model.id === selection.model,
365
+ tooltip: `${model.name} via ${harnessDisplayName(selection.harness)}${selection.effort ? ` · ${selection.effort} reasoning` : ''}. Model is fixed for this conversation. Use New Chat to choose another model.`,
366
+ };
367
+ });
368
+ },
369
+ // Like the workbench's AgentHostLanguageModelProvider, these are session
370
+ // model descriptors. The participant owns the conversation and tools.
371
+ provideLanguageModelChatResponse: async () => { throw new Error('Use these models through their Volter Harness chat session. Direct model requests are not supported.'); },
372
+ provideTokenCount: async () => 0,
373
+ }));
374
+ }
375
+ const currentOptions = (resource) => {
376
+ const id = resource ? boundId(resource) : controls?.state.activeSession;
377
+ return { permissions: id ? permissions.get(id) ?? 'ask' : permission };
378
+ };
379
+ const connectManaged = async () => {
380
+ const env = controls.state.connection;
381
+ if (!env || !controls.state.activeSession)
382
+ throw new Error('The requested conversation has no runtime connection.');
383
+ const id = controls.state.activeSession;
384
+ const existing = frontends.get(id);
385
+ if (existing && existing.baseUrl === env.SUPERCODE_FRONTEND_URL)
386
+ return existing;
387
+ await frontends.disconnect();
388
+ const connection = await frontends.connect({ connectionId: id, baseUrl: env.SUPERCODE_FRONTEND_URL,
389
+ token: await readCredentialFile(env.SUPERCODE_FRONTEND_CREDENTIAL_FILE), clientId: env.SUPERCODE_FRONTEND_CLIENT_ID,
390
+ permissions: normalizePermissions(env.SUPERCODE_FRONTEND_PERMISSIONS, ['observe']), label: identity().name }, log);
391
+ if (!connection)
392
+ throw new Error('Could not connect to the requested conversation.');
393
+ return connection;
394
+ };
395
+ const revealManaged = async () => {
396
+ const id = controls?.state.activeSession;
397
+ if (!id || !controls?.state.openSessionCommand)
398
+ throw new Error('The host cannot reveal this conversation.');
399
+ await vscode.commands.executeCommand(controls.state.openSessionCommand, vscode.Uri.from({ scheme: SESSION_SCHEME, authority: id, path: '/conversation' }).toString());
400
+ };
401
+ const connectionFor = async (resource) => {
402
+ if (!controls) {
403
+ const connection = frontends.get(resource.authority);
404
+ if (!connection)
405
+ throw new Error('This conversation is no longer connected.');
406
+ return connection;
407
+ }
408
+ const id = boundId(resource);
409
+ if (!id)
410
+ throw new Error('This chat has no backend session binding. Start a new chat; no message was sent.');
411
+ const active = frontends.get(undefined);
412
+ if (active && active.id !== id && (active.session.descriptor?.turn_state === 'busy' || active.session.pendingRequests.size))
413
+ throw new Error('Finish or cancel the active turn before opening another conversation.');
414
+ try {
415
+ await controls.open(id);
416
+ }
417
+ catch (error) {
418
+ log.error(`Could not restore conversation ${id}: ${String(error)}`);
419
+ throw error;
420
+ }
421
+ const history = controls.state.history;
422
+ const historyTruncated = controls.state.historyTruncated;
423
+ const connection = await connectManaged();
424
+ connection.persistedHistory = history;
425
+ connection.historyTruncated = historyTruncated;
426
+ // Resolving an already-focused session must not republish its catalogue
427
+ // while the native widget is still opening that same resource.
428
+ if (active !== connection)
429
+ controlsChanged.fire();
430
+ return connection;
431
+ };
432
+ async function applySelection(selection, openChat = true) {
433
+ if (!controls)
434
+ throw new Error('This host does not expose harness or model configuration.');
435
+ const active = frontends.get(undefined);
436
+ if (active?.session.descriptor?.turn_state === 'busy' || active?.session.pendingRequests.size)
437
+ throw new Error('Finish or cancel the current turn before starting a new chat.');
438
+ await controls.select(selection);
439
+ await connectManaged();
440
+ controlsChanged.fire();
441
+ if (openChat)
442
+ await revealManaged();
443
+ }
444
+ const enqueueSelection = (selection, openChat = true) => {
445
+ const pending = applying.then(() => applySelection(selection, openChat));
446
+ applying = pending.catch(() => undefined);
447
+ return pending;
448
+ };
449
+ // Publish capability-honest input groups only after the handoff descriptor is available.
450
+ if (controls?.state.connection)
451
+ await connectManaged().catch(error => log.warn(`Could not attach before registering chat input: ${String(error)}`));
452
+ const providerControls = {
453
+ changed: controlsChanged.event,
454
+ currentOptions,
455
+ options: (resource) => {
456
+ const id = resource ? boundId(resource) : undefined;
457
+ const active = resource ? (id ? frontends.get(id) : undefined) : frontends.get(undefined);
458
+ const canApprove = active?.session.descriptor?.actions.respond && active.session.permissions.has('approve');
459
+ const permissionItems = [{ id: 'ask', name: canApprove ? 'Ask for approval' : 'Harness permissions', description: canApprove ? 'Show each approval requested by the harness.' : 'Approval controls are unavailable for this harness.' },
460
+ ...((canApprove || (!active && resource && currentOptions(resource).permissions === 'autoApprove')) ? [{ id: 'autoApprove', name: 'Auto approve', description: 'Allow tool approval requests for this chat. Harness sandbox rules still apply.' }] : [])];
461
+ const groups = [{ id: 'permissions', name: 'Permissions', kind: 'permissions', items: permissionItems, selected: permissionItems.find(i => i.id === (resource ? currentOptions(resource).permissions : 'ask')) }];
462
+ return groups;
463
+ },
464
+ setPermission: (resource, value) => {
465
+ const id = boundId(resource);
466
+ if (controls && !id)
467
+ throw new Error('This conversation has no session binding.');
468
+ if (id)
469
+ savePermission(id, turnPermission(value));
470
+ else
471
+ permission = turnPermission(value);
472
+ },
473
+ create: async (inputState) => {
474
+ if (!controls)
475
+ throw new Error('The host does not expose conversation creation.');
476
+ await enqueueSelection({ ...controls.state.selection }, false);
477
+ const connection = frontends.get(controls.state.activeSession);
478
+ savePermission(connection.id, turnPermission(inputState.groups.find(group => group.id === 'permissions')?.selected?.id));
479
+ return connection;
480
+ },
481
+ resolve: connectionFor,
482
+ catalogue: () => controls?.state.sessions ?? [],
483
+ bindings: boundId,
484
+ };
485
+ context.subscriptions.push(vscode.commands.registerCommand('supercode.frontend.activateSession', async (resource) => { await connectionFor(vscode.Uri.parse(resource)); }));
486
+ context.subscriptions.push(vscode.commands.registerCommand('supercode.frontend.openSession', async (id) => {
487
+ await connectionFor(vscode.Uri.from({ scheme: SESSION_SCHEME, authority: id, path: '/conversation' }));
488
+ await revealManaged();
489
+ }));
490
+ context.subscriptions.push(vscode.commands.registerCommand('supercode.frontend.configure', async (supplied) => {
491
+ if (supplied?.permissions) {
492
+ const id = controls?.state.activeSession;
493
+ if (id)
494
+ savePermission(id, turnPermission(supplied.permissions));
495
+ else
496
+ permission = turnPermission(supplied.permissions);
497
+ controlsChanged.fire();
498
+ return currentOptions();
499
+ }
500
+ if (supplied && typeof supplied.harness === 'string') {
501
+ await enqueueSelection(supplied);
502
+ return currentOptions();
503
+ }
504
+ if (!controls) {
505
+ void vscode.window.showInformationMessage('This host does not expose chat configuration.');
506
+ return;
507
+ }
508
+ const selection = { ...controls.state.selection };
509
+ const harness = await vscode.window.showQuickPick(controls.state.harnesses.map(h => ({ label: h.name, id: h.id })), { title: 'New chat · Harness', placeHolder: 'Choose the harness for a new conversation. Existing chats keep their harness and history.' });
510
+ if (!harness)
511
+ return;
512
+ if (harness.id !== selection.harness) {
513
+ selection.harness = harness.id;
514
+ selection.model = '';
515
+ selection.effort = '';
516
+ }
517
+ if (['claude-code', 'codex'].includes(selection.harness)) {
518
+ const choices = controls.state.modelsByHarness?.[selection.harness] ?? [{ id: '', name: 'Harness default' }];
519
+ 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.' });
520
+ if (!picked)
521
+ return;
522
+ if (picked.id === '__custom__') {
523
+ 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 });
524
+ if (model === undefined)
525
+ return;
526
+ selection.model = model.trim();
527
+ }
528
+ else
529
+ selection.model = picked.id;
530
+ const effort = await vscode.window.showQuickPick([{ label: 'Harness default', id: '' }, ...['low', 'medium', 'high'].map(id => ({ label: id, id }))], { title: 'New chat · Reasoning effort' });
531
+ if (!effort)
532
+ return;
533
+ selection.effort = effort.id;
534
+ }
535
+ await enqueueSelection(selection);
536
+ }));
537
+ context.subscriptions.push({ dispose: () => frontends.dispose() });
538
+ context.subscriptions.push(frontends.onDidChange(() => {
539
+ void vscode.commands.executeCommand('setContext', ATTACHED_CONTEXT_KEY, frontends.all.length > 0);
540
+ }));
541
+ // From the connections, not `false`: one made earlier in activation (the editor host
542
+ // attaches before this line) already fired its change, and `false` here outlived it.
543
+ await vscode.commands.executeCommand('setContext', ATTACHED_CONTEXT_KEY, frontends.all.length > 0);
544
+ const runTurnInto = async (connection, prompt, stream, token, approval) => {
545
+ if (approval === 'autoApprove' && (!connection.session.descriptor?.actions.respond || !connection.session.permissions.has('approve'))) {
546
+ throw new Error('This harness does not support automatic approval through Chat. Select Harness permissions.');
547
+ }
548
+ const answered = new Set();
549
+ const state = { toolInputs: new Map(), toolNames: new Map() };
550
+ const projection = {
551
+ connectionId: connection.id,
552
+ pendingRequest: (requestId) => connection.session.pendingRequests.get(requestId) ?? connection.session.resolvedRequests.get(requestId),
553
+ canRespond: connection.session.descriptor?.actions.respond !== false,
554
+ };
555
+ const cancellation = token.onCancellationRequested(() => {
556
+ void connection.session.interrupt().catch(() => {
557
+ // The runtime may already be idle; interrupting twice is not an error
558
+ // this view reports.
559
+ });
560
+ });
561
+ try {
562
+ await connection.session.runTurn(prompt, (event) => {
563
+ writeProjected(stream, projectEvent(event, projection), granted, state);
564
+ if (approval === 'autoApprove' && !token.isCancellationRequested) {
565
+ for (const pending of connection.session.pendingRequests.values()) {
566
+ if (pending.kind !== 'approval' || answered.has(pending.id))
567
+ continue;
568
+ const decisions = acceptedDecisions(pending);
569
+ if (decisions && !decisions.includes('allow'))
570
+ continue;
571
+ answered.add(pending.id);
572
+ void connection.session.respond({ kind: 'approval', request_id: pending.id, decision: 'allow' }).catch(error => {
573
+ stream.markdown(`\nApproval failed: ${String(error.message || error)}. Use the approval buttons to answer.\n`);
574
+ });
575
+ }
576
+ }
577
+ });
578
+ }
579
+ finally {
580
+ cancellation.dispose();
581
+ if (controls) {
582
+ if (connection.session.descriptor?.session_id)
583
+ await controls.remember(connection.id, connection.session.descriptor.session_id).catch(error => log.warn(`Could not save conversation identity: ${String(error)}`));
584
+ await controls.refresh().catch(error => log.warn(`Could not refresh chat identity: ${String(error)}`));
585
+ controlsChanged.fire();
586
+ }
587
+ }
588
+ };
589
+ const handler = async (request, chatContext, stream, token) => {
590
+ await applying;
591
+ const requestedResource = chatContext.chatSessionContext?.chatSessionItem?.resource;
592
+ const connection = requestedResource
593
+ ? await connectionFor(requestedResource)
594
+ : controls ? undefined : frontends.get(undefined);
595
+ if (controls && connection && request.model?.vendor === 'supercode-runtime') {
596
+ const selection = controls.state.sessions.find(s => s.id === connection.id).selection;
597
+ const expected = `${selection.harness}/${encodeURIComponent(selection.model || '__default__')}`;
598
+ if (request.model.id !== expected || (typeof request.modelConfiguration?.reasoningEffort === 'string' && request.modelConfiguration.reasoningEffort !== selection.effort)) {
599
+ throw new Error('This conversation has fixed model settings. Start a new chat to choose different settings; no message was sent.');
600
+ }
601
+ }
602
+ if (!connection) {
603
+ stream.markdown(unavailableMarkdown(frontends.unavailable));
604
+ return {};
605
+ }
606
+ // While a request is pending, a typed message is its answer — this is the
607
+ // path for the answers no button can carry (`allow for session`, `cancel`,
608
+ // and an elicitation's free-text content).
609
+ const pending = pendingRequestOf(connection);
610
+ if (pending) {
611
+ const answer = typedAnswer(pending, request.prompt);
612
+ await connection.session.respond(answer);
613
+ stream.markdown(`_answered request #${pending.id}: ${opaqueText(answer, 256)}_`);
614
+ return {};
615
+ }
616
+ if (request.prompt.trim().length === 0) {
617
+ stream.markdown('_nothing to submit_');
618
+ return {};
619
+ }
620
+ const approval = turnPermission(controls ? permissions.get(connection.id) ?? 'ask' : request.permissionLevel);
621
+ const attached = [];
622
+ for (const reference of request.references) {
623
+ const value = reference.value;
624
+ const location = value instanceof vscode.Location ? value : undefined;
625
+ const uri = location?.uri ?? (value instanceof vscode.Uri ? value : undefined);
626
+ if (!uri || !['file', 'vscode-remote', 'untitled'].includes(uri.scheme))
627
+ throw new Error('This harness integration supports text files and selections only. Remove unsupported context before sending.');
628
+ const document = await vscode.workspace.openTextDocument(uri);
629
+ attached.push({ name: uri.toString(), text: document.getText(location?.range) });
630
+ }
631
+ const prompt = withTextContext(request.prompt, attached);
632
+ await runTurnInto(connection, prompt, stream, token, approval);
633
+ return {};
634
+ };
635
+ const participants = [];
636
+ for (const id of granted.has('defaultChatParticipant') ? [PARTICIPANT_ID, DEFAULT_PARTICIPANT_ID] : [PARTICIPANT_ID]) {
637
+ try {
638
+ const participant = vscode.chat.createChatParticipant(id, handler);
639
+ participant.iconPath = icon();
640
+ participants.push(participant);
641
+ context.subscriptions.push(participant);
642
+ }
643
+ catch (error) {
644
+ log.warn(`Volter Harness chat participant ${id} was not registered: ${String(error?.message ?? error)}`);
645
+ }
646
+ }
647
+ // The runtime's sessions in the Chat view's own picker. Without the
648
+ // `chatSessionsProvider` grant the participant alone still works.
649
+ if (granted.has('chatSessionsProvider')) {
650
+ try {
651
+ // Requests route to the session type. Register its dynamic identity here;
652
+ // canDelegate is disabled to avoid a generic workbench-owned registration.
653
+ // A participant names the harness; the native model picker names the model.
654
+ // Keep the author stable when a model changes during request dispatch.
655
+ const participantName = () => harnessDisplayName(controls?.state.selection.harness ?? frontends.get(undefined)?.session.descriptor?.source_harness);
656
+ const participantKey = () => `${participantName()}/${identity().provider ?? ''}`;
657
+ const makeParticipant = () => {
658
+ const current = { name: participantName() };
659
+ const participant = granted.has('chatParticipantPrivate')
660
+ ? vscode.chat.createDynamicChatParticipant(SESSION_SCHEME, { name: current.name.replace(/[^a-zA-Z0-9]/g, ''), fullName: current.name, publisherName: 'Volter', description: 'Ask a question or describe a task' }, handler)
661
+ : vscode.chat.createChatParticipant(SESSION_SCHEME, handler);
662
+ participant.iconPath = icon();
663
+ return participant;
664
+ };
665
+ let sessionParticipant = makeParticipant();
666
+ let previousIdentity = participantKey();
667
+ const updateIdentity = () => {
668
+ if (participantKey() === previousIdentity)
669
+ return;
670
+ previousIdentity = participantKey();
671
+ sessionParticipant.dispose();
672
+ sessionParticipant = makeParticipant();
673
+ for (const p of participants)
674
+ p.iconPath = icon();
675
+ };
676
+ context.subscriptions.push({ dispose: () => sessionParticipant.dispose() }, controlsChanged.event(updateIdentity), frontends.onDidChange(updateIdentity));
677
+ registerSessions(context, frontends, sessionParticipant, granted, handler, controls ? providerControls : undefined);
678
+ }
679
+ catch (error) {
680
+ log.warn(`Volter Harness chat sessions were not registered: ${String(error?.message ?? error)}`);
681
+ }
682
+ }
683
+ /**
684
+ * The one door an answer takes, whether it came from the inline question or
685
+ * from a fallback command button. A button cannot be withdrawn once pushed,
686
+ * so a click on an already-resolved request must be a no-op that SAYS so —
687
+ * never a stale `respond`, and never a silent nothing.
688
+ */
689
+ async function answerRequest(answer) {
690
+ if (!answer || typeof answer.requestId !== 'number')
691
+ return;
692
+ const connection = frontends.get(answer.connectionId);
693
+ if (!connection) {
694
+ void vscode.window.showWarningMessage('Volter Harness is no longer attached to that runtime.');
695
+ return;
696
+ }
697
+ const request = connection.session.pendingRequests.get(answer.requestId);
698
+ if (!request) {
699
+ void vscode.window.showInformationMessage(`Volter Harness: request #${answer.requestId} is already resolved.`);
700
+ return;
701
+ }
702
+ let response = answer.response;
703
+ if (answer.promptForContent) {
704
+ const content = await vscode.window.showInputBox({
705
+ title: 'Volter Harness runtime request',
706
+ prompt: 'The runtime asked for a value; a button cannot carry one.',
707
+ ignoreFocusOut: true,
708
+ });
709
+ if (content === undefined)
710
+ return;
711
+ response = { ...response, content };
712
+ }
713
+ try {
714
+ await connection.session.respond(response);
715
+ void vscode.window.setStatusBarMessage(`Volter Harness: ${resolutionSentence(response, request, answer.requestId)}`, 4000);
716
+ }
717
+ catch (error) {
718
+ if (error instanceof RequestNotPendingError) {
719
+ void vscode.window.showInformationMessage(`Volter Harness: request #${answer.requestId} is already resolved.`);
720
+ return;
721
+ }
722
+ void vscode.window.showErrorMessage(`Volter Harness could not answer request #${answer.requestId}: ${String(error?.message ?? error)}`);
723
+ }
724
+ }
725
+ const secretKey = (baseUrl) => `supercode.frontend.token:${baseUrl}`;
726
+ // The runtime's credential FILE is one-shot, and an extension host reloads.
727
+ // Remembering which runtime this window was handed lets the next activation
728
+ // reconnect from SecretStorage instead of losing the attachment.
729
+ const HANDED_OVER_KEY = 'supercode.frontend.handedOverBaseUrl';
730
+ context.subscriptions.push(vscode.commands.registerCommand('supercode.frontend.setToken', async () => {
731
+ const settings = vscode.workspace.getConfiguration('supercode.frontend');
732
+ const baseUrl = await vscode.window.showInputBox({
733
+ title: 'Volter Harness runtime',
734
+ prompt: 'Base URL of the already-running Volter Harness SDK runtime',
735
+ value: settings.get('baseUrl') ?? 'http://127.0.0.1:0',
736
+ ignoreFocusOut: true,
737
+ });
738
+ if (!baseUrl)
739
+ return;
740
+ const token = await vscode.window.showInputBox({
741
+ title: 'Volter Harness runtime token',
742
+ prompt: 'Scoped runtime credential — stored in SecretStorage, sent only in Authorization',
743
+ password: true,
744
+ ignoreFocusOut: true,
745
+ });
746
+ if (!token)
747
+ return;
748
+ await settings.update('baseUrl', baseUrl, vscode.ConfigurationTarget.Global);
749
+ await context.secrets.store(secretKey(baseUrl), token);
750
+ await connectFromSettings();
751
+ }), vscode.commands.registerCommand('supercode.frontend.connect', () => connectFromSettings()),
752
+ // The create command the core's own `chatSessions/newSession` menu invokes
753
+ // for this session type (`chatSessions.contribution.ts`
754
+ // `_resolveCreateSubMenuCommandId`). It opens a chat on the supercode
755
+ // session type through the editor's own action; nothing here is a second
756
+ // way to reach the runtime.
757
+ vscode.commands.registerCommand('supercode.frontend.newSession', async (position) => {
758
+ if (controls) {
759
+ await vscode.commands.executeCommand('supercode.frontend.configure');
760
+ return;
761
+ }
762
+ const where = position === 'editor' ? 'editor' : 'sidebar';
763
+ await vscode.commands.executeCommand(`workbench.action.chat.openNewChatSessionInPlace.${SESSION_SCHEME}`, where);
764
+ }), vscode.commands.registerCommand('supercode.frontend.disconnect', () => frontends.disconnect()),
765
+ // The one door an answer button uses. It answers the runtime's pending
766
+ // request through `frontend.v2.respond` and narrates itself, because a
767
+ // click whose effect is invisible is a click nobody can trust.
768
+ vscode.commands.registerCommand('supercode.frontend.respond', (answer) => answerRequest(answer)), vscode.commands.registerCommand('supercode.frontend.status', async (inspect) => {
769
+ if (controls)
770
+ await controls.refresh();
771
+ if (inspect === true)
772
+ return { connections: frontends.all.map(c => ({ ...frontends.info(c), permissions: [...c.session.permissions], pendingRequests: [...c.session.pendingRequests.values()] })), selection: controls?.state.selection, activeSession: controls?.state.activeSession, busy: Boolean(controls?.state.busy || frontends.all.some(c => c.session.descriptor?.turn_state === 'busy' || c.session.pendingRequests.size)), sessions: controls?.state.sessions, permission: currentOptions().permissions, identity: identity() };
773
+ const rows = frontends.all.map((connection) => {
774
+ const info = frontends.info(connection);
775
+ return `${info.label} · ${info.baseUrl} · ${info.model ?? 'unknown model'} · ${info.turnState ?? 'unattached'}`;
776
+ });
777
+ const proposals = [...granted];
778
+ await vscode.window.showInformationMessage(rows.length ? rows.join('\n') : 'Volter Harness is not attached to a runtime.', {
779
+ modal: true,
780
+ detail: proposals.length
781
+ ? proposals.map((name) => `${name}: ${PROPOSAL_EFFECTS[name].grants}`).join('\n')
782
+ : REQUESTED_PROPOSALS.map((name) => `${name} not granted: ${PROPOSAL_EFFECTS[name].without}`).join('\n'),
783
+ });
784
+ }));
785
+ async function connectFromSettings() {
786
+ const settings = vscode.workspace.getConfiguration('supercode.frontend');
787
+ const baseUrl = settings.get('baseUrl')?.trim() || context.globalState.get(HANDED_OVER_KEY);
788
+ if (!baseUrl)
789
+ return;
790
+ const token = await context.secrets.get(secretKey(baseUrl));
791
+ if (!token) {
792
+ log.info('Volter Harness: a base URL is configured but no token is stored; run Volter Harness: Set Runtime Token.');
793
+ return;
794
+ }
795
+ await frontends
796
+ .connect({ baseUrl, token, permissions: normalizePermissions(settings.get('permissions'), ['observe']) }, log)
797
+ .catch((error) => {
798
+ log.error(`Volter Harness could not attach to ${baseUrl}: ${String(error?.message ?? error)}`);
799
+ });
800
+ }
801
+ /**
802
+ * The handover path: a runtime (or a product that launched this editor's
803
+ * server) exports the endpoint and a scoped credential FILE, mirroring what
804
+ * `crates/cli/src/frontend_client.rs` already does for the pi frontend. No
805
+ * pasted token, no secret in argv or a setting.
806
+ */
807
+ async function connectFromEnvironment() {
808
+ const env = controls?.state.connection ?? process.env;
809
+ const baseUrl = env[ENV.url]?.trim();
810
+ const credentialFile = env[ENV.credentialFile]?.trim();
811
+ // A host that could not start a runtime says so here, and that sentence is
812
+ // what the person reads instead of an empty chat that looks like a working
813
+ // one.
814
+ const unavailable = process.env[ENV.unavailable]?.trim();
815
+ if (unavailable)
816
+ frontends.setUnavailable(unavailable);
817
+ if (!baseUrl || !credentialFile)
818
+ return false;
819
+ try {
820
+ const token = await readCredentialFile(credentialFile);
821
+ // The file is read, never consumed: an extension host reloads and must be
822
+ // able to reconnect. SecretStorage is a convenience for the settings
823
+ // path, not a durable home in every host — see `credential.ts`.
824
+ await context.secrets.store(secretKey(baseUrl), token);
825
+ await context.globalState.update(HANDED_OVER_KEY, baseUrl);
826
+ await frontends.connect({
827
+ baseUrl,
828
+ token,
829
+ clientId: env[ENV.clientId]?.trim() || undefined,
830
+ connectionId: controls?.state.activeSession ?? undefined,
831
+ permissions: normalizePermissions(env[ENV.permissions], ['observe', 'interact', 'approve']),
832
+ label: identity().name,
833
+ }, log);
834
+ return true;
835
+ }
836
+ catch (error) {
837
+ log.error(`Volter Harness could not use ${ENV.credentialFile}: ${String(error?.message ?? error)}`);
838
+ return false;
839
+ }
840
+ }
841
+ /**
842
+ * A RESUMED runtime arrives with the person's conversation already in it —
843
+ * vgai reopens a project on `claude --resume <id>`. Opening the panel beside
844
+ * that conversation instead of on it is the same defect as not showing it at
845
+ * all: the history exists and nobody sees it. So an attachment that brings
846
+ * history opens its own session, once, on the surface the person is looking
847
+ * at. A runtime with no history is left alone; there is nothing to show and
848
+ * stealing the panel would be rude.
849
+ */
850
+ async function openResumedSession() {
851
+ if (!granted.has('chatSessionsProvider'))
852
+ return;
853
+ if (controls) {
854
+ try {
855
+ await revealManaged();
856
+ }
857
+ catch (error) {
858
+ log.error(`Could not open the saved conversation: ${String(error)}`);
859
+ }
860
+ return;
861
+ }
862
+ const connection = frontends.all.find((entry) => Boolean(controls) || entry.session.history.length > 0 ||
863
+ entry.session.replay.some((event) => event.kind === 'user_message'));
864
+ if (!connection)
865
+ return;
866
+ try {
867
+ // The editor's own action for this session type. `vscode.openWith` on
868
+ // the session resource opens a LOCAL chat instead — observed — because
869
+ // nothing routes a `supercode:` uri to the session provider's editor.
870
+ await vscode.commands.executeCommand(`workbench.action.chat.openNewChatSessionInPlace.${SESSION_SCHEME}`, 'sidebar');
871
+ log.info(`Volter Harness opened the resumed session ${connection.label} on its history`);
872
+ }
873
+ catch (error) {
874
+ log.warn(`Volter Harness could not open the resumed session: ${String(error?.message ?? error)}`);
875
+ }
876
+ }
877
+ // Precedence: a host's own `connect()` wins, then the runtime's environment
878
+ // handover, then the configured base URL plus its stored secret.
879
+ await vscode.commands.executeCommand('setContext', DEFAULT_PARTICIPANT_CONTEXT_KEY, granted.has('defaultChatParticipant'));
880
+ if (!(await connectFromEnvironment()))
881
+ await connectFromSettings();
882
+ // NOT awaited: this opens an editor, and activation must return before the
883
+ // workbench will run one. Awaiting it here left activation hanging and the
884
+ // panel on "Loading…" — observed.
885
+ const opening = setTimeout(() => void openResumedSession(), 500);
886
+ context.subscriptions.push({ dispose: () => clearTimeout(opening) });
887
+ return {
888
+ connect: async (options) => {
889
+ const connection = await frontends.connect(options, log);
890
+ return connection ? frontends.info(connection) : undefined;
891
+ },
892
+ disconnect: (connectionId) => frontends.disconnect(connectionId),
893
+ get connections() {
894
+ return frontends.all.map((connection) => frontends.info(connection));
895
+ },
896
+ get grantedProposals() {
897
+ return [...granted];
898
+ },
899
+ onDidChangeConnections: frontends.onDidChange,
900
+ };
901
+ }
902
+ export function deactivate() {
903
+ // Every disposable is owned by the extension context.
904
+ }
905
+ /**
906
+ * One rendered part as the markdown a HISTORY turn carries. A replayed turn is
907
+ * built from `ChatResponseMarkdownPart`s only — the live part types belong to a
908
+ * running stream — so a tool row becomes its own one-line summary rather than
909
+ * the JSON of the object that would have drawn it.
910
+ */
911
+ function historyText(part) {
912
+ switch (part.part) {
913
+ case 'markdown':
914
+ case 'progress':
915
+ return part.value;
916
+ case 'warning':
917
+ return `> ⚠ ${part.value}`;
918
+ case 'thinking':
919
+ return `> ${part.value}`;
920
+ case 'codeblock':
921
+ return part.value;
922
+ case 'reference':
923
+ return `\`${part.path}\``;
924
+ case 'tool': {
925
+ // No `<details>` here: a HISTORY turn's markdown is rendered without raw
926
+ // HTML, so the tags show up as text — observed. A replayed tool is one
927
+ // line plus its output, fenced.
928
+ const summary = part.pastTenseMessage ?? part.invocationMessage ?? part.toolName;
929
+ if (!summary)
930
+ return '';
931
+ const verb = part.isError ? 'Tool failed:' : 'Tool call:';
932
+ const result = part.resultText ? `\n\n${fence(part.resultText)}` : '';
933
+ return `${verb} \`${summary}\`${result}`;
934
+ }
935
+ case 'answers':
936
+ return `**${part.title}**\n\n${part.message}`;
937
+ }
938
+ }
939
+ /**
940
+ * The editor's session list ALWAYS reads `timing.created` and renders the epoch
941
+ * ("57 yrs ago") when a provider supplies none — measured in
942
+ * `agentSessionsViewer`, which has no path that omits the time. `frontend.v2`
943
+ * carries no session timestamp of its own, so the true times this frontend
944
+ * knows are its own: when this window attached, and the turn boundaries it
945
+ * watched. Those are what the item gets.
946
+ */
947
+ function applyTiming(item, connection) {
948
+ const created = connection.session.attachedAtMs;
949
+ if (created === null)
950
+ return;
951
+ item.timing = {
952
+ created,
953
+ lastRequestStarted: connection.session.lastTurnStartedMs ?? undefined,
954
+ lastRequestEnded: connection.session.lastTurnEndedMs ?? undefined,
955
+ };
956
+ }
957
+ function registerSessions(context, frontends, participant, granted, handler, controls) {
958
+ const turns = vscode;
959
+ /**
960
+ * The session list is the door that actually SHOWS a reason on screen.
961
+ * `chatViewsWelcome` cannot: `ChatViewPane.shouldShowWelcome()` in
962
+ * `src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts`
963
+ * begins `const shouldShow = !hasCoreAgent && …`, so the whole welcome
964
+ * control is suppressed whenever ANY core agent is registered for the Chat
965
+ * location — which a Code-OSS derivative with an agent host always has. The
966
+ * `when` clause is never even consulted there.
967
+ */
968
+ const unavailableItem = () => {
969
+ if (!frontends.unavailable || frontends.all.length > 0)
970
+ return undefined;
971
+ const item = controller.createChatSessionItem(unavailableResource(), 'Volter Harness · no runtime');
972
+ item.description = frontends.unavailable.split('\n')[0];
973
+ item.status = vscode.ChatSessionStatus.Failed;
974
+ return item;
975
+ };
976
+ const items = () => {
977
+ const unavailable = unavailableItem();
978
+ if (unavailable)
979
+ return [unavailable];
980
+ return (controls?.catalogue() ?? []).filter(entry => !frontends.get(entry.id)).map(entry => {
981
+ const item = controller.createChatSessionItem(vscode.Uri.from({ scheme: SESSION_SCHEME, authority: entry.id, path: '/conversation' }), entry.title);
982
+ item.description = runtimeIdentity(entry.selection.harness, entry.selection.model).name;
983
+ item.status = vscode.ChatSessionStatus.Completed;
984
+ item.timing = { created: entry.created };
985
+ return item;
986
+ });
987
+ };
988
+ const connectedItem = (connection) => {
989
+ const item = controller.createChatSessionItem(sessionResource(connection), connection.label);
990
+ item.description = connection.label;
991
+ applyTiming(item, connection);
992
+ item.status = vscode.ChatSessionStatus[sessionStatus(connection.session)];
993
+ return item;
994
+ };
995
+ const refreshItems = () => controller.items.replace(items().concat(frontends.all.map(connectedItem)));
996
+ const controller = vscode.chat.createChatSessionItemController(SESSION_SCHEME, async () => { refreshItems(); });
997
+ context.subscriptions.push(controller, frontends.onDidChange(refreshItems));
998
+ if (controls) {
999
+ const states = new Set();
1000
+ controller.getChatSessionInputState = (resource, { previousInputState }) => {
1001
+ const previous = previousInputState?.groups.find(group => group.id === 'permissions')?.selected?.id;
1002
+ // Saved conversation preferences survive extension-host reloads. Native input state
1003
+ // owns live changes; a blank conversation always starts with its own Ask default.
1004
+ const groups = controls.options(resource).map(group => ({ ...group,
1005
+ selected: group.items.find(item => item.id === (resource ? group.selected?.id : previous ?? group.selected?.id)) ?? group.selected,
1006
+ }));
1007
+ const state = controller.createChatSessionInputState(groups);
1008
+ states.add(state);
1009
+ const changed = state.onDidChange(() => {
1010
+ const target = state.sessionResource ?? resource;
1011
+ if (target)
1012
+ controls.setPermission(target, state.groups.find(group => group.id === 'permissions')?.selected?.id);
1013
+ });
1014
+ const disposed = state.onDidDispose(() => { states.delete(state); changed.dispose(); disposed.dispose(); });
1015
+ return state;
1016
+ };
1017
+ controller.newChatSessionItemHandler = async ({ inputState }, token) => {
1018
+ if (token.isCancellationRequested)
1019
+ throw new vscode.CancellationError();
1020
+ return connectedItem(await controls.create(inputState));
1021
+ };
1022
+ context.subscriptions.push(controls.changed(() => {
1023
+ refreshItems();
1024
+ for (const state of states) {
1025
+ if (state.sessionResource)
1026
+ state.groups = controls.options(state.sessionResource);
1027
+ }
1028
+ }));
1029
+ }
1030
+ context.subscriptions.push(vscode.chat.registerChatSessionContentProvider(SESSION_SCHEME, {
1031
+ provideChatSessionContent: async (resource, _token, { inputState }) => {
1032
+ const boundHandler = (request, chatContext, stream, token) => handler(request, { ...chatContext, chatSessionContext: { chatSessionItem: { resource, label: 'Conversation' } } }, stream, token);
1033
+ if (resource.path === UNAVAILABLE_PATH) {
1034
+ // As HISTORY, not `activeResponseCallback`: the editor only invokes
1035
+ // that callback for a session it believes is mid-response, and this
1036
+ // one is not running anything. A history turn renders the moment
1037
+ // the session opens, with no request behind it and nothing to wait
1038
+ // for — which is the whole point, since the person has not typed.
1039
+ return {
1040
+ title: 'Volter Harness · no runtime',
1041
+ history: [
1042
+ new turns.ChatRequestTurn(STATUS_MARKER_PROMPT, undefined, [], participant.id, []),
1043
+ new turns.ChatResponseTurn([new vscode.ChatResponseMarkdownPart(unavailableMarkdown(frontends.unavailable))], {}, participant.id),
1044
+ ],
1045
+ requestHandler: boundHandler,
1046
+ };
1047
+ }
1048
+ // Only a native untitled resource can create a conversation. Existing
1049
+ // resources must resolve their exact saved binding; there is no fallback.
1050
+ const connection = controls ? (resource.path.startsWith('/untitled-') && !controls.bindings(resource) ? undefined : await controls.resolve(resource)) : frontends.get(resource.authority);
1051
+ if (!connection) {
1052
+ return { history: [], requestHandler: boundHandler };
1053
+ }
1054
+ if (controls) {
1055
+ controls.setPermission(resource, inputState.groups.find(group => group.id === 'permissions')?.selected?.id);
1056
+ inputState.groups = controls.options(resource);
1057
+ }
1058
+ // Re-take the attachment snapshot: the runtime republishes its
1059
+ // canonical history at every turn boundary (`submit_claimed` writes
1060
+ // `state.history`), so the copy taken when this window attached is
1061
+ // stale as soon as a turn runs. The generated client reuses the
1062
+ // already-open event stream, so this is a snapshot, not a reconnect.
1063
+ await connection.session.attach().catch(() => {
1064
+ // An unreachable runtime replays what this window last saw.
1065
+ });
1066
+ // The attach snapshot IS the session's history: the runtime's own
1067
+ // canonical transcript, replayed as turns.
1068
+ const history = [];
1069
+ // A hosted harness runtime has no canonical ChatMessage history —
1070
+ // `HostedHarnessRuntime::attach` returns an empty one and puts the
1071
+ // conversation in `replay`. Read whichever the runtime actually
1072
+ // filled, or a resumed session opens blank beside the person's own
1073
+ // conversation.
1074
+ const canonicalHistory = connection.persistedHistory?.length ? connection.persistedHistory : connection.session.history;
1075
+ const pendingToolNames = new Set();
1076
+ if (connection.session.descriptor?.turn_state === 'busy') {
1077
+ for (const request of connection.session.pendingRequests.values()) {
1078
+ const payload = request.payload;
1079
+ if (request.kind !== 'approval' || !payload || typeof payload !== 'object' || Array.isArray(payload))
1080
+ continue;
1081
+ const name = payload.tool ?? payload.tool_name;
1082
+ if (typeof name === 'string')
1083
+ pendingToolNames.add(name);
1084
+ }
1085
+ }
1086
+ const projected = canonicalHistory.length > 0
1087
+ ? projectHistory(canonicalHistory, granted, pendingToolNames)
1088
+ : projectReplayTurns(connection.session.replay, granted, {
1089
+ connectionId: connection.id,
1090
+ pendingRequest: (id) => connection.session.pendingRequests.get(id) ?? connection.session.resolvedRequests.get(id),
1091
+ canRespond: connection.session.descriptor?.actions.respond !== false,
1092
+ });
1093
+ if (connection.historyTruncated)
1094
+ 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));
1095
+ for (const turn of projected) {
1096
+ if (turn.role === 'user') {
1097
+ history.push(new turns.ChatRequestTurn(turn.prompt ?? '', undefined, [], participant.id, []));
1098
+ continue;
1099
+ }
1100
+ const markdown = (turn.parts ?? [])
1101
+ .map(historyText)
1102
+ .filter(Boolean)
1103
+ .join('\n\n');
1104
+ history.push(new turns.ChatResponseTurn([new vscode.ChatResponseMarkdownPart(markdown)], {}, participant.id));
1105
+ }
1106
+ const busy = connection.session.descriptor?.turn_state === 'busy';
1107
+ return {
1108
+ title: connection.label,
1109
+ options: controls?.currentOptions(resource),
1110
+ history,
1111
+ requestHandler: boundHandler,
1112
+ activeResponseCallback: busy
1113
+ ? async (stream, token) => {
1114
+ const state = { toolInputs: new Map(), toolNames: new Map() };
1115
+ writeProjected(stream, projectPendingRequests(connection.session.pendingRequests.values(), {
1116
+ connectionId: connection.id,
1117
+ canRespond: connection.session.descriptor?.actions.respond !== false,
1118
+ }), granted, state);
1119
+ await observeActiveResponse(connection.session, token, (event) => {
1120
+ writeProjected(stream, projectEvent(event, {
1121
+ connectionId: connection.id,
1122
+ pendingRequest: (id) => connection.session.pendingRequests.get(id) ?? connection.session.resolvedRequests.get(id),
1123
+ canRespond: connection.session.descriptor?.actions.respond !== false,
1124
+ }), granted, state);
1125
+ });
1126
+ }
1127
+ : undefined,
1128
+ };
1129
+ },
1130
+ }, participant, { supportsInterruptions: true }));
1131
+ }