@f5xc-salesdemos/xcsh 18.91.4 → 18.92.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@f5xc-salesdemos/xcsh",
4
- "version": "18.91.4",
4
+ "version": "18.92.0",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://github.com/f5xc-salesdemos/xcsh",
7
7
  "author": "Can Boluk",
@@ -50,15 +50,15 @@
50
50
  "dependencies": {
51
51
  "@agentclientprotocol/sdk": "0.16.1",
52
52
  "@mozilla/readability": "^0.6",
53
- "@f5xc-salesdemos/xcsh-stats": "18.91.4",
54
- "@f5xc-salesdemos/pi-agent-core": "18.91.4",
55
- "@f5xc-salesdemos/pi-ai": "18.91.4",
56
- "@f5xc-salesdemos/pi-natives": "18.91.4",
57
- "@f5xc-salesdemos/pi-tui": "18.91.4",
58
- "@f5xc-salesdemos/pi-utils": "18.91.4",
53
+ "@f5xc-salesdemos/xcsh-stats": "18.92.0",
54
+ "@f5xc-salesdemos/pi-agent-core": "18.92.0",
55
+ "@f5xc-salesdemos/pi-ai": "18.92.0",
56
+ "@f5xc-salesdemos/pi-natives": "18.92.0",
57
+ "@f5xc-salesdemos/pi-tui": "18.92.0",
58
+ "@f5xc-salesdemos/pi-utils": "18.92.0",
59
59
  "@sinclair/typebox": "^0.34",
60
60
  "@xterm/headless": "^6.0",
61
- "ajv": "^8.18",
61
+ "ajv": "^8.20",
62
62
  "chalk": "^5.6",
63
63
  "diff": "^8.0",
64
64
  "fflate": "0.8.2",
@@ -61,7 +61,9 @@ function generateTypeScript(config: BrandingConfig): string {
61
61
  "",
62
62
  `export const BRANDING_CANONICAL = ${JSON.stringify(config.canonical, null, 2)} as const;`,
63
63
  "",
64
- `export const BRANDING_DEPRECATIONS = ${JSON.stringify(config.deprecations, null, 2)} as const;`,
64
+ config.deprecations
65
+ ? `export const BRANDING_DEPRECATIONS = ${JSON.stringify(config.deprecations, null, 2)} as const;`
66
+ : "export const BRANDING_DEPRECATIONS = null;",
65
67
  "",
66
68
  `export const BRANDING_GLOSSARY = ${JSON.stringify(config.glossary, null, 2)} as const;`,
67
69
  "",
@@ -35,7 +35,7 @@ export const BRANDING_CANONICAL = {
35
35
  },
36
36
  } as const;
37
37
 
38
- export const BRANDING_DEPRECATIONS = undefined as const;
38
+ export const BRANDING_DEPRECATIONS = null;
39
39
 
40
40
  export const BRANDING_GLOSSARY = {
41
41
  XCKS: {
@@ -17,17 +17,17 @@ export interface BuildInfo {
17
17
  }
18
18
 
19
19
  export const BUILD_INFO: BuildInfo = {
20
- "version": "18.91.4",
21
- "commit": "256f0bffcb5ca8e6faee69f91f57a85708ba72b6",
22
- "shortCommit": "256f0bf",
20
+ "version": "18.92.0",
21
+ "commit": "2f4efd2a77846d164e2c0bdfbf3c6b204f3c6561",
22
+ "shortCommit": "2f4efd2",
23
23
  "branch": "main",
24
- "tag": "v18.91.4",
25
- "commitDate": "2026-06-02T12:57:02-04:00",
26
- "buildDate": "2026-06-02T17:18:19.466Z",
24
+ "tag": "v18.92.0",
25
+ "commitDate": "2026-06-03T01:35:38Z",
26
+ "buildDate": "2026-06-03T01:53:31.494Z",
27
27
  "dirty": true,
28
28
  "prNumber": "",
29
29
  "repoUrl": "https://github.com/f5xc-salesdemos/xcsh",
30
30
  "repoSlug": "f5xc-salesdemos/xcsh",
31
- "commitUrl": "https://github.com/f5xc-salesdemos/xcsh/commit/256f0bffcb5ca8e6faee69f91f57a85708ba72b6",
32
- "releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v18.91.4"
31
+ "commitUrl": "https://github.com/f5xc-salesdemos/xcsh/commit/2f4efd2a77846d164e2c0bdfbf3c6b204f3c6561",
32
+ "releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v18.92.0"
33
33
  };
@@ -92,6 +92,23 @@ function renderL0(index: TerraformIndex): string {
92
92
  "|----------|-----------|-------------|",
93
93
  ...index.categories.map(c => `| ${c.name} | ${c.resource_count} | ${c.description} |`),
94
94
  "",
95
+ "## Quick Reference",
96
+ "",
97
+ "Common resources — output these in ```terraform code blocks:",
98
+ "",
99
+ "- `f5xc_http_loadbalancer`: name, namespace, domains, advertise_on_public_default_vip {}",
100
+ '- `f5xc_origin_pool`: name, namespace, port, origin_servers { public_ip { ip = "x.x.x.x" } }',
101
+ '- `f5xc_healthcheck`: name, namespace, http_health_check { path = "/healthz" }, timeout, interval, unhealthy_threshold, healthy_threshold',
102
+ "- `f5xc_app_firewall`: name, namespace, blocking {}",
103
+ "- `f5xc_service_policy`: name, namespace, rule_list { rules { ... } }, any_server {}",
104
+ "- `f5xc_certificate`: name, namespace, certificate_url, private_key { blindfold_secret_info { location } }",
105
+ "- `f5xc_rate_limiter_policy`: name, namespace, any_server {}",
106
+ '- `f5xc_api_definition`: name, namespace, swagger_specs = ["string:///..."]',
107
+ "- `f5xc_namespace`: name",
108
+ "",
109
+ "Import: `terraform import f5xc_{type}.example namespace/name`",
110
+ 'Cross-refs use blocks: `app_firewall { name = "x" namespace = "y" }` not string refs.',
111
+ "",
95
112
  ];
96
113
  return lines.join("\n");
97
114
  }
@@ -122,58 +139,26 @@ function renderL1(cat: TerraformCategory, allResources: Readonly<Record<string,
122
139
  return lines.join("\n");
123
140
  }
124
141
 
125
- function renderL2(name: string, res: TerraformResource, categorySlug: string): string {
126
- const lines = [
127
- `# f5xc_${name}`,
128
- "",
129
- `Category: ${res.category} | \`xcsh://terraform/${categorySlug}\``,
130
- "",
131
- res.description,
132
- "",
133
- ];
134
-
135
- if (res.required.length > 0) {
136
- lines.push("## Required", "");
137
- for (const f of res.required) lines.push(`- ${f}`);
138
- lines.push("");
139
- }
142
+ function renderL2(name: string, res: TerraformResource, _categorySlug: string): string {
143
+ const lines = [`# f5xc_${name}`, "", res.description, "", `Required: ${res.required.join(", ") || "none"}`];
140
144
 
141
145
  if (res.oneof_groups && res.oneof_groups.length > 0) {
142
- lines.push("## OneOf Groups", "");
143
- for (const g of res.oneof_groups) {
144
- if (g.parent) {
145
- lines.push(`Within ${g.parent}, pick exactly one:`);
146
- } else {
147
- lines.push("Pick exactly one:");
148
- }
149
- for (const f of g.fields) lines.push(` ${f}`);
150
- lines.push("");
146
+ const groups = res.oneof_groups.filter(g => !g.parent).map(g => g.fields.join(" | "));
147
+ if (groups.length > 0) {
148
+ lines.push("", "OneOf (pick one per group, use empty block `field {}`):");
149
+ for (const g of groups) lines.push(`- ${g}`);
151
150
  }
152
151
  }
153
152
 
154
- if (res.server_defaults && res.server_defaults.length > 0) {
155
- lines.push("## Server Defaults (safe to omit)", "");
156
- for (const f of res.server_defaults) lines.push(`- ${f}`);
157
- lines.push("");
158
- }
159
-
160
153
  if (res.minimal_config) {
161
- lines.push("## Minimal Valid Config", "", "```terraform", res.minimal_config, "```", "");
154
+ lines.push("", "## Config", "", "```terraform", res.minimal_config, "```");
162
155
  }
163
156
 
164
- lines.push("## Dependencies", "");
157
+ lines.push("", `Import: \`${res.import_syntax}\``);
165
158
  if (res.dependencies.requires.length > 0) {
166
- lines.push(`Requires: ${res.dependencies.requires.join(", ")}`);
167
- } else {
168
- lines.push("Requires: none");
169
- }
170
- if (res.dependencies.used_by && res.dependencies.used_by.length > 0) {
171
- lines.push(`Used by: ${res.dependencies.used_by.join(", ")}`);
159
+ lines.push(`Depends on: ${res.dependencies.requires.join(", ")}`);
172
160
  }
173
161
  lines.push("");
174
-
175
- lines.push("## Import", "", `\`${res.import_syntax}\``, "");
176
-
177
162
  return lines.join("\n");
178
163
  }
179
164
 
@@ -0,0 +1,5 @@
1
+ export * from "./plugin-dashboard";
2
+ export * from "./plugin-inspector-pane";
3
+ export * from "./plugin-list-pane";
4
+ export * from "./state-manager";
5
+ export * from "./types";
@@ -0,0 +1,449 @@
1
+ import * as os from "node:os";
2
+ import * as path from "node:path";
3
+ import {
4
+ type Component,
5
+ Container,
6
+ matchesKey,
7
+ padding,
8
+ replaceTabs,
9
+ Spacer,
10
+ Text,
11
+ truncateToWidth,
12
+ visibleWidth,
13
+ } from "@f5xc-salesdemos/pi-tui";
14
+ import { getConfigDirName } from "@f5xc-salesdemos/pi-utils";
15
+ import { invalidate as invalidateFsCache } from "../../../capability/fs";
16
+ import { clearClaudePluginRootsCache, resolveActiveProjectRegistryPath } from "../../../discovery/helpers";
17
+ import { PluginManager } from "../../../extensibility/plugins";
18
+ import {
19
+ getInstalledPluginsRegistryPath,
20
+ getMarketplacesCacheDir,
21
+ getMarketplacesRegistryPath,
22
+ getPluginsCacheDir,
23
+ MarketplaceManager,
24
+ } from "../../../extensibility/plugins/marketplace";
25
+ import { theme } from "../../theme/theme";
26
+ import { matchesAppInterrupt } from "../../utils/keybinding-matchers";
27
+ import { DynamicBorder } from "../dynamic-border";
28
+ import { PluginInspectorPane } from "./plugin-inspector-pane";
29
+ import { PluginListPane } from "./plugin-list-pane";
30
+ import { applySearch, buildTabs, createInitialState, filterByTab, loadAllPlugins } from "./state-manager";
31
+ import type { DashboardPlugin, PluginDashboardState, PluginTabId } from "./types";
32
+
33
+ const DEFAULT_MARKETPLACE = "anthropics/claude-plugins-official";
34
+
35
+ class TwoColumnBody implements Component {
36
+ constructor(
37
+ private readonly leftPane: PluginListPane,
38
+ private readonly rightPane: PluginInspectorPane,
39
+ private readonly maxHeight: number,
40
+ ) {}
41
+
42
+ render(width: number): string[] {
43
+ const leftWidth = Math.floor(width * 0.5);
44
+ const rightWidth = width - leftWidth - 3;
45
+ const leftLines = this.leftPane.render(leftWidth);
46
+ const rightLines = this.rightPane.render(rightWidth);
47
+ const lineCount = Math.min(this.maxHeight, Math.max(leftLines.length, rightLines.length));
48
+ const out: string[] = [];
49
+ const separator = theme.fg("dim", ` ${theme.boxSharp.vertical} `);
50
+
51
+ for (let i = 0; i < lineCount; i++) {
52
+ const left = truncateToWidth(leftLines[i] ?? "", leftWidth);
53
+ const leftPadded = left + padding(Math.max(0, leftWidth - visibleWidth(left)));
54
+ const right = truncateToWidth(rightLines[i] ?? "", rightWidth);
55
+ out.push(leftPadded + separator + right);
56
+ }
57
+
58
+ return out;
59
+ }
60
+
61
+ invalidate(): void {
62
+ this.leftPane.invalidate?.();
63
+ this.rightPane.invalidate?.();
64
+ }
65
+ }
66
+
67
+ export class PluginDashboard extends Container {
68
+ #state!: PluginDashboardState;
69
+ #mgr!: MarketplaceManager;
70
+ #npmMgr!: PluginManager;
71
+
72
+ onClose?: () => void;
73
+ onRequestRender?: () => void;
74
+
75
+ private constructor(
76
+ private readonly cwd: string,
77
+ private readonly terminalHeight: number,
78
+ ) {
79
+ super();
80
+ }
81
+
82
+ static async create(cwd: string, terminalHeight?: number): Promise<PluginDashboard> {
83
+ const dashboard = new PluginDashboard(cwd, terminalHeight ?? process.stdout.rows ?? 24);
84
+ await dashboard.#init();
85
+ return dashboard;
86
+ }
87
+
88
+ async #init(): Promise<void> {
89
+ const projectRegistryPath = (await resolveActiveProjectRegistryPath(this.cwd)) ?? undefined;
90
+
91
+ this.#mgr = new MarketplaceManager({
92
+ marketplacesRegistryPath: getMarketplacesRegistryPath(),
93
+ installedRegistryPath: getInstalledPluginsRegistryPath(),
94
+ projectInstalledRegistryPath: projectRegistryPath,
95
+ marketplacesCacheDir: getMarketplacesCacheDir(),
96
+ pluginsCacheDir: getPluginsCacheDir(),
97
+ clearPluginRootsCache: (extraPaths?: readonly string[]) => {
98
+ const home = os.homedir();
99
+ invalidateFsCache(path.join(home, getConfigDirName(), "plugins", "installed_plugins.json"));
100
+ for (const p of extraPaths ?? []) invalidateFsCache(p);
101
+ clearClaudePluginRootsCache();
102
+ },
103
+ });
104
+
105
+ this.#npmMgr = new PluginManager();
106
+
107
+ try {
108
+ const marketplaces = await this.#mgr.listMarketplaces();
109
+ if (marketplaces.length === 0) {
110
+ await this.#mgr.addMarketplace(DEFAULT_MARKETPLACE);
111
+ }
112
+ } catch {
113
+ // Network failure on first run is fine — continue with whatever is available
114
+ }
115
+
116
+ try {
117
+ this.#state = await createInitialState(this.#mgr, this.#npmMgr);
118
+ } catch (error) {
119
+ this.#state = {
120
+ tabs: [{ id: "installed", label: "Installed", count: 0 }],
121
+ activeTabIndex: 0,
122
+ allPlugins: [],
123
+ tabFiltered: [],
124
+ searchFiltered: [],
125
+ searchQuery: "",
126
+ selectedIndex: 0,
127
+ scrollOffset: 0,
128
+ notice: null,
129
+ loading: false,
130
+ loadError: error instanceof Error ? error.message : String(error),
131
+ };
132
+ }
133
+
134
+ this.#buildLayout();
135
+ }
136
+
137
+ #selectedPlugin(): DashboardPlugin | null {
138
+ return this.#state.searchFiltered[this.#state.selectedIndex] ?? null;
139
+ }
140
+
141
+ #activeTabId(): PluginTabId {
142
+ return this.#state.tabs[this.#state.activeTabIndex]?.id ?? "installed";
143
+ }
144
+
145
+ #getMaxVisibleItems(): number {
146
+ return Math.max(5, this.terminalHeight - 14);
147
+ }
148
+
149
+ #applyFilters(): void {
150
+ this.#state.tabFiltered = filterByTab(this.#state.allPlugins, this.#activeTabId());
151
+ this.#state.searchFiltered = applySearch(this.#state.tabFiltered, this.#state.searchQuery);
152
+ this.#clampSelection();
153
+ }
154
+
155
+ #clampSelection(): void {
156
+ const len = this.#state.searchFiltered.length;
157
+ if (len === 0) {
158
+ this.#state.selectedIndex = 0;
159
+ this.#state.scrollOffset = 0;
160
+ return;
161
+ }
162
+
163
+ this.#state.selectedIndex = Math.min(this.#state.selectedIndex, len - 1);
164
+ this.#state.selectedIndex = Math.max(0, this.#state.selectedIndex);
165
+
166
+ const maxVisible = this.#getMaxVisibleItems();
167
+ if (this.#state.selectedIndex < this.#state.scrollOffset) {
168
+ this.#state.scrollOffset = this.#state.selectedIndex;
169
+ } else if (this.#state.selectedIndex >= this.#state.scrollOffset + maxVisible) {
170
+ this.#state.scrollOffset = this.#state.selectedIndex - maxVisible + 1;
171
+ }
172
+ }
173
+
174
+ #switchTab(direction: 1 | -1): void {
175
+ if (this.#state.tabs.length === 0) return;
176
+ this.#state.activeTabIndex =
177
+ (this.#state.activeTabIndex + direction + this.#state.tabs.length) % this.#state.tabs.length;
178
+ this.#state.selectedIndex = 0;
179
+ this.#state.scrollOffset = 0;
180
+ this.#applyFilters();
181
+ this.#buildLayout();
182
+ }
183
+
184
+ #moveSelection(delta: -1 | 1): void {
185
+ if (this.#state.searchFiltered.length === 0) return;
186
+ this.#state.selectedIndex = Math.max(
187
+ 0,
188
+ Math.min(this.#state.searchFiltered.length - 1, this.#state.selectedIndex + delta),
189
+ );
190
+ this.#clampSelection();
191
+ this.#buildLayout();
192
+ }
193
+
194
+ async #reloadData(): Promise<void> {
195
+ this.#state.loading = true;
196
+ this.#state.loadError = null;
197
+ this.#buildLayout();
198
+
199
+ try {
200
+ const selectedId = this.#selectedPlugin()?.id;
201
+ const allPlugins = await loadAllPlugins(this.#mgr, this.#npmMgr);
202
+ const tabs = buildTabs(allPlugins);
203
+ const prevTabId = this.#activeTabId();
204
+ const nextTabIndex = Math.max(
205
+ 0,
206
+ tabs.findIndex(t => t.id === prevTabId),
207
+ );
208
+
209
+ this.#state.allPlugins = allPlugins;
210
+ this.#state.tabs = tabs;
211
+ this.#state.activeTabIndex = nextTabIndex;
212
+ this.#applyFilters();
213
+
214
+ if (selectedId) {
215
+ const idx = this.#state.searchFiltered.findIndex(p => p.id === selectedId);
216
+ if (idx >= 0) this.#state.selectedIndex = idx;
217
+ }
218
+ this.#clampSelection();
219
+ } catch (error) {
220
+ this.#state.loadError = error instanceof Error ? error.message : String(error);
221
+ } finally {
222
+ this.#state.loading = false;
223
+ this.#rebuildAndRender();
224
+ }
225
+ }
226
+
227
+ async #toggleEnabled(): Promise<void> {
228
+ const plugin = this.#selectedPlugin();
229
+ if (!plugin?.installed) return;
230
+
231
+ try {
232
+ if (plugin.source === "marketplace") {
233
+ await this.#mgr.setPluginEnabled(plugin.id, !plugin.enabled, plugin.scope);
234
+ }
235
+ plugin.enabled = !plugin.enabled;
236
+ this.#state.notice = `${plugin.enabled ? "Enabled" : "Disabled"} ${plugin.name}`;
237
+ } catch (error) {
238
+ this.#state.notice = `Toggle failed: ${error instanceof Error ? error.message : String(error)}`;
239
+ }
240
+ this.#rebuildAndRender();
241
+ }
242
+
243
+ async #handleEnterAction(): Promise<void> {
244
+ const plugin = this.#selectedPlugin();
245
+ if (!plugin) return;
246
+ const tabId = this.#activeTabId();
247
+
248
+ if (tabId === "available" && !plugin.installed) {
249
+ await this.#installPlugin(plugin);
250
+ } else if (tabId === "updates" && plugin.hasUpdate) {
251
+ await this.#upgradePlugin(plugin);
252
+ } else if (tabId === "installed" && plugin.installed && plugin.source === "marketplace") {
253
+ await this.#uninstallPlugin(plugin);
254
+ }
255
+ }
256
+
257
+ async #installPlugin(plugin: DashboardPlugin): Promise<void> {
258
+ if (!plugin.marketplace) return;
259
+ this.#state.notice = `Installing ${plugin.name}...`;
260
+ this.#rebuildAndRender();
261
+
262
+ try {
263
+ await this.#mgr.installPlugin(plugin.name, plugin.marketplace);
264
+ this.#state.notice = `Installed ${plugin.name}`;
265
+ await this.#reloadData();
266
+ } catch (error) {
267
+ this.#state.notice = `Install failed: ${error instanceof Error ? error.message : String(error)}`;
268
+ this.#rebuildAndRender();
269
+ }
270
+ }
271
+
272
+ async #uninstallPlugin(plugin: DashboardPlugin): Promise<void> {
273
+ this.#state.notice = `Uninstalling ${plugin.name}...`;
274
+ this.#rebuildAndRender();
275
+
276
+ try {
277
+ await this.#mgr.uninstallPlugin(plugin.id, plugin.scope);
278
+ this.#state.notice = `Uninstalled ${plugin.name}`;
279
+ await this.#reloadData();
280
+ } catch (error) {
281
+ this.#state.notice = `Uninstall failed: ${error instanceof Error ? error.message : String(error)}`;
282
+ this.#rebuildAndRender();
283
+ }
284
+ }
285
+
286
+ async #upgradePlugin(plugin: DashboardPlugin): Promise<void> {
287
+ this.#state.notice = `Upgrading ${plugin.name}...`;
288
+ this.#rebuildAndRender();
289
+
290
+ try {
291
+ await this.#mgr.upgradePlugin(plugin.id, plugin.scope);
292
+ this.#state.notice = `Upgraded ${plugin.name}`;
293
+ await this.#reloadData();
294
+ } catch (error) {
295
+ this.#state.notice = `Upgrade failed: ${error instanceof Error ? error.message : String(error)}`;
296
+ this.#rebuildAndRender();
297
+ }
298
+ }
299
+
300
+ #renderTabBar(): string {
301
+ const parts: string[] = [" "];
302
+ for (let i = 0; i < this.#state.tabs.length; i++) {
303
+ const tab = this.#state.tabs[i];
304
+ const label = `${tab.label} (${tab.count})`;
305
+ if (i === this.#state.activeTabIndex) {
306
+ parts.push(theme.bg("selectedBg", ` ${label} `));
307
+ } else {
308
+ parts.push(theme.fg("muted", ` ${label} `));
309
+ }
310
+ }
311
+ return parts.join("");
312
+ }
313
+
314
+ #getHelpText(): string {
315
+ const tabId = this.#activeTabId();
316
+ switch (tabId) {
317
+ case "available":
318
+ return " ↑/↓: navigate Enter: install Tab: next tab Ctrl+R: reload Esc: close";
319
+ case "updates":
320
+ return " ↑/↓: navigate Enter: upgrade Tab: next tab Ctrl+R: reload Esc: close";
321
+ default:
322
+ return " ↑/↓: navigate Space: toggle Enter: uninstall U: upgrade Tab: next tab Ctrl+R: reload Esc: close";
323
+ }
324
+ }
325
+
326
+ #rebuildAndRender(): void {
327
+ this.#buildLayout();
328
+ this.onRequestRender?.();
329
+ }
330
+
331
+ #buildLayout(): void {
332
+ this.clear();
333
+ this.addChild(new DynamicBorder());
334
+ this.addChild(new Text(theme.bold(theme.fg("contentAccent", " Plugin Control Center")), 0, 0));
335
+ this.addChild(new Text(this.#renderTabBar(), 0, 0));
336
+ this.addChild(new Spacer(1));
337
+
338
+ if (this.#state.notice) {
339
+ this.addChild(new Text(theme.fg("success", replaceTabs(this.#state.notice)), 0, 0));
340
+ this.addChild(new Spacer(1));
341
+ }
342
+
343
+ if (this.#state.loading) {
344
+ this.addChild(new Text(theme.fg("muted", "Loading plugins..."), 0, 0));
345
+ this.addChild(new Spacer(1));
346
+ } else if (this.#state.loadError) {
347
+ this.addChild(
348
+ new Text(theme.fg("error", `Failed to load plugins: ${replaceTabs(this.#state.loadError)}`), 0, 0),
349
+ );
350
+ this.addChild(new Spacer(1));
351
+ } else {
352
+ const selected = this.#selectedPlugin();
353
+ const listPane = new PluginListPane(
354
+ this.#state.searchFiltered,
355
+ this.#state.selectedIndex,
356
+ this.#state.scrollOffset,
357
+ this.#state.searchQuery,
358
+ this.#getMaxVisibleItems(),
359
+ this.#activeTabId(),
360
+ );
361
+ const inspector = new PluginInspectorPane(selected);
362
+ const bodyHeight = Math.max(5, this.terminalHeight - 8);
363
+ this.addChild(new TwoColumnBody(listPane, inspector, bodyHeight));
364
+ this.addChild(new Spacer(1));
365
+ this.addChild(new Text(theme.fg("dim", this.#getHelpText()), 0, 0));
366
+ }
367
+
368
+ this.addChild(new DynamicBorder());
369
+ }
370
+
371
+ handleInput(data: string): void {
372
+ if (matchesKey(data, "ctrl+c")) {
373
+ this.onClose?.();
374
+ return;
375
+ }
376
+
377
+ if (matchesAppInterrupt(data)) {
378
+ if (this.#state.searchQuery) {
379
+ this.#state.searchQuery = "";
380
+ this.#applyFilters();
381
+ this.#buildLayout();
382
+ } else {
383
+ this.onClose?.();
384
+ }
385
+ return;
386
+ }
387
+
388
+ if (matchesKey(data, "tab")) {
389
+ this.#switchTab(1);
390
+ return;
391
+ }
392
+
393
+ if (matchesKey(data, "shift+tab")) {
394
+ this.#switchTab(-1);
395
+ return;
396
+ }
397
+
398
+ if (matchesKey(data, "up") || data === "k") {
399
+ this.#moveSelection(-1);
400
+ return;
401
+ }
402
+
403
+ if (matchesKey(data, "down") || data === "j") {
404
+ this.#moveSelection(1);
405
+ return;
406
+ }
407
+
408
+ if (matchesKey(data, "space")) {
409
+ void this.#toggleEnabled();
410
+ return;
411
+ }
412
+
413
+ if (matchesKey(data, "enter") || matchesKey(data, "return") || data === "\n") {
414
+ void this.#handleEnterAction();
415
+ return;
416
+ }
417
+
418
+ if (data.toLowerCase() === "u") {
419
+ const plugin = this.#selectedPlugin();
420
+ if (plugin?.hasUpdate) {
421
+ void this.#upgradePlugin(plugin);
422
+ }
423
+ return;
424
+ }
425
+
426
+ if (matchesKey(data, "ctrl+r")) {
427
+ void this.#reloadData();
428
+ return;
429
+ }
430
+
431
+ if (matchesKey(data, "backspace") || matchesKey(data, "delete") || data === "\x7f") {
432
+ if (this.#state.searchQuery.length > 0) {
433
+ this.#state.searchQuery = this.#state.searchQuery.slice(0, -1);
434
+ this.#state.notice = null;
435
+ this.#applyFilters();
436
+ this.#buildLayout();
437
+ }
438
+ return;
439
+ }
440
+
441
+ if (data.length === 1 && data >= " " && data <= "~" && data !== "j" && data !== "k" && data !== "u") {
442
+ this.#state.searchQuery += data;
443
+ this.#state.notice = null;
444
+ this.#applyFilters();
445
+ this.#buildLayout();
446
+ return;
447
+ }
448
+ }
449
+ }
@@ -0,0 +1,84 @@
1
+ import type { Component } from "@f5xc-salesdemos/pi-tui";
2
+ import { replaceTabs, truncateToWidth, wrapTextWithAnsi } from "@f5xc-salesdemos/pi-tui";
3
+ import { theme } from "../../theme/theme";
4
+ import type { DashboardPlugin } from "./types";
5
+
6
+ export class PluginInspectorPane implements Component {
7
+ constructor(private readonly plugin: DashboardPlugin | null) {}
8
+
9
+ render(width: number): string[] {
10
+ if (!this.plugin) {
11
+ return [theme.fg("muted", "Select a plugin"), theme.fg("dim", "to view details")];
12
+ }
13
+
14
+ const lines: string[] = [];
15
+ const p = this.plugin;
16
+
17
+ lines.push(theme.bold(theme.fg("contentAccent", replaceTabs(p.name))));
18
+ lines.push("");
19
+
20
+ lines.push(`${theme.fg("muted", "Source:")} ${p.source}`);
21
+
22
+ if (p.marketplace) {
23
+ lines.push(`${theme.fg("muted", "Marketplace:")} ${replaceTabs(p.marketplace)}`);
24
+ }
25
+
26
+ if (p.installed) {
27
+ const statusIcon = p.enabled
28
+ ? theme.fg("success", `${theme.status.enabled} Enabled`)
29
+ : theme.fg("dim", `${theme.status.disabled} Disabled`);
30
+ lines.push(`${theme.fg("muted", "Status:")} ${statusIcon}`);
31
+ } else {
32
+ lines.push(`${theme.fg("muted", "Status:")} ${theme.fg("dim", "Not installed")}`);
33
+ }
34
+
35
+ if (p.scope) {
36
+ lines.push(`${theme.fg("muted", "Scope:")} ${p.scope}`);
37
+ }
38
+
39
+ if (p.version) {
40
+ lines.push(`${theme.fg("muted", "Version:")} ${p.version}`);
41
+ }
42
+
43
+ if (p.hasUpdate && p.updateVersion) {
44
+ lines.push(`${theme.fg("muted", "Update:")} ${theme.fg("warning", `v${p.updateVersion} available`)}`);
45
+ }
46
+
47
+ if (p.shadowedBy) {
48
+ lines.push(`${theme.fg("muted", "Shadowed:")} ${theme.fg("warning", `by ${p.shadowedBy} scope`)}`);
49
+ }
50
+
51
+ if (p.description) {
52
+ lines.push("");
53
+ lines.push(theme.fg("muted", "Description:"));
54
+ for (const wrapped of wrapTextWithAnsi(replaceTabs(p.description), Math.max(10, width - 2))) {
55
+ lines.push(truncateToWidth(` ${wrapped}`, width));
56
+ }
57
+ }
58
+
59
+ if (p.author) {
60
+ lines.push("");
61
+ lines.push(`${theme.fg("muted", "Author:")} ${replaceTabs(p.author)}`);
62
+ }
63
+
64
+ if (p.license) {
65
+ lines.push(`${theme.fg("muted", "License:")} ${replaceTabs(p.license)}`);
66
+ }
67
+
68
+ if (p.homepage) {
69
+ lines.push(`${theme.fg("muted", "Homepage:")} ${replaceTabs(p.homepage)}`);
70
+ }
71
+
72
+ if (p.category) {
73
+ lines.push(`${theme.fg("muted", "Category:")} ${replaceTabs(p.category)}`);
74
+ }
75
+
76
+ if (p.tags && p.tags.length > 0) {
77
+ lines.push(`${theme.fg("muted", "Tags:")} ${p.tags.map(t => replaceTabs(t)).join(", ")}`);
78
+ }
79
+
80
+ return lines;
81
+ }
82
+
83
+ invalidate(): void {}
84
+ }
@@ -0,0 +1,104 @@
1
+ import type { Component } from "@f5xc-salesdemos/pi-tui";
2
+ import { truncateToWidth } from "@f5xc-salesdemos/pi-tui";
3
+ import { theme } from "../../theme/theme";
4
+ import type { DashboardPlugin, PluginTabId } from "./types";
5
+
6
+ export class PluginListPane implements Component {
7
+ constructor(
8
+ private readonly plugins: DashboardPlugin[],
9
+ private readonly selectedIndex: number,
10
+ private readonly scrollOffset: number,
11
+ private readonly searchQuery: string,
12
+ private readonly maxVisible: number,
13
+ private readonly activeTab: PluginTabId,
14
+ ) {}
15
+
16
+ render(width: number): string[] {
17
+ const lines: string[] = [];
18
+ const searchPrefix = theme.fg("muted", "Search: ");
19
+ const searchText = this.searchQuery || theme.fg("dim", "type to filter");
20
+ lines.push(`${searchPrefix}${searchText}`);
21
+ lines.push("");
22
+
23
+ if (this.plugins.length === 0) {
24
+ const msg =
25
+ this.activeTab === "available"
26
+ ? "No plugins available. Add a marketplace first."
27
+ : this.activeTab === "updates"
28
+ ? "All plugins are up to date."
29
+ : "No plugins installed.";
30
+ lines.push(theme.fg("muted", ` ${msg}`));
31
+ return lines;
32
+ }
33
+
34
+ const start = this.scrollOffset;
35
+ const end = Math.min(start + this.maxVisible, this.plugins.length);
36
+
37
+ for (let i = start; i < end; i++) {
38
+ const plugin = this.plugins[i];
39
+ const selected = i === this.selectedIndex;
40
+ let line = this.#formatPluginLine(plugin);
41
+
42
+ if (selected) {
43
+ line = theme.bg("selectedBg", theme.bold(theme.fg("chromeAccent", line)));
44
+ } else if (!plugin.enabled && plugin.installed) {
45
+ line = theme.fg("dim", line);
46
+ }
47
+
48
+ lines.push(truncateToWidth(line, width));
49
+ }
50
+
51
+ if (this.plugins.length > this.maxVisible) {
52
+ lines.push(theme.fg("muted", ` (${this.selectedIndex + 1}/${this.plugins.length})`));
53
+ }
54
+
55
+ return lines;
56
+ }
57
+
58
+ #formatPluginLine(plugin: DashboardPlugin): string {
59
+ const parts: string[] = [" "];
60
+
61
+ if (plugin.installed) {
62
+ if (plugin.hasUpdate) {
63
+ parts.push(theme.fg("warning", "▲"));
64
+ } else if (plugin.enabled) {
65
+ parts.push(theme.fg("success", theme.status.enabled));
66
+ } else {
67
+ parts.push(theme.fg("dim", theme.status.disabled));
68
+ }
69
+ } else {
70
+ parts.push(theme.fg("dim", "·"));
71
+ }
72
+
73
+ parts.push(" ");
74
+ parts.push(plugin.name);
75
+
76
+ if (plugin.version) {
77
+ parts.push(theme.fg("dim", ` v${plugin.version}`));
78
+ }
79
+
80
+ if (plugin.hasUpdate && plugin.updateVersion) {
81
+ parts.push(theme.fg("warning", ` → v${plugin.updateVersion}`));
82
+ }
83
+
84
+ if (plugin.scope) {
85
+ parts.push(theme.fg("muted", ` [${plugin.scope}]`));
86
+ }
87
+
88
+ if (plugin.shadowedBy) {
89
+ parts.push(theme.fg("dim", " [shadowed]"));
90
+ }
91
+
92
+ if (plugin.installed && !plugin.enabled) {
93
+ parts.push(theme.fg("dim", " (disabled)"));
94
+ }
95
+
96
+ if (plugin.source === "npm") {
97
+ parts.push(theme.fg("dim", " (npm)"));
98
+ }
99
+
100
+ return parts.join("");
101
+ }
102
+
103
+ invalidate(): void {}
104
+ }
@@ -0,0 +1,211 @@
1
+ import type { PluginManager } from "../../../extensibility/plugins/manager";
2
+ import type { MarketplaceManager } from "../../../extensibility/plugins/marketplace";
3
+ import type { InstalledPluginSummary, MarketplacePluginEntry } from "../../../extensibility/plugins/marketplace/types";
4
+ import type { DashboardPlugin, PluginDashboardState, PluginTab, PluginTabId } from "./types";
5
+
6
+ function npmToDashboard(npm: { name: string; version: string; enabled: boolean }): DashboardPlugin {
7
+ return {
8
+ id: `npm:${npm.name}`,
9
+ name: npm.name,
10
+ source: "npm",
11
+ version: npm.version,
12
+ installed: true,
13
+ enabled: npm.enabled !== false,
14
+ hasUpdate: false,
15
+ };
16
+ }
17
+
18
+ function installedToDashboard(summary: InstalledPluginSummary, updateMap: Map<string, string>): DashboardPlugin {
19
+ const entry = summary.entries[0];
20
+ const atIdx = summary.id.lastIndexOf("@");
21
+ const name = atIdx > 0 ? summary.id.slice(0, atIdx) : summary.id;
22
+ const marketplace = atIdx > 0 ? summary.id.slice(atIdx + 1) : undefined;
23
+ const updateVersion = updateMap.get(`${summary.id}:${summary.scope}`);
24
+
25
+ return {
26
+ id: summary.id,
27
+ name,
28
+ marketplace,
29
+ source: "marketplace",
30
+ scope: summary.scope,
31
+ version: entry?.version,
32
+ installed: true,
33
+ enabled: entry?.enabled !== false,
34
+ shadowedBy: summary.shadowedBy,
35
+ hasUpdate: !!updateVersion,
36
+ updateVersion,
37
+ };
38
+ }
39
+
40
+ function catalogToDashboard(entry: MarketplacePluginEntry, marketplace: string): DashboardPlugin {
41
+ return {
42
+ id: `${entry.name}@${marketplace}`,
43
+ name: entry.name,
44
+ marketplace,
45
+ source: "marketplace",
46
+ version: entry.version,
47
+ catalogVersion: entry.version,
48
+ description: entry.description,
49
+ category: entry.category,
50
+ tags: entry.tags,
51
+ author: entry.author?.name,
52
+ homepage: entry.homepage,
53
+ license: entry.license,
54
+ installed: false,
55
+ enabled: false,
56
+ hasUpdate: false,
57
+ };
58
+ }
59
+
60
+ export async function loadAllPlugins(mgr: MarketplaceManager, npmMgr: PluginManager): Promise<DashboardPlugin[]> {
61
+ const [npmPlugins, installedSummaries, updates] = await Promise.all([
62
+ npmMgr.list().catch(() => []),
63
+ mgr.listInstalledPlugins().catch(() => []),
64
+ mgr.checkForUpdates().catch(() => []),
65
+ ]);
66
+
67
+ const updateMap = new Map<string, string>();
68
+ for (const u of updates) {
69
+ updateMap.set(`${u.pluginId}:${u.scope}`, u.to);
70
+ }
71
+
72
+ const plugins: DashboardPlugin[] = [];
73
+
74
+ for (const p of npmPlugins) {
75
+ plugins.push(npmToDashboard(p));
76
+ }
77
+
78
+ const installedIds = new Set<string>();
79
+ for (const s of installedSummaries) {
80
+ installedIds.add(s.id);
81
+ plugins.push(installedToDashboard(s, updateMap));
82
+ }
83
+
84
+ const marketplaces = await mgr.listMarketplaces().catch(() => []);
85
+ for (const mkt of marketplaces) {
86
+ const available = await mgr.listAvailablePlugins(mkt.name).catch(() => []);
87
+ for (const entry of available) {
88
+ const pluginId = `${entry.name}@${mkt.name}`;
89
+ if (installedIds.has(pluginId)) {
90
+ const existing = plugins.find(p => p.id === pluginId);
91
+ if (existing) {
92
+ existing.description = existing.description || entry.description;
93
+ existing.category = existing.category || entry.category;
94
+ existing.tags = existing.tags || entry.tags;
95
+ existing.author = existing.author || entry.author?.name;
96
+ existing.homepage = existing.homepage || entry.homepage;
97
+ existing.license = existing.license || entry.license;
98
+ existing.catalogVersion = entry.version;
99
+ }
100
+ continue;
101
+ }
102
+ plugins.push(catalogToDashboard(entry, mkt.name));
103
+ }
104
+ }
105
+
106
+ plugins.sort((a, b) => {
107
+ if (a.installed !== b.installed) return a.installed ? -1 : 1;
108
+ if (a.installed && b.installed) {
109
+ if (a.enabled !== b.enabled) return a.enabled ? -1 : 1;
110
+ }
111
+ return a.name.localeCompare(b.name);
112
+ });
113
+
114
+ return plugins;
115
+ }
116
+
117
+ export function buildTabs(plugins: DashboardPlugin[]): PluginTab[] {
118
+ const tabs: PluginTab[] = [];
119
+ const installedCount = plugins.filter(p => p.installed).length;
120
+ const availableCount = plugins.filter(p => !p.installed).length;
121
+ const updatesCount = plugins.filter(p => p.hasUpdate).length;
122
+
123
+ tabs.push({ id: "installed", label: "Installed", count: installedCount });
124
+ if (availableCount > 0) {
125
+ tabs.push({ id: "available", label: "Available", count: availableCount });
126
+ }
127
+ if (updatesCount > 0) {
128
+ tabs.push({ id: "updates", label: "Updates", count: updatesCount });
129
+ }
130
+ return tabs;
131
+ }
132
+
133
+ export function filterByTab(plugins: DashboardPlugin[], tabId: PluginTabId): DashboardPlugin[] {
134
+ switch (tabId) {
135
+ case "installed":
136
+ return plugins.filter(p => p.installed);
137
+ case "available":
138
+ return plugins.filter(p => !p.installed);
139
+ case "updates":
140
+ return plugins.filter(p => p.hasUpdate);
141
+ default:
142
+ return plugins;
143
+ }
144
+ }
145
+
146
+ export function applySearch(plugins: DashboardPlugin[], query: string): DashboardPlugin[] {
147
+ if (!query) return plugins;
148
+ const q = query.toLowerCase();
149
+ return plugins.filter(p => {
150
+ if (p.name.toLowerCase().includes(q)) return true;
151
+ if (p.description?.toLowerCase().includes(q)) return true;
152
+ if (p.marketplace?.toLowerCase().includes(q)) return true;
153
+ if (p.category?.toLowerCase().includes(q)) return true;
154
+ if (p.tags?.some(t => t.toLowerCase().includes(q))) return true;
155
+ if (p.author?.toLowerCase().includes(q)) return true;
156
+ return false;
157
+ });
158
+ }
159
+
160
+ export async function createInitialState(
161
+ mgr: MarketplaceManager,
162
+ npmMgr: PluginManager,
163
+ ): Promise<PluginDashboardState> {
164
+ const allPlugins = await loadAllPlugins(mgr, npmMgr);
165
+ const tabs = buildTabs(allPlugins);
166
+ const activeTab = tabs[0] ?? { id: "installed" as const, label: "Installed", count: 0 };
167
+ const tabFiltered = filterByTab(allPlugins, activeTab.id);
168
+
169
+ return {
170
+ tabs,
171
+ activeTabIndex: 0,
172
+ allPlugins,
173
+ tabFiltered,
174
+ searchFiltered: tabFiltered,
175
+ searchQuery: "",
176
+ selectedIndex: 0,
177
+ scrollOffset: 0,
178
+ notice: null,
179
+ loading: false,
180
+ loadError: null,
181
+ };
182
+ }
183
+
184
+ export async function refreshState(
185
+ state: PluginDashboardState,
186
+ mgr: MarketplaceManager,
187
+ npmMgr: PluginManager,
188
+ ): Promise<PluginDashboardState> {
189
+ const allPlugins = await loadAllPlugins(mgr, npmMgr);
190
+ const tabs = buildTabs(allPlugins);
191
+ const prevTabId = state.tabs[state.activeTabIndex]?.id ?? "installed";
192
+ const nextTabIndex = Math.max(
193
+ 0,
194
+ tabs.findIndex(t => t.id === prevTabId),
195
+ );
196
+ const activeTab = tabs[nextTabIndex] ?? tabs[0];
197
+ const tabFiltered = filterByTab(allPlugins, activeTab?.id ?? "installed");
198
+ const searchFiltered = applySearch(tabFiltered, state.searchQuery);
199
+
200
+ return {
201
+ ...state,
202
+ tabs,
203
+ activeTabIndex: nextTabIndex,
204
+ allPlugins,
205
+ tabFiltered,
206
+ searchFiltered,
207
+ notice: state.notice,
208
+ loading: false,
209
+ loadError: null,
210
+ };
211
+ }
@@ -0,0 +1,42 @@
1
+ export interface DashboardPlugin {
2
+ id: string;
3
+ name: string;
4
+ marketplace?: string;
5
+ source: "npm" | "marketplace";
6
+ scope?: "user" | "project";
7
+ version?: string;
8
+ catalogVersion?: string;
9
+ description?: string;
10
+ category?: string;
11
+ tags?: string[];
12
+ author?: string;
13
+ homepage?: string;
14
+ license?: string;
15
+ installed: boolean;
16
+ enabled: boolean;
17
+ shadowedBy?: "project";
18
+ hasUpdate: boolean;
19
+ updateVersion?: string;
20
+ }
21
+
22
+ export type PluginTabId = "installed" | "available" | "updates";
23
+
24
+ export interface PluginTab {
25
+ id: PluginTabId;
26
+ label: string;
27
+ count: number;
28
+ }
29
+
30
+ export interface PluginDashboardState {
31
+ tabs: PluginTab[];
32
+ activeTabIndex: number;
33
+ allPlugins: DashboardPlugin[];
34
+ tabFiltered: DashboardPlugin[];
35
+ searchFiltered: DashboardPlugin[];
36
+ searchQuery: string;
37
+ selectedIndex: number;
38
+ scrollOffset: number;
39
+ notice: string | null;
40
+ loading: boolean;
41
+ loadError: string | null;
42
+ }
@@ -49,6 +49,7 @@ import { HistorySearchComponent } from "../components/history-search";
49
49
  import { ModelSelectorComponent } from "../components/model-selector";
50
50
  import { OAuthSelectorComponent } from "../components/oauth-selector";
51
51
  import { PluginSelectorComponent } from "../components/plugin-selector";
52
+ import { PluginDashboard } from "../components/plugins";
52
53
  import { SessionObserverOverlayComponent } from "../components/session-observer-overlay";
53
54
  import { SessionSelectorComponent } from "../components/session-selector";
54
55
  import { SettingsSelectorComponent } from "../components/settings-selector";
@@ -220,6 +221,20 @@ export class SelectorController {
220
221
  });
221
222
  }
222
223
 
224
+ async showPluginDashboard(): Promise<void> {
225
+ const dashboard = await PluginDashboard.create(getProjectDir(), this.ctx.ui.terminal.rows);
226
+ this.showSelector(done => {
227
+ dashboard.onClose = () => {
228
+ done();
229
+ this.ctx.ui.requestRender();
230
+ };
231
+ dashboard.onRequestRender = () => {
232
+ this.ctx.ui.requestRender();
233
+ };
234
+ return { component: dashboard, focus: dashboard };
235
+ });
236
+ }
237
+
223
238
  /**
224
239
  * Handle setting changes from the settings selector.
225
240
  * Most settings are saved directly via SettingsManager in the definitions.
@@ -1515,6 +1515,10 @@ export class InteractiveMode implements InteractiveModeContext {
1515
1515
  void this.#selectorController.showPluginSelector(mode);
1516
1516
  }
1517
1517
 
1518
+ showPluginDashboard(): void {
1519
+ void this.#selectorController.showPluginDashboard();
1520
+ }
1521
+
1518
1522
  showUserMessageSelector(): void {
1519
1523
  this.#selectorController.showUserMessageSelector();
1520
1524
  }
@@ -202,6 +202,7 @@ export interface InteractiveModeContext {
202
202
  showAgentsDashboard(): void;
203
203
  showModelSelector(options?: { temporaryOnly?: boolean }): void;
204
204
  showPluginSelector(mode?: "install" | "uninstall"): void;
205
+ showPluginDashboard(): void;
205
206
  showUserMessageSelector(): void;
206
207
  showTreeSelector(): void;
207
208
  showSessionSelector(): void;
@@ -1082,9 +1082,15 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
1082
1082
  handle: async (command, runtime) => {
1083
1083
  runtime.ctx.editor.setText("");
1084
1084
  const args = command.args.trim().split(/\s+/);
1085
- const sub = args[0] || "list";
1085
+ const sub = args[0] || "";
1086
1086
  const rest = args.slice(1).join(" ").trim();
1087
1087
 
1088
+ // No args or bare "list" with no further args → open interactive dashboard
1089
+ if (!sub || (sub === "list" && !rest)) {
1090
+ runtime.ctx.showPluginDashboard();
1091
+ return;
1092
+ }
1093
+
1088
1094
  try {
1089
1095
  const mgr = new MarketplaceManager({
1090
1096
  marketplacesRegistryPath: getMarketplacesRegistryPath(),
@@ -1118,7 +1124,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
1118
1124
  runtime.ctx.showStatus(`${isEnable ? "Enabled" : "Disabled"} ${parsed.pluginId}`);
1119
1125
  break;
1120
1126
  }
1121
- default: {
1127
+ case "list": {
1122
1128
  const lines: string[] = [];
1123
1129
 
1124
1130
  const npm = new PluginManager();
@@ -1150,6 +1156,16 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
1150
1156
  }
1151
1157
  break;
1152
1158
  }
1159
+ default: {
1160
+ runtime.ctx.showStatus(
1161
+ "Usage: /plugins [list|enable|disable]\n\n" +
1162
+ " /plugins Open plugin dashboard\n" +
1163
+ " /plugins list <query> List plugins matching query\n" +
1164
+ " /plugins enable <id> Enable a plugin\n" +
1165
+ " /plugins disable <id> Disable a plugin",
1166
+ );
1167
+ break;
1168
+ }
1153
1169
  }
1154
1170
  } catch (err) {
1155
1171
  runtime.ctx.showStatus(`Plugin error: ${err}`);