@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,738 @@
|
|
|
1
|
+
import { matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from '@earendil-works/pi-tui';
|
|
2
|
+
import { BROWSER_SECTIONS, browserItems, catalogSection } from "../../browser.js";
|
|
3
|
+
import { PI_SURFACE } from "../../../variant/surface.js";
|
|
4
|
+
import { BACK, applyPluginAction, menuRows, rowActions } from "../../pluginMenu.js";
|
|
5
|
+
import { safeText, selectedRow, selectionText } from "../chrome.js";
|
|
6
|
+
import { FilterList } from "../list.js";
|
|
7
|
+
const TITLES = { overview: 'Overview', foundation: 'Foundation', variant: 'Variant', skills: 'Skills', plugins: 'Plugins', connections: 'Connections', tools: 'Tools', library: 'Library' };
|
|
8
|
+
const LOCAL = ['all', 'local', 'built-in'];
|
|
9
|
+
export class BrowserPage {
|
|
10
|
+
options;
|
|
11
|
+
presentation = 'fullscreen';
|
|
12
|
+
ownsNavigation = true;
|
|
13
|
+
section;
|
|
14
|
+
source;
|
|
15
|
+
channel = 'all';
|
|
16
|
+
local = 'all';
|
|
17
|
+
focus = 'list';
|
|
18
|
+
snapshot;
|
|
19
|
+
permissions;
|
|
20
|
+
error;
|
|
21
|
+
busy = false;
|
|
22
|
+
disposed = false;
|
|
23
|
+
generation = 0;
|
|
24
|
+
detail;
|
|
25
|
+
content;
|
|
26
|
+
contentOffset = 0;
|
|
27
|
+
contentTotal = 0;
|
|
28
|
+
contentRows = 1;
|
|
29
|
+
saved = new Map();
|
|
30
|
+
list;
|
|
31
|
+
actionList;
|
|
32
|
+
toolbarHits = [];
|
|
33
|
+
filterStart = 0;
|
|
34
|
+
filterRow = 0;
|
|
35
|
+
inspectorActionRow = 0;
|
|
36
|
+
actionRow = 2;
|
|
37
|
+
pointer = { wide: false, body: 0, listWidth: 1 };
|
|
38
|
+
constructor(options) {
|
|
39
|
+
this.options = options;
|
|
40
|
+
this.section = options.section ?? 'overview';
|
|
41
|
+
this.source = options.source ?? 'installed';
|
|
42
|
+
this.list = new FilterList({ theme: options.theme, search: 'always', noun: ['resource', 'resources'],
|
|
43
|
+
empty: 'No matching resources. Change the filter or Discover.', onActivate: row => { void this.open(row.data); } });
|
|
44
|
+
this.actionList = new FilterList({ theme: options.theme, search: 'never', noun: ['action', 'actions'],
|
|
45
|
+
onActivate: row => { if (row.data)
|
|
46
|
+
void this.perform(row.data.run); } });
|
|
47
|
+
}
|
|
48
|
+
get focused() { return this.list.focused; }
|
|
49
|
+
set focused(value) { this.list.focused = value; this.actionList.focused = value; }
|
|
50
|
+
tabs() { return BROWSER_SECTIONS.map(id => ({ id, title: TITLES[id] })); }
|
|
51
|
+
activeTab() { return this.section; }
|
|
52
|
+
selectTab(id) {
|
|
53
|
+
if (!this.busy && BROWSER_SECTIONS.includes(id))
|
|
54
|
+
this.navigate(id);
|
|
55
|
+
}
|
|
56
|
+
hint() { return this.content !== undefined ? '↑↓ scroll · PgUp/PgDn page' : this.detail ? 'Enter action · Tab controls' : 'Tab controls · ←→ tabs · Enter inspect · Ctrl+F search'; }
|
|
57
|
+
activate() { void this.load(); }
|
|
58
|
+
dispose() { this.disposed = true; this.generation += 1; }
|
|
59
|
+
async load(refresh = false) {
|
|
60
|
+
const generation = ++this.generation;
|
|
61
|
+
try {
|
|
62
|
+
const [snapshot, permissions] = await Promise.all([
|
|
63
|
+
this.options.call('terminal.browser.snapshot', { refresh }), this.options.permissions()
|
|
64
|
+
]);
|
|
65
|
+
if (this.disposed || generation !== this.generation)
|
|
66
|
+
return;
|
|
67
|
+
this.snapshot = snapshot;
|
|
68
|
+
this.permissions = permissions;
|
|
69
|
+
this.error = undefined;
|
|
70
|
+
this.fill();
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
if (this.disposed || generation !== this.generation)
|
|
74
|
+
return;
|
|
75
|
+
this.error = error instanceof Error ? error.message : String(error);
|
|
76
|
+
}
|
|
77
|
+
this.options.changed();
|
|
78
|
+
}
|
|
79
|
+
async perform(action) {
|
|
80
|
+
if (this.busy || this.disposed)
|
|
81
|
+
return;
|
|
82
|
+
this.busy = true;
|
|
83
|
+
this.options.changed();
|
|
84
|
+
try {
|
|
85
|
+
await action();
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
if (!this.disposed)
|
|
89
|
+
this.options.ui.notify(error instanceof Error ? error.message : String(error), 'error');
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
this.busy = false;
|
|
93
|
+
if (!this.disposed) {
|
|
94
|
+
if (!this.detail && this.focus === 'inspector')
|
|
95
|
+
this.focus = 'list';
|
|
96
|
+
await this.load();
|
|
97
|
+
this.options.changed();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
resources() { return this.snapshot ? browserItems(this.snapshot, this.options.runtime()) : []; }
|
|
102
|
+
discoverable() { return this.section !== 'foundation' && this.section !== 'variant'; }
|
|
103
|
+
rows() {
|
|
104
|
+
if (!this.snapshot)
|
|
105
|
+
return [];
|
|
106
|
+
if (this.source === 'discover' && this.discoverable()) {
|
|
107
|
+
const hub = this.snapshot.hub;
|
|
108
|
+
return (hub?.entries ?? []).filter(entry => (this.section === 'overview' || catalogSection(entry) === this.section || (this.section === 'tools' && entry.delivery.kind === 'pi-package'))
|
|
109
|
+
&& (this.channel === 'all' || entry.channel === this.channel)).map(entry => ({
|
|
110
|
+
id: `hub:${entry.id}@${entry.version}`, label: entry.title, detail: hub?.installed.some(receipt => receipt.entry.id === entry.id && receipt.entry.version === entry.version) ? 'Installed' : entry.channel,
|
|
111
|
+
group: entry.category, searchText: `${entry.title} ${entry.description} ${entry.publisher} ${entry.tags.join(' ')}`,
|
|
112
|
+
data: { hub: entry }
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
115
|
+
const all = this.resources();
|
|
116
|
+
if (this.section === 'overview')
|
|
117
|
+
return BROWSER_SECTIONS.filter(section => section !== 'overview').map(section => ({
|
|
118
|
+
id: `section:${section}`, label: TITLES[section], detail: `${all.filter(item => item.section === section).length}`,
|
|
119
|
+
data: { section }, searchText: `${TITLES[section]} ${section === 'connections' ? 'MCP' : ''}`
|
|
120
|
+
}));
|
|
121
|
+
const rows = all.filter(item => (item.section === this.section || (this.section === 'library' && item.componentId))
|
|
122
|
+
&& (this.local === 'all' || (this.local === 'built-in' ? /built.?in|^pi$/i.test(item.origin) : !/built.?in|^pi$/i.test(item.origin))))
|
|
123
|
+
.map(item => ({ id: item.id, label: item.name, detail: item.status, group: item.owner, tone: item.available ? 'success' : item.status.includes('trust') || item.status.includes('failed') ? 'warning' : 'muted',
|
|
124
|
+
searchText: `${item.name} ${item.description} ${item.owner} ${item.origin} ${item.path ?? ''}`, data: { item } }));
|
|
125
|
+
if (this.section === 'variant') {
|
|
126
|
+
for (const component of [...PI_SURFACE.components].reverse()) {
|
|
127
|
+
if (!rows.some(row => row.data?.item?.description.startsWith(`c${component.index} ·`)))
|
|
128
|
+
rows.unshift({
|
|
129
|
+
id: `empty:${component.id}`, label: `c${component.index} · ${component.title}`, detail: 'Empty',
|
|
130
|
+
data: { item: { id: `empty:${component.id}`, section: 'variant', name: component.title, description: `${component.description} Path: ${component.path}. Loaded: ${component.reach}.`, owner: 'Variant', origin: 'Variant', status: 'No files in this component' } }
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
for (const receipt of this.snapshot.hub?.installed ?? []) {
|
|
135
|
+
if (receipt.entry.delivery.kind !== 'document')
|
|
136
|
+
continue;
|
|
137
|
+
if (this.section !== catalogSection(receipt.entry) && this.section !== 'library')
|
|
138
|
+
continue;
|
|
139
|
+
rows.push({ id: `installed:${receipt.entry.id}@${receipt.entry.version}`, label: receipt.entry.title,
|
|
140
|
+
detail: receipt.state === 'installed' || !receipt.state ? 'Downloaded definition' : receipt.state, group: 'Hub documents',
|
|
141
|
+
searchText: `${receipt.entry.title} ${receipt.entry.description} ${receipt.entry.publisher}`, data: { hub: receipt.entry } });
|
|
142
|
+
}
|
|
143
|
+
return rows;
|
|
144
|
+
}
|
|
145
|
+
fill() { this.list.setRows(this.rows()); }
|
|
146
|
+
navigate(section) {
|
|
147
|
+
this.saved.set(this.section, { query: this.list.query, selected: this.list.selected?.id });
|
|
148
|
+
this.section = section;
|
|
149
|
+
this.detail = undefined;
|
|
150
|
+
this.content = undefined;
|
|
151
|
+
const saved = this.saved.get(section);
|
|
152
|
+
this.list.setQuery(saved?.query ?? '');
|
|
153
|
+
this.fill();
|
|
154
|
+
if (saved?.selected)
|
|
155
|
+
this.list.select(saved.selected);
|
|
156
|
+
this.focus = 'list';
|
|
157
|
+
this.options.changed();
|
|
158
|
+
}
|
|
159
|
+
async open(row) {
|
|
160
|
+
if (!row || this.busy)
|
|
161
|
+
return;
|
|
162
|
+
if (row.section) {
|
|
163
|
+
this.navigate(row.section);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (row.action) {
|
|
167
|
+
await this.perform(() => this.toolbar(row.action));
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
this.detail = row;
|
|
171
|
+
this.focus = 'inspector';
|
|
172
|
+
this.content = undefined;
|
|
173
|
+
this.actionList.setRows(this.actions(row).map(action => ({ id: action.id, label: action.label, data: action })));
|
|
174
|
+
this.options.changed();
|
|
175
|
+
}
|
|
176
|
+
actionGroup(id) {
|
|
177
|
+
if (id === 'use')
|
|
178
|
+
return 'Use';
|
|
179
|
+
if (['preview', 'read', 'details', 'latest', 'related', 'owner', 'contributions', 'tools'].includes(id))
|
|
180
|
+
return 'Inspect';
|
|
181
|
+
if (['test', 'auth', 'enable', 'permission'].includes(id))
|
|
182
|
+
return 'Access';
|
|
183
|
+
if (id === 'remove' || id === 'remove-connection')
|
|
184
|
+
return 'Remove';
|
|
185
|
+
return 'Manage';
|
|
186
|
+
}
|
|
187
|
+
groupedActions(row) {
|
|
188
|
+
const order = ['Use', 'Inspect', 'Manage', 'Access', 'Remove'];
|
|
189
|
+
return this.actions(row).sort((left, right) => order.indexOf(this.actionGroup(left.id)) - order.indexOf(this.actionGroup(right.id)));
|
|
190
|
+
}
|
|
191
|
+
actions(row) {
|
|
192
|
+
const actions = [];
|
|
193
|
+
const item = row.item;
|
|
194
|
+
if (row.hub) {
|
|
195
|
+
const entry = row.hub;
|
|
196
|
+
const installed = this.snapshot?.hub?.installed.some(receipt => receipt.entry.id === entry.id && receipt.entry.version === entry.version);
|
|
197
|
+
actions.push({ id: 'preview', label: 'Preview reviewed content', run: async () => {
|
|
198
|
+
const page = await this.options.call('terminal.browser.hubPreview', { id: entry.id, version: entry.version });
|
|
199
|
+
this.showContent(page.content + (page.done ? '' : '\n\n[Preview limited to first page.]'));
|
|
200
|
+
} });
|
|
201
|
+
if (!installed)
|
|
202
|
+
actions.push({ id: 'install', label: `Install ${entry.version}`, run: async () => {
|
|
203
|
+
if (!await this.options.ui.confirm(`Install ${entry.title}?`, `${entry.description}\nPublisher: ${entry.publisher}\nLicense: ${entry.license}\nSource: ${entry.sourceUrl}\nVersion: ${entry.version}\nInstallation does not grant trust, enablement, credentials or permissions.`))
|
|
204
|
+
return;
|
|
205
|
+
await this.options.call('hub.install', { id: entry.id, version: entry.version });
|
|
206
|
+
this.options.ui.notify('Installed. Review it in Installed before enabling or adding it to a variant.', 'info');
|
|
207
|
+
this.source = 'installed';
|
|
208
|
+
this.section = catalogSection(entry);
|
|
209
|
+
this.detail = undefined;
|
|
210
|
+
} });
|
|
211
|
+
if (installed && entry.category === 'connector' && entry.delivery.kind === 'document')
|
|
212
|
+
actions.push({ id: 'setup', label: 'Set up a connection from this definition', run: async () => {
|
|
213
|
+
const result = await this.options.call('terminal.browser.hubDocument', { id: entry.id, version: entry.version });
|
|
214
|
+
const value = JSON.parse(result.content);
|
|
215
|
+
if (!value || typeof value !== 'object')
|
|
216
|
+
throw new Error('Unsupported connector definition.');
|
|
217
|
+
const definition = value;
|
|
218
|
+
if (definition.kind !== 'remote' || typeof definition.url !== 'string' || typeof definition.name !== 'string')
|
|
219
|
+
throw new Error('Unsupported connector definition. Configure a connection manually.');
|
|
220
|
+
await this.configureConnection(undefined, { name: definition.name, url: definition.url, auth: typeof definition.auth === 'string' ? definition.auth : 'none', catalogId: entry.id });
|
|
221
|
+
} });
|
|
222
|
+
return actions;
|
|
223
|
+
}
|
|
224
|
+
if (!item)
|
|
225
|
+
return actions;
|
|
226
|
+
const related = { 'foundation:prompt': 'variant', 'foundation:surface': 'variant', 'foundation:loader': 'plugins', 'foundation:gate': 'tools' };
|
|
227
|
+
const section = related[item.id];
|
|
228
|
+
if (section)
|
|
229
|
+
actions.push({ id: 'related', label: `Browse ${TITLES[section]}`, run: () => this.navigate(section) });
|
|
230
|
+
if (item.read)
|
|
231
|
+
actions.push({ id: 'read', label: item.section === 'variant' ? 'Read pinned version' : 'Read content', run: () => this.read(item) });
|
|
232
|
+
actions.push({ id: 'details', label: 'Read full description and metadata', run: () => this.showContent(this.description(row).join('\n\n')) });
|
|
233
|
+
if (item.section === 'variant' && item.read && this.snapshot?.variant.latest !== this.snapshot?.variant.pinned)
|
|
234
|
+
actions.push({ id: 'latest', label: 'Compare pinned and latest saved version', run: async () => {
|
|
235
|
+
const pinned = await this.options.call('terminal.browser.read', { id: item.id });
|
|
236
|
+
const latest = await this.options.call('terminal.browser.read', { id: item.id, latest: true });
|
|
237
|
+
this.showContent(`PINNED ${this.snapshot?.variant.pinned}\n${pinned.content}\n\nLATEST ${this.snapshot?.variant.latest}\n${latest.content}`);
|
|
238
|
+
} });
|
|
239
|
+
if (item.skillName && item.available && this.options.runtime().skillCommands)
|
|
240
|
+
actions.unshift({ id: 'use', label: `Use $${item.skillName} in draft`, run: () => this.options.useSkill(item.skillName) });
|
|
241
|
+
if (item.componentId && !item.added && !item.componentId.startsWith('prompts/') && this.snapshot?.variant.writable)
|
|
242
|
+
actions.push({ id: 'add', label: 'Add to variant for future sessions', run: async () => {
|
|
243
|
+
if (!await this.options.ui.confirm(`Add ${item.name}?`, 'Creates a validated variant version for future sessions. This terminal keeps its pinned version.'))
|
|
244
|
+
return;
|
|
245
|
+
const result = await this.options.call('terminal.components.add', { id: item.componentId });
|
|
246
|
+
this.options.ui.notify(`Saved for a future session · ${result.fingerprint}`, 'info');
|
|
247
|
+
this.detail = undefined;
|
|
248
|
+
} });
|
|
249
|
+
const plugin = this.snapshot?.plugins.find(plugin => plugin.id === item.pluginId);
|
|
250
|
+
if (plugin) {
|
|
251
|
+
if (item.section !== 'plugins')
|
|
252
|
+
actions.push({ id: 'owner', label: 'View owning plugin and its contributions', run: () => {
|
|
253
|
+
this.navigate('plugins');
|
|
254
|
+
this.list.select(`plugin:${plugin.id}`);
|
|
255
|
+
} });
|
|
256
|
+
actions.push({ id: 'manage', label: 'Manage attachment and code trust', run: () => this.managePlugin(plugin, item.contributionId) });
|
|
257
|
+
if (item.section === 'plugins')
|
|
258
|
+
actions.push({ id: 'contributions', label: 'Inspect plugin contributions', run: () => this.showContent(plugin.contributions.map(entry => `${entry.kind}: ${entry.name} [${entry.state}]\n${entry.description}${entry.path ? '\n' + entry.path : ''}`).join('\n\n')) });
|
|
259
|
+
if (plugin.settings.length)
|
|
260
|
+
actions.push({ id: 'settings', label: 'Configure plugin settings', run: () => this.configurePlugin(plugin) });
|
|
261
|
+
for (const template of plugin.connectorTemplates)
|
|
262
|
+
actions.push({ id: `template:${template.id}`, label: `Set up ${template.name} connection`, run: () => this.configureConnection(undefined, { name: template.name, url: template.url, auth: template.auth, catalogId: template.id }) });
|
|
263
|
+
if (plugin.install && !plugin.required)
|
|
264
|
+
actions.push({ id: 'remove', label: 'Remove installed plugin', run: async () => {
|
|
265
|
+
if (await this.options.ui.confirm(`Remove ${plugin.title}?`, 'Removes this installed resource and its saved attachment choices. The running session is not reloaded.')) {
|
|
266
|
+
await this.options.call('plugins.remove', { pluginId: plugin.id });
|
|
267
|
+
this.detail = undefined;
|
|
268
|
+
}
|
|
269
|
+
} });
|
|
270
|
+
}
|
|
271
|
+
if (item.connectionId) {
|
|
272
|
+
const connection = this.snapshot?.connections.find(connection => connection.id === item.connectionId);
|
|
273
|
+
if (connection) {
|
|
274
|
+
actions.push({ id: 'configure', label: 'Edit connection configuration', run: () => this.configureConnection(connection) });
|
|
275
|
+
actions.push({ id: 'test', label: 'Test connection and list its tools', run: async () => {
|
|
276
|
+
if (!await this.options.ui.confirm('Test connection?', 'Starts the configured local process or contacts the remote server to list tools. No connector tool is called.'))
|
|
277
|
+
return;
|
|
278
|
+
const result = await this.options.call('terminal.browser.connections.test', { id: connection.id });
|
|
279
|
+
this.options.ui.notify(result.error || 'Connection test passed.', result.error ? 'error' : 'info');
|
|
280
|
+
this.detail = undefined;
|
|
281
|
+
} });
|
|
282
|
+
if (connection.auth === 'oauth')
|
|
283
|
+
actions.push({ id: 'auth', label: connection.authState === 'signed_in' ? 'Sign out' : 'Sign in through browser', run: async () => {
|
|
284
|
+
await this.options.call(`terminal.browser.connections.${connection.authState === 'signed_in' ? 'signOut' : 'signIn'}`, { id: connection.id });
|
|
285
|
+
this.detail = undefined;
|
|
286
|
+
} });
|
|
287
|
+
actions.push({ id: 'enable', label: connection.enabled ? 'Disable connection' : 'Enable connection', run: async () => {
|
|
288
|
+
if (!await this.options.ui.confirm(`${connection.enabled ? 'Disable' : 'Enable'} ${connection.name}?`, 'Changes its saved availability. Scope, host policy and per-call approvals still apply.'))
|
|
289
|
+
return;
|
|
290
|
+
await this.options.call('terminal.browser.connections.save', { input: { ...connection, enabled: !connection.enabled } });
|
|
291
|
+
this.detail = undefined;
|
|
292
|
+
} });
|
|
293
|
+
actions.push({ id: 'tools', label: 'Inspect last reported tools', run: () => this.showContent(JSON.stringify(connection.tools, null, 2)) });
|
|
294
|
+
actions.push({ id: 'remove-connection', label: 'Remove connection', run: async () => {
|
|
295
|
+
if (await this.options.ui.confirm(`Remove ${connection.name}?`, 'Removes the connection and its saved credentials.')) {
|
|
296
|
+
await this.options.call('terminal.browser.connections.remove', { id: connection.id });
|
|
297
|
+
this.detail = undefined;
|
|
298
|
+
}
|
|
299
|
+
} });
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (item.section === 'tools')
|
|
303
|
+
actions.push({ id: 'permission', label: 'Review saved approval for this tool', run: async () => {
|
|
304
|
+
const state = await this.options.permissions();
|
|
305
|
+
const allowed = state.globalTools.includes(item.name);
|
|
306
|
+
if (await this.options.ui.confirm(`${allowed ? 'Revoke' : 'Allow'} ${item.name} everywhere?`, 'Changes eligible future approvals in terminal sessions sharing this device state. Hard denials and capability restrictions still apply.'))
|
|
307
|
+
await this.options.setGlobalTool(item.name, !allowed);
|
|
308
|
+
} });
|
|
309
|
+
return actions;
|
|
310
|
+
}
|
|
311
|
+
async read(item) {
|
|
312
|
+
const result = await this.options.call('terminal.browser.read', { id: item.id });
|
|
313
|
+
this.showContent(result.content);
|
|
314
|
+
}
|
|
315
|
+
showContent(content) { this.content = content; this.contentOffset = 0; this.options.changed(); }
|
|
316
|
+
async managePlugin(plugin, contributionId) {
|
|
317
|
+
const projectId = (await this.options.call('plugins.session')).projectId;
|
|
318
|
+
const rows = menuRows(this.snapshot?.plugins ?? [], projectId).filter(row => row.plugin.id === plugin.id);
|
|
319
|
+
let row = rows.find(row => contributionId ? row.id === contributionId : row.whole);
|
|
320
|
+
if (!row && !contributionId && rows.length) {
|
|
321
|
+
const selected = await this.options.ui.select('Choose contribution', rows.map(row => row.label));
|
|
322
|
+
row = rows.find(row => row.label === selected);
|
|
323
|
+
if (!row)
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
if (!row) {
|
|
327
|
+
this.options.ui.notify(plugin.required || 'This resource is not switchable.', 'info');
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
const action = await this.options.ui.select(plugin.title, rowActions(row, projectId));
|
|
331
|
+
if (action && action !== BACK && await applyPluginAction(this.options.ui, this.options.call, row, action))
|
|
332
|
+
this.options.ui.notify('Saved. Reopen the terminal to apply resource changes; the current selection stays pinned.', 'info');
|
|
333
|
+
this.detail = undefined;
|
|
334
|
+
}
|
|
335
|
+
async configurePlugin(plugin) {
|
|
336
|
+
const selected = await this.options.ui.select(`${plugin.title} settings`, plugin.settings.map(setting => `${setting.label}: ${setting.value}`));
|
|
337
|
+
const setting = plugin.settings.find(setting => selected === `${setting.label}: ${setting.value}`);
|
|
338
|
+
if (!setting)
|
|
339
|
+
return;
|
|
340
|
+
const answer = await this.options.ui.input(`${setting.label} (${setting.min}–${setting.max})`, String(setting.value));
|
|
341
|
+
if (answer === undefined)
|
|
342
|
+
return;
|
|
343
|
+
const value = Number(answer);
|
|
344
|
+
if (!answer.trim() || !Number.isInteger(value) || value < setting.min || value > setting.max)
|
|
345
|
+
throw new Error('Enter a whole number inside the setting range.');
|
|
346
|
+
await this.options.call('plugins.configure', { pluginId: plugin.id, settings: { [setting.key]: value } });
|
|
347
|
+
this.options.ui.notify('Setting saved.', 'info');
|
|
348
|
+
this.detail = undefined;
|
|
349
|
+
}
|
|
350
|
+
async configureConnection(existing, template) {
|
|
351
|
+
const ui = this.options.ui;
|
|
352
|
+
let kind = existing?.kind ?? (template ? 'remote' : '');
|
|
353
|
+
if (!kind) {
|
|
354
|
+
const transport = await ui.select('Connection transport', ['Local command (stdio)', 'Remote URL (HTTP)']);
|
|
355
|
+
if (!transport)
|
|
356
|
+
return;
|
|
357
|
+
kind = transport === 'Local command (stdio)' ? 'local' : 'remote';
|
|
358
|
+
}
|
|
359
|
+
const name = await ui.input('Connection name', existing?.name ?? template?.name ?? '');
|
|
360
|
+
if (!name?.trim())
|
|
361
|
+
return;
|
|
362
|
+
const input = { ...existing, name, kind, enabled: existing?.enabled ?? false, catalogId: template?.catalogId ?? existing?.catalogId };
|
|
363
|
+
if (kind === 'remote') {
|
|
364
|
+
const url = await ui.input('MCP URL', existing?.url ?? template?.url ?? '');
|
|
365
|
+
if (url === undefined)
|
|
366
|
+
return;
|
|
367
|
+
const parsed = new URL(url);
|
|
368
|
+
if (!['http:', 'https:'].includes(parsed.protocol) || parsed.username || parsed.password)
|
|
369
|
+
throw new Error('Use an HTTP(S) URL without embedded credentials.');
|
|
370
|
+
input.url = url;
|
|
371
|
+
const auth = await ui.select('Authentication', ['None', 'Headers / token', 'OAuth browser sign-in']);
|
|
372
|
+
if (!auth)
|
|
373
|
+
return;
|
|
374
|
+
input.auth = auth === 'None' ? 'none' : auth === 'Headers / token' ? 'headers' : 'oauth';
|
|
375
|
+
if (input.auth === 'headers') {
|
|
376
|
+
const header = await ui.input('Credential header name', 'Authorization');
|
|
377
|
+
if (!header)
|
|
378
|
+
return;
|
|
379
|
+
const value = await ui.secretInput('Header value (include Bearer prefix if needed)');
|
|
380
|
+
if (value === undefined)
|
|
381
|
+
return;
|
|
382
|
+
if (value)
|
|
383
|
+
input.headers = { [header]: value };
|
|
384
|
+
}
|
|
385
|
+
if (input.auth === 'oauth') {
|
|
386
|
+
const clients = await this.options.call('terminal.browser.connections.appClients');
|
|
387
|
+
const appClient = Boolean(input.catalogId && clients.catalogIds?.includes(input.catalogId));
|
|
388
|
+
const client = await ui.select('OAuth client', [...(appClient ? ['Application client'] : []), 'Dynamic registration', 'My client ID']);
|
|
389
|
+
if (!client)
|
|
390
|
+
return;
|
|
391
|
+
input.oauthClient = client === 'My client ID' ? 'user' : client === 'Application client' ? 'app' : 'dynamic';
|
|
392
|
+
const scopes = await ui.input('OAuth scopes (space-separated; empty for defaults)', existing?.oauthScopes.join(' ') ?? '');
|
|
393
|
+
if (scopes === undefined)
|
|
394
|
+
return;
|
|
395
|
+
input.oauthScopes = scopes.trim().split(/\s+/).filter(Boolean);
|
|
396
|
+
if (input.oauthClient === 'user') {
|
|
397
|
+
const clientId = await ui.input('OAuth client ID');
|
|
398
|
+
if (!clientId)
|
|
399
|
+
return;
|
|
400
|
+
const clientSecret = await ui.secretInput('OAuth client secret (empty for public client)');
|
|
401
|
+
if (clientSecret === undefined)
|
|
402
|
+
return;
|
|
403
|
+
input.oauthCredentials = { clientId, clientSecret };
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
const command = await ui.input('Executable (no shell command string)', existing?.command ?? '');
|
|
409
|
+
if (!command?.trim())
|
|
410
|
+
return;
|
|
411
|
+
const argumentsText = await ui.input('Arguments as a JSON array', JSON.stringify(existing?.args ?? []));
|
|
412
|
+
if (argumentsText === undefined)
|
|
413
|
+
return;
|
|
414
|
+
const args = JSON.parse(argumentsText);
|
|
415
|
+
if (!Array.isArray(args) || !args.every(arg => typeof arg === 'string'))
|
|
416
|
+
throw new Error('Arguments must be a JSON array of strings.');
|
|
417
|
+
input.command = command;
|
|
418
|
+
input.args = args;
|
|
419
|
+
const cwd = await ui.input('Working directory (empty for default)', existing?.cwd ?? '');
|
|
420
|
+
if (cwd === undefined)
|
|
421
|
+
return;
|
|
422
|
+
input.cwd = cwd || null;
|
|
423
|
+
const key = await ui.input('Environment variable name (empty to skip)');
|
|
424
|
+
if (key === undefined)
|
|
425
|
+
return;
|
|
426
|
+
if (key) {
|
|
427
|
+
const value = await ui.secretInput(`Value for ${key}`);
|
|
428
|
+
if (value === undefined)
|
|
429
|
+
return;
|
|
430
|
+
input.env = { [key]: value };
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
const projectId = (await this.options.call('plugins.session')).projectId;
|
|
434
|
+
const scope = await ui.select('Availability scope', projectId ? ['Current project', 'Everywhere'] : ['Everywhere']);
|
|
435
|
+
if (!scope)
|
|
436
|
+
return;
|
|
437
|
+
input.global = scope === 'Everywhere';
|
|
438
|
+
input.projectIds = scope === 'Current project' ? [projectId] : [];
|
|
439
|
+
if (!await ui.confirm(`Save ${name}?`, `${kind === 'remote' ? 'Remote MCP server' : 'Local MCP process'} · ${scope}. ${input.enabled ? 'Keeps its current enabled setting.' : 'Saved disabled.'} Saving does not connect, sign in or grant tool approvals.`))
|
|
440
|
+
return;
|
|
441
|
+
await this.options.call('terminal.browser.connections.save', { input });
|
|
442
|
+
this.detail = undefined;
|
|
443
|
+
this.options.ui.notify('Connection saved. Test, sign in if required, then enable it here.', 'info');
|
|
444
|
+
}
|
|
445
|
+
async toolbar(action) {
|
|
446
|
+
if (action === 'refresh') {
|
|
447
|
+
await this.load(this.source === 'discover');
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
if (action === 'connection') {
|
|
451
|
+
await this.configureConnection();
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
const source = await this.options.ui.input(action === 'import' ? 'Skill folder or SKILL.md path' : 'Plugin folder, npm package or Git source');
|
|
455
|
+
if (!source?.trim())
|
|
456
|
+
return;
|
|
457
|
+
if (!await this.options.ui.confirm(action === 'import' ? 'Import local skill?' : 'Install plugin?', `${source}\nThe resource is saved for review. Installing grants no attachment, code trust or permissions.`))
|
|
458
|
+
return;
|
|
459
|
+
if (action === 'import')
|
|
460
|
+
await this.options.call('terminal.browser.importSkill', { path: source });
|
|
461
|
+
else
|
|
462
|
+
await this.options.call('plugins.install', { source });
|
|
463
|
+
this.options.ui.notify('Saved locally. Review it in Installed.', 'info');
|
|
464
|
+
this.source = 'installed';
|
|
465
|
+
}
|
|
466
|
+
toolbarActions() {
|
|
467
|
+
return [...(this.section === 'skills' ? [{ action: 'import' }] : []), ...(this.section === 'plugins' ? [{ action: 'package' }] : []),
|
|
468
|
+
...(this.section === 'connections' ? [{ action: 'connection' }] : []), { action: 'refresh' }];
|
|
469
|
+
}
|
|
470
|
+
toolbarLabel(row) {
|
|
471
|
+
return row.action === 'import' ? 'Import skill' : row.action === 'package' ? 'Add plugin' : row.action === 'connection' ? 'Add connection' : this.source === 'discover' ? 'Refresh Hub' : 'Refresh';
|
|
472
|
+
}
|
|
473
|
+
handleInput(data) {
|
|
474
|
+
if (this.busy)
|
|
475
|
+
return !matchesKey(data, 'ctrl+c');
|
|
476
|
+
if (this.focus === 'section' && (matchesKey(data, 'left') || matchesKey(data, 'right'))) {
|
|
477
|
+
const delta = matchesKey(data, 'left') ? -1 : 1;
|
|
478
|
+
const index = BROWSER_SECTIONS.indexOf(this.section);
|
|
479
|
+
this.navigate(BROWSER_SECTIONS[(index + delta + BROWSER_SECTIONS.length) % BROWSER_SECTIONS.length]);
|
|
480
|
+
this.focus = 'section';
|
|
481
|
+
return true;
|
|
482
|
+
}
|
|
483
|
+
if (!this.snapshot && matchesKey(data, 'tab')) {
|
|
484
|
+
this.focus = 'section';
|
|
485
|
+
return true;
|
|
486
|
+
}
|
|
487
|
+
if (!this.snapshot) {
|
|
488
|
+
if (matchesKey(data, 'enter')) {
|
|
489
|
+
void this.load();
|
|
490
|
+
return true;
|
|
491
|
+
}
|
|
492
|
+
return !matchesKey(data, 'escape');
|
|
493
|
+
}
|
|
494
|
+
if (matchesKey(data, 'ctrl+f')) {
|
|
495
|
+
this.detail = undefined;
|
|
496
|
+
this.content = undefined;
|
|
497
|
+
this.focus = 'list';
|
|
498
|
+
return true;
|
|
499
|
+
}
|
|
500
|
+
if (matchesKey(data, 'tab') || matchesKey(data, 'shift+tab')) {
|
|
501
|
+
const order = ['section', ...(this.discoverable() ? ['source', 'filter'] : []), 'actions', 'list',
|
|
502
|
+
...(this.detail || this.pointer.wide && (this.list.selected?.data?.item || this.list.selected?.data?.hub) ? ['inspector'] : [])];
|
|
503
|
+
const index = order.indexOf(this.focus);
|
|
504
|
+
this.focus = order[(index + (matchesKey(data, 'shift+tab') ? -1 : 1) + order.length) % order.length];
|
|
505
|
+
if (this.focus === 'inspector' && !this.detail)
|
|
506
|
+
void this.open(this.list.selected?.data);
|
|
507
|
+
if (this.focus !== 'section' && this.focus !== 'inspector') {
|
|
508
|
+
this.detail = undefined;
|
|
509
|
+
this.content = undefined;
|
|
510
|
+
}
|
|
511
|
+
return true;
|
|
512
|
+
}
|
|
513
|
+
if (this.focus === 'inspector' && this.content !== undefined) {
|
|
514
|
+
if (matchesKey(data, 'escape')) {
|
|
515
|
+
this.content = undefined;
|
|
516
|
+
return true;
|
|
517
|
+
}
|
|
518
|
+
const delta = matchesKey(data, 'down') ? 1 : matchesKey(data, 'up') ? -1 : matchesKey(data, 'pageDown') ? this.contentRows : matchesKey(data, 'pageUp') ? -this.contentRows : 0;
|
|
519
|
+
if (matchesKey(data, 'home'))
|
|
520
|
+
this.contentOffset = 0;
|
|
521
|
+
else if (matchesKey(data, 'end'))
|
|
522
|
+
this.contentOffset = Math.max(0, this.contentTotal - this.contentRows);
|
|
523
|
+
else
|
|
524
|
+
this.contentOffset = Math.max(0, Math.min(Math.max(0, this.contentTotal - this.contentRows), this.contentOffset + delta));
|
|
525
|
+
return true;
|
|
526
|
+
}
|
|
527
|
+
if (this.focus === 'inspector' && this.detail) {
|
|
528
|
+
if (matchesKey(data, 'escape')) {
|
|
529
|
+
this.detail = undefined;
|
|
530
|
+
this.focus = 'list';
|
|
531
|
+
return true;
|
|
532
|
+
}
|
|
533
|
+
this.actionList.handleInput(data);
|
|
534
|
+
return true;
|
|
535
|
+
}
|
|
536
|
+
if (this.focus !== 'list') {
|
|
537
|
+
const delta = matchesKey(data, 'left') ? -1 : matchesKey(data, 'right') ? 1 : 0;
|
|
538
|
+
if (this.focus === 'source' && delta) {
|
|
539
|
+
this.source = this.source === 'installed' ? 'discover' : 'installed';
|
|
540
|
+
this.fill();
|
|
541
|
+
}
|
|
542
|
+
else if (this.focus === 'filter' && delta) {
|
|
543
|
+
if (this.source === 'discover') {
|
|
544
|
+
const channels = ['all', 'community', 'official'];
|
|
545
|
+
this.channel = channels[(channels.indexOf(this.channel) + delta + channels.length) % channels.length];
|
|
546
|
+
}
|
|
547
|
+
else
|
|
548
|
+
this.local = LOCAL[(LOCAL.indexOf(this.local) + delta + LOCAL.length) % LOCAL.length];
|
|
549
|
+
this.fill();
|
|
550
|
+
}
|
|
551
|
+
else if (this.focus === 'section' && matchesKey(data, 'enter')) {
|
|
552
|
+
this.focus = 'list';
|
|
553
|
+
}
|
|
554
|
+
else if (matchesKey(data, 'enter')) {
|
|
555
|
+
void this.perform(async () => {
|
|
556
|
+
if (this.focus === 'actions') {
|
|
557
|
+
const actions = this.toolbarActions();
|
|
558
|
+
const selected = await this.options.ui.select('Resource actions', actions.map(row => this.toolbarLabel(row)));
|
|
559
|
+
const action = actions.find(row => this.toolbarLabel(row) === selected);
|
|
560
|
+
if (action?.action)
|
|
561
|
+
await this.toolbar(action.action);
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
const choices = this.focus === 'source' ? ['Installed', 'Discover'] : this.source === 'discover' ? ['All', 'Community', 'Official'] : ['All', 'Local', 'Built-in'];
|
|
565
|
+
const selected = await this.options.ui.select('Choose view', choices);
|
|
566
|
+
if (!selected)
|
|
567
|
+
return;
|
|
568
|
+
if (this.focus === 'source')
|
|
569
|
+
this.source = selected.toLowerCase();
|
|
570
|
+
else if (this.source === 'discover')
|
|
571
|
+
this.channel = selected.toLowerCase();
|
|
572
|
+
else
|
|
573
|
+
this.local = selected.toLowerCase();
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
else if (matchesKey(data, 'escape')) {
|
|
577
|
+
this.focus = 'list';
|
|
578
|
+
this.detail = undefined;
|
|
579
|
+
this.content = undefined;
|
|
580
|
+
return true;
|
|
581
|
+
}
|
|
582
|
+
return true;
|
|
583
|
+
}
|
|
584
|
+
if (matchesKey(data, 'escape'))
|
|
585
|
+
return this.list.query ? this.list.handleInput(data) : false;
|
|
586
|
+
return this.list.handleInput(data);
|
|
587
|
+
}
|
|
588
|
+
handleMouse(event) {
|
|
589
|
+
if (event.type === 'wheel') {
|
|
590
|
+
if (this.busy)
|
|
591
|
+
return { handled: true, render: false };
|
|
592
|
+
const inspector = this.pointer.wide ? event.x >= this.pointer.listWidth + 3 : Boolean(this.detail);
|
|
593
|
+
if (inspector && !this.detail)
|
|
594
|
+
void this.open(this.list.selected?.data);
|
|
595
|
+
this.focus = inspector && this.detail ? 'inspector' : 'list';
|
|
596
|
+
if (this.focus === 'list') {
|
|
597
|
+
this.detail = undefined;
|
|
598
|
+
this.content = undefined;
|
|
599
|
+
}
|
|
600
|
+
this.handleInput((event.wheelDelta ?? 1) < 0 ? '\x1b[A' : '\x1b[B');
|
|
601
|
+
return { handled: true, render: true };
|
|
602
|
+
}
|
|
603
|
+
if (event.type !== 'click' || event.button !== 'left' || this.busy)
|
|
604
|
+
return undefined;
|
|
605
|
+
if (event.y < this.pointer.body && (this.pointer.wide || !this.detail)) {
|
|
606
|
+
if ((event.y === 0 || event.y === this.filterRow) && this.discoverable()) {
|
|
607
|
+
this.focus = this.filterRow !== 0 ? event.y === 0 ? 'source' : 'filter' : event.x < this.filterStart ? 'source' : 'filter';
|
|
608
|
+
this.handleInput('\r');
|
|
609
|
+
return { handled: true, render: true };
|
|
610
|
+
}
|
|
611
|
+
if (event.y === this.actionRow) {
|
|
612
|
+
const hit = this.toolbarHits.find(hit => event.x >= hit.start && event.x < hit.end);
|
|
613
|
+
if (!hit)
|
|
614
|
+
return undefined;
|
|
615
|
+
void this.open(hit.row);
|
|
616
|
+
return { handled: true, render: true };
|
|
617
|
+
}
|
|
618
|
+
return undefined;
|
|
619
|
+
}
|
|
620
|
+
if ((this.pointer.wide && event.x >= this.pointer.listWidth + 3) || (!this.pointer.wide && this.detail)) {
|
|
621
|
+
if (!this.detail)
|
|
622
|
+
void this.open(this.list.selected?.data);
|
|
623
|
+
this.focus = 'inspector';
|
|
624
|
+
if (this.content !== undefined)
|
|
625
|
+
return { handled: true, render: true };
|
|
626
|
+
return this.actionList.handleMouse({ ...event, x: event.x - (this.pointer.wide ? this.pointer.listWidth + 3 : 0), y: event.y - this.inspectorActionRow });
|
|
627
|
+
}
|
|
628
|
+
this.focus = 'list';
|
|
629
|
+
this.detail = undefined;
|
|
630
|
+
this.content = undefined;
|
|
631
|
+
return this.list.handleMouse({ ...event, y: event.y - this.pointer.body });
|
|
632
|
+
}
|
|
633
|
+
description(row) {
|
|
634
|
+
if (!row)
|
|
635
|
+
return ['Select a resource to inspect it.'];
|
|
636
|
+
if (row.hub) {
|
|
637
|
+
const entry = row.hub;
|
|
638
|
+
return [entry.title, entry.description, `Publisher: ${entry.publisher}`, `Channel: ${entry.channel} · ${entry.version}`, `License: ${entry.license}`, `Source: ${entry.sourceUrl}`, 'Installation does not activate this resource.'];
|
|
639
|
+
}
|
|
640
|
+
const item = row.item;
|
|
641
|
+
if (!item)
|
|
642
|
+
return [row.section ? TITLES[row.section] : ''];
|
|
643
|
+
const state = this.options.runtime().skillCommands ? '' : item.section === 'skills' ? 'Skill commands are off. Browsing remains available; enable them in Settings to insert an invocation.' : '';
|
|
644
|
+
const connection = this.snapshot?.connections.find(connection => connection.id === item.connectionId);
|
|
645
|
+
let endpoint;
|
|
646
|
+
if (connection?.url) {
|
|
647
|
+
try {
|
|
648
|
+
const url = new URL(connection.url);
|
|
649
|
+
endpoint = `${url.origin}${url.pathname}`;
|
|
650
|
+
}
|
|
651
|
+
catch {
|
|
652
|
+
endpoint = 'Invalid URL';
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
return [item.name, `Owner: ${item.owner}`, `Origin: ${item.origin}`, `State: ${item.status}`, item.description, ...(item.scope ? [`Scope: ${item.scope}`] : []),
|
|
656
|
+
...(connection ? [endpoint ? `Endpoint: ${endpoint}` : `Executable: ${connection.command} · ${connection.args.length} arguments`,
|
|
657
|
+
`Authentication: ${connection.auth}`, `Environment keys: ${connection.envKeys.join(', ') || 'none'}`, `Header keys: ${connection.headerKeys.join(', ') || 'none'}`,
|
|
658
|
+
...(connection.error ? [`Last error: ${connection.error}`] : [])] : []),
|
|
659
|
+
...(item.path ? [`Path: ${item.path}`] : []), ...(item.required ? [item.required] : []), ...(state ? [state] : []),
|
|
660
|
+
...(item.section === 'tools' ? [`Saved approval: ${this.permissions?.globalTools.includes(item.name) ? 'allowed everywhere' : 'no per-tool grant'}${this.permissions?.full ? ' · Full Permission on' : ''}`,
|
|
661
|
+
'Availability is separate from authorization. Every call still passes the approval gate.', ...(item.parameters ? [JSON.stringify(item.parameters, null, 2)] : [])] : [])];
|
|
662
|
+
}
|
|
663
|
+
renderInspector(width, rows, row, bodyStart) {
|
|
664
|
+
const theme = this.options.theme();
|
|
665
|
+
if (this.content !== undefined) {
|
|
666
|
+
const lines = this.content.split('\n').flatMap(line => wrapTextWithAnsi(safeText(line), width));
|
|
667
|
+
this.contentRows = Math.max(1, rows - 1);
|
|
668
|
+
this.contentTotal = lines.length;
|
|
669
|
+
this.contentOffset = Math.min(this.contentOffset, Math.max(0, lines.length - this.contentRows));
|
|
670
|
+
return [theme.bold(`Content · ${this.contentOffset + 1}/${Math.max(1, lines.length)}`), ...lines.slice(this.contentOffset, this.contentOffset + this.contentRows)];
|
|
671
|
+
}
|
|
672
|
+
const description = this.description(row).flatMap(line => wrapTextWithAnsi(safeText(line), width))
|
|
673
|
+
.slice(0, Math.max(1, Math.min(6, Math.floor(rows / 2))));
|
|
674
|
+
const actions = row ? this.groupedActions(row) : [];
|
|
675
|
+
this.actionList.setRows(actions.map(action => ({ id: action.id, label: action.label, group: this.actionGroup(action.id), data: action })));
|
|
676
|
+
this.actionList.focused = this.focus === 'inspector';
|
|
677
|
+
this.inspectorActionRow = bodyStart + description.length + 1;
|
|
678
|
+
return [...description, '', ...this.actionList.render(width, Math.max(0, rows - description.length - 1))].slice(0, rows);
|
|
679
|
+
}
|
|
680
|
+
render(width, rows) {
|
|
681
|
+
if (width < 1 || rows < 1)
|
|
682
|
+
return [];
|
|
683
|
+
const theme = this.options.theme();
|
|
684
|
+
const clip = (lines) => lines.slice(0, rows).map(line => truncateToWidth(line, width, ''));
|
|
685
|
+
if (!this.snapshot)
|
|
686
|
+
return clip([theme.fg(this.error ? 'error' : 'dim', this.error ? `Could not read resources: ${this.error}` : 'Reading Harness resources…'), 'Enter retry · Esc close']);
|
|
687
|
+
const wide = width >= 88;
|
|
688
|
+
if (!wide && this.detail) {
|
|
689
|
+
this.pointer = { wide: false, body: 0, listWidth: width };
|
|
690
|
+
return clip(this.renderInspector(width, rows, this.detail, 0));
|
|
691
|
+
}
|
|
692
|
+
const control = (text, focus) => this.focus === focus ? selectedRow(selectionText(theme, text), Math.min(width, visibleWidth(text)), theme) : theme.fg('muted', text);
|
|
693
|
+
const header = [];
|
|
694
|
+
this.filterStart = visibleWidth(this.source === 'installed' ? '[Installed] Discover' : 'Installed [Discover]') + 2;
|
|
695
|
+
const sourceLabel = control(this.source === 'installed' ? '[Installed] Discover' : 'Installed [Discover]', 'source');
|
|
696
|
+
const filterLabel = control(`Filter: ${this.source === 'discover' ? this.channel : this.local}`, 'filter');
|
|
697
|
+
this.filterRow = width < 44 && this.discoverable() ? 1 : 0;
|
|
698
|
+
if (this.discoverable())
|
|
699
|
+
header.push(...(this.filterRow === 1 ? [sourceLabel, filterLabel] : [`${sourceLabel} ${filterLabel}`]));
|
|
700
|
+
else if (this.section === 'foundation')
|
|
701
|
+
header.push(theme.fg('muted', 'Fixed runtime · changes by release'));
|
|
702
|
+
else if (!this.snapshot.variant.pinned)
|
|
703
|
+
header.push(theme.fg('muted', 'No variant selected · read-only'));
|
|
704
|
+
else if (width < 48)
|
|
705
|
+
header.push(theme.fg('muted', `Pinned: ${this.snapshot.variant.pinned}`), theme.fg('muted', `Latest: ${this.snapshot.variant.latest ?? 'none'}`));
|
|
706
|
+
else
|
|
707
|
+
header.push(theme.fg('muted', `Pinned ${this.snapshot.variant.pinned} · Latest ${this.snapshot.variant.latest ?? 'none'}`));
|
|
708
|
+
const toolbar = this.toolbarActions();
|
|
709
|
+
this.actionRow = header.length;
|
|
710
|
+
let actionX = 0;
|
|
711
|
+
this.toolbarHits = toolbar.map(row => {
|
|
712
|
+
const start = actionX;
|
|
713
|
+
actionX += visibleWidth(this.toolbarLabel(row)) + 3;
|
|
714
|
+
return { start, end: actionX - 3, row };
|
|
715
|
+
});
|
|
716
|
+
header.push(control(toolbar.map(row => this.toolbarLabel(row)).join(' · '), 'actions'));
|
|
717
|
+
const status = this.error || (this.source === 'discover' && this.discoverable() ? `${this.snapshot.hub?.origin ?? 'unavailable'} catalog${this.snapshot.hub?.refreshedAt ? ` · ${this.snapshot.hub.refreshedAt}` : ''}${this.snapshot.hub?.error ? ` · ${this.snapshot.hub.error}` : ''}` : '');
|
|
718
|
+
if (this.busy || status)
|
|
719
|
+
header.push(theme.fg(this.error ? 'error' : 'dim', this.busy ? 'Working…' : status));
|
|
720
|
+
const bodyRows = Math.max(0, rows - header.length);
|
|
721
|
+
const listWidth = wide ? Math.max(32, Math.floor((width - 3) * 0.4)) : width;
|
|
722
|
+
this.pointer = { wide, body: header.length, listWidth };
|
|
723
|
+
this.list.focused = this.focus === 'list';
|
|
724
|
+
let list = this.list.render(listWidth, bodyRows);
|
|
725
|
+
if (!this.list.total && bodyRows >= 3) {
|
|
726
|
+
const message = this.source === 'discover' ? `No ${this.channel === 'all' ? '' : this.channel + ' '}entries for ${TITLES[this.section]}. Change the filter or refresh Hub.` : this.section === 'variant' ? 'No variant selected. This terminal uses foundation resources only.' : 'No resources in this view. Change the filter, import locally, or choose Discover.';
|
|
727
|
+
list = wrapTextWithAnsi(message, listWidth).slice(0, bodyRows);
|
|
728
|
+
}
|
|
729
|
+
if (!wide)
|
|
730
|
+
return clip([...header, ...list]);
|
|
731
|
+
const inspector = this.renderInspector(width - listWidth - 3, bodyRows, this.detail ?? this.list.selected?.data, header.length);
|
|
732
|
+
const body = Array.from({ length: bodyRows }, (_, index) => {
|
|
733
|
+
const line = truncateToWidth(list[index] ?? '', listWidth, '');
|
|
734
|
+
return line + ' '.repeat(Math.max(0, listWidth - visibleWidth(line))) + theme.fg('dim', ' │ ') + (inspector[index] ?? '');
|
|
735
|
+
});
|
|
736
|
+
return clip([...header, ...body]);
|
|
737
|
+
}
|
|
738
|
+
}
|