@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
package/dist/view.js ADDED
@@ -0,0 +1,717 @@
1
+ /**
2
+ * The bounded projection this presentation package renders. Mirrors
3
+ * `@volter/supercode-frontend-browser`'s rule: the view is size-bounded
4
+ * and says so when it omits something; the runtime's own history is unchanged
5
+ * and remains authoritative.
6
+ */
7
+ export const VSCODE_CHAT_LIMITS = Object.freeze({
8
+ partCharacters: 64 * 1024,
9
+ titleCharacters: 256,
10
+ opaqueCharacters: 2 * 1024,
11
+ historyTurns: 500,
12
+ });
13
+ const stable = (emit) => ({ emit });
14
+ function clamp(text, limit) {
15
+ const characters = [...text];
16
+ return characters.length <= limit ? text : `${characters.slice(0, limit - 1).join('')}…`;
17
+ }
18
+ const CONTROL_CHARACTERS = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f\u009b]/gu;
19
+ function stripControls(text) {
20
+ return text.replace(CONTROL_CHARACTERS, '');
21
+ }
22
+ /** Markdown-safe rendering of an arbitrary JSON payload, bounded. */
23
+ export function opaqueText(value, limit = VSCODE_CHAT_LIMITS.opaqueCharacters) {
24
+ let rendered;
25
+ if (typeof value === 'string')
26
+ rendered = value;
27
+ else {
28
+ try {
29
+ rendered = JSON.stringify(value) ?? String(value);
30
+ }
31
+ catch {
32
+ rendered = '<unavailable>';
33
+ }
34
+ }
35
+ return clamp(stripControls(rendered), limit);
36
+ }
37
+ /** A fenced block that its own content cannot break out of. */
38
+ export function fence(body, language = '') {
39
+ const longest = [...body.matchAll(/`{3,}/g)].reduce((max, match) => Math.max(max, match[0].length), 2);
40
+ const ticks = '`'.repeat(Math.max(3, longest + 1));
41
+ return `${ticks}${language}\n${body}\n${ticks}`;
42
+ }
43
+ /**
44
+ * The files a unified diff says it touched, read from its own `+++ b/<path>`
45
+ * headers. Nothing else is derived from the diff: this frontend never rebuilds
46
+ * edits from it (see the `diff_updated` arm).
47
+ */
48
+ export function diffPaths(diff) {
49
+ const paths = [];
50
+ for (const line of diff.split('\n')) {
51
+ const match = /^\+\+\+ (?:b\/)?(.+?)(?:\t.*)?$/.exec(line);
52
+ const value = match?.[1]?.trim();
53
+ if (value && value !== '/dev/null' && !paths.includes(value))
54
+ paths.push(value);
55
+ }
56
+ return paths;
57
+ }
58
+ function payloadOf(event) {
59
+ return event.payload && typeof event.payload === 'object' && !Array.isArray(event.payload)
60
+ ? event.payload
61
+ : {};
62
+ }
63
+ function text(value, fallback = '') {
64
+ if (typeof value === 'string')
65
+ return value;
66
+ if (value === undefined || value === null)
67
+ return fallback;
68
+ return opaqueText(value);
69
+ }
70
+ function parseArguments(value) {
71
+ try {
72
+ return JSON.stringify(JSON.parse(value), null, 2);
73
+ }
74
+ catch {
75
+ return value;
76
+ }
77
+ }
78
+ /**
79
+ * The one-line subject a tool call is about, read from its own arguments.
80
+ * The editor renders `invocationMessage` verbatim, so a bare tool name is a
81
+ * wasted row; `write_file · hello.txt` says what it did.
82
+ */
83
+ export function toolSubject(argumentsText) {
84
+ let parsed;
85
+ try {
86
+ parsed = JSON.parse(argumentsText);
87
+ }
88
+ catch {
89
+ return undefined;
90
+ }
91
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
92
+ return undefined;
93
+ const fields = parsed;
94
+ for (const field of ['path', 'file', 'file_path', 'command', 'pattern', 'query', 'url', 'job_id']) {
95
+ const value = fields[field];
96
+ if (typeof value === 'string' && value.length > 0)
97
+ return clamp(stripControls(value), 120);
98
+ }
99
+ return undefined;
100
+ }
101
+ /** The summary line this view shows for a request payload. */
102
+ export function requestSummary(request) {
103
+ const payload = request.payload;
104
+ if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
105
+ const fields = payload;
106
+ // `decide_approval` in crates/harness/src/server.rs publishes
107
+ // `{tool, subject, raw_args}` and `ask_elicitation` publishes
108
+ // `{message, requested_schema}`; the contract's own conformance fixture
109
+ // (sdk/frontend/test/fixtures/conformance.json) carries `{tool_name,
110
+ // arguments}`. The payload is opaque by design, so this reads whichever
111
+ // naming a runtime uses and falls back to bounded JSON.
112
+ for (const field of ['subject', 'message', 'tool', 'tool_name', 'summary', 'prompt', 'reason']) {
113
+ const value = fields[field];
114
+ if (typeof value === 'string')
115
+ return clamp(stripControls(value), VSCODE_CHAT_LIMITS.partCharacters);
116
+ }
117
+ }
118
+ return opaqueText(payload);
119
+ }
120
+ /** The answers this view offers for a request, one button each. */
121
+ export function requestAnswers(request, connectionId) {
122
+ const base = { connectionId, requestId: request.id };
123
+ if (request.kind === 'approval') {
124
+ const all = [
125
+ { ...base, label: 'Allow', response: { kind: 'approval', request_id: request.id, decision: 'allow' } },
126
+ {
127
+ ...base,
128
+ label: 'Allow for session',
129
+ response: { kind: 'approval', request_id: request.id, decision: 'allow_for_session' },
130
+ },
131
+ { ...base, label: 'Deny', response: { kind: 'approval', request_id: request.id, decision: 'deny' } },
132
+ ];
133
+ // A runtime may state which decisions its own harness protocol can carry
134
+ // (`hosted_answerable_decisions` in crates/harness/src/runtime/hosted.rs —
135
+ // Claude Code's permission-prompt-tool has allow and deny and no
136
+ // always-allow BEHAVIOR at all). Offer exactly those rather than a button
137
+ // the runtime would refuse.
138
+ const accepted = acceptedDecisions(request);
139
+ if (accepted) {
140
+ return all.filter((answer) => answer.response.kind === 'approval' && accepted.includes(answer.response.decision));
141
+ }
142
+ return all;
143
+ }
144
+ const kind = request.kind === 'elicitation' ? 'elicitation' : 'other';
145
+ const needsContent = requestNeedsTypedReply(request);
146
+ return [
147
+ {
148
+ ...base,
149
+ label: needsContent ? 'Answer…' : 'Accept',
150
+ response: { kind, request_id: request.id, action: 'accept' },
151
+ promptForContent: needsContent,
152
+ },
153
+ { ...base, label: 'Decline', response: { kind, request_id: request.id, action: 'decline' } },
154
+ { ...base, label: 'Cancel', response: { kind, request_id: request.id, action: 'cancel' } },
155
+ ];
156
+ }
157
+ /**
158
+ * The typed-reply path, for a request that is still pending when the turn is
159
+ * NOT running: the person types the answer as the next message.
160
+ */
161
+ export function typedAnswer(request, reply) {
162
+ const normalized = reply.trim().toLowerCase();
163
+ if (request.kind === 'approval') {
164
+ if (['allow for session', 'allow_for_session', 'session'].includes(normalized)) {
165
+ return { kind: 'approval', request_id: request.id, decision: 'allow_for_session' };
166
+ }
167
+ if (['deny', 'no', 'reject'].includes(normalized)) {
168
+ return { kind: 'approval', request_id: request.id, decision: 'deny' };
169
+ }
170
+ return { kind: 'approval', request_id: request.id, decision: 'allow' };
171
+ }
172
+ const kind = request.kind === 'elicitation' ? 'elicitation' : 'other';
173
+ if (['cancel', 'stop'].includes(normalized))
174
+ return { kind, request_id: request.id, action: 'cancel' };
175
+ if (['decline', 'no'].includes(normalized))
176
+ return { kind, request_id: request.id, action: 'decline' };
177
+ return { kind, request_id: request.id, action: 'accept', content: reply };
178
+ }
179
+ /** The decisions a runtime said its harness accepts, when it said so. */
180
+ export function acceptedDecisions(request) {
181
+ const payload = request.payload;
182
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload))
183
+ return undefined;
184
+ const declared = payload.decisions;
185
+ if (!Array.isArray(declared))
186
+ return undefined;
187
+ const decisions = declared.filter((entry) => typeof entry === 'string');
188
+ return decisions.length > 0 ? decisions : undefined;
189
+ }
190
+ /** True when this request asks for content a button click cannot carry. */
191
+ export function requestNeedsTypedReply(request) {
192
+ if (request.kind === 'approval')
193
+ return false;
194
+ const payload = request.payload;
195
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload))
196
+ return false;
197
+ const schema = payload.requested_schema;
198
+ return schema !== undefined && schema !== null;
199
+ }
200
+ /**
201
+ * The resolution of a request as a sentence, built from the response's own
202
+ * fields and the request it answers — never the raw JSON, which says the same
203
+ * thing in a form only this protocol's authors can read.
204
+ */
205
+ export function resolutionSentence(response, request, requestId) {
206
+ const about = request ? requestSubject(request) : undefined;
207
+ const suffix = about ? ` ${about}` : ` request #${requestId}`;
208
+ if (!response)
209
+ return `Resolved${suffix}`;
210
+ if (response.kind === 'approval') {
211
+ const verb = { allow: 'Allowed', allow_for_session: 'Allowed for session', deny: 'Denied' }[response.decision];
212
+ return `${verb ?? 'Resolved'}${suffix}`;
213
+ }
214
+ const verb = { accept: 'Accepted', decline: 'Declined', cancel: 'Cancelled' }[response.action];
215
+ const content = typeof response.content === 'string' && response.content.length > 0
216
+ ? ` — ${clamp(stripControls(response.content), 160)}`
217
+ : '';
218
+ return `${verb ?? 'Resolved'}${suffix}${content}`;
219
+ }
220
+ /** `write_file on hello.txt`, read from the request's own payload. */
221
+ export function requestSubject(request) {
222
+ const payload = request.payload;
223
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload))
224
+ return undefined;
225
+ const fields = payload;
226
+ const tool = typeof fields.tool === 'string' ? fields.tool
227
+ : typeof fields.tool_name === 'string' ? fields.tool_name
228
+ : undefined;
229
+ const subject = typeof fields.subject === 'string' ? fields.subject
230
+ : typeof fields.raw_args === 'string' ? fields.raw_args
231
+ : typeof fields.arguments === 'object' && fields.arguments !== null && !Array.isArray(fields.arguments)
232
+ ? toolSubject(JSON.stringify(fields.arguments))
233
+ : undefined;
234
+ if (tool && subject)
235
+ return `${tool} on ${clamp(stripControls(subject), 120)}`;
236
+ if (tool)
237
+ return tool;
238
+ if (subject)
239
+ return clamp(stripControls(subject), 120);
240
+ return undefined;
241
+ }
242
+ /** Pending requests belong to the live runtime, even when saved transcript
243
+ * history predates them. Rehydrate their controls before waiting for new events. */
244
+ export function projectPendingRequests(requests, context) {
245
+ return Array.from(requests).flatMap(request => projectEvent({
246
+ sequence: 0, kind: 'request', payload: { type: 'request', request },
247
+ }, context));
248
+ }
249
+ /**
250
+ * The event to chat-response-part mapping. Pure: every input is the runtime's
251
+ * own sequenced event, every output describes a VS Code chat part. Each arm
252
+ * names the producer it reads.
253
+ */
254
+ export function projectEvent(event, context) {
255
+ const payload = payloadOf(event);
256
+ // Hosted Codex 0.4.38 labels terminal polling/output notifications as tool
257
+ // starts with null ids/names. The actual command item carries its identity,
258
+ // command and final output; render that item rather than anonymous tool rows.
259
+ const raw = payload.raw && typeof payload.raw === 'object' && !Array.isArray(payload.raw) ? payload.raw : {};
260
+ if (event.kind === 'tool_call_started' && !payload.id && !payload.name &&
261
+ ['item/commandExecution/terminalInteraction', 'item/commandExecution/outputDelta'].includes(text(raw.method)))
262
+ return [];
263
+ // Hosted harnesses retain transport metadata in replay. Known lifecycle
264
+ // notices are not assistant prose; unknown events still use the bounded fallback.
265
+ if (event.kind === 'native_event') {
266
+ const object = (v) => v && typeof v === 'object' && !Array.isArray(v) ? v : {};
267
+ const raw = object(payload.raw);
268
+ const params = object(raw.params);
269
+ const kind = text(payload.kind).toLowerCase();
270
+ const item = object(params.item);
271
+ if (['item/started', 'item/completed'].includes(kind) && item.type === 'commandExecution' && typeof item.id === 'string') {
272
+ return projectEvent({ ...event, kind: kind === 'item/started' ? 'tool_call_started' : 'tool_call_completed', payload: {
273
+ id: item.id, name: 'Shell', arguments: JSON.stringify({ command: item.command, cwd: item.cwd }),
274
+ output: item.aggregatedOutput ?? '', is_error: typeof item.exitCode === 'number' && item.exitCode !== 0,
275
+ } }, context);
276
+ }
277
+ if (['turn/started', 'thread/started', 'thread/status/changed', 'thread/tokenusage/updated', 'account/ratelimits/updated', 'remotecontrol/status/changed', 'thread/goal/cleared'].includes(kind))
278
+ return [];
279
+ if (['item/started', 'item/completed'].includes(kind) && ['userMessage', 'agentMessage', 'reasoning'].includes(text(object(params.item).type)))
280
+ return [];
281
+ if (kind === 'mcpserver/startupstatus/updated') {
282
+ if (params.error)
283
+ return [stable({ part: 'warning', value: text(params.error) })];
284
+ if (['starting', 'ready'].includes(text(params.status)))
285
+ return [];
286
+ }
287
+ if (kind === 'deprecationnotice')
288
+ return [stable({ part: 'progress', value: text(params.summary, 'Harness deprecation notice') })];
289
+ if (kind === 'system') {
290
+ if (['init', 'hook_started', 'thinking_tokens'].includes(text(raw.subtype)))
291
+ return [];
292
+ if (raw.subtype === 'hook_response' && raw.exit_code === 0)
293
+ return [];
294
+ }
295
+ }
296
+ switch (event.kind) {
297
+ // crates/harness/src/server.rs `submit_claimed` — the runtime's echo of the
298
+ // prompt. VS Code already renders the request turn, so the live view drops
299
+ // its own echo; only the session-history projection reads it.
300
+ case 'user_message':
301
+ return context.echoOwnPrompts
302
+ ? [stable({ part: 'markdown', value: clamp(stripControls(text(payload.text)), VSCODE_CHAT_LIMITS.partCharacters) })]
303
+ : [];
304
+ // crates/harness/src/server.rs `submit_claimed`, and
305
+ // crates/runtime/src/event.rs `AgentEvent::TurnCompleted` (one model
306
+ // round-trip). Lifecycle only — the turn resolves on these, never renders.
307
+ case 'turn_started':
308
+ case 'turn_completed':
309
+ case 'turn_succeeded':
310
+ return [];
311
+ // crates/runtime/src/event.rs `AgentEvent::TextDelta`.
312
+ case 'text_delta':
313
+ return [stable({ part: 'markdown', value: text(payload.text) })];
314
+ // crates/runtime/src/event.rs `AgentEvent::ToolCallStarted`.
315
+ case 'tool_call_started': {
316
+ const toolName = text(payload.name, 'tool');
317
+ const toolCallId = text(payload.id, `event-${event.sequence}`);
318
+ const rawArguments = text(payload.arguments);
319
+ const inputText = clamp(stripControls(parseArguments(rawArguments)), VSCODE_CHAT_LIMITS.partCharacters);
320
+ const subject = toolSubject(rawArguments);
321
+ return [{
322
+ emit: {
323
+ part: 'tool',
324
+ toolName,
325
+ toolCallId,
326
+ invocationMessage: clamp(subject ? `${toolName} · ${subject}` : toolName, VSCODE_CHAT_LIMITS.titleCharacters),
327
+ inputText,
328
+ isComplete: false,
329
+ },
330
+ needs: 'chatParticipantAdditions',
331
+ fallback: [{
332
+ part: 'progress',
333
+ value: clamp(subject ? `${toolName} · ${subject}` : toolName, VSCODE_CHAT_LIMITS.titleCharacters),
334
+ }],
335
+ }];
336
+ }
337
+ // crates/runtime/src/event.rs `AgentEvent::ToolCallCompleted`.
338
+ case 'tool_call_completed': {
339
+ // A harness may name the tool only when the call STARTS — Claude Code's
340
+ // `tool_result` block carries just the `tool_use_id` it answers — so an
341
+ // empty name here means "same row", not a tool called `tool`.
342
+ const toolName = text(payload.name, '');
343
+ const toolCallId = text(payload.id, `event-${event.sequence}`);
344
+ const output = clamp(stripControls(text(payload.output)), VSCODE_CHAT_LIMITS.partCharacters);
345
+ const isError = payload.is_error === true;
346
+ return [{
347
+ emit: {
348
+ part: 'tool',
349
+ toolName,
350
+ toolCallId,
351
+ invocationMessage: clamp(toolName, VSCODE_CHAT_LIMITS.titleCharacters),
352
+ pastTenseMessage: clamp(toolName, VSCODE_CHAT_LIMITS.titleCharacters),
353
+ isError,
354
+ isComplete: true,
355
+ resultText: output,
356
+ },
357
+ needs: 'chatParticipantAdditions',
358
+ fallback: [{
359
+ part: 'markdown',
360
+ value: `**${isError ? 'Tool failed' : 'Tool result'}${toolName ? ` · ${toolName}` : ''}**\n\n${fence(output)}`,
361
+ }],
362
+ }];
363
+ }
364
+ // crates/codex-frontend/src/codex_app_server_v0_144.rs maps a stock Codex
365
+ // `turn/diff/updated` onto this kind. MEASURED: its payload carries ONE
366
+ // unified-diff string (`payload.diff`) and nothing else — no per-file
367
+ // edits, no resources, no applier. So none of the editor's richer edit
368
+ // parts can be built from it without inventing data:
369
+ // ChatResponseTextEditPart — wants TextEdit ranges, and the runtime has
370
+ // ALREADY written the file, so replaying them would double-apply.
371
+ // ChatResponseMultiDiffPart — wants an originalUri and a modifiedUri per
372
+ // file, i.e. two resources holding before/after content this frontend
373
+ // would have to fabricate and serve.
374
+ // ChatResponseExternalEditPart — brackets a callback that PERFORMS the
375
+ // edit so the editor can take a before snapshot; ours would be a no-op
376
+ // after the write, making the editor's undo stop wrong.
377
+ // What the payload does state is which files it touched, so this renders
378
+ // the diff as a `codeblockUri`-bound diff block plus one stable reference
379
+ // per named file.
380
+ case 'diff_updated': {
381
+ const diff = clamp(stripControls(text(payload.diff)), VSCODE_CHAT_LIMITS.partCharacters);
382
+ const block = fence(diff, 'diff');
383
+ return [
384
+ {
385
+ emit: { part: 'codeblock', uri: 'supercode-diff:/turn.diff', value: block, language: 'diff' },
386
+ needs: 'chatParticipantAdditions',
387
+ fallback: [{ part: 'markdown', value: block }],
388
+ },
389
+ ...diffPaths(diff).map((file) => stable({ part: 'reference', path: file })),
390
+ ];
391
+ }
392
+ // crates/harness/src/server.rs `FrontendRequestBroker::publish`.
393
+ case 'request': {
394
+ const request = (payload.request ?? payload);
395
+ if (!request || typeof request.id !== 'number') {
396
+ return [stable({
397
+ part: 'markdown',
398
+ value: `**supercode** · malformed request event\n\n${fence(opaqueText(event.payload), 'json')}`,
399
+ })];
400
+ }
401
+ const summary = requestSummary(request);
402
+ const answers = requestAnswers(request, context.connectionId);
403
+ const subject = requestSubject(request);
404
+ const title = subject
405
+ ? `Approve ${subject}`
406
+ : `${request.kind === 'approval' ? 'Approval' : 'Assistant'} request #${request.id}`;
407
+ // Command buttons, and MEASURED rather than assumed. Two richer
408
+ // mechanisms were tried against a live runtime in the editor's own Chat
409
+ // view and both failed, so neither ships:
410
+ // `ChatResponseConfirmationPart` retires its own buttons on click, but
411
+ // the click answers by sending a NEW chat request, and a supercode
412
+ // approval is raised while the turn still runs (`decide_approval`
413
+ // blocks the agent loop) — the editor refuses that request, so the
414
+ // click does nothing and the runtime waits forever.
415
+ // `stream.questionCarousel` resolves inside the running turn and the
416
+ // widget retires itself — the right shape on paper. Observed: the
417
+ // widget renders with its options, Submit retires it, and the
418
+ // promise never delivers an answer this frontend can read, so the
419
+ // runtime is left blocked. A preferred path that silently swallows
420
+ // the answer is worse than a plain one that works.
421
+ // A command button runs inside the live turn and carries its own answer,
422
+ // so all three approval decisions are offered. It cannot be withdrawn
423
+ // once pushed (`chatCommandContentPart` enables on `!isStale` and has no
424
+ // used-state), so `supercode.frontend.respond` refuses a request that is
425
+ // no longer pending and SAYS so instead of sending a stale answer.
426
+ if (context.canRespond === false) {
427
+ return [stable({
428
+ part: 'markdown',
429
+ value: `\n\n**${title}**\n\n${summary}\n\n_This runtime answers its own requests; Volter Harness is observing._\n\n`,
430
+ })];
431
+ }
432
+ return [stable({
433
+ part: 'answers',
434
+ title: clamp(title, VSCODE_CHAT_LIMITS.titleCharacters),
435
+ message: summary,
436
+ answers,
437
+ })];
438
+ }
439
+ // crates/harness/src/server.rs `FrontendRequestBroker::respond`.
440
+ case 'request_resolved': {
441
+ const requestId = typeof payload.request_id === 'number' ? payload.request_id : Number.NaN;
442
+ const response = payload.response;
443
+ const sentence = resolutionSentence(response, context.pendingRequest?.(requestId), requestId);
444
+ return [stable({ part: 'markdown', value: `\n\n_${sentence}_\n\n` })];
445
+ }
446
+ // crates/runtime/src/event.rs `AgentEvent::CacheWarning`.
447
+ case 'cache_warning': {
448
+ const message = clamp(stripControls(text(payload.message)), VSCODE_CHAT_LIMITS.partCharacters);
449
+ return [{
450
+ emit: { part: 'warning', value: message },
451
+ needs: 'chatParticipantAdditions',
452
+ fallback: [{ part: 'markdown', value: `> ⚠ ${message}` }],
453
+ }];
454
+ }
455
+ // crates/runtime/src/event.rs `AgentEvent::Usage`.
456
+ case 'usage':
457
+ return [stable({
458
+ part: 'progress',
459
+ value: `prompt ${text(payload.prompt_tokens, '?')} · completion ${text(payload.completion_tokens, '?')} · total ${text(payload.total_tokens, '?')} tokens`,
460
+ })];
461
+ // crates/runtime/src/event.rs `AgentEvent::BackgroundOutput`.
462
+ case 'background_output': {
463
+ const chunk = fence(clamp(stripControls(text(payload.chunk)), VSCODE_CHAT_LIMITS.partCharacters));
464
+ const truncated = payload.truncated === true ? '\n\n_the runtime stopped retaining this job’s output_' : '';
465
+ return [stable({
466
+ part: 'markdown',
467
+ value: `\n\n**background job ${text(payload.job_id)}**\n\n${chunk}${truncated}\n\n`,
468
+ })];
469
+ }
470
+ // crates/harness/src/server.rs `submit_claimed`.
471
+ case 'turn_interrupted':
472
+ return [stable({ part: 'markdown', value: '\n\n_turn interrupted_\n\n' })];
473
+ // crates/harness/src/server.rs `submit_claimed` emits `turn_failed`. The
474
+ // other four are the error kinds the donor-neutral reader in
475
+ // crates/frontend-model/src/transcript.rs treats identically:
476
+ // `transport_error` from crates/opencode-frontend/src/server.rs,
477
+ // `watch_error` from crates/interchange/src/watch.rs, and
478
+ // `runtime_disconnected`, which a frontend synthesizes for itself
479
+ // (crates/frontend-tui/src/app.rs). `stream_error` has no producer in this
480
+ // repository and is read only because that reader reads it.
481
+ case 'turn_failed':
482
+ case 'stream_error':
483
+ case 'transport_error':
484
+ case 'watch_error':
485
+ case 'runtime_disconnected': {
486
+ const message = typeof payload.message === 'string' ? payload.message : opaqueText(event.payload);
487
+ const value = `${event.kind.replaceAll('_', ' ')}: ${clamp(stripControls(message), VSCODE_CHAT_LIMITS.partCharacters)}`;
488
+ return [{
489
+ emit: { part: 'warning', value },
490
+ needs: 'chatParticipantAdditions',
491
+ fallback: [{ part: 'markdown', value: `> ⚠ ${value}` }],
492
+ }];
493
+ }
494
+ // Declared by the runtime descriptor's `display.event_kinds`
495
+ // (crates/harness/src/server.rs `frontend_descriptor`); read here the way
496
+ // crates/frontend-model/src/transcript.rs reads them.
497
+ case 'scheduled_prompt_started':
498
+ case 'scheduled_prompt_deferred':
499
+ case 'scheduled_prompt_completed':
500
+ return [stable({
501
+ part: 'progress',
502
+ value: `${event.kind.replaceAll('_', ' ')} · ${opaqueText(event.payload, 256)}`,
503
+ })];
504
+ case 'scheduler_error': {
505
+ const value = `scheduler error: ${opaqueText(event.payload, 256)}`;
506
+ return [{
507
+ emit: { part: 'warning', value },
508
+ needs: 'chatParticipantAdditions',
509
+ fallback: [{ part: 'markdown', value: `> ⚠ ${value}` }],
510
+ }];
511
+ }
512
+ // crates/harness/src/runtime/hosted.rs `native_payload`, declared by the
513
+ // hosted descriptor: a harness's own event that has no canonical
514
+ // projection (pi's `agent_start`, `message_update`'s `text_start`, …).
515
+ // What the turn says arrives beside it as `text_delta`/`tool_call_*`, so
516
+ // it is a transient status line naming the harness's kind, never a JSON
517
+ // block in the transcript.
518
+ case 'native_event':
519
+ return [stable({
520
+ part: 'progress',
521
+ value: clamp(stripControls(text(payload.kind, 'native event')), VSCODE_CHAT_LIMITS.titleCharacters),
522
+ })];
523
+ // Not in this runtime's declared set; read the way
524
+ // crates/frontend-model/src/transcript.rs reads a donor that emits them.
525
+ case 'reasoning':
526
+ case 'thinking': {
527
+ const body = clamp(stripControls(text(payload.text ?? payload.summary, opaqueText(event.payload))), VSCODE_CHAT_LIMITS.partCharacters);
528
+ return [{
529
+ emit: { part: 'thinking', value: body },
530
+ needs: 'chatParticipantAdditions',
531
+ fallback: [{ part: 'markdown', value: `\n\n> ${body.replaceAll('\n', '\n> ')}\n\n` }],
532
+ }];
533
+ }
534
+ // The contract says every kind but `request` is opaque JSON by design, so a
535
+ // kind this version does not know is rendered bounded, never dropped.
536
+ default:
537
+ // The surrounding blank lines are load-bearing: a fenced block that the
538
+ // next streamed delta lands against on the same line never closes, and
539
+ // it swallows everything after it — observed doing exactly that to an
540
+ // approval request.
541
+ return [stable({
542
+ part: 'markdown',
543
+ value: `\n\n_supercode event \`${clamp(event.kind, VSCODE_CHAT_LIMITS.titleCharacters)}\` is not displayable by this frontend version_\n\n${fence(opaqueText(event.payload), 'json')}\n\n`,
544
+ })];
545
+ }
546
+ }
547
+ /** Replace every part whose proposal the host did not grant with its fallback. */
548
+ export function resolveParts(parts, granted) {
549
+ const resolved = [];
550
+ for (const part of parts) {
551
+ if (!part.needs || granted.has(part.needs))
552
+ resolved.push(part.emit);
553
+ else
554
+ resolved.push(...(part.fallback ?? []));
555
+ }
556
+ return resolved;
557
+ }
558
+ /**
559
+ * The prompt of the marker turn that carries runtime history with no user turn
560
+ * of its own. It names itself; nothing here is presented as a person's words.
561
+ */
562
+ export const HISTORY_MARKER_PROMPT = '⟨supercode runtime history⟩';
563
+ function messageText(message) {
564
+ const content = message.content;
565
+ if (typeof content === 'string')
566
+ return content;
567
+ if (!Array.isArray(content))
568
+ return '';
569
+ return content
570
+ .map((entry) => {
571
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry))
572
+ return '';
573
+ const part = entry;
574
+ if (['text', 'input_text', 'output_text'].includes(String(part.type)))
575
+ return String(part.text ?? '');
576
+ if (['image', 'image_url', 'input_image'].includes(String(part.type)))
577
+ return '[image]';
578
+ return '';
579
+ })
580
+ .filter(Boolean)
581
+ .join('\n');
582
+ }
583
+ /**
584
+ * A HOSTED harness runtime's transcript as chat turns.
585
+ *
586
+ * `HostedHarnessRuntime::attach` returns `history: Vec::new()` and puts the
587
+ * whole conversation in `replay` — the canonical ChatMessage history belongs to
588
+ * supercode's own agent loop, and a hosted harness has none. So a resumed
589
+ * Claude Code session's turns are here or nowhere.
590
+ */
591
+ export function projectReplayTurns(events, granted, context) {
592
+ const turns = [];
593
+ let streamedText;
594
+ for (const event of events) {
595
+ if (event.kind === 'user_message') {
596
+ streamedText = undefined;
597
+ const payload = payloadOf(event);
598
+ turns.push({ role: 'user', prompt: clamp(stripControls(text(payload.text)), VSCODE_CHAT_LIMITS.partCharacters) });
599
+ continue;
600
+ }
601
+ const parts = resolveParts(projectEvent(event, context), granted);
602
+ if (parts.length === 0)
603
+ continue;
604
+ if (event.kind === 'text_delta' && parts[0]?.part === 'markdown') {
605
+ if (streamedText) {
606
+ streamedText.value = clamp(streamedText.value + parts[0].value, VSCODE_CHAT_LIMITS.partCharacters);
607
+ continue;
608
+ }
609
+ streamedText = parts[0];
610
+ }
611
+ else {
612
+ streamedText = undefined;
613
+ }
614
+ if (turns.length === 0)
615
+ turns.push({ role: 'user', prompt: HISTORY_MARKER_PROMPT });
616
+ const last = turns[turns.length - 1];
617
+ if (last.role === 'assistant' && last.parts)
618
+ last.parts.push(...parts);
619
+ else
620
+ turns.push({ role: 'assistant', parts });
621
+ }
622
+ if (turns.length > VSCODE_CHAT_LIMITS.historyTurns) {
623
+ const omitted = turns.length - VSCODE_CHAT_LIMITS.historyTurns;
624
+ const kept = turns.slice(-VSCODE_CHAT_LIMITS.historyTurns);
625
+ kept.unshift({
626
+ role: 'assistant',
627
+ parts: [{
628
+ part: 'markdown',
629
+ value: `_${omitted} earlier turns are omitted by this view’s bound; the runtime’s own history is unchanged and authoritative._`,
630
+ }],
631
+ });
632
+ return kept;
633
+ }
634
+ return turns;
635
+ }
636
+ /**
637
+ * The attach snapshot's canonical history as chat turns. Bounded: when older
638
+ * turns are omitted the view says so, the way the browser observer does.
639
+ */
640
+ export function projectHistory(history, granted, pendingToolNames = new Set()) {
641
+ const turns = [];
642
+ const pushAssistant = (parts) => {
643
+ const last = turns[turns.length - 1];
644
+ if (last && last.role === 'assistant' && last.parts) {
645
+ last.parts.push(...parts);
646
+ return;
647
+ }
648
+ // VS Code's chat history is request/response PAIRS and drops a response
649
+ // with no request before it. A runtime transcript legitimately opens with
650
+ // assistant or system content — the runtime compacts, so
651
+ // `bounded_history_snapshot` can begin `[earlier conversation compacted:
652
+ // N messages]` with no user turn left. Rather than drop it, this pairs it
653
+ // with a marker turn that names itself as the runtime's history, so
654
+ // nothing is invented as a person's prompt.
655
+ if (turns.length === 0)
656
+ turns.push({ role: 'user', prompt: HISTORY_MARKER_PROMPT });
657
+ turns.push({ role: 'assistant', parts });
658
+ };
659
+ for (const message of history) {
660
+ const body = messageText(message);
661
+ if (message.role === 'user') {
662
+ turns.push({ role: 'user', prompt: clamp(stripControls(body), VSCODE_CHAT_LIMITS.partCharacters) });
663
+ continue;
664
+ }
665
+ // A person never sees a system prompt — not in a chat product, not in
666
+ // Blender's own panels. The runtime's canonical history carries it because
667
+ // the MODEL needs it; a reader does not, and a collapsed block is still a
668
+ // reader's problem. It renders NOTHING, deliberately, and the transcript
669
+ // starts where the conversation does.
670
+ if (message.role === 'system')
671
+ continue;
672
+ if (message.role === 'assistant') {
673
+ if (body) {
674
+ pushAssistant([{ part: 'markdown', value: clamp(stripControls(body), VSCODE_CHAT_LIMITS.partCharacters) }]);
675
+ }
676
+ for (const call of message.tool_calls ?? []) {
677
+ pushAssistant(resolveParts([{
678
+ emit: {
679
+ part: 'tool',
680
+ toolName: call.function.name,
681
+ toolCallId: call.id,
682
+ invocationMessage: clamp(call.function.name, VSCODE_CHAT_LIMITS.titleCharacters),
683
+ isComplete: true,
684
+ },
685
+ needs: 'chatParticipantAdditions',
686
+ fallback: [{
687
+ part: 'markdown',
688
+ value: `**Tool call · ${call.function.name}**\n\n${fence(parseArguments(call.function.arguments))}`,
689
+ }],
690
+ }], granted));
691
+ }
692
+ continue;
693
+ }
694
+ // The SDK loader inserts this synthetic result for an unfinished native
695
+ // call. A currently pending approval is authoritative; do not call it an
696
+ // interruption. Only the final matching tool is affected, never old history.
697
+ const pending = message === history.at(-1) && pendingToolNames.has(message.name ?? '')
698
+ && body === '[no tool result recorded — turn interrupted]';
699
+ pushAssistant([{
700
+ part: 'markdown',
701
+ value: pending ? '**Awaiting approval** — no tool result yet.'
702
+ : `**Tool result${message.name ? ` · ${message.name}` : ''}**\n\n${fence(clamp(stripControls(body), VSCODE_CHAT_LIMITS.partCharacters))}`,
703
+ }]);
704
+ }
705
+ if (turns.length <= VSCODE_CHAT_LIMITS.historyTurns)
706
+ return turns;
707
+ const omitted = turns.length - VSCODE_CHAT_LIMITS.historyTurns;
708
+ const kept = turns.slice(-VSCODE_CHAT_LIMITS.historyTurns);
709
+ kept.unshift({
710
+ role: 'assistant',
711
+ parts: [{
712
+ part: 'markdown',
713
+ value: `_${omitted} earlier turns are omitted by this view’s bound; the runtime’s own history is unchanged and authoritative._`,
714
+ }],
715
+ });
716
+ return kept;
717
+ }