@xingular/harness 1.3.1-unstable.2 → 1.3.1-unstable.3
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/CHANGELOG.md +49 -0
- package/README.md +1 -1
- package/dist/cli.js +1217 -701
- package/dist/sdk/computer/screenshot.d.ts +32 -0
- package/dist/sdk/computer/screenshot.js +35 -0
- package/dist/sdk/context/attribution.d.ts +18 -0
- package/dist/sdk/context/attribution.js +295 -0
- package/dist/sdk/context/types.d.ts +94 -0
- package/dist/sdk/context/types.js +10 -0
- package/dist/sdk/index.d.ts +2 -1
- package/dist/sdk/index.js +2 -1
- package/dist/sdk/plugins/builtin.js +1 -0
- package/dist/sdk/prompt/compile.d.ts +42 -2
- package/dist/sdk/prompt/compile.js +87 -15
- package/dist/sdk/server/methods.js +3 -0
- package/dist/sdk/sessions/contracts.d.ts +4 -0
- package/dist/sdk/sessions/contracts.js +2 -0
- package/dist/sdk/sessions/runner.d.ts +10 -0
- package/dist/sdk/sessions/runner.js +17 -0
- package/dist/sdk/sessions/subagentService.d.ts +11 -1
- package/dist/sdk/sessions/subagentService.js +120 -38
- package/dist/sdk/sessions/subagents.d.ts +31 -0
- package/dist/sdk/sessions/subagents.js +45 -0
- package/dist/sdk/terminal/client.d.ts +1 -1
- package/dist/sdk/terminal/client.js +13 -6
- package/dist/sdk/terminal/control.d.ts +1 -1
- package/dist/sdk/terminal/control.js +2 -2
- package/dist/sdk/terminal/controller.d.ts +9 -0
- package/dist/sdk/terminal/controller.js +48 -11
- package/dist/sdk/terminal/piExtension.js +35 -1
- package/dist/sdk/terminal/terminals.d.ts +2 -0
- package/dist/sdk/terminal/terminals.js +11 -0
- package/dist/sdk/terminal/tui/commands.d.ts +1 -2
- package/dist/sdk/terminal/tui/commands.js +1 -4
- package/dist/sdk/terminal/tui/dialogs.d.ts +3 -2
- package/dist/sdk/terminal/tui/dialogs.js +4 -3
- package/dist/sdk/terminal/tui/keybindings.d.ts +1 -1
- package/dist/sdk/terminal/tui/keybindings.js +1 -1
- package/dist/sdk/terminal/tui/modeChoices.d.ts +3 -2
- package/dist/sdk/terminal/tui/modeChoices.js +4 -4
- package/dist/sdk/terminal/tui/mouseReport.d.ts +30 -0
- package/dist/sdk/terminal/tui/mouseReport.js +80 -0
- package/dist/sdk/terminal/tui/pages/annotatePage.js +3 -2
- package/dist/sdk/terminal/tui/pages/browserPage.d.ts +1 -1
- package/dist/sdk/terminal/tui/pages/contextPage.d.ts +54 -0
- package/dist/sdk/terminal/tui/pages/contextPage.js +393 -0
- package/dist/sdk/terminal/tui/pages/trajectoryPage.js +5 -4
- package/dist/sdk/terminal/tui/runtime.js +8 -2
- package/dist/sdk/terminal/tui/search.js +3 -2
- package/dist/sdk/terminal/tui/skillInvocation.d.ts +21 -2
- package/dist/sdk/terminal/tui/skillInvocation.js +31 -11
- package/dist/sdk/tools/defaultSkills.d.ts +19 -0
- package/dist/sdk/tools/defaultSkills.js +27 -1
- package/dist/sdk/tools/dispatch.js +8 -9
- package/dist/sdk/tools/spawnSubagent.d.ts +1 -1
- package/dist/sdk/tools/spawnSubagent.js +4 -2
- package/dist/terminal-opentui.js +570 -240
- package/dist/terminal-pi.js +142 -28
- package/docs/README.md +1 -0
- package/docs/architecture/02-sessions-and-runs.md +9 -0
- package/docs/architecture/04-prompt.md +13 -4
- package/docs/architecture/07-tools-and-gate.md +1 -1
- package/docs/architecture/11-terminal.md +12 -2
- package/docs/architecture/13-world-tools.md +17 -3
- package/docs/architecture/14-surfaces-and-parity.md +3 -3
- package/docs/architecture/README.md +1 -0
- package/docs/architecture/proposals/PROP-0049-context-attribution.md +382 -0
- package/docs/architecture/proposals/README.md +1 -0
- package/docs/decisions/ADR-0048-required-skills-from-a-message.md +42 -0
- package/docs/decisions/ADR-0050-approved-window-screenshots.md +19 -0
- package/docs/decisions/ADR-0051-terminal-decision-wait.md +10 -0
- package/docs/decisions/ADR-0052-per-profile-subagent-budgets.md +25 -0
- package/docs/parity.md +6 -6
- package/docs/releases/1.3.1-unstable.3.md +54 -0
- package/docs/terminal-tui.md +28 -15
- package/package.json +1 -1
|
@@ -46,18 +46,18 @@ function putIdle(path, stream) {
|
|
|
46
46
|
entries.add(entry);
|
|
47
47
|
idle.set(path, entries);
|
|
48
48
|
}
|
|
49
|
-
function request(connection, body, what, timeoutMs) {
|
|
49
|
+
function request(connection, body, what, timeoutMs, signal) {
|
|
50
50
|
const reused = takeIdle(connection.socket);
|
|
51
|
-
return attempt(connection, body, what, timeoutMs, reused).catch((error) => {
|
|
51
|
+
return attempt(connection, body, what, timeoutMs, reused, signal).catch((error) => {
|
|
52
52
|
// An idle connection the harness had already closed fails before any reply; one fresh retry.
|
|
53
53
|
if (reused && error instanceof StaleConnection)
|
|
54
|
-
return attempt(connection, body, what, timeoutMs, undefined);
|
|
54
|
+
return attempt(connection, body, what, timeoutMs, undefined, signal);
|
|
55
55
|
throw error;
|
|
56
56
|
});
|
|
57
57
|
}
|
|
58
58
|
class StaleConnection extends Error {
|
|
59
59
|
}
|
|
60
|
-
function attempt(connection, body, what, timeoutMs, reused) {
|
|
60
|
+
function attempt(connection, body, what, timeoutMs, reused, signal) {
|
|
61
61
|
return new Promise((resolve, reject) => {
|
|
62
62
|
const stream = reused ?? createConnection(connection.socket);
|
|
63
63
|
let buffer = '';
|
|
@@ -91,6 +91,7 @@ function attempt(connection, body, what, timeoutMs, reused) {
|
|
|
91
91
|
return;
|
|
92
92
|
settled = true;
|
|
93
93
|
clearTimeout(timer);
|
|
94
|
+
signal?.removeEventListener('abort', onAbort);
|
|
94
95
|
stream.off('connect', send);
|
|
95
96
|
stream.off('data', onData);
|
|
96
97
|
stream.off('error', onError);
|
|
@@ -107,6 +108,12 @@ function attempt(connection, body, what, timeoutMs, reused) {
|
|
|
107
108
|
stream.destroy();
|
|
108
109
|
finish(() => reject(new SocketError('timeout', `The harness did not answer ${what}.`)));
|
|
109
110
|
}, timeoutMs);
|
|
111
|
+
const onAbort = () => { stream.destroy(); finish(() => reject(new SocketError('aborted', `${what} was cancelled.`))); };
|
|
112
|
+
if (signal?.aborted) {
|
|
113
|
+
onAbort();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
110
117
|
stream.on('data', onData);
|
|
111
118
|
stream.on('error', onError);
|
|
112
119
|
stream.on('close', onClose);
|
|
@@ -117,8 +124,8 @@ function attempt(connection, body, what, timeoutMs, reused) {
|
|
|
117
124
|
});
|
|
118
125
|
}
|
|
119
126
|
/** A memory, Wiki, recall, or connected-app call made on behalf of the session that owns the token. */
|
|
120
|
-
export function socketCall(connection, call, params = {}, timeoutMs = 120_000, control) {
|
|
121
|
-
return request(connection, { call, params, ...(control ? { control } : {}) }, call, timeoutMs);
|
|
127
|
+
export function socketCall(connection, call, params = {}, timeoutMs = 120_000, control, signal) {
|
|
128
|
+
return request(connection, { call, params, ...(control ? { control } : {}) }, call, timeoutMs, signal);
|
|
122
129
|
}
|
|
123
130
|
/** Telemetry and decisions from the native `pi` process. Approvals may wait on a person, so the default wait is long. */
|
|
124
131
|
export function terminalCall(event, data = {}, timeoutMs = 600_000) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { type SocketConnection } from "./client.js";
|
|
2
2
|
/** A process owns the claim across native session replacement; it never enters the environment. */
|
|
3
3
|
export declare function claimTerminalControl(connection?: SocketConnection): Promise<string>;
|
|
4
|
-
export declare function controlCall<T = unknown>(call: string, params?: Record<string, unknown>, timeoutMs?: number): Promise<T>;
|
|
4
|
+
export declare function controlCall<T = unknown>(call: string, params?: Record<string, unknown>, timeoutMs?: number, signal?: AbortSignal): Promise<T>;
|
|
@@ -18,8 +18,8 @@ export function claimTerminalControl(connection = connectionFromEnv()) {
|
|
|
18
18
|
}
|
|
19
19
|
return claim;
|
|
20
20
|
}
|
|
21
|
-
export async function controlCall(call, params = {}, timeoutMs = 120_000) {
|
|
21
|
+
export async function controlCall(call, params = {}, timeoutMs = 120_000, signal) {
|
|
22
22
|
const connection = connectionFromEnv();
|
|
23
23
|
const control = await claimTerminalControl(connection);
|
|
24
|
-
return socketCall(connection, call, params, timeoutMs, control);
|
|
24
|
+
return signal ? socketCall(connection, call, params, timeoutMs, control, signal) : socketCall(connection, call, params, timeoutMs, control);
|
|
25
25
|
}
|
|
@@ -57,7 +57,10 @@ export interface ControllerHooks {
|
|
|
57
57
|
export declare class TerminalController {
|
|
58
58
|
private readonly hooks;
|
|
59
59
|
private run;
|
|
60
|
+
private lastRunId;
|
|
60
61
|
private readonly pending;
|
|
62
|
+
private pendingRevision;
|
|
63
|
+
private readonly pendingWaiters;
|
|
61
64
|
private readonly subagents;
|
|
62
65
|
private subagentParentRunId;
|
|
63
66
|
private ready;
|
|
@@ -71,8 +74,14 @@ export declare class TerminalController {
|
|
|
71
74
|
private readonly seen;
|
|
72
75
|
private readonly commands;
|
|
73
76
|
constructor(hooks: ControllerHooks);
|
|
77
|
+
private pendingChanged;
|
|
78
|
+
private setPending;
|
|
79
|
+
private deletePending;
|
|
80
|
+
private waitPending;
|
|
74
81
|
get nativeSessionId(): string | null;
|
|
75
82
|
get activeRun(): ActiveRun | null;
|
|
83
|
+
/** The run this terminal is showing, or the last one it finished: `/context` reads it after a reply. */
|
|
84
|
+
get lastRun(): string | null;
|
|
76
85
|
state(): {
|
|
77
86
|
ready: boolean;
|
|
78
87
|
dirty: boolean;
|
|
@@ -26,7 +26,10 @@ const ACK_TIMEOUT_MS = 25_000;
|
|
|
26
26
|
export class TerminalController {
|
|
27
27
|
hooks;
|
|
28
28
|
run = null;
|
|
29
|
+
lastRunId = null;
|
|
29
30
|
pending = new Map();
|
|
31
|
+
pendingRevision = 0;
|
|
32
|
+
pendingWaiters = new Set();
|
|
30
33
|
subagents = new SubagentMonitor();
|
|
31
34
|
subagentParentRunId = null;
|
|
32
35
|
ready = false;
|
|
@@ -43,12 +46,39 @@ export class TerminalController {
|
|
|
43
46
|
this.hooks = hooks;
|
|
44
47
|
this.nativeId = hooks.nativeSessionId;
|
|
45
48
|
}
|
|
49
|
+
pendingChanged() {
|
|
50
|
+
this.pendingRevision += 1;
|
|
51
|
+
for (const wake of this.pendingWaiters)
|
|
52
|
+
wake();
|
|
53
|
+
}
|
|
54
|
+
setPending(key, value) {
|
|
55
|
+
this.pending.set(key, value);
|
|
56
|
+
this.pendingChanged();
|
|
57
|
+
}
|
|
58
|
+
deletePending(key) {
|
|
59
|
+
if (this.pending.delete(key))
|
|
60
|
+
this.pendingChanged();
|
|
61
|
+
}
|
|
62
|
+
async waitPending(revision) {
|
|
63
|
+
if (revision === this.pendingRevision && !this.disposed) {
|
|
64
|
+
await new Promise(resolve => {
|
|
65
|
+
const wake = () => { clearTimeout(timer); this.pendingWaiters.delete(wake); resolve(); };
|
|
66
|
+
const timer = setTimeout(wake, 20_000);
|
|
67
|
+
this.pendingWaiters.add(wake);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return { revision: this.pendingRevision, requests: [...this.pending.values()] };
|
|
71
|
+
}
|
|
46
72
|
get nativeSessionId() {
|
|
47
73
|
return this.nativeId;
|
|
48
74
|
}
|
|
49
75
|
get activeRun() {
|
|
50
76
|
return this.run && !this.run.finished ? this.run : null;
|
|
51
77
|
}
|
|
78
|
+
/** The run this terminal is showing, or the last one it finished: `/context` reads it after a reply. */
|
|
79
|
+
get lastRun() {
|
|
80
|
+
return this.activeRun?.runId ?? this.lastRunId;
|
|
81
|
+
}
|
|
52
82
|
state() {
|
|
53
83
|
return { ready: this.ready, dirty: this.dirty, active: !!this.activeRun || !!this.delivery };
|
|
54
84
|
}
|
|
@@ -67,6 +97,7 @@ export class TerminalController {
|
|
|
67
97
|
throw new Error('The native prompt did not match the reserved delivery. Inspect the terminal before continuing.');
|
|
68
98
|
const run = this.hooks.begin({ reservedRunId: delivery?.runId ?? null, nativeTurnId, text, ...launch });
|
|
69
99
|
this.run = run;
|
|
100
|
+
this.lastRunId = run.runId;
|
|
70
101
|
this.subagentParentRunId = run.runId;
|
|
71
102
|
this.subagents.begin(run.runId);
|
|
72
103
|
run.onInterrupt(() => void this.commands.push({ type: 'interrupt' }));
|
|
@@ -80,7 +111,10 @@ export class TerminalController {
|
|
|
80
111
|
this.run?.finish(outcome, message ? { code: 'terminal_attention', message } : undefined, reason);
|
|
81
112
|
this.run = null;
|
|
82
113
|
this.seen.clear();
|
|
83
|
-
this.pending.
|
|
114
|
+
if (this.pending.size) {
|
|
115
|
+
this.pending.clear();
|
|
116
|
+
this.pendingChanged();
|
|
117
|
+
}
|
|
84
118
|
}
|
|
85
119
|
/** Hands the native process an instruction and waits until it acknowledges the prompt. */
|
|
86
120
|
async send(runId, text) {
|
|
@@ -145,19 +179,19 @@ export class TerminalController {
|
|
|
145
179
|
return;
|
|
146
180
|
if (event.type === 'question.requested') {
|
|
147
181
|
const payload = event.payload;
|
|
148
|
-
this.
|
|
182
|
+
this.setPending(String(payload.questionId), { ...payload, type: 'question', requestId: payload.questionId, runId: event.runId });
|
|
149
183
|
}
|
|
150
184
|
else if (event.type === 'approval.requested') {
|
|
151
185
|
const payload = event.payload;
|
|
152
186
|
if (!payload.automatic && (descendant || payload.target === 'workspace-write')) {
|
|
153
|
-
this.
|
|
187
|
+
this.setPending(String(payload.approvalId), { ...payload, type: 'approval', requestId: payload.approvalId, runId: event.runId });
|
|
154
188
|
}
|
|
155
189
|
}
|
|
156
190
|
else if (event.type === 'approval.resolved') {
|
|
157
|
-
this.
|
|
191
|
+
this.deletePending(String(event.payload.approvalId));
|
|
158
192
|
}
|
|
159
193
|
else if (event.type === 'question.answered') {
|
|
160
|
-
this.
|
|
194
|
+
this.deletePending(String(event.payload.questionId));
|
|
161
195
|
}
|
|
162
196
|
}
|
|
163
197
|
/** A child event that belongs to the active terminal parent run. */
|
|
@@ -197,6 +231,8 @@ export class TerminalController {
|
|
|
197
231
|
}
|
|
198
232
|
if (call === 'terminal.pending')
|
|
199
233
|
return { requests: [...this.pending.values()] };
|
|
234
|
+
if (call === 'terminal.pending.wait')
|
|
235
|
+
return this.waitPending(data.revision);
|
|
200
236
|
if (call === 'terminal.interrupt')
|
|
201
237
|
return this.activeRun && this.hooks.interrupt ? this.hooks.interrupt(this.activeRun.runId) : {};
|
|
202
238
|
if (call === 'terminal.policy') {
|
|
@@ -292,7 +328,7 @@ export class TerminalController {
|
|
|
292
328
|
rememberable: policy.rememberable === true,
|
|
293
329
|
standing: policy.standing
|
|
294
330
|
});
|
|
295
|
-
this.
|
|
331
|
+
this.setPending(key, {
|
|
296
332
|
type: 'approval', requestId: key, runId: run.runId, target: tool, action,
|
|
297
333
|
operation, rememberable: policy.rememberable === true, direct: true
|
|
298
334
|
});
|
|
@@ -301,7 +337,7 @@ export class TerminalController {
|
|
|
301
337
|
return { decision: await response };
|
|
302
338
|
}
|
|
303
339
|
finally {
|
|
304
|
-
this.
|
|
340
|
+
this.deletePending(key);
|
|
305
341
|
}
|
|
306
342
|
}
|
|
307
343
|
async accept(event, data) {
|
|
@@ -380,9 +416,9 @@ export class TerminalController {
|
|
|
380
416
|
const summary = typeof data.summary === 'string' && data.summary.trim() ? data.summary.trim() : undefined;
|
|
381
417
|
const sketchMode = data.uiSketch === undefined ? {} : { uiSketch: data.uiSketch };
|
|
382
418
|
const response = run.requestAnswer({ requestId: key, ...(summary ? { summary } : {}), ...sketchMode, question: String(data.question ?? ''), options: data.options ?? [], multiSelect: !!data.multiSelect, allowText: true });
|
|
383
|
-
this.
|
|
419
|
+
this.setPending(key, { type: 'question', requestId: key, ...(summary ? { summary } : {}), ...sketchMode, question: String(data.question ?? ''), options: data.options ?? [], multiSelect: !!data.multiSelect });
|
|
384
420
|
this.seen.set(key, response);
|
|
385
|
-
return response.finally(() => this.
|
|
421
|
+
return response.finally(() => this.deletePending(key));
|
|
386
422
|
}
|
|
387
423
|
const response = run.requestApproval({
|
|
388
424
|
requestId: key,
|
|
@@ -391,13 +427,13 @@ export class TerminalController {
|
|
|
391
427
|
operation: { tool: String(data.tool ?? data.target), args: data.args ?? {} },
|
|
392
428
|
rememberable: false
|
|
393
429
|
});
|
|
394
|
-
this.
|
|
430
|
+
this.setPending(key, { type: 'approval', requestId: key, target: String(data.target ?? 'Action'), action: String(data.action ?? '{}') });
|
|
395
431
|
this.seen.set(key, response);
|
|
396
432
|
try {
|
|
397
433
|
return { decision: await response };
|
|
398
434
|
}
|
|
399
435
|
finally {
|
|
400
|
-
this.
|
|
436
|
+
this.deletePending(key);
|
|
401
437
|
}
|
|
402
438
|
}
|
|
403
439
|
if (event === 'usage')
|
|
@@ -425,6 +461,7 @@ export class TerminalController {
|
|
|
425
461
|
if (this.disposed)
|
|
426
462
|
return;
|
|
427
463
|
this.disposed = true;
|
|
464
|
+
this.pendingChanged();
|
|
428
465
|
this.ready = false;
|
|
429
466
|
this.delivery?.reject(new Error('The terminal stopped before prompt acknowledgement.'));
|
|
430
467
|
this.delivery = null;
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
// session does to the harness, asks before each tool call, takes instructions the harness
|
|
3
3
|
// queues for it, and gives the session the harness's own tools. It runs in the `pi` process,
|
|
4
4
|
// so everything it knows about the harness comes through the session socket.
|
|
5
|
+
import { readFileSync } from 'node:fs';
|
|
5
6
|
import { createBashTool, createEditTool, createPowerShellTool, createWriteTool } from '@earendil-works/pi-coding-agent';
|
|
6
7
|
import { Type } from 'typebox';
|
|
8
|
+
import { bodyOf } from "../core/frontmatter.js";
|
|
7
9
|
import { connectionFromEnv, socketCall, terminalCall } from "./client.js";
|
|
8
10
|
import { controlCall } from "./control.js";
|
|
9
11
|
import { SPAWN_SUBAGENT, SUBAGENT_OWNER, SUBAGENT_RECEIPT, SUBAGENT_RESULTS, subagentHistory } from "../sessions/subagents.js";
|
|
@@ -17,9 +19,10 @@ import { SESSION_NAME, deriveSessionName, sessionNameTool } from "../tools/sessi
|
|
|
17
19
|
import { CAPABILITY_DISCOVERY, describeCapabilities } from "../tools/getCapabilities.js";
|
|
18
20
|
import { PLAN_INSTRUCTIONS, planRefusal } from "../sessions/planMode.js";
|
|
19
21
|
import { compactionCheckpoint } from "../sessions/compactionCheckpoint.js";
|
|
20
|
-
import { sessionStateText } from "../prompt/compile.js";
|
|
22
|
+
import { requiredSkillsText, sessionStateText } from "../prompt/compile.js";
|
|
21
23
|
import { pruneToolImages } from "../world/images.js";
|
|
22
24
|
import { parseInitialBashCommand } from "../sessions/initialBash.js";
|
|
25
|
+
import { skillInvocations } from "./tui/skillInvocation.js";
|
|
23
26
|
import { inspectWorkspaceWrite } from "../sessions/workspaceBoundary.js";
|
|
24
27
|
import { createWorkspaceShellTool } from "../core/workspaceBashTool.js";
|
|
25
28
|
import { createWorkspaceBashOperations, runWithWorkspaceWriteScope } from "../core/workspaceSandbox.js";
|
|
@@ -298,6 +301,37 @@ export default async function (pi, planRun, getSession) {
|
|
|
298
301
|
if (notice)
|
|
299
302
|
return { message: { customType: SUBAGENT_RESULTS, content: notice.text, display: false, details: { [SUBAGENT_RECEIPT]: notice.receipt } } };
|
|
300
303
|
});
|
|
304
|
+
// A `$name` anywhere in the message makes that skill required for this request. The skill's text is
|
|
305
|
+
// delivered as a hidden request-scoped message, so the person's own wording reaches the model unchanged.
|
|
306
|
+
pi.on('before_agent_start', async (event) => {
|
|
307
|
+
if (event.messageType !== 'user' || /^\s*!/u.test(event.prompt))
|
|
308
|
+
return;
|
|
309
|
+
if (pi.getSettings?.().enableSkillCommands === false)
|
|
310
|
+
return;
|
|
311
|
+
const skills = (pi.getCommands?.() ?? [])
|
|
312
|
+
.filter((command) => command.source === 'skill')
|
|
313
|
+
.map((command) => ({
|
|
314
|
+
name: command.name.replace(/^skill:/, ''),
|
|
315
|
+
path: command.sourceInfo.path,
|
|
316
|
+
...(command.sourceInfo.baseDir ? { baseDir: command.sourceInfo.baseDir } : {})
|
|
317
|
+
}));
|
|
318
|
+
if (!skills.length)
|
|
319
|
+
return;
|
|
320
|
+
const names = skillInvocations(event.prompt, new Set(skills.map((skill) => skill.name)));
|
|
321
|
+
if (!names.length)
|
|
322
|
+
return;
|
|
323
|
+
const entries = names.map((name) => {
|
|
324
|
+
const skill = skills.find((candidate) => candidate.name === name);
|
|
325
|
+
try {
|
|
326
|
+
return { name, path: skill.path, baseDir: skill.baseDir, body: bodyOf(readFileSync(skill.path, 'utf8')) };
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
return { name, path: skill.path, body: '' };
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
const content = requiredSkillsText(entries);
|
|
333
|
+
return content ? { message: { customType: 'xingular-required-skills', content, display: false, details: { skills: names } } } : undefined;
|
|
334
|
+
});
|
|
301
335
|
// A new user turn starts with an empty todo. The previous list is context, not this run's list.
|
|
302
336
|
// A substantive first turn also gets a provisional name when the terminal policy allows it.
|
|
303
337
|
pi.on('before_agent_start', async (event, ctx) => {
|
|
@@ -198,6 +198,8 @@ export declare class Terminals {
|
|
|
198
198
|
private agentsCall;
|
|
199
199
|
/** `/worktree`: the managed worktrees of the repository this session works in. */
|
|
200
200
|
private worktreesCall;
|
|
201
|
+
/** `/context`: what the session's latest request was made of, and one row's text on request. */
|
|
202
|
+
private contextCall;
|
|
201
203
|
/** `/usage:analysis` and `/usage:limit`: usage for this session's own account, project or session. */
|
|
202
204
|
private usageCall;
|
|
203
205
|
/** `/annotate:notes` and `/annotate:`: Web annotations and their visible capture browser. */
|
|
@@ -21,6 +21,7 @@ import { defaultSkillPaths } from "../tools/defaultSkills.js";
|
|
|
21
21
|
import { materialise } from "../variant/materialise.js";
|
|
22
22
|
import * as componentLibrary from "../variant/components.js";
|
|
23
23
|
import * as variantStore from "../variant/store.js";
|
|
24
|
+
import { attribution, rowText } from "../context/attribution.js";
|
|
24
25
|
import { compile, copyToRun, record, render, saveForSession } from "../prompt/compile.js";
|
|
25
26
|
import { isActiveStatus } from "../sessions/contracts.js";
|
|
26
27
|
import { findRun, loadRun, readEvents, recordedSubagents, saveRun, updateRun } from "../sessions/runStore.js";
|
|
@@ -575,6 +576,14 @@ export class Terminals {
|
|
|
575
576
|
throw new HarnessError('unknown_method', `Unknown call ${call}.`);
|
|
576
577
|
}
|
|
577
578
|
}
|
|
579
|
+
/** `/context`: what the session's latest request was made of, and one row's text on request. */
|
|
580
|
+
contextCall(live, params) {
|
|
581
|
+
const runId = live.controller.lastRun;
|
|
582
|
+
if (!runId)
|
|
583
|
+
return { attribution: null, text: null };
|
|
584
|
+
const text = typeof params.section === 'string' && params.section ? rowText(this.options.env, runId, params.section) : null;
|
|
585
|
+
return { attribution: attribution(this.options.env, runId), text };
|
|
586
|
+
}
|
|
578
587
|
/** `/usage:analysis` and `/usage:limit`: usage for this session's own account, project or session. */
|
|
579
588
|
async usageCall(live, call, params) {
|
|
580
589
|
const usage = this.options.usage;
|
|
@@ -1290,6 +1299,8 @@ export class Terminals {
|
|
|
1290
1299
|
}
|
|
1291
1300
|
if (call.startsWith('terminal.usage.'))
|
|
1292
1301
|
return this.usageCall(live, call, params);
|
|
1302
|
+
if (call === 'terminal.context')
|
|
1303
|
+
return this.contextCall(live, params);
|
|
1293
1304
|
if (call !== 'terminal.resources')
|
|
1294
1305
|
return live.controller.control(call, params);
|
|
1295
1306
|
const current = await this.options.plugins?.loading({ cwd: input.cwd, agentDir: configDir(accounts.dir, preparedAccount), install: false, caller: { projectId: input.projectId ?? null } }) ?? null;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type AgentSession, type ExtensionUIContext } from '@earendil-works/pi-coding-agent';
|
|
2
|
-
import {
|
|
2
|
+
import type { SkillEntry } from "./skillInvocation.js";
|
|
3
3
|
import type { TerminalRuntime } from "./runtime.js";
|
|
4
4
|
import type { TerminalTrajectoryRun, TerminalTrajectorySnapshot } from "../trajectory.js";
|
|
5
5
|
import type { TodoEntry } from "../opentui/model.js";
|
|
@@ -47,6 +47,5 @@ export declare function createTerminalCommands(host: TerminalCommandHost): {
|
|
|
47
47
|
execute: (text: string) => Promise<boolean>;
|
|
48
48
|
completions: () => TerminalCommandCompletion[];
|
|
49
49
|
skills: () => SkillEntry[];
|
|
50
|
-
expandSkill: (text: string) => string;
|
|
51
50
|
legacyAliases: () => string[];
|
|
52
51
|
};
|
|
@@ -10,7 +10,6 @@ import { TreePicker } from "./treePicker.js";
|
|
|
10
10
|
import { createCommandPage } from "./commandPages.js";
|
|
11
11
|
import { pageOverlayOptions } from "./dialogGeometry.js";
|
|
12
12
|
import { ResumeSessionsPage } from "./sessionPicker.js";
|
|
13
|
-
import { expandSkillInvocation } from "./skillInvocation.js";
|
|
14
13
|
import { groupTerminalCommands } from "../commandHelp.js";
|
|
15
14
|
import { createBuiltinPluginCommandHandlers } from "./pluginCommands.js";
|
|
16
15
|
export { parseAnnotate } from "./pluginCommands.js";
|
|
@@ -43,8 +42,6 @@ export function createTerminalCommands(host) {
|
|
|
43
42
|
return [];
|
|
44
43
|
return session.resourceLoader.getSkills().skills.map((skill) => ({ name: skill.name, description: skill.description }));
|
|
45
44
|
};
|
|
46
|
-
/** The text to send: a leading `$name` of a loaded skill becomes the `/skill:name` form Pi expands. */
|
|
47
|
-
const expandSkill = (text) => expandSkillInvocation(text, new Set(skills().map((skill) => skill.name)));
|
|
48
45
|
async function openPage(id, options = {}) {
|
|
49
46
|
let close = () => { };
|
|
50
47
|
const page = createCommandPage(host, id, { ...options, close: () => close() });
|
|
@@ -486,5 +483,5 @@ export function createTerminalCommands(host) {
|
|
|
486
483
|
}
|
|
487
484
|
return true;
|
|
488
485
|
}
|
|
489
|
-
return { execute, completions, skills,
|
|
486
|
+
return { execute, completions, skills, legacyAliases };
|
|
490
487
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ExtensionUIContext, ExtensionUIDialogOptions, KeybindingsManager, Theme } from '@earendil-works/pi-coding-agent';
|
|
2
|
-
import {
|
|
2
|
+
import { type Component, type Focusable, type SelectListTheme, type TUI, type TuiMouseEventResult } from '@earendil-works/pi-tui';
|
|
3
3
|
import type { TerminalMouseInput } from "./mouse.js";
|
|
4
|
+
import { MouseSafeInput } from "./mouseReport.js";
|
|
4
5
|
export interface GroupedSelectOption {
|
|
5
6
|
label: string;
|
|
6
7
|
group?: string;
|
|
@@ -12,7 +13,7 @@ export type TerminalDialogOptions = ExtensionUIDialogOptions & {
|
|
|
12
13
|
export type DialogVariant = 'block' | 'page';
|
|
13
14
|
export declare function selectTheme(theme: Theme): SelectListTheme;
|
|
14
15
|
/** Input keeps the original value; only its rendered cells are masked. */
|
|
15
|
-
export declare class SecretInput extends
|
|
16
|
+
export declare class SecretInput extends MouseSafeInput {
|
|
16
17
|
render(width: number): string[];
|
|
17
18
|
}
|
|
18
19
|
/** Choices as a `FilterList`, with the standard search field visible for every list. */
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { Editor,
|
|
1
|
+
import { Editor, matchesKey, stripTerminalSequences, truncateToWidth, visibleWidth, wrapTextWithAnsi } from '@earendil-works/pi-tui';
|
|
2
2
|
import { CENTER_DIALOG_PAD, CHROME_ROWS, blockFrame, centerDialogChromeRows, centerDialogContentWidth, centerDialogFrame, centerDialogVerticalPad, contentWidth, pageFrame, safeText, selectionText } from "./chrome.js";
|
|
3
3
|
import { centeredDialogHost, dialogFits, pageOverlayOptions, pageRows } from "./dialogGeometry.js";
|
|
4
4
|
import { FilterList } from "./list.js";
|
|
5
5
|
import { searchFieldRowsFor } from "./search.js";
|
|
6
|
+
import { MouseSafeInput } from "./mouseReport.js";
|
|
6
7
|
export function selectTheme(theme) {
|
|
7
8
|
return {
|
|
8
9
|
selectedPrefix: text => selectionText(theme, text), selectedText: text => selectionText(theme, text.replace(/^→/, '❯')),
|
|
@@ -11,7 +12,7 @@ export function selectTheme(theme) {
|
|
|
11
12
|
};
|
|
12
13
|
}
|
|
13
14
|
/** Input keeps the original value; only its rendered cells are masked. */
|
|
14
|
-
export class SecretInput extends
|
|
15
|
+
export class SecretInput extends MouseSafeInput {
|
|
15
16
|
render(width) {
|
|
16
17
|
const lines = super.render(width);
|
|
17
18
|
if (!this.getValue())
|
|
@@ -280,7 +281,7 @@ export class TerminalDialogs {
|
|
|
280
281
|
}
|
|
281
282
|
input(title, placeholder, opts, secret = false) {
|
|
282
283
|
return this.show(done => {
|
|
283
|
-
const InputType = secret ? SecretInput :
|
|
284
|
+
const InputType = secret ? SecretInput : MouseSafeInput;
|
|
284
285
|
const input = new InputType({ prompt: secret ? '' : '> ', placeholder, placeholderStyle: text => this.theme().fg('dim', text) });
|
|
285
286
|
input.onSubmit = done;
|
|
286
287
|
input.onEscape = () => done(undefined);
|
|
@@ -4,7 +4,7 @@ declare module '@earendil-works/pi-tui' {
|
|
|
4
4
|
interface Keybindings {
|
|
5
5
|
'app.thinking.increase': true;
|
|
6
6
|
'app.thinking.decrease': true;
|
|
7
|
-
'app.message.
|
|
7
|
+
'app.message.steer': true;
|
|
8
8
|
}
|
|
9
9
|
}
|
|
10
10
|
export declare const HARNESS_KEYBINDINGS: KeybindingDefinitions;
|
|
@@ -11,7 +11,7 @@ const actions = {
|
|
|
11
11
|
'app.model.cycleForward': 'ctrl+p', 'app.model.cycleBackward': 'alt+p',
|
|
12
12
|
'app.model.select': 'ctrl+l', 'app.tools.expand': 'ctrl+o', 'app.thinking.toggle': 'ctrl+t',
|
|
13
13
|
'app.editor.external': 'ctrl+g', 'app.message.copy': 'ctrl+x',
|
|
14
|
-
'app.message.
|
|
14
|
+
'app.message.steer': ['super+enter', 'ctrl+enter', 'alt+enter'], 'app.message.dequeue': 'alt+up',
|
|
15
15
|
'app.clipboard.pasteImage': process.platform === 'darwin' ? ['ctrl+v', 'alt+v'] : 'ctrl+v',
|
|
16
16
|
'app.session.toggleNamedFilter': 'ctrl+n', 'app.session.togglePath': 'ctrl+p',
|
|
17
17
|
'app.session.toggleSort': 'ctrl+s', 'app.session.rename': 'ctrl+r',
|
|
@@ -34,9 +34,10 @@ export declare class ModeChoices {
|
|
|
34
34
|
quietly<T>(action: () => T | Promise<T>): Promise<T>;
|
|
35
35
|
/**
|
|
36
36
|
* Applies a mode's saved model and effort to the session. A model the account cannot use is
|
|
37
|
-
* skipped, and Pi clamps an effort the model does not support.
|
|
37
|
+
* skipped, and Pi clamps an effort the model does not support. A mode with no effort of its own
|
|
38
|
+
* returns to `fallback`, so a level chosen in another mode does not stay with this one.
|
|
38
39
|
*/
|
|
39
|
-
apply(session: ChoiceSession, mode: ComposerMode, modelFor: (reference: string) => ChoiceModel | undefined): Promise<void>;
|
|
40
|
+
apply(session: ChoiceSession, mode: ComposerMode, modelFor: (reference: string) => ChoiceModel | undefined, fallback: AgentSession['thinkingLevel']): Promise<void>;
|
|
40
41
|
/** Records a selection the person made in `mode`. Skipped while the terminal is making its own change. */
|
|
41
42
|
record(mode: ComposerMode, patch: ChoicePatch): Promise<void>;
|
|
42
43
|
}
|
|
@@ -31,9 +31,10 @@ export class ModeChoices {
|
|
|
31
31
|
}
|
|
32
32
|
/**
|
|
33
33
|
* Applies a mode's saved model and effort to the session. A model the account cannot use is
|
|
34
|
-
* skipped, and Pi clamps an effort the model does not support.
|
|
34
|
+
* skipped, and Pi clamps an effort the model does not support. A mode with no effort of its own
|
|
35
|
+
* returns to `fallback`, so a level chosen in another mode does not stay with this one.
|
|
35
36
|
*/
|
|
36
|
-
async apply(session, mode, modelFor) {
|
|
37
|
+
async apply(session, mode, modelFor, fallback) {
|
|
37
38
|
const choice = this.resolve(mode);
|
|
38
39
|
await this.quietly(async () => {
|
|
39
40
|
const model = choice.model ? modelFor(choice.model) : undefined;
|
|
@@ -45,8 +46,7 @@ export class ModeChoices {
|
|
|
45
46
|
// Not usable on this account (no key, or no longer offered): keep the session's model.
|
|
46
47
|
}
|
|
47
48
|
}
|
|
48
|
-
|
|
49
|
-
session.setThinkingLevel(choice.effort, { persist: false });
|
|
49
|
+
session.setThinkingLevel((choice.effort ?? fallback), { persist: false });
|
|
50
50
|
});
|
|
51
51
|
}
|
|
52
52
|
/** Records a selection the person made in `mode`. Skipped while the terminal is making its own change. */
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Input } from '@earendil-works/pi-tui';
|
|
2
|
+
/** A leaked report is echoed within one read; ten characters in this window is 50 characters a second. */
|
|
3
|
+
export declare const MOUSE_REPORT_TAIL_WINDOW_MS = 200;
|
|
4
|
+
export interface MouseReportMatch {
|
|
5
|
+
/** The whole report the keys spelled, without its `ESC[` prefix. */
|
|
6
|
+
report: string;
|
|
7
|
+
/** What a text field already held before the completing sequence. */
|
|
8
|
+
inserted: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Follows the printable characters of one report. A caller that receives the completing sequence
|
|
12
|
+
* before insertion removes `inserted`; a caller that receives it after insertion removes `report`.
|
|
13
|
+
*/
|
|
14
|
+
export declare class MouseReportTail {
|
|
15
|
+
private text;
|
|
16
|
+
private startedAt;
|
|
17
|
+
push(sequence: string, nowMs?: number): MouseReportMatch | undefined;
|
|
18
|
+
reset(): void;
|
|
19
|
+
}
|
|
20
|
+
/** A single-line input that drops a leaked mouse report instead of keeping it as text. */
|
|
21
|
+
export declare class MouseSafeInput extends Input {
|
|
22
|
+
private readonly mouseTail;
|
|
23
|
+
handleInput(data: string): void;
|
|
24
|
+
setValue(value: string): void;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Removes reports from text that is about to be printed. A bare `<\d;\d;\d>M` stays: once recorded it
|
|
28
|
+
* is content, and rewriting it would misreport what a run received.
|
|
29
|
+
*/
|
|
30
|
+
export declare function stripMouseReports(text: string): string;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// A mouse report that the input parser split across its timeout loses its `ESC[` prefix: the orphaned
|
|
2
|
+
// prefix is flushed as an unknown response and the rest is decoded as keystrokes. These helpers keep
|
|
3
|
+
// that tail out of text fields, without touching what a person types or pastes.
|
|
4
|
+
import { Input } from '@earendil-works/pi-tui';
|
|
5
|
+
/** A leaked report is echoed within one read; ten characters in this window is 50 characters a second. */
|
|
6
|
+
export const MOUSE_REPORT_TAIL_WINDOW_MS = 200;
|
|
7
|
+
const REPORT = /^<\d{1,3};\d{1,4};\d{1,4}[Mm]$/;
|
|
8
|
+
const PARTIAL = /^<\d{0,3}(;\d{0,4}){0,2}$/;
|
|
9
|
+
const WHOLE = /^(?:\x1b\[)?<\d{1,3};\d{1,4};\d{1,4}[Mm]$/;
|
|
10
|
+
const SGR_REPORT = /\x1b\[<\d+;\d+;\d+[Mm]|\\u001b\[<\d+;\d+;\d+[Mm]|\\x1b\[<\d+;\d+;\d+[Mm]/g;
|
|
11
|
+
const X10_REPORT = /\x1b\[M[\u0020-\u00ff]{3}/g;
|
|
12
|
+
/**
|
|
13
|
+
* Follows the printable characters of one report. A caller that receives the completing sequence
|
|
14
|
+
* before insertion removes `inserted`; a caller that receives it after insertion removes `report`.
|
|
15
|
+
*/
|
|
16
|
+
export class MouseReportTail {
|
|
17
|
+
text = '';
|
|
18
|
+
startedAt = 0;
|
|
19
|
+
push(sequence, nowMs = Date.now()) {
|
|
20
|
+
if (WHOLE.test(sequence)) {
|
|
21
|
+
this.reset();
|
|
22
|
+
return { report: sequence.replace(/^\x1b\[/, ''), inserted: '' };
|
|
23
|
+
}
|
|
24
|
+
if (sequence.length !== 1) {
|
|
25
|
+
this.reset();
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
if (sequence === '<') {
|
|
29
|
+
this.text = '<';
|
|
30
|
+
this.startedAt = nowMs;
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
if (!this.text)
|
|
34
|
+
return undefined;
|
|
35
|
+
if (nowMs - this.startedAt > MOUSE_REPORT_TAIL_WINDOW_MS) {
|
|
36
|
+
this.reset();
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
const next = this.text + sequence;
|
|
40
|
+
if (REPORT.test(next)) {
|
|
41
|
+
const inserted = this.text;
|
|
42
|
+
this.reset();
|
|
43
|
+
return { report: next, inserted };
|
|
44
|
+
}
|
|
45
|
+
if (!PARTIAL.test(next)) {
|
|
46
|
+
this.reset();
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
this.text = next;
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
reset() {
|
|
53
|
+
this.text = '';
|
|
54
|
+
this.startedAt = 0;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** A single-line input that drops a leaked mouse report instead of keeping it as text. */
|
|
58
|
+
export class MouseSafeInput extends Input {
|
|
59
|
+
mouseTail = new MouseReportTail();
|
|
60
|
+
handleInput(data) {
|
|
61
|
+
super.handleInput(data);
|
|
62
|
+
const match = this.mouseTail.push(data);
|
|
63
|
+
if (!match)
|
|
64
|
+
return;
|
|
65
|
+
const value = this.getValue();
|
|
66
|
+
if (value.endsWith(match.report))
|
|
67
|
+
this.setValue(value.slice(0, -match.report.length));
|
|
68
|
+
}
|
|
69
|
+
setValue(value) {
|
|
70
|
+
this.mouseTail.reset();
|
|
71
|
+
super.setValue(value);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Removes reports from text that is about to be printed. A bare `<\d;\d;\d>M` stays: once recorded it
|
|
76
|
+
* is content, and rewriting it would misreport what a run received.
|
|
77
|
+
*/
|
|
78
|
+
export function stripMouseReports(text) {
|
|
79
|
+
return text.replace(SGR_REPORT, '').replace(X10_REPORT, '');
|
|
80
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { matchesKey, truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';
|
|
2
|
+
import { MouseSafeInput } from "../mouseReport.js";
|
|
2
3
|
import { FilterList } from "../list.js";
|
|
3
4
|
import { normalizeAddress, normalizeDevTools } from "../annotateCommand.js";
|
|
4
5
|
import { describeSend, sendUnsent, unsent } from "../annotateSend.js";
|
|
@@ -185,7 +186,7 @@ export class AnnotatePage {
|
|
|
185
186
|
return 'Load the bridge script in the app (shown at the top), then reload the app.';
|
|
186
187
|
}
|
|
187
188
|
openField(kind) {
|
|
188
|
-
const input = new
|
|
189
|
+
const input = new MouseSafeInput({ prompt: '', placeholder: FIELDS[kind].placeholder, placeholderStyle: text => this.options.theme().fg('dim', text) });
|
|
189
190
|
this.field = { kind, input };
|
|
190
191
|
this.syncFocus();
|
|
191
192
|
this.options.changed();
|
|
@@ -52,7 +52,7 @@ export declare class BrowserPage implements TerminalPage {
|
|
|
52
52
|
get focused(): boolean;
|
|
53
53
|
set focused(value: boolean);
|
|
54
54
|
tabs(): {
|
|
55
|
-
id: "skills" | "tools" | "connections" | "plugins" | "variant" | "library" | "
|
|
55
|
+
id: "skills" | "tools" | "connections" | "plugins" | "variant" | "library" | "foundation" | "overview";
|
|
56
56
|
title: string;
|
|
57
57
|
}[];
|
|
58
58
|
activeTab(): string;
|