@f5xc-salesdemos/xcsh 19.27.1 → 19.28.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": "19.27.1",
4
+ "version": "19.28.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.27.1",
54
- "@f5xc-salesdemos/pi-agent-core": "19.27.1",
55
- "@f5xc-salesdemos/pi-ai": "19.27.1",
56
- "@f5xc-salesdemos/pi-natives": "19.27.1",
57
- "@f5xc-salesdemos/pi-tui": "19.27.1",
58
- "@f5xc-salesdemos/pi-utils": "19.27.1",
53
+ "@f5xc-salesdemos/xcsh-stats": "19.28.0",
54
+ "@f5xc-salesdemos/pi-agent-core": "19.28.0",
55
+ "@f5xc-salesdemos/pi-ai": "19.28.0",
56
+ "@f5xc-salesdemos/pi-natives": "19.28.0",
57
+ "@f5xc-salesdemos/pi-tui": "19.28.0",
58
+ "@f5xc-salesdemos/pi-utils": "19.28.0",
59
59
  "@sinclair/typebox": "^0.34",
60
60
  "@xterm/headless": "^6.0",
61
61
  "ajv": "^8.20",
@@ -17,17 +17,17 @@ export interface BuildInfo {
17
17
  }
18
18
 
19
19
  export const BUILD_INFO: BuildInfo = {
20
- "version": "19.27.1",
21
- "commit": "733f34673d030878f7fba143e2d2d3d89f3294cc",
22
- "shortCommit": "733f346",
20
+ "version": "19.28.0",
21
+ "commit": "f5e2cb01ca859d340f3d8803be61d501719c3971",
22
+ "shortCommit": "f5e2cb0",
23
23
  "branch": "main",
24
- "tag": "v19.27.1",
25
- "commitDate": "2026-06-11T03:12:32Z",
26
- "buildDate": "2026-06-11T03:35:17.807Z",
24
+ "tag": "v19.28.0",
25
+ "commitDate": "2026-06-11T13:31:38Z",
26
+ "buildDate": "2026-06-11T14:03:40.572Z",
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/733f34673d030878f7fba143e2d2d3d89f3294cc",
32
- "releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v19.27.1"
31
+ "commitUrl": "https://github.com/f5xc-salesdemos/xcsh/commit/f5e2cb01ca859d340f3d8803be61d501719c3971",
32
+ "releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v19.28.0"
33
33
  };
@@ -273,6 +273,15 @@
273
273
  "welcome.runLoginReconnect": "شغّل /login لإعادة الاتصال",
274
274
  "welcome.noModelProvider": "لم يتم تكوين مزود نموذج",
275
275
  "welcome.runLoginConnect": "شغّل /login للاتصال",
276
+ "login.wizard.urlPrompt": "Model Provider URL",
277
+ "login.wizard.urlPlaceholder": "https://your-proxy.example.com",
278
+ "login.wizard.apiKeyPrompt": "API Key",
279
+ "login.wizard.apiKeyPlaceholder": "sk-...",
280
+ "login.wizard.connecting": "Connecting to {url}…",
281
+ "login.wizard.success": "Connected — {count} models available",
282
+ "login.wizard.configSaved": "Configuration saved. You're ready to go!",
283
+ "login.wizard.failed": "Connection failed — {error}",
284
+ "login.wizard.retryHint": "Press Escape to skip, or enter a new value:",
276
285
  "plugins.tabs.installed": "مثبتة",
277
286
  "plugins.tabs.recommended": "موصى بها",
278
287
  "plugins.tabs.discover": "استكشاف",
@@ -273,6 +273,15 @@
273
273
  "welcome.runLoginReconnect": "Führen Sie /login aus, um die Verbindung wiederherzustellen",
274
274
  "welcome.noModelProvider": "Kein Modellanbieter konfiguriert",
275
275
  "welcome.runLoginConnect": "Führen Sie /login aus, um sich zu verbinden",
276
+ "login.wizard.urlPrompt": "Model Provider URL",
277
+ "login.wizard.urlPlaceholder": "https://your-proxy.example.com",
278
+ "login.wizard.apiKeyPrompt": "API Key",
279
+ "login.wizard.apiKeyPlaceholder": "sk-...",
280
+ "login.wizard.connecting": "Connecting to {url}…",
281
+ "login.wizard.success": "Connected — {count} models available",
282
+ "login.wizard.configSaved": "Configuration saved. You're ready to go!",
283
+ "login.wizard.failed": "Connection failed — {error}",
284
+ "login.wizard.retryHint": "Press Escape to skip, or enter a new value:",
276
285
  "plugins.tabs.installed": "Installiert",
277
286
  "plugins.tabs.recommended": "Empfohlen",
278
287
  "plugins.tabs.discover": "Entdecken",
@@ -291,6 +291,15 @@
291
291
  "welcome.runLoginReconnect": "Run /login to reconnect",
292
292
  "welcome.noModelProvider": "No model provider configured",
293
293
  "welcome.runLoginConnect": "Run /login to connect",
294
+ "login.wizard.urlPrompt": "Model Provider URL",
295
+ "login.wizard.urlPlaceholder": "https://your-proxy.example.com",
296
+ "login.wizard.apiKeyPrompt": "API Key",
297
+ "login.wizard.apiKeyPlaceholder": "sk-...",
298
+ "login.wizard.connecting": "Connecting to {url}…",
299
+ "login.wizard.success": "Connected — {count} models available",
300
+ "login.wizard.configSaved": "Configuration saved. You're ready to go!",
301
+ "login.wizard.failed": "Connection failed — {error}",
302
+ "login.wizard.retryHint": "Press Escape to skip, or enter a new value:",
294
303
 
295
304
  "plugins.tabs.installed": "Installed",
296
305
  "plugins.tabs.recommended": "Recommended",
@@ -273,6 +273,15 @@
273
273
  "welcome.runLoginReconnect": "Ejecute /login para reconectar",
274
274
  "welcome.noModelProvider": "No hay proveedor de modelo configurado",
275
275
  "welcome.runLoginConnect": "Ejecute /login para conectar",
276
+ "login.wizard.urlPrompt": "Model Provider URL",
277
+ "login.wizard.urlPlaceholder": "https://your-proxy.example.com",
278
+ "login.wizard.apiKeyPrompt": "API Key",
279
+ "login.wizard.apiKeyPlaceholder": "sk-...",
280
+ "login.wizard.connecting": "Connecting to {url}…",
281
+ "login.wizard.success": "Connected — {count} models available",
282
+ "login.wizard.configSaved": "Configuration saved. You're ready to go!",
283
+ "login.wizard.failed": "Connection failed — {error}",
284
+ "login.wizard.retryHint": "Press Escape to skip, or enter a new value:",
276
285
  "plugins.tabs.installed": "Instalados",
277
286
  "plugins.tabs.recommended": "Recomendados",
278
287
  "plugins.tabs.discover": "Descubrir",
@@ -273,6 +273,15 @@
273
273
  "welcome.runLoginReconnect": "Exécutez /login pour vous reconnecter",
274
274
  "welcome.noModelProvider": "Aucun fournisseur de modèle configuré",
275
275
  "welcome.runLoginConnect": "Exécutez /login pour vous connecter",
276
+ "login.wizard.urlPrompt": "Model Provider URL",
277
+ "login.wizard.urlPlaceholder": "https://your-proxy.example.com",
278
+ "login.wizard.apiKeyPrompt": "API Key",
279
+ "login.wizard.apiKeyPlaceholder": "sk-...",
280
+ "login.wizard.connecting": "Connecting to {url}…",
281
+ "login.wizard.success": "Connected — {count} models available",
282
+ "login.wizard.configSaved": "Configuration saved. You're ready to go!",
283
+ "login.wizard.failed": "Connection failed — {error}",
284
+ "login.wizard.retryHint": "Press Escape to skip, or enter a new value:",
276
285
  "plugins.tabs.installed": "Installés",
277
286
  "plugins.tabs.recommended": "Recommandés",
278
287
  "plugins.tabs.discover": "Découvrir",
@@ -273,6 +273,15 @@
273
273
  "welcome.runLoginReconnect": "पुनः कनेक्ट करने के लिए /login चलाएँ",
274
274
  "welcome.noModelProvider": "कोई मॉडल प्रदाता कॉन्फ़िगर नहीं है",
275
275
  "welcome.runLoginConnect": "कनेक्ट करने के लिए /login चलाएँ",
276
+ "login.wizard.urlPrompt": "Model Provider URL",
277
+ "login.wizard.urlPlaceholder": "https://your-proxy.example.com",
278
+ "login.wizard.apiKeyPrompt": "API Key",
279
+ "login.wizard.apiKeyPlaceholder": "sk-...",
280
+ "login.wizard.connecting": "Connecting to {url}…",
281
+ "login.wizard.success": "Connected — {count} models available",
282
+ "login.wizard.configSaved": "Configuration saved. You're ready to go!",
283
+ "login.wizard.failed": "Connection failed — {error}",
284
+ "login.wizard.retryHint": "Press Escape to skip, or enter a new value:",
276
285
  "plugins.tabs.installed": "इंस्टॉल किए गए",
277
286
  "plugins.tabs.recommended": "अनुशंसित",
278
287
  "plugins.tabs.discover": "खोजें",
@@ -273,6 +273,15 @@
273
273
  "welcome.runLoginReconnect": "Esegui /login per riconnetterti",
274
274
  "welcome.noModelProvider": "Nessun provider del modello configurato",
275
275
  "welcome.runLoginConnect": "Esegui /login per connetterti",
276
+ "login.wizard.urlPrompt": "Model Provider URL",
277
+ "login.wizard.urlPlaceholder": "https://your-proxy.example.com",
278
+ "login.wizard.apiKeyPrompt": "API Key",
279
+ "login.wizard.apiKeyPlaceholder": "sk-...",
280
+ "login.wizard.connecting": "Connecting to {url}…",
281
+ "login.wizard.success": "Connected — {count} models available",
282
+ "login.wizard.configSaved": "Configuration saved. You're ready to go!",
283
+ "login.wizard.failed": "Connection failed — {error}",
284
+ "login.wizard.retryHint": "Press Escape to skip, or enter a new value:",
276
285
  "plugins.tabs.installed": "Installati",
277
286
  "plugins.tabs.recommended": "Consigliati",
278
287
  "plugins.tabs.discover": "Scopri",
@@ -273,6 +273,15 @@
273
273
  "welcome.runLoginReconnect": "/login を実行して再接続してください",
274
274
  "welcome.noModelProvider": "モデルプロバイダーが設定されていません",
275
275
  "welcome.runLoginConnect": "/login を実行して接続してください",
276
+ "login.wizard.urlPrompt": "Model Provider URL",
277
+ "login.wizard.urlPlaceholder": "https://your-proxy.example.com",
278
+ "login.wizard.apiKeyPrompt": "API Key",
279
+ "login.wizard.apiKeyPlaceholder": "sk-...",
280
+ "login.wizard.connecting": "Connecting to {url}…",
281
+ "login.wizard.success": "Connected — {count} models available",
282
+ "login.wizard.configSaved": "Configuration saved. You're ready to go!",
283
+ "login.wizard.failed": "Connection failed — {error}",
284
+ "login.wizard.retryHint": "Press Escape to skip, or enter a new value:",
276
285
  "plugins.tabs.installed": "インストール済み",
277
286
  "plugins.tabs.recommended": "推奨",
278
287
  "plugins.tabs.discover": "検索",
@@ -273,6 +273,15 @@
273
273
  "welcome.runLoginReconnect": "/login을 실행하여 재연결하세요",
274
274
  "welcome.noModelProvider": "모델 프로바이더가 설정되지 않았습니다",
275
275
  "welcome.runLoginConnect": "/login을 실행하여 연결하세요",
276
+ "login.wizard.urlPrompt": "Model Provider URL",
277
+ "login.wizard.urlPlaceholder": "https://your-proxy.example.com",
278
+ "login.wizard.apiKeyPrompt": "API Key",
279
+ "login.wizard.apiKeyPlaceholder": "sk-...",
280
+ "login.wizard.connecting": "Connecting to {url}…",
281
+ "login.wizard.success": "Connected — {count} models available",
282
+ "login.wizard.configSaved": "Configuration saved. You're ready to go!",
283
+ "login.wizard.failed": "Connection failed — {error}",
284
+ "login.wizard.retryHint": "Press Escape to skip, or enter a new value:",
276
285
  "plugins.tabs.installed": "설치됨",
277
286
  "plugins.tabs.recommended": "권장",
278
287
  "plugins.tabs.discover": "검색",
@@ -273,6 +273,15 @@
273
273
  "welcome.runLoginReconnect": "Execute /login para reconectar",
274
274
  "welcome.noModelProvider": "Nenhum provedor de modelo configurado",
275
275
  "welcome.runLoginConnect": "Execute /login para conectar",
276
+ "login.wizard.urlPrompt": "Model Provider URL",
277
+ "login.wizard.urlPlaceholder": "https://your-proxy.example.com",
278
+ "login.wizard.apiKeyPrompt": "API Key",
279
+ "login.wizard.apiKeyPlaceholder": "sk-...",
280
+ "login.wizard.connecting": "Connecting to {url}…",
281
+ "login.wizard.success": "Connected — {count} models available",
282
+ "login.wizard.configSaved": "Configuration saved. You're ready to go!",
283
+ "login.wizard.failed": "Connection failed — {error}",
284
+ "login.wizard.retryHint": "Press Escape to skip, or enter a new value:",
276
285
  "plugins.tabs.installed": "Instalados",
277
286
  "plugins.tabs.recommended": "Recomendados",
278
287
  "plugins.tabs.discover": "Descobrir",
@@ -273,6 +273,15 @@
273
273
  "welcome.runLoginReconnect": "รัน /login เพื่อเชื่อมต่อใหม่",
274
274
  "welcome.noModelProvider": "ยังไม่ได้กำหนดค่าผู้ให้บริการโมเดล",
275
275
  "welcome.runLoginConnect": "รัน /login เพื่อเชื่อมต่อ",
276
+ "login.wizard.urlPrompt": "Model Provider URL",
277
+ "login.wizard.urlPlaceholder": "https://your-proxy.example.com",
278
+ "login.wizard.apiKeyPrompt": "API Key",
279
+ "login.wizard.apiKeyPlaceholder": "sk-...",
280
+ "login.wizard.connecting": "Connecting to {url}…",
281
+ "login.wizard.success": "Connected — {count} models available",
282
+ "login.wizard.configSaved": "Configuration saved. You're ready to go!",
283
+ "login.wizard.failed": "Connection failed — {error}",
284
+ "login.wizard.retryHint": "Press Escape to skip, or enter a new value:",
276
285
  "plugins.tabs.installed": "ติดตั้งแล้ว",
277
286
  "plugins.tabs.recommended": "แนะนำ",
278
287
  "plugins.tabs.discover": "ค้นหา",
@@ -273,6 +273,15 @@
273
273
  "welcome.runLoginReconnect": "运行 /login 重新连接",
274
274
  "welcome.noModelProvider": "未配置模型提供商",
275
275
  "welcome.runLoginConnect": "运行 /login 连接",
276
+ "login.wizard.urlPrompt": "Model Provider URL",
277
+ "login.wizard.urlPlaceholder": "https://your-proxy.example.com",
278
+ "login.wizard.apiKeyPrompt": "API Key",
279
+ "login.wizard.apiKeyPlaceholder": "sk-...",
280
+ "login.wizard.connecting": "Connecting to {url}…",
281
+ "login.wizard.success": "Connected — {count} models available",
282
+ "login.wizard.configSaved": "Configuration saved. You're ready to go!",
283
+ "login.wizard.failed": "Connection failed — {error}",
284
+ "login.wizard.retryHint": "Press Escape to skip, or enter a new value:",
276
285
  "plugins.tabs.installed": "已安装",
277
286
  "plugins.tabs.recommended": "推荐",
278
287
  "plugins.tabs.discover": "发现",
@@ -273,6 +273,15 @@
273
273
  "welcome.runLoginReconnect": "執行 /login 重新連線",
274
274
  "welcome.noModelProvider": "未設定模型供應商",
275
275
  "welcome.runLoginConnect": "執行 /login 連線",
276
+ "login.wizard.urlPrompt": "Model Provider URL",
277
+ "login.wizard.urlPlaceholder": "https://your-proxy.example.com",
278
+ "login.wizard.apiKeyPrompt": "API Key",
279
+ "login.wizard.apiKeyPlaceholder": "sk-...",
280
+ "login.wizard.connecting": "Connecting to {url}…",
281
+ "login.wizard.success": "Connected — {count} models available",
282
+ "login.wizard.configSaved": "Configuration saved. You're ready to go!",
283
+ "login.wizard.failed": "Connection failed — {error}",
284
+ "login.wizard.retryHint": "Press Escape to skip, or enter a new value:",
276
285
  "plugins.tabs.installed": "已安裝",
277
286
  "plugins.tabs.recommended": "推薦",
278
287
  "plugins.tabs.discover": "探索",
@@ -5,7 +5,7 @@ import { ThinkingLevel } from "@f5xc-salesdemos/pi-agent-core";
5
5
  import { getOAuthProviders, loginLiteLLM, type OAuthProvider } from "@f5xc-salesdemos/pi-ai";
6
6
  import type { Component } from "@f5xc-salesdemos/pi-tui";
7
7
  import { Input, Loader, Spacer, Text } from "@f5xc-salesdemos/pi-tui";
8
- import { getAgentDbPath, getAgentDir, getConfigDirName, getProjectDir } from "@f5xc-salesdemos/pi-utils";
8
+ import { getAgentDbPath, getAgentDir, getConfigDirName, getProjectDir, t } from "@f5xc-salesdemos/pi-utils";
9
9
  import { invalidate as invalidateFsCache } from "../../capability/fs";
10
10
  import {
11
11
  generateConfigYml,
@@ -1038,6 +1038,169 @@ export class SelectorController {
1038
1038
  }
1039
1039
  }
1040
1040
 
1041
+ async showFirstRunLogin(): Promise<void> {
1042
+ const modelsPath = path.join(getAgentDir(), "models.yml");
1043
+
1044
+ // Prompt helper: renders label + hint + input together in the editor
1045
+ // container so they're always visible below the welcome branding.
1046
+ const promptInput = async (message: string, placeholder?: string): Promise<string | null> => {
1047
+ const { promise, resolve } = Promise.withResolvers<string | null>();
1048
+ const input = new Input();
1049
+ input.onSubmit = () => {
1050
+ const raw = input.getValue();
1051
+ const value = raw.replace(/\x1b\[\d{3}~/g, "").replace(/\x1b[[\]()][^\x1b]*/g, "");
1052
+ this.ctx.editorContainer.clear();
1053
+ this.ctx.editorContainer.addChild(this.ctx.editor);
1054
+ this.ctx.ui.setFocus(this.ctx.editor);
1055
+ resolve(value);
1056
+ };
1057
+ input.onEscape = () => {
1058
+ this.ctx.editorContainer.clear();
1059
+ this.ctx.editorContainer.addChild(this.ctx.editor);
1060
+ this.ctx.ui.setFocus(this.ctx.editor);
1061
+ resolve(null);
1062
+ };
1063
+
1064
+ // Pack label + hint + input into the editor area as a single block
1065
+ this.ctx.editorContainer.clear();
1066
+ this.ctx.editorContainer.addChild(new Spacer(1));
1067
+ this.ctx.editorContainer.addChild(new Text(theme.bold(theme.fg("text", ` ${message}`)), 0, 0));
1068
+ if (placeholder) {
1069
+ this.ctx.editorContainer.addChild(new Text(theme.fg("dim", ` ${placeholder}`), 0, 0));
1070
+ }
1071
+ this.ctx.editorContainer.addChild(new Spacer(1));
1072
+ this.ctx.editorContainer.addChild(input);
1073
+ this.ctx.ui.setFocus(input);
1074
+ this.ctx.ui.requestRender();
1075
+ return promise;
1076
+ };
1077
+
1078
+ try {
1079
+ // Step 1: URL prompt
1080
+ let baseUrl: string | null = null;
1081
+ while (!baseUrl) {
1082
+ const urlInput = await promptInput(t("login.wizard.urlPrompt"), t("login.wizard.urlPlaceholder"));
1083
+ if (urlInput === null) return; // Escape pressed
1084
+ const trimmed = urlInput.trim();
1085
+ if (!trimmed) continue;
1086
+ if (!trimmed.startsWith("http://") && !trimmed.startsWith("https://")) {
1087
+ this.ctx.chatContainer.addChild(
1088
+ new Text(theme.fg("error", "URL must start with http:// or https://"), 1, 0),
1089
+ );
1090
+ this.ctx.ui.requestRender();
1091
+ continue;
1092
+ }
1093
+ baseUrl = trimmed.replace(/\/+$/, "");
1094
+ }
1095
+
1096
+ // Auto-detect known providers by hostname
1097
+ try {
1098
+ const hostname = new URL(baseUrl).hostname.toLowerCase();
1099
+ const providerMap: Record<string, string> = {
1100
+ "api.anthropic.com": "anthropic",
1101
+ "api.openai.com": "openai-codex",
1102
+ "api.together.xyz": "together",
1103
+ };
1104
+ const detectedProvider =
1105
+ providerMap[hostname] ?? (hostname.endsWith(".googleapis.com") ? "google-gemini-cli" : null);
1106
+ if (detectedProvider) {
1107
+ this.ctx.editorContainer.clear();
1108
+ this.ctx.editorContainer.addChild(new Spacer(1));
1109
+ this.ctx.editorContainer.addChild(
1110
+ new Text(theme.fg("dim", `Detected ${detectedProvider} — launching login…`), 1, 0),
1111
+ );
1112
+ this.ctx.ui.requestRender();
1113
+ await this.#handleOAuthLogin(detectedProvider);
1114
+ return;
1115
+ }
1116
+ } catch {
1117
+ // URL parse failed — continue with proxy flow
1118
+ }
1119
+
1120
+ // Step 2: API Key prompt (for non-OAuth proxy)
1121
+ let apiKey: string | null = null;
1122
+ let probeSuccess = false;
1123
+
1124
+ while (!probeSuccess) {
1125
+ if (!apiKey) {
1126
+ const keyInput = await promptInput(t("login.wizard.apiKeyPrompt"), t("login.wizard.apiKeyPlaceholder"));
1127
+ if (keyInput === null) return;
1128
+ const trimmedKey = keyInput.trim();
1129
+ if (!trimmedKey) continue;
1130
+ apiKey = trimmedKey;
1131
+ }
1132
+
1133
+ // Show connection status in editor area
1134
+ this.ctx.editorContainer.clear();
1135
+ this.ctx.editorContainer.addChild(new Spacer(1));
1136
+ this.ctx.editorContainer.addChild(
1137
+ new Text(theme.fg("dim", ` ${t("login.wizard.connecting", { url: baseUrl })}`), 0, 0),
1138
+ );
1139
+ this.ctx.ui.requestRender();
1140
+
1141
+ let probe = await probeLiteLLMConnection(baseUrl, apiKey);
1142
+
1143
+ // Auto-retry once on network errors
1144
+ if (!probe.reachable && probe.error && !/\b(401|403|Unauthorized|Forbidden)\b/i.test(probe.error)) {
1145
+ await new Promise(resolve => setTimeout(resolve, 1000));
1146
+ probe = await probeLiteLLMConnection(baseUrl, apiKey);
1147
+ }
1148
+
1149
+ if (probe.reachable) {
1150
+ // Auto-select best model
1151
+ const preferenceOrder = ["claude-opus", "claude-sonnet", "claude", "gpt-4", "gpt"];
1152
+ let selectedModel: string | undefined;
1153
+ for (const pref of preferenceOrder) {
1154
+ selectedModel = probe.models.find(m => m.toLowerCase().includes(pref));
1155
+ if (selectedModel) break;
1156
+ }
1157
+ if (!selectedModel && probe.models.length > 0) {
1158
+ selectedModel = probe.models[0];
1159
+ }
1160
+
1161
+ // Save config
1162
+ const yml = generateModelsYml(baseUrl, {
1163
+ apiBasePath: probe.apiBasePath,
1164
+ apiKeyLiteral: apiKey,
1165
+ });
1166
+ fs.mkdirSync(path.dirname(modelsPath), { recursive: true, mode: 0o700 });
1167
+ fs.writeFileSync(modelsPath, yml, { mode: 0o600 });
1168
+
1169
+ const configPath = path.join(path.dirname(modelsPath), "config.yml");
1170
+ if (!fs.existsSync(configPath)) {
1171
+ fs.writeFileSync(configPath, generateConfigYml());
1172
+ }
1173
+ healConfigYmlModelRoles(configPath);
1174
+
1175
+ await this.ctx.session.modelRegistry.refresh("online");
1176
+ await this.ctx.refreshWelcomeAfterLogin();
1177
+ probeSuccess = true;
1178
+ } else {
1179
+ const errorMsg = probe.error ?? "connection failed";
1180
+
1181
+ // Classify error and re-prompt the appropriate field
1182
+ const isAuthError = /\b(401|403|Unauthorized|Forbidden)\b/i.test(errorMsg);
1183
+ if (isAuthError) {
1184
+ apiKey = null;
1185
+ } else {
1186
+ const urlRetry = await promptInput(
1187
+ `${theme.status.error} ${t("login.wizard.failed", { error: errorMsg })}\n\n ${t("login.wizard.urlPrompt")}`,
1188
+ baseUrl,
1189
+ );
1190
+ if (urlRetry === null) return;
1191
+ const trimmedUrl = urlRetry.trim();
1192
+ if (trimmedUrl && (trimmedUrl.startsWith("http://") || trimmedUrl.startsWith("https://"))) {
1193
+ baseUrl = trimmedUrl.replace(/\/+$/, "");
1194
+ }
1195
+ apiKey = null;
1196
+ }
1197
+ }
1198
+ }
1199
+ } catch (error: unknown) {
1200
+ this.ctx.showError(`Login failed: ${error instanceof Error ? error.message : String(error)}`);
1201
+ }
1202
+ }
1203
+
1041
1204
  async showOAuthSelector(mode: "login" | "logout", providerId?: string): Promise<void> {
1042
1205
  if (providerId) {
1043
1206
  if (mode === "login") {
@@ -32,6 +32,7 @@ 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/f5xc-context";
35
36
  import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
36
37
  import { HistoryStorage } from "../session/history-storage";
37
38
  import type { SessionContext, SessionManager } from "../session/session-manager";
@@ -336,34 +337,43 @@ export class InteractiveMode implements InteractiveModeContext {
336
337
  this.ui.addChild(new Spacer(1));
337
338
  }
338
339
 
340
+ // When model is not connected, the wizard will auto-launch — show a clean
341
+ // welcome screen without plugins or confusing provider names.
342
+ const needsLogin = welcomeResult.model.state === "no_provider" || welcomeResult.model.state === "auth_error";
343
+ const welcomeModelStatus = needsLogin
344
+ ? { state: "no_provider" as const, provider: undefined }
345
+ : welcomeResult.model;
346
+
339
347
  const services: ServiceStatus[] =
340
- !startupQuiet && welcomeResult.model.state === "connected"
341
- ? [mapContextStatus(welcomeResult.context ?? { state: "no_context" })]
342
- : [];
348
+ !startupQuiet && !needsLogin ? [mapContextStatus(welcomeResult.context ?? { state: "no_context" })] : [];
343
349
 
344
350
  // Build unified plugin list from service status contributions + marketplace
351
+ // Skip when model is not configured — nothing works without a model.
345
352
  const pluginContributions = this.session.extensionRunner?.getAllRegisteredServiceStatuses() ?? [];
346
- const plugins = !startupQuiet ? await buildUnifiedPluginList(pluginContributions).catch(() => []) : [];
353
+ const plugins =
354
+ !startupQuiet && !needsLogin ? await buildUnifiedPluginList(pluginContributions).catch(() => []) : [];
347
355
  this.#currentPlugins = plugins;
348
356
 
349
357
  const fixableServices: FixableService[] = [];
350
- for (const contribution of pluginContributions) {
351
- if (contribution.fix) {
352
- const plugin = plugins.find(p => p.name.toLowerCase() === contribution.name.toLowerCase());
353
- if (plugin && plugin.state === "unauthenticated") {
354
- fixableServices.push({
355
- name: contribution.name,
356
- prompt: contribution.fix.prompt,
357
- command: contribution.fix.command,
358
- recheck: async () => {
359
- try {
360
- const result = await contribution.check();
361
- return { name: contribution.name, ...result };
362
- } catch {
363
- return { name: contribution.name, state: "unavailable" as const, hint: "recheck failed" };
364
- }
365
- },
366
- });
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
+ }
367
377
  }
368
378
  }
369
379
  }
@@ -371,7 +381,7 @@ export class InteractiveMode implements InteractiveModeContext {
371
381
  if (!startupQuiet) {
372
382
  this.#welcomeComponent = new WelcomeComponent(
373
383
  this.#version,
374
- welcomeResult.model,
384
+ welcomeModelStatus,
375
385
  services,
376
386
  this.#initialUpdateStatus,
377
387
  [],
@@ -395,6 +405,11 @@ export class InteractiveMode implements InteractiveModeContext {
395
405
  this.ui.addChild(this.hookWidgetContainerBelow);
396
406
  this.ui.setFocus(this.editor);
397
407
 
408
+ // Auto-launch login wizard when model provider is missing or unreachable
409
+ if (needsLogin) {
410
+ queueMicrotask(() => void this.#selectorController.showFirstRunLogin());
411
+ }
412
+
398
413
  this.#inputController.setupKeyHandlers();
399
414
  this.#inputController.setupEditorSubmitHandler();
400
415
 
@@ -1449,6 +1464,49 @@ export class InteractiveMode implements InteractiveModeContext {
1449
1464
  this.#observerRegistry.setMainSession(this.sessionManager.getSessionFile() ?? undefined);
1450
1465
  }
1451
1466
 
1467
+ async refreshWelcomeAfterLogin(): Promise<void> {
1468
+ this.#welcomeComponent?.setModelStatus({ state: "connected", provider: "anthropic" });
1469
+
1470
+ // Validate F5 XC Context independently — call validateToken() for a live
1471
+ // check instead of reading cached state (which may be "unknown" if
1472
+ // validation hasn't run yet in this session).
1473
+ const services: ServiceStatus[] = [];
1474
+ try {
1475
+ const ctxService = ContextService.instance;
1476
+ const ctxStatus = ctxService.getStatus();
1477
+ if (ctxStatus.isConfigured) {
1478
+ const name = ctxStatus.activeContextTenant ?? ctxStatus.activeContextName ?? undefined;
1479
+ const result = await ctxService.validateToken({ timeoutMs: 5000 });
1480
+ services.push(
1481
+ mapContextStatus({
1482
+ state:
1483
+ result.status === "connected"
1484
+ ? "connected"
1485
+ : result.status === "auth_error"
1486
+ ? "auth_error"
1487
+ : "offline",
1488
+ name,
1489
+ }),
1490
+ );
1491
+ }
1492
+ } catch {
1493
+ // ContextService not initialized — skip
1494
+ }
1495
+ this.#welcomeComponent?.setServices(services);
1496
+
1497
+ // Run plugin checks and update the welcome screen
1498
+ const pluginContributions = this.session.extensionRunner?.getAllRegisteredServiceStatuses() ?? [];
1499
+ const plugins = await buildUnifiedPluginList(pluginContributions).catch(() => []);
1500
+ this.#currentPlugins = plugins;
1501
+ this.#welcomeComponent?.setPlugins(plugins);
1502
+
1503
+ // Restore editor
1504
+ this.editorContainer.clear();
1505
+ this.editorContainer.addChild(this.editor);
1506
+ this.ui.setFocus(this.editor);
1507
+ this.ui.requestRender();
1508
+ }
1509
+
1452
1510
  handleBashCommand(command: string, excludeFromContext?: boolean): Promise<void> {
1453
1511
  return this.#commandController.handleBashCommand(command, excludeFromContext);
1454
1512
  }
@@ -213,6 +213,7 @@ export interface InteractiveModeContext {
213
213
  showDebugSelector(): void;
214
214
  showSessionObserver(): void;
215
215
  resetObserverRegistry(): void;
216
+ refreshWelcomeAfterLogin(): Promise<void>;
216
217
 
217
218
  // Input handling
218
219
  handleCtrlC(): void;