@xingular/harness 1.2.0 → 1.3.1-unstable.0
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 +35 -0
- package/README.md +10 -5
- package/dist/cli.js +5560 -5056
- package/dist/sdk/agents/builtin.js +3 -3
- package/dist/sdk/agents/planResearch.d.ts +23 -1
- package/dist/sdk/agents/planResearch.js +71 -8
- package/dist/sdk/agents/profiles.d.ts +4 -1
- package/dist/sdk/agents/profiles.js +17 -6
- package/dist/sdk/capabilities/runSnapshot.js +10 -4
- package/dist/sdk/commands/registry.d.ts +11 -3
- package/dist/sdk/commands/registry.js +3 -2
- package/dist/sdk/core/shell.d.ts +20 -0
- package/dist/sdk/core/shell.js +73 -0
- package/dist/sdk/core/workspaceBashTool.d.ts +17 -1
- package/dist/sdk/core/workspaceBashTool.js +16 -2
- package/dist/sdk/core/workspaceSandbox.d.ts +10 -0
- package/dist/sdk/core/workspaceSandbox.js +52 -12
- package/dist/sdk/core/workspaceWriteOperations.d.ts +11 -0
- package/dist/sdk/core/workspaceWriteOperations.js +26 -8
- package/dist/sdk/doctor/checks.js +71 -12
- package/dist/sdk/hub/hub.js +5 -3
- package/dist/sdk/index.d.ts +2 -2
- package/dist/sdk/index.js +1 -1
- package/dist/sdk/plugins/builtin.js +3 -3
- package/dist/sdk/plugins/install.d.ts +6 -2
- package/dist/sdk/plugins/install.js +29 -5
- package/dist/sdk/plugins/inventory.js +2 -2
- package/dist/sdk/prompt/compile.d.ts +2 -2
- package/dist/sdk/prompt/compile.js +14 -16
- package/dist/sdk/server/methods.js +4 -2
- package/dist/sdk/sessions/compactionCheckpoint.d.ts +3 -3
- package/dist/sdk/sessions/compactionCheckpoint.js +12 -12
- package/dist/sdk/sessions/continuation.d.ts +4 -2
- package/dist/sdk/sessions/continuation.js +7 -6
- package/dist/sdk/sessions/contracts.d.ts +22 -4
- package/dist/sdk/sessions/contracts.js +12 -7
- package/dist/sdk/sessions/planMode.js +2 -2
- package/dist/sdk/sessions/runStore.js +3 -1
- package/dist/sdk/sessions/runner.js +2 -2
- package/dist/sdk/sessions/sessions.js +29 -11
- package/dist/sdk/sessions/subagentService.d.ts +15 -0
- package/dist/sdk/sessions/subagentService.js +194 -54
- package/dist/sdk/sessions/subagents.d.ts +10 -1
- package/dist/sdk/sessions/subagents.js +4 -0
- package/dist/sdk/sessions/workSlice.d.ts +8 -5
- package/dist/sdk/sessions/workSlice.js +77 -45
- package/dist/sdk/terminal/controller.d.ts +20 -0
- package/dist/sdk/terminal/controller.js +43 -18
- package/dist/sdk/terminal/opentui/ansi.d.ts +25 -0
- package/dist/sdk/terminal/opentui/ansi.js +192 -0
- package/dist/sdk/terminal/opentui/model.d.ts +163 -0
- package/dist/sdk/terminal/opentui/model.js +482 -0
- package/dist/sdk/terminal/opentui/subagentsWidget.d.ts +3 -3
- package/dist/sdk/terminal/opentui/subagentsWidget.js +7 -5
- package/dist/sdk/terminal/opentui/theme.js +1 -0
- package/dist/sdk/terminal/piExtension.js +28 -14
- package/dist/sdk/terminal/subagentMonitor.d.ts +17 -4
- package/dist/sdk/terminal/subagentMonitor.js +29 -8
- package/dist/sdk/terminal/terminals.d.ts +7 -0
- package/dist/sdk/terminal/terminals.js +86 -14
- package/dist/sdk/terminal/trajectory.d.ts +6 -1
- package/dist/sdk/terminal/trajectory.js +8 -0
- package/dist/sdk/terminal/tui/chrome.d.ts +4 -0
- package/dist/sdk/terminal/tui/chrome.js +13 -0
- package/dist/sdk/terminal/tui/commandPages.js +14 -4
- package/dist/sdk/terminal/tui/commands.d.ts +4 -0
- package/dist/sdk/terminal/tui/commands.js +8 -0
- package/dist/sdk/terminal/tui/pageOverlay.js +13 -6
- package/dist/sdk/terminal/tui/pages/browserPage.js +5 -4
- package/dist/sdk/terminal/tui/pages/hubPage.d.ts +14 -1
- package/dist/sdk/terminal/tui/pages/hubPage.js +192 -28
- package/dist/sdk/terminal/tui/pages/pluginsPage.d.ts +8 -1
- package/dist/sdk/terminal/tui/pages/pluginsPage.js +197 -25
- package/dist/sdk/terminal/tui/pages/subagentsPage.js +8 -5
- package/dist/sdk/tools/sessionName.d.ts +2 -0
- package/dist/sdk/tools/sessionName.js +5 -3
- package/dist/sdk/tools/spawnSubagent.js +9 -7
- package/dist/sdk/tools/todo.d.ts +15 -0
- package/dist/sdk/tools/todo.js +64 -0
- package/dist/terminal-opentui.js +1416 -348
- package/dist/terminal-pi.js +315 -126
- package/docs/architecture/00-overview.md +1 -1
- package/docs/architecture/01-extension-points.md +1 -1
- package/docs/architecture/02-sessions-and-runs.md +65 -34
- package/docs/architecture/04-prompt.md +8 -6
- package/docs/architecture/06-plugins-and-capabilities.md +10 -6
- package/docs/architecture/07-tools-and-gate.md +31 -5
- package/docs/architecture/09-accounts-and-connectors.md +8 -2
- package/docs/architecture/10-server-protocol.md +16 -9
- package/docs/architecture/11-terminal.md +51 -26
- package/docs/architecture/14-surfaces-and-parity.md +4 -1
- package/docs/architecture/README.md +18 -17
- package/docs/architecture/proposals/PROP-0006-run-checklist.md +2 -0
- package/docs/architecture/proposals/PROP-0041-bounded-work-slices.md +32 -21
- package/docs/architecture/proposals/PROP-0044-work-slice-defaults.md +93 -0
- package/docs/architecture/proposals/PROP-0045-rename-checklist-to-todo.md +132 -0
- package/docs/architecture/proposals/PROP-0046-progress-aware-stall-guards.md +82 -0
- package/docs/architecture/proposals/PROP-0047-plugins-hub-separation.md +122 -0
- package/docs/architecture/proposals/README.md +10 -6
- package/docs/decisions/ADR-0039-progress-aware-stall-guards.md +39 -0
- package/docs/decisions/ADR-0040-architecture-change-workflow.md +31 -0
- package/docs/decisions/ADR-0041-plugins-hub-separation.md +41 -0
- package/docs/decisions/ADR-0042-windows-shell-without-sandbox.md +40 -0
- package/docs/decisions/ADR-0043-parents-continue-stopped-subagents.md +49 -0
- package/docs/doctor.md +14 -1
- package/docs/parity.md +9 -9
- package/docs/releases/1.3.0.md +21 -0
- package/docs/releases/README.md +2 -2
- package/docs/terminal-tui.md +85 -46
- package/package.json +1 -1
- package/dist/sdk/tools/checklist.d.ts +0 -9
- package/dist/sdk/tools/checklist.js +0 -55
|
@@ -21,24 +21,26 @@ function mark(status, frame) {
|
|
|
21
21
|
case 'running': return { mark: frame, tone: 'warning' };
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
|
-
export function subagentTasks(
|
|
25
|
-
return
|
|
24
|
+
export function subagentTasks(todo) {
|
|
25
|
+
return todo && todo.total > 0 ? `${todo.done} of ${todo.total} tasks` : null;
|
|
26
26
|
}
|
|
27
27
|
/**
|
|
28
28
|
* The compact live roster shown under the status line: active children first, at most three
|
|
29
29
|
* rows. The session name is cut first when the terminal is narrow.
|
|
30
30
|
*/
|
|
31
31
|
export function subagentRoster(items, width, frame = getWorkingSpinnerFrames()[0] ?? '') {
|
|
32
|
-
|
|
32
|
+
// The status line already describes the main agent; its row is only on the /subagents page.
|
|
33
|
+
const children = items.filter(item => item.relation !== 'main');
|
|
34
|
+
if (!children.length || width < 1)
|
|
33
35
|
return null;
|
|
34
|
-
const ordered = [...
|
|
36
|
+
const ordered = [...children].sort((left, right) => {
|
|
35
37
|
const active = Number(ACTIVE_SUBAGENT_STATUSES.has(right.status)) - Number(ACTIVE_SUBAGENT_STATUSES.has(left.status));
|
|
36
38
|
return active || left.startedAt.localeCompare(right.startedAt) || left.id.localeCompare(right.id);
|
|
37
39
|
});
|
|
38
40
|
const rows = ordered.slice(0, MAX_VISIBLE_ROWS).map((item) => {
|
|
39
41
|
const status = mark(item.status, frame);
|
|
40
42
|
const profile = cut(safe(item.profile?.name ?? '') || 'subagent', Math.max(1, Math.min(24, width - 2)));
|
|
41
|
-
const tasks = subagentTasks(item.
|
|
43
|
+
const tasks = subagentTasks(item.todo);
|
|
42
44
|
const used = 2 + visibleWidth(profile) + GAP.length + (tasks ? GAP.length + tasks.length : 0);
|
|
43
45
|
const title = cut(safe(item.title) || 'Subagent', Math.max(1, width - used));
|
|
44
46
|
return { id: item.id, mark: status.mark, tone: status.tone, profile, color: item.profile?.color ?? null, title, tasks };
|
|
@@ -2,15 +2,15 @@
|
|
|
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 { createBashTool, createEditTool, createWriteTool } from '@earendil-works/pi-coding-agent';
|
|
5
|
+
import { createBashTool, createEditTool, createPowerShellTool, createWriteTool } from '@earendil-works/pi-coding-agent';
|
|
6
6
|
import { Type } from 'typebox';
|
|
7
7
|
import { connectionFromEnv, socketCall, terminalCall } from "./client.js";
|
|
8
8
|
import { controlCall } from "./control.js";
|
|
9
9
|
import { SPAWN_SUBAGENT, SUBAGENT_OWNER, SUBAGENT_RECEIPT, SUBAGENT_RESULTS, subagentHistory } from "../sessions/subagents.js";
|
|
10
10
|
import { PLAN_RESEARCH_TOOLS } from "../agents/planResearch.js";
|
|
11
11
|
import { askUserTool } from "../tools/askUser.js";
|
|
12
|
-
import {
|
|
13
|
-
import { checkpointTool } from "../tools/checkpoint.js";
|
|
12
|
+
import { todoTool } from "../tools/todo.js";
|
|
13
|
+
import { CHECKPOINT_WRITE, checkpointTool } from "../tools/checkpoint.js";
|
|
14
14
|
import { createSliceState, parseWorkSlice, workSliceGuard } from "../sessions/workSlice.js";
|
|
15
15
|
import { DEFAULT_COMMAND_TIMEOUT_SECONDS, withCommandBudget } from "../core/commandBudget.js";
|
|
16
16
|
import { sessionNameTool } from "../tools/sessionName.js";
|
|
@@ -20,7 +20,7 @@ import { compactionCheckpoint } from "../sessions/compactionCheckpoint.js";
|
|
|
20
20
|
import { pruneToolImages } from "../world/images.js";
|
|
21
21
|
import { parseInitialBashCommand } from "../sessions/initialBash.js";
|
|
22
22
|
import { inspectWorkspaceWrite } from "../sessions/workspaceBoundary.js";
|
|
23
|
-
import {
|
|
23
|
+
import { createWorkspaceShellTool } from "../core/workspaceBashTool.js";
|
|
24
24
|
import { createWorkspaceBashOperations, runWithWorkspaceWriteScope } from "../core/workspaceSandbox.js";
|
|
25
25
|
import { withWorkspaceWriteAuthorization, workspaceEditOperations, workspaceWriteOperations } from "../core/workspaceWriteOperations.js";
|
|
26
26
|
import { BUDGET_GUARD, OUTPUT_TRIM, budgetGuard, outputTrim } from "../plugins/firstParty.js";
|
|
@@ -48,6 +48,7 @@ export default async function (pi, planRun, getSession) {
|
|
|
48
48
|
let context = null;
|
|
49
49
|
let nativeSubagents = false;
|
|
50
50
|
let subagentParent = false;
|
|
51
|
+
let stoppedChildren = null;
|
|
51
52
|
let timer = null;
|
|
52
53
|
let polling = false;
|
|
53
54
|
let failure;
|
|
@@ -135,7 +136,7 @@ export default async function (pi, planRun, getSession) {
|
|
|
135
136
|
},
|
|
136
137
|
workspaceWriteScope: () => planRun?.active ? 'read-only' : undefined
|
|
137
138
|
});
|
|
138
|
-
pi.registerTool(
|
|
139
|
+
pi.registerTool(createWorkspaceShellTool({ createBashTool, createPowerShellTool }, process.cwd(), workspaceBash));
|
|
139
140
|
pi.registerTool(withWorkspaceWriteAuthorization(createWriteTool(process.cwd(), { operations: workspaceWriteOperations }), writeAuthorizations));
|
|
140
141
|
pi.registerTool(withWorkspaceWriteAuthorization(createEditTool(process.cwd(), { operations: workspaceEditOperations }), writeAuthorizations));
|
|
141
142
|
compactionCheckpoint({ planInstructions: () => planRun?.active ? PLAN_INSTRUCTIONS : null })(pi);
|
|
@@ -145,7 +146,7 @@ export default async function (pi, planRun, getSession) {
|
|
|
145
146
|
const sliceState = createSliceState(slicePolicy);
|
|
146
147
|
// `XINGULAR_WORK_SLICE=off` at launch leaves terminal replies unbounded, as before PROP-0041.
|
|
147
148
|
if (slicePolicy.enabled)
|
|
148
|
-
workSliceGuard({ policy: slicePolicy, state: sliceState })(pi);
|
|
149
|
+
workSliceGuard({ policy: slicePolicy, state: sliceState, checkpointAvailable: () => !planRun?.active || planRun.allowedTools.has(CHECKPOINT_WRITE) })(pi);
|
|
149
150
|
// Only the latest screenshots stay in what the model sees (world/images.ts).
|
|
150
151
|
pi.on('context', (event) => {
|
|
151
152
|
const messages = pruneToolImages(event.messages);
|
|
@@ -155,7 +156,8 @@ export default async function (pi, planRun, getSession) {
|
|
|
155
156
|
context = ctx;
|
|
156
157
|
if (timer)
|
|
157
158
|
clearInterval(timer);
|
|
158
|
-
|
|
159
|
+
// The branch's parent runs let the controller show the latest one's rows again on reopen.
|
|
160
|
+
await terminalCall('ready', { nativeSessionId: ctx.sessionManager.getSessionFile(), owners: subagentHistory(ctx.sessionManager.getBranch?.() ?? []).owners });
|
|
159
161
|
timer = setInterval(() => {
|
|
160
162
|
if (polling || !context)
|
|
161
163
|
return;
|
|
@@ -188,15 +190,20 @@ export default async function (pi, planRun, getSession) {
|
|
|
188
190
|
reasoningTokens = undefined;
|
|
189
191
|
costReported = false;
|
|
190
192
|
// A run a plugin starts with a custom message has no user prompt to record; it still begins a turn.
|
|
191
|
-
const begun = await terminalCall('begin', { text: event.messageType === 'user' ? event.prompt : '' });
|
|
193
|
+
const begun = await terminalCall('begin', { text: event.messageType === 'user' ? event.prompt : '', model: ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : null, effort: pi.getThinkingLevel?.() ?? null });
|
|
192
194
|
workspaceRoot = typeof begun.workspaceRoot === 'string' ? begun.workspaceRoot : null;
|
|
193
195
|
if (!workspaceRoot)
|
|
194
196
|
throw new Error('The active project workspace could not be resolved; the turn was not started.');
|
|
197
|
+
// Every turn marks its run on the branch, so a reopened conversation finds its latest one.
|
|
198
|
+
pi.appendEntry(SUBAGENT_OWNER, { runId: begun.runId });
|
|
199
|
+
stoppedChildren = null;
|
|
195
200
|
if (nativeSubagents) {
|
|
196
|
-
pi.appendEntry(SUBAGENT_OWNER, { runId: begun.runId });
|
|
197
201
|
const snapshot = await controlCall('terminal.subagents.register', { tools: pi.getActiveTools(), model: ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : null, effort: pi.getThinkingLevel(), plan: planRun?.active === true, history: subagentHistory(ctx.sessionManager.getBranch()) });
|
|
198
202
|
if (planRun && Array.isArray(snapshot.connectors))
|
|
199
203
|
planRun.connectors = snapshot.connectors;
|
|
204
|
+
if (planRun && snapshot.subagentProfiles)
|
|
205
|
+
planRun.subagentProfiles = snapshot.subagentProfiles;
|
|
206
|
+
stoppedChildren = snapshot.notice ?? null;
|
|
200
207
|
subagentParent = true;
|
|
201
208
|
}
|
|
202
209
|
if (planRun && event.messageType === 'user') {
|
|
@@ -260,6 +267,13 @@ export default async function (pi, planRun, getSession) {
|
|
|
260
267
|
: 'The user’s initial Bash command has already run. Read its native Bash result and explain what it shows. Do not run the same command again unless the user explicitly asks.';
|
|
261
268
|
return { message: { customType: 'xingular-initial-bash', content: instruction, display: false, details: {} } };
|
|
262
269
|
});
|
|
270
|
+
// A second handler, so the notice joins any initial-Bash message rather than replacing it.
|
|
271
|
+
pi.on('before_agent_start', async () => {
|
|
272
|
+
const notice = stoppedChildren;
|
|
273
|
+
stoppedChildren = null;
|
|
274
|
+
if (notice)
|
|
275
|
+
return { message: { customType: SUBAGENT_RESULTS, content: notice.text, display: false, details: { [SUBAGENT_RECEIPT]: notice.receipt } } };
|
|
276
|
+
});
|
|
263
277
|
const deliverChildren = async (event, ctx, settle) => {
|
|
264
278
|
if (!nativeSubagents || (settle && event.outcome !== 'completed') || ctx.signal?.aborted)
|
|
265
279
|
return;
|
|
@@ -437,15 +451,15 @@ export default async function (pi, planRun, getSession) {
|
|
|
437
451
|
return { content: [{ type: 'text', text: typeof result === 'string' ? result : result.text }], details: undefined };
|
|
438
452
|
}
|
|
439
453
|
});
|
|
440
|
-
// The
|
|
454
|
+
// The todo list is recorded on the harness run; the shared tool validates and the socket carries the event.
|
|
441
455
|
let recorded = Promise.resolve();
|
|
442
|
-
const
|
|
456
|
+
const todo = todoTool({ emit: (_type, payload) => { recorded = terminalCall('todo', payload); } });
|
|
443
457
|
pi.registerTool({
|
|
444
|
-
name:
|
|
445
|
-
parameters: Type.Unsafe(
|
|
458
|
+
name: todo.name, label: todo.label, description: todo.description,
|
|
459
|
+
parameters: Type.Unsafe(todo.parameters(Type)),
|
|
446
460
|
async execute(_id, input) {
|
|
447
461
|
recorded = Promise.resolve();
|
|
448
|
-
const result = await
|
|
462
|
+
const result = await todo.execute(input);
|
|
449
463
|
await recorded;
|
|
450
464
|
return { content: [{ type: 'text', text: typeof result === 'string' ? result : result.text }], details: undefined };
|
|
451
465
|
}
|
|
@@ -5,10 +5,13 @@ export interface TerminalSubagentProfile {
|
|
|
5
5
|
name: string;
|
|
6
6
|
color: string;
|
|
7
7
|
}
|
|
8
|
+
/** The main agent's row: the parent run itself, shown by name rather than by a profile. */
|
|
9
|
+
export declare const MAIN_PROFILE: TerminalSubagentProfile;
|
|
8
10
|
export interface TerminalSubagentRun {
|
|
9
11
|
id: string;
|
|
10
12
|
parentRunId: string | null;
|
|
11
|
-
|
|
13
|
+
/** `main` is the parent run itself; every other row is a child or nested child. */
|
|
14
|
+
relation: 'main' | 'subagent';
|
|
12
15
|
/** The short name the parent gave the child (PROP-0030). */
|
|
13
16
|
title: string;
|
|
14
17
|
goal: string | null;
|
|
@@ -27,8 +30,8 @@ export interface TerminalSubagentActivity extends TerminalSubagentRun {
|
|
|
27
30
|
activity: string;
|
|
28
31
|
/** Cumulative run usage only; provider reasoning summaries and request details stay private. */
|
|
29
32
|
usage: TerminalSubagentUsage | null;
|
|
30
|
-
/** How far the child's own
|
|
31
|
-
|
|
33
|
+
/** How far the child's own todo is: counts only, never the items. */
|
|
34
|
+
todo: {
|
|
32
35
|
done: number;
|
|
33
36
|
total: number;
|
|
34
37
|
} | null;
|
|
@@ -41,7 +44,17 @@ export declare class SubagentMonitor {
|
|
|
41
44
|
private parentRunId;
|
|
42
45
|
private readonly activities;
|
|
43
46
|
begin(parentRunId: string): void;
|
|
44
|
-
|
|
47
|
+
/** Folds one event of the tracked parent (`main`) or one of its descendants; `null` seeds the row. */
|
|
48
|
+
observe(parentRunId: string, run: TerminalSubagentRun, event: RunEvent | null): void;
|
|
49
|
+
/**
|
|
50
|
+
* Rebuilds the rows of a parent run that is no longer live, such as the latest one of a reopened
|
|
51
|
+
* conversation, by folding each run's saved events. Rows keep their final status.
|
|
52
|
+
*/
|
|
53
|
+
restore(parentRunId: string, runs: readonly {
|
|
54
|
+
run: TerminalSubagentRun;
|
|
55
|
+
events: readonly RunEvent[];
|
|
56
|
+
}[]): void;
|
|
57
|
+
/** The main row first, then children in start order. */
|
|
45
58
|
snapshot(parentRunId: string): TerminalSubagentActivity[];
|
|
46
59
|
clear(parentRunId?: string): void;
|
|
47
60
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { usagePayloadSchema } from "../sessions/contracts.js";
|
|
2
|
+
/** The main agent's row: the parent run itself, shown by name rather than by a profile. */
|
|
3
|
+
export const MAIN_PROFILE = { name: 'main', color: 'main' };
|
|
2
4
|
export const ACTIVE_SUBAGENT_STATUSES = new Set(['queued', 'running', 'waiting_for_approval', 'waiting_for_input']);
|
|
3
5
|
export function terminalSubagentDuration(activity, now = Date.now()) {
|
|
4
6
|
const start = Date.parse(activity.startedAt);
|
|
@@ -50,8 +52,11 @@ export class SubagentMonitor {
|
|
|
50
52
|
this.parentRunId = parentRunId;
|
|
51
53
|
this.activities.clear();
|
|
52
54
|
}
|
|
55
|
+
/** Folds one event of the tracked parent (`main`) or one of its descendants; `null` seeds the row. */
|
|
53
56
|
observe(parentRunId, run, event) {
|
|
54
|
-
if (this.parentRunId !== parentRunId
|
|
57
|
+
if (this.parentRunId !== parentRunId)
|
|
58
|
+
return;
|
|
59
|
+
if ((run.relation === 'main') !== (run.id === parentRunId))
|
|
55
60
|
return;
|
|
56
61
|
const row = this.activities.get(run.id) ?? {
|
|
57
62
|
id: run.id,
|
|
@@ -63,7 +68,7 @@ export class SubagentMonitor {
|
|
|
63
68
|
startedAt: run.startedAt,
|
|
64
69
|
endedAt: run.endedAt,
|
|
65
70
|
activity: statusActivity(run.status),
|
|
66
|
-
|
|
71
|
+
todo: null,
|
|
67
72
|
profile: run.profile ?? null,
|
|
68
73
|
accountLabel: run.accountLabel ?? null,
|
|
69
74
|
model: run.model ?? null,
|
|
@@ -86,8 +91,11 @@ export class SubagentMonitor {
|
|
|
86
91
|
row.status = run.status;
|
|
87
92
|
row.startedAt = run.startedAt;
|
|
88
93
|
row.endedAt = run.endedAt;
|
|
89
|
-
const payload = payloadOf(event);
|
|
90
|
-
if (event
|
|
94
|
+
const payload = event ? payloadOf(event) : {};
|
|
95
|
+
if (!event) {
|
|
96
|
+
// A seeded row keeps what it already folded.
|
|
97
|
+
}
|
|
98
|
+
else if (event.type === 'tool.started') {
|
|
91
99
|
const toolCallId = stringOf(payload.toolCallId);
|
|
92
100
|
const name = stringOf(payload.name) ?? 'Tool';
|
|
93
101
|
if (toolCallId)
|
|
@@ -125,22 +133,35 @@ export class SubagentMonitor {
|
|
|
125
133
|
};
|
|
126
134
|
}
|
|
127
135
|
}
|
|
128
|
-
else if (event.type === 'checklist.updated') {
|
|
136
|
+
else if (event.type === 'todo.updated' || event.type === 'checklist.updated') {
|
|
129
137
|
const items = Array.isArray(payload.items) ? payload.items : [];
|
|
130
|
-
row.
|
|
138
|
+
row.todo = items.length ? { done: items.filter(item => item?.status === 'completed').length, total: items.length } : null;
|
|
131
139
|
}
|
|
132
140
|
if (run.status !== 'running')
|
|
133
141
|
row.tools.clear();
|
|
134
142
|
row.activity = run.status === 'running'
|
|
135
|
-
? latestTool(row.tools) ?? row.lastTool?.text ?? (event
|
|
143
|
+
? latestTool(row.tools) ?? row.lastTool?.text ?? (event?.type === 'message.delta' || (!event && row.activity === 'Responding') ? 'Responding' : 'Working')
|
|
136
144
|
: statusActivity(run.status);
|
|
137
145
|
this.activities.set(run.id, row);
|
|
138
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* Rebuilds the rows of a parent run that is no longer live, such as the latest one of a reopened
|
|
149
|
+
* conversation, by folding each run's saved events. Rows keep their final status.
|
|
150
|
+
*/
|
|
151
|
+
restore(parentRunId, runs) {
|
|
152
|
+
this.begin(parentRunId);
|
|
153
|
+
for (const { run, events } of runs) {
|
|
154
|
+
this.observe(parentRunId, run, null);
|
|
155
|
+
for (const event of events)
|
|
156
|
+
this.observe(parentRunId, run, event);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/** The main row first, then children in start order. */
|
|
139
160
|
snapshot(parentRunId) {
|
|
140
161
|
if (this.parentRunId !== parentRunId)
|
|
141
162
|
return [];
|
|
142
163
|
return [...this.activities.values()]
|
|
143
|
-
.sort((left, right) => left.startedAt.localeCompare(right.startedAt) || left.id.localeCompare(right.id))
|
|
164
|
+
.sort((left, right) => Number(right.relation === 'main') - Number(left.relation === 'main') || left.startedAt.localeCompare(right.startedAt) || left.id.localeCompare(right.id))
|
|
144
165
|
.map(({ tools: _tools, lastTool: _lastTool, ...activity }) => activity);
|
|
145
166
|
}
|
|
146
167
|
clear(parentRunId) {
|
|
@@ -150,6 +150,13 @@ export declare class Terminals {
|
|
|
150
150
|
/** Each child's profile name and colour, looked up once per child run. */
|
|
151
151
|
private readonly childProfiles;
|
|
152
152
|
private subagentProfile;
|
|
153
|
+
/** Account, model and effort for a monitor row, from the run record. */
|
|
154
|
+
private monitorRuntime;
|
|
155
|
+
/**
|
|
156
|
+
* Shows the reopened conversation's latest parent run again: the last owner on its native branch
|
|
157
|
+
* that ran in this folder, with every descendant flat, as each ended.
|
|
158
|
+
*/
|
|
159
|
+
private hydrateMonitor;
|
|
153
160
|
constructor(options: TerminalsOptions);
|
|
154
161
|
/** The commands a terminal session finds on PATH. Rewritten only when they change. */
|
|
155
162
|
private ensureCommands;
|
|
@@ -22,9 +22,9 @@ import * as componentLibrary from "../variant/components.js";
|
|
|
22
22
|
import * as variantStore from "../variant/store.js";
|
|
23
23
|
import { compile, copyToRun, record, render, saveForSession } from "../prompt/compile.js";
|
|
24
24
|
import { isActiveStatus } from "../sessions/contracts.js";
|
|
25
|
-
import { findRun, loadRun, saveRun, updateRun } from "../sessions/runStore.js";
|
|
25
|
+
import { findRun, loadRun, readEvents, recordedSubagents, saveRun, updateRun } from "../sessions/runStore.js";
|
|
26
26
|
import { combineWorkspaceScope, inspectRunWorkspaceWrite } from "../sessions/workspaceBoundary.js";
|
|
27
|
-
import {
|
|
27
|
+
import { TODO_WRITE } from "../tools/todo.js";
|
|
28
28
|
import { DEFAULT_TOOL_NAMES } from "../tools/definitions.js";
|
|
29
29
|
import { harnessTools } from "../tools/harnessTools.js";
|
|
30
30
|
import { HARNESS_PLUGINS } from "../plugins/builtin.js";
|
|
@@ -42,7 +42,7 @@ import { TerminalController } from "./controller.js";
|
|
|
42
42
|
import { terminalCommandFiles } from "./launch.js";
|
|
43
43
|
import { SessionSocket, socketPath } from "./socket.js";
|
|
44
44
|
import { harnessLogInfo, readHarnessLogChunk } from "./logs.js";
|
|
45
|
-
import { listTerminalTrajectoryRuns, readTerminalTrajectory } from "./trajectory.js";
|
|
45
|
+
import { listTerminalTrajectoryRuns, readTerminalTrajectory, latestTerminalTodo } from "./trajectory.js";
|
|
46
46
|
import { BrowserControl } from "./browserControl.js";
|
|
47
47
|
/** Identity of the exact resource decision that this terminal was prepared with. */
|
|
48
48
|
function resourceIdentity(loading) {
|
|
@@ -98,12 +98,61 @@ export class Terminals {
|
|
|
98
98
|
}
|
|
99
99
|
return this.childProfiles.get(record.id) ?? null;
|
|
100
100
|
}
|
|
101
|
+
/** Account, model and effort for a monitor row, from the run record. */
|
|
102
|
+
monitorRuntime(live, record) {
|
|
103
|
+
let accountLabel = record.accountId === live.account.id ? live.account.label : null;
|
|
104
|
+
if (accountLabel === null && record.accountId) {
|
|
105
|
+
try {
|
|
106
|
+
accountLabel = this.options.accounts.find(record.accountId).label;
|
|
107
|
+
}
|
|
108
|
+
catch { }
|
|
109
|
+
}
|
|
110
|
+
return { accountLabel, model: record.model ?? null, effort: record.effort ?? null };
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Shows the reopened conversation's latest parent run again: the last owner on its native branch
|
|
114
|
+
* that ran in this folder, with every descendant flat, as each ended.
|
|
115
|
+
*/
|
|
116
|
+
hydrateMonitor(live, owners) {
|
|
117
|
+
const { root } = this.options.env;
|
|
118
|
+
const native = live.controller.nativeSessionId;
|
|
119
|
+
if (!native)
|
|
120
|
+
return;
|
|
121
|
+
for (const id of [...owners].reverse()) {
|
|
122
|
+
const record = findRun(root, id);
|
|
123
|
+
if (!record || record.relation || record.nativeSessionId !== native || record.workingDirectory !== live.input.cwd)
|
|
124
|
+
continue;
|
|
125
|
+
const children = [];
|
|
126
|
+
const seen = new Set([record.id]);
|
|
127
|
+
const visit = (parentId) => {
|
|
128
|
+
for (const childId of recordedSubagents(root, parentId)) {
|
|
129
|
+
if (seen.has(childId))
|
|
130
|
+
continue;
|
|
131
|
+
seen.add(childId);
|
|
132
|
+
const child = findRun(root, childId);
|
|
133
|
+
if (child?.relation !== 'subagent')
|
|
134
|
+
continue;
|
|
135
|
+
children.push(child);
|
|
136
|
+
visit(child.id);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
visit(record.id);
|
|
140
|
+
const entry = (run, profile = null) => ({ record: run, events: readEvents(root, run.id), profile, runtime: this.monitorRuntime(live, run) });
|
|
141
|
+
live.controller.restoreMonitor(entry(record), children.map(child => entry(child, this.subagentProfile(child))));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
101
145
|
constructor(options) {
|
|
102
146
|
this.options = options;
|
|
103
147
|
this.assets = options.assets ?? defaultAssets();
|
|
104
148
|
options.runs.listen(event => {
|
|
105
|
-
this.sessions.get(event.sessionId)
|
|
149
|
+
const session = this.sessions.get(event.sessionId);
|
|
150
|
+
session?.controller.observe(event);
|
|
106
151
|
const record = findRun(options.env.root, event.runId);
|
|
152
|
+
if (record && !record.relation && session) {
|
|
153
|
+
session.controller.observeMain(record, event, this.monitorRuntime(session, record));
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
107
156
|
if (record?.relation !== 'subagent')
|
|
108
157
|
return;
|
|
109
158
|
let parentId = record.parentRunId;
|
|
@@ -119,8 +168,8 @@ export class Terminals {
|
|
|
119
168
|
const settings = options.subagents?.runtimeSettings(record.id);
|
|
120
169
|
owner.controller.observeSubagent(parent.id, record, event, this.subagentProfile(record), {
|
|
121
170
|
accountLabel: owner.account.label,
|
|
122
|
-
model: settings?.model ?? null,
|
|
123
|
-
effort: settings?.effort ?? null
|
|
171
|
+
model: settings?.model ?? record.model ?? null,
|
|
172
|
+
effort: settings?.effort ?? record.effort ?? null
|
|
124
173
|
});
|
|
125
174
|
break;
|
|
126
175
|
}
|
|
@@ -690,7 +739,7 @@ export class Terminals {
|
|
|
690
739
|
};
|
|
691
740
|
}
|
|
692
741
|
/** The run a native turn reports into: the one reserved by `send`, or a new one for what the person typed. */
|
|
693
|
-
begin(live, reservedRunId, text) {
|
|
742
|
+
begin(live, reservedRunId, text, launch = {}) {
|
|
694
743
|
const { root } = this.options.env;
|
|
695
744
|
const { sessionId } = live.input;
|
|
696
745
|
if (this.options.runs.activeForSession(sessionId))
|
|
@@ -706,6 +755,14 @@ export class Terminals {
|
|
|
706
755
|
saveRun(root, this.runRecord(live, runId, text || null));
|
|
707
756
|
copyToRun(root, live.input.sessionFolder, runId);
|
|
708
757
|
}
|
|
758
|
+
if (launch.model || launch.effort) {
|
|
759
|
+
updateRun(root, runId, (record) => {
|
|
760
|
+
if (launch.model)
|
|
761
|
+
record.model = launch.model;
|
|
762
|
+
if (launch.effort)
|
|
763
|
+
record.effort = launch.effort;
|
|
764
|
+
});
|
|
765
|
+
}
|
|
709
766
|
return this.options.runs.start(sessionId, runId);
|
|
710
767
|
}
|
|
711
768
|
async decide(live, run, tool, args) {
|
|
@@ -747,7 +804,7 @@ export class Terminals {
|
|
|
747
804
|
}
|
|
748
805
|
if (inspection.outside)
|
|
749
806
|
return { allowed: false, requiresApproval: true, rememberable: false, standing: 'switch', reason: inspection.reason, workspaceRoot };
|
|
750
|
-
if (name === SESSION_NAME || name ===
|
|
807
|
+
if (name === SESSION_NAME || name === TODO_WRITE || name === SPAWN_SUBAGENT || name === WAIT_SUBAGENTS || READ_ONLY_TOOLS.has(name) || this.worldReadOnly.has(name) || this.options.apps.toolNames.includes(name) || ['bash', 'edit', 'write'].includes(name)) {
|
|
751
808
|
return { allowed: true, workspaceRoot };
|
|
752
809
|
}
|
|
753
810
|
return { allowed: false, requiresApproval: true, rememberable: true, reason: 'This native action requires approval.', workspaceRoot };
|
|
@@ -850,13 +907,22 @@ export class Terminals {
|
|
|
850
907
|
if (params.plan !== undefined && typeof params.plan !== 'boolean')
|
|
851
908
|
throw invalid('plan must be a boolean.');
|
|
852
909
|
const readOnly = live.input.readOnly === true;
|
|
853
|
-
const readTools = new Set([...READ_ONLY_TOOLS, SESSION_NAME,
|
|
910
|
+
const readTools = new Set([...READ_ONLY_TOOLS, SESSION_NAME, TODO_WRITE, SPAWN_SUBAGENT, WAIT_SUBAGENTS, 'ask_user', 'mcp_list_tools', 'mcp_call_tool', 'history_search', 'history_read', 'wiki_search', 'wiki_read', 'wiki_list', 'memory_facts', 'skill_load']);
|
|
854
911
|
const tools = [...new Set(params.tools)].filter(name => (!readOnly || readTools.has(name)) && (params.plan === true || (name !== 'ask_user' && name !== 'ask_user_question')));
|
|
855
912
|
const caller = { ...live.caller, runId: run.runId };
|
|
856
913
|
const capabilities = this.options.resolveCapabilities ? await this.options.resolveCapabilities(caller, tools, live.input.cwd) : { version: 1, tools, connectors: [], contributions: [] };
|
|
914
|
+
const variantDir = this.variantDirs.get(live.input.sessionId) ?? null;
|
|
915
|
+
const subagentProfiles = {
|
|
916
|
+
normal: profileSection(this.options.env.root, live.input.cwd, variantDir).body,
|
|
917
|
+
plan: profileSection(this.options.env.root, live.input.cwd, variantDir, true, {
|
|
918
|
+
frozenTools: capabilities.tools,
|
|
919
|
+
contributions: capabilities.contributions,
|
|
920
|
+
descriptors: HARNESS_PLUGINS
|
|
921
|
+
}).body
|
|
922
|
+
};
|
|
857
923
|
freezeRun(this.options.env.root, run.runId, caller, capabilities);
|
|
858
924
|
if (!service || live.input.subagents === null)
|
|
859
|
-
return { runId: run.runId, connectors: capabilities.connectors };
|
|
925
|
+
return { runId: run.runId, connectors: capabilities.connectors, subagentProfiles };
|
|
860
926
|
service.register({
|
|
861
927
|
run: { id: run.runId, sessionId: live.input.sessionId, mode: live.input.run?.mode ?? 'code', projectId: live.input.projectId, worktreeId: live.input.run?.worktreeId },
|
|
862
928
|
cwd: live.input.cwd, sessionDir: this.historyDir(live.input), text: '', caller: live.caller,
|
|
@@ -868,8 +934,11 @@ export class Terminals {
|
|
|
868
934
|
variant: live.variant ? { dir: live.variant.dir, scope: live.variant.scope } : null,
|
|
869
935
|
capabilities
|
|
870
936
|
}, live.account.id, capabilities);
|
|
871
|
-
|
|
872
|
-
|
|
937
|
+
const history = params.history ?? { owners: [], receipts: [] };
|
|
938
|
+
service.restore(run.runId, history);
|
|
939
|
+
// Children an earlier reply left stopped, reported before the model answers.
|
|
940
|
+
const notice = await service.delivery(run.runId, history, false);
|
|
941
|
+
return { runId: run.runId, connectors: capabilities.connectors, subagentProfiles, notice };
|
|
873
942
|
}
|
|
874
943
|
if (call === 'terminal.subagents.verify') {
|
|
875
944
|
await service.verify(run.runId, params.history);
|
|
@@ -1030,7 +1099,7 @@ export class Terminals {
|
|
|
1030
1099
|
this.variantDirs.set(sessionId, variant.dir);
|
|
1031
1100
|
const worldTools = await this.worldToolNames(input, preparedAccount);
|
|
1032
1101
|
const ownTools = await this.ownToolNames(input, preparedAccount);
|
|
1033
|
-
const launchTools = [...new Set([...(input.launch?.tools ?? []),
|
|
1102
|
+
const launchTools = [...new Set([...(input.launch?.tools ?? []), TODO_WRITE, SESSION_NAME, ...ownTools, ...(broker ? apps.toolNames : []), ...worldTools])];
|
|
1034
1103
|
// The profiles replace the variant's own subagent list, which they include.
|
|
1035
1104
|
const profiles = this.options.subagents && input.subagents !== null ? profileSection(root, input.cwd, variant?.dir ?? null) : null;
|
|
1036
1105
|
const launchSections = profiles ? [...(input.launch?.sections ?? []).filter((section) => section.id !== profiles.id), profiles] : input.launch?.sections;
|
|
@@ -1050,7 +1119,8 @@ export class Terminals {
|
|
|
1050
1119
|
controller: new TerminalController({
|
|
1051
1120
|
sessionId,
|
|
1052
1121
|
nativeSessionId: input.nativeSessionId ?? null,
|
|
1053
|
-
begin: ({ reservedRunId, text }) => this.begin(live, reservedRunId, text),
|
|
1122
|
+
begin: ({ reservedRunId, text, model, effort }) => this.begin(live, reservedRunId, text, { model, effort }),
|
|
1123
|
+
hydrate: (owners) => this.hydrateMonitor(live, owners),
|
|
1054
1124
|
policy: (run, tool, args) => this.decide(live, run, tool, args),
|
|
1055
1125
|
workspaceRoot: run => {
|
|
1056
1126
|
const record = loadRun(this.options.env.root, run.runId);
|
|
@@ -1168,6 +1238,8 @@ export class Terminals {
|
|
|
1168
1238
|
return this.componentsCall(live, call, params);
|
|
1169
1239
|
if (call.startsWith('terminal.trajectory.'))
|
|
1170
1240
|
return this.trajectoryCall(live, call, params);
|
|
1241
|
+
if (call === 'terminal.todo')
|
|
1242
|
+
return latestTerminalTodo(this.options.env.root, live.input.sessionId, live.controller.nativeSessionId);
|
|
1171
1243
|
if (call === 'terminal.permissions.get') {
|
|
1172
1244
|
const policy = this.options.approvals.read();
|
|
1173
1245
|
return { full: this.options.approvals.isAuto('pi'), globalTools: policy.globalTools.pi ?? [] };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { RunEvent, RunRecord } from "../sessions/contracts.js";
|
|
1
|
+
import type { RunEvent, RunRecord, TodoItem } from "../sessions/contracts.js";
|
|
2
2
|
export type TerminalTrajectoryRun = Pick<RunRecord, 'id' | 'mode' | 'goal' | 'startedAt' | 'endedAt' | 'status'>;
|
|
3
3
|
export interface TerminalTrajectorySnapshot {
|
|
4
4
|
run: TerminalTrajectoryRun;
|
|
@@ -6,5 +6,10 @@ export interface TerminalTrajectorySnapshot {
|
|
|
6
6
|
}
|
|
7
7
|
/** The native conversation file is the terminal's boundary for this read-only view. */
|
|
8
8
|
export declare function listTerminalTrajectoryRuns(root: string, sessionId: string, nativeSessionId: string | null): TerminalTrajectoryRun[];
|
|
9
|
+
/** The todo of the latest reply in this native conversation, for a host's /todo view. */
|
|
10
|
+
export declare function latestTerminalTodo(root: string, sessionId: string, nativeSessionId: string | null): {
|
|
11
|
+
runId: string;
|
|
12
|
+
todo: TodoItem[];
|
|
13
|
+
};
|
|
9
14
|
/** Read a run only when it belongs to both this terminal and its current native conversation. */
|
|
10
15
|
export declare function readTerminalTrajectory(root: string, sessionId: string, nativeSessionId: string | null, runId: string, afterSequence?: number): TerminalTrajectorySnapshot;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { notFound } from "../core/errors.js";
|
|
2
2
|
import { findRun, listRuns, readEvents } from "../sessions/runStore.js";
|
|
3
|
+
import { todoOf } from "../tools/todo.js";
|
|
3
4
|
function summary(run) {
|
|
4
5
|
return {
|
|
5
6
|
id: run.id,
|
|
@@ -18,6 +19,13 @@ export function listTerminalTrajectoryRuns(root, sessionId, nativeSessionId) {
|
|
|
18
19
|
.filter(run => run.nativeSessionId === nativeSessionId || run.trigger === 'side-question' && run.sessionId === sessionId)
|
|
19
20
|
.map(summary);
|
|
20
21
|
}
|
|
22
|
+
/** The todo of the latest reply in this native conversation, for a host's /todo view. */
|
|
23
|
+
export function latestTerminalTodo(root, sessionId, nativeSessionId) {
|
|
24
|
+
if (!nativeSessionId)
|
|
25
|
+
return { runId: '', todo: [] };
|
|
26
|
+
const run = listRuns(root, { sessionId }).find(candidate => candidate.nativeSessionId === nativeSessionId && candidate.trigger !== 'side-question');
|
|
27
|
+
return run ? { runId: run.id, todo: todoOf(readEvents(root, run.id)) } : { runId: '', todo: [] };
|
|
28
|
+
}
|
|
21
29
|
/** Read a run only when it belongs to both this terminal and its current native conversation. */
|
|
22
30
|
export function readTerminalTrajectory(root, sessionId, nativeSessionId, runId, afterSequence) {
|
|
23
31
|
if (!nativeSessionId || !Number.isSafeInteger(afterSequence ?? 0) || (afterSequence ?? 0) < 0)
|
|
@@ -62,3 +62,7 @@ export declare function pageFrame({ title, meta, body, footer, width, rows, them
|
|
|
62
62
|
rows: number;
|
|
63
63
|
headerLine?: string;
|
|
64
64
|
}): string[];
|
|
65
|
+
/** A full-width two-pane list uses a 40/60 split once there is room to read both panes. */
|
|
66
|
+
export declare function inspectorListWidth(width: number): number | undefined;
|
|
67
|
+
/** Joins a list and passive detail pane without drawing a box around either. */
|
|
68
|
+
export declare function splitColumns(left: readonly string[], right: readonly string[], leftWidth: number, width: number, rows: number, theme: Theme): string[];
|
|
@@ -167,3 +167,16 @@ export function pageFrame({ title, meta, body, footer, width, rows, theme, heade
|
|
|
167
167
|
footerRow(footer, width, theme)
|
|
168
168
|
].slice(0, rows);
|
|
169
169
|
}
|
|
170
|
+
/** A full-width two-pane list uses a 40/60 split once there is room to read both panes. */
|
|
171
|
+
export function inspectorListWidth(width) {
|
|
172
|
+
return width < 88 ? undefined : Math.max(32, Math.floor((width - 3) * 0.4));
|
|
173
|
+
}
|
|
174
|
+
/** Joins a list and passive detail pane without drawing a box around either. */
|
|
175
|
+
export function splitColumns(left, right, leftWidth, width, rows, theme) {
|
|
176
|
+
const rightWidth = Math.max(0, width - leftWidth - 3);
|
|
177
|
+
return Array.from({ length: Math.max(0, Math.trunc(rows)) }, (_, index) => {
|
|
178
|
+
const leftLine = truncateToWidth(left[index] ?? '', leftWidth, '');
|
|
179
|
+
const rightLine = truncateToWidth(right[index] ?? '', rightWidth, '');
|
|
180
|
+
return leftLine + ' '.repeat(Math.max(0, leftWidth - visibleWidth(leftLine))) + theme.fg('dim', ' │ ') + rightLine;
|
|
181
|
+
});
|
|
182
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { DoctorPage } from "./pages/doctorPage.js";
|
|
2
2
|
import { AccountsPage } from "./pages/accountsPage.js";
|
|
3
3
|
import { BrowserPage } from "./pages/browserPage.js";
|
|
4
|
+
import { PluginsPage } from "./pages/pluginsPage.js";
|
|
5
|
+
import { HubPage } from "./pages/hubPage.js";
|
|
4
6
|
import { LogPage, formatRunEventLog } from "./pages/logPage.js";
|
|
5
7
|
import { PermissionsPage } from "./pages/permissionsPage.js";
|
|
6
8
|
import { SettingsPage } from "./pages/settingsPage.js";
|
|
@@ -40,17 +42,25 @@ export function createCommandPage(host, id, options = {}) {
|
|
|
40
42
|
activeTools: () => host.session().getActiveToolNames(),
|
|
41
43
|
confirm: (title, message) => host.ui.confirm(title, message), notify, changed: host.changed
|
|
42
44
|
});
|
|
45
|
+
case 'plugins':
|
|
46
|
+
return new PluginsPage({
|
|
47
|
+
theme, call: host.control, changed: host.changed,
|
|
48
|
+
ui: { select: host.ui.select.bind(host.ui), confirm: host.ui.confirm.bind(host.ui), notify, input: host.ui.input.bind(host.ui) }
|
|
49
|
+
});
|
|
50
|
+
case 'hub':
|
|
51
|
+
return new HubPage({
|
|
52
|
+
theme, call: host.control, changed: host.changed,
|
|
53
|
+
ui: { select: host.ui.select.bind(host.ui), confirm: host.ui.confirm.bind(host.ui), notify }
|
|
54
|
+
});
|
|
43
55
|
case 'harness':
|
|
44
56
|
case 'skills':
|
|
45
57
|
case 'connections':
|
|
46
58
|
case 'tools':
|
|
47
|
-
case 'plugins':
|
|
48
|
-
case 'hub':
|
|
49
59
|
case 'components':
|
|
50
60
|
return new BrowserPage({
|
|
51
61
|
theme, call: host.control, changed: host.changed,
|
|
52
|
-
section: id === 'harness'
|
|
53
|
-
source:
|
|
62
|
+
section: id === 'harness' ? 'overview' : id === 'components' ? 'library' : id,
|
|
63
|
+
source: 'installed',
|
|
54
64
|
ui: {
|
|
55
65
|
select: host.ui.select.bind(host.ui), confirm: host.ui.confirm.bind(host.ui), notify: host.ui.notify.bind(host.ui), input: host.ui.input.bind(host.ui),
|
|
56
66
|
secretInput: (title, placeholder) => {
|
|
@@ -2,6 +2,7 @@ import { type AgentSession, type ExtensionUIContext } from '@earendil-works/pi-c
|
|
|
2
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
|
+
import type { TodoEntry } from "../opentui/model.js";
|
|
5
6
|
import type { TerminalSubagentActivity } from "../subagentMonitor.js";
|
|
6
7
|
import { type TerminalCommandCompletion, type TerminalCommandHelpGroup } from "../commandHelp.js";
|
|
7
8
|
export type { TerminalCommandCompletion } from "../commandHelp.js";
|
|
@@ -20,6 +21,9 @@ export interface TerminalCommandHost {
|
|
|
20
21
|
refresh(): void;
|
|
21
22
|
changed(): void;
|
|
22
23
|
showHelp(groups: TerminalCommandHelpGroup[]): void;
|
|
24
|
+
/** The latest todo list of this reply, or an empty list. */
|
|
25
|
+
todo(): Promise<readonly TodoEntry[]>;
|
|
26
|
+
showTodo(items: readonly TodoEntry[]): void;
|
|
23
27
|
plan(brief: string): void;
|
|
24
28
|
/** Asks a side question in its own tool-free session; empty text dismisses the one showing. */
|
|
25
29
|
btw(question: string): Promise<void>;
|
|
@@ -399,6 +399,14 @@ export function createTerminalCommands(host) {
|
|
|
399
399
|
case 'trajectory':
|
|
400
400
|
await openPage('trajectory', { runId: argument });
|
|
401
401
|
break;
|
|
402
|
+
case 'todo': {
|
|
403
|
+
const items = await host.todo();
|
|
404
|
+
if (!items.length)
|
|
405
|
+
notify('No todo list in this reply yet. The agent writes one with todo_write for multi-step work.');
|
|
406
|
+
else
|
|
407
|
+
host.showTodo(items);
|
|
408
|
+
break;
|
|
409
|
+
}
|
|
402
410
|
case 'subagents':
|
|
403
411
|
await openPage('subagents');
|
|
404
412
|
break;
|