@volter/supercode-frontend-vscode 0.1.61 → 0.1.63
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -6
- package/dist/extension.js +132 -23
- package/dist/view.d.ts +93 -9
- package/dist/view.js +352 -47
- package/package.json +2 -1
- package/vendor/supercode-interchange/user-provenance.d.mts +19 -0
- package/vendor/supercode-interchange/user-provenance.mjs +230 -0
package/dist/view.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { userProvenance } from '../vendor/supercode-interchange/user-provenance.mjs';
|
|
1
2
|
/**
|
|
2
3
|
* The bounded projection this presentation package renders. Mirrors
|
|
3
4
|
* `@volter/supercode-frontend-browser`'s rule: the view is size-bounded
|
|
@@ -173,6 +174,26 @@ export function toolTitles(toolName, argumentsText) {
|
|
|
173
174
|
}
|
|
174
175
|
if (name === 'todowrite' || name === 'update_plan')
|
|
175
176
|
return both('Updating the plan', 'Updated the plan');
|
|
177
|
+
if (name === 'taskcreate') {
|
|
178
|
+
const subject = field('subject');
|
|
179
|
+
if (subject)
|
|
180
|
+
return both(`Adding a task: ${plain(subject)}`, `Added a task: ${plain(subject)}`);
|
|
181
|
+
}
|
|
182
|
+
if (name === 'taskupdate') {
|
|
183
|
+
const id = field('taskId'), status = field('status');
|
|
184
|
+
if (id) {
|
|
185
|
+
const task = `task #${plain(id)}`;
|
|
186
|
+
if (status === 'in_progress')
|
|
187
|
+
return both(`Starting ${task}`, `Started ${task}`);
|
|
188
|
+
if (status === 'completed')
|
|
189
|
+
return both(`Completing ${task}`, `Completed ${task}`);
|
|
190
|
+
if (status === 'deleted')
|
|
191
|
+
return both(`Removing ${task}`, `Removed ${task}`);
|
|
192
|
+
return both(`Updating ${task}`, `Updated ${task}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (name === TASK_LIST_TOOL)
|
|
196
|
+
return both('Reading the task list', 'Read the task list');
|
|
176
197
|
// Claude Code names an MCP tool `mcp__<server>__<tool>`; VS Code writes "Ran `tool` – server".
|
|
177
198
|
const mcp = /^mcp__(.+?)__(.+)$/.exec(toolName);
|
|
178
199
|
if (mcp)
|
|
@@ -180,6 +201,159 @@ export function toolTitles(toolName, argumentsText) {
|
|
|
180
201
|
const subject = toolSubject(argumentsText);
|
|
181
202
|
return subject ? both(`Running ${code(toolName)} · ${plain(subject)}`, `Ran ${code(toolName)} · ${plain(subject)}`) : both(`Running ${code(toolName)}`, `Ran ${code(toolName)}`);
|
|
182
203
|
}
|
|
204
|
+
/** A hook's row as words, as VS Code titles its own (`chatHookContentPart.ts`): "Blocked by …" or "Warning from …". */
|
|
205
|
+
export function hookSentence(hookEvent, blocked, reason) {
|
|
206
|
+
return `${blocked ? 'Blocked by' : 'Warning from'} the ${hookEvent} hook${reason ? `: ${reason}` : ''}`;
|
|
207
|
+
}
|
|
208
|
+
/** The hook events VS Code's own Chat names (`ChatHookType`), which are Claude Code's. */
|
|
209
|
+
export const HOOK_TYPES = ['SessionStart', 'SessionEnd', 'UserPromptSubmit', 'PreToolUse', 'PostToolUse', 'PreCompact',
|
|
210
|
+
'SubagentStart', 'SubagentStop', 'Stop', 'ErrorOccurred'];
|
|
211
|
+
/** Claude Code's TaskList tool, lower-cased as this view compares tool names. */
|
|
212
|
+
const TASK_LIST_TOOL = 'tasklist'; // process-read: a Claude Code tool name, not the Windows command
|
|
213
|
+
/** A plan call's items (Claude Code's TodoWrite `todos` of `content`; the `update_plan` tool's `plan` of `step`, as
|
|
214
|
+
* supercode's own and Codex's take it), each its text and status, read from its own arguments; undefined for any
|
|
215
|
+
* other tool, or arguments holding none. Both carry the whole list, so they need no state. */
|
|
216
|
+
export function planItems(toolName, argumentsText) {
|
|
217
|
+
const name = toolName.toLowerCase();
|
|
218
|
+
if (name !== 'todowrite' && name !== 'update_plan')
|
|
219
|
+
return undefined;
|
|
220
|
+
const list = argumentFields(argumentsText)[name === 'todowrite' ? 'todos' : 'plan'];
|
|
221
|
+
if (!Array.isArray(list))
|
|
222
|
+
return undefined;
|
|
223
|
+
const items = list.flatMap((item) => {
|
|
224
|
+
if (!item || typeof item !== 'object' || Array.isArray(item))
|
|
225
|
+
return [];
|
|
226
|
+
const fields = item;
|
|
227
|
+
const title = planTitle(name === 'todowrite' ? fields.content : fields.step);
|
|
228
|
+
return title ? [{ title, status: planStatus(fields.status) }] : [];
|
|
229
|
+
});
|
|
230
|
+
return items.length > 0 ? items : undefined;
|
|
231
|
+
}
|
|
232
|
+
function argumentFields(argumentsText) {
|
|
233
|
+
try {
|
|
234
|
+
const parsed = JSON.parse(argumentsText);
|
|
235
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
|
|
236
|
+
return parsed;
|
|
237
|
+
}
|
|
238
|
+
catch { /* not JSON: no fields */ }
|
|
239
|
+
return {};
|
|
240
|
+
}
|
|
241
|
+
const planTitle = (value) => typeof value === 'string' ? clamp(stripControls(value.trim()), VSCODE_CHAT_LIMITS.titleCharacters) : '';
|
|
242
|
+
/** Claude Code's task statuses (`pending`, `in_progress`, `completed`) as VS Code's own todo statuses. */
|
|
243
|
+
const planStatus = (value) => value === 'completed' ? 'completed' : value === 'in_progress' ? 'in-progress' : 'not-started';
|
|
244
|
+
/**
|
|
245
|
+
* The task list Claude Code keeps with its task tools, which it offers in place of TodoWrite (TaskCreate, TaskUpdate,
|
|
246
|
+
* TaskList; Claude Code 2.1.312 enables them by default and TodoWrite only when its tasks are turned off). Each call
|
|
247
|
+
* changes ONE task, while VS Code's own todo list shows the WHOLE list, so this keeps the list for one conversation as
|
|
248
|
+
* the calls go by and gives each call's row the list as that call left it.
|
|
249
|
+
*
|
|
250
|
+
* Read from the calls' own arguments, and from Claude Code's own result text where only the result has it (measured
|
|
251
|
+
* on 2.1.312, its `mapToolResultToToolResultBlockParam`): TaskCreate's `Task #<id> created successfully: <subject>`,
|
|
252
|
+
* TaskUpdate's `Updated task #<id> <fields>`, and TaskList's lines `#<id> [<status>] <subject>…`. A result in other
|
|
253
|
+
* words changes nothing here; the row is then an ordinary one.
|
|
254
|
+
*/
|
|
255
|
+
export class PlanTracker {
|
|
256
|
+
#tasks = [];
|
|
257
|
+
#calls = new Map();
|
|
258
|
+
/** A call that started: the list as it leaves it, or undefined when it is not a task call (or changes nothing). */
|
|
259
|
+
started(toolName, argumentsText, toolCallId) {
|
|
260
|
+
const name = toolName.toLowerCase();
|
|
261
|
+
if (!['taskcreate', 'taskupdate', TASK_LIST_TOOL].includes(name))
|
|
262
|
+
return undefined;
|
|
263
|
+
const fields = argumentFields(argumentsText);
|
|
264
|
+
if (name === TASK_LIST_TOOL) {
|
|
265
|
+
this.#calls.set(toolCallId, { name });
|
|
266
|
+
return undefined;
|
|
267
|
+
}
|
|
268
|
+
if (name === 'taskcreate') {
|
|
269
|
+
const title = planTitle(fields.subject);
|
|
270
|
+
if (!title)
|
|
271
|
+
return undefined;
|
|
272
|
+
const created = { title, status: 'not-started' };
|
|
273
|
+
this.#tasks.push(created);
|
|
274
|
+
this.#calls.set(toolCallId, { name, created });
|
|
275
|
+
return this.items();
|
|
276
|
+
}
|
|
277
|
+
const id = typeof fields.taskId === 'string' ? fields.taskId : undefined;
|
|
278
|
+
const index = id === undefined ? -1 : this.#tasks.findIndex(task => task.id === id);
|
|
279
|
+
if (index < 0)
|
|
280
|
+
return undefined;
|
|
281
|
+
const task = this.#tasks[index];
|
|
282
|
+
const deleted = fields.status === 'deleted';
|
|
283
|
+
this.#calls.set(toolCallId, { name, before: { task: { ...task }, at: index, deleted } });
|
|
284
|
+
if (deleted)
|
|
285
|
+
this.#tasks.splice(index, 1);
|
|
286
|
+
else {
|
|
287
|
+
const title = planTitle(fields.subject);
|
|
288
|
+
// (changed in place, so a create still waiting for its id keeps its entry)
|
|
289
|
+
Object.assign(task, title ? { title } : {}, typeof fields.status === 'string' ? { status: planStatus(fields.status) } : {});
|
|
290
|
+
}
|
|
291
|
+
return this.items();
|
|
292
|
+
}
|
|
293
|
+
/** A call that completed: the list as it now stands (a failed call's change undone; TaskList's result is the
|
|
294
|
+
* list), so a finished row never shows an older list than the calls before it left. */
|
|
295
|
+
completed(toolCallId, output, isError) {
|
|
296
|
+
const call = this.#calls.get(toolCallId);
|
|
297
|
+
if (!call)
|
|
298
|
+
return undefined;
|
|
299
|
+
this.#calls.delete(toolCallId);
|
|
300
|
+
if (call.name === 'taskcreate' && call.created) {
|
|
301
|
+
const id = isError ? undefined : /^Task #(\S+) created successfully: /.exec(output)?.[1];
|
|
302
|
+
if (id !== undefined)
|
|
303
|
+
call.created.id = id;
|
|
304
|
+
// (a task Claude Code did not create is not on its list)
|
|
305
|
+
else
|
|
306
|
+
this.#tasks = this.#tasks.filter(task => task !== call.created);
|
|
307
|
+
return this.items();
|
|
308
|
+
}
|
|
309
|
+
if (call.name === 'taskupdate') {
|
|
310
|
+
const before = call.before;
|
|
311
|
+
if ((isError || !/^Updated task #/.test(output)) && before) {
|
|
312
|
+
if (before.deleted)
|
|
313
|
+
this.#tasks.splice(Math.min(before.at, this.#tasks.length), 0, before.task);
|
|
314
|
+
else {
|
|
315
|
+
const task = this.#tasks.find(other => other.id === before.task.id);
|
|
316
|
+
if (task)
|
|
317
|
+
Object.assign(task, { title: before.task.title, status: before.task.status });
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return this.items();
|
|
321
|
+
}
|
|
322
|
+
if (isError)
|
|
323
|
+
return undefined;
|
|
324
|
+
if (output.trim() === 'No tasks found') {
|
|
325
|
+
this.#tasks = [];
|
|
326
|
+
return [];
|
|
327
|
+
}
|
|
328
|
+
const listed = output.split('\n').flatMap(line => {
|
|
329
|
+
const match = /^#(\S+) \[(\w+)\] (.*?)(?: \[blocked by [^\]]*\])?$/.exec(line);
|
|
330
|
+
const title = match ? planTitle(match[3]) : '';
|
|
331
|
+
return match && title ? [{ id: match[1], title, status: planStatus(match[2]) }] : [];
|
|
332
|
+
});
|
|
333
|
+
if (listed.length === 0)
|
|
334
|
+
return undefined;
|
|
335
|
+
this.#tasks = listed;
|
|
336
|
+
return this.items();
|
|
337
|
+
}
|
|
338
|
+
items() {
|
|
339
|
+
return this.#tasks.map(({ title, status }) => ({ title, status }));
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
/** A subagent call's description, kind and prompt (Claude Code's Task and Agent tools: `description`,
|
|
343
|
+
* `subagent_type`, `prompt`), read from its own arguments; undefined for any other tool. */
|
|
344
|
+
export function subagentCall(toolName, argumentsText) {
|
|
345
|
+
if (!['task', 'agent'].includes(toolName.toLowerCase()))
|
|
346
|
+
return undefined;
|
|
347
|
+
const fields = argumentFields(argumentsText);
|
|
348
|
+
const field = (name, limit) => {
|
|
349
|
+
const value = fields[name];
|
|
350
|
+
return typeof value === 'string' && value.trim() ? clamp(stripControls(value.trim()), limit) : undefined;
|
|
351
|
+
};
|
|
352
|
+
const description = field('description', VSCODE_CHAT_LIMITS.titleCharacters);
|
|
353
|
+
const agentName = field('subagent_type', VSCODE_CHAT_LIMITS.titleCharacters);
|
|
354
|
+
const prompt = field('prompt', VSCODE_CHAT_LIMITS.partCharacters);
|
|
355
|
+
return { ...(description ? { description } : {}), ...(agentName ? { agentName } : {}), ...(prompt ? { prompt } : {}) };
|
|
356
|
+
}
|
|
183
357
|
/** The command line of a shell tool call, read from its own arguments; undefined for any other tool. */
|
|
184
358
|
export function shellCommand(toolName, argumentsText) {
|
|
185
359
|
if (!['bash', 'shell', 'exec_command', 'local_shell', 'powershell'].includes(toolName.toLowerCase()))
|
|
@@ -365,6 +539,52 @@ export function acceptedDecisions(request) {
|
|
|
365
539
|
const decisions = declared.filter((entry) => typeof entry === 'string');
|
|
366
540
|
return decisions.length > 0 ? decisions : undefined;
|
|
367
541
|
}
|
|
542
|
+
/**
|
|
543
|
+
* An elicitation that asks questions, the shape supercode's `ask_user` asks with and a hosted Claude Code's
|
|
544
|
+
* AskUserQuestion is raised as (`question-tool.mjs` `questionElicitation`): every property of its
|
|
545
|
+
* `requested_schema` is one question, with a `title`, the question alone (`x-question`) and its `x-options`
|
|
546
|
+
* (`[{label, description}]`, with `x-multi-select`; none for free text). Any other form (an MCP server's schema with
|
|
547
|
+
* its own `enum`, `format` or `required`) is undefined here and keeps the `Answer…` door, which honours them.
|
|
548
|
+
*/
|
|
549
|
+
export function requestQuestions(request) {
|
|
550
|
+
if (request.kind !== 'elicitation')
|
|
551
|
+
return undefined;
|
|
552
|
+
const payload = request.payload;
|
|
553
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload))
|
|
554
|
+
return undefined;
|
|
555
|
+
const schema = payload.requested_schema;
|
|
556
|
+
if (!schema || typeof schema !== 'object' || Array.isArray(schema))
|
|
557
|
+
return undefined;
|
|
558
|
+
const properties = schema.properties;
|
|
559
|
+
if (!properties || typeof properties !== 'object' || Array.isArray(properties))
|
|
560
|
+
return undefined;
|
|
561
|
+
const fields = [];
|
|
562
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
563
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
564
|
+
return undefined;
|
|
565
|
+
const property = value;
|
|
566
|
+
const title = typeof property.title === 'string' ? clamp(stripControls(property.title.trim()), VSCODE_CHAT_LIMITS.titleCharacters) : '';
|
|
567
|
+
if (!title)
|
|
568
|
+
return undefined;
|
|
569
|
+
const listed = property['x-options'];
|
|
570
|
+
if (typeof property['x-question'] !== 'string' || !Array.isArray(listed))
|
|
571
|
+
return undefined;
|
|
572
|
+
const options = listed.flatMap(option => {
|
|
573
|
+
if (!option || typeof option !== 'object' || Array.isArray(option) || typeof option.label !== 'string' || !option.label.trim())
|
|
574
|
+
return [];
|
|
575
|
+
const label = clamp(stripControls(option.label.trim()), VSCODE_CHAT_LIMITS.titleCharacters);
|
|
576
|
+
const description = typeof option.description === 'string' && option.description.trim()
|
|
577
|
+
? clamp(stripControls(option.description.trim()), VSCODE_CHAT_LIMITS.titleCharacters) : undefined;
|
|
578
|
+
return [{ label, ...(description ? { description } : {}) }];
|
|
579
|
+
});
|
|
580
|
+
// The question alone (`x-question`) where the elicitation states it: its `description` repeats the options for
|
|
581
|
+
// a frontend that cannot show them, and the carousel shows them itself.
|
|
582
|
+
const asked = clamp(stripControls(property['x-question'].trim()), VSCODE_CHAT_LIMITS.partCharacters);
|
|
583
|
+
const message = asked && asked !== title ? asked : undefined;
|
|
584
|
+
fields.push({ key, title, ...(message ? { message } : {}), options, multiSelect: property['x-multi-select'] === true });
|
|
585
|
+
}
|
|
586
|
+
return fields.length > 0 ? fields : undefined;
|
|
587
|
+
}
|
|
368
588
|
/** True when this request asks for content a button click cannot carry. */
|
|
369
589
|
export function requestNeedsTypedReply(request) {
|
|
370
590
|
if (request.kind === 'approval')
|
|
@@ -439,18 +659,21 @@ export function projectPendingRequests(requests, context) {
|
|
|
439
659
|
export function projectEvent(event, context) {
|
|
440
660
|
const payload = payloadOf(event);
|
|
441
661
|
switch (event.kind) {
|
|
442
|
-
//
|
|
443
|
-
//
|
|
444
|
-
//
|
|
662
|
+
// runtime-hosted.mjs `#submitClaimed` — the runtime's echo of the initial
|
|
663
|
+
// prompt, before `turn_started`. VS Code renders that request turn. A
|
|
664
|
+
// rejoined turn has no submitting handler; later runtime input is visible
|
|
445
665
|
// within its live response and remains a user turn in canonical history.
|
|
666
|
+
// The harness's own user-role records (runtime-native-projection.mjs
|
|
667
|
+
// `claudeBlocks`) arrive stamped `source: 'harness'`: a notice or nothing.
|
|
446
668
|
case 'user_message': {
|
|
669
|
+
// (a subagent's own input, `parent_id`: what it was asked is on its row)
|
|
670
|
+
if (text(payload.parent_id))
|
|
671
|
+
return [];
|
|
672
|
+
const notice = notPersonNotice(payload);
|
|
673
|
+
if (notice !== undefined)
|
|
674
|
+
return notice ? [stable({ part: 'markdown', value: `\n\n${notice}\n\n` })] : [];
|
|
447
675
|
if (context.liveUserMessages) {
|
|
448
676
|
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
677
|
const prefix = '\n\n**Additional input**\n\n';
|
|
455
678
|
const suffix = '\n\n';
|
|
456
679
|
const value = clamp(stripControls(body), VSCODE_CHAT_LIMITS.partCharacters - prefix.length - suffix.length);
|
|
@@ -469,6 +692,10 @@ export function projectEvent(event, context) {
|
|
|
469
692
|
return [];
|
|
470
693
|
// crates/runtime/src/event.rs `AgentEvent::TextDelta`.
|
|
471
694
|
case 'text_delta':
|
|
695
|
+
// A subagent's own words (`parent_id`): VS Code's own subagent row shows its calls and its answer, not its
|
|
696
|
+
// running text, which would otherwise read as the agent's reply.
|
|
697
|
+
if (text(payload.parent_id))
|
|
698
|
+
return [];
|
|
472
699
|
return [stable({ part: 'markdown', value: text(payload.text) })];
|
|
473
700
|
// crates/runtime/src/event.rs `AgentEvent::ToolCallStarted`.
|
|
474
701
|
case 'tool_call_started': {
|
|
@@ -478,6 +705,9 @@ export function projectEvent(event, context) {
|
|
|
478
705
|
const inputText = clamp(stripControls(parseArguments(rawArguments)), VSCODE_CHAT_LIMITS.partCharacters);
|
|
479
706
|
const titles = toolTitles(toolName, rawArguments);
|
|
480
707
|
const commandLine = shellCommand(toolName, rawArguments);
|
|
708
|
+
const plan = planItems(toolName, rawArguments) ?? context.plan?.started(toolName, rawArguments, toolCallId);
|
|
709
|
+
const subagent = subagentCall(toolName, rawArguments);
|
|
710
|
+
const parentToolCallId = text(payload.parent_id);
|
|
481
711
|
return [{
|
|
482
712
|
emit: {
|
|
483
713
|
part: 'tool',
|
|
@@ -488,6 +718,9 @@ export function projectEvent(event, context) {
|
|
|
488
718
|
inputText,
|
|
489
719
|
isComplete: false,
|
|
490
720
|
...(commandLine ? { commandLine } : {}),
|
|
721
|
+
...(plan ? { plan } : {}),
|
|
722
|
+
...(subagent ? { subagent } : {}),
|
|
723
|
+
...(parentToolCallId ? { parentToolCallId } : {}),
|
|
491
724
|
},
|
|
492
725
|
needs: 'chatParticipantAdditions',
|
|
493
726
|
fallback: [{ part: 'progress', value: titles.invocation }],
|
|
@@ -505,6 +738,9 @@ export function projectEvent(event, context) {
|
|
|
505
738
|
// The titles stay the ones the call started with (the writer keeps them per call); a completion that names its
|
|
506
739
|
// tool without having started on this stream is titled by that name.
|
|
507
740
|
const titles = toolName ? toolTitles(toolName, text(payload.arguments)) : undefined;
|
|
741
|
+
const parentToolCallId = text(payload.parent_id);
|
|
742
|
+
const plan = context.plan?.completed(toolCallId, text(payload.output), isError);
|
|
743
|
+
const background = payload.background === true;
|
|
508
744
|
return [{
|
|
509
745
|
emit: {
|
|
510
746
|
part: 'tool',
|
|
@@ -515,6 +751,9 @@ export function projectEvent(event, context) {
|
|
|
515
751
|
isError,
|
|
516
752
|
isComplete: true,
|
|
517
753
|
resultText: output,
|
|
754
|
+
...(plan ? { plan } : {}),
|
|
755
|
+
...(parentToolCallId ? { parentToolCallId } : {}),
|
|
756
|
+
...(background ? { background } : {}),
|
|
518
757
|
},
|
|
519
758
|
needs: 'chatParticipantAdditions',
|
|
520
759
|
fallback: [{
|
|
@@ -563,9 +802,11 @@ export function projectEvent(event, context) {
|
|
|
563
802
|
const summary = requestSummary(request);
|
|
564
803
|
const answers = requestAnswers(request, context.connectionId);
|
|
565
804
|
const subject = requestSubject(request);
|
|
805
|
+
const asked = requestQuestions(request);
|
|
566
806
|
const title = request.kind === 'approval'
|
|
567
807
|
? subject ? `Allow ${stripControls(subject)}?` : 'Approval required'
|
|
568
|
-
:
|
|
808
|
+
: asked ? asked.length === 1 ? asked[0].title : `${asked.length} questions`
|
|
809
|
+
: subject ? `Approve ${subject}` : `Assistant request #${request.id}`;
|
|
569
810
|
// The host capability decides whether these original answers can use
|
|
570
811
|
// a native in-turn confirmation card. Stock confirmation clicks start a
|
|
571
812
|
// new chat request, so stable command buttons remain the fallback.
|
|
@@ -575,13 +816,22 @@ export function projectEvent(event, context) {
|
|
|
575
816
|
value: `\n\n**${title}**\n\n${summary}\n\n_This runtime answers its own requests; Volter Harness is observing._\n\n`,
|
|
576
817
|
})];
|
|
577
818
|
}
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
819
|
+
const buttons = {
|
|
820
|
+
part: 'answers',
|
|
821
|
+
approval: request.kind === 'approval',
|
|
822
|
+
title: clamp(title, VSCODE_CHAT_LIMITS.titleCharacters),
|
|
823
|
+
message: summary,
|
|
824
|
+
answers,
|
|
825
|
+
};
|
|
826
|
+
// A harness's questions: VS Code's own carousel, answered inside the running turn.
|
|
827
|
+
if (asked) {
|
|
828
|
+
return [{
|
|
829
|
+
emit: { part: 'questions', connectionId: context.connectionId, requestId: request.id, questions: asked, fallback: buttons },
|
|
830
|
+
needs: 'chatParticipantAdditions',
|
|
831
|
+
fallback: [buttons],
|
|
832
|
+
}];
|
|
833
|
+
}
|
|
834
|
+
return [stable(buttons)];
|
|
585
835
|
}
|
|
586
836
|
// crates/harness/src/server.rs `FrontendRequestBroker::respond`.
|
|
587
837
|
case 'request_resolved': {
|
|
@@ -603,10 +853,29 @@ export function projectEvent(event, context) {
|
|
|
603
853
|
fallback: [{ part: 'markdown', value: `> ⚠ ${message}` }],
|
|
604
854
|
}];
|
|
605
855
|
}
|
|
856
|
+
// A harness hook that blocked or warned (`runtime-native-projection.mjs`, from Claude Code's own hook records).
|
|
857
|
+
// VS Code's own hook row, for the hook events it names; its words otherwise. In the pinned host `push` of this part
|
|
858
|
+
// checks no proposal and `hookProgress` checks `chatParticipantAdditions`; `chatHooks` gates nothing this frontend
|
|
859
|
+
// uses. It is keyed to `chatParticipantAdditions` with the other proposed parts, so a host without it gets words.
|
|
860
|
+
case 'hook': {
|
|
861
|
+
const hookType = HOOK_TYPES.find(type => type === text(payload.hook_event));
|
|
862
|
+
const reason = clamp(stripControls(text(payload.reason)), VSCODE_CHAT_LIMITS.partCharacters);
|
|
863
|
+
const blocked = payload.blocked === true;
|
|
864
|
+
const said = hookSentence(text(payload.hook_event, 'harness'), blocked, reason);
|
|
865
|
+
return [{
|
|
866
|
+
emit: hookType ? { part: 'hook', hookType, blocked, reason } : { part: 'warning', value: said },
|
|
867
|
+
needs: 'chatParticipantAdditions',
|
|
868
|
+
fallback: [{ part: 'markdown', value: `> ⚠ ${said}` }],
|
|
869
|
+
}];
|
|
870
|
+
}
|
|
606
871
|
// crates/runtime/src/event.rs `AgentEvent::Usage`.
|
|
607
872
|
// Token counts are not conversation: VS Code's own chat keeps them out of the transcript.
|
|
608
873
|
case 'usage':
|
|
609
874
|
return [];
|
|
875
|
+
// The harness's background tasks are the session's state (`session.ts` lists them while a turn runs, each with
|
|
876
|
+
// its Stop), not conversation.
|
|
877
|
+
case 'background_tasks':
|
|
878
|
+
return [];
|
|
610
879
|
// crates/runtime/src/event.rs `AgentEvent::BackgroundOutput`.
|
|
611
880
|
case 'background_output': {
|
|
612
881
|
const chunk = fence(clamp(stripControls(text(payload.chunk)), VSCODE_CHAT_LIMITS.partCharacters));
|
|
@@ -664,6 +933,15 @@ export function projectEvent(event, context) {
|
|
|
664
933
|
case 'native_event':
|
|
665
934
|
return [];
|
|
666
935
|
case 'status':
|
|
936
|
+
// A subagent's task finishing (`call_id` names the subagent call): its answer belongs to that call's row (or to
|
|
937
|
+
// the agent's reply, for a subagent started in the background), so the notice is its first line, and the task's
|
|
938
|
+
// output file where it names one (a background shell's notice names the same call).
|
|
939
|
+
if (text(payload.call_id)) {
|
|
940
|
+
const lines = stripControls(text(payload.message)).split('\n');
|
|
941
|
+
const output = lines.find(line => line.startsWith('Output: ') && line.length > 8);
|
|
942
|
+
const line = [clamp((lines[0] ?? '').trim(), VSCODE_CHAT_LIMITS.titleCharacters), output].filter(Boolean).join('\n');
|
|
943
|
+
return line ? [stable({ part: 'progress', value: clamp(line, VSCODE_CHAT_LIMITS.partCharacters) })] : [];
|
|
944
|
+
}
|
|
667
945
|
return [stable({ part: 'progress', value: clamp(stripControls(text(payload.message)), VSCODE_CHAT_LIMITS.partCharacters) })];
|
|
668
946
|
case 'warning':
|
|
669
947
|
return [stable({ part: 'warning', value: clamp(stripControls(text(payload.message)), VSCODE_CHAT_LIMITS.partCharacters) })];
|
|
@@ -671,6 +949,9 @@ export function projectEvent(event, context) {
|
|
|
671
949
|
// sdk/interchange/frontend-transcript.mjs reads a donor that emits them.
|
|
672
950
|
case 'reasoning':
|
|
673
951
|
case 'thinking': {
|
|
952
|
+
// (a subagent's own reasoning, `parent_id`, as its running text above)
|
|
953
|
+
if (text(payload.parent_id))
|
|
954
|
+
return [];
|
|
674
955
|
const body = clamp(stripControls(text(payload.text ?? payload.summary, opaqueText(event.payload))), VSCODE_CHAT_LIMITS.partCharacters);
|
|
675
956
|
return [{
|
|
676
957
|
emit: { part: 'thinking', value: body },
|
|
@@ -730,47 +1011,65 @@ function messageText(message) {
|
|
|
730
1011
|
.join('\n');
|
|
731
1012
|
}
|
|
732
1013
|
/**
|
|
733
|
-
*
|
|
734
|
-
*
|
|
735
|
-
*
|
|
1014
|
+
* What a `user_message` shows when no person typed it: undefined for the person's words, '' to show nothing, else a
|
|
1015
|
+
* short notice. The harness's own records carry `source: 'harness'` (and a `notice` where one is shown) from the one
|
|
1016
|
+
* provenance rule the runtime applies (`@volter/supercode-interchange/user-provenance`). The editor's automatic
|
|
1017
|
+
* tripwire nudge travels through the person's input door, so the editor's frame is what names it.
|
|
736
1018
|
*/
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
const status = tag('status');
|
|
743
|
-
return { notice: `_Background task${status ? ` ${status}` : ''}${summary ? `: ${summary}` : ''}_` };
|
|
744
|
-
}
|
|
745
|
-
if (trimmed.startsWith('[Volter editor tripwire'))
|
|
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._' };
|
|
1019
|
+
function notPersonNotice(payload) {
|
|
1020
|
+
if (payload.source === 'harness')
|
|
1021
|
+
return noticeMarkdown(text(payload.notice));
|
|
1022
|
+
if (editorNudge(text(payload.text)))
|
|
1023
|
+
return '';
|
|
750
1024
|
return undefined;
|
|
751
1025
|
}
|
|
1026
|
+
/** The rule's plain notice as this view's italic line; '' for none. */
|
|
1027
|
+
function noticeMarkdown(notice) {
|
|
1028
|
+
const value = clamp(stripControls(notice ?? ''), VSCODE_CHAT_LIMITS.partCharacters - 2);
|
|
1029
|
+
return value ? `_${value}_` : '';
|
|
1030
|
+
}
|
|
1031
|
+
function editorNudge(body) {
|
|
1032
|
+
return body.trimStart().startsWith('[Volter editor tripwire');
|
|
1033
|
+
}
|
|
752
1034
|
export function projectReplayTurns(events, granted, context, history = []) {
|
|
753
1035
|
// Join canonical history with its sequenced tail. Retained request/decision
|
|
754
1036
|
// events can be re-sequenced past history_cursor with no originating turn.
|
|
755
1037
|
// Before the replay supplies a prompt, give its parts their own runtime
|
|
756
1038
|
// history turn rather than attributing old approvals to the latest answer.
|
|
757
1039
|
const turns = history.map(turn => ({ ...turn, parts: turn.parts?.slice() }));
|
|
1040
|
+
// An approval the replay shows allowed is not drawn again: live, its card is hidden (Auto approve) or answered,
|
|
1041
|
+
// and its allowed resolution says nothing more, so the tool row is the record (`request_resolved` above).
|
|
1042
|
+
const allowed = new Set();
|
|
1043
|
+
for (const event of events) {
|
|
1044
|
+
if (event.kind !== 'request_resolved')
|
|
1045
|
+
continue;
|
|
1046
|
+
const payload = payloadOf(event);
|
|
1047
|
+
const response = payload.response;
|
|
1048
|
+
if (typeof payload.request_id === 'number' && response?.kind === 'approval' && response.decision !== 'deny')
|
|
1049
|
+
allowed.add(payload.request_id);
|
|
1050
|
+
}
|
|
758
1051
|
let replayTurn = false;
|
|
759
1052
|
let streamedText;
|
|
760
1053
|
for (const event of events) {
|
|
761
|
-
if (event.kind === '
|
|
1054
|
+
if (event.kind === 'request') {
|
|
1055
|
+
const request = (payloadOf(event).request ?? payloadOf(event));
|
|
1056
|
+
if (typeof request?.id === 'number' && allowed.has(request.id))
|
|
1057
|
+
continue;
|
|
1058
|
+
}
|
|
1059
|
+
// (a subagent's own input is no turn of the conversation; `projectEvent` draws nothing for it)
|
|
1060
|
+
if (event.kind === 'user_message' && !text(payloadOf(event).parent_id)) {
|
|
762
1061
|
streamedText = undefined;
|
|
763
1062
|
const payload = payloadOf(event);
|
|
764
|
-
const
|
|
765
|
-
if (
|
|
766
|
-
if (!
|
|
1063
|
+
const shown = notPersonNotice(payload);
|
|
1064
|
+
if (shown !== undefined) {
|
|
1065
|
+
if (!shown)
|
|
767
1066
|
continue;
|
|
768
1067
|
if (!replayTurn) {
|
|
769
1068
|
turns.push({ role: 'user', prompt: HISTORY_MARKER_PROMPT });
|
|
770
1069
|
replayTurn = true;
|
|
771
1070
|
}
|
|
772
1071
|
const last = turns[turns.length - 1];
|
|
773
|
-
const notice = { part: 'markdown', value: `\n\n${
|
|
1072
|
+
const notice = { part: 'markdown', value: `\n\n${shown}\n\n` };
|
|
774
1073
|
if (last.role === 'assistant' && last.parts)
|
|
775
1074
|
last.parts.push(notice);
|
|
776
1075
|
else
|
|
@@ -822,7 +1121,7 @@ export function projectReplayTurns(events, granted, context, history = []) {
|
|
|
822
1121
|
* The attach snapshot's canonical history as chat turns. Bounded: when older
|
|
823
1122
|
* turns are omitted the view says so, the way the browser observer does.
|
|
824
1123
|
*/
|
|
825
|
-
export function projectHistory(history, granted, pendingToolNames = new Set()) {
|
|
1124
|
+
export function projectHistory(history, granted, pendingToolNames = new Set(), plan = new PlanTracker()) {
|
|
826
1125
|
const turns = [];
|
|
827
1126
|
// Keep results inside the native invocation, as the live stream does. A
|
|
828
1127
|
// separate Markdown result bypasses the editor's collapsed tool-step group.
|
|
@@ -870,17 +1169,16 @@ export function projectHistory(history, granted, pendingToolNames = new Set()) {
|
|
|
870
1169
|
pushAssistant([{ part: 'markdown', value: clamp(stripControls(text), VSCODE_CHAT_LIMITS.partCharacters) }]);
|
|
871
1170
|
continue;
|
|
872
1171
|
}
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
1172
|
+
// The codec preserves the harness's provenance; the one rule reads it.
|
|
1173
|
+
// Image-coordinate notes, compact summaries, task notifications and the
|
|
1174
|
+
// interruption record are not the person's words.
|
|
1175
|
+
const provenance = userProvenance(message.metadata, body);
|
|
1176
|
+
if (provenance.source === 'harness' || editorNudge(body)) {
|
|
1177
|
+
const notice = noticeMarkdown(provenance.notice);
|
|
1178
|
+
if (notice)
|
|
1179
|
+
pushAssistant([{ part: 'markdown', value: `\n\n${notice}\n\n` }]);
|
|
877
1180
|
continue;
|
|
878
1181
|
}
|
|
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;
|
|
884
1182
|
toolRows.clear();
|
|
885
1183
|
turns.push({ role: 'user', prompt: clamp(stripControls(body), VSCODE_CHAT_LIMITS.partCharacters) });
|
|
886
1184
|
continue;
|
|
@@ -906,6 +1204,8 @@ export function projectHistory(history, granted, pendingToolNames = new Set()) {
|
|
|
906
1204
|
for (const call of message.tool_calls ?? []) {
|
|
907
1205
|
const titles = toolTitles(call.function.name, call.function.arguments);
|
|
908
1206
|
const commandLine = shellCommand(call.function.name, call.function.arguments);
|
|
1207
|
+
const items = planItems(call.function.name, call.function.arguments) ?? plan.started(call.function.name, call.function.arguments, call.id);
|
|
1208
|
+
const subagent = subagentCall(call.function.name, call.function.arguments);
|
|
909
1209
|
const row = {
|
|
910
1210
|
part: 'tool',
|
|
911
1211
|
toolName: call.function.name,
|
|
@@ -913,6 +1213,8 @@ export function projectHistory(history, granted, pendingToolNames = new Set()) {
|
|
|
913
1213
|
invocationMessage: titles.invocation,
|
|
914
1214
|
pastTenseMessage: titles.pastTense,
|
|
915
1215
|
...(commandLine ? { commandLine } : {}),
|
|
1216
|
+
...(items ? { plan: items } : {}),
|
|
1217
|
+
...(subagent ? { subagent } : {}),
|
|
916
1218
|
inputText: clamp(stripControls(parseArguments(call.function.arguments)), VSCODE_CHAT_LIMITS.partCharacters),
|
|
917
1219
|
isComplete: true,
|
|
918
1220
|
};
|
|
@@ -950,6 +1252,9 @@ export function projectHistory(history, granted, pendingToolNames = new Set()) {
|
|
|
950
1252
|
// establish failure; do not guess from arbitrary result prose.
|
|
951
1253
|
if (body === '[no tool result recorded — turn interrupted]' || (Array.isArray(message.content) && message.content.some(part => part && typeof part === 'object' && !Array.isArray(part) && part.is_error === true)))
|
|
952
1254
|
row.isError = true;
|
|
1255
|
+
const items = plan.completed(message.tool_call_id, body, row.isError === true);
|
|
1256
|
+
if (items)
|
|
1257
|
+
row.plan = items;
|
|
953
1258
|
toolRows.delete(message.tool_call_id);
|
|
954
1259
|
}
|
|
955
1260
|
else {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-frontend-vscode",
|
|
3
3
|
"displayName": "Volter Harness",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.63",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "VS Code chat-view frontend for an existing Volter Harness SDK runtime",
|
|
7
7
|
"repository": {
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"LICENSE-APACHE",
|
|
23
23
|
"dist",
|
|
24
24
|
"vendor/supercode-frontend",
|
|
25
|
+
"vendor/supercode-interchange",
|
|
25
26
|
"scripts/pack-extension.mjs",
|
|
26
27
|
"README.md",
|
|
27
28
|
"NOTICE.md",
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Generated from sdk/interchange/user-provenance.mjs by scripts/user-provenance-copies.mjs (the pre-commit hook
|
|
2
|
+
// runs it): edit that rule, not this file.
|
|
3
|
+
export type UserSource = "person" | "harness";
|
|
4
|
+
export interface UserProvenance {
|
|
5
|
+
source: UserSource;
|
|
6
|
+
/** The short line shown in place of a harness record, where one is shown. */
|
|
7
|
+
notice?: string;
|
|
8
|
+
}
|
|
9
|
+
export const CLAUDE_INTERRUPTION_TEXTS: readonly string[];
|
|
10
|
+
export function isClaudeInterruption(text: unknown): boolean;
|
|
11
|
+
export function userProvenance(
|
|
12
|
+
metadata: Readonly<Record<string, string>> | null | undefined,
|
|
13
|
+
text?: string,
|
|
14
|
+
): UserProvenance;
|
|
15
|
+
export function isHarnessAuthoredUser(
|
|
16
|
+
metadata: Readonly<Record<string, string>> | null | undefined,
|
|
17
|
+
text?: string,
|
|
18
|
+
): boolean;
|
|
19
|
+
export function claudeRecordProvenance(record: unknown): Record<string, string>;
|