@f5xc-salesdemos/xcsh 19.7.0 → 19.9.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/examples/extensions/todo.ts +1 -1
- package/package.json +7 -7
- package/src/extensibility/plugins/marketplace/prerequisites.ts +50 -0
- package/src/extensibility/plugins/marketplace/types.ts +2 -0
- package/src/internal-urls/build-info.generated.ts +8 -8
- package/src/lsp/render.ts +1 -1
- package/src/mcp/oauth-flow.ts +2 -2
- package/src/modes/components/agent-dashboard.ts +1 -1
- package/src/modes/components/diff.ts +2 -2
- package/src/modes/components/plugins/plugin-dashboard.ts +37 -1
- package/src/modes/components/plugins/plugin-list-pane.ts +7 -3
- package/src/modes/components/plugins/state-manager.ts +10 -0
- package/src/modes/components/plugins/types.ts +3 -1
- package/src/prompts/tools/xcsh-api.md +31 -0
- package/src/session/agent-session.ts +2 -2
- package/src/slash-commands/builtin-registry.ts +76 -1
- package/src/vim/engine.ts +3 -3
|
@@ -118,7 +118,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
118
118
|
for (const entry of ctx.sessionManager.getBranch()) {
|
|
119
119
|
if (entry.type !== "message") continue;
|
|
120
120
|
const msg = (entry as { message?: { role?: string; toolName?: string; details?: unknown } }).message;
|
|
121
|
-
if (
|
|
121
|
+
if (msg?.role !== "toolResult" || msg.toolName !== "todo") continue;
|
|
122
122
|
|
|
123
123
|
const details = msg.details as TodoDetails | undefined;
|
|
124
124
|
if (details) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5xc-salesdemos/xcsh",
|
|
4
|
-
"version": "19.
|
|
4
|
+
"version": "19.9.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,12 +50,12 @@
|
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"@agentclientprotocol/sdk": "0.16.1",
|
|
52
52
|
"@mozilla/readability": "^0.6",
|
|
53
|
-
"@f5xc-salesdemos/xcsh-stats": "19.
|
|
54
|
-
"@f5xc-salesdemos/pi-agent-core": "19.
|
|
55
|
-
"@f5xc-salesdemos/pi-ai": "19.
|
|
56
|
-
"@f5xc-salesdemos/pi-natives": "19.
|
|
57
|
-
"@f5xc-salesdemos/pi-tui": "19.
|
|
58
|
-
"@f5xc-salesdemos/pi-utils": "19.
|
|
53
|
+
"@f5xc-salesdemos/xcsh-stats": "19.9.0",
|
|
54
|
+
"@f5xc-salesdemos/pi-agent-core": "19.9.0",
|
|
55
|
+
"@f5xc-salesdemos/pi-ai": "19.9.0",
|
|
56
|
+
"@f5xc-salesdemos/pi-natives": "19.9.0",
|
|
57
|
+
"@f5xc-salesdemos/pi-tui": "19.9.0",
|
|
58
|
+
"@f5xc-salesdemos/pi-utils": "19.9.0",
|
|
59
59
|
"@sinclair/typebox": "^0.34",
|
|
60
60
|
"@xterm/headless": "^6.0",
|
|
61
61
|
"ajv": "^8.20",
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { MarketplacePluginEntry } from "./types";
|
|
2
|
+
|
|
3
|
+
const cache = new Map<string, boolean>();
|
|
4
|
+
|
|
5
|
+
export async function checkPrerequisite(detectCmd: string): Promise<boolean> {
|
|
6
|
+
const cached = cache.get(detectCmd);
|
|
7
|
+
if (cached !== undefined) return cached;
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
const [cmd, ...args] = detectCmd.split(/\s+/);
|
|
11
|
+
const proc = Bun.spawn([cmd!, ...args], {
|
|
12
|
+
stdout: "ignore",
|
|
13
|
+
stderr: "ignore",
|
|
14
|
+
});
|
|
15
|
+
const exitCode = await proc.exited;
|
|
16
|
+
const available = exitCode === 0;
|
|
17
|
+
cache.set(detectCmd, available);
|
|
18
|
+
return available;
|
|
19
|
+
} catch {
|
|
20
|
+
cache.set(detectCmd, false);
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function checkAllPrerequisites(
|
|
26
|
+
plugins: MarketplacePluginEntry[],
|
|
27
|
+
): Promise<Map<string, { available: boolean; missing: string[] }>> {
|
|
28
|
+
const results = new Map<string, { available: boolean; missing: string[] }>();
|
|
29
|
+
|
|
30
|
+
for (const plugin of plugins) {
|
|
31
|
+
if (!plugin.prerequisites || plugin.prerequisites.length === 0) {
|
|
32
|
+
results.set(plugin.name, { available: true, missing: [] });
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const missing: string[] = [];
|
|
37
|
+
for (const prereq of plugin.prerequisites) {
|
|
38
|
+
const ok = await checkPrerequisite(prereq.detectCmd);
|
|
39
|
+
if (!ok) missing.push(prereq.tool);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
results.set(plugin.name, { available: missing.length === 0, missing });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return results;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function clearPrerequisiteCache(): void {
|
|
49
|
+
cache.clear();
|
|
50
|
+
}
|
|
@@ -89,6 +89,8 @@ export interface MarketplacePluginEntry {
|
|
|
89
89
|
tags?: string[];
|
|
90
90
|
strict?: boolean;
|
|
91
91
|
defaultEnabled?: boolean;
|
|
92
|
+
recommended?: boolean;
|
|
93
|
+
prerequisites?: Array<{ tool: string; installCmd: string; detectCmd: string }>;
|
|
92
94
|
commands?: string | string[];
|
|
93
95
|
agents?: string | string[];
|
|
94
96
|
hooks?: string | Record<string, unknown>;
|
|
@@ -17,17 +17,17 @@ export interface BuildInfo {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export const BUILD_INFO: BuildInfo = {
|
|
20
|
-
"version": "19.
|
|
21
|
-
"commit": "
|
|
22
|
-
"shortCommit": "
|
|
20
|
+
"version": "19.9.0",
|
|
21
|
+
"commit": "146551aadd2d3231eb436e0f5f760c96dc3ee602",
|
|
22
|
+
"shortCommit": "146551a",
|
|
23
23
|
"branch": "main",
|
|
24
|
-
"tag": "v19.
|
|
25
|
-
"commitDate": "2026-06-
|
|
26
|
-
"buildDate": "2026-06-
|
|
24
|
+
"tag": "v19.9.0",
|
|
25
|
+
"commitDate": "2026-06-05T02:16:52Z",
|
|
26
|
+
"buildDate": "2026-06-05T02:38:22.277Z",
|
|
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/
|
|
32
|
-
"releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v19.
|
|
31
|
+
"commitUrl": "https://github.com/f5xc-salesdemos/xcsh/commit/146551aadd2d3231eb436e0f5f760c96dc3ee602",
|
|
32
|
+
"releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v19.9.0"
|
|
33
33
|
};
|
package/src/lsp/render.ts
CHANGED
|
@@ -110,7 +110,7 @@ export function renderResult(
|
|
|
110
110
|
args?: LspParams,
|
|
111
111
|
): Component {
|
|
112
112
|
const content = result.content?.[0];
|
|
113
|
-
if (
|
|
113
|
+
if (content?.type !== "text" || !("text" in content) || !content.text) {
|
|
114
114
|
const icon = formatStatusIcon("warning", theme, options.spinnerFrame);
|
|
115
115
|
const header = `${icon} LSP`;
|
|
116
116
|
return new Text([header, theme.fg("dim", "No result")].join("\n"), 0, 0);
|
package/src/mcp/oauth-flow.ts
CHANGED
|
@@ -42,7 +42,7 @@ function getUriPort(uri: URL): number {
|
|
|
42
42
|
|
|
43
43
|
function validateRedirectConfig(config: MCPOAuthConfig, redirectUri: string | undefined): void {
|
|
44
44
|
const parsed = parseRedirectUri(redirectUri);
|
|
45
|
-
if (
|
|
45
|
+
if (parsed?.protocol !== "https:" || !isLoopbackHostname(parsed.hostname)) {
|
|
46
46
|
return;
|
|
47
47
|
}
|
|
48
48
|
|
|
@@ -63,7 +63,7 @@ function resolveCallbackPort(callbackPort: number | undefined, redirectUri: stri
|
|
|
63
63
|
if (callbackPort !== undefined) return callbackPort;
|
|
64
64
|
|
|
65
65
|
const parsed = parseRedirectUri(redirectUri);
|
|
66
|
-
if (
|
|
66
|
+
if (parsed?.protocol !== "http:" || !isLoopbackHostname(parsed.hostname)) {
|
|
67
67
|
return DEFAULT_PORT;
|
|
68
68
|
}
|
|
69
69
|
|
|
@@ -121,7 +121,7 @@ function matchAgent(agent: DashboardAgent, query: string): boolean {
|
|
|
121
121
|
function extractAssistantText(messages: AgentMessage[]): string | null {
|
|
122
122
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
123
123
|
const message = messages[i];
|
|
124
|
-
if (
|
|
124
|
+
if (message?.role !== "assistant") continue;
|
|
125
125
|
const blocks = message.content;
|
|
126
126
|
if (!Array.isArray(blocks)) continue;
|
|
127
127
|
const text = blocks
|
|
@@ -132,7 +132,7 @@ export function renderDiff(diffText: string, options: RenderDiffOptions = {}): s
|
|
|
132
132
|
const removedLines: { lineNum: string; content: string }[] = [];
|
|
133
133
|
while (i < lines.length) {
|
|
134
134
|
const p = parseDiffLine(lines[i]);
|
|
135
|
-
if (
|
|
135
|
+
if (p?.prefix !== "-") break;
|
|
136
136
|
removedLines.push({ lineNum: p.lineNum, content: p.content });
|
|
137
137
|
i++;
|
|
138
138
|
}
|
|
@@ -141,7 +141,7 @@ export function renderDiff(diffText: string, options: RenderDiffOptions = {}): s
|
|
|
141
141
|
const addedLines: { lineNum: string; content: string }[] = [];
|
|
142
142
|
while (i < lines.length) {
|
|
143
143
|
const p = parseDiffLine(lines[i]);
|
|
144
|
-
if (
|
|
144
|
+
if (p?.prefix !== "+") break;
|
|
145
145
|
addedLines.push({ lineNum: p.lineNum, content: p.content });
|
|
146
146
|
i++;
|
|
147
147
|
}
|
|
@@ -251,7 +251,7 @@ export class PluginDashboard extends Container {
|
|
|
251
251
|
if (!plugin) return;
|
|
252
252
|
const tabId = this.#activeTabId();
|
|
253
253
|
|
|
254
|
-
if (tabId === "discover" && !plugin.installed) {
|
|
254
|
+
if ((tabId === "discover" || tabId === "recommended") && !plugin.installed) {
|
|
255
255
|
await this.#installPlugin(plugin);
|
|
256
256
|
} else if (tabId === "updates" && plugin.hasUpdate) {
|
|
257
257
|
await this.#upgradePlugin(plugin);
|
|
@@ -289,6 +289,35 @@ export class PluginDashboard extends Container {
|
|
|
289
289
|
}
|
|
290
290
|
}
|
|
291
291
|
|
|
292
|
+
async #installAllRecommended(): Promise<void> {
|
|
293
|
+
const recommended = this.#state.allPlugins.filter(p => !p.installed && p.recommended && p.marketplace);
|
|
294
|
+
if (recommended.length === 0) {
|
|
295
|
+
this.#state.notice = "All recommended plugins are already installed";
|
|
296
|
+
this.#rebuildAndRender();
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
this.#state.notice = `Installing ${recommended.length} recommended plugin(s)...`;
|
|
301
|
+
this.#rebuildAndRender();
|
|
302
|
+
|
|
303
|
+
let installed = 0;
|
|
304
|
+
let failed = 0;
|
|
305
|
+
for (const plugin of recommended) {
|
|
306
|
+
try {
|
|
307
|
+
await this.#mgr.installPlugin(plugin.name, plugin.marketplace!);
|
|
308
|
+
installed++;
|
|
309
|
+
} catch {
|
|
310
|
+
failed++;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
this.#state.notice =
|
|
315
|
+
failed > 0
|
|
316
|
+
? `Installed ${installed}/${recommended.length} recommended plugin(s), ${failed} failed`
|
|
317
|
+
: `Installed ${installed} recommended plugin(s)`;
|
|
318
|
+
await this.#reloadData();
|
|
319
|
+
}
|
|
320
|
+
|
|
292
321
|
async #upgradePlugin(plugin: DashboardPlugin): Promise<void> {
|
|
293
322
|
this.#state.notice = `Upgrading ${plugin.name}...`;
|
|
294
323
|
this.#rebuildAndRender();
|
|
@@ -320,6 +349,8 @@ export class PluginDashboard extends Container {
|
|
|
320
349
|
#getHelpText(): string {
|
|
321
350
|
const tabId = this.#activeTabId();
|
|
322
351
|
switch (tabId) {
|
|
352
|
+
case "recommended":
|
|
353
|
+
return " ↑/↓: navigate Enter: install A: install all Tab: next tab Ctrl+R: reload Esc: close";
|
|
323
354
|
case "discover":
|
|
324
355
|
return " ↑/↓: navigate Enter: install Tab: next tab Ctrl+R: reload Esc: close";
|
|
325
356
|
case "updates":
|
|
@@ -421,6 +452,11 @@ export class PluginDashboard extends Container {
|
|
|
421
452
|
return;
|
|
422
453
|
}
|
|
423
454
|
|
|
455
|
+
if (data.toLowerCase() === "a" && this.#activeTabId() === "recommended") {
|
|
456
|
+
void this.#installAllRecommended();
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
|
|
424
460
|
if (data.toLowerCase() === "u") {
|
|
425
461
|
const plugin = this.#selectedPlugin();
|
|
426
462
|
if (plugin?.hasUpdate) {
|
|
@@ -24,9 +24,11 @@ export class PluginListPane implements Component {
|
|
|
24
24
|
const msg =
|
|
25
25
|
this.activeTab === "discover"
|
|
26
26
|
? "No plugins available. Add a marketplace first."
|
|
27
|
-
: this.activeTab === "
|
|
28
|
-
? "All plugins are
|
|
29
|
-
:
|
|
27
|
+
: this.activeTab === "recommended"
|
|
28
|
+
? "All recommended plugins are installed."
|
|
29
|
+
: this.activeTab === "updates"
|
|
30
|
+
? "All plugins are up to date."
|
|
31
|
+
: "No plugins installed.";
|
|
30
32
|
lines.push(theme.fg("muted", ` ${msg}`));
|
|
31
33
|
return lines;
|
|
32
34
|
}
|
|
@@ -66,6 +68,8 @@ export class PluginListPane implements Component {
|
|
|
66
68
|
} else {
|
|
67
69
|
parts.push(theme.fg("dim", theme.status.disabled));
|
|
68
70
|
}
|
|
71
|
+
} else if (plugin.recommended) {
|
|
72
|
+
parts.push(theme.fg("warning", "*"));
|
|
69
73
|
} else {
|
|
70
74
|
parts.push(theme.fg("dim", "·"));
|
|
71
75
|
}
|
|
@@ -62,6 +62,8 @@ function catalogToDashboard(entry: MarketplacePluginEntry, marketplace: string):
|
|
|
62
62
|
installed: false,
|
|
63
63
|
enabled: false,
|
|
64
64
|
hasUpdate: false,
|
|
65
|
+
recommended: entry.recommended,
|
|
66
|
+
prerequisites: entry.prerequisites,
|
|
65
67
|
};
|
|
66
68
|
}
|
|
67
69
|
|
|
@@ -98,6 +100,8 @@ export async function loadAllPlugins(mgr: MarketplaceManager, npmMgr: PluginMana
|
|
|
98
100
|
const existing = plugins.find(p => p.id === pluginId);
|
|
99
101
|
if (existing) {
|
|
100
102
|
existing.displayName = existing.displayName || entry.displayName;
|
|
103
|
+
existing.recommended = existing.recommended || entry.recommended;
|
|
104
|
+
existing.prerequisites = existing.prerequisites || entry.prerequisites;
|
|
101
105
|
existing.description = existing.description || entry.description;
|
|
102
106
|
existing.category = existing.category || entry.category;
|
|
103
107
|
existing.tags = existing.tags || entry.tags;
|
|
@@ -126,10 +130,14 @@ export async function loadAllPlugins(mgr: MarketplaceManager, npmMgr: PluginMana
|
|
|
126
130
|
export function buildTabs(plugins: DashboardPlugin[]): PluginTab[] {
|
|
127
131
|
const tabs: PluginTab[] = [];
|
|
128
132
|
const installedCount = plugins.filter(p => p.installed).length;
|
|
133
|
+
const recommendedCount = plugins.filter(p => !p.installed && p.recommended).length;
|
|
129
134
|
const discoverCount = plugins.filter(p => !p.installed).length;
|
|
130
135
|
const updatesCount = plugins.filter(p => p.hasUpdate).length;
|
|
131
136
|
|
|
132
137
|
tabs.push({ id: "installed", label: "Installed", count: installedCount });
|
|
138
|
+
if (recommendedCount > 0) {
|
|
139
|
+
tabs.push({ id: "recommended", label: "Recommended", count: recommendedCount });
|
|
140
|
+
}
|
|
133
141
|
if (discoverCount > 0) {
|
|
134
142
|
tabs.push({ id: "discover", label: "Discover", count: discoverCount });
|
|
135
143
|
}
|
|
@@ -143,6 +151,8 @@ export function filterByTab(plugins: DashboardPlugin[], tabId: PluginTabId): Das
|
|
|
143
151
|
switch (tabId) {
|
|
144
152
|
case "installed":
|
|
145
153
|
return plugins.filter(p => p.installed);
|
|
154
|
+
case "recommended":
|
|
155
|
+
return plugins.filter(p => !p.installed && p.recommended);
|
|
146
156
|
case "discover":
|
|
147
157
|
return plugins.filter(p => !p.installed);
|
|
148
158
|
case "updates":
|
|
@@ -18,9 +18,11 @@ export interface DashboardPlugin {
|
|
|
18
18
|
shadowedBy?: "project";
|
|
19
19
|
hasUpdate: boolean;
|
|
20
20
|
updateVersion?: string;
|
|
21
|
+
recommended?: boolean;
|
|
22
|
+
prerequisites?: Array<{ tool: string; installCmd: string; detectCmd: string }>;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
export type PluginTabId = "installed" | "discover" | "updates";
|
|
25
|
+
export type PluginTabId = "installed" | "recommended" | "discover" | "updates";
|
|
24
26
|
|
|
25
27
|
export interface PluginTab {
|
|
26
28
|
id: PluginTabId;
|
|
@@ -22,3 +22,34 @@ API calls to the same F5 XC tenant reuse a single TLS connection — sequential
|
|
|
22
22
|
|
|
23
23
|
**Relationship queries**: When the batch response says "Inventory complete" and includes a `Resource relationships:` section, the specs and relationships are already fully fetched. Answer directly from that data. Do NOT make additional GET calls to read individual resources you already have from the batch.
|
|
24
24
|
**Tenant-wide queries**: When asked about resources across ALL namespaces (e.g. "show all LBs in the entire tenant"), use `paths: ["*"]` with `params: {namespace: "*"}` to batch every namespace in ONE call. Do NOT list namespaces first — the wildcard handles discovery automatically.
|
|
25
|
+
|
|
26
|
+
**HTTP load balancer with origin pool** — when asked to create an LB routing to a named pool, use this exact payload structure (POST to `http_loadbalancers`):
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"metadata": { "name": "<lb-name>", "namespace": "<ns>" },
|
|
31
|
+
"spec": {
|
|
32
|
+
"domains": ["<domain>"],
|
|
33
|
+
"advertise_on_public_default_vip": {},
|
|
34
|
+
"http": { "port": 80 },
|
|
35
|
+
"default_route_pools": [
|
|
36
|
+
{ "pool": { "namespace": "<ns>", "name": "<pool-name>" }, "weight": 1, "priority": 1 }
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
For HTTPS: replace `"http": {"port": 80}` with `"https_auto_cert": {"http_redirect": true, "default_header": {}, "tls_config": {"default_security": {}}, "no_mtls": {}}`. For no pool (advertise only): omit `default_route_pools`.
|
|
43
|
+
|
|
44
|
+
**Resource disambiguation**: Several F5 XC resource types have similar names but different API paths. When the user's intent maps to one of these, use the exact API path shown:
|
|
45
|
+
|
|
46
|
+
|User says|Catalog category|API path segment|NOT|
|
|
47
|
+
|---|---|---|---|
|
|
48
|
+
|"rate limiter policy"|`rate-limiter-policys`|`rate_limiter_policys`|`policers`, `rate_limiters`|
|
|
49
|
+
|"policer"|`policers`|`policers`|`rate_limiter_policys`|
|
|
50
|
+
|"rate limiter"|`rate-limiters`|`rate_limiters`|`rate_limiter_policys`, `policers`|
|
|
51
|
+
|
|
52
|
+
**payload schemas for rate-limiting resources:**
|
|
53
|
+
- `policers` / "policer": `{"metadata":{"name":"<n>","namespace":"<ns>"},"spec":{"burst_size":<int>,"committed_information_rate":<int>}}` — network-level byte/Mbps limiting
|
|
54
|
+
- `rate_limiters` / "rate limiter": `{"metadata":{"name":"<n>","namespace":"<ns>"},"spec":{"burst_size":<int>,"committed_information_rate":<int>}}` — HTTP request-level limiting (rps)
|
|
55
|
+
- `rate_limiter_policys` / "rate limiter policy": requires existing `rate_limiter` reference; use `{"metadata":{"name":"<n>","namespace":"<ns>"},"spec":{"any_server":{},"rules":[{"metadata":{"name":"r"},"spec":{"any_client":{},"any_ip":{},"rate_limiter":{"namespace":"<ns>","name":"<rl-name>"}}}]}}`
|
|
@@ -4517,7 +4517,7 @@ export class AgentSession {
|
|
|
4517
4517
|
|
|
4518
4518
|
#closeCodexProviderSessionsForHistoryRewrite(): void {
|
|
4519
4519
|
const currentModel = this.model;
|
|
4520
|
-
if (
|
|
4520
|
+
if (currentModel?.api !== "openai-codex-responses") return;
|
|
4521
4521
|
this.#closeProviderSessionsForModelSwitch(currentModel, currentModel);
|
|
4522
4522
|
}
|
|
4523
4523
|
|
|
@@ -6115,7 +6115,7 @@ export class AgentSession {
|
|
|
6115
6115
|
const previousSessionFile = this.sessionFile;
|
|
6116
6116
|
const selectedEntry = this.sessionManager.getEntry(entryId);
|
|
6117
6117
|
|
|
6118
|
-
if (
|
|
6118
|
+
if (selectedEntry?.type !== "message" || selectedEntry.message.role !== "user") {
|
|
6119
6119
|
throw new Error("Invalid entry ID for branching");
|
|
6120
6120
|
}
|
|
6121
6121
|
|
|
@@ -875,6 +875,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
875
875
|
{ name: "discover", description: "Browse available plugins", usage: "[marketplace]" },
|
|
876
876
|
{ name: "list", description: "List all installed plugins" },
|
|
877
877
|
{ name: "validate", description: "Validate marketplace or plugin manifest", usage: "[path]" },
|
|
878
|
+
{ name: "setup", description: "Guided setup for recommended plugins" },
|
|
878
879
|
{ name: "help", description: "Show usage guide" },
|
|
879
880
|
],
|
|
880
881
|
allowArgs: true,
|
|
@@ -969,7 +970,6 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
969
970
|
}
|
|
970
971
|
break;
|
|
971
972
|
}
|
|
972
|
-
case "list":
|
|
973
973
|
default: {
|
|
974
974
|
const marketplaces = await mgr.listMarketplaces();
|
|
975
975
|
if (marketplaces.length === 0) {
|
|
@@ -1162,6 +1162,80 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
1162
1162
|
}
|
|
1163
1163
|
break;
|
|
1164
1164
|
}
|
|
1165
|
+
// ── Setup (guided recommended plugin install) ──
|
|
1166
|
+
case "setup": {
|
|
1167
|
+
const { checkPrerequisite } = await import("../extensibility/plugins/marketplace/prerequisites");
|
|
1168
|
+
const allPlugins = await mgr.listAvailablePlugins();
|
|
1169
|
+
const recommended = allPlugins.filter(p => p.recommended);
|
|
1170
|
+
if (recommended.length === 0) {
|
|
1171
|
+
runtime.ctx.showStatus("No recommended plugins found in configured marketplaces");
|
|
1172
|
+
break;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
const installed = await mgr.listInstalledPlugins();
|
|
1176
|
+
const installedIds = new Set(installed.map(p => p.id));
|
|
1177
|
+
const toInstall = recommended.filter(
|
|
1178
|
+
p => !Array.from(installedIds).some(id => id.startsWith(`${p.name}@`)),
|
|
1179
|
+
);
|
|
1180
|
+
|
|
1181
|
+
if (toInstall.length === 0) {
|
|
1182
|
+
runtime.ctx.showStatus("All recommended plugins are already installed");
|
|
1183
|
+
break;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
const lines: string[] = ["Recommended plugins setup:\n"];
|
|
1187
|
+
let installedCount = 0;
|
|
1188
|
+
let skippedCount = 0;
|
|
1189
|
+
const skippedReasons: string[] = [];
|
|
1190
|
+
|
|
1191
|
+
for (const plugin of toInstall) {
|
|
1192
|
+
if (plugin.prerequisites && plugin.prerequisites.length > 0) {
|
|
1193
|
+
const missing: string[] = [];
|
|
1194
|
+
for (const prereq of plugin.prerequisites) {
|
|
1195
|
+
const ok = await checkPrerequisite(prereq.detectCmd);
|
|
1196
|
+
if (!ok) missing.push(`${prereq.tool} (${prereq.installCmd})`);
|
|
1197
|
+
}
|
|
1198
|
+
if (missing.length > 0) {
|
|
1199
|
+
lines.push(` ⊘ ${plugin.displayName || plugin.name} — missing: ${missing.join(", ")}`);
|
|
1200
|
+
skippedCount++;
|
|
1201
|
+
skippedReasons.push(...missing);
|
|
1202
|
+
continue;
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
const marketplaces = await mgr.listMarketplaces();
|
|
1207
|
+
let installed = false;
|
|
1208
|
+
for (const mkt of marketplaces) {
|
|
1209
|
+
const available = await mgr.listAvailablePlugins(mkt.name);
|
|
1210
|
+
if (available.some(a => a.name === plugin.name)) {
|
|
1211
|
+
try {
|
|
1212
|
+
await mgr.installPlugin(plugin.name, mkt.name);
|
|
1213
|
+
lines.push(` + ${plugin.displayName || plugin.name} — installed`);
|
|
1214
|
+
installedCount++;
|
|
1215
|
+
installed = true;
|
|
1216
|
+
} catch (err) {
|
|
1217
|
+
lines.push(
|
|
1218
|
+
` ! ${plugin.displayName || plugin.name} — ${err instanceof Error ? err.message : String(err)}`,
|
|
1219
|
+
);
|
|
1220
|
+
skippedCount++;
|
|
1221
|
+
}
|
|
1222
|
+
break;
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
if (!installed && skippedCount === 0) {
|
|
1226
|
+
lines.push(` ? ${plugin.displayName || plugin.name} — not found in any marketplace`);
|
|
1227
|
+
skippedCount++;
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
lines.push("");
|
|
1232
|
+
lines.push(`Installed ${installedCount}/${toInstall.length} recommended plugin(s)`);
|
|
1233
|
+
if (skippedCount > 0) {
|
|
1234
|
+
lines.push(`${skippedCount} skipped — install missing tools and run /plugin setup again`);
|
|
1235
|
+
}
|
|
1236
|
+
runtime.ctx.showStatus(lines.join("\n"));
|
|
1237
|
+
break;
|
|
1238
|
+
}
|
|
1165
1239
|
// ── Help ──
|
|
1166
1240
|
case "help": {
|
|
1167
1241
|
runtime.ctx.showStatus(
|
|
@@ -1180,6 +1254,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
1180
1254
|
" /plugin upgrade [name@marketplace] Upgrade plugin(s)",
|
|
1181
1255
|
" /plugin list List installed plugins",
|
|
1182
1256
|
" /plugin validate [path] Validate marketplace or plugin",
|
|
1257
|
+
" /plugin setup Guided setup for recommended plugins",
|
|
1183
1258
|
"",
|
|
1184
1259
|
"Quick start:",
|
|
1185
1260
|
" /plugin marketplace add f5xc-salesdemos/marketplace",
|
package/src/vim/engine.ts
CHANGED
|
@@ -867,7 +867,7 @@ export class VimEngine {
|
|
|
867
867
|
}
|
|
868
868
|
case "r": {
|
|
869
869
|
const replacement = tokens[nextIndex + 1];
|
|
870
|
-
if (
|
|
870
|
+
if (replacement?.value.length !== 1) {
|
|
871
871
|
throw new VimError("Visual replace requires a literal character", opToken);
|
|
872
872
|
}
|
|
873
873
|
const visual = expandVisualOffsets(
|
|
@@ -1134,7 +1134,7 @@ export class VimEngine {
|
|
|
1134
1134
|
return nextIndex + 1;
|
|
1135
1135
|
case "r": {
|
|
1136
1136
|
const replacement = tokens[nextIndex + 1];
|
|
1137
|
-
if (
|
|
1137
|
+
if (replacement?.value.length !== 1) {
|
|
1138
1138
|
throw new VimError("r requires a replacement character", token);
|
|
1139
1139
|
}
|
|
1140
1140
|
await this.#applyAtomicChange(["r", replacement.value], () => {
|
|
@@ -1763,7 +1763,7 @@ export class VimEngine {
|
|
|
1763
1763
|
case "t":
|
|
1764
1764
|
case "T": {
|
|
1765
1765
|
const searchToken = tokens[index + 1];
|
|
1766
|
-
if (
|
|
1766
|
+
if (searchToken?.value.length !== 1) {
|
|
1767
1767
|
throw new VimError(`${token.value} requires a literal character`, token);
|
|
1768
1768
|
}
|
|
1769
1769
|
this.lastCharFind = { char: searchToken.value, mode: token.value as "f" | "F" | "t" | "T" };
|