@f5-sales-demo/xcsh 19.56.2 → 19.57.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/package.json +8 -8
- package/src/extensibility/extensions/loader.ts +40 -24
- package/src/internal-urls/build-info.generated.ts +8 -8
- package/src/locales/ar.json +2 -0
- package/src/locales/de.json +2 -0
- package/src/locales/en.json +2 -0
- package/src/locales/es.json +2 -0
- package/src/locales/fr.json +2 -0
- package/src/locales/hi.json +2 -0
- package/src/locales/it.json +2 -0
- package/src/locales/ja.json +2 -0
- package/src/locales/ko.json +2 -0
- package/src/locales/pt-br.json +2 -0
- package/src/locales/th.json +2 -0
- package/src/locales/zh-cn.json +2 -0
- package/src/locales/zh-tw.json +2 -0
- package/src/main.ts +35 -48
- package/src/modes/components/welcome-checks.ts +11 -104
- package/src/modes/components/welcome.ts +39 -189
- package/src/modes/controllers/input-controller.ts +17 -1
- package/src/modes/controllers/login-model.ts +32 -0
- package/src/modes/controllers/selector-controller.ts +4 -0
- package/src/modes/interactive-mode.ts +18 -164
- package/src/modes/types.ts +2 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5-sales-demo/xcsh",
|
|
4
|
-
"version": "19.
|
|
4
|
+
"version": "19.57.1",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/f5-sales-demo/xcsh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -55,13 +55,13 @@
|
|
|
55
55
|
"dependencies": {
|
|
56
56
|
"@agentclientprotocol/sdk": "0.16.1",
|
|
57
57
|
"@mozilla/readability": "^0.6",
|
|
58
|
-
"@f5-sales-demo/xcsh-stats": "19.
|
|
59
|
-
"@f5-sales-demo/pi-agent-core": "19.
|
|
60
|
-
"@f5-sales-demo/pi-ai": "19.
|
|
61
|
-
"@f5-sales-demo/pi-natives": "19.
|
|
62
|
-
"@f5-sales-demo/pi-resource-management": "19.
|
|
63
|
-
"@f5-sales-demo/pi-tui": "19.
|
|
64
|
-
"@f5-sales-demo/pi-utils": "19.
|
|
58
|
+
"@f5-sales-demo/xcsh-stats": "19.57.1",
|
|
59
|
+
"@f5-sales-demo/pi-agent-core": "19.57.1",
|
|
60
|
+
"@f5-sales-demo/pi-ai": "19.57.1",
|
|
61
|
+
"@f5-sales-demo/pi-natives": "19.57.1",
|
|
62
|
+
"@f5-sales-demo/pi-resource-management": "19.57.1",
|
|
63
|
+
"@f5-sales-demo/pi-tui": "19.57.1",
|
|
64
|
+
"@f5-sales-demo/pi-utils": "19.57.1",
|
|
65
65
|
"@sinclair/typebox": "^0.34",
|
|
66
66
|
"@xterm/headless": "^6.0",
|
|
67
67
|
"ajv": "^8.20",
|
|
@@ -284,7 +284,7 @@ async function loadExtension(
|
|
|
284
284
|
const resolvedPath = resolvePath(extensionPath, cwd);
|
|
285
285
|
|
|
286
286
|
try {
|
|
287
|
-
const module = await import(resolvedPath);
|
|
287
|
+
const module = await logger.time("ext:loadModule", () => import(resolvedPath));
|
|
288
288
|
const factory = (module.default ?? module) as ExtensionFactory;
|
|
289
289
|
|
|
290
290
|
if (typeof factory !== "function") {
|
|
@@ -295,7 +295,13 @@ async function loadExtension(
|
|
|
295
295
|
}
|
|
296
296
|
|
|
297
297
|
const extension = createExtension(extensionPath, resolvedPath);
|
|
298
|
-
const api = new ConcreteExtensionAPI(
|
|
298
|
+
const api = new ConcreteExtensionAPI(
|
|
299
|
+
await logger.time("ext:barrelImport", () => import("@f5-sales-demo/xcsh")),
|
|
300
|
+
extension,
|
|
301
|
+
runtime,
|
|
302
|
+
cwd,
|
|
303
|
+
eventBus,
|
|
304
|
+
);
|
|
299
305
|
await factory(api);
|
|
300
306
|
|
|
301
307
|
return { extension, error: null };
|
|
@@ -316,7 +322,13 @@ export async function loadExtensionFromFactory(
|
|
|
316
322
|
name = "<inline>",
|
|
317
323
|
): Promise<Extension> {
|
|
318
324
|
const extension = createExtension(name, name);
|
|
319
|
-
const api = new ConcreteExtensionAPI(
|
|
325
|
+
const api = new ConcreteExtensionAPI(
|
|
326
|
+
await logger.time("ext:barrelImport", () => import("@f5-sales-demo/xcsh")),
|
|
327
|
+
extension,
|
|
328
|
+
runtime,
|
|
329
|
+
cwd,
|
|
330
|
+
eventBus,
|
|
331
|
+
);
|
|
320
332
|
await factory(api);
|
|
321
333
|
return extension;
|
|
322
334
|
}
|
|
@@ -540,7 +552,9 @@ export async function discoverAndLoadExtensions(
|
|
|
540
552
|
};
|
|
541
553
|
|
|
542
554
|
// 1. Discover extension modules via capability API (native .omp/.pi only)
|
|
543
|
-
const discovered = await
|
|
555
|
+
const discovered = await logger.time("ext:discoverCapability", () =>
|
|
556
|
+
loadCapability<ExtensionModule>(extensionModuleCapability.id, { cwd }),
|
|
557
|
+
);
|
|
544
558
|
for (const ext of discovered.items) {
|
|
545
559
|
if (ext._source.provider !== "native") continue;
|
|
546
560
|
if (isDisabledName(ext.name)) continue;
|
|
@@ -548,29 +562,31 @@ export async function discoverAndLoadExtensions(
|
|
|
548
562
|
}
|
|
549
563
|
|
|
550
564
|
// 2. Discover extension entry points from installed plugins (node_modules path)
|
|
551
|
-
addPaths(await getAllPluginExtensionPaths(cwd));
|
|
565
|
+
addPaths(await logger.time("ext:pluginPaths", () => getAllPluginExtensionPaths(cwd)));
|
|
552
566
|
|
|
553
567
|
// 2b. Discover extension entry points from marketplace-cached plugins
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
+
await logger.time("ext:marketplaceRoots", async () => {
|
|
569
|
+
for (const root of getPreloadedPluginRoots()) {
|
|
570
|
+
try {
|
|
571
|
+
const pkgPath = path.join(root.path, "package.json");
|
|
572
|
+
const pkg = await Bun.file(pkgPath).json();
|
|
573
|
+
const manifest = pkg?.xcsh ?? pkg?.pi;
|
|
574
|
+
const extensions = manifest?.extensions;
|
|
575
|
+
if (Array.isArray(extensions)) {
|
|
576
|
+
for (const entry of extensions) {
|
|
577
|
+
if (typeof entry !== "string") continue;
|
|
578
|
+
if (path.isAbsolute(entry) || entry.includes("..")) continue;
|
|
579
|
+
const resolved = path.resolve(root.path, entry);
|
|
580
|
+
if (!resolved.startsWith(root.path + path.sep) && resolved !== root.path) continue;
|
|
581
|
+
if (isDisabledName(getExtensionNameFromPath(resolved))) continue;
|
|
582
|
+
addPath(resolved);
|
|
583
|
+
}
|
|
568
584
|
}
|
|
585
|
+
} catch {
|
|
586
|
+
// No package.json or invalid — skip
|
|
569
587
|
}
|
|
570
|
-
} catch {
|
|
571
|
-
// No package.json or invalid — skip
|
|
572
588
|
}
|
|
573
|
-
}
|
|
589
|
+
});
|
|
574
590
|
|
|
575
591
|
// 3. Explicitly configured paths
|
|
576
592
|
for (const configuredPath of configuredPaths) {
|
|
@@ -601,7 +617,7 @@ export async function discoverAndLoadExtensions(
|
|
|
601
617
|
}
|
|
602
618
|
|
|
603
619
|
const resolvedEventBus = eventBus ?? new EventBus();
|
|
604
|
-
const result = await loadExtensions(allPaths, cwd, resolvedEventBus);
|
|
605
|
-
await loadBundledExtensions(result, cwd, resolvedEventBus, isDisabledName);
|
|
620
|
+
const result = await logger.time("ext:loadLoop", () => loadExtensions(allPaths, cwd, resolvedEventBus));
|
|
621
|
+
await logger.time("ext:loadBundled", () => loadBundledExtensions(result, cwd, resolvedEventBus, isDisabledName));
|
|
606
622
|
return result;
|
|
607
623
|
}
|
|
@@ -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.57.1",
|
|
21
|
+
"commit": "afdece90c0203627849699502d77b189fd3fb320",
|
|
22
|
+
"shortCommit": "afdece9",
|
|
23
23
|
"branch": "main",
|
|
24
|
-
"tag": "v19.
|
|
25
|
-
"commitDate": "2026-07-
|
|
26
|
-
"buildDate": "2026-07-
|
|
24
|
+
"tag": "v19.57.1",
|
|
25
|
+
"commitDate": "2026-07-04T20:58:43Z",
|
|
26
|
+
"buildDate": "2026-07-04T21:21:44.204Z",
|
|
27
27
|
"dirty": true,
|
|
28
28
|
"prNumber": "",
|
|
29
29
|
"repoUrl": "https://github.com/f5-sales-demo/xcsh",
|
|
30
30
|
"repoSlug": "f5-sales-demo/xcsh",
|
|
31
|
-
"commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/
|
|
32
|
-
"releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v19.
|
|
31
|
+
"commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/afdece90c0203627849699502d77b189fd3fb320",
|
|
32
|
+
"releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v19.57.1"
|
|
33
33
|
};
|
package/src/locales/ar.json
CHANGED
|
@@ -282,6 +282,8 @@
|
|
|
282
282
|
"login.wizard.configSaved": "Configuration saved. You're ready to go!",
|
|
283
283
|
"login.wizard.failed": "Connection failed — {error}",
|
|
284
284
|
"login.wizard.retryHint": "Press Escape to skip, or enter a new value:",
|
|
285
|
+
"gate.noProvider": "No LLM provider configured — run /login to connect one before sending a message.",
|
|
286
|
+
"gate.noProviderBlock": "No LLM provider configured. Run /login to connect one, then retry. (/help and /model still work.)",
|
|
285
287
|
"plugins.tabs.installed": "مثبتة",
|
|
286
288
|
"plugins.tabs.recommended": "موصى بها",
|
|
287
289
|
"plugins.tabs.discover": "استكشاف",
|
package/src/locales/de.json
CHANGED
|
@@ -282,6 +282,8 @@
|
|
|
282
282
|
"login.wizard.configSaved": "Configuration saved. You're ready to go!",
|
|
283
283
|
"login.wizard.failed": "Connection failed — {error}",
|
|
284
284
|
"login.wizard.retryHint": "Press Escape to skip, or enter a new value:",
|
|
285
|
+
"gate.noProvider": "No LLM provider configured — run /login to connect one before sending a message.",
|
|
286
|
+
"gate.noProviderBlock": "No LLM provider configured. Run /login to connect one, then retry. (/help and /model still work.)",
|
|
285
287
|
"plugins.tabs.installed": "Installiert",
|
|
286
288
|
"plugins.tabs.recommended": "Empfohlen",
|
|
287
289
|
"plugins.tabs.discover": "Entdecken",
|
package/src/locales/en.json
CHANGED
|
@@ -303,6 +303,8 @@
|
|
|
303
303
|
"login.wizard.configSaved": "Configuration saved. You're ready to go!",
|
|
304
304
|
"login.wizard.failed": "Connection failed — {error}",
|
|
305
305
|
"login.wizard.retryHint": "Press Escape to skip, or enter a new value:",
|
|
306
|
+
"gate.noProvider": "No LLM provider configured — run /login to connect one before sending a message.",
|
|
307
|
+
"gate.noProviderBlock": "No LLM provider configured. Run /login to connect one, then retry. (/help and /model still work.)",
|
|
306
308
|
|
|
307
309
|
"plugins.tabs.installed": "Installed",
|
|
308
310
|
"plugins.tabs.recommended": "Recommended",
|
package/src/locales/es.json
CHANGED
|
@@ -282,6 +282,8 @@
|
|
|
282
282
|
"login.wizard.configSaved": "Configuration saved. You're ready to go!",
|
|
283
283
|
"login.wizard.failed": "Connection failed — {error}",
|
|
284
284
|
"login.wizard.retryHint": "Press Escape to skip, or enter a new value:",
|
|
285
|
+
"gate.noProvider": "No LLM provider configured — run /login to connect one before sending a message.",
|
|
286
|
+
"gate.noProviderBlock": "No LLM provider configured. Run /login to connect one, then retry. (/help and /model still work.)",
|
|
285
287
|
"plugins.tabs.installed": "Instalados",
|
|
286
288
|
"plugins.tabs.recommended": "Recomendados",
|
|
287
289
|
"plugins.tabs.discover": "Descubrir",
|
package/src/locales/fr.json
CHANGED
|
@@ -282,6 +282,8 @@
|
|
|
282
282
|
"login.wizard.configSaved": "Configuration saved. You're ready to go!",
|
|
283
283
|
"login.wizard.failed": "Connection failed — {error}",
|
|
284
284
|
"login.wizard.retryHint": "Press Escape to skip, or enter a new value:",
|
|
285
|
+
"gate.noProvider": "No LLM provider configured — run /login to connect one before sending a message.",
|
|
286
|
+
"gate.noProviderBlock": "No LLM provider configured. Run /login to connect one, then retry. (/help and /model still work.)",
|
|
285
287
|
"plugins.tabs.installed": "Installés",
|
|
286
288
|
"plugins.tabs.recommended": "Recommandés",
|
|
287
289
|
"plugins.tabs.discover": "Découvrir",
|
package/src/locales/hi.json
CHANGED
|
@@ -282,6 +282,8 @@
|
|
|
282
282
|
"login.wizard.configSaved": "Configuration saved. You're ready to go!",
|
|
283
283
|
"login.wizard.failed": "Connection failed — {error}",
|
|
284
284
|
"login.wizard.retryHint": "Press Escape to skip, or enter a new value:",
|
|
285
|
+
"gate.noProvider": "No LLM provider configured — run /login to connect one before sending a message.",
|
|
286
|
+
"gate.noProviderBlock": "No LLM provider configured. Run /login to connect one, then retry. (/help and /model still work.)",
|
|
285
287
|
"plugins.tabs.installed": "इंस्टॉल किए गए",
|
|
286
288
|
"plugins.tabs.recommended": "अनुशंसित",
|
|
287
289
|
"plugins.tabs.discover": "खोजें",
|
package/src/locales/it.json
CHANGED
|
@@ -282,6 +282,8 @@
|
|
|
282
282
|
"login.wizard.configSaved": "Configuration saved. You're ready to go!",
|
|
283
283
|
"login.wizard.failed": "Connection failed — {error}",
|
|
284
284
|
"login.wizard.retryHint": "Press Escape to skip, or enter a new value:",
|
|
285
|
+
"gate.noProvider": "No LLM provider configured — run /login to connect one before sending a message.",
|
|
286
|
+
"gate.noProviderBlock": "No LLM provider configured. Run /login to connect one, then retry. (/help and /model still work.)",
|
|
285
287
|
"plugins.tabs.installed": "Installati",
|
|
286
288
|
"plugins.tabs.recommended": "Consigliati",
|
|
287
289
|
"plugins.tabs.discover": "Scopri",
|
package/src/locales/ja.json
CHANGED
|
@@ -282,6 +282,8 @@
|
|
|
282
282
|
"login.wizard.configSaved": "Configuration saved. You're ready to go!",
|
|
283
283
|
"login.wizard.failed": "Connection failed — {error}",
|
|
284
284
|
"login.wizard.retryHint": "Press Escape to skip, or enter a new value:",
|
|
285
|
+
"gate.noProvider": "No LLM provider configured — run /login to connect one before sending a message.",
|
|
286
|
+
"gate.noProviderBlock": "No LLM provider configured. Run /login to connect one, then retry. (/help and /model still work.)",
|
|
285
287
|
"plugins.tabs.installed": "インストール済み",
|
|
286
288
|
"plugins.tabs.recommended": "推奨",
|
|
287
289
|
"plugins.tabs.discover": "検索",
|
package/src/locales/ko.json
CHANGED
|
@@ -282,6 +282,8 @@
|
|
|
282
282
|
"login.wizard.configSaved": "Configuration saved. You're ready to go!",
|
|
283
283
|
"login.wizard.failed": "Connection failed — {error}",
|
|
284
284
|
"login.wizard.retryHint": "Press Escape to skip, or enter a new value:",
|
|
285
|
+
"gate.noProvider": "No LLM provider configured — run /login to connect one before sending a message.",
|
|
286
|
+
"gate.noProviderBlock": "No LLM provider configured. Run /login to connect one, then retry. (/help and /model still work.)",
|
|
285
287
|
"plugins.tabs.installed": "설치됨",
|
|
286
288
|
"plugins.tabs.recommended": "권장",
|
|
287
289
|
"plugins.tabs.discover": "검색",
|
package/src/locales/pt-br.json
CHANGED
|
@@ -282,6 +282,8 @@
|
|
|
282
282
|
"login.wizard.configSaved": "Configuration saved. You're ready to go!",
|
|
283
283
|
"login.wizard.failed": "Connection failed — {error}",
|
|
284
284
|
"login.wizard.retryHint": "Press Escape to skip, or enter a new value:",
|
|
285
|
+
"gate.noProvider": "No LLM provider configured — run /login to connect one before sending a message.",
|
|
286
|
+
"gate.noProviderBlock": "No LLM provider configured. Run /login to connect one, then retry. (/help and /model still work.)",
|
|
285
287
|
"plugins.tabs.installed": "Instalados",
|
|
286
288
|
"plugins.tabs.recommended": "Recomendados",
|
|
287
289
|
"plugins.tabs.discover": "Descobrir",
|
package/src/locales/th.json
CHANGED
|
@@ -282,6 +282,8 @@
|
|
|
282
282
|
"login.wizard.configSaved": "Configuration saved. You're ready to go!",
|
|
283
283
|
"login.wizard.failed": "Connection failed — {error}",
|
|
284
284
|
"login.wizard.retryHint": "Press Escape to skip, or enter a new value:",
|
|
285
|
+
"gate.noProvider": "No LLM provider configured — run /login to connect one before sending a message.",
|
|
286
|
+
"gate.noProviderBlock": "No LLM provider configured. Run /login to connect one, then retry. (/help and /model still work.)",
|
|
285
287
|
"plugins.tabs.installed": "ติดตั้งแล้ว",
|
|
286
288
|
"plugins.tabs.recommended": "แนะนำ",
|
|
287
289
|
"plugins.tabs.discover": "ค้นหา",
|
package/src/locales/zh-cn.json
CHANGED
|
@@ -282,6 +282,8 @@
|
|
|
282
282
|
"login.wizard.configSaved": "Configuration saved. You're ready to go!",
|
|
283
283
|
"login.wizard.failed": "Connection failed — {error}",
|
|
284
284
|
"login.wizard.retryHint": "Press Escape to skip, or enter a new value:",
|
|
285
|
+
"gate.noProvider": "No LLM provider configured — run /login to connect one before sending a message.",
|
|
286
|
+
"gate.noProviderBlock": "No LLM provider configured. Run /login to connect one, then retry. (/help and /model still work.)",
|
|
285
287
|
"plugins.tabs.installed": "已安装",
|
|
286
288
|
"plugins.tabs.recommended": "推荐",
|
|
287
289
|
"plugins.tabs.discover": "发现",
|
package/src/locales/zh-tw.json
CHANGED
|
@@ -282,6 +282,8 @@
|
|
|
282
282
|
"login.wizard.configSaved": "Configuration saved. You're ready to go!",
|
|
283
283
|
"login.wizard.failed": "Connection failed — {error}",
|
|
284
284
|
"login.wizard.retryHint": "Press Escape to skip, or enter a new value:",
|
|
285
|
+
"gate.noProvider": "No LLM provider configured — run /login to connect one before sending a message.",
|
|
286
|
+
"gate.noProviderBlock": "No LLM provider configured. Run /login to connect one, then retry. (/help and /model still work.)",
|
|
285
287
|
"plugins.tabs.installed": "已安裝",
|
|
286
288
|
"plugins.tabs.recommended": "推薦",
|
|
287
289
|
"plugins.tabs.discover": "探索",
|
package/src/main.ts
CHANGED
|
@@ -165,33 +165,16 @@ async function runInteractiveMode(
|
|
|
165
165
|
versionCheckPromise.catch(() => undefined),
|
|
166
166
|
new Promise<string | undefined>(resolve => setTimeout(() => resolve(undefined), INITIAL_UPDATE_CHECK_TIMEOUT_MS)),
|
|
167
167
|
]);
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
: undefined;
|
|
171
|
-
|
|
172
|
-
const mode = new InteractiveMode(
|
|
173
|
-
session,
|
|
174
|
-
version,
|
|
175
|
-
initialUpdateStatus,
|
|
176
|
-
setExtensionUIContext,
|
|
177
|
-
lspServers,
|
|
178
|
-
mcpManager,
|
|
179
|
-
eventBus,
|
|
180
|
-
);
|
|
168
|
+
|
|
169
|
+
const mode = new InteractiveMode(session, version, setExtensionUIContext, lspServers, mcpManager, eventBus);
|
|
181
170
|
|
|
182
171
|
await mode.init();
|
|
183
172
|
|
|
184
|
-
if
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
}
|
|
190
|
-
if (newVersion) {
|
|
191
|
-
mode.setUpdateStatus({ available: true, latestVersion: newVersion });
|
|
192
|
-
}
|
|
193
|
-
})
|
|
194
|
-
.catch(() => {});
|
|
173
|
+
// Non-blocking, one-shot update notice: only surface it if the background check
|
|
174
|
+
// already resolved within the startup race (no waiting, no live re-render). If it
|
|
175
|
+
// resolves later, the update is available on demand via /plugins.
|
|
176
|
+
if (initialUpdateVersion && settings.get("startup.checkUpdate")) {
|
|
177
|
+
mode.showStatus(`Update available: v${initialUpdateVersion} — run: xcsh update`, { dim: true });
|
|
195
178
|
}
|
|
196
179
|
|
|
197
180
|
mode.renderInitialMessages();
|
|
@@ -717,34 +700,38 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
|
|
|
717
700
|
sessionManager = await SessionManager.open(selectedPath);
|
|
718
701
|
}
|
|
719
702
|
|
|
720
|
-
// Refresh stale marketplace clones
|
|
703
|
+
// Refresh stale marketplace clones in the background so startup never blocks on
|
|
704
|
+
// git/network. Upgraded plugin code applies on the next launch (same trade-off as
|
|
705
|
+
// the "notify" branch below). Offline/corrupt data is tolerated by the try/catch.
|
|
721
706
|
const autoUpdate = settings.get("marketplace.autoUpdate");
|
|
722
707
|
if (autoUpdate !== "off") {
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
708
|
+
void (async () => {
|
|
709
|
+
try {
|
|
710
|
+
const startupMgr = new MarketplaceManager({
|
|
711
|
+
marketplacesRegistryPath: getMarketplacesRegistryPath(),
|
|
712
|
+
installedRegistryPath: getInstalledPluginsRegistryPath(),
|
|
713
|
+
projectInstalledRegistryPath: (await resolveActiveProjectRegistryPath(getProjectDir())) ?? undefined,
|
|
714
|
+
marketplacesCacheDir: getMarketplacesCacheDir(),
|
|
715
|
+
pluginsCacheDir: getPluginsCacheDir(),
|
|
716
|
+
clearPluginRootsCache: (extraPaths?: readonly string[]) => {
|
|
717
|
+
const h = os.homedir();
|
|
718
|
+
invalidateFsCache(path.join(h, getConfigDirName(), "plugins", "installed_plugins.json"));
|
|
719
|
+
for (const p of extraPaths ?? []) invalidateFsCache(p);
|
|
720
|
+
clearXcshPluginRootsCache();
|
|
721
|
+
},
|
|
722
|
+
});
|
|
723
|
+
await startupMgr.refreshStaleMarketplaces();
|
|
724
|
+
if (autoUpdate === "auto") {
|
|
725
|
+
const updates = await startupMgr.checkForUpdates();
|
|
726
|
+
if (updates.length > 0) {
|
|
727
|
+
await startupMgr.upgradeAllPlugins();
|
|
728
|
+
logger.debug(`Auto-upgraded ${updates.length} marketplace plugin(s) at startup`);
|
|
729
|
+
}
|
|
743
730
|
}
|
|
731
|
+
} catch {
|
|
732
|
+
// Network failure, corrupt data, offline — proceed with cached plugins.
|
|
744
733
|
}
|
|
745
|
-
}
|
|
746
|
-
// Network failure, corrupt data, offline — proceed with cached plugins.
|
|
747
|
-
}
|
|
734
|
+
})();
|
|
748
735
|
}
|
|
749
736
|
|
|
750
737
|
// Wire --plugin-dir and preload plugin roots for sync consumers (LSP config)
|
|
@@ -1,17 +1,9 @@
|
|
|
1
1
|
import type { Model } from "@f5-sales-demo/pi-ai";
|
|
2
2
|
import { validateApiKeyAgainstModelsEndpoint } from "@f5-sales-demo/pi-ai/utils/oauth/api-key-validation";
|
|
3
3
|
import { logger } from "@f5-sales-demo/pi-utils";
|
|
4
|
-
import { MarketplaceManager } from "../../extensibility/plugins/marketplace";
|
|
5
|
-
import {
|
|
6
|
-
getInstalledPluginsRegistryPath,
|
|
7
|
-
getMarketplacesCacheDir,
|
|
8
|
-
getMarketplacesRegistryPath,
|
|
9
|
-
getPluginsCacheDir,
|
|
10
|
-
} from "../../extensibility/plugins/marketplace/registry";
|
|
11
4
|
import { type AuthStatus, ContextService } from "../../services/xcsh-context";
|
|
12
5
|
import { deriveTenantFromUrl } from "../../services/xcsh-env";
|
|
13
6
|
import type { AuthStorage } from "../../session/auth-storage";
|
|
14
|
-
import { normalizePluginDisplayName } from "./plugins/utils";
|
|
15
7
|
|
|
16
8
|
// Startup validation budget. These are longer than validateToken's 3000ms default because
|
|
17
9
|
// the welcome path runs during TLS/DNS cold-start — a single 3s shot races against warm-up
|
|
@@ -75,6 +67,17 @@ export interface WelcomeCheckResult {
|
|
|
75
67
|
/** Providers that don't store API keys (local inference servers) */
|
|
76
68
|
const KEYLESS_PROVIDERS = new Set(["ollama", "llama.cpp", "lm-studio", "llamafile", "local"]);
|
|
77
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Instant, local check for whether the session has a usable LLM provider configured.
|
|
72
|
+
* Zero network: a model must be resolved and either be keyless (local inference) or
|
|
73
|
+
* have credentials present in the auth store. Used by the startup readiness gate to
|
|
74
|
+
* decide whether natural-language input can be processed.
|
|
75
|
+
*/
|
|
76
|
+
export function hasActiveLlmProvider(model: Model | undefined, authStorage: Pick<AuthStorage, "hasAuth">): boolean {
|
|
77
|
+
if (!model) return false;
|
|
78
|
+
return KEYLESS_PROVIDERS.has(model.provider) || authStorage.hasAuth(model.provider);
|
|
79
|
+
}
|
|
80
|
+
|
|
78
81
|
/**
|
|
79
82
|
* Run blocking startup checks for the welcome screen.
|
|
80
83
|
* Model check always runs. Context check only runs if model is connected.
|
|
@@ -215,99 +218,3 @@ export function mapContextStatus(status: WelcomeContextStatus): ServiceStatus {
|
|
|
215
218
|
}
|
|
216
219
|
}
|
|
217
220
|
}
|
|
218
|
-
|
|
219
|
-
export interface FixableService {
|
|
220
|
-
name: string;
|
|
221
|
-
prompt: string;
|
|
222
|
-
command: string[];
|
|
223
|
-
recheck: () => Promise<ServiceStatus>;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
export type UnifiedPluginState = "connected" | "unauthenticated" | "unavailable" | "installed" | "not_installed";
|
|
227
|
-
|
|
228
|
-
export interface UnifiedPluginStatus {
|
|
229
|
-
name: string;
|
|
230
|
-
state: UnifiedPluginState;
|
|
231
|
-
hint?: string;
|
|
232
|
-
group?: string;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
export interface ServiceStatusContributionInput {
|
|
236
|
-
name: string;
|
|
237
|
-
group?: string;
|
|
238
|
-
check: () => Promise<{ state: ServiceState; hint?: string }>;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
export async function buildUnifiedPluginList(
|
|
242
|
-
contributions: ServiceStatusContributionInput[],
|
|
243
|
-
): Promise<UnifiedPluginStatus[]> {
|
|
244
|
-
const map = new Map<string, UnifiedPluginStatus>();
|
|
245
|
-
|
|
246
|
-
for (const contribution of contributions) {
|
|
247
|
-
try {
|
|
248
|
-
const status = await contribution.check();
|
|
249
|
-
map.set(contribution.name.toLowerCase(), {
|
|
250
|
-
name: contribution.name,
|
|
251
|
-
state: status.state,
|
|
252
|
-
hint: status.hint,
|
|
253
|
-
group: contribution.group,
|
|
254
|
-
});
|
|
255
|
-
} catch {
|
|
256
|
-
map.set(contribution.name.toLowerCase(), {
|
|
257
|
-
name: contribution.name,
|
|
258
|
-
state: "unavailable",
|
|
259
|
-
hint: "check failed",
|
|
260
|
-
group: contribution.group,
|
|
261
|
-
});
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
try {
|
|
266
|
-
const mgr = new MarketplaceManager({
|
|
267
|
-
marketplacesRegistryPath: getMarketplacesRegistryPath(),
|
|
268
|
-
installedRegistryPath: getInstalledPluginsRegistryPath(),
|
|
269
|
-
marketplacesCacheDir: getMarketplacesCacheDir(),
|
|
270
|
-
pluginsCacheDir: getPluginsCacheDir(),
|
|
271
|
-
clearPluginRootsCache: () => {},
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
const [marketplaces, installedSummaries] = await Promise.all([
|
|
275
|
-
mgr.listMarketplaces(),
|
|
276
|
-
mgr.listInstalledPlugins(),
|
|
277
|
-
]);
|
|
278
|
-
|
|
279
|
-
const installedIds = new Set(installedSummaries.map(s => s.id));
|
|
280
|
-
|
|
281
|
-
for (const mkt of marketplaces) {
|
|
282
|
-
const available = await mgr.listAvailablePlugins(mkt.name).catch(() => []);
|
|
283
|
-
for (const entry of available) {
|
|
284
|
-
if (!entry.recommended) continue;
|
|
285
|
-
const displayName = normalizePluginDisplayName(entry.name);
|
|
286
|
-
const key = displayName.toLowerCase();
|
|
287
|
-
if (map.has(key)) continue;
|
|
288
|
-
const pluginId = `${entry.name}@${mkt.name}`;
|
|
289
|
-
map.set(key, {
|
|
290
|
-
name: displayName,
|
|
291
|
-
state: installedIds.has(pluginId) ? "installed" : "not_installed",
|
|
292
|
-
hint: installedIds.has(pluginId) ? undefined : "run: /plugin setup",
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
} catch (err) {
|
|
297
|
-
logger.debug("buildUnifiedPluginList marketplace check failed", { error: String(err) });
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
const stateOrder: Record<UnifiedPluginState, number> = {
|
|
301
|
-
connected: 0,
|
|
302
|
-
unauthenticated: 1,
|
|
303
|
-
unavailable: 2,
|
|
304
|
-
installed: 3,
|
|
305
|
-
not_installed: 4,
|
|
306
|
-
};
|
|
307
|
-
|
|
308
|
-
return [...map.values()].sort((a, b) => {
|
|
309
|
-
const orderDiff = stateOrder[a.state] - stateOrder[b.state];
|
|
310
|
-
if (orderDiff !== 0) return orderDiff;
|
|
311
|
-
return a.name.localeCompare(b.name);
|
|
312
|
-
});
|
|
313
|
-
}
|
|
@@ -1,92 +1,53 @@
|
|
|
1
1
|
import { type Component, padding, truncateToWidth, visibleWidth } from "@f5-sales-demo/pi-tui";
|
|
2
|
-
import { APP_NAME
|
|
2
|
+
import { APP_NAME } from "@f5-sales-demo/pi-utils";
|
|
3
3
|
import { theme } from "../../modes/theme/theme";
|
|
4
|
-
import { formatStatusIcon } from "../../services/xcsh-context-indicators";
|
|
5
|
-
import type { ModelStatus, ServiceStatus, UnifiedPluginStatus } from "./welcome-checks";
|
|
6
|
-
|
|
7
|
-
export interface UpdateStatus {
|
|
8
|
-
available: boolean;
|
|
9
|
-
latestVersion?: string;
|
|
10
|
-
}
|
|
11
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Startup splash: the F5 logo under a ` xcsh vX.Y.Z ` title bar. Intentionally
|
|
7
|
+
* static and status-free — session/provider/plugin status lives in on-demand
|
|
8
|
+
* commands (/plugins, /context) so startup stays instant and never blocks or
|
|
9
|
+
* live-updates. See docs/superpowers/specs for the fast-startup design.
|
|
10
|
+
*/
|
|
12
11
|
export class WelcomeComponent implements Component {
|
|
13
|
-
constructor(
|
|
14
|
-
private readonly version: string,
|
|
15
|
-
private modelStatus: ModelStatus,
|
|
16
|
-
private services: ServiceStatus[] = [],
|
|
17
|
-
private updateStatus?: UpdateStatus,
|
|
18
|
-
private plugins: UnifiedPluginStatus[] = [],
|
|
19
|
-
) {}
|
|
12
|
+
constructor(private readonly version: string) {}
|
|
20
13
|
invalidate(): void {}
|
|
21
|
-
setModelStatus(status: ModelStatus): void {
|
|
22
|
-
this.modelStatus = status;
|
|
23
|
-
}
|
|
24
|
-
setServices(services: ServiceStatus[]): void {
|
|
25
|
-
this.services = services;
|
|
26
|
-
}
|
|
27
|
-
setUpdateStatus(status: UpdateStatus | undefined): void {
|
|
28
|
-
this.updateStatus = status;
|
|
29
|
-
}
|
|
30
|
-
setPlugins(plugins: UnifiedPluginStatus[]): void {
|
|
31
|
-
this.plugins = plugins;
|
|
32
|
-
}
|
|
33
14
|
|
|
34
15
|
render(termWidth: number): string[] {
|
|
35
|
-
const minLeftCol = 48;
|
|
36
|
-
const minRightCol = 20;
|
|
37
16
|
const preferredLeftCol = 50;
|
|
17
|
+
const logoMaxWidth = 46;
|
|
38
18
|
|
|
39
|
-
|
|
40
|
-
const naturalRight = this.#measureStatusWidth() + 1; // +1 right padding
|
|
41
|
-
const idealRight = Math.max(naturalRight, minRightCol);
|
|
42
|
-
const idealBox = preferredLeftCol + idealRight + 3; // 3 border chars: │ + │ + │
|
|
43
|
-
const boxWidth = Math.min(idealBox, Math.max(0, termWidth - 2));
|
|
19
|
+
const boxWidth = Math.min(preferredLeftCol + 2, Math.max(0, termWidth - 2));
|
|
44
20
|
if (boxWidth < 4) return [];
|
|
45
|
-
|
|
46
|
-
const dualContentWidth = boxWidth - 3;
|
|
47
|
-
// When terminal is narrower than ideal, shrink left column toward minLeftCol first
|
|
48
|
-
const dualLeftCol =
|
|
49
|
-
dualContentWidth >= preferredLeftCol + idealRight
|
|
50
|
-
? preferredLeftCol
|
|
51
|
-
: Math.max(minLeftCol, dualContentWidth - idealRight);
|
|
52
|
-
const dualRightCol = Math.max(0, dualContentWidth - dualLeftCol);
|
|
53
|
-
// Only show dual column when both columns have enough room for their content
|
|
54
|
-
const showRightColumn = dualLeftCol >= minLeftCol && dualRightCol >= Math.max(minRightCol, naturalRight);
|
|
55
|
-
const leftCol = showRightColumn ? dualLeftCol : boxWidth - 2;
|
|
56
|
-
const rightCol = showRightColumn ? dualRightCol : 0;
|
|
21
|
+
const leftCol = boxWidth - 2;
|
|
57
22
|
|
|
58
23
|
// biome-ignore format: preserve ASCII art layout
|
|
59
24
|
const f5Logo = [
|
|
60
25
|
" ________",
|
|
61
|
-
" (
|
|
62
|
-
" (
|
|
63
|
-
" (
|
|
64
|
-
" (
|
|
65
|
-
" (
|
|
66
|
-
" (
|
|
67
|
-
" (
|
|
68
|
-
"(
|
|
69
|
-
"
|
|
70
|
-
"
|
|
71
|
-
"(
|
|
72
|
-
" (
|
|
73
|
-
" (
|
|
74
|
-
" (
|
|
75
|
-
" (
|
|
76
|
-
" (
|
|
77
|
-
" (
|
|
78
|
-
" (
|
|
26
|
+
" (▒▒▒▒▓▓▓▓▓▓▓▓▒▒▒▒)",
|
|
27
|
+
" (▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒)",
|
|
28
|
+
" (▒▒▓▓▓▓██████████▓▓▓▓███████████████)",
|
|
29
|
+
" (▒▓▓▓▓██████▒▒▒▒▒███▓▓█████████████████▒)",
|
|
30
|
+
" (▒▓▓▓▓██████▒▓▓▓▓▓▒▒▒▓██▒▒▒▒▒▒▒▒▒▒▒▒▒▓▒)",
|
|
31
|
+
" (▒▓▓▓▓▓██████▓▓▓▓▓▓▓▓▓██▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒)",
|
|
32
|
+
" (▒▓▓████████████████▓▓▓▓████████████▓▓▓▓▓▓▒)",
|
|
33
|
+
"(▒▓▓▓▒▒▒███████▒▒▒▒▒▓▓▓████████████████▓▓▓▓▓▒)",
|
|
34
|
+
"|▒▓▓▓▓▓▓▒██████▓▓▓▓▓▓▓████████████████████▓▓▒|",
|
|
35
|
+
"|▒▓▓▓▓▓▓▓██████▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒██████████▓▒|",
|
|
36
|
+
"(▒▓▓▓▓▓▓▓██████▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒██████████▒▒)",
|
|
37
|
+
" (▒▓▓▓▓▓▓██████▓▓▓▓▓▓▓███▓▓▓▓▓▓▓▓▓▒▒▒████▒▒)",
|
|
38
|
+
" (▒▓▓▓▓▓██████▓▓▓▓▓▓██████▓▓▓▓▓▓▓▓▓▓▓▓████▒▒)",
|
|
39
|
+
" (▒▒██████████▓▓▓▓▓▒██████▓▓▓▓▓▓▓▓██████▒▒▒)",
|
|
40
|
+
" (▒▒▒▒▒██████████▓▓▒▒██████████████▒▒▓▒)",
|
|
41
|
+
" (▒▓▓▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▒)",
|
|
42
|
+
" (▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒)",
|
|
43
|
+
" (▒▒▒▒▓▓▓▓▓▓▓▓▒▒▒▒)",
|
|
79
44
|
];
|
|
80
45
|
|
|
81
46
|
const logoColored = f5Logo.map(line => this.#f5ColorLine(line));
|
|
82
|
-
const logoMaxWidth = 46;
|
|
83
47
|
const logoBlockPad = Math.max(0, Math.floor((leftCol - logoMaxWidth) / 2));
|
|
84
48
|
const logoPadStr = padding(logoBlockPad);
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
if (!showRightColumn) {
|
|
88
|
-
leftLines.push(...rightLines);
|
|
89
|
-
}
|
|
49
|
+
const contentLines = [...logoColored.map(l => logoPadStr + l), ""];
|
|
50
|
+
|
|
90
51
|
const border = (s: string) => theme.fg("borderMuted", s);
|
|
91
52
|
const hChar = theme.boxRound.horizontal;
|
|
92
53
|
const h = border(hChar);
|
|
@@ -95,6 +56,7 @@ export class WelcomeComponent implements Component {
|
|
|
95
56
|
const tr = border(theme.boxRound.topRight);
|
|
96
57
|
const bl = border(theme.boxRound.bottomLeft);
|
|
97
58
|
const br = border(theme.boxRound.bottomRight);
|
|
59
|
+
|
|
98
60
|
const lines: string[] = [];
|
|
99
61
|
const title = ` ${APP_NAME} v${this.version} `;
|
|
100
62
|
const titlePrefixRaw = hChar.repeat(3);
|
|
@@ -106,125 +68,13 @@ export class WelcomeComponent implements Component {
|
|
|
106
68
|
} else {
|
|
107
69
|
lines.push(tl + titleStyled + border(hChar.repeat(titleSpace - titleVisLen)) + tr);
|
|
108
70
|
}
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
const left = this.#fitToWidth(leftLines[i] ?? "", leftCol);
|
|
112
|
-
if (showRightColumn) {
|
|
113
|
-
const right = this.#fitToWidth(rightLines[i] ?? "", rightCol);
|
|
114
|
-
lines.push(v + left + v + right + v);
|
|
115
|
-
} else {
|
|
116
|
-
lines.push(v + left + v);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
if (showRightColumn) {
|
|
120
|
-
lines.push(bl + h.repeat(leftCol) + border(theme.boxSharp.teeUp) + h.repeat(rightCol) + br);
|
|
121
|
-
} else {
|
|
122
|
-
lines.push(bl + h.repeat(leftCol) + br);
|
|
123
|
-
}
|
|
124
|
-
return lines;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
#measureStatusWidth(): number {
|
|
128
|
-
const lines: string[] = [` ${t("welcome.modelProvider")}`, ...this.#renderModelStatus()];
|
|
129
|
-
const coreServices = this.services.filter(s => !s._isPlugin);
|
|
130
|
-
for (const svc of coreServices) {
|
|
131
|
-
lines.push(this.#renderServiceLine(svc));
|
|
132
|
-
}
|
|
133
|
-
if (this.plugins.length > 0) {
|
|
134
|
-
lines.push(` ${t("welcome.plugins")}`);
|
|
135
|
-
for (const p of this.plugins) {
|
|
136
|
-
lines.push(this.#renderUnifiedPluginLine(p));
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
if (this.#showUpdateSection()) {
|
|
140
|
-
lines.push(this.#renderUpdateLine());
|
|
141
|
-
}
|
|
142
|
-
return Math.max(...lines.map(l => visibleWidth(l)));
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
#renderUnifiedPluginLine(plugin: UnifiedPluginStatus): string {
|
|
146
|
-
if (plugin.state === "connected" || plugin.state === "installed") {
|
|
147
|
-
return ` ${formatStatusIcon("connected")} ${theme.fg("muted", plugin.name)}`;
|
|
148
|
-
}
|
|
149
|
-
if (plugin.state === "not_installed") {
|
|
150
|
-
return ` ${theme.fg("dim", "·")} ${theme.fg("dim", plugin.name)}`;
|
|
71
|
+
for (const line of contentLines) {
|
|
72
|
+
lines.push(v + this.#fitToWidth(line, leftCol) + v);
|
|
151
73
|
}
|
|
152
|
-
|
|
153
|
-
return ` ${formatStatusIcon("warning")} ${theme.fg("muted", plugin.name)} ${theme.fg("dim", hint)}`;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
#buildStatusLines(rightCol: number): string[] {
|
|
157
|
-
const lines: string[] = [];
|
|
158
|
-
const separatorWidth = Math.max(0, rightCol - 2);
|
|
159
|
-
const separator = ` ${theme.fg("muted", theme.boxRound.horizontal.repeat(separatorWidth))}`;
|
|
160
|
-
lines.push("");
|
|
161
|
-
lines.push(` ${theme.bold(theme.fg("contentAccent", t("welcome.modelProvider")))}`);
|
|
162
|
-
lines.push(...this.#renderModelStatus());
|
|
163
|
-
lines.push("");
|
|
164
|
-
const coreServices = this.services.filter(s => !s._isPlugin);
|
|
165
|
-
const hasContent = coreServices.length > 0 || this.plugins.length > 0 || this.#showUpdateSection();
|
|
166
|
-
if (hasContent) {
|
|
167
|
-
lines.push(separator);
|
|
168
|
-
for (const svc of coreServices) {
|
|
169
|
-
lines.push(this.#renderServiceLine(svc));
|
|
170
|
-
}
|
|
171
|
-
if (this.plugins.length > 0) {
|
|
172
|
-
lines.push("");
|
|
173
|
-
lines.push(` ${theme.fg("dim", t("welcome.plugins"))}`);
|
|
174
|
-
for (const p of this.plugins) {
|
|
175
|
-
lines.push(this.#renderUnifiedPluginLine(p));
|
|
176
|
-
}
|
|
177
|
-
const missing = this.plugins.filter(p => p.state === "not_installed");
|
|
178
|
-
if (missing.length > 0) {
|
|
179
|
-
lines.push(` ${theme.fg("dim", t("welcome.pluginSetupHint"))}`);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
if (this.#showUpdateSection()) {
|
|
184
|
-
lines.push(this.#renderUpdateLine());
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
lines.push("");
|
|
74
|
+
lines.push(bl + h.repeat(leftCol) + br);
|
|
188
75
|
return lines;
|
|
189
76
|
}
|
|
190
77
|
|
|
191
|
-
#showUpdateSection(): boolean {
|
|
192
|
-
return this.updateStatus?.available === true;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
#renderServiceLine(service: ServiceStatus): string {
|
|
196
|
-
if (service.state === "connected") {
|
|
197
|
-
return ` ${formatStatusIcon("connected")} ${theme.fg("muted", service.name)}`;
|
|
198
|
-
}
|
|
199
|
-
const hint = service.hint ?? "";
|
|
200
|
-
return ` ${formatStatusIcon("warning")} ${theme.fg("muted", service.name)} ${theme.fg("dim", hint)}`;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
#renderUpdateLine(): string {
|
|
204
|
-
const latest = this.updateStatus?.latestVersion;
|
|
205
|
-
const label = latest ? `v${latest}` : t("welcome.newVersion");
|
|
206
|
-
return ` ${theme.fg("warning", "↑")} ${theme.fg("muted", label)} ${theme.fg("dim", t("welcome.updateHint"))}`;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
#renderModelStatus(): string[] {
|
|
210
|
-
const { state, provider } = this.modelStatus;
|
|
211
|
-
const p = provider ?? "unknown";
|
|
212
|
-
switch (state) {
|
|
213
|
-
case "connected":
|
|
214
|
-
return [` ${formatStatusIcon("connected")} ${theme.fg("muted", p)}`];
|
|
215
|
-
case "auth_error":
|
|
216
|
-
return [
|
|
217
|
-
` ${formatStatusIcon("error")} ${theme.fg("muted", p)} ${theme.fg("error", t("welcome.connectionFailed"))}`,
|
|
218
|
-
` ${theme.fg("dim", t("welcome.runLoginReconnect"))}`,
|
|
219
|
-
];
|
|
220
|
-
case "no_provider":
|
|
221
|
-
return [
|
|
222
|
-
` ${formatStatusIcon("error")} ${theme.fg("error", t("welcome.noModelProvider"))}`,
|
|
223
|
-
` ${theme.fg("dim", t("welcome.runLoginConnect"))}`,
|
|
224
|
-
];
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
78
|
#f5ColorLine(line: string): string {
|
|
229
79
|
const red = "\x1b[38;5;160m";
|
|
230
80
|
const white = "\x1b[1;37m";
|
|
@@ -236,9 +86,9 @@ export class WelcomeComponent implements Component {
|
|
|
236
86
|
const reset = "\x1b[0m";
|
|
237
87
|
let result = "";
|
|
238
88
|
for (const char of line) {
|
|
239
|
-
if (char === "
|
|
240
|
-
else if (char === "
|
|
241
|
-
else if (char === "
|
|
89
|
+
if (char === "▓") result += `${red}█${reset}`;
|
|
90
|
+
else if (char === "█") result += `${white}█${reset}`;
|
|
91
|
+
else if (char === "▒") result += `${red}${shadowBg}▒${reset}`;
|
|
242
92
|
else if ("()|_".includes(char)) result += `${red}${char}${reset}`;
|
|
243
93
|
else result += char;
|
|
244
94
|
}
|
|
@@ -248,7 +98,7 @@ export class WelcomeComponent implements Component {
|
|
|
248
98
|
#fitToWidth(str: string, width: number): string {
|
|
249
99
|
const visLen = visibleWidth(str);
|
|
250
100
|
if (visLen > width) {
|
|
251
|
-
const ellipsis = "
|
|
101
|
+
const ellipsis = "…";
|
|
252
102
|
const maxW = Math.max(0, width - visibleWidth(ellipsis));
|
|
253
103
|
let t = "";
|
|
254
104
|
let cw = 0;
|
|
@@ -3,7 +3,7 @@ import * as path from "node:path";
|
|
|
3
3
|
import { type AgentMessage, ThinkingLevel } from "@f5-sales-demo/pi-agent-core";
|
|
4
4
|
import { sanitizeText } from "@f5-sales-demo/pi-natives";
|
|
5
5
|
import { type AutocompleteProvider, ChordDispatcher, type SlashCommand } from "@f5-sales-demo/pi-tui";
|
|
6
|
-
import { $env } from "@f5-sales-demo/pi-utils";
|
|
6
|
+
import { $env, t } from "@f5-sales-demo/pi-utils";
|
|
7
7
|
import { settings } from "../../config/settings";
|
|
8
8
|
import { createStreamingAssistantGutter } from "../../modes/components/gutter-block";
|
|
9
9
|
import { createPromptActionAutocompleteProvider } from "../../modes/prompt-action-autocomplete";
|
|
@@ -374,6 +374,16 @@ export class InputController {
|
|
|
374
374
|
}
|
|
375
375
|
}
|
|
376
376
|
|
|
377
|
+
// LLM readiness gate: without a configured provider, natural language
|
|
378
|
+
// cannot be processed. Slash/skill/bash/python commands are handled and
|
|
379
|
+
// returned above, so only genuine natural language reaches here — block it
|
|
380
|
+
// with a clear /login prompt and preserve the user's text.
|
|
381
|
+
if (!this.ctx.hasActiveLlmProvider()) {
|
|
382
|
+
this.ctx.showWarning(t("gate.noProviderBlock"));
|
|
383
|
+
this.ctx.editor.setText(text);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
377
387
|
// Queue input during compaction
|
|
378
388
|
if (this.ctx.session.isCompacting) {
|
|
379
389
|
if (this.ctx.pendingImages.length > 0) {
|
|
@@ -479,6 +489,12 @@ export class InputController {
|
|
|
479
489
|
const text = this.ctx.editor.getText().trim();
|
|
480
490
|
if (!text) return;
|
|
481
491
|
|
|
492
|
+
// LLM readiness gate — a follow-up is still natural language for the model.
|
|
493
|
+
if (!this.ctx.hasActiveLlmProvider()) {
|
|
494
|
+
this.ctx.showWarning(t("gate.noProviderBlock"));
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
|
|
482
498
|
if (this.ctx.session.isCompacting) {
|
|
483
499
|
this.ctx.queueCompactionMessage(text, "followUp");
|
|
484
500
|
return;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Model } from "@f5-sales-demo/pi-ai";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Minimal session surface needed to apply a model after a successful login.
|
|
5
|
+
* Kept structural so the login flow can call it without pulling in the full
|
|
6
|
+
* AgentSession type, and so it stays trivially unit-testable.
|
|
7
|
+
*/
|
|
8
|
+
interface ModelApplicableSession {
|
|
9
|
+
model: Model | undefined;
|
|
10
|
+
modelRegistry: { getAll(): Model[] };
|
|
11
|
+
setModel(model: Model, role: "default", options?: { selector?: string }): Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* After a successful `/login`, make the freshly-configured provider immediately
|
|
16
|
+
* usable by setting it as the session's default model — but only when the session
|
|
17
|
+
* has no model yet, so we never override a model the user already chose.
|
|
18
|
+
*
|
|
19
|
+
* Returns true when a model was applied. The login wizard auto-selects a model id
|
|
20
|
+
* from the provider's /models list; resolving it here (post-registry-refresh) is
|
|
21
|
+
* what lets the LLM readiness gate lift without a manual `/model` step.
|
|
22
|
+
*/
|
|
23
|
+
export async function applyModelAfterLogin(
|
|
24
|
+
session: ModelApplicableSession,
|
|
25
|
+
selectedModelId: string | undefined,
|
|
26
|
+
): Promise<boolean> {
|
|
27
|
+
if (session.model || !selectedModelId) return false;
|
|
28
|
+
const resolved = session.modelRegistry.getAll().find(m => m.id === selectedModelId);
|
|
29
|
+
if (!resolved) return false;
|
|
30
|
+
await session.setModel(resolved, "default", { selector: resolved.id });
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
@@ -58,6 +58,7 @@ import { ToolExecutionComponent } from "../components/tool-execution";
|
|
|
58
58
|
import { TreeSelectorComponent } from "../components/tree-selector";
|
|
59
59
|
import { UserMessageSelectorComponent } from "../components/user-message-selector";
|
|
60
60
|
import type { SessionObserverRegistry } from "../session-observer-registry";
|
|
61
|
+
import { applyModelAfterLogin } from "./login-model";
|
|
61
62
|
|
|
62
63
|
const CALLBACK_SERVER_PROVIDERS = new Set<OAuthProvider>([
|
|
63
64
|
"anthropic",
|
|
@@ -1173,6 +1174,9 @@ export class SelectorController {
|
|
|
1173
1174
|
healConfigYmlModelRoles(configPath);
|
|
1174
1175
|
|
|
1175
1176
|
await this.ctx.session.modelRegistry.refresh("online");
|
|
1177
|
+
// Make the freshly-configured provider usable immediately so the LLM
|
|
1178
|
+
// readiness gate lifts without requiring a manual /model selection.
|
|
1179
|
+
await applyModelAfterLogin(this.ctx.session, selectedModel);
|
|
1176
1180
|
await this.ctx.refreshWelcomeAfterLogin();
|
|
1177
1181
|
probeSuccess = true;
|
|
1178
1182
|
} else {
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
} from "@f5-sales-demo/pi-ai";
|
|
16
16
|
import type { Component, SlashCommand } from "@f5-sales-demo/pi-tui";
|
|
17
17
|
import { Container, Loader, Markdown, ProcessTerminal, Spacer, Text, TUI, visibleWidth } from "@f5-sales-demo/pi-tui";
|
|
18
|
-
import { getProjectDir, hsvToRgb, isEnoent, logger, postmortem, prompt } from "@f5-sales-demo/pi-utils";
|
|
18
|
+
import { getProjectDir, hsvToRgb, isEnoent, logger, postmortem, prompt, t } from "@f5-sales-demo/pi-utils";
|
|
19
19
|
import chalk from "chalk";
|
|
20
20
|
import { KeybindingsManager } from "../config/keybindings";
|
|
21
21
|
import { type Settings, settings } from "../config/settings";
|
|
@@ -32,7 +32,6 @@ import { seedComputerProfile } from "../internal-urls/computer-profile";
|
|
|
32
32
|
import { reconcileFromCollectors } from "../internal-urls/user-profile";
|
|
33
33
|
import { renameApprovedPlanFile } from "../plan-mode/approved-plan";
|
|
34
34
|
import planModeApprovedPrompt from "../prompts/system/plan-mode-approved.md" with { type: "text" };
|
|
35
|
-
import { ContextService } from "../services/xcsh-context";
|
|
36
35
|
import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
|
|
37
36
|
import { HistoryStorage } from "../session/history-storage";
|
|
38
37
|
import type { SessionContext, SessionManager } from "../session/session-manager";
|
|
@@ -52,14 +51,8 @@ import type { HookSelectorComponent } from "./components/hook-selector";
|
|
|
52
51
|
import type { PythonExecutionComponent } from "./components/python-execution";
|
|
53
52
|
import { StatusLineComponent } from "./components/status-line";
|
|
54
53
|
import type { ToolExecutionHandle } from "./components/tool-execution";
|
|
55
|
-
import {
|
|
56
|
-
import {
|
|
57
|
-
buildUnifiedPluginList,
|
|
58
|
-
type FixableService,
|
|
59
|
-
mapContextStatus,
|
|
60
|
-
runWelcomeChecks,
|
|
61
|
-
type ServiceStatus,
|
|
62
|
-
} from "./components/welcome-checks";
|
|
54
|
+
import { WelcomeComponent } from "./components/welcome";
|
|
55
|
+
import { hasActiveLlmProvider } from "./components/welcome-checks";
|
|
63
56
|
import { BtwController } from "./controllers/btw-controller";
|
|
64
57
|
import { CommandController } from "./controllers/command-controller";
|
|
65
58
|
import { EventController } from "./controllers/event-controller";
|
|
@@ -170,7 +163,6 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
170
163
|
#pendingSlashCommands: SlashCommand[] = [];
|
|
171
164
|
#cleanupUnsubscribe?: () => void;
|
|
172
165
|
readonly #version: string;
|
|
173
|
-
readonly #initialUpdateStatus: UpdateStatus | undefined;
|
|
174
166
|
#planModePreviousTools: string[] | undefined;
|
|
175
167
|
#planModePreviousModelState: { model: Model; thinkingLevel?: ThinkingLevel } | undefined;
|
|
176
168
|
#pendingModelSwitch: { model: Model; thinkingLevel?: ThinkingLevel } | undefined;
|
|
@@ -197,12 +189,10 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
197
189
|
#eventBus?: EventBus;
|
|
198
190
|
#eventBusUnsubscribers: Array<() => void> = [];
|
|
199
191
|
#welcomeComponent?: WelcomeComponent;
|
|
200
|
-
#currentPlugins: import("./components/welcome-checks").UnifiedPluginStatus[] = [];
|
|
201
192
|
|
|
202
193
|
constructor(
|
|
203
194
|
session: AgentSession,
|
|
204
195
|
version: string,
|
|
205
|
-
initialUpdateStatus: UpdateStatus | undefined = undefined,
|
|
206
196
|
setToolUIContext: (uiContext: ExtensionUIContext, hasUI: boolean) => void = () => {},
|
|
207
197
|
lspServers?: import("../tools").LspStartupServerInfo[],
|
|
208
198
|
mcpManager?: import("../mcp").MCPManager,
|
|
@@ -214,7 +204,6 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
214
204
|
this.keybindings = KeybindingsManager.inMemory();
|
|
215
205
|
this.agent = session.agent;
|
|
216
206
|
this.#version = version;
|
|
217
|
-
this.#initialUpdateStatus = initialUpdateStatus;
|
|
218
207
|
this.#toolUiContextSetter = setToolUIContext;
|
|
219
208
|
this.lspServers = lspServers;
|
|
220
209
|
this.mcpManager = mcpManager;
|
|
@@ -317,11 +306,6 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
317
306
|
getProjectDir(),
|
|
318
307
|
);
|
|
319
308
|
|
|
320
|
-
// Run blocking welcome screen status checks
|
|
321
|
-
const welcomeResult = await logger.time("InteractiveMode.init:welcomeChecks", () =>
|
|
322
|
-
runWelcomeChecks(this.session.model, this.session.modelRegistry.authStorage),
|
|
323
|
-
);
|
|
324
|
-
|
|
325
309
|
// Refresh user profile in background — fire and forget
|
|
326
310
|
reconcileFromCollectors().catch(err => logger.warn("Background profile refresh failed", { error: String(err) }));
|
|
327
311
|
// Refresh computer profile in background — fire and forget
|
|
@@ -337,60 +321,20 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
337
321
|
this.ui.addChild(new Spacer(1));
|
|
338
322
|
}
|
|
339
323
|
|
|
340
|
-
//
|
|
341
|
-
//
|
|
342
|
-
|
|
343
|
-
const
|
|
344
|
-
? { state: "no_provider" as const, provider: undefined }
|
|
345
|
-
: welcomeResult.model;
|
|
346
|
-
|
|
347
|
-
const services: ServiceStatus[] =
|
|
348
|
-
!startupQuiet && !needsLogin ? [mapContextStatus(welcomeResult.context ?? { state: "no_context" })] : [];
|
|
349
|
-
|
|
350
|
-
// Build unified plugin list from service status contributions + marketplace
|
|
351
|
-
// Skip when model is not configured — nothing works without a model.
|
|
352
|
-
const pluginContributions = this.session.extensionRunner?.getAllRegisteredServiceStatuses() ?? [];
|
|
353
|
-
const plugins =
|
|
354
|
-
!startupQuiet && !needsLogin ? await buildUnifiedPluginList(pluginContributions).catch(() => []) : [];
|
|
355
|
-
this.#currentPlugins = plugins;
|
|
356
|
-
|
|
357
|
-
const fixableServices: FixableService[] = [];
|
|
358
|
-
if (!needsLogin) {
|
|
359
|
-
for (const contribution of pluginContributions) {
|
|
360
|
-
if (contribution.fix) {
|
|
361
|
-
const plugin = plugins.find(p => p.name.toLowerCase() === contribution.name.toLowerCase());
|
|
362
|
-
if (plugin && plugin.state === "unauthenticated") {
|
|
363
|
-
fixableServices.push({
|
|
364
|
-
name: contribution.name,
|
|
365
|
-
prompt: contribution.fix.prompt,
|
|
366
|
-
command: contribution.fix.command,
|
|
367
|
-
recheck: async () => {
|
|
368
|
-
try {
|
|
369
|
-
const result = await contribution.check();
|
|
370
|
-
return { name: contribution.name, ...result };
|
|
371
|
-
} catch {
|
|
372
|
-
return { name: contribution.name, state: "unavailable" as const, hint: "recheck failed" };
|
|
373
|
-
}
|
|
374
|
-
},
|
|
375
|
-
});
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
}
|
|
324
|
+
// LLM readiness gate: instant, local check (no network). When no provider is
|
|
325
|
+
// configured we still initialize immediately, but warn the user to /login —
|
|
326
|
+
// natural-language input is blocked until then (enforced in input-controller).
|
|
327
|
+
const needsLogin = !hasActiveLlmProvider(this.session.model, this.session.modelRegistry.authStorage);
|
|
380
328
|
|
|
381
329
|
if (!startupQuiet) {
|
|
382
|
-
this.#welcomeComponent = new WelcomeComponent(
|
|
383
|
-
this.#version,
|
|
384
|
-
welcomeModelStatus,
|
|
385
|
-
services,
|
|
386
|
-
this.#initialUpdateStatus,
|
|
387
|
-
plugins,
|
|
388
|
-
);
|
|
389
|
-
|
|
390
|
-
// Setup UI layout
|
|
330
|
+
this.#welcomeComponent = new WelcomeComponent(this.#version);
|
|
391
331
|
this.ui.addChild(new Spacer(1));
|
|
392
332
|
this.ui.addChild(this.#welcomeComponent);
|
|
393
333
|
this.ui.addChild(new Spacer(1));
|
|
334
|
+
if (needsLogin) {
|
|
335
|
+
this.ui.addChild(new Text(theme.fg("warning", t("gate.noProvider")), 1, 0));
|
|
336
|
+
this.ui.addChild(new Spacer(1));
|
|
337
|
+
}
|
|
394
338
|
}
|
|
395
339
|
|
|
396
340
|
this.ui.addChild(this.chatContainer);
|
|
@@ -436,15 +380,6 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
436
380
|
// Initialize hooks with TUI-based UI context
|
|
437
381
|
await this.initHooksAndCustomTools();
|
|
438
382
|
|
|
439
|
-
// Offer to fix expired cloud credentials before entering the main loop
|
|
440
|
-
if (fixableServices.length > 0) {
|
|
441
|
-
await this.#offerCredentialFixes(fixableServices, services);
|
|
442
|
-
// Clear any gibberish that accumulated in the editor during the
|
|
443
|
-
// stop/start cycles (Kitty protocol re-negotiation can leave
|
|
444
|
-
// raw escape sequences in the input buffer).
|
|
445
|
-
this.editor.setText("");
|
|
446
|
-
}
|
|
447
|
-
|
|
448
383
|
// Restore mode from session (e.g. plan mode on resume)
|
|
449
384
|
await this.#restoreModeFromSession();
|
|
450
385
|
|
|
@@ -944,52 +879,6 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
944
879
|
}
|
|
945
880
|
}
|
|
946
881
|
|
|
947
|
-
async #offerCredentialFixes(fixable: FixableService[], currentServices: ServiceStatus[]): Promise<void> {
|
|
948
|
-
for (const service of fixable) {
|
|
949
|
-
const confirmed = await this.#extensionUiController.showHookConfirm(
|
|
950
|
-
`${service.name} login`,
|
|
951
|
-
`${service.prompt}. Fix now?`,
|
|
952
|
-
);
|
|
953
|
-
if (!confirmed) continue;
|
|
954
|
-
|
|
955
|
-
// Drain pending terminal input (e.g. an in-flight OSC 11 poll response)
|
|
956
|
-
// before dropping raw mode, so it is consumed here instead of echoed as
|
|
957
|
-
// gibberish by the terminal while the cooked-mode subprocess runs.
|
|
958
|
-
await this.ui.suspendForSubprocess();
|
|
959
|
-
try {
|
|
960
|
-
const proc = Bun.spawn(service.command, {
|
|
961
|
-
stdin: "inherit",
|
|
962
|
-
stdout: "inherit",
|
|
963
|
-
stderr: "inherit",
|
|
964
|
-
});
|
|
965
|
-
await proc.exited;
|
|
966
|
-
} catch (error) {
|
|
967
|
-
logger.warn(`Auto-fix for ${service.name} failed`, { error: String(error) });
|
|
968
|
-
} finally {
|
|
969
|
-
const clearScreen = settings.get("startup.clearScreen");
|
|
970
|
-
this.ui.start(clearScreen);
|
|
971
|
-
this.ui.requestRender(true);
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
const updated = await service.recheck();
|
|
975
|
-
const idx = currentServices.findIndex(s => s.name === service.name);
|
|
976
|
-
if (idx !== -1) {
|
|
977
|
-
currentServices[idx] = updated;
|
|
978
|
-
this.#welcomeComponent?.setServices([...currentServices]);
|
|
979
|
-
}
|
|
980
|
-
const pluginIdx = this.#currentPlugins.findIndex(p => p.name.toLowerCase() === service.name.toLowerCase());
|
|
981
|
-
if (pluginIdx !== -1) {
|
|
982
|
-
this.#currentPlugins[pluginIdx] = {
|
|
983
|
-
...this.#currentPlugins[pluginIdx],
|
|
984
|
-
state: updated.state,
|
|
985
|
-
hint: updated.hint,
|
|
986
|
-
};
|
|
987
|
-
this.#welcomeComponent?.setPlugins([...this.#currentPlugins]);
|
|
988
|
-
}
|
|
989
|
-
this.ui.requestRender();
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
|
|
993
882
|
async #approvePlan(
|
|
994
883
|
planContent: string,
|
|
995
884
|
options: { planFilePath: string; finalPlanFilePath: string },
|
|
@@ -1208,6 +1097,10 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1208
1097
|
this.#uiHelpers.showWarning(message);
|
|
1209
1098
|
}
|
|
1210
1099
|
|
|
1100
|
+
hasActiveLlmProvider(): boolean {
|
|
1101
|
+
return hasActiveLlmProvider(this.session.model, this.session.modelRegistry.authStorage);
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1211
1104
|
ensureLoadingAnimation(): void {
|
|
1212
1105
|
if (!this.loadingAnimation) {
|
|
1213
1106
|
this.statusContainer.clear();
|
|
@@ -1251,11 +1144,6 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1251
1144
|
this.setWorkingMessage(message);
|
|
1252
1145
|
}
|
|
1253
1146
|
|
|
1254
|
-
setUpdateStatus(status: UpdateStatus | undefined): void {
|
|
1255
|
-
this.#welcomeComponent?.setUpdateStatus(status);
|
|
1256
|
-
this.ui.requestRender();
|
|
1257
|
-
}
|
|
1258
|
-
|
|
1259
1147
|
clearEditor(): void {
|
|
1260
1148
|
this.#uiHelpers.clearEditor();
|
|
1261
1149
|
}
|
|
@@ -1471,42 +1359,8 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1471
1359
|
}
|
|
1472
1360
|
|
|
1473
1361
|
async refreshWelcomeAfterLogin(): Promise<void> {
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
// Validate F5 XC Context independently — call validateToken() for a live
|
|
1477
|
-
// check instead of reading cached state (which may be "unknown" if
|
|
1478
|
-
// validation hasn't run yet in this session).
|
|
1479
|
-
const services: ServiceStatus[] = [];
|
|
1480
|
-
try {
|
|
1481
|
-
const ctxService = ContextService.instance;
|
|
1482
|
-
const ctxStatus = ctxService.getStatus();
|
|
1483
|
-
if (ctxStatus.isConfigured) {
|
|
1484
|
-
const name = ctxStatus.activeContextTenant ?? ctxStatus.activeContextName ?? undefined;
|
|
1485
|
-
const result = await ctxService.validateToken({ timeoutMs: 5000 });
|
|
1486
|
-
services.push(
|
|
1487
|
-
mapContextStatus({
|
|
1488
|
-
state:
|
|
1489
|
-
result.status === "connected"
|
|
1490
|
-
? "connected"
|
|
1491
|
-
: result.status === "auth_error"
|
|
1492
|
-
? "auth_error"
|
|
1493
|
-
: "offline",
|
|
1494
|
-
name,
|
|
1495
|
-
}),
|
|
1496
|
-
);
|
|
1497
|
-
}
|
|
1498
|
-
} catch {
|
|
1499
|
-
// ContextService not initialized — skip
|
|
1500
|
-
}
|
|
1501
|
-
this.#welcomeComponent?.setServices(services);
|
|
1502
|
-
|
|
1503
|
-
// Run plugin checks and update the welcome screen
|
|
1504
|
-
const pluginContributions = this.session.extensionRunner?.getAllRegisteredServiceStatuses() ?? [];
|
|
1505
|
-
const plugins = await buildUnifiedPluginList(pluginContributions).catch(() => []);
|
|
1506
|
-
this.#currentPlugins = plugins;
|
|
1507
|
-
this.#welcomeComponent?.setPlugins(plugins);
|
|
1508
|
-
|
|
1509
|
-
// Restore editor
|
|
1362
|
+
// The welcome splash is static (logo + version only); after login the model
|
|
1363
|
+
// gate lifts on its own (see applyModelAfterLogin). Just restore the editor.
|
|
1510
1364
|
this.editorContainer.clear();
|
|
1511
1365
|
this.editorContainer.addChild(this.editor);
|
|
1512
1366
|
this.ui.setFocus(this.editor);
|
package/src/modes/types.ts
CHANGED
|
@@ -137,6 +137,8 @@ export interface InteractiveModeContext {
|
|
|
137
137
|
showStatus(message: string, options?: { dim?: boolean }): void;
|
|
138
138
|
showError(message: string): void;
|
|
139
139
|
showWarning(message: string): void;
|
|
140
|
+
/** Instant, local check for whether a usable LLM provider is configured (no network). */
|
|
141
|
+
hasActiveLlmProvider(): boolean;
|
|
140
142
|
clearEditor(): void;
|
|
141
143
|
updatePendingMessagesDisplay(): void;
|
|
142
144
|
queueCompactionMessage(text: string, mode: "steer" | "followUp"): void;
|