@xingular/harness 1.0.0 → 1.1.1
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 +33 -5
- package/README.md +70 -19
- package/dist/cli.js +10321 -5785
- package/dist/sdk/agents/builtin.d.ts +2 -0
- package/dist/sdk/agents/builtin.js +34 -0
- package/dist/sdk/agents/narrow.d.ts +13 -0
- package/dist/sdk/agents/narrow.js +29 -0
- package/dist/sdk/agents/profiles.d.ts +80 -0
- package/dist/sdk/agents/profiles.js +221 -0
- package/dist/sdk/annotations/bridge.d.ts +42 -0
- package/dist/sdk/annotations/bridge.js +264 -0
- package/dist/sdk/annotations/capture.d.ts +116 -0
- package/dist/sdk/annotations/capture.js +308 -0
- package/dist/sdk/annotations/delivery.d.ts +72 -0
- package/dist/sdk/annotations/delivery.js +130 -0
- package/dist/sdk/annotations/inspect.d.ts +10 -0
- package/dist/sdk/annotations/inspect.js +33 -0
- package/dist/sdk/annotations/isolated.d.ts +6 -0
- package/dist/sdk/annotations/isolated.js +51 -0
- package/dist/sdk/annotations/pageScript.d.ts +28 -0
- package/dist/sdk/annotations/pageScript.js +570 -0
- package/dist/sdk/annotations/part.d.ts +10 -0
- package/dist/sdk/annotations/part.js +144 -0
- package/dist/sdk/annotations/queue.d.ts +4 -0
- package/dist/sdk/annotations/queue.js +40 -0
- package/dist/sdk/annotations/serverMethods.d.ts +3 -0
- package/dist/sdk/annotations/serverMethods.js +62 -0
- package/dist/sdk/annotations/store.d.ts +160 -0
- package/dist/sdk/annotations/store.js +386 -0
- package/dist/sdk/browser/index.d.ts +14 -3
- package/dist/sdk/browser/index.js +11 -4
- package/dist/sdk/browser/serverMethods.d.ts +3 -0
- package/dist/sdk/browser/serverMethods.js +7 -0
- package/dist/sdk/browser/session.d.ts +1 -0
- package/dist/sdk/browser/session.js +45 -9
- package/dist/sdk/cli.js +19 -11
- package/dist/sdk/core/bus.js +1 -1
- package/dist/sdk/core/env.d.ts +1 -1
- package/dist/sdk/core/env.js +1 -1
- package/dist/sdk/doctor/serverMethods.d.ts +3 -0
- package/dist/sdk/doctor/serverMethods.js +8 -0
- package/dist/sdk/harness.d.ts +15 -0
- package/dist/sdk/harness.js +44 -5
- package/dist/sdk/history/recall.js +8 -8
- package/dist/sdk/history/serverMethods.d.ts +3 -0
- package/dist/sdk/history/serverMethods.js +8 -0
- package/dist/sdk/hub/cli.d.ts +1 -1
- package/dist/sdk/hub/cli.js +1 -1
- package/dist/sdk/hub/schema.d.ts +1 -1
- package/dist/sdk/hub/serverMethods.d.ts +3 -0
- package/dist/sdk/hub/serverMethods.js +11 -0
- package/dist/sdk/index.d.ts +10 -0
- package/dist/sdk/index.js +7 -0
- package/dist/sdk/mcp/connections.js +14 -7
- package/dist/sdk/mcp/serverMethods.d.ts +3 -0
- package/dist/sdk/mcp/serverMethods.js +22 -0
- package/dist/sdk/plugins/builtin.d.ts +1 -0
- package/dist/sdk/plugins/builtin.js +128 -33
- package/dist/sdk/plugins/builtinRegistry.d.ts +53 -0
- package/dist/sdk/plugins/builtinRegistry.js +49 -0
- package/dist/sdk/plugins/cli.d.ts +1 -1
- package/dist/sdk/plugins/cli.js +4 -3
- package/dist/sdk/prompt/compile.d.ts +5 -2
- package/dist/sdk/prompt/compile.js +25 -8
- package/dist/sdk/refine/cli.d.ts +1 -1
- package/dist/sdk/refine/cli.js +2 -2
- package/dist/sdk/refine/serverMethods.d.ts +3 -0
- package/dist/sdk/refine/serverMethods.js +20 -0
- package/dist/sdk/server/methods.js +9 -86
- package/dist/sdk/server/params.d.ts +4 -0
- package/dist/sdk/server/params.js +11 -0
- package/dist/sdk/sessions/contracts.d.ts +17 -0
- package/dist/sdk/sessions/contracts.js +42 -3
- package/dist/sdk/sessions/planMode.d.ts +5 -2
- package/dist/sdk/sessions/planMode.js +12 -3
- package/dist/sdk/sessions/reasoningSummaries.d.ts +2 -2
- package/dist/sdk/sessions/reasoningSummaries.js +3 -3
- package/dist/sdk/sessions/registry.d.ts +1 -0
- package/dist/sdk/sessions/registry.js +4 -1
- package/dist/sdk/sessions/runIndex.js +7 -7
- package/dist/sdk/sessions/runStore.js +3 -1
- package/dist/sdk/sessions/runner.js +2 -2
- package/dist/sdk/sessions/sessions.d.ts +17 -5
- package/dist/sdk/sessions/sessions.js +79 -12
- package/dist/sdk/sessions/sideQuestionServerMethods.d.ts +3 -0
- package/dist/sdk/sessions/sideQuestionServerMethods.js +19 -0
- package/dist/sdk/sessions/sideQuestions.d.ts +66 -0
- package/dist/sdk/sessions/sideQuestions.js +290 -0
- package/dist/sdk/sessions/subagentServerMethods.d.ts +3 -0
- package/dist/sdk/sessions/subagentServerMethods.js +41 -0
- package/dist/sdk/sessions/subagentService.d.ts +13 -2
- package/dist/sdk/sessions/subagentService.js +34 -16
- package/dist/sdk/sessions/subagents.d.ts +9 -1
- package/dist/sdk/sessions/subagents.js +19 -0
- package/dist/sdk/terminal/browser.d.ts +62 -0
- package/dist/sdk/terminal/browser.js +41 -0
- package/dist/sdk/terminal/browserControl.d.ts +29 -0
- package/dist/sdk/terminal/browserControl.js +186 -0
- package/dist/sdk/terminal/commandHelp.d.ts +17 -0
- package/dist/sdk/terminal/commandHelp.js +20 -0
- package/dist/sdk/terminal/controller.d.ts +7 -1
- package/dist/sdk/terminal/controller.js +34 -2
- package/dist/sdk/terminal/opentui/subagentsWidget.d.ts +28 -0
- package/dist/sdk/terminal/opentui/subagentsWidget.js +52 -0
- package/dist/sdk/terminal/piExtension.js +33 -48
- package/dist/sdk/terminal/socket.d.ts +6 -3
- package/dist/sdk/terminal/socket.js +40 -9
- package/dist/sdk/terminal/standalone.d.ts +2 -0
- package/dist/sdk/terminal/standalone.js +50 -10
- package/dist/sdk/terminal/subagentMonitor.d.ts +38 -0
- package/dist/sdk/terminal/subagentMonitor.js +121 -0
- package/dist/sdk/terminal/terminals.d.ts +42 -6
- package/dist/sdk/terminal/terminals.js +359 -18
- package/dist/sdk/terminal/trajectory.js +2 -2
- package/dist/sdk/terminal/tui/annotateCommand.d.ts +3 -0
- package/dist/sdk/terminal/tui/annotateCommand.js +22 -0
- package/dist/sdk/terminal/tui/commandPages.d.ts +5 -1
- package/dist/sdk/terminal/tui/commandPages.js +54 -6
- package/dist/sdk/terminal/tui/commands.d.ts +8 -4
- package/dist/sdk/terminal/tui/commands.js +181 -58
- package/dist/sdk/terminal/tui/dialogGeometry.d.ts +2 -2
- package/dist/sdk/terminal/tui/dialogGeometry.js +4 -2
- package/dist/sdk/terminal/tui/dialogs.d.ts +6 -1
- package/dist/sdk/terminal/tui/dialogs.js +9 -2
- package/dist/sdk/terminal/tui/modelPicker.js +1 -6
- package/dist/sdk/terminal/tui/page.d.ts +3 -0
- package/dist/sdk/terminal/tui/pageOverlay.js +17 -6
- package/dist/sdk/terminal/tui/pages/accountsPage.d.ts +26 -4
- package/dist/sdk/terminal/tui/pages/accountsPage.js +264 -36
- package/dist/sdk/terminal/tui/pages/agentsPage.d.ts +74 -0
- package/dist/sdk/terminal/tui/pages/agentsPage.js +190 -0
- package/dist/sdk/terminal/tui/pages/annotatePage.d.ts +94 -0
- package/dist/sdk/terminal/tui/pages/annotatePage.js +341 -0
- package/dist/sdk/terminal/tui/pages/browserPage.d.ts +87 -0
- package/dist/sdk/terminal/tui/pages/browserPage.js +738 -0
- package/dist/sdk/terminal/tui/pages/doctorPage.d.ts +2 -0
- package/dist/sdk/terminal/tui/pages/doctorPage.js +103 -15
- package/dist/sdk/terminal/tui/pages/notesPage.d.ts +75 -0
- package/dist/sdk/terminal/tui/pages/notesPage.js +201 -0
- package/dist/sdk/terminal/tui/pages/settingsPage.d.ts +8 -0
- package/dist/sdk/terminal/tui/pages/settingsPage.js +16 -1
- package/dist/sdk/terminal/tui/pages/subagentsPage.d.ts +23 -0
- package/dist/sdk/terminal/tui/pages/subagentsPage.js +70 -0
- package/dist/sdk/terminal/tui/pages/trajectoryPage.d.ts +12 -5
- package/dist/sdk/terminal/tui/pages/trajectoryPage.js +156 -83
- package/dist/sdk/terminal/tui/pages/worktreesPage.d.ts +72 -0
- package/dist/sdk/terminal/tui/pages/worktreesPage.js +203 -0
- package/dist/sdk/terminal/tui/planMode.d.ts +1 -1
- package/dist/sdk/terminal/tui/planMode.js +1 -1
- package/dist/sdk/terminal/tui/pluginCommands.d.ts +14 -0
- package/dist/sdk/terminal/tui/pluginCommands.js +27 -0
- package/dist/sdk/terminal/tui/questions.d.ts +2 -0
- package/dist/sdk/terminal/tui/runtime.js +4 -2
- package/dist/sdk/terminal/tui/settingsModel.d.ts +2 -0
- package/dist/sdk/terminal/tui/settingsModel.js +32 -2
- package/dist/sdk/terminal/tui/sideQuestion.d.ts +26 -26
- package/dist/sdk/terminal/tui/sideQuestion.js +131 -98
- package/dist/sdk/terminal/tui/ui.d.ts +2 -0
- package/dist/sdk/terminal/tui/ui.js +1 -0
- package/dist/sdk/tools/askUser.js +26 -8
- package/dist/sdk/tools/checklist.js +2 -2
- package/dist/sdk/tools/harnessTools.d.ts +2 -0
- package/dist/sdk/tools/harnessTools.js +1 -1
- package/dist/sdk/tools/spawnSubagent.js +13 -4
- package/dist/sdk/web/part.d.ts +10 -0
- package/dist/sdk/web/part.js +6 -0
- package/dist/sdk/web/serverMethods.d.ts +3 -0
- package/dist/sdk/web/serverMethods.js +7 -0
- package/dist/sdk/wiki/serverMethods.d.ts +3 -0
- package/dist/sdk/wiki/serverMethods.js +26 -0
- package/dist/sdk/worktrees/cli.d.ts +11 -0
- package/dist/sdk/worktrees/cli.js +146 -0
- package/dist/sdk/worktrees/copy.d.ts +9 -0
- package/dist/sdk/worktrees/copy.js +111 -0
- package/dist/sdk/worktrees/git.d.ts +16 -0
- package/dist/sdk/worktrees/git.js +81 -0
- package/dist/sdk/worktrees/serverMethods.d.ts +3 -0
- package/dist/sdk/worktrees/serverMethods.js +30 -0
- package/dist/sdk/worktrees/service.d.ts +73 -0
- package/dist/sdk/worktrees/service.js +483 -0
- package/dist/sdk/worktrees/setup.d.ts +23 -0
- package/dist/sdk/worktrees/setup.js +65 -0
- package/dist/sdk/worktrees/types.d.ts +91 -0
- package/dist/sdk/worktrees/types.js +1 -0
- package/dist/sdk/world/cli.d.ts +2 -2
- package/dist/sdk/world/cli.js +6 -6
- package/dist/sdk/world/world.d.ts +3 -1
- package/dist/sdk/world/world.js +8 -9
- package/dist/terminal-opentui.js +14540 -7374
- package/dist/terminal-pi.js +285 -666
- package/docs/README.md +22 -2
- package/docs/architecture/00-overview.md +28 -16
- package/docs/architecture/02-sessions-and-runs.md +72 -15
- package/docs/architecture/03-variants.md +10 -2
- package/docs/architecture/04-prompt.md +26 -6
- package/docs/architecture/06-plugins-and-capabilities.md +39 -3
- package/docs/architecture/07-tools-and-gate.md +21 -6
- package/docs/architecture/09-accounts-and-connectors.md +8 -0
- package/docs/architecture/10-server-protocol.md +111 -2
- package/docs/architecture/11-terminal.md +185 -5
- package/docs/architecture/12-storage.md +15 -1
- package/docs/architecture/13-world-tools.md +28 -6
- package/docs/architecture/14-surfaces-and-parity.md +89 -0
- package/docs/architecture/README.md +15 -4
- package/docs/architecture/proposals/PROP-0006-run-checklist.md +4 -2
- package/docs/architecture/proposals/PROP-0013-resource-browser.md +58 -0
- package/docs/architecture/proposals/PROP-0014-fullscreen-browser-tabs.md +58 -0
- package/docs/architecture/proposals/PROP-0015-one-capability-surface.md +88 -0
- package/docs/architecture/proposals/PROP-0016-managed-worktrees.md +156 -0
- package/docs/architecture/proposals/PROP-0017-page-annotations.md +127 -0
- package/docs/architecture/proposals/PROP-0018-annotation-queue-and-app-targets.md +179 -0
- package/docs/architecture/proposals/PROP-0019-contextual-side-questions.md +122 -0
- package/docs/architecture/proposals/PROP-0020-builtin-feature-plugins.md +105 -0
- package/docs/architecture/proposals/PROP-0021-command-namespaces-and-duplicates.md +99 -0
- package/docs/architecture/proposals/PROP-0022-plan-only-questions.md +102 -0
- package/docs/architecture/proposals/PROP-0023-xagent-cli-name.md +61 -0
- package/docs/architecture/proposals/PROP-0024-question-sketch-previews.md +130 -0
- package/docs/architecture/proposals/PROP-0025-contextual-question-wireframes.md +49 -0
- package/docs/architecture/proposals/PROP-0026-connected-sketch-display.md +94 -0
- package/docs/architecture/proposals/PROP-0027-xingular-agent-product-identity.md +60 -0
- package/docs/architecture/proposals/PROP-0028-normalize-question-wireframes.md +55 -0
- package/docs/architecture/proposals/PROP-0029-agent-profiles.md +124 -0
- package/docs/architecture/proposals/PROP-0030-subagent-title.md +86 -0
- package/docs/architecture/proposals/README.md +25 -0
- package/docs/decisions/ADR-0009-xharness-command.md +6 -5
- package/docs/decisions/ADR-0014-public-release-baseline.md +3 -2
- package/docs/decisions/ADR-0015-resource-browser.md +28 -0
- package/docs/decisions/ADR-0016-fullscreen-browser-tabs.md +31 -0
- package/docs/decisions/ADR-0017-one-capability-surface.md +34 -0
- package/docs/decisions/ADR-0018-builtin-feature-plugins.md +35 -0
- package/docs/decisions/ADR-0019-managed-git-worktrees.md +31 -0
- package/docs/decisions/ADR-0020-page-annotations.md +37 -0
- package/docs/decisions/ADR-0021-required-command-namespaces.md +40 -0
- package/docs/decisions/ADR-0022-plan-only-question-tools.md +34 -0
- package/docs/decisions/ADR-0023-contextual-side-question-runs.md +35 -0
- package/docs/decisions/ADR-0024-xagent-command.md +40 -0
- package/docs/decisions/ADR-0025-question-choice-sketches.md +49 -0
- package/docs/decisions/ADR-0026-contextual-question-wireframes.md +58 -0
- package/docs/decisions/ADR-0027-connected-sketch-display.md +60 -0
- package/docs/decisions/ADR-0028-xingular-agent-product-identity.md +56 -0
- package/docs/decisions/ADR-0029-normalized-question-wireframe-borders.md +33 -0
- package/docs/decisions/ADR-0030-agent-profiles.md +43 -0
- package/docs/doctor.md +10 -4
- package/docs/features/README.md +26 -0
- package/docs/features/interfaces/README.md +1 -1
- package/docs/future-work.md +48 -0
- package/docs/parity.md +78 -0
- package/docs/releases/1.0.0.md +2 -2
- package/docs/releases/4.0.0.md +46 -0
- package/docs/releases/README.md +20 -15
- package/docs/study/00-index.md +5 -0
- package/docs/study/autonomous-engineering-sources.md +277 -0
- package/docs/study/autonomy-sources.md +147 -0
- package/docs/study/proactive-autonomy.md +408 -0
- package/docs/study/tui-resource-browser.md +78 -0
- package/docs/terminal-tui.md +325 -93
- package/package.json +3 -3
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as path from 'node:path';
|
|
2
|
+
import type { Hub } from "../hub/hub.js";
|
|
3
|
+
import type { ConnectedApps } from "../mcp/service.js";
|
|
4
|
+
import type { Plugin } from "../plugins/types.js";
|
|
5
|
+
import type { Materialised } from "../variant/materialise.js";
|
|
6
|
+
import type { BrowserSnapshot } from "./browser.js";
|
|
7
|
+
type PathApi = Pick<typeof path, 'relative' | 'sep' | 'isAbsolute'>;
|
|
8
|
+
export declare function isWithinBoundary(boundary: string, file: string, pathApi?: PathApi): boolean;
|
|
9
|
+
export interface BrowserControlOptions {
|
|
10
|
+
root: string;
|
|
11
|
+
cwd: string;
|
|
12
|
+
pinned: Materialised | null;
|
|
13
|
+
writable: boolean;
|
|
14
|
+
plugins(): Promise<Plugin[]>;
|
|
15
|
+
hub?: Hub;
|
|
16
|
+
apps: ConnectedApps;
|
|
17
|
+
}
|
|
18
|
+
/** Only the protected terminal control route calls this adapter. It never loads discovered code. */
|
|
19
|
+
export declare class BrowserControl {
|
|
20
|
+
private readonly options;
|
|
21
|
+
constructor(options: BrowserControlOptions);
|
|
22
|
+
snapshot(refresh?: boolean): Promise<BrowserSnapshot>;
|
|
23
|
+
read(id: string, latestVersion?: boolean): Promise<{
|
|
24
|
+
content: string;
|
|
25
|
+
}>;
|
|
26
|
+
importSkill(input: string): void;
|
|
27
|
+
connection(action: string, params: Record<string, unknown>): Promise<unknown>;
|
|
28
|
+
}
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { constants, closeSync, fstatSync, lstatSync, openSync, readFileSync, readdirSync, realpathSync } from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
4
|
+
import { invalid } from "../core/errors.js";
|
|
5
|
+
import { connectionInputSchema } from "../mcp/types.js";
|
|
6
|
+
import * as library from "../variant/components.js";
|
|
7
|
+
import { PI_SURFACE, componentOf } from "../variant/surface.js";
|
|
8
|
+
import { headVariant } from "../variant/store.js";
|
|
9
|
+
const MAX_CONTENT = 256 * 1024;
|
|
10
|
+
export function isWithinBoundary(boundary, file, pathApi = path) {
|
|
11
|
+
const within = pathApi.relative(boundary, file);
|
|
12
|
+
return within !== '..' && !within.startsWith(`..${pathApi.sep}`) && !pathApi.isAbsolute(within);
|
|
13
|
+
}
|
|
14
|
+
function readText(path) {
|
|
15
|
+
const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
16
|
+
try {
|
|
17
|
+
const stat = fstatSync(fd);
|
|
18
|
+
if (!stat.isFile() || stat.size > MAX_CONTENT)
|
|
19
|
+
throw invalid('Preview requires a regular text file smaller than 256 KiB.');
|
|
20
|
+
return readFileSync(fd, 'utf8');
|
|
21
|
+
}
|
|
22
|
+
finally {
|
|
23
|
+
closeSync(fd);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** Only the protected terminal control route calls this adapter. It never loads discovered code. */
|
|
27
|
+
export class BrowserControl {
|
|
28
|
+
options;
|
|
29
|
+
constructor(options) {
|
|
30
|
+
this.options = options;
|
|
31
|
+
}
|
|
32
|
+
async snapshot(refresh = false) {
|
|
33
|
+
const { root, pinned, apps } = this.options;
|
|
34
|
+
const plugins = await this.options.plugins();
|
|
35
|
+
const scope = pinned?.scope ?? null;
|
|
36
|
+
const latest = scope ? headVariant(root, scope) : null;
|
|
37
|
+
const items = [];
|
|
38
|
+
const add = (item) => items.push(item);
|
|
39
|
+
for (const [id, name, description] of [
|
|
40
|
+
['runtime', 'Runtime and recording', 'The foundation runs sessions and records their events. Changes require a release.'],
|
|
41
|
+
['prompt', 'Prompt compiler', 'Assembles the stable prompt and context updates. Variant c1–c4 supply configurable instructions.'],
|
|
42
|
+
['loader', 'Resource loader', 'Selects plugins and their contributions. Discovered code waits for exact-code trust.'],
|
|
43
|
+
['gate', 'Tool approval gate', 'Checks capabilities, policy and approvals for every tool call. Availability never grants authorization.'],
|
|
44
|
+
['accounts', 'Accounts and secrets', 'Keeps account identities isolated and credentials in the secret store.'],
|
|
45
|
+
['surface', 'Variant surface', 'Defines c1–c8, validation and immutable versions. Only a validated variant version may change.']
|
|
46
|
+
])
|
|
47
|
+
add({ id: `foundation:${id}`, section: 'foundation', name: name, description: description, owner: 'Foundation', origin: 'Built-in', status: 'Read-only' });
|
|
48
|
+
for (const plugin of plugins) {
|
|
49
|
+
add({ id: `plugin:${plugin.id}`, section: 'plugins', name: plugin.title, description: plugin.description,
|
|
50
|
+
owner: plugin.title, origin: plugin.source, status: plugin.required ? 'Required' : plugin.trust === 'untrusted' ? 'Needs trust' : 'Saved configuration',
|
|
51
|
+
pluginId: plugin.id, path: plugin.path ?? undefined, required: plugin.required });
|
|
52
|
+
for (const entry of plugin.contributions) {
|
|
53
|
+
if (!['skill', 'tool', 'command', 'instruction'].includes(entry.kind))
|
|
54
|
+
continue;
|
|
55
|
+
if ((entry.kind === 'command' || entry.kind === 'instruction') && !entry.path)
|
|
56
|
+
continue;
|
|
57
|
+
const section = entry.kind === 'skill' ? 'skills' : entry.kind === 'tool' ? 'tools' : 'library';
|
|
58
|
+
add({ id: `contribution:${entry.id}`, section, name: entry.name, description: entry.description,
|
|
59
|
+
owner: plugin.title, origin: plugin.source, pluginId: plugin.id, contributionId: entry.id,
|
|
60
|
+
path: entry.path ?? undefined, scope: entry.scope, status: entry.state === 'off' ? 'Disabled' : entry.state === 'untrusted' ? 'Needs trust' : 'Enabled for future sessions',
|
|
61
|
+
read: Boolean(entry.path && ['skill', 'command', 'instruction'].includes(entry.kind)) });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
for (const item of library.list(root)) {
|
|
65
|
+
const added = Boolean(latest && library.isInVariant(root, latest.variant, item.id));
|
|
66
|
+
const existing = items.find(existing => existing.path === item.path);
|
|
67
|
+
if (existing) {
|
|
68
|
+
existing.componentId = item.id;
|
|
69
|
+
existing.added = added;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
add({ id: `library:${item.id}`, section: item.kind === 'skills' ? 'skills' : 'library', name: item.name,
|
|
73
|
+
description: item.description, owner: 'Reusable library', origin: item.builtin ? 'Built-in library' : 'Local library', path: item.path,
|
|
74
|
+
componentId: item.id, added, status: added ? 'Saved in latest variant' : 'Library only', read: true });
|
|
75
|
+
}
|
|
76
|
+
const paths = new Set([...Object.keys(pinned?.files ?? {}), ...Object.keys(latest?.variant ?? {})]);
|
|
77
|
+
for (const path of [...paths].sort()) {
|
|
78
|
+
const component = PI_SURFACE.components.find(entry => entry.id === componentOf(PI_SURFACE, path));
|
|
79
|
+
if (!component)
|
|
80
|
+
continue;
|
|
81
|
+
const current = pinned?.files[path];
|
|
82
|
+
const future = latest?.variant[path];
|
|
83
|
+
const status = !latest || current === future ? 'Pinned version' : current === undefined ? 'Added in latest' : future === undefined ? 'Removed in latest' : 'Changed in latest';
|
|
84
|
+
add({ id: `variant:${path}`, section: 'variant', name: path, description: `c${component.index} · ${component.title}: ${component.description}`,
|
|
85
|
+
owner: 'Variant', origin: 'Variant', status, read: true });
|
|
86
|
+
if (/^skills\/[^/]+\/SKILL\.md$/.test(path) && current !== undefined) {
|
|
87
|
+
const name = path.split('/')[1];
|
|
88
|
+
const fullPath = join(pinned.dir, path);
|
|
89
|
+
if (!items.some(item => item.section === 'skills' && item.path === fullPath))
|
|
90
|
+
add({ id: `variant-skill:${path}`, section: 'skills', name,
|
|
91
|
+
description: /^description:\s*(.+)$/m.exec(current)?.[1] ?? '', owner: 'Variant', origin: 'Variant', status: 'Prepared for this session',
|
|
92
|
+
path: fullPath, read: true });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const connections = apps.connections.list().map(connection => ({ ...connection,
|
|
96
|
+
error: connection.error ? String(apps.connections.scrub(connection.error, connection.id)) : null,
|
|
97
|
+
tools: apps.connections.scrub(connection.tools, connection.id) }));
|
|
98
|
+
for (const connection of connections)
|
|
99
|
+
add({ id: `connection:${connection.id}`, section: 'connections', name: connection.name || connection.id,
|
|
100
|
+
description: `${connection.kind === 'remote' ? 'Remote HTTP' : 'Local stdio'} MCP · ${connection.tools.length} tools${connection.checkedAt ? ` · last checked ${connection.checkedAt}` : ' · never tested'}`,
|
|
101
|
+
owner: 'Connected apps', origin: connection.kind === 'remote' ? 'Remote' : 'Local', connectionId: connection.id,
|
|
102
|
+
scope: connection.global ? 'Everywhere' : connection.projectIds.join(', ') || 'No project scope',
|
|
103
|
+
status: !connection.enabled ? 'Disabled' : connection.auth === 'oauth' && connection.authState !== 'signed_in' ? 'Sign in required' : connection.error ? 'Last check failed' : connection.checkedAt ? 'Last check passed' : 'Not tested' });
|
|
104
|
+
return { items, plugins, connections, variant: { scope, pinned: pinned?.fingerprint ?? null, latest: latest?.fingerprint ?? null,
|
|
105
|
+
writable: this.options.writable && Boolean(scope && latest) }, hub: this.options.hub ? await this.options.hub.list(refresh) : null };
|
|
106
|
+
}
|
|
107
|
+
async read(id, latestVersion = false) {
|
|
108
|
+
const { root, pinned } = this.options;
|
|
109
|
+
const snapshot = await this.snapshot();
|
|
110
|
+
const item = snapshot.items.find(entry => entry.id === id);
|
|
111
|
+
if (!item?.read)
|
|
112
|
+
throw invalid('This resource has no inspectable content.');
|
|
113
|
+
if (id.startsWith('variant:') || id.startsWith('variant-skill:')) {
|
|
114
|
+
const path = id.slice(id.indexOf(':') + 1);
|
|
115
|
+
const files = latestVersion && pinned?.scope ? headVariant(root, pinned.scope)?.variant : pinned?.files;
|
|
116
|
+
const content = files?.[path] ?? '(File absent in this version.)';
|
|
117
|
+
return { content: content.length > MAX_CONTENT ? content.slice(0, MAX_CONTENT) + '\n[Preview truncated.]' : content };
|
|
118
|
+
}
|
|
119
|
+
if (!item.path)
|
|
120
|
+
throw invalid('This resource has no file.');
|
|
121
|
+
const plugin = snapshot.plugins.find(plugin => plugin.id === item.pluginId);
|
|
122
|
+
const registryItem = item.componentId ? library.list(root).find(entry => entry.id === item.componentId) : undefined;
|
|
123
|
+
const boundary = realpathSync(registryItem && !registryItem.builtin ? join(root, 'registry') : plugin?.path ?? dirname(item.path));
|
|
124
|
+
const file = realpathSync(item.path);
|
|
125
|
+
if (!isWithinBoundary(boundary, file))
|
|
126
|
+
throw invalid('Resource is outside its owner folder.');
|
|
127
|
+
return { content: readText(file) };
|
|
128
|
+
}
|
|
129
|
+
importSkill(input) {
|
|
130
|
+
const start = resolve(this.options.cwd, input);
|
|
131
|
+
const folder = lstatSync(start).isDirectory() ? start : dirname(start);
|
|
132
|
+
if (start !== folder && basename(start) !== 'SKILL.md')
|
|
133
|
+
throw invalid('Choose a skill folder or its SKILL.md.');
|
|
134
|
+
const files = [];
|
|
135
|
+
let bytes = 0;
|
|
136
|
+
let directories = 0;
|
|
137
|
+
const visit = (directory, depth = 0) => {
|
|
138
|
+
if (++directories > 100 || depth > 16)
|
|
139
|
+
throw invalid('Skill import exceeds its directory limit.');
|
|
140
|
+
for (const name of readdirSync(directory)) {
|
|
141
|
+
if (name.startsWith('.'))
|
|
142
|
+
continue;
|
|
143
|
+
const path = join(directory, name);
|
|
144
|
+
const stat = lstatSync(path);
|
|
145
|
+
if (stat.isSymbolicLink())
|
|
146
|
+
throw invalid('Skill imports cannot contain symbolic links.');
|
|
147
|
+
if (stat.isDirectory()) {
|
|
148
|
+
visit(path, depth + 1);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (!stat.isFile() || files.length >= 100 || stat.size > 10 * 1024 * 1024)
|
|
152
|
+
throw invalid('Skill import exceeds its file limit.');
|
|
153
|
+
bytes += stat.size;
|
|
154
|
+
if (bytes > 10 * 1024 * 1024)
|
|
155
|
+
throw invalid('Skill import exceeds 10 MiB.');
|
|
156
|
+
files.push({ path: relative(folder, path), content: readFileSync(path).toString('base64'), encoding: 'base64' });
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
if (lstatSync(folder).isSymbolicLink())
|
|
160
|
+
throw invalid('Choose a regular skill folder.');
|
|
161
|
+
visit(folder);
|
|
162
|
+
library.importItems(this.options.root, 'skills', [{ name: basename(folder), files }]);
|
|
163
|
+
}
|
|
164
|
+
async connection(action, params) {
|
|
165
|
+
const apps = this.options.apps;
|
|
166
|
+
if (action === 'appClients')
|
|
167
|
+
return apps.appClients();
|
|
168
|
+
if (action === 'save')
|
|
169
|
+
return apps.save(connectionInputSchema.parse(params.input));
|
|
170
|
+
const id = typeof params.id === 'string' ? params.id : '';
|
|
171
|
+
if (!apps.connections.list().some(connection => connection.id === id))
|
|
172
|
+
throw invalid('Unknown connection.');
|
|
173
|
+
if (action === 'test') {
|
|
174
|
+
const result = await apps.test(id);
|
|
175
|
+
return { ...result, error: result.error ? String(apps.connections.scrub(result.error, id)) : null,
|
|
176
|
+
tools: apps.connections.scrub(result.tools, id) };
|
|
177
|
+
}
|
|
178
|
+
if (action === 'signIn')
|
|
179
|
+
return apps.signIn(id);
|
|
180
|
+
if (action === 'signOut')
|
|
181
|
+
return apps.signOut(id);
|
|
182
|
+
if (action === 'remove')
|
|
183
|
+
return apps.remove(id);
|
|
184
|
+
throw invalid('Unknown connection action.');
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface TerminalCommandCompletion {
|
|
2
|
+
name: string;
|
|
3
|
+
aliases?: string[];
|
|
4
|
+
description?: string;
|
|
5
|
+
group: string;
|
|
6
|
+
}
|
|
7
|
+
export interface TerminalCommandHelpEntry {
|
|
8
|
+
name: string;
|
|
9
|
+
aliases?: string[];
|
|
10
|
+
description?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function formatTerminalCommandLabel(name: string, aliases?: readonly string[]): string;
|
|
13
|
+
export interface TerminalCommandHelpGroup {
|
|
14
|
+
name: string;
|
|
15
|
+
commands: TerminalCommandHelpEntry[];
|
|
16
|
+
}
|
|
17
|
+
export declare function groupTerminalCommands(commands: readonly TerminalCommandCompletion[]): TerminalCommandHelpGroup[];
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function formatTerminalCommandLabel(name, aliases) {
|
|
2
|
+
return aliases?.length ? `${name} (${aliases.join(', ')})` : name;
|
|
3
|
+
}
|
|
4
|
+
const GROUP_ORDER = ['Core', 'Run', 'Model', 'Session', 'Inspect', 'Harness', 'Plugins', 'Extensions', 'Prompts'];
|
|
5
|
+
export function groupTerminalCommands(commands) {
|
|
6
|
+
const groups = new Map();
|
|
7
|
+
for (const command of commands) {
|
|
8
|
+
const group = groups.get(command.group) ?? [];
|
|
9
|
+
group.push({
|
|
10
|
+
name: command.name,
|
|
11
|
+
...(command.aliases?.length ? { aliases: command.aliases } : {}),
|
|
12
|
+
description: command.description
|
|
13
|
+
});
|
|
14
|
+
groups.set(command.group, group);
|
|
15
|
+
}
|
|
16
|
+
const order = new Map(GROUP_ORDER.map((name, index) => [name, index]));
|
|
17
|
+
return [...groups]
|
|
18
|
+
.sort(([left], [right]) => (order.get(left) ?? GROUP_ORDER.length) - (order.get(right) ?? GROUP_ORDER.length))
|
|
19
|
+
.map(([name, entries]) => ({ name, commands: entries }));
|
|
20
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type RunEvent, type RunRecord } from "../sessions/contracts.js";
|
|
2
2
|
import type { ActiveRun, ApprovalDecision, QuestionAnswer } from "../sessions/registry.js";
|
|
3
|
+
import { type TerminalSubagentActivity, type TerminalSubagentProfile } from "./subagentMonitor.js";
|
|
3
4
|
export interface PolicyReply {
|
|
4
5
|
allowed: boolean;
|
|
5
6
|
reason?: string | null;
|
|
@@ -35,6 +36,7 @@ export declare class TerminalController {
|
|
|
35
36
|
private readonly hooks;
|
|
36
37
|
private run;
|
|
37
38
|
private readonly pending;
|
|
39
|
+
private readonly subagents;
|
|
38
40
|
private ready;
|
|
39
41
|
private disposed;
|
|
40
42
|
private dirty;
|
|
@@ -64,6 +66,10 @@ export declare class TerminalController {
|
|
|
64
66
|
disconnected(): void;
|
|
65
67
|
/** Project registry-owned questions, including grouped requests, into the terminal inbox. */
|
|
66
68
|
observe(event: RunEvent, descendant?: boolean): void;
|
|
69
|
+
/** A child event that belongs to the active terminal parent run. */
|
|
70
|
+
observeSubagent(parentRunId: string, run: RunRecord, event: RunEvent, profile?: TerminalSubagentProfile | null): void;
|
|
71
|
+
/** Read-only child activity for the active run. */
|
|
72
|
+
subagentSnapshot(): TerminalSubagentActivity[];
|
|
67
73
|
/** The socket grants this interface only to the process that owns terminal input. */
|
|
68
74
|
control(call: string, data: Record<string, any>): Promise<unknown>;
|
|
69
75
|
private requestNativeApproval;
|
|
@@ -4,12 +4,16 @@
|
|
|
4
4
|
// extension's questions about policy, approvals, and what to do next.
|
|
5
5
|
import { createHash, randomUUID } from 'node:crypto';
|
|
6
6
|
import { HarnessError } from "../core/errors.js";
|
|
7
|
+
import { runStatusSchema } from "../sessions/contracts.js";
|
|
8
|
+
import { briefTitle } from "../sessions/subagents.js";
|
|
9
|
+
import { SubagentMonitor } from "./subagentMonitor.js";
|
|
7
10
|
const READY_TIMEOUT_MS = 90_000;
|
|
8
11
|
const ACK_TIMEOUT_MS = 25_000;
|
|
9
12
|
export class TerminalController {
|
|
10
13
|
hooks;
|
|
11
14
|
run = null;
|
|
12
15
|
pending = new Map();
|
|
16
|
+
subagents = new SubagentMonitor();
|
|
13
17
|
ready = false;
|
|
14
18
|
disposed = false;
|
|
15
19
|
dirty = false;
|
|
@@ -48,6 +52,7 @@ export class TerminalController {
|
|
|
48
52
|
throw new Error('The native prompt did not match the reserved delivery. Inspect the terminal before continuing.');
|
|
49
53
|
const run = this.hooks.begin({ reservedRunId: delivery?.runId ?? null, nativeTurnId, text });
|
|
50
54
|
this.run = run;
|
|
55
|
+
this.subagents.begin(run.runId);
|
|
51
56
|
run.onInterrupt(() => void this.commands.push({ type: 'interrupt' }));
|
|
52
57
|
if (text)
|
|
53
58
|
run.emit('message.completed', { messageId: nativeTurnId, role: 'user', text });
|
|
@@ -56,7 +61,9 @@ export class TerminalController {
|
|
|
56
61
|
return run;
|
|
57
62
|
}
|
|
58
63
|
finish(outcome, message) {
|
|
64
|
+
const runId = this.run?.runId;
|
|
59
65
|
this.run?.finish(outcome, message ? { code: 'terminal_attention', message } : undefined);
|
|
66
|
+
this.subagents.clear(runId);
|
|
60
67
|
this.run = null;
|
|
61
68
|
this.seen.clear();
|
|
62
69
|
this.pending.clear();
|
|
@@ -138,6 +145,27 @@ export class TerminalController {
|
|
|
138
145
|
this.pending.delete(String(event.payload.questionId));
|
|
139
146
|
}
|
|
140
147
|
}
|
|
148
|
+
/** A child event that belongs to the active terminal parent run. */
|
|
149
|
+
observeSubagent(parentRunId, run, event, profile = null) {
|
|
150
|
+
if (this.disposed || this.activeRun?.runId !== parentRunId || run.relation !== 'subagent')
|
|
151
|
+
return;
|
|
152
|
+
this.subagents.observe(parentRunId, {
|
|
153
|
+
id: run.id,
|
|
154
|
+
parentRunId: run.parentRunId,
|
|
155
|
+
relation: 'subagent',
|
|
156
|
+
title: run.title ?? briefTitle(run.goal),
|
|
157
|
+
goal: run.goal,
|
|
158
|
+
status: runStatusSchema.parse(run.status),
|
|
159
|
+
startedAt: run.startedAt,
|
|
160
|
+
endedAt: run.endedAt,
|
|
161
|
+
profile
|
|
162
|
+
}, event);
|
|
163
|
+
}
|
|
164
|
+
/** Read-only child activity for the active run. */
|
|
165
|
+
subagentSnapshot() {
|
|
166
|
+
const run = this.activeRun;
|
|
167
|
+
return run ? this.subagents.snapshot(run.runId) : [];
|
|
168
|
+
}
|
|
141
169
|
/** The socket grants this interface only to the process that owns terminal input. */
|
|
142
170
|
async control(call, data) {
|
|
143
171
|
if (this.disposed)
|
|
@@ -301,8 +329,10 @@ export class TerminalController {
|
|
|
301
329
|
if (existing)
|
|
302
330
|
return event === 'approval' ? { decision: await existing } : existing;
|
|
303
331
|
if (event === 'question') {
|
|
304
|
-
const
|
|
305
|
-
|
|
332
|
+
const summary = typeof data.summary === 'string' && data.summary.trim() ? data.summary.trim() : undefined;
|
|
333
|
+
const sketchMode = data.uiSketch === undefined ? {} : { uiSketch: data.uiSketch };
|
|
334
|
+
const response = run.requestAnswer({ requestId: key, ...(summary ? { summary } : {}), ...sketchMode, question: String(data.question ?? ''), options: data.options ?? [], multiSelect: !!data.multiSelect, allowText: true });
|
|
335
|
+
this.pending.set(key, { type: 'question', requestId: key, ...(summary ? { summary } : {}), ...sketchMode, question: String(data.question ?? ''), options: data.options ?? [], multiSelect: !!data.multiSelect });
|
|
306
336
|
this.seen.set(key, response);
|
|
307
337
|
return response.finally(() => this.pending.delete(key));
|
|
308
338
|
}
|
|
@@ -324,6 +354,8 @@ export class TerminalController {
|
|
|
324
354
|
}
|
|
325
355
|
if (event === 'usage')
|
|
326
356
|
run.emit('usage.updated', data);
|
|
357
|
+
else if (event === 'checklist')
|
|
358
|
+
run.emit('checklist.updated', { items: data.items });
|
|
327
359
|
else if (event === 'file')
|
|
328
360
|
run.emit('file.changed', data);
|
|
329
361
|
else if (event === 'message')
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type TerminalSubagentActivity } from "../subagentMonitor.js";
|
|
2
|
+
export type SubagentTone = 'success' | 'warning' | 'error' | 'muted' | 'accent';
|
|
3
|
+
/** One roster row: status mark, profile, session name, checklist progress, in that order. */
|
|
4
|
+
export interface SubagentRowView {
|
|
5
|
+
id: string;
|
|
6
|
+
mark: string;
|
|
7
|
+
tone: SubagentTone;
|
|
8
|
+
/** The profile name, or `subagent` for a copy of the parent. */
|
|
9
|
+
profile: string;
|
|
10
|
+
/** The profile's colour name (PROP-0029); null for a copy of the parent. */
|
|
11
|
+
color: string | null;
|
|
12
|
+
title: string;
|
|
13
|
+
/** `n of m tasks`, or null when the child wrote no checklist. */
|
|
14
|
+
tasks: string | null;
|
|
15
|
+
}
|
|
16
|
+
export interface SubagentRosterView {
|
|
17
|
+
rows: SubagentRowView[];
|
|
18
|
+
/** `+N more · /subagents` when rows were left out. */
|
|
19
|
+
overflow: string | null;
|
|
20
|
+
}
|
|
21
|
+
export declare function subagentTasks(checklist: TerminalSubagentActivity['checklist']): string | null;
|
|
22
|
+
/**
|
|
23
|
+
* The compact live roster shown under the status line: active children first, at most three
|
|
24
|
+
* rows. The session name is cut first when the terminal is narrow.
|
|
25
|
+
*/
|
|
26
|
+
export declare function subagentRoster(items: readonly TerminalSubagentActivity[], width: number, frame?: string): SubagentRosterView | null;
|
|
27
|
+
/** A row as plain text, the way it reads on screen. */
|
|
28
|
+
export declare function subagentRowText(row: SubagentRowView): string;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { stripTerminalSequences, truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';
|
|
2
|
+
import { ACTIVE_SUBAGENT_STATUSES } from "../subagentMonitor.js";
|
|
3
|
+
import { getWorkingSpinnerFrames } from "../tui/workingStatus.js";
|
|
4
|
+
const MAX_VISIBLE_ROWS = 3;
|
|
5
|
+
const GAP = ' ';
|
|
6
|
+
/** Cuts plain text to a width; the cut adds no styling codes. */
|
|
7
|
+
function cut(text, width) {
|
|
8
|
+
return stripTerminalSequences(truncateToWidth(text, width, '…'));
|
|
9
|
+
}
|
|
10
|
+
function safe(text) {
|
|
11
|
+
return stripTerminalSequences(text).replace(/[\x00-\x1f\x7f]/g, ' ').replace(/\s+/g, ' ').trim();
|
|
12
|
+
}
|
|
13
|
+
function mark(status, frame) {
|
|
14
|
+
switch (status) {
|
|
15
|
+
case 'completed': return { mark: '✓', tone: 'success' };
|
|
16
|
+
case 'failed': return { mark: '✕', tone: 'error' };
|
|
17
|
+
case 'interrupted': return { mark: '!', tone: 'muted' };
|
|
18
|
+
case 'queued': return { mark: '○', tone: 'muted' };
|
|
19
|
+
case 'waiting_for_approval':
|
|
20
|
+
case 'waiting_for_input': return { mark: '●', tone: 'accent' };
|
|
21
|
+
case 'running': return { mark: frame, tone: 'warning' };
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function subagentTasks(checklist) {
|
|
25
|
+
return checklist && checklist.total > 0 ? `${checklist.done} of ${checklist.total} tasks` : null;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The compact live roster shown under the status line: active children first, at most three
|
|
29
|
+
* rows. The session name is cut first when the terminal is narrow.
|
|
30
|
+
*/
|
|
31
|
+
export function subagentRoster(items, width, frame = getWorkingSpinnerFrames()[0] ?? '') {
|
|
32
|
+
if (!items.length || width < 1)
|
|
33
|
+
return null;
|
|
34
|
+
const ordered = [...items].sort((left, right) => {
|
|
35
|
+
const active = Number(ACTIVE_SUBAGENT_STATUSES.has(right.status)) - Number(ACTIVE_SUBAGENT_STATUSES.has(left.status));
|
|
36
|
+
return active || left.startedAt.localeCompare(right.startedAt) || left.id.localeCompare(right.id);
|
|
37
|
+
});
|
|
38
|
+
const rows = ordered.slice(0, MAX_VISIBLE_ROWS).map((item) => {
|
|
39
|
+
const status = mark(item.status, frame);
|
|
40
|
+
const profile = cut(safe(item.profile?.name ?? '') || 'subagent', Math.max(1, Math.min(24, width - 2)));
|
|
41
|
+
const tasks = subagentTasks(item.checklist);
|
|
42
|
+
const used = 2 + visibleWidth(profile) + GAP.length + (tasks ? GAP.length + tasks.length : 0);
|
|
43
|
+
const title = cut(safe(item.title) || 'Subagent', Math.max(1, width - used));
|
|
44
|
+
return { id: item.id, mark: status.mark, tone: status.tone, profile, color: item.profile?.color ?? null, title, tasks };
|
|
45
|
+
});
|
|
46
|
+
const overflow = ordered.length - rows.length;
|
|
47
|
+
return { rows, overflow: overflow > 0 ? cut(`+${overflow} more · /subagents`, width) : null };
|
|
48
|
+
}
|
|
49
|
+
/** A row as plain text, the way it reads on screen. */
|
|
50
|
+
export function subagentRowText(row) {
|
|
51
|
+
return [`${row.mark} ${row.profile}`, row.title, row.tasks].filter(Boolean).join(GAP);
|
|
52
|
+
}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { Type } from 'typebox';
|
|
2
|
+
import { QUESTION_SKETCH_MAX_LENGTH, QUESTION_SKETCH_PATTERN, questionSketchError } from "../sessions/contracts.js";
|
|
2
3
|
import { connectionFromEnv, socketCall, terminalCall } from "./client.js";
|
|
3
|
-
import {
|
|
4
|
-
import { runPluginsMenu } from "./pluginMenu.js";
|
|
5
|
-
import { runHubMenu } from "./hubMenu.js";
|
|
6
|
-
import { claimTerminalControl, controlCall } from "./control.js";
|
|
4
|
+
import { controlCall } from "./control.js";
|
|
7
5
|
import { SUBAGENT_OWNER, SUBAGENT_RECEIPT, SUBAGENT_RESULTS, subagentHistory } from "../sessions/subagents.js";
|
|
8
6
|
import { askUserTool } from "../tools/askUser.js";
|
|
7
|
+
import { checklistTool } from "../tools/checklist.js";
|
|
9
8
|
import { sessionNameTool } from "../tools/sessionName.js";
|
|
10
9
|
import { PLAN_INSTRUCTIONS, planRefusal } from "../sessions/planMode.js";
|
|
11
10
|
import { pruneToolImages } from "../world/images.js";
|
|
@@ -28,8 +27,6 @@ async function withParentCancellation(signal, work) {
|
|
|
28
27
|
signal?.removeEventListener('abort', stop);
|
|
29
28
|
}
|
|
30
29
|
}
|
|
31
|
-
/** /hub, /plugins and /accounts are pages: the owned terminal fills the screen, and Pi's own dialogs ignore the field. */
|
|
32
|
-
const PAGE = { page: true };
|
|
33
30
|
export default async function (pi, planRun, getSession) {
|
|
34
31
|
await terminalCall('startup', { phase: 'Pi extension loaded' });
|
|
35
32
|
let context = null;
|
|
@@ -50,45 +47,6 @@ export default async function (pi, planRun, getSession) {
|
|
|
50
47
|
try {
|
|
51
48
|
await terminalCall('startup', { phase: stage });
|
|
52
49
|
const connection = connectionFromEnv();
|
|
53
|
-
// Claimed here, before the model has run anything, and kept in this process's memory: a
|
|
54
|
-
// command the model runs can read the session token from its environment, never this key.
|
|
55
|
-
const control = await claimTerminalControl(connection).catch(() => null);
|
|
56
|
-
pi.registerCommand('hub', {
|
|
57
|
-
description: 'Browse and install reviewed skills, plugins, and reusable documents',
|
|
58
|
-
handler: async (_args, ctx) => {
|
|
59
|
-
if (!control)
|
|
60
|
-
return ctx.ui.notify('This session cannot install hub items.', 'warning');
|
|
61
|
-
await runHubMenu({ select: (title, options) => ctx.ui.select(title, options, PAGE), confirm: (title, message) => ctx.ui.confirm(title, message), notify: (message, level) => ctx.ui.notify(message, level) }, (method, params) => socketCall(connection, method, params ?? {}, 300_000, control));
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
pi.registerCommand('plugins', {
|
|
65
|
-
description: 'See what this session loads, and switch a plugin on or off or trust its code',
|
|
66
|
-
handler: async (_args, ctx) => {
|
|
67
|
-
if (!control)
|
|
68
|
-
return ctx.ui.notify('This session cannot change plugins.', 'warning');
|
|
69
|
-
const { projectId } = await socketCall(connection, 'plugins.session', {}, 15_000, control).catch(() => ({ projectId: null }));
|
|
70
|
-
await runPluginsMenu({ select: (title, options) => ctx.ui.select(title, options, PAGE), confirm: (title, message) => ctx.ui.confirm(title, message), notify: (message, level) => ctx.ui.notify(message, level) }, (method, params) => socketCall(connection, method, params ?? {}, 60_000, control), projectId);
|
|
71
|
-
}
|
|
72
|
-
});
|
|
73
|
-
pi.registerCommand('accounts', {
|
|
74
|
-
description: 'Add, sign in to, check, switch, or remove the Pi accounts this harness uses',
|
|
75
|
-
handler: async (_args, ctx) => {
|
|
76
|
-
if (!control)
|
|
77
|
-
return ctx.ui.notify('This session cannot manage accounts.', 'warning');
|
|
78
|
-
const session = await socketCall(connection, 'accounts.session', {}, 15_000, control).catch(() => null);
|
|
79
|
-
if (!session)
|
|
80
|
-
return ctx.ui.notify('This session has no account to manage.', 'warning');
|
|
81
|
-
const outcome = await runAccountsMenu({
|
|
82
|
-
select: (title, options) => ctx.ui.select(title, options, PAGE),
|
|
83
|
-
confirm: (title, message) => ctx.ui.confirm(title, message),
|
|
84
|
-
input: (title, placeholder) => ctx.ui.input(title, placeholder),
|
|
85
|
-
notify: (message, level) => ctx.ui.notify(message, level)
|
|
86
|
-
}, (method, params) => socketCall(connection, method, params ?? {}, 120_000, control), session);
|
|
87
|
-
// The session restarts on the account it chose: Pi ends here, and the launcher starts it again.
|
|
88
|
-
if (outcome === 'relaunch')
|
|
89
|
-
ctx.shutdown();
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
50
|
const { tools } = await socketCall(connection, 'tools.list', {}, 15_000);
|
|
93
51
|
stage = 'Tool registration';
|
|
94
52
|
for (const tool of tools) {
|
|
@@ -355,6 +313,19 @@ export default async function (pi, planRun, getSession) {
|
|
|
355
313
|
return { content: [{ type: 'text', text: typeof result === 'string' ? result : result.text }], details: undefined };
|
|
356
314
|
}
|
|
357
315
|
});
|
|
316
|
+
// The checklist is recorded on the harness run; the shared tool validates and the socket carries the event.
|
|
317
|
+
let recorded = Promise.resolve();
|
|
318
|
+
const checklist = checklistTool({ emit: (_type, payload) => { recorded = terminalCall('checklist', payload); } });
|
|
319
|
+
pi.registerTool({
|
|
320
|
+
name: checklist.name, label: checklist.label, description: checklist.description,
|
|
321
|
+
parameters: Type.Unsafe(checklist.parameters(Type)),
|
|
322
|
+
async execute(_id, input) {
|
|
323
|
+
recorded = Promise.resolve();
|
|
324
|
+
const result = await checklist.execute(input);
|
|
325
|
+
await recorded;
|
|
326
|
+
return { content: [{ type: 'text', text: typeof result === 'string' ? result : result.text }], details: undefined };
|
|
327
|
+
}
|
|
328
|
+
});
|
|
358
329
|
const ask = askUserTool({ requestAnswers: questions => terminalCall('questions', { questions }, 1_230_000) });
|
|
359
330
|
pi.registerTool({
|
|
360
331
|
name: ask.name, label: ask.label, description: ask.description,
|
|
@@ -367,10 +338,17 @@ export default async function (pi, planRun, getSession) {
|
|
|
367
338
|
pi.registerTool({
|
|
368
339
|
name: 'ask_user_question',
|
|
369
340
|
label: 'Ask a question',
|
|
370
|
-
description: 'Ask the user a question and wait for their answer. Every question must include at least one choice and at least one choice must be recommended. Recommendations are visual only and never preselect answers; submitted choices keep their labels. Mark at most one choice recommended for single-choice questions or several when multiSelect is true.',
|
|
341
|
+
description: 'Ask the user a question and wait for their answer. Include a concise 1–2 word summary label; if omitted, the terminal derives one from the question. Every question must include at least one choice and at least one choice must be recommended. Recommendations are visual only and never preselect answers; submitted choices keep their labels. Mark at most one choice recommended for single-choice questions or several when multiSelect is true. When a UI diagram helps compare choices, set uiSketch: true and include a detailed, bounded ASCII wireframe for every choice. Use ASCII tables and panel borders to show the affected element alongside relevant neighboring UI, with readable labels and spacing. Keep each panel’s outer `+` corners in fixed columns, include side edges at those same columns on every content row, and run horizontal borders corner to corner; if a title sits in a top border, leave at least two dashes on both sides. Keep framing consistent across alternatives and concentrate detail on what changes, not a symbolic logo or one-line illustration. The preview supplies its outer border; draw internal borders only for actual UI panels.',
|
|
371
342
|
parameters: Type.Object({
|
|
343
|
+
summary: Type.Optional(Type.String({ description: 'A concise label of one or two words' })),
|
|
372
344
|
question: Type.String(),
|
|
373
|
-
|
|
345
|
+
uiSketch: Type.Optional(Type.Literal(true, { description: 'Enable an ASCII sketch preview; when true, every choice must include a sketch.' })),
|
|
346
|
+
options: Type.Array(Type.Object({
|
|
347
|
+
label: Type.String(),
|
|
348
|
+
description: Type.Optional(Type.String()),
|
|
349
|
+
recommended: Type.Optional(Type.Boolean()),
|
|
350
|
+
sketch: Type.Optional(Type.String({ minLength: 1, maxLength: QUESTION_SKETCH_MAX_LENGTH, pattern: QUESTION_SKETCH_PATTERN, description: 'Literal ASCII UI wireframe: 1–24 LF-separated lines, at most 80 printable ASCII characters per line. Include the affected element and neighboring UI using structured table/panel borders with fixed, matching outer-edge columns on every row. Required for every choice when uiSketch is true.' }))
|
|
351
|
+
}), {
|
|
374
352
|
minItems: 1,
|
|
375
353
|
contains: Type.Object({ recommended: Type.Literal(true) })
|
|
376
354
|
}),
|
|
@@ -384,7 +362,14 @@ export default async function (pi, planRun, getSession) {
|
|
|
384
362
|
throw new Error('Every question needs at least one recommended choice.');
|
|
385
363
|
if (!input.multiSelect && recommendedCount > 1)
|
|
386
364
|
throw new Error('A single-choice question may recommend at most one option.');
|
|
387
|
-
const
|
|
365
|
+
const sketchError = questionSketchError(input.uiSketch, input.options);
|
|
366
|
+
if (sketchError)
|
|
367
|
+
throw new Error(sketchError);
|
|
368
|
+
const summary = typeof input.summary === 'string' ? input.summary.trim() : '';
|
|
369
|
+
if (summary && (summary.length > 100 || summary.split(/\s+/u).filter(Boolean).length > 2)) {
|
|
370
|
+
throw new Error('The question summary must be a concise 1–2 word label of no more than 100 characters.');
|
|
371
|
+
}
|
|
372
|
+
const answer = await terminalCall('question', { requestId: id, ...input, ...(summary ? { summary } : {}) });
|
|
388
373
|
return { content: [{ type: 'text', text: JSON.stringify(answer) }], details: undefined };
|
|
389
374
|
}
|
|
390
375
|
});
|
|
@@ -28,9 +28,12 @@ export interface SocketHandlers {
|
|
|
28
28
|
accounts?(grant: SocketGrant, call: string, params: Record<string, unknown>, control: boolean): Promise<unknown>;
|
|
29
29
|
}
|
|
30
30
|
/**
|
|
31
|
-
* Windows has no socket files for Node's `net`: it
|
|
32
|
-
* machine-wide namespace
|
|
33
|
-
*
|
|
31
|
+
* Each Harness process listens on its own path. Windows has no socket files for Node's `net`: it
|
|
32
|
+
* listens on a named pipe in a machine-wide namespace, and a random name keeps another local process
|
|
33
|
+
* from guessing it. On Unix the file is named for this process, because several Harness processes
|
|
34
|
+
* may share one state folder: closing a server removes its socket file, so a shared name would let
|
|
35
|
+
* one process delete, and the next one take over, the path another's sessions still call. The token
|
|
36
|
+
* still decides what a caller may do.
|
|
34
37
|
*/
|
|
35
38
|
export declare function socketPath(stateDir: string): string;
|
|
36
39
|
export declare class SessionSocket {
|