@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
|
@@ -6,13 +6,16 @@
|
|
|
6
6
|
import { describePlugin, surfaceBadge } from "../../../plugins/surfaces.js";
|
|
7
7
|
import { matchesKey, truncateToWidth, wrapTextWithAnsi } from '@earendil-works/pi-tui';
|
|
8
8
|
import { BACK, applyPluginAction, menuRows, rowActions } from "../../pluginMenu.js";
|
|
9
|
-
import { safeText } from "../chrome.js";
|
|
9
|
+
import { inspectorListWidth, safeText, splitColumns } from "../chrome.js";
|
|
10
10
|
import { FilterList } from "../list.js";
|
|
11
11
|
const SECTIONS = [
|
|
12
12
|
{ id: 'plugins', title: 'Plugins' },
|
|
13
|
-
{ id: 'trust', title: 'Trust' }
|
|
13
|
+
{ id: 'trust', title: 'Trust' },
|
|
14
|
+
{ id: 'packages', title: 'Packages' }
|
|
14
15
|
];
|
|
15
16
|
const SETTINGS_ROW = 'settings:';
|
|
17
|
+
const PACKAGE_ROW = 'package:';
|
|
18
|
+
const INSTALL_PACKAGE = 'action:install-package';
|
|
16
19
|
const isSwitch = (setting) => setting.min === 0 && setting.max === 1;
|
|
17
20
|
const settingValueText = (setting) => isSwitch(setting)
|
|
18
21
|
? setting.value === 1 ? 'on' : 'off'
|
|
@@ -20,14 +23,17 @@ const settingValueText = (setting) => isSwitch(setting)
|
|
|
20
23
|
const settingsOf = (plugin) => plugin.settings ?? [];
|
|
21
24
|
export class PluginsPage {
|
|
22
25
|
options;
|
|
26
|
+
presentation = 'fullscreen';
|
|
23
27
|
plugins;
|
|
24
28
|
section = 'plugins';
|
|
25
29
|
projectId = null;
|
|
30
|
+
hubManagedIds = new Set();
|
|
26
31
|
error;
|
|
27
32
|
restart = false;
|
|
28
33
|
busy = false;
|
|
29
34
|
disposed = false;
|
|
30
35
|
list;
|
|
36
|
+
pointerLayout;
|
|
31
37
|
/** The plugin whose settings are open, or null on the list. */
|
|
32
38
|
settingsFor = null;
|
|
33
39
|
saved = false;
|
|
@@ -70,6 +76,12 @@ export class PluginsPage {
|
|
|
70
76
|
this.projectId = (await call('plugins.session').catch(() => ({ projectId: null }))).projectId;
|
|
71
77
|
this.plugins = await call('plugins.list');
|
|
72
78
|
this.error = undefined;
|
|
79
|
+
this.hubManagedIds = new Set();
|
|
80
|
+
try {
|
|
81
|
+
const overview = await call('hub.list');
|
|
82
|
+
this.hubManagedIds = new Set(overview.installed.filter(receipt => receipt.entry.delivery.kind === 'pi-package').flatMap(receipt => receipt.resourceIds));
|
|
83
|
+
}
|
|
84
|
+
catch { /* Package actions still go through the receipt-aware Harness service. */ }
|
|
73
85
|
}
|
|
74
86
|
catch (error) {
|
|
75
87
|
this.error = error instanceof Error ? error.message : String(error);
|
|
@@ -91,8 +103,17 @@ export class PluginsPage {
|
|
|
91
103
|
searchText: `${setting.label} ${setting.description}`
|
|
92
104
|
})));
|
|
93
105
|
}
|
|
94
|
-
/**
|
|
106
|
+
/** Switch, trust, settings and package actions stay grouped under their owning plugin. */
|
|
95
107
|
rows() {
|
|
108
|
+
if (this.section === 'packages')
|
|
109
|
+
return [
|
|
110
|
+
...(this.plugins ?? []).filter(plugin => plugin.install).map(plugin => {
|
|
111
|
+
const source = plugin.install.kind === 'folder' ? 'local folder' : 'package';
|
|
112
|
+
const hub = this.hubManagedIds.has(plugin.id);
|
|
113
|
+
return { id: `${PACKAGE_ROW}${plugin.id}`, group: safeText(plugin.title), label: 'Manage package', detail: `${source} · ${hub ? 'Hub' : 'local'} · ${plugin.version ?? plugin.install.source}`, tone: 'muted', searchText: `${plugin.title} ${plugin.install.source} ${plugin.version ?? ''} package ${hub ? 'Hub' : 'local'}` };
|
|
114
|
+
}),
|
|
115
|
+
{ id: INSTALL_PACKAGE, group: 'Packages', label: 'Install package…', detail: 'Folder, npm or Git', tone: 'accent', searchText: 'install add package plugin npm Git folder' }
|
|
116
|
+
];
|
|
96
117
|
const menu = this.menu();
|
|
97
118
|
const out = [];
|
|
98
119
|
const placed = new Set();
|
|
@@ -107,18 +128,19 @@ export class PluginsPage {
|
|
|
107
128
|
if (this.section === 'plugins' && last && settingsOf(row.plugin).length)
|
|
108
129
|
out.push(settingsRow(row.plugin));
|
|
109
130
|
});
|
|
110
|
-
if (this.section === 'plugins')
|
|
131
|
+
if (this.section === 'plugins')
|
|
111
132
|
for (const plugin of this.plugins ?? [])
|
|
112
133
|
if (settingsOf(plugin).length && !placed.has(plugin.id))
|
|
113
134
|
out.push(settingsRow(plugin));
|
|
114
|
-
}
|
|
115
135
|
return out;
|
|
116
136
|
}
|
|
117
137
|
menu() {
|
|
118
138
|
const rows = menuRows(this.plugins ?? [], this.projectId);
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
139
|
+
if (this.section === 'trust')
|
|
140
|
+
return rows.filter(row => row.whole && Boolean(row.plugin.code));
|
|
141
|
+
if (this.section === 'packages')
|
|
142
|
+
return [];
|
|
143
|
+
return rows.filter(row => rowActions(row, this.projectId).some(action => action.startsWith('Switch ')));
|
|
122
144
|
}
|
|
123
145
|
key(row) { return `${row.whole ? 'plugin' : 'item'}:${row.id}`; }
|
|
124
146
|
toRow(row) {
|
|
@@ -137,6 +159,16 @@ export class PluginsPage {
|
|
|
137
159
|
this.options.changed();
|
|
138
160
|
return;
|
|
139
161
|
}
|
|
162
|
+
if (id === INSTALL_PACKAGE) {
|
|
163
|
+
await this.installPackage();
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (id.startsWith(PACKAGE_ROW)) {
|
|
167
|
+
const plugin = this.plugins?.find(candidate => candidate.id === id.slice(PACKAGE_ROW.length));
|
|
168
|
+
if (plugin?.install)
|
|
169
|
+
await this.managePackage(plugin);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
140
172
|
const row = this.menu().find(candidate => this.key(candidate) === id);
|
|
141
173
|
if (!row || this.busy)
|
|
142
174
|
return;
|
|
@@ -151,6 +183,74 @@ export class PluginsPage {
|
|
|
151
183
|
}
|
|
152
184
|
await this.reload();
|
|
153
185
|
}
|
|
186
|
+
async installPackage() {
|
|
187
|
+
if (this.busy)
|
|
188
|
+
return;
|
|
189
|
+
this.busy = true;
|
|
190
|
+
try {
|
|
191
|
+
const source = await this.options.ui.input('Install local plugin package', 'npm:package@version, Git address, or absolute folder path');
|
|
192
|
+
if (!source?.trim())
|
|
193
|
+
return;
|
|
194
|
+
const confirmed = await this.options.ui.confirm('Install package?', `${source.trim()} will be installed without running package code. It arrives switched off and code remains untrusted until reviewed.`);
|
|
195
|
+
if (!confirmed)
|
|
196
|
+
return;
|
|
197
|
+
await this.options.call('plugins.install', { source: source.trim() });
|
|
198
|
+
this.restart = true;
|
|
199
|
+
this.options.ui.notify('Installed switched off. Changes apply when this session restarts.', 'info');
|
|
200
|
+
await this.reload();
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
this.options.ui.notify(error instanceof Error ? error.message : String(error), 'error');
|
|
204
|
+
}
|
|
205
|
+
finally {
|
|
206
|
+
this.busy = false;
|
|
207
|
+
if (!this.disposed)
|
|
208
|
+
this.options.changed();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
async managePackage(plugin) {
|
|
212
|
+
if (this.busy || !plugin.install)
|
|
213
|
+
return;
|
|
214
|
+
const hubManaged = this.hubManagedIds.has(plugin.id);
|
|
215
|
+
const actions = [
|
|
216
|
+
...(hubManaged || plugin.install.kind === 'folder' ? [] : ['Update package']),
|
|
217
|
+
hubManaged ? 'Uninstall Hub version' : 'Remove package',
|
|
218
|
+
BACK
|
|
219
|
+
];
|
|
220
|
+
this.busy = true;
|
|
221
|
+
try {
|
|
222
|
+
const action = await this.options.ui.select(`${plugin.title}: ${plugin.install.source}`, actions);
|
|
223
|
+
if (!action || action === BACK)
|
|
224
|
+
return;
|
|
225
|
+
if (action === 'Update package') {
|
|
226
|
+
const confirmed = await this.options.ui.confirm(`Update ${plugin.title}?`, 'Fetches a newer package version without running its scripts. Changed code remains subject to trust review.');
|
|
227
|
+
if (!confirmed)
|
|
228
|
+
return;
|
|
229
|
+
await this.options.call('plugins.update', { pluginId: plugin.id });
|
|
230
|
+
this.options.ui.notify('Updated. Changes apply when this session restarts.', 'info');
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
const title = hubManaged ? `Uninstall ${plugin.title} from Hub?` : `Remove ${plugin.title}?`;
|
|
234
|
+
const message = hubManaged
|
|
235
|
+
? 'This removes the exact Hub receipt and only its recorded package resources.'
|
|
236
|
+
: 'This removes the installed package. A local folder is moved to the Harness trash.';
|
|
237
|
+
if (!await this.options.ui.confirm(title, message))
|
|
238
|
+
return;
|
|
239
|
+
await this.options.call('plugins.remove', { pluginId: plugin.id });
|
|
240
|
+
this.options.ui.notify(hubManaged ? 'Hub package uninstalled.' : 'Package removed.', 'info');
|
|
241
|
+
}
|
|
242
|
+
this.restart = true;
|
|
243
|
+
await this.reload();
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
this.options.ui.notify(error instanceof Error ? error.message : String(error), 'error');
|
|
247
|
+
}
|
|
248
|
+
finally {
|
|
249
|
+
this.busy = false;
|
|
250
|
+
if (!this.disposed)
|
|
251
|
+
this.options.changed();
|
|
252
|
+
}
|
|
253
|
+
}
|
|
154
254
|
back() {
|
|
155
255
|
this.settingsFor = null;
|
|
156
256
|
this.saved = false;
|
|
@@ -196,7 +296,12 @@ export class PluginsPage {
|
|
|
196
296
|
}
|
|
197
297
|
}
|
|
198
298
|
handleMouse(event) {
|
|
199
|
-
|
|
299
|
+
const layout = this.pointerLayout;
|
|
300
|
+
if (!layout)
|
|
301
|
+
return undefined;
|
|
302
|
+
if (event.x < 0 || event.x >= layout.width || event.y < layout.top || event.y >= layout.top + layout.rows)
|
|
303
|
+
return { handled: true, render: false };
|
|
304
|
+
return layout.list.handleMouse({ ...event, y: event.y - layout.top });
|
|
200
305
|
}
|
|
201
306
|
handleInput(data) {
|
|
202
307
|
if (this.settingsPlugin()) {
|
|
@@ -206,44 +311,111 @@ export class PluginsPage {
|
|
|
206
311
|
}
|
|
207
312
|
if (matchesKey(data, 'tab') || matchesKey(data, 'shift+tab')) {
|
|
208
313
|
if (!this.list.query) {
|
|
209
|
-
|
|
314
|
+
const direction = matchesKey(data, 'shift+tab') ? -1 : 1;
|
|
315
|
+
const current = SECTIONS.findIndex(section => section.id === this.section);
|
|
316
|
+
this.section = SECTIONS[(current + direction + SECTIONS.length) % SECTIONS.length].id;
|
|
210
317
|
this.fill();
|
|
211
318
|
}
|
|
212
319
|
return true;
|
|
213
320
|
}
|
|
214
321
|
return this.list.handleInput(data);
|
|
215
322
|
}
|
|
323
|
+
selectedPlugin() {
|
|
324
|
+
const selectedId = this.list.selected?.id;
|
|
325
|
+
return selectedId?.startsWith(SETTINGS_ROW)
|
|
326
|
+
? this.plugins?.find(candidate => `${SETTINGS_ROW}${candidate.id}` === selectedId)
|
|
327
|
+
: selectedId?.startsWith(PACKAGE_ROW)
|
|
328
|
+
? this.plugins?.find(candidate => `${PACKAGE_ROW}${candidate.id}` === selectedId)
|
|
329
|
+
: this.menu().find(candidate => this.key(candidate) === selectedId)?.plugin;
|
|
330
|
+
}
|
|
331
|
+
pluginInspector(plugin, width, rows) {
|
|
332
|
+
const theme = this.options.theme();
|
|
333
|
+
if (width < 1 || rows < 1)
|
|
334
|
+
return [];
|
|
335
|
+
if (!plugin) {
|
|
336
|
+
const action = this.list.selected?.id === INSTALL_PACKAGE;
|
|
337
|
+
return [theme.fg('muted', action ? 'Add a package from npm, Git, or a local folder.' : 'Select a plugin to inspect it.')].slice(0, rows).map(line => truncateToWidth(line, width, ''));
|
|
338
|
+
}
|
|
339
|
+
const selectedId = this.list.selected?.id;
|
|
340
|
+
const row = this.menu().find(candidate => this.key(candidate) === selectedId);
|
|
341
|
+
const title = row ? row.whole ? row.plugin.title : row.name : plugin.title;
|
|
342
|
+
const lines = [theme.bold(safeText(title))];
|
|
343
|
+
if (title !== plugin.title)
|
|
344
|
+
lines.push(theme.fg('dim', `Part of ${safeText(plugin.title)}`));
|
|
345
|
+
lines.push('', ...wrapTextWithAnsi(safeText(describePlugin(plugin)), width));
|
|
346
|
+
if (row) {
|
|
347
|
+
const status = this.section === 'trust' && row.whole
|
|
348
|
+
? plugin.trust === 'trusted' ? 'Code trusted' : 'Code not trusted'
|
|
349
|
+
: row.on ? 'Enabled' : 'Disabled';
|
|
350
|
+
lines.push('', theme.fg(row.on ? 'success' : 'dim', status));
|
|
351
|
+
}
|
|
352
|
+
if (plugin.code) {
|
|
353
|
+
lines.push(theme.fg('muted', `Code trust: ${plugin.trust === 'trusted' ? 'trusted' : 'not trusted'}`));
|
|
354
|
+
lines.push(theme.fg('muted', `Fingerprint: ${plugin.code.hash.slice(0, 12)}`));
|
|
355
|
+
}
|
|
356
|
+
if (plugin.install) {
|
|
357
|
+
lines.push('', theme.fg('muted', `Package: ${this.hubManagedIds.has(plugin.id) ? 'Hub receipt' : 'local'}`));
|
|
358
|
+
lines.push(...wrapTextWithAnsi(theme.fg('dim', safeText(plugin.install.source)), width));
|
|
359
|
+
}
|
|
360
|
+
if (settingsOf(plugin).length)
|
|
361
|
+
lines.push('', theme.fg('muted', `${settingsOf(plugin).length} settings`));
|
|
362
|
+
return lines.slice(0, rows).map(line => truncateToWidth(line, width, ''));
|
|
363
|
+
}
|
|
216
364
|
render(width, rows) {
|
|
217
365
|
const theme = this.options.theme();
|
|
218
366
|
if (width < 1 || rows < 1)
|
|
219
367
|
return [];
|
|
220
|
-
if (!this.plugins)
|
|
368
|
+
if (!this.plugins) {
|
|
369
|
+
this.pointerLayout = undefined;
|
|
221
370
|
return [theme.fg(this.error ? 'error' : 'dim', this.error ? `Could not read plugins: ${this.error}` : 'Reading plugins…')];
|
|
371
|
+
}
|
|
222
372
|
const plugin = this.settingsPlugin();
|
|
223
373
|
if (plugin)
|
|
224
374
|
return this.renderSettings(plugin, width, rows);
|
|
225
|
-
const
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
375
|
+
const note = this.restart ? [theme.fg('warning', 'Saved. Changes apply when this session restarts.')] : [];
|
|
376
|
+
const paneWidth = inspectorListWidth(width);
|
|
377
|
+
if (paneWidth !== undefined) {
|
|
378
|
+
const bodyRows = Math.max(0, rows - note.length);
|
|
379
|
+
const rightWidth = Math.max(0, width - paneWidth - 3);
|
|
380
|
+
const listLines = this.list.render(paneWidth, bodyRows);
|
|
381
|
+
this.pointerLayout = { list: this.list, width: paneWidth, top: note.length, rows: listLines.length };
|
|
382
|
+
return [...note, ...splitColumns(listLines, this.pluginInspector(this.selectedPlugin(), rightWidth, bodyRows), paneWidth, width, bodyRows, theme)]
|
|
383
|
+
.slice(0, rows).map(line => truncateToWidth(line, width, ''));
|
|
384
|
+
}
|
|
385
|
+
const selected = this.selectedPlugin();
|
|
229
386
|
const detail = selected && rows >= 8 ? wrapTextWithAnsi(safeText(describePlugin(selected)), width).slice(0, 3) : [];
|
|
230
|
-
const
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
387
|
+
const listRows = Math.max(2, rows - note.length - (detail.length ? detail.length + 1 : 0));
|
|
388
|
+
const listLines = this.list.render(width, listRows);
|
|
389
|
+
this.pointerLayout = { list: this.list, width, top: note.length, rows: Math.min(listLines.length, Math.max(0, rows - note.length)) };
|
|
390
|
+
const lines = [...note, ...listLines];
|
|
391
|
+
if (detail.length)
|
|
392
|
+
lines.push('', ...detail.map(line => theme.fg('muted', line)));
|
|
235
393
|
return lines.slice(0, rows).map(line => truncateToWidth(line, width, ''));
|
|
236
394
|
}
|
|
237
395
|
renderSettings(plugin, width, rows) {
|
|
238
396
|
const theme = this.options.theme();
|
|
239
397
|
const setting = settingsOf(plugin).find(candidate => candidate.key === this.settingsList.selected?.id);
|
|
240
398
|
const range = setting && !isSwitch(setting) ? [`From ${setting.min.toLocaleString('en-US')} to ${setting.max.toLocaleString('en-US')} · default ${setting.default.toLocaleString('en-US')}`] : [];
|
|
399
|
+
const note = this.saved ? [theme.fg('success', 'Saved.')] : [];
|
|
400
|
+
const paneWidth = inspectorListWidth(width);
|
|
401
|
+
if (paneWidth !== undefined) {
|
|
402
|
+
const bodyRows = Math.max(0, rows - note.length);
|
|
403
|
+
const rightWidth = Math.max(0, width - paneWidth - 3);
|
|
404
|
+
const listLines = this.settingsList.render(paneWidth, bodyRows);
|
|
405
|
+
const details = setting
|
|
406
|
+
? [theme.bold(safeText(setting.label)), '', ...wrapTextWithAnsi(safeText(setting.description), rightWidth), '', ...range.map(line => theme.fg('muted', line)), theme.fg('dim', `Current value: ${settingValueText(setting)}`)]
|
|
407
|
+
: [theme.fg('muted', 'Select a setting to review its value and description.')];
|
|
408
|
+
this.pointerLayout = { list: this.settingsList, width: paneWidth, top: note.length, rows: listLines.length };
|
|
409
|
+
return [...note, ...splitColumns(listLines, details, paneWidth, width, bodyRows, theme)]
|
|
410
|
+
.slice(0, rows).map(line => truncateToWidth(line, width, ''));
|
|
411
|
+
}
|
|
241
412
|
const detail = setting && rows >= 8 ? [...wrapTextWithAnsi(safeText(setting.description), width).slice(0, 3), ...range] : [];
|
|
242
|
-
const
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
413
|
+
const listRows = Math.max(2, rows - note.length - (detail.length ? detail.length + 1 : 0));
|
|
414
|
+
const listLines = this.settingsList.render(width, listRows);
|
|
415
|
+
this.pointerLayout = { list: this.settingsList, width, top: note.length, rows: Math.min(listLines.length, Math.max(0, rows - note.length)) };
|
|
416
|
+
const lines = [...note, ...listLines];
|
|
417
|
+
if (detail.length)
|
|
418
|
+
lines.push('', ...detail.map(line => theme.fg('muted', line)));
|
|
247
419
|
return lines.slice(0, rows).map(line => truncateToWidth(line, width, ''));
|
|
248
420
|
}
|
|
249
421
|
}
|
|
@@ -88,9 +88,11 @@ export class SubagentsPage {
|
|
|
88
88
|
activeTab() { return 'subagents'; }
|
|
89
89
|
hint() { return '↑↓ select · type to search'; }
|
|
90
90
|
rows(width = 80, now = this.options.now?.() ?? Date.now()) {
|
|
91
|
+
// The main agent is pinned first so its children read against it.
|
|
91
92
|
const ordered = [...this.options.snapshot()].sort((left, right) => {
|
|
93
|
+
const main = Number(right.relation === 'main') - Number(left.relation === 'main');
|
|
92
94
|
const active = Number(ACTIVE_SUBAGENT_STATUSES.has(right.status)) - Number(ACTIVE_SUBAGENT_STATUSES.has(left.status));
|
|
93
|
-
return active || left.startedAt.localeCompare(right.startedAt) || left.id.localeCompare(right.id);
|
|
95
|
+
return main || active || left.startedAt.localeCompare(right.startedAt) || left.id.localeCompare(right.id);
|
|
94
96
|
});
|
|
95
97
|
return ordered.map(item => {
|
|
96
98
|
const profile = clean(item.profile?.name ?? '') || 'subagent';
|
|
@@ -115,9 +117,10 @@ export class SubagentsPage {
|
|
|
115
117
|
render(width, rows) {
|
|
116
118
|
const items = this.rows(width, this.options.now?.() ?? Date.now());
|
|
117
119
|
this.list.setRows(items);
|
|
118
|
-
const
|
|
120
|
+
const children = items.filter(item => item.data?.relation !== 'main');
|
|
121
|
+
const active = children.filter(item => item.data && ACTIVE_SUBAGENT_STATUSES.has(item.data.status)).length;
|
|
119
122
|
const selected = this.list.selected?.data;
|
|
120
|
-
const header = headerRow('Subagents', `${active} active · ${
|
|
123
|
+
const header = headerRow('Subagents', `${active} active · ${children.length} total`, width, this.options.theme());
|
|
121
124
|
const bodyRows = Math.max(0, rows - 7);
|
|
122
125
|
const listRows = Math.max(1, Math.min(this.list.height, Math.floor(bodyRows * 0.6)));
|
|
123
126
|
const output = [header, '', ...this.list.render(width, listRows), ''];
|
|
@@ -129,8 +132,8 @@ export class SubagentsPage {
|
|
|
129
132
|
output.push(neutralRule(width, theme));
|
|
130
133
|
output.push(padded(theme.fg('muted', `Profile: ${clean(selected.profile?.name ?? '') || 'copy of the parent'}`), width));
|
|
131
134
|
output.push(padded(theme.fg('accent', 'Task'), width));
|
|
132
|
-
output.push(...detailLines(selected.goal || 'Subagent'));
|
|
133
|
-
output.push(padded(theme.fg('muted', `Status: ${[selected.status.replaceAll('_', ' '), subagentTasks(selected.
|
|
135
|
+
output.push(...detailLines(selected.goal || (selected.relation === 'main' ? 'Main agent' : 'Subagent')));
|
|
136
|
+
output.push(padded(theme.fg('muted', `Status: ${[selected.status.replaceAll('_', ' '), subagentTasks(selected.todo), terminalSubagentDuration(selected)].filter(Boolean).join(' · ')}`), width));
|
|
134
137
|
output.push(padded(theme.fg('accent', 'Current activity'), width));
|
|
135
138
|
output.push(...detailLines(selected.activity));
|
|
136
139
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { ToolSpec } from "./types.js";
|
|
2
2
|
export declare const SESSION_NAME = "session_name";
|
|
3
|
+
/** The trigger the prompt and the tool description share, so naming guidance cannot drift. */
|
|
4
|
+
export declare const SESSION_GUIDANCE: string;
|
|
3
5
|
export interface NamedSession {
|
|
4
6
|
readonly sessionName?: string;
|
|
5
7
|
setSessionName(name: string): void;
|
|
@@ -2,13 +2,15 @@
|
|
|
2
2
|
// the current native session; it never receives a target session id from the model.
|
|
3
3
|
import { invalid } from "../core/errors.js";
|
|
4
4
|
export const SESSION_NAME = 'session_name';
|
|
5
|
+
/** The trigger the prompt and the tool description share, so naming guidance cannot drift. */
|
|
6
|
+
export const SESSION_GUIDANCE = 'If the request needs tools or changes state — not a greeting, thanks, or a short conversational reply — call session_name once before your first other tool call, naming the work from the request. ' +
|
|
7
|
+
'Use one short imperative sentence of at most six words. Revise the name only when the overall objective changes; keep the current name when it does not.';
|
|
5
8
|
export function sessionNameTool(current) {
|
|
6
9
|
return {
|
|
7
10
|
name: SESSION_NAME,
|
|
8
11
|
label: 'Name the session',
|
|
9
|
-
description:
|
|
10
|
-
'
|
|
11
|
-
'You may revise a manually assigned name. Keep a useful current name when the objective is unchanged. This only names your own session.',
|
|
12
|
+
description: `Set or revise this session’s name. ${SESSION_GUIDANCE} ` +
|
|
13
|
+
'For example, "Add web browser integrations". You may revise a manually assigned name. This only names your own session.',
|
|
12
14
|
parameters: Type => Type.Object({ name: Type.String({ description: 'One short sentence describing the objective, at most six words' }) }),
|
|
13
15
|
planModeSafe: () => true,
|
|
14
16
|
execute: async (input) => {
|
|
@@ -27,20 +27,22 @@ export function subagentTools(service, parentRunId) {
|
|
|
27
27
|
return [{
|
|
28
28
|
name: SPAWN_SUBAGENT,
|
|
29
29
|
label: 'Start a subagent',
|
|
30
|
-
description: 'Start a fresh child run for independent work. Delegate substantial, bounded tasks that benefit from a separate context, not simple lookups, tiny edits or tightly coupled steps. Prefer a named specialist profile; omit agent only when no profile fits. Plan mode permits only eligible read-only research profiles. Give a short title and a self-contained brief: the child has no conversation history and no more capabilities than you. Returns its run ID immediately by default so you can continue other useful work. Results arrive automatically before you finish. Use wait_subagents when you need results before proceeding, or wait: true for an inline result. Avoid overlapping write assignments. Stopping you stops your children.',
|
|
30
|
+
description: 'Start a fresh child run for independent work. Delegate substantial, bounded tasks that benefit from a separate context, not simple lookups, tiny edits or tightly coupled steps. Prefer a named specialist profile; omit agent only when no profile fits. Plan mode permits only eligible read-only research profiles. Give a short title and a self-contained brief: the child has no conversation history and no more capabilities than you. The optional tools field only narrows the profile and parent intersection; in plan mode, omit it unless you need a stricter subset. Returns its run ID immediately by default so you can continue other useful work. Results arrive automatically before you finish. Use wait_subagents when you need results before proceeding, or wait: true for an inline result. Avoid overlapping write assignments. Stopping you stops your children. To resume a child that stopped before finishing (interrupted or failed), pass continue with its run ID and no title, agent or tools: it reopens that child\'s own conversation with the same profile and tools, and brief becomes an optional extra instruction.',
|
|
31
31
|
planModeSafe: input => service.planSpawnRefusal(parentRunId, input) === null,
|
|
32
32
|
parameters: (Type) => Type.Object({
|
|
33
|
-
title: Type.String({ description: `
|
|
34
|
-
brief: Type.String({ description: '
|
|
33
|
+
title: Type.Optional(Type.String({ description: `Required for a new child: a short imperative title for the roster, at most ${MAX_SUBAGENT_TITLE_WORDS} words, such as "Map the auth flow"` })),
|
|
34
|
+
brief: Type.Optional(Type.String({ description: 'Required for a new child: a self-contained task: goal, relevant context or paths, non-goals, expected result, and acceptance or verification. The child cannot see this conversation. With continue, an optional extra instruction.' })),
|
|
35
|
+
continue: Type.Optional(Type.String({ description: 'The run ID of one of your subagents that stopped before finishing, to continue it from where it stopped instead of starting a fresh child' })),
|
|
35
36
|
agent: Type.Optional(Type.String({ description: 'The name of a specialist profile from your Subagents list; omit only when no listed profile fits (omitting starts an unprofiled copy)' })),
|
|
36
|
-
tools: Type.Optional(Type.Array(Type.String())),
|
|
37
|
+
tools: Type.Optional(Type.Array(Type.String({ description: 'Only request a stricter subset of the profile and parent tools. In plan mode, omit this field to inherit the safe available research intersection.' }))),
|
|
37
38
|
wait: Type.Optional(Type.Boolean({ description: 'Wait for the child and return its result; default false' }))
|
|
38
39
|
}),
|
|
39
40
|
execute: execute(async (input) => {
|
|
40
|
-
const
|
|
41
|
-
|
|
41
|
+
const resume = input.continue === undefined ? undefined : input.continue;
|
|
42
|
+
const title = resume === undefined ? subagentTitle(input.title) : undefined;
|
|
43
|
+
if (resume === undefined && !title)
|
|
42
44
|
throw invalid(`Give the subagent a title of at most ${MAX_SUBAGENT_TITLE_WORDS} words, with no control characters.`);
|
|
43
|
-
const child = await service.spawn({ parentRunId, brief: input.brief, title, agent: input.agent, tools: input.tools, wait: input.wait });
|
|
45
|
+
const child = await service.spawn({ parentRunId, brief: input.brief, title, agent: input.agent, tools: input.tools, wait: input.wait, ...(resume === undefined ? {} : { continue: resume }) });
|
|
44
46
|
if (input.wait === true)
|
|
45
47
|
return reported(parentRunId, service.results({ parentRunId, runIds: [child.runId] }), [child.runId]);
|
|
46
48
|
return { text: JSON.stringify(child), details: { [SUBAGENT_RECEIPT]: { version: 1, ownerRunId: parentRunId, spawned: [child.runId] } } };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type TodoItem, type RunEvent } from "../sessions/contracts.js";
|
|
2
|
+
import type { ActiveRun } from "../sessions/registry.js";
|
|
3
|
+
import type { ToolSpec } from "./types.js";
|
|
4
|
+
export declare const TODO_WRITE = "todo_write";
|
|
5
|
+
/** The trigger the prompt and the tool description share, so progress guidance cannot drift. */
|
|
6
|
+
export declare const TODO_GUIDANCE: string;
|
|
7
|
+
/** The todo list as the model reads it back. */
|
|
8
|
+
export declare function describeTodo(items: readonly TodoItem[]): string;
|
|
9
|
+
/** The latest todo list a run wrote, or an empty list. Reads the pre-rename event too. */
|
|
10
|
+
export declare function todoOf(events: readonly RunEvent[]): TodoItem[];
|
|
11
|
+
export declare function todoTool(run: Pick<ActiveRun, 'emit'>): ToolSpec;
|
|
12
|
+
/** @deprecated Use `TODO_WRITE`; kept for one release so embedders can migrate. */
|
|
13
|
+
export declare const CHECKLIST_WRITE = "todo_write";
|
|
14
|
+
/** @deprecated Use `todoOf`; kept for one release so embedders can migrate. */
|
|
15
|
+
export declare const checklistOf: typeof todoOf;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// The run's own todo list: the model writes the whole list each time, and the run records it as
|
|
2
|
+
// a `todo.updated` event. It only changes run state, so it stays callable in plan mode.
|
|
3
|
+
import { todoItemSchema, MAX_TODO_ITEMS } from "../sessions/contracts.js";
|
|
4
|
+
export const TODO_WRITE = 'todo_write';
|
|
5
|
+
/** The trigger the prompt and the tool description share, so progress guidance cannot drift. */
|
|
6
|
+
export const TODO_GUIDANCE = 'If the work has several distinct steps or will take more than one tool call, call todo_write before your first other tool call with the steps you know so far, and keep the list current as steps start and finish. ' +
|
|
7
|
+
'Before your final reply, write one final update: mark every fully completed step completed and leave unfinished or blocked steps accurate. Never mark unfinished work completed. ' +
|
|
8
|
+
'The person follows your work from it, and a subagent’s todo list shows as its progress in the person’s list of subagents.';
|
|
9
|
+
/** The event a run writes; `checklist.updated` is the pre-rename name, still read for old runs. */
|
|
10
|
+
const TODO_EVENTS = new Set(['todo.updated', 'checklist.updated']);
|
|
11
|
+
const MARK = { pending: '[ ]', in_progress: '[~]', completed: '[x]' };
|
|
12
|
+
/** The todo list as the model reads it back. */
|
|
13
|
+
export function describeTodo(items) {
|
|
14
|
+
if (!items.length)
|
|
15
|
+
return 'The todo list is empty.';
|
|
16
|
+
return items.map((item) => `${MARK[item.status]} ${item.text}`).join('\n');
|
|
17
|
+
}
|
|
18
|
+
/** The latest todo list a run wrote, or an empty list. Reads the pre-rename event too. */
|
|
19
|
+
export function todoOf(events) {
|
|
20
|
+
const last = events.filter((event) => TODO_EVENTS.has(event.type)).at(-1);
|
|
21
|
+
const items = last?.payload?.items;
|
|
22
|
+
return Array.isArray(items) ? items.flatMap((item) => {
|
|
23
|
+
const parsed = todoItemSchema.safeParse(item);
|
|
24
|
+
return parsed.success ? [parsed.data] : [];
|
|
25
|
+
}) : [];
|
|
26
|
+
}
|
|
27
|
+
export function todoTool(run) {
|
|
28
|
+
return {
|
|
29
|
+
name: TODO_WRITE,
|
|
30
|
+
label: 'Write the todo list',
|
|
31
|
+
description: `Keep a todo list of the steps of the current work. The user watches this list to follow your progress. ${TODO_GUIDANCE} ` +
|
|
32
|
+
'Send the whole list every time; it replaces the previous one. Do not use it for a single, simple step or for conversation. ' +
|
|
33
|
+
'Statuses: pending, in_progress, completed. Keep exactly one step in_progress while you work, mark a step completed as soon as it is done (not in batches), and add steps you discover. ' +
|
|
34
|
+
'Only mark a step completed when it is fully done; if something blocks it, keep it in_progress and add a step for the blocker.',
|
|
35
|
+
parameters: (Type) => Type.Object({
|
|
36
|
+
items: Type.Array(Type.Object({
|
|
37
|
+
text: Type.String({ description: 'One step, as a short imperative line' }),
|
|
38
|
+
status: Type.Union([Type.Literal('pending'), Type.Literal('in_progress'), Type.Literal('completed')])
|
|
39
|
+
}), { maxItems: MAX_TODO_ITEMS })
|
|
40
|
+
}),
|
|
41
|
+
readOnly: true,
|
|
42
|
+
planModeSafe: () => true,
|
|
43
|
+
execute: async (input) => {
|
|
44
|
+
if (!Array.isArray(input.items))
|
|
45
|
+
return 'Send items: the whole todo list as an array.';
|
|
46
|
+
if (input.items.length > MAX_TODO_ITEMS)
|
|
47
|
+
return `Keep the todo list to ${MAX_TODO_ITEMS} steps or fewer.`;
|
|
48
|
+
const items = [];
|
|
49
|
+
for (const raw of input.items) {
|
|
50
|
+
const entry = raw;
|
|
51
|
+
const parsed = todoItemSchema.safeParse({ text: typeof entry?.text === 'string' ? entry.text.trim() : entry?.text, status: entry?.status });
|
|
52
|
+
if (!parsed.success)
|
|
53
|
+
return 'Every step needs text of 1 to 500 characters and a status of pending, in_progress or completed.';
|
|
54
|
+
items.push(parsed.data);
|
|
55
|
+
}
|
|
56
|
+
run.emit('todo.updated', { items });
|
|
57
|
+
return describeTodo(items);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/** @deprecated Use `TODO_WRITE`; kept for one release so embedders can migrate. */
|
|
62
|
+
export const CHECKLIST_WRITE = TODO_WRITE;
|
|
63
|
+
/** @deprecated Use `todoOf`; kept for one release so embedders can migrate. */
|
|
64
|
+
export const checklistOf = todoOf;
|