@volter/supercode-frontend-vscode 0.1.57 → 0.1.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/view.js CHANGED
@@ -195,11 +195,75 @@ export function shellCommand(toolName, argumentsText) {
195
195
  catch { /* not JSON */ }
196
196
  return undefined;
197
197
  }
198
- /** The summary line this view shows for a request payload. */
198
+ /** The harness-supplied arguments of an approval; no files are read to reconstruct an edit. */
199
+ function approvalArguments(fields) {
200
+ const value = fields.arguments ?? fields.raw_args ?? fields.input;
201
+ if (typeof value !== 'string')
202
+ return value;
203
+ try {
204
+ return JSON.parse(value);
205
+ }
206
+ catch {
207
+ return value;
208
+ }
209
+ }
210
+ /** An approval preview is display text, never a patch to apply or an answer to the request. */
211
+ function approvalSummary(fields) {
212
+ const args = approvalArguments(fields);
213
+ const tool = text(fields.tool ?? fields.tool_name);
214
+ const values = args && typeof args === 'object' && !Array.isArray(args) ? args : undefined;
215
+ const path = values ? text(values.file_path ?? values.path ?? values.file ?? values.notebook_path) : '';
216
+ const heading = [tool, path || text(fields.subject ?? fields.message ?? fields.reason)].filter(Boolean)
217
+ .map(value => plain(clamp(stripControls(value), VSCODE_CHAT_LIMITS.titleCharacters))).join(' · ');
218
+ // Bound before adding fences, so truncation cannot leave an open code block.
219
+ const preview = (value, language = '') => {
220
+ const cleaned = stripControls(value);
221
+ const limited = clamp(cleaned, 24 * 1024);
222
+ return fence(limited, language) + (limited !== cleaned ? '\n\n_Preview truncated; the harness request is unchanged._' : '');
223
+ };
224
+ const changes = [];
225
+ const edits = values && Array.isArray(values.edits) ? values.edits : values ? [values] : [];
226
+ for (const edit of edits.slice(0, 4)) {
227
+ if (!edit || typeof edit !== 'object' || Array.isArray(edit))
228
+ continue;
229
+ if (typeof edit.old_string === 'string' && typeof edit.new_string === 'string') {
230
+ changes.push(`**Replace${edit.replace_all === true ? ' every match' : ''}**\n\n${preview(edit.old_string)}\n\n**With**\n\n${preview(edit.new_string)}`);
231
+ }
232
+ }
233
+ if (edits.length > 4)
234
+ changes.push('_Additional edits omitted from this preview; the harness request is unchanged._');
235
+ if (!changes.length && values) {
236
+ const patch = values.patch ?? values.diff;
237
+ const contents = values.content ?? values.new_source;
238
+ if (typeof patch === 'string')
239
+ changes.push(preview(patch, 'diff'));
240
+ else if (typeof contents === 'string')
241
+ changes.push(`**Proposed contents**\n\n${preview(contents)}`);
242
+ }
243
+ if (!changes.length && args !== undefined && args !== null) {
244
+ changes.push(preview(typeof args === 'string' ? args : JSON.stringify(args, null, 2), typeof args === 'string' ? '' : 'json'));
245
+ }
246
+ if (!changes.length)
247
+ changes.push('_The harness supplied no change preview._');
248
+ // Each edit is separately bounded; the overall card is bounded too. Retain
249
+ // complete fenced sections rather than cutting through their markup.
250
+ let result = heading;
251
+ for (const change of changes) {
252
+ if (result.length + change.length > VSCODE_CHAT_LIMITS.partCharacters - 128) {
253
+ result += '\n\n_Additional preview omitted; the harness request is unchanged._';
254
+ break;
255
+ }
256
+ result += `\n\n${change}`;
257
+ }
258
+ return result;
259
+ }
260
+ /** The information this view shows for a request payload. */
199
261
  export function requestSummary(request) {
200
262
  const payload = request.payload;
201
263
  if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
202
264
  const fields = payload;
265
+ if (request.kind === 'approval')
266
+ return approvalSummary(fields);
203
267
  // `decide_approval` in crates/harness/src/server.rs publishes
204
268
  // `{tool, subject, raw_args}` and `ask_elicitation` publishes
205
269
  // `{message, requested_schema}`; the contract's own conformance fixture
@@ -348,11 +412,10 @@ export function requestSubject(request) {
348
412
  const tool = typeof fields.tool === 'string' ? fields.tool
349
413
  : typeof fields.tool_name === 'string' ? fields.tool_name
350
414
  : undefined;
415
+ const args = approvalArguments(fields);
351
416
  const subject = typeof fields.subject === 'string' ? fields.subject
352
- : typeof fields.raw_args === 'string' ? fields.raw_args
353
- : typeof fields.arguments === 'object' && fields.arguments !== null && !Array.isArray(fields.arguments)
354
- ? toolSubject(JSON.stringify(fields.arguments))
355
- : undefined;
417
+ : args && typeof args === 'object' ? toolSubject(JSON.stringify(args))
418
+ : undefined;
356
419
  if (tool && subject)
357
420
  return `${tool} on ${clamp(stripControls(subject), 120)}`;
358
421
  if (tool)
@@ -377,12 +440,26 @@ export function projectEvent(event, context) {
377
440
  const payload = payloadOf(event);
378
441
  switch (event.kind) {
379
442
  // crates/harness/src/server.rs `submit_claimed` — the runtime's echo of the
380
- // prompt. VS Code already renders the request turn, so the live view drops
381
- // its own echo; only the session-history projection reads it.
382
- case 'user_message':
443
+ // initial prompt, before `turn_started`. VS Code renders that request turn.
444
+ // A rejoined turn has no submitting handler; later runtime input is visible
445
+ // within its live response and remains a user turn in canonical history.
446
+ case 'user_message': {
447
+ if (context.liveUserMessages) {
448
+ const body = text(payload.text);
449
+ const automatic = automaticPrompt(body);
450
+ if (automatic)
451
+ return automatic.notice
452
+ ? [stable({ part: 'markdown', value: `\n\n${automatic.notice}\n\n` })]
453
+ : [];
454
+ const prefix = '\n\n**Additional input**\n\n';
455
+ const suffix = '\n\n';
456
+ const value = clamp(stripControls(body), VSCODE_CHAT_LIMITS.partCharacters - prefix.length - suffix.length);
457
+ return value ? [stable({ part: 'markdown', value: `${prefix}${value}${suffix}` })] : [];
458
+ }
383
459
  return context.echoOwnPrompts
384
460
  ? [stable({ part: 'markdown', value: clamp(stripControls(text(payload.text)), VSCODE_CHAT_LIMITS.partCharacters) })]
385
461
  : [];
462
+ }
386
463
  // crates/harness/src/server.rs `submit_claimed`, and
387
464
  // crates/runtime/src/event.rs `AgentEvent::TurnCompleted` (one model
388
465
  // round-trip). Lifecycle only — the turn resolves on these, never renders.
@@ -487,7 +564,7 @@ export function projectEvent(event, context) {
487
564
  const answers = requestAnswers(request, context.connectionId);
488
565
  const subject = requestSubject(request);
489
566
  const title = request.kind === 'approval'
490
- ? 'Approval required'
567
+ ? subject ? `Allow ${stripControls(subject)}?` : 'Approval required'
491
568
  : subject ? `Approve ${subject}` : `Assistant request #${request.id}`;
492
569
  // The host capability decides whether these original answers can use
493
570
  // a native in-turn confirmation card. Stock confirmation clicks start a
@@ -545,11 +622,11 @@ export function projectEvent(event, context) {
545
622
  return [stable({ part: 'warning', value: 'The turn was interrupted.' })];
546
623
  // crates/harness/src/server.rs `submit_claimed` emits `turn_failed`. The
547
624
  // other four are the error kinds the donor-neutral reader in
548
- // crates/frontend-model/src/transcript.rs treats identically:
625
+ // sdk/interchange/frontend-transcript.mjs treats identically:
549
626
  // `transport_error` from crates/opencode-frontend/src/server.rs,
550
627
  // `watch_error` from crates/interchange/src/watch.rs, and
551
628
  // `runtime_disconnected`, which a frontend synthesizes for itself
552
- // (crates/frontend-tui/src/app.rs). `stream_error` has no producer in this
629
+ // (sdk/interchange/frontend-app.mjs). `stream_error` has no producer in this
553
630
  // repository and is read only because that reader reads it.
554
631
  case 'turn_failed':
555
632
  case 'stream_error':
@@ -566,7 +643,7 @@ export function projectEvent(event, context) {
566
643
  }
567
644
  // Declared by the runtime descriptor's `display.event_kinds`
568
645
  // (crates/harness/src/server.rs `frontend_descriptor`); read here the way
569
- // crates/frontend-model/src/transcript.rs reads them.
646
+ // sdk/interchange/frontend-transcript.mjs reads them.
570
647
  case 'scheduled_prompt_started':
571
648
  case 'scheduled_prompt_deferred':
572
649
  case 'scheduled_prompt_completed':
@@ -591,7 +668,7 @@ export function projectEvent(event, context) {
591
668
  case 'warning':
592
669
  return [stable({ part: 'warning', value: clamp(stripControls(text(payload.message)), VSCODE_CHAT_LIMITS.partCharacters) })];
593
670
  // Not in this runtime's declared set; read the way
594
- // crates/frontend-model/src/transcript.rs reads a donor that emits them.
671
+ // sdk/interchange/frontend-transcript.mjs reads a donor that emits them.
595
672
  case 'reasoning':
596
673
  case 'thinking': {
597
674
  const body = clamp(stripControls(text(payload.text ?? payload.summary, opaqueText(event.payload))), VSCODE_CHAT_LIMITS.partCharacters);
@@ -641,8 +718,10 @@ function messageText(message) {
641
718
  if (!entry || typeof entry !== 'object' || Array.isArray(entry))
642
719
  return '';
643
720
  const part = entry;
721
+ // Opaque transcript parts can carry structured text. Use the same bounded
722
+ // JSON rendering as live payloads rather than JavaScript's object coercion.
644
723
  if (['text', 'input_text', 'output_text'].includes(String(part.type)))
645
- return String(part.text ?? '');
724
+ return text(part.text);
646
725
  if (['image', 'image_url', 'input_image'].includes(String(part.type)))
647
726
  return '[image]';
648
727
  return '';
@@ -652,8 +731,8 @@ function messageText(message) {
652
731
  }
653
732
  /**
654
733
  * A "user" message no person typed: Claude Code's `<task-notification>` (a background task reporting back) or the
655
- * editor's automatic tripwire nudge. Neither is shown as the person's words. A task notification is said as one
656
- * short line ("Background task completed: <its summary>"); a nudge is not shown. Undefined for a person's message.
734
+ * editor's automatic tripwire nudge or Claude's unmarked interruption record. None is shown as the person's words.
735
+ * A task notification and interruption are short notices; a nudge is not shown. Undefined for a person's message.
657
736
  */
658
737
  export function automaticPrompt(body) {
659
738
  const trimmed = body.trimStart();
@@ -665,6 +744,9 @@ export function automaticPrompt(body) {
665
744
  }
666
745
  if (trimmed.startsWith('[Volter editor tripwire'))
667
746
  return {};
747
+ // Claude persists this reserved notice as an unmarked user-role record.
748
+ if (trimmed.trimEnd() === '[Request interrupted by user]')
749
+ return { notice: '_Turn interrupted._' };
668
750
  return undefined;
669
751
  }
670
752
  export function projectReplayTurns(events, granted, context, history = []) {
@@ -794,6 +876,11 @@ export function projectHistory(history, granted, pendingToolNames = new Set()) {
794
876
  pushAssistant([{ part: 'markdown', value: `\n\n${automatic.notice}\n\n` }]);
795
877
  continue;
796
878
  }
879
+ // The codec preserves the harness's provenance. Image-coordinate notes,
880
+ // compact summaries and injected context are not the person's words.
881
+ if (message.metadata?.isMeta === 'true' || message.metadata?.isCompactSummary === 'true' ||
882
+ message.metadata?.promptSource === 'system')
883
+ continue;
797
884
  toolRows.clear();
798
885
  turns.push({ role: 'user', prompt: clamp(stripControls(body), VSCODE_CHAT_LIMITS.partCharacters) });
799
886
  continue;
@@ -1 +1 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="-1 3.5 26 18" fill="#d97757"><path fill-rule="evenodd" d="M20.998 10.949H24v3.102h-3v3.028h-1.487V20H18v-2.921h-1.487V20H15v-2.921H9V20H7.488v-2.921H6V20H4.487v-2.921H3V14.05H0V10.95h3V5h17.998v5.949zM6 10.949h1.488V8.102H6v2.847zm10.51 0H18V8.102h-1.49v2.847z"/></svg>
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="-1 3.5 26 18" fill="#d97757"><path fill-rule="evenodd" clip-rule="evenodd" d="M20.998 10.949H24v3.102h-3v3.028h-1.487V20H18v-2.921h-1.487V20H15v-2.921H9V20H7.488v-2.921H6V20H4.487v-2.921H3V14.05H0V10.95h3V5h17.998v5.949zM6 10.949h1.488V8.102H6v2.847zm10.51 0H18V8.102h-1.49v2.847z"/></svg>
package/media/google.svg CHANGED
@@ -1 +1 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#8b8df8"><path fill-rule="evenodd" d="M12 24C12 17.373 6.627 12 0 12 6.627 12 12 6.627 12 0c0 6.627 5.373 12 12 12-6.627 0-12 5.373-12 12z"/></svg>
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#8b8df8"><path d="M12 24C12 17.373 6.627 12 0 12 6.627 12 12 6.627 12 0c0 6.627 5.373 12 12 12-6.627 0-12 5.373-12 12z"/></svg>
package/media/openai.svg CHANGED
@@ -1 +1 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="-1 -1 26 26" fill="#aaaaaa"><path fill-rule="evenodd" d="M8.086.457a6.105 6.105 0 013.046-.415c1.333.153 2.521.72 3.564 1.7a.117.117 0 00.107.029c1.408-.346 2.762-.224 4.061.366l.063.03.154.076c1.357.703 2.33 1.77 2.918 3.198.278.679.418 1.388.421 2.126a5.655 5.655 0 01-.18 1.631.167.167 0 00.04.155 5.982 5.982 0 011.578 2.891c.385 1.901-.01 3.615-1.183 5.14l-.182.22a6.063 6.063 0 01-2.934 1.851.162.162 0 00-.108.102c-.255.736-.511 1.364-.987 1.992-1.199 1.582-2.962 2.462-4.948 2.451-1.583-.008-2.986-.587-4.21-1.736a.145.145 0 00-.14-.032c-.518.167-1.04.191-1.604.185a5.924 5.924 0 01-2.595-.622 6.058 6.058 0 01-2.146-1.781c-.203-.269-.404-.522-.551-.821a7.74 7.74 0 01-.495-1.283 6.11 6.11 0 01-.017-3.064.166.166 0 00.008-.074.115.115 0 00-.037-.064 5.958 5.958 0 01-1.38-2.202 5.196 5.196 0 01-.333-1.589 6.915 6.915 0 01.188-2.132c.45-1.484 1.309-2.648 2.577-3.493.282-.188.55-.334.802-.438.286-.12.573-.22.861-.304a.129.129 0 00.087-.087A6.016 6.016 0 015.635 2.31C6.315 1.464 7.132.846 8.086.457zm-.804 7.85a.848.848 0 00-1.473.842l1.694 2.965-1.688 2.848a.849.849 0 001.46.864l1.94-3.272a.849.849 0 00.007-.854l-1.94-3.393zm5.446 6.24a.849.849 0 000 1.695h4.848a.849.849 0 000-1.696h-4.848z"/></svg>
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="-1 -1 26 26" fill="#aaaaaa"><path fill-rule="evenodd" clip-rule="evenodd" d="M8.086.457a6.105 6.105 0 013.046-.415c1.333.153 2.521.72 3.564 1.7a.117.117 0 00.107.029c1.408-.346 2.762-.224 4.061.366l.063.03.154.076c1.357.703 2.33 1.77 2.918 3.198.278.679.418 1.388.421 2.126a5.655 5.655 0 01-.18 1.631.167.167 0 00.04.155 5.982 5.982 0 011.578 2.891c.385 1.901-.01 3.615-1.183 5.14l-.182.22a6.063 6.063 0 01-2.934 1.851.162.162 0 00-.108.102c-.255.736-.511 1.364-.987 1.992-1.199 1.582-2.962 2.462-4.948 2.451-1.583-.008-2.986-.587-4.21-1.736a.145.145 0 00-.14-.032c-.518.167-1.04.191-1.604.185a5.924 5.924 0 01-2.595-.622 6.058 6.058 0 01-2.146-1.781c-.203-.269-.404-.522-.551-.821a7.74 7.74 0 01-.495-1.283 6.11 6.11 0 01-.017-3.064.166.166 0 00.008-.074.115.115 0 00-.037-.064 5.958 5.958 0 01-1.38-2.202 5.196 5.196 0 01-.333-1.589 6.915 6.915 0 01.188-2.132c.45-1.484 1.309-2.648 2.577-3.493.282-.188.55-.334.802-.438.286-.12.573-.22.861-.304a.129.129 0 00.087-.087A6.016 6.016 0 015.635 2.31C6.315 1.464 7.132.846 8.086.457zm-.804 7.85a.848.848 0 00-1.473.842l1.694 2.965-1.688 2.848a.849.849 0 001.46.864l1.94-3.272a.849.849 0 00.007-.854l-1.94-3.393zm5.446 6.24a.849.849 0 000 1.695h4.848a.849.849 0 000-1.696h-4.848z"/></svg>
package/media/xai.svg ADDED
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="-1 -1 26 26" fill="#aaaaaa"><path d="M9.27 15.29l7.978-5.897c.391-.29.95-.177 1.137.272.98 2.369.542 5.215-1.41 7.169-1.951 1.954-4.667 2.382-7.149 1.406l-2.711 1.257c3.889 2.661 8.611 2.003 11.562-.953 2.341-2.344 3.066-5.539 2.388-8.42l.006.007c-.983-4.232.242-5.924 2.75-9.383.06-.082.12-.164.179-.248l-3.301 3.305v-.01L9.267 15.292M7.623 16.723c-2.792-2.67-2.31-6.801.071-9.184 1.761-1.763 4.647-2.483 7.166-1.425l2.705-1.25a7.808 7.808 0 00-1.829-1A8.975 8.975 0 005.984 5.83c-2.533 2.536-3.33 6.436-1.962 9.764 1.022 2.487-.653 4.246-2.34 6.022-.599.63-1.199 1.259-1.682 1.925l7.62-6.815"/></svg>
package/package.json CHANGED
@@ -1,9 +1,21 @@
1
1
  {
2
2
  "name": "@volter/supercode-frontend-vscode",
3
+ "devEngines": {
4
+ "packageManager": {
5
+ "name": "pnpm",
6
+ "version": ">=12.15.0",
7
+ "onFail": "error"
8
+ }
9
+ },
3
10
  "displayName": "Volter Harness",
4
- "version": "0.1.57",
11
+ "version": "0.1.60",
5
12
  "type": "module",
6
13
  "description": "VS Code chat-view frontend for an existing Volter Harness SDK runtime",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/volter-ai/supercode.git",
17
+ "directory": "sdk/frontend-vscode"
18
+ },
7
19
  "publisher": "volter-ai-dev",
8
20
  "icon": "media/icon.png",
9
21
  "main": "./dist/extension.js",
@@ -22,11 +34,6 @@
22
34
  "NOTICE.md",
23
35
  "media"
24
36
  ],
25
- "scripts": {
26
- "build": "node -e \"require('node:fs').rmSync('dist', {recursive:true, force:true})\" && node scripts/sync-sdk.mjs && node scripts/fetch-icon.mjs && tsc -p tsconfig.build.json",
27
- "extension": "npm run build && node scripts/pack-extension.mjs",
28
- "prepack": "npm run build"
29
- },
30
37
  "engines": {
31
38
  "node": ">=20",
32
39
  "vscode": "^1.138.0"
@@ -50,6 +57,20 @@
50
57
  "chatProvider"
51
58
  ],
52
59
  "contributes": {
60
+ "taskDefinitions": [
61
+ {
62
+ "type": "supercode-setup",
63
+ "required": [
64
+ "operation"
65
+ ],
66
+ "properties": {
67
+ "operation": {
68
+ "type": "string",
69
+ "description": "The identity of this coding-agent installation."
70
+ }
71
+ }
72
+ }
73
+ ],
53
74
  "chatParticipants": [
54
75
  {
55
76
  "id": "supercode.chat",
@@ -190,8 +211,12 @@
190
211
  },
191
212
  "supercodeFrontendContract": {
192
213
  "name": "@volter/supercode-frontend",
193
- "version": "0.2.9",
214
+ "version": "0.2.12",
194
215
  "mode": "generated-artifact"
195
216
  },
196
- "license": "MIT OR Apache-2.0"
197
- }
217
+ "license": "MIT OR Apache-2.0",
218
+ "scripts": {
219
+ "build": "node -e \"require('node:fs').rmSync('dist', {recursive:true, force:true})\" && node scripts/sync-sdk.mjs && node scripts/fetch-icon.mjs && node scripts/sync-marks.mjs && tsc -p tsconfig.build.json",
220
+ "extension": "pnpm run build && node scripts/pack-extension.mjs"
221
+ }
222
+ }
@@ -16,6 +16,33 @@ race; HTTP uses bearer authentication plus an opaque client id and permission
16
16
  narrowing. Dropping or detaching this client never closes the runtime;
17
17
  `close()` is a separate terminate-authorized action.
18
18
 
19
+ Attachment snapshots may include `pending_requests`, the complete open-request
20
+ set at the snapshot boundary. An empty array means no request remains open;
21
+ absence denotes an older runtime with replay-derived state. IDs and payloads
22
+ come from the runtime; responses still use its existing exactly-once door.
23
+
24
+ `snapshot_cursor` is the full capture boundary of the descriptor and request
25
+ set, taken under the native projection lock. It differs from `history_cursor`,
26
+ which covers only canonical and retired display history. Transports return
27
+ unfiltered snapshot replay even when an existing stream has advanced.
28
+ `mergeFrontendSnapshot` joins later retained observations in sequence order;
29
+ an evicted suffix refuses attachment with `event_gap`. The view installs the
30
+ authoritative request set at capture before applying later requests, resolutions
31
+ and native turn boundaries. Legacy snapshots without the added cursor retain
32
+ their full replay boundary, and absence of the request set retains legacy
33
+ replay-derived behavior. These display fields grant no response authority.
34
+
35
+ `history_events` contains original sequenced events retired from replay since
36
+ canonical history was last captured. `history_cursor` covers canonical history
37
+ and these historical events; live/replay delivery begins after that boundary.
38
+ Historical events are display data, not events to send back to a harness. Both
39
+ native projection paths retain up to 4096 retired events plus 4096 replay events.
40
+ `history_events_dropped` counts older events outside that display window. A
41
+ frontend must show that omission rather than claim a complete transcript.
42
+ Canonical message history remains bounded separately. Open requests survive
43
+ independently of both event windows. Older clients need updating to render these
44
+ additive snapshot fields. See the [source and compilation record](../../docs/evidence/frontend-pending-snapshot-2026-10-10.md).
45
+
19
46
  `FrontendWebSocketClient.connect()` requires a header-capable socket factory
20
47
  and passes the bearer token only in `Authorization`. It deliberately has no
21
48
  query-token fallback: browser clients that cannot set upgrade headers use the
@@ -33,6 +33,12 @@ export class FrontendClientError extends Error {
33
33
  extensions: Record<string, unknown>;
34
34
  }
35
35
 
36
+ /** Native snapshot plus contiguous later retained events; refuses an evicted suffix. */
37
+ export function mergeFrontendSnapshot(snapshot: FrontendAttachSnapshot, observed?: readonly FrontendEvent[], cursor?: number): {
38
+ captureCursor: number; cursor: number; replay: FrontendEvent[]; later: FrontendEvent[];
39
+ };
40
+ export function projectFrontendDescriptor(descriptor: FrontendRuntimeDescriptor | null, event: FrontendEvent): FrontendRuntimeDescriptor | null;
41
+
36
42
  export class FrontendClient extends GeneratedFrontendClient<RequestOptions> {
37
43
  constructor(options: FrontendClientOptions);
38
44
  readonly clientId: string;
@@ -43,7 +49,7 @@ export class FrontendClient extends GeneratedFrontendClient<RequestOptions> {
43
49
  attach(limit?: number, options?: RequestOptions & {afterSequence?: number}): Promise<FrontendAttachSnapshot>;
44
50
  sendInput(prompt: string, options?: RequestOptions): Promise<{accepted:boolean}>;
45
51
  submit(prompt: string, options?: RequestOptions): Promise<{reply:string}>;
46
- interrupt(options?: RequestOptions): Promise<{interrupted:boolean}>;
52
+ interrupt(options?: RequestOptions & {expectedTurn?: string}): Promise<{interrupted:boolean}>;
47
53
  steer(prompt: string, options?: RequestOptions): Promise<{queued:boolean}>;
48
54
  respond(response: FrontendResponse, options?: RequestOptions): Promise<{accepted:boolean}>;
49
55
  invoke(operation: FrontendOperationInvocation, options?: RequestOptions): Promise<FrontendOperationResult>;
@@ -69,6 +69,79 @@ function validateDescriptor(descriptor) {
69
69
  return descriptor;
70
70
  }
71
71
 
72
+ function validateSnapshot(snapshot) {
73
+ validateDescriptor(snapshot?.descriptor);
74
+ if (!Array.isArray(snapshot.history) || !Array.isArray(snapshot.replay)
75
+ || !Number.isSafeInteger(snapshot.history_cursor) || snapshot.history_cursor < 0
76
+ || (snapshot.history_events !== undefined && !Array.isArray(snapshot.history_events))
77
+ || (snapshot.history_events_dropped !== undefined
78
+ && (!Number.isSafeInteger(snapshot.history_events_dropped) || snapshot.history_events_dropped < 0))) {
79
+ throw new FrontendClientError({name:'transport', message:'invalid frontend attachment snapshot'});
80
+ }
81
+ let previous = 0;
82
+ for (const event of snapshot.history_events ?? []) {
83
+ if (!Number.isSafeInteger(event?.sequence) || event.sequence <= previous
84
+ || (previous > 0 && event.sequence !== previous + 1)
85
+ || event.sequence > snapshot.history_cursor || typeof event.kind !== 'string') {
86
+ throw new FrontendClientError({name:'transport', message:'invalid frontend historical event boundary'});
87
+ }
88
+ previous = event.sequence;
89
+ }
90
+ if (previous > 0 && previous !== snapshot.history_cursor) {
91
+ throw new FrontendClientError({name:'transport', message:'incomplete frontend historical event boundary'});
92
+ }
93
+ }
94
+
95
+ /** Join a captured snapshot to retained later observations without changing
96
+ * the native capture point. A missing bounded suffix refuses attachment. */
97
+ export function mergeFrontendSnapshot(snapshot, observed = [], cursor = 0) {
98
+ validateSnapshot(snapshot);
99
+ const captureCursor = snapshot.snapshot_cursor ?? Math.max(snapshot.history_cursor,
100
+ ...snapshot.replay.map(event => event.sequence));
101
+ if (!Number.isSafeInteger(captureCursor) || captureCursor < snapshot.history_cursor
102
+ || !Number.isSafeInteger(cursor) || cursor < 0) {
103
+ throw new FrontendClientError({name:'transport', message:'invalid frontend snapshot capture boundary'});
104
+ }
105
+ const events = new Map();
106
+ for (const event of snapshot.replay) {
107
+ if (!Number.isSafeInteger(event?.sequence) || event.sequence <= snapshot.history_cursor
108
+ || event.sequence > captureCursor || typeof event.kind !== 'string') {
109
+ throw new FrontendClientError({name:'transport', message:'invalid frontend snapshot replay boundary'});
110
+ }
111
+ events.set(event.sequence, event);
112
+ }
113
+ for (const event of observed) {
114
+ if (event.sequence > captureCursor) events.set(event.sequence, event);
115
+ }
116
+ const replay = [...events.values()].sort((a, b) => a.sequence - b.sequence);
117
+ const later = replay.filter(event => event.sequence > captureCursor);
118
+ let projected = captureCursor;
119
+ for (const event of later) {
120
+ if (!Number.isSafeInteger(event.sequence) || event.sequence !== projected + 1) {
121
+ throw new FrontendClientError({name:'event_gap', message:`frontend snapshot needs event ${projected + 1}, received ${event.sequence}`});
122
+ }
123
+ projected = event.sequence;
124
+ }
125
+ if (projected < cursor) {
126
+ throw new FrontendClientError({name:'event_gap', message:`frontend snapshot lost retained events ${projected + 1} through ${cursor}`});
127
+ }
128
+ return {captureCursor, cursor:projected, replay, later};
129
+ }
130
+
131
+ /** Project only native lifecycle events; a model round-trip does not idle a turn. */
132
+ export function projectFrontendDescriptor(descriptor, event) {
133
+ if (!descriptor) return descriptor;
134
+ if (event.kind === 'runtime_disconnected') {
135
+ return {...descriptor, connection_state:'shutting_down', turn_state:'idle',
136
+ actions:{...descriptor.actions, submit:false, steer:false, interrupt:false, respond:false}};
137
+ }
138
+ if (['turn_succeeded', 'turn_failed', 'turn_interrupted'].includes(event.kind)) {
139
+ return {...descriptor, turn_state:'idle'};
140
+ }
141
+ if (['turn_started', 'user_message'].includes(event.kind)) return {...descriptor, turn_state:'busy'};
142
+ return descriptor;
143
+ }
144
+
72
145
  export class FrontendClient extends FrontendEmitter {
73
146
  #baseUrl;
74
147
  #token;
@@ -132,13 +205,12 @@ export class FrontendClient extends FrontendEmitter {
132
205
  this.stopEvents();
133
206
  throw error;
134
207
  }
135
- validateDescriptor(snapshot?.descriptor);
136
- if (!Array.isArray(snapshot.history) || !Array.isArray(snapshot.replay) || !Number.isSafeInteger(snapshot.history_cursor)) {
137
- throw new FrontendClientError({ name: 'transport', message: 'invalid frontend attachment snapshot' });
138
- }
139
- this.#lastSequence = Math.max(this.#lastSequence, afterSequence, snapshot.history_cursor);
140
- snapshot.replay = snapshot.replay.filter((event) => event.sequence > this.#lastSequence);
141
- for (const event of snapshot.replay) this.#lastSequence = Math.max(this.#lastSequence, event.sequence);
208
+ validateSnapshot(snapshot);
209
+ // Replay remains the native snapshot's, even if a running stream already
210
+ // delivered some of it. View adapters need that capture boundary to merge
211
+ // their later observations; the stream's delivery cursor cannot erase it.
212
+ const {captureCursor} = mergeFrontendSnapshot(snapshot);
213
+ this.#lastSequence = Math.max(this.#lastSequence, afterSequence, captureCursor);
142
214
  return snapshot;
143
215
  }
144
216
  async #openEventStream(afterSequence, signal) {
@@ -288,10 +360,12 @@ export class FrontendWebSocketClient extends FrontendEmitter {
288
360
  async describe() { return validateDescriptor(await this.rpc(FRONTEND_METHODS.describe)); }
289
361
  async attach(limit = 1000, {afterSequence = 0} = {}) {
290
362
  const snapshot = await this.rpc(FRONTEND_METHODS.attach, {limit, after_sequence:afterSequence});
291
- validateDescriptor(snapshot?.descriptor);
292
- this.#lastSequence = Math.max(this.#lastSequence, afterSequence, snapshot.history_cursor);
293
- snapshot.replay = snapshot.replay.filter((event) => event.sequence > this.#lastSequence);
294
- for (const event of snapshot.replay) this.#lastSequence = Math.max(this.#lastSequence, event.sequence);
363
+ validateSnapshot(snapshot);
364
+ // Replay remains the native snapshot's, even if a running stream already
365
+ // delivered some of it. View adapters need that capture boundary to merge
366
+ // their later observations; the stream's delivery cursor cannot erase it.
367
+ const {captureCursor} = mergeFrontendSnapshot(snapshot);
368
+ this.#lastSequence = Math.max(this.#lastSequence, afterSequence, captureCursor);
295
369
  return snapshot;
296
370
  }
297
371
  async detach() { const result = await this.rpc(FRONTEND_METHODS.detach); this.closeTransport(); return result; }
@@ -28,6 +28,7 @@
28
28
  "types": {
29
29
  "JsonValue": {"kind": "json"},
30
30
  "Empty": {"kind": "object", "fields": {}},
31
+ "InterruptParams": {"kind": "object", "fields": {"expected_turn": {"type": "string", "optional": true}}},
31
32
  "ChatRole": {"kind": "enum", "values": ["system", "user", "assistant", "tool"]},
32
33
  "FunctionCall": {
33
34
  "kind": "object",
@@ -143,7 +144,11 @@
143
144
  "descriptor": {"type": "FrontendRuntimeDescriptor"},
144
145
  "history": {"type": {"kind": "array", "items": "ChatMessage"}},
145
146
  "history_cursor": {"type": "u64"},
146
- "replay": {"type": {"kind": "array", "items": "FrontendEvent"}}
147
+ "snapshot_cursor": {"type": "u64", "optional": true, "nullable": true},
148
+ "history_events": {"type": {"kind": "array", "items": "FrontendEvent"}, "optional": true},
149
+ "history_events_dropped": {"type": "u64", "optional": true},
150
+ "replay": {"type": {"kind": "array", "items": "FrontendEvent"}},
151
+ "pending_requests": {"type": {"kind": "array", "items": "FrontendRequest"}, "optional": true, "nullable": true}
147
152
  }
148
153
  },
149
154
  "FrontendApprovalDecision": {"kind": "enum", "values": ["deny", "allow", "allow_for_session"]},
@@ -260,7 +265,7 @@
260
265
  {"id": "attach", "wire": "frontend.v2.attach", "sdk_action": "events", "params": "AttachParams", "result": "FrontendAttachSnapshot", "transports": ["local", "http", "websocket", "acp"]},
261
266
  {"id": "send_input", "wire": "frontend.v2.send_input", "sdk_action": "input", "params": "MultimodalPromptParams", "result": "Accepted", "transports": ["local", "http", "websocket", "acp"]},
262
267
  {"id": "submit", "wire": "frontend.v2.submit", "sdk_action": "input", "params": "MultimodalPromptParams", "result": "Reply", "compatibility": "completion_waiting_alias", "transports": ["local", "http", "websocket", "acp"]},
263
- {"id": "interrupt", "wire": "frontend.v2.interrupt", "sdk_action": "interrupt", "params": "Empty", "result": "Interrupted", "transports": ["local", "http", "websocket", "acp"]},
268
+ {"id": "interrupt", "wire": "frontend.v2.interrupt", "sdk_action": "interrupt", "params": "InterruptParams", "result": "Interrupted", "transports": ["local", "http", "websocket", "acp"]},
264
269
  {"id": "steer", "wire": "frontend.v2.steer", "sdk_action": "steer", "params": "PromptParams", "result": "Queued", "transports": ["local", "http", "websocket", "acp"]},
265
270
  {"id": "stop_task", "wire": "frontend.v2.stop_task", "sdk_action": "stop_task", "params": "TaskParams", "result": "Stopped", "transports": ["local", "http", "websocket", "acp"]},
266
271
  {"id": "respond", "wire": "frontend.v2.respond", "sdk_action": "respond", "params": "RespondParams", "result": "Accepted", "transports": ["local", "http", "websocket", "acp"]},
@@ -6,7 +6,7 @@ export abstract class GeneratedFrontendClient<Options = unknown> {
6
6
  attach(limit?: number, options?: Options & {afterSequence?: number}): Promise<FrontendAttachSnapshot>;
7
7
  sendInput(prompt: string, options?: Options & {imageUrls?: string[]}): Promise<Accepted>;
8
8
  submit(prompt: string, options?: Options & {imageUrls?: string[]}): Promise<Reply>;
9
- interrupt(options?: Options): Promise<Interrupted>;
9
+ interrupt(options?: Options & {expectedTurn?: string}): Promise<Interrupted>;
10
10
  steer(prompt: string, options?: Options): Promise<Queued>;
11
11
  stopTask(taskId: string, options?: Options): Promise<Stopped>;
12
12
  respond(response: FrontendResponse, options?: Options): Promise<Accepted>;
@@ -7,7 +7,7 @@ export class GeneratedFrontendClient {
7
7
  attach(limit = 1000, { afterSequence, ...options } = {}) { return this.rpc(FRONTEND_METHODS.attach, { limit, ...(afterSequence === undefined ? {} : {after_sequence:afterSequence}) }, options); }
8
8
  sendInput(prompt, { imageUrls, ...options } = {}) { return this.rpc(FRONTEND_METHODS.send_input, { prompt, ...(imageUrls?.length ? {image_urls:imageUrls} : {}) }, options); }
9
9
  submit(prompt, { imageUrls, ...options } = {}) { return this.rpc(FRONTEND_METHODS.submit, { prompt, ...(imageUrls?.length ? {image_urls:imageUrls} : {}) }, options); }
10
- interrupt(options) { return this.rpc(FRONTEND_METHODS.interrupt, {}, options); }
10
+ interrupt({ expectedTurn, ...options } = {}) { return this.rpc(FRONTEND_METHODS.interrupt, { ...(expectedTurn === undefined ? {} : { expected_turn: expectedTurn }) }, options); }
11
11
  steer(prompt, options) { return this.rpc(FRONTEND_METHODS.steer, { prompt }, options); }
12
12
  stopTask(taskId, options) { return this.rpc(FRONTEND_METHODS.stop_task, { task_id: taskId }, options); }
13
13
  respond(response, options) { return this.rpc(FRONTEND_METHODS.respond, { response }, options); }
@@ -2,6 +2,9 @@
2
2
  export type JsonPrimitive = string | number | boolean | null;
3
3
  export type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue };
4
4
  export type Empty = Record<string, never>;
5
+ export interface InterruptParams {
6
+ expected_turn?: string;
7
+ }
5
8
  export type ChatRole = "system" | "user" | "assistant" | "tool";
6
9
  export interface FunctionCall {
7
10
  name: string;
@@ -79,7 +82,11 @@ export interface FrontendAttachSnapshot {
79
82
  descriptor: FrontendRuntimeDescriptor;
80
83
  history: ChatMessage[];
81
84
  history_cursor: number;
85
+ snapshot_cursor?: number | null;
86
+ history_events?: FrontendEvent[];
87
+ history_events_dropped?: number;
82
88
  replay: FrontendEvent[];
89
+ pending_requests?: FrontendRequest[] | null;
83
90
  }
84
91
  export type FrontendApprovalDecision = "deny" | "allow" | "allow_for_session";
85
92
  export type FrontendElicitationAction = "accept" | "decline" | "cancel";
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@volter/supercode-frontend",
3
- "version": "0.2.9",
3
+ "devEngines": { "packageManager": { "name": "pnpm", "version": ">=12.15.0", "onFail": "error" } },
4
+ "version": "0.2.12",
4
5
  "type": "module",
5
6
  "description": "Generated language-neutral attachment client for Volter Harness SDK runtimes",
6
7
  "exports": {