@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.
- package/NOTICE.md +19 -0
- package/README.md +293 -0
- package/dist/bindings.d.ts +7 -0
- package/dist/bindings.js +5 -0
- package/dist/client.d.ts +11 -0
- package/dist/client.js +5 -0
- package/dist/context.d.ts +6 -0
- package/dist/context.js +14 -0
- package/dist/credential.d.ts +17 -0
- package/dist/credential.js +53 -0
- package/dist/extension.d.ts +39 -0
- package/dist/extension.js +1131 -0
- package/dist/host-controls.d.ts +58 -0
- package/dist/host-controls.js +66 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +5 -0
- package/dist/native-session.d.ts +6 -0
- package/dist/native-session.js +41 -0
- package/dist/proposals.d.ts +24 -0
- package/dist/proposals.js +54 -0
- package/dist/session.d.ts +154 -0
- package/dist/session.js +431 -0
- package/dist/view.d.ts +176 -0
- package/dist/view.js +717 -0
- package/media/anthropic.svg +1 -0
- package/media/google.svg +1 -0
- package/media/icon.png +0 -0
- package/media/openai.svg +1 -0
- package/package.json +200 -0
- package/scripts/pack-extension.mjs +63 -0
- package/vendor/supercode-frontend/README.md +22 -0
- package/vendor/supercode-frontend/client.d.mts +106 -0
- package/vendor/supercode-frontend/client.mjs +300 -0
- package/vendor/supercode-frontend/contract.json +289 -0
- package/vendor/supercode-frontend/generated-client.d.mts +18 -0
- package/vendor/supercode-frontend/generated-client.mjs +20 -0
- package/vendor/supercode-frontend/generated.d.mts +146 -0
- package/vendor/supercode-frontend/generated.mjs +19 -0
- 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
|
+
}
|