@volter/supercode-frontend-vscode 0.1.60 → 0.1.62
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 +12 -5
- package/dist/extension.js +11 -0
- package/dist/host-controls.d.ts +11 -0
- package/dist/host-controls.js +8 -0
- package/dist/view.d.ts +0 -8
- package/dist/view.js +54 -37
- package/dist/workbench-tools.d.ts +13 -0
- package/dist/workbench-tools.js +81 -0
- package/package.json +9 -14
- package/vendor/supercode-frontend/package.json +0 -1
- package/vendor/supercode-interchange/user-provenance.d.mts +19 -0
- package/vendor/supercode-interchange/user-provenance.mjs +230 -0
package/README.md
CHANGED
|
@@ -357,7 +357,7 @@ it needs and the fallback it degrades to. Each arm names the producer it reads.
|
|
|
357
357
|
| Event kind | Producer | Rendered as |
|
|
358
358
|
|---|---|---|
|
|
359
359
|
| `text_delta` | `crates/runtime/src/event.rs` `AgentEvent::TextDelta` | `response.markdown` (streamed) |
|
|
360
|
-
| `user_message` | `sdk/interchange/agent-runtime-host.mjs` accepted-turn projection | the request turn in history; dropped live (VS Code already shows it) |
|
|
360
|
+
| `user_message` | `sdk/interchange/agent-runtime-host.mjs` accepted-turn projection; `runtime-native-projection.mjs` `claudeBlocks` for Claude's own user-role records (`source: harness`) | the request turn in history; dropped live (VS Code already shows it); a harness record shows its `notice` or nothing |
|
|
361
361
|
| `turn_started` / `turn_completed` / `turn_succeeded` | `submit_claimed`; `AgentEvent::TurnCompleted` | lifecycle only — the handler resolves, nothing renders |
|
|
362
362
|
| `turn_interrupted` | `submit_claimed` | an italic notice |
|
|
363
363
|
| `turn_failed` | `submit_claimed` | `ChatResponseWarningPart` → blockquote |
|
|
@@ -438,10 +438,17 @@ needs it; a reader does not, and a collapsed block is still a reader's problem.
|
|
|
438
438
|
one to a person, so `role: 'system'` is dropped outright and the transcript starts where the
|
|
439
439
|
conversation does.
|
|
440
440
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
441
|
+
Whose words a user-role record carries is one rule,
|
|
442
|
+
`@volter/supercode-interchange/user-provenance`, vendored here unchanged. Live
|
|
443
|
+
and replayed `user_message` events carry its answer as `source`
|
|
444
|
+
(`person` | `harness`) with a `notice` where one is shown; saved history is
|
|
445
|
+
read through the same rule. Records the harness wrote (`isMeta`,
|
|
446
|
+
`isSynthetic` — the stream's image-coordinate note — `isCompactSummary`,
|
|
447
|
+
`promptSource: system`, sidechain and tool-result records, file attachments)
|
|
448
|
+
create no person-message bubble, live or restored; ordinary quoted text
|
|
449
|
+
without those flags stays a person message. A task notification becomes a
|
|
450
|
+
short background-task notice, and Claude's unmarked, exact interruption
|
|
451
|
+
records (`[Request interrupted by user]`, `… for tool use]`) a short turn
|
|
445
452
|
interruption notice. The native transcript remains authoritative and unchanged.
|
|
446
453
|
[Source evidence and remaining qualification](../../docs/evidence/chat-native-notices-20261013.md).
|
|
447
454
|
|
package/dist/extension.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { resolveSessionBinding } from './bindings.js';
|
|
2
2
|
import { withTextContext } from './context.js';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { WorkbenchTools } from './workbench-tools.js';
|
|
4
5
|
import { ChatHostControls, runtimeIdentity, harnessDisplayName, turnPermission } from './host-controls.js';
|
|
5
6
|
import { SETUP_BEGIN, SETUP_CANCEL, SETUP_STATE, setupAgents, setupPresentation, setupWelcome } from './chat-setup.js';
|
|
6
7
|
import { ChatSetupFlow } from './setup-flow.js';
|
|
@@ -360,6 +361,16 @@ export async function activate(context) {
|
|
|
360
361
|
const controls = await ChatHostControls.fromEnvironment();
|
|
361
362
|
const controlsChanged = new vscode.EventEmitter();
|
|
362
363
|
context.subscriptions.push(controlsChanged);
|
|
364
|
+
// The workbench's tools for the host's runtimes, on the endpoint the host names (ADR 0255).
|
|
365
|
+
if (controls) {
|
|
366
|
+
const workbenchTools = new WorkbenchTools();
|
|
367
|
+
context.subscriptions.push(workbenchTools);
|
|
368
|
+
const serveTools = (state) => {
|
|
369
|
+
workbenchTools.serve(state.workbenchTools).catch(error => log.warn(`Workbench tools unavailable: ${String(error)}`));
|
|
370
|
+
};
|
|
371
|
+
context.subscriptions.push({ dispose: controls.onState(serveTools) });
|
|
372
|
+
serveTools(controls.state);
|
|
373
|
+
}
|
|
363
374
|
// A person's explicit picker choice: per conversation once it is bound, and for the new chat before then.
|
|
364
375
|
let permission;
|
|
365
376
|
const permissions = new Map(context.workspaceState.get('supercode.sessionPermissions', []));
|
package/dist/host-controls.d.ts
CHANGED
|
@@ -79,15 +79,26 @@ export interface HostState {
|
|
|
79
79
|
/** Project policy for the next start/resume; an existing runtime uses its recorded session policy. */
|
|
80
80
|
newChatRuntimePolicy?: 'default' | 'yolo' | null;
|
|
81
81
|
runtimePolicyError?: string | null;
|
|
82
|
+
/** The workbench tools this host's runtimes reach (ADR 0255): the tag they carry, and the socket and token the host
|
|
83
|
+
* named, where their relay (`supercode mcp workbench`) calls them. Absent, they reach none. */
|
|
84
|
+
workbenchTools?: WorkbenchToolsEndpoint;
|
|
85
|
+
}
|
|
86
|
+
export interface WorkbenchToolsEndpoint {
|
|
87
|
+
tag: string;
|
|
88
|
+
socket: string;
|
|
89
|
+
token: string;
|
|
82
90
|
}
|
|
83
91
|
export interface HostSelectionResult extends HostState {
|
|
84
92
|
connection: Record<string, string>;
|
|
85
93
|
}
|
|
86
94
|
export declare class ChatHostControls {
|
|
95
|
+
#private;
|
|
87
96
|
readonly baseUrl: string;
|
|
88
97
|
private readonly credential;
|
|
89
98
|
state: HostState;
|
|
90
99
|
private updating;
|
|
100
|
+
/** Call `listener` with each state the host replies with; returns the unsubscribe. */
|
|
101
|
+
onState(listener: (state: HostState) => void): () => void;
|
|
91
102
|
private constructor();
|
|
92
103
|
static fromEnvironment(env?: NodeJS.ProcessEnv): Promise<ChatHostControls | undefined>;
|
|
93
104
|
refresh(): Promise<void>;
|
package/dist/host-controls.js
CHANGED
|
@@ -4,6 +4,12 @@ export class ChatHostControls {
|
|
|
4
4
|
credential;
|
|
5
5
|
state;
|
|
6
6
|
updating = Promise.resolve();
|
|
7
|
+
#stateListeners = new Set();
|
|
8
|
+
/** Call `listener` with each state the host replies with; returns the unsubscribe. */
|
|
9
|
+
onState(listener) {
|
|
10
|
+
this.#stateListeners.add(listener);
|
|
11
|
+
return () => this.#stateListeners.delete(listener);
|
|
12
|
+
}
|
|
7
13
|
constructor(baseUrl, credential, state) {
|
|
8
14
|
this.baseUrl = baseUrl;
|
|
9
15
|
this.credential = credential;
|
|
@@ -37,6 +43,8 @@ export class ChatHostControls {
|
|
|
37
43
|
const pending = this.updating.then(async () => {
|
|
38
44
|
const result = await this.request(path, selection);
|
|
39
45
|
this.state = result;
|
|
46
|
+
for (const listener of this.#stateListeners)
|
|
47
|
+
listener(result);
|
|
40
48
|
return result;
|
|
41
49
|
});
|
|
42
50
|
this.updating = pending.then(() => undefined, () => undefined);
|
package/dist/view.d.ts
CHANGED
|
@@ -172,14 +172,6 @@ export interface ProjectedTurn {
|
|
|
172
172
|
/** Rendered parts for an assistant turn. */
|
|
173
173
|
parts?: ChatPart[];
|
|
174
174
|
}
|
|
175
|
-
/**
|
|
176
|
-
* A "user" message no person typed: Claude Code's `<task-notification>` (a background task reporting back) or the
|
|
177
|
-
* editor's automatic tripwire nudge or Claude's unmarked interruption record. None is shown as the person's words.
|
|
178
|
-
* A task notification and interruption are short notices; a nudge is not shown. Undefined for a person's message.
|
|
179
|
-
*/
|
|
180
|
-
export declare function automaticPrompt(body: string): {
|
|
181
|
-
notice?: string;
|
|
182
|
-
} | undefined;
|
|
183
175
|
export declare function projectReplayTurns(events: readonly FrontendEvent[], granted: ReadonlySet<string>, context: ProjectionContext, history?: readonly ProjectedTurn[]): ProjectedTurn[];
|
|
184
176
|
/**
|
|
185
177
|
* The attach snapshot's canonical history as chat turns. Bounded: when older
|
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
|
|
@@ -439,18 +440,18 @@ export function projectPendingRequests(requests, context) {
|
|
|
439
440
|
export function projectEvent(event, context) {
|
|
440
441
|
const payload = payloadOf(event);
|
|
441
442
|
switch (event.kind) {
|
|
442
|
-
//
|
|
443
|
-
//
|
|
444
|
-
//
|
|
443
|
+
// runtime-hosted.mjs `#submitClaimed` — the runtime's echo of the initial
|
|
444
|
+
// prompt, before `turn_started`. VS Code renders that request turn. A
|
|
445
|
+
// rejoined turn has no submitting handler; later runtime input is visible
|
|
445
446
|
// within its live response and remains a user turn in canonical history.
|
|
447
|
+
// The harness's own user-role records (runtime-native-projection.mjs
|
|
448
|
+
// `claudeBlocks`) arrive stamped `source: 'harness'`: a notice or nothing.
|
|
446
449
|
case 'user_message': {
|
|
450
|
+
const notice = notPersonNotice(payload);
|
|
451
|
+
if (notice !== undefined)
|
|
452
|
+
return notice ? [stable({ part: 'markdown', value: `\n\n${notice}\n\n` })] : [];
|
|
447
453
|
if (context.liveUserMessages) {
|
|
448
454
|
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
455
|
const prefix = '\n\n**Additional input**\n\n';
|
|
455
456
|
const suffix = '\n\n';
|
|
456
457
|
const value = clamp(stripControls(body), VSCODE_CHAT_LIMITS.partCharacters - prefix.length - suffix.length);
|
|
@@ -730,47 +731,64 @@ function messageText(message) {
|
|
|
730
731
|
.join('\n');
|
|
731
732
|
}
|
|
732
733
|
/**
|
|
733
|
-
*
|
|
734
|
-
*
|
|
735
|
-
*
|
|
734
|
+
* What a `user_message` shows when no person typed it: undefined for the person's words, '' to show nothing, else a
|
|
735
|
+
* short notice. The harness's own records carry `source: 'harness'` (and a `notice` where one is shown) from the one
|
|
736
|
+
* provenance rule the runtime applies (`@volter/supercode-interchange/user-provenance`). The editor's automatic
|
|
737
|
+
* tripwire nudge travels through the person's input door, so the editor's frame is what names it.
|
|
736
738
|
*/
|
|
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._' };
|
|
739
|
+
function notPersonNotice(payload) {
|
|
740
|
+
if (payload.source === 'harness')
|
|
741
|
+
return noticeMarkdown(text(payload.notice));
|
|
742
|
+
if (editorNudge(text(payload.text)))
|
|
743
|
+
return '';
|
|
750
744
|
return undefined;
|
|
751
745
|
}
|
|
746
|
+
/** The rule's plain notice as this view's italic line; '' for none. */
|
|
747
|
+
function noticeMarkdown(notice) {
|
|
748
|
+
const value = clamp(stripControls(notice ?? ''), VSCODE_CHAT_LIMITS.partCharacters - 2);
|
|
749
|
+
return value ? `_${value}_` : '';
|
|
750
|
+
}
|
|
751
|
+
function editorNudge(body) {
|
|
752
|
+
return body.trimStart().startsWith('[Volter editor tripwire');
|
|
753
|
+
}
|
|
752
754
|
export function projectReplayTurns(events, granted, context, history = []) {
|
|
753
755
|
// Join canonical history with its sequenced tail. Retained request/decision
|
|
754
756
|
// events can be re-sequenced past history_cursor with no originating turn.
|
|
755
757
|
// Before the replay supplies a prompt, give its parts their own runtime
|
|
756
758
|
// history turn rather than attributing old approvals to the latest answer.
|
|
757
759
|
const turns = history.map(turn => ({ ...turn, parts: turn.parts?.slice() }));
|
|
760
|
+
// An approval the replay shows allowed is not drawn again: live, its card is hidden (Auto approve) or answered,
|
|
761
|
+
// and its allowed resolution says nothing more, so the tool row is the record (`request_resolved` above).
|
|
762
|
+
const allowed = new Set();
|
|
763
|
+
for (const event of events) {
|
|
764
|
+
if (event.kind !== 'request_resolved')
|
|
765
|
+
continue;
|
|
766
|
+
const payload = payloadOf(event);
|
|
767
|
+
const response = payload.response;
|
|
768
|
+
if (typeof payload.request_id === 'number' && response?.kind === 'approval' && response.decision !== 'deny')
|
|
769
|
+
allowed.add(payload.request_id);
|
|
770
|
+
}
|
|
758
771
|
let replayTurn = false;
|
|
759
772
|
let streamedText;
|
|
760
773
|
for (const event of events) {
|
|
774
|
+
if (event.kind === 'request') {
|
|
775
|
+
const request = (payloadOf(event).request ?? payloadOf(event));
|
|
776
|
+
if (typeof request?.id === 'number' && allowed.has(request.id))
|
|
777
|
+
continue;
|
|
778
|
+
}
|
|
761
779
|
if (event.kind === 'user_message') {
|
|
762
780
|
streamedText = undefined;
|
|
763
781
|
const payload = payloadOf(event);
|
|
764
|
-
const
|
|
765
|
-
if (
|
|
766
|
-
if (!
|
|
782
|
+
const shown = notPersonNotice(payload);
|
|
783
|
+
if (shown !== undefined) {
|
|
784
|
+
if (!shown)
|
|
767
785
|
continue;
|
|
768
786
|
if (!replayTurn) {
|
|
769
787
|
turns.push({ role: 'user', prompt: HISTORY_MARKER_PROMPT });
|
|
770
788
|
replayTurn = true;
|
|
771
789
|
}
|
|
772
790
|
const last = turns[turns.length - 1];
|
|
773
|
-
const notice = { part: 'markdown', value: `\n\n${
|
|
791
|
+
const notice = { part: 'markdown', value: `\n\n${shown}\n\n` };
|
|
774
792
|
if (last.role === 'assistant' && last.parts)
|
|
775
793
|
last.parts.push(notice);
|
|
776
794
|
else
|
|
@@ -870,17 +888,16 @@ export function projectHistory(history, granted, pendingToolNames = new Set()) {
|
|
|
870
888
|
pushAssistant([{ part: 'markdown', value: clamp(stripControls(text), VSCODE_CHAT_LIMITS.partCharacters) }]);
|
|
871
889
|
continue;
|
|
872
890
|
}
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
891
|
+
// The codec preserves the harness's provenance; the one rule reads it.
|
|
892
|
+
// Image-coordinate notes, compact summaries, task notifications and the
|
|
893
|
+
// interruption record are not the person's words.
|
|
894
|
+
const provenance = userProvenance(message.metadata, body);
|
|
895
|
+
if (provenance.source === 'harness' || editorNudge(body)) {
|
|
896
|
+
const notice = noticeMarkdown(provenance.notice);
|
|
897
|
+
if (notice)
|
|
898
|
+
pushAssistant([{ part: 'markdown', value: `\n\n${notice}\n\n` }]);
|
|
877
899
|
continue;
|
|
878
900
|
}
|
|
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
901
|
toolRows.clear();
|
|
885
902
|
turns.push({ role: 'user', prompt: clamp(stripControls(body), VSCODE_CHAT_LIMITS.partCharacters) });
|
|
886
903
|
continue;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as vscode from 'vscode';
|
|
2
|
+
import type { WorkbenchToolsEndpoint } from './host-controls.js';
|
|
3
|
+
export declare class WorkbenchTools implements vscode.Disposable {
|
|
4
|
+
#private;
|
|
5
|
+
/** Serve on the endpoint the host names, or nothing when it names none. A changed endpoint replaces the server.
|
|
6
|
+
* Calls apply in order, so two quick state replies cannot race a close against a listen. */
|
|
7
|
+
serve(endpoint: WorkbenchToolsEndpoint | undefined): Promise<void>;
|
|
8
|
+
private apply;
|
|
9
|
+
private answer;
|
|
10
|
+
private handle;
|
|
11
|
+
private close;
|
|
12
|
+
dispose(): void;
|
|
13
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The workbench's tools, for the harness agents in its Chat (ADR 0255).
|
|
3
|
+
*
|
|
4
|
+
* The host names the tools and where they run (`HostState.workbenchTools`: a tag, a socket and a token), and lists
|
|
5
|
+
* them to its runtimes itself, through `supercode mcp workbench`. That relay calls them here: this serves, in the
|
|
6
|
+
* extension host where `vscode.lm` exists, one call of a tool `vscode.lm.tools` lists with the tag, through
|
|
7
|
+
* Code-OSS's own tool service. With no endpoint it serves nothing.
|
|
8
|
+
*
|
|
9
|
+
* The wire is one JSON line each way per connection: `{token, name, input}` → `{text, isError}`.
|
|
10
|
+
*/
|
|
11
|
+
import { rmSync } from 'node:fs';
|
|
12
|
+
import { createServer } from 'node:net';
|
|
13
|
+
import * as vscode from 'vscode';
|
|
14
|
+
export class WorkbenchTools {
|
|
15
|
+
#served;
|
|
16
|
+
#applying = Promise.resolve();
|
|
17
|
+
/** Serve on the endpoint the host names, or nothing when it names none. A changed endpoint replaces the server.
|
|
18
|
+
* Calls apply in order, so two quick state replies cannot race a close against a listen. */
|
|
19
|
+
serve(endpoint) {
|
|
20
|
+
const applying = this.#applying.then(() => this.apply(endpoint));
|
|
21
|
+
this.#applying = applying.catch(() => undefined);
|
|
22
|
+
return applying;
|
|
23
|
+
}
|
|
24
|
+
async apply(endpoint) {
|
|
25
|
+
const current = this.#served?.endpoint;
|
|
26
|
+
if (current?.tag === endpoint?.tag && current?.socket === endpoint?.socket && current?.token === endpoint?.token)
|
|
27
|
+
return;
|
|
28
|
+
this.close();
|
|
29
|
+
if (!endpoint)
|
|
30
|
+
return;
|
|
31
|
+
// A socket left by an earlier extension host of this window is this window's own.
|
|
32
|
+
if (process.platform !== 'win32')
|
|
33
|
+
rmSync(endpoint.socket, { force: true });
|
|
34
|
+
const server = createServer(socket => this.answer(socket, endpoint));
|
|
35
|
+
await new Promise((resolve, reject) => { server.once('error', reject); server.listen(endpoint.socket, resolve); });
|
|
36
|
+
this.#served = { endpoint, server };
|
|
37
|
+
}
|
|
38
|
+
answer(socket, endpoint) {
|
|
39
|
+
let buffer = '';
|
|
40
|
+
socket.setEncoding('utf8');
|
|
41
|
+
socket.on('data', chunk => {
|
|
42
|
+
buffer += chunk;
|
|
43
|
+
const end = buffer.indexOf('\n');
|
|
44
|
+
if (end < 0)
|
|
45
|
+
return;
|
|
46
|
+
const line = buffer.slice(0, end);
|
|
47
|
+
buffer = '';
|
|
48
|
+
void this.handle(line, endpoint).then(reply => socket.end(`${JSON.stringify(reply)}\n`));
|
|
49
|
+
});
|
|
50
|
+
socket.on('error', () => socket.destroy());
|
|
51
|
+
}
|
|
52
|
+
async handle(line, { tag, token }) {
|
|
53
|
+
let request;
|
|
54
|
+
try {
|
|
55
|
+
request = JSON.parse(line);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return { text: 'The call was not JSON.', isError: true };
|
|
59
|
+
}
|
|
60
|
+
if (request.token !== token)
|
|
61
|
+
return { text: 'The call was refused.', isError: true };
|
|
62
|
+
const name = request.name;
|
|
63
|
+
if (typeof name !== 'string' || !vscode.lm.tools.some(tool => tool.name === name && tool.tags.includes(tag))) {
|
|
64
|
+
return { text: `The workbench has no tool ${String(name)}.`, isError: true };
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
const input = request.input && typeof request.input === 'object' ? request.input : {};
|
|
68
|
+
const result = await vscode.lm.invokeTool(name, { input, toolInvocationToken: undefined });
|
|
69
|
+
return { text: result.content.map(part => part instanceof vscode.LanguageModelTextPart ? part.value : '').join(''), isError: false };
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
return { text: error instanceof Error ? error.message : String(error), isError: true };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
close() {
|
|
76
|
+
const served = this.#served;
|
|
77
|
+
this.#served = undefined;
|
|
78
|
+
served?.server.close();
|
|
79
|
+
}
|
|
80
|
+
dispose() { this.close(); }
|
|
81
|
+
}
|
package/package.json
CHANGED
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-frontend-vscode",
|
|
3
|
-
"devEngines": {
|
|
4
|
-
"packageManager": {
|
|
5
|
-
"name": "pnpm",
|
|
6
|
-
"version": ">=12.15.0",
|
|
7
|
-
"onFail": "error"
|
|
8
|
-
}
|
|
9
|
-
},
|
|
10
3
|
"displayName": "Volter Harness",
|
|
11
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.62",
|
|
12
5
|
"type": "module",
|
|
13
6
|
"description": "VS Code chat-view frontend for an existing Volter Harness SDK runtime",
|
|
14
7
|
"repository": {
|
|
@@ -29,11 +22,17 @@
|
|
|
29
22
|
"LICENSE-APACHE",
|
|
30
23
|
"dist",
|
|
31
24
|
"vendor/supercode-frontend",
|
|
25
|
+
"vendor/supercode-interchange",
|
|
32
26
|
"scripts/pack-extension.mjs",
|
|
33
27
|
"README.md",
|
|
34
28
|
"NOTICE.md",
|
|
35
29
|
"media"
|
|
36
30
|
],
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "node -e \"require('node:fs').rmSync('dist', {recursive:true, force:true})\" && node scripts/sync-sdk.mjs && node scripts/fetch-icon.mjs && node scripts/sync-marks.mjs && tsc -p tsconfig.build.json",
|
|
33
|
+
"extension": "npm run build && node scripts/pack-extension.mjs",
|
|
34
|
+
"prepack": "npm run build"
|
|
35
|
+
},
|
|
37
36
|
"engines": {
|
|
38
37
|
"node": ">=20",
|
|
39
38
|
"vscode": "^1.138.0"
|
|
@@ -214,9 +213,5 @@
|
|
|
214
213
|
"version": "0.2.12",
|
|
215
214
|
"mode": "generated-artifact"
|
|
216
215
|
},
|
|
217
|
-
"license": "MIT OR Apache-2.0"
|
|
218
|
-
|
|
219
|
-
"build": "node -e \"require('node:fs').rmSync('dist', {recursive:true, force:true})\" && node scripts/sync-sdk.mjs && node scripts/fetch-icon.mjs && node scripts/sync-marks.mjs && tsc -p tsconfig.build.json",
|
|
220
|
-
"extension": "pnpm run build && node scripts/pack-extension.mjs"
|
|
221
|
-
}
|
|
222
|
-
}
|
|
216
|
+
"license": "MIT OR Apache-2.0"
|
|
217
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-frontend",
|
|
3
|
-
"devEngines": { "packageManager": { "name": "pnpm", "version": ">=12.15.0", "onFail": "error" } },
|
|
4
3
|
"version": "0.2.12",
|
|
5
4
|
"type": "module",
|
|
6
5
|
"description": "Generated language-neutral attachment client for Volter Harness SDK runtimes",
|
|
@@ -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>;
|
|
@@ -0,0 +1,230 @@
|
|
|
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
|
+
// Whose words a user-role record carries: the person's, or the harness's own.
|
|
4
|
+
// One rule for every reader (the live projection, replay, history, client
|
|
5
|
+
// transcripts, Teams' turn and mail readers, voice, trace). It imports nothing,
|
|
6
|
+
// so a host that cannot resolve this package (the VS Code extension, the browser
|
|
7
|
+
// observer, the hosted viewer, supercode-ui) carries its exact bytes; the
|
|
8
|
+
// pre-commit hook writes those copies (scripts/user-provenance-copies.mjs).
|
|
9
|
+
//
|
|
10
|
+
// The rule reads, in order:
|
|
11
|
+
// 1. a person's marker: the record says a person wrote it. A person who quotes
|
|
12
|
+
// a frame (`<task-notification> showed up in my chat`) or types a reserved
|
|
13
|
+
// notice is still the person.
|
|
14
|
+
// 2. a harness marker: the record says the harness wrote it.
|
|
15
|
+
// 3. only for a record with no marker either way, its text: a reserved notice
|
|
16
|
+
// or a frame the harness opens its unmarked records with.
|
|
17
|
+
// A record with nothing of the three is the person's.
|
|
18
|
+
//
|
|
19
|
+
// Measured (local Claude Code corpus; Claude Code 2.1.312 stream-json):
|
|
20
|
+
// - person records carry `origin.kind = "human"` or `promptSource` typed /
|
|
21
|
+
// queued / sdk; no task notification carries either. Claude Code's relay of a
|
|
22
|
+
// person's line into a busy fork ("The user sent a new message while you were
|
|
23
|
+
// working: …") carries both `isMeta` and `origin.kind = "human"` (15): the
|
|
24
|
+
// person's words, so the person's marker decides. `isMeta` records otherwise
|
|
25
|
+
// carry `promptSource = "system"` only (35,680);
|
|
26
|
+
// - task notifications: `origin.kind = "task-notification"` and
|
|
27
|
+
// `promptSource = "system"` on every user record opening `<task-notification>`
|
|
28
|
+
// (1,964); `commandMode = "task-notification"` on every such `queued_command`
|
|
29
|
+
// attachment (1,586, against 926 `commandMode = "prompt"`, each a person's);
|
|
30
|
+
// - local-command caveats and image-coordinate notes: `isMeta` in the
|
|
31
|
+
// transcript (397 caveats);
|
|
32
|
+
// - the image-coordinate note on the live stream-json door: `isSynthetic: true`
|
|
33
|
+
// and no `isMeta` (the transcript stores the same uuid with `isMeta: true`).
|
|
34
|
+
// The stream carries no person's marker at all: it does not echo the person's
|
|
35
|
+
// prompt (the host publishes that itself), so its records reach step 2 or 3;
|
|
36
|
+
// - compact summaries: `isCompactSummary` (with `isVisibleInTranscriptOnly`);
|
|
37
|
+
// - subagent traffic: `isSidechain` in the transcript, `parent_tool_use_id` on
|
|
38
|
+
// the stream; tool results: `toolUseResult` / `tool_use_result` or a
|
|
39
|
+
// `tool_result` block.
|
|
40
|
+
// Unmarked records, whose text is the only evidence (a 400-transcript sample of
|
|
41
|
+
// unmarked main-thread user text: `<command-name>` 18, `<local-command-stdout>`
|
|
42
|
+
// 16, `<bash-input>` 13, `<bash-stdout>` 13, the interruption notice 13, plain 2):
|
|
43
|
+
// - the interruption record, whose whole text is one of Claude's reserved
|
|
44
|
+
// notices (832 and 195 corpus occurrences of the two below);
|
|
45
|
+
// - slash-command and shell-mode plumbing (`<command-name>`, `<command-message>`,
|
|
46
|
+
// `<local-command-stdout>`, `<bash-input>`, `<bash-stdout>`, …): a local
|
|
47
|
+
// command the terminal ran and its output, not a message to the agent;
|
|
48
|
+
// - a task notification on a live adapter path, which can arrive without the
|
|
49
|
+
// persisted stamps (not yet measured on the stream-json door);
|
|
50
|
+
// - context other harnesses and hosts open a user turn with (AGENTS.md / CLAUDE.md
|
|
51
|
+
// instructions, `<environment_context>`, `<system-reminder>`, …) and
|
|
52
|
+
// supercode's own frames (`<supercode-notice>`, a cross-session notice, an
|
|
53
|
+
// edited-file attachment flattened to text).
|
|
54
|
+
// Frames are openers, never substrings: the tag is the part the harness controls,
|
|
55
|
+
// and a person quoting one mid-sentence is still talking.
|
|
56
|
+
|
|
57
|
+
/** `promptSource` values a person's input carries (Claude Code's, and the hosts'
|
|
58
|
+
* that deliver a person's turn: Teams rooms, terminals, apps). */
|
|
59
|
+
export const PERSON_PROMPT_SOURCES = Object.freeze([
|
|
60
|
+
"typed",
|
|
61
|
+
"queued",
|
|
62
|
+
"sdk",
|
|
63
|
+
"user",
|
|
64
|
+
"human",
|
|
65
|
+
"terminal",
|
|
66
|
+
"api",
|
|
67
|
+
"room",
|
|
68
|
+
"app",
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
/** Metadata a host stamps on a turn it delivered for an authenticated person. */
|
|
72
|
+
const PERSON_ATTRIBUTION_KEYS = Object.freeze([
|
|
73
|
+
"source_via",
|
|
74
|
+
"source_principal_id",
|
|
75
|
+
"source_name",
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
/** Attachment subtypes that are an envelope the runtime built around a file
|
|
79
|
+
* body. `attachmentType` is Claude Code's subtype, kept by the session loader. */
|
|
80
|
+
const CONTEXT_ATTACHMENT_TYPES = new Set(["file", "edited_text_file", "nested_memory"]);
|
|
81
|
+
|
|
82
|
+
/** Claude's reserved interruption notices, persisted as unmarked user records. */
|
|
83
|
+
export const CLAUDE_INTERRUPTION_TEXTS = Object.freeze([
|
|
84
|
+
"[Request interrupted by user]",
|
|
85
|
+
"[Request interrupted by user for tool use]",
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
/** The openers of user-role records the harness or host writes with no marker. */
|
|
89
|
+
const HARNESS_FRAMES = Object.freeze([
|
|
90
|
+
/^<(?:command-name|command-message|local-command-[a-z]+|bash-input|bash-stdout|bash-stderr)>/,
|
|
91
|
+
/^<(?:system-reminder|environment_context|user_instructions|user_info|hook_prompt|supercode-notice)\b/,
|
|
92
|
+
/^(?:# )?(?:AGENTS|CLAUDE)\.md instructions for .+\n\n<INSTRUCTIONS>/,
|
|
93
|
+
/^<recommended_plugins>[\s\S]*<\/recommended_plugins>\s*(?:# )?(?:AGENTS|CLAUDE)\.md instructions for .+\n\n<INSTRUCTIONS>/,
|
|
94
|
+
/^\[Cross-session [a-z ]*notice\]/i,
|
|
95
|
+
/^\[edited file:/,
|
|
96
|
+
]);
|
|
97
|
+
const TASK_NOTIFICATION_OPENER = "<task-notification>";
|
|
98
|
+
|
|
99
|
+
const TURN_INTERRUPTED_NOTICE = "Turn interrupted.";
|
|
100
|
+
const NOTICE_FIELD_CHARACTERS = 200;
|
|
101
|
+
|
|
102
|
+
// The loader keeps flags as "true"; a host that kept the native boolean is read the same.
|
|
103
|
+
const flag = (metadata, key) =>
|
|
104
|
+
metadata?.[key] === "true" || metadata?.[key] === true;
|
|
105
|
+
const object = (value) =>
|
|
106
|
+
value !== null && typeof value === "object" && !Array.isArray(value);
|
|
107
|
+
|
|
108
|
+
/** Whether a host delivered this turn for an authenticated person. */
|
|
109
|
+
export function isPersonAttributed(metadata) {
|
|
110
|
+
return PERSON_ATTRIBUTION_KEYS.some((key) => metadata?.[key] != null);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Whether the record carries a person's marker (rule step 1). */
|
|
114
|
+
export function isPersonMarked(metadata) {
|
|
115
|
+
if (!metadata) return false;
|
|
116
|
+
return (
|
|
117
|
+
isPersonAttributed(metadata) ||
|
|
118
|
+
Object.keys(metadata).some((key) => key.startsWith("codex_typed_user")) ||
|
|
119
|
+
PERSON_PROMPT_SOURCES.includes(metadata.promptSource) ||
|
|
120
|
+
metadata.origin === "human" ||
|
|
121
|
+
metadata.commandMode === "prompt"
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const taskNotificationMarked = (metadata) =>
|
|
126
|
+
metadata?.origin === "task-notification" ||
|
|
127
|
+
metadata?.commandMode === "task-notification";
|
|
128
|
+
|
|
129
|
+
/** Whether the record carries a harness marker (rule step 2). */
|
|
130
|
+
export function isHarnessMarked(metadata) {
|
|
131
|
+
if (!metadata) return false;
|
|
132
|
+
return (
|
|
133
|
+
taskNotificationMarked(metadata) ||
|
|
134
|
+
metadata.promptSource === "system" ||
|
|
135
|
+
flag(metadata, "isMeta") ||
|
|
136
|
+
flag(metadata, "isSynthetic") ||
|
|
137
|
+
flag(metadata, "isCompactSummary") ||
|
|
138
|
+
flag(metadata, "isSidechain") ||
|
|
139
|
+
flag(metadata, "toolResult") ||
|
|
140
|
+
CONTEXT_ATTACHMENT_TYPES.has(metadata.attachmentType)
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Whether `text` is, in full, one of Claude's reserved interruption notices. */
|
|
145
|
+
export function isClaudeInterruption(text) {
|
|
146
|
+
return typeof text === "string" && CLAUDE_INTERRUPTION_TEXTS.includes(text.trim());
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Whether `text` opens with a frame the harness or host writes its unmarked
|
|
150
|
+
* user-role records with (rule step 3; a task notification included). */
|
|
151
|
+
export function isHarnessFrame(text) {
|
|
152
|
+
if (typeof text !== "string") return false;
|
|
153
|
+
const body = text.trimStart();
|
|
154
|
+
return (
|
|
155
|
+
body.startsWith(TASK_NOTIFICATION_OPENER) ||
|
|
156
|
+
HARNESS_FRAMES.some((frame) => frame.test(body))
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function taskNotice(text) {
|
|
161
|
+
const tag = (name) => {
|
|
162
|
+
const value =
|
|
163
|
+
new RegExp(`<${name}>([\\s\\S]*?)</${name}>`).exec(text)?.[1]?.trim() ?? "";
|
|
164
|
+
return [...value].length <= NOTICE_FIELD_CHARACTERS
|
|
165
|
+
? value
|
|
166
|
+
: `${[...value].slice(0, NOTICE_FIELD_CHARACTERS - 1).join("")}…`;
|
|
167
|
+
};
|
|
168
|
+
const summary = tag("summary"),
|
|
169
|
+
status = tag("status");
|
|
170
|
+
return `Background task${status ? ` ${status}` : ""}${summary ? `: ${summary}` : ""}`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* The provenance of one user-role record: `metadata` is the string map the
|
|
175
|
+
* session loader keeps (or `claudeRecordProvenance` reads from a native record);
|
|
176
|
+
* `text` is the record's text. `source` is `"person"` for the person's own words
|
|
177
|
+
* and `"harness"` for a record the harness wrote; `notice` is the short plain-text
|
|
178
|
+
* line a reader shows in place of a harness record, where one is shown at all
|
|
179
|
+
* (each renderer styles it; none shows the record's own text).
|
|
180
|
+
* @param {Record<string, string> | null | undefined} metadata
|
|
181
|
+
* @param {string} [text]
|
|
182
|
+
* @returns {{ source: "person" | "harness", notice?: string }}
|
|
183
|
+
*/
|
|
184
|
+
export function userProvenance(metadata, text = "") {
|
|
185
|
+
const body = typeof text === "string" ? text : "";
|
|
186
|
+
if (isPersonMarked(metadata)) return { source: "person" };
|
|
187
|
+
if (taskNotificationMarked(metadata))
|
|
188
|
+
return { source: "harness", notice: taskNotice(body) };
|
|
189
|
+
if (isHarnessMarked(metadata)) return { source: "harness" };
|
|
190
|
+
if (body.trimStart().startsWith(TASK_NOTIFICATION_OPENER))
|
|
191
|
+
return { source: "harness", notice: taskNotice(body) };
|
|
192
|
+
if (isClaudeInterruption(body))
|
|
193
|
+
return { source: "harness", notice: TURN_INTERRUPTED_NOTICE };
|
|
194
|
+
if (isHarnessFrame(body)) return { source: "harness" };
|
|
195
|
+
return { source: "person" };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Whether a user-role record is the harness's own, not the person's words. */
|
|
199
|
+
export function isHarnessAuthoredUser(metadata, text = "") {
|
|
200
|
+
return userProvenance(metadata, text).source === "harness";
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* The rule's metadata read from one whole native Claude Code `user` record, as
|
|
205
|
+
* the stream-json door emits it or the transcript stores it. Reads the record,
|
|
206
|
+
* never one content block of it: the stamps sit beside `message`.
|
|
207
|
+
* @param {unknown} record
|
|
208
|
+
* @returns {Record<string, string>}
|
|
209
|
+
*/
|
|
210
|
+
export function claudeRecordProvenance(record) {
|
|
211
|
+
const metadata = {};
|
|
212
|
+
if (!object(record)) return metadata;
|
|
213
|
+
if (typeof record.promptSource === "string")
|
|
214
|
+
metadata.promptSource = record.promptSource;
|
|
215
|
+
for (const key of ["isMeta", "isSynthetic", "isCompactSummary", "isSidechain"])
|
|
216
|
+
if (record[key] === true) metadata[key] = "true";
|
|
217
|
+
if (typeof record.parent_tool_use_id === "string")
|
|
218
|
+
metadata.isSidechain = "true";
|
|
219
|
+
if (object(record.origin) && typeof record.origin.kind === "string")
|
|
220
|
+
metadata.origin = record.origin.kind;
|
|
221
|
+
const content = object(record.message) ? record.message.content : undefined;
|
|
222
|
+
if (
|
|
223
|
+
Object.hasOwn(record, "toolUseResult") ||
|
|
224
|
+
Object.hasOwn(record, "tool_use_result") ||
|
|
225
|
+
(Array.isArray(content) &&
|
|
226
|
+
content.some((block) => object(block) && block.type === "tool_result"))
|
|
227
|
+
)
|
|
228
|
+
metadata.toolResult = "true";
|
|
229
|
+
return metadata;
|
|
230
|
+
}
|