@f5xc-salesdemos/xcsh 18.5.5 → 18.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@f5xc-salesdemos/xcsh",
4
- "version": "18.5.5",
4
+ "version": "18.7.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",
@@ -47,12 +47,12 @@
47
47
  "dependencies": {
48
48
  "@agentclientprotocol/sdk": "0.16.1",
49
49
  "@mozilla/readability": "^0.6",
50
- "@f5xc-salesdemos/xcsh-stats": "18.5.5",
51
- "@f5xc-salesdemos/pi-agent-core": "18.5.5",
52
- "@f5xc-salesdemos/pi-ai": "18.5.5",
53
- "@f5xc-salesdemos/pi-natives": "18.5.5",
54
- "@f5xc-salesdemos/pi-tui": "18.5.5",
55
- "@f5xc-salesdemos/pi-utils": "18.5.5",
50
+ "@f5xc-salesdemos/xcsh-stats": "18.7.0",
51
+ "@f5xc-salesdemos/pi-agent-core": "18.7.0",
52
+ "@f5xc-salesdemos/pi-ai": "18.7.0",
53
+ "@f5xc-salesdemos/pi-natives": "18.7.0",
54
+ "@f5xc-salesdemos/pi-tui": "18.7.0",
55
+ "@f5xc-salesdemos/pi-utils": "18.7.0",
56
56
  "@sinclair/typebox": "^0.34",
57
57
  "@xterm/headless": "^6.0",
58
58
  "ajv": "^8.18",
@@ -263,6 +263,18 @@ export const SETTINGS_SCHEMA = {
263
263
  },
264
264
  },
265
265
 
266
+ "theme.forceSlot": {
267
+ type: "enum",
268
+ values: ["auto", "dark", "light"] as const,
269
+ default: "auto",
270
+ ui: {
271
+ tab: "appearance",
272
+ label: "Force Theme Slot",
273
+ description: "Override auto dark/light detection (useful inside tmux where OSC 11 is unreliable)",
274
+ submenu: true,
275
+ },
276
+ },
277
+
266
278
  symbolPreset: {
267
279
  type: "enum",
268
280
  values: ["unicode", "nerd", "ascii"] as const,
@@ -26,7 +26,13 @@ import { YAML } from "bun";
26
26
  import { type Settings as SettingsCapabilityItem, settingsCapability } from "../capability/settings";
27
27
  import type { ModelRole } from "../config/model-registry";
28
28
  import { loadCapability } from "../discovery";
29
- import { isLightTheme, setAutoThemeMapping, setColorBlindMode, setSymbolPreset } from "../modes/theme/theme";
29
+ import {
30
+ isLightTheme,
31
+ setAutoThemeMapping,
32
+ setColorBlindMode,
33
+ setForceSlot,
34
+ setSymbolPreset,
35
+ } from "../modes/theme/theme";
30
36
  import { AgentStorage } from "../session/agent-storage";
31
37
  import { type EditMode, normalizeEditMode } from "../utils/edit-mode";
32
38
  import { withFileLock } from "./file-lock";
@@ -649,6 +655,11 @@ const SETTING_HOOKS: Partial<Record<SettingPath, SettingHook<any>>> = {
649
655
  setAutoThemeMapping("light", value);
650
656
  }
651
657
  },
658
+ "theme.forceSlot": value => {
659
+ if (value === "auto" || value === "dark" || value === "light") {
660
+ setForceSlot(value);
661
+ }
662
+ },
652
663
  symbolPreset: value => {
653
664
  if (typeof value === "string" && (value === "unicode" || value === "nerd" || value === "ascii")) {
654
665
  setSymbolPreset(value).catch(err => {
@@ -110,6 +110,16 @@ export function renderAboutDoc(info: RuntimeBuildInfo): string {
110
110
  `- This commit on GitHub: ${info.commitUrl}`,
111
111
  `- Release for this version: ${info.releaseUrl}`,
112
112
  "",
113
+ "## Product knowledge",
114
+ "",
115
+ "xcsh serves F5 Distributed Cloud sales engineers. Product documentation is",
116
+ "federated across the f5xc-salesdemos GitHub organization. Entry point:",
117
+ "https://f5xc-salesdemos.github.io/docs/llms.txt",
118
+ "",
119
+ "Each product repo publishes: llms.txt (index with sidebar nav), custom sets",
120
+ "at /_llms-txt/{topic}.txt, per-page content at /{slug}.md, plus",
121
+ "llms-small.txt (compact) and llms-full.txt (complete).",
122
+ "",
113
123
  "## What to do when asked about xcsh itself",
114
124
  "",
115
125
  "1. Confirm the user is running the version above. If unsure, ask them to run `xcsh --version`.",
@@ -17,17 +17,17 @@ export interface BuildInfo {
17
17
  }
18
18
 
19
19
  export const BUILD_INFO: BuildInfo = {
20
- "version": "18.5.5",
21
- "commit": "8a2e95fbb3e190549a9381ebd927013366f02fb6",
22
- "shortCommit": "8a2e95f",
20
+ "version": "18.7.0",
21
+ "commit": "134e551dc4f0c8c481931d64c92d6c2f704aad9e",
22
+ "shortCommit": "134e551",
23
23
  "branch": "main",
24
- "tag": "v18.5.5",
25
- "commitDate": "2026-04-22T05:35:04Z",
26
- "buildDate": "2026-04-22T05:57:25.741Z",
24
+ "tag": "v18.7.0",
25
+ "commitDate": "2026-04-22T03:10:13-04:00",
26
+ "buildDate": "2026-04-22T07:28:38.519Z",
27
27
  "dirty": false,
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/8a2e95fbb3e190549a9381ebd927013366f02fb6",
32
- "releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v18.5.5"
31
+ "commitUrl": "https://github.com/f5xc-salesdemos/xcsh/commit/134e551dc4f0c8c481931d64c92d6c2f704aad9e",
32
+ "releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v18.7.0"
33
33
  };
package/src/main.ts CHANGED
@@ -704,6 +704,7 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
704
704
  settings.get("colorBlindMode"),
705
705
  settings.get("theme.dark"),
706
706
  settings.get("theme.light"),
707
+ settings.get("theme.forceSlot"),
707
708
  );
708
709
 
709
710
  let scopedModels: ScopedModel[] = [];
@@ -1,9 +1,44 @@
1
1
  import type { Model } from "@f5xc-salesdemos/pi-ai";
2
2
  import { validateApiKeyAgainstModelsEndpoint } from "@f5xc-salesdemos/pi-ai/utils/oauth/api-key-validation";
3
3
  import { logger } from "@f5xc-salesdemos/pi-utils";
4
- import { ProfileService } from "../../services/f5xc-profile";
4
+ import { type AuthStatus, ProfileService } from "../../services/f5xc-profile";
5
5
  import type { AuthStorage } from "../../session/auth-storage";
6
6
 
7
+ // Startup validation budget. These are longer than validateToken's 3000ms default because
8
+ // the welcome path runs during TLS/DNS cold-start — a single 3s shot races against warm-up
9
+ // and falsely reports offline for profiles that reconnect cleanly moments later.
10
+ const STARTUP_FIRST_TIMEOUT_MS = 4000;
11
+ const STARTUP_RETRY_TIMEOUT_MS = 5000;
12
+ const STARTUP_RETRY_DELAY_MS = 500;
13
+
14
+ type ProfileValidator = (opts: { timeoutMs: number }) => Promise<{ status: AuthStatus; latencyMs?: number }>;
15
+
16
+ /**
17
+ * Runs the profile validator once with a startup-sized timeout; if the result is `offline`
18
+ * (the only transient class — auth_error/connected/unknown are definitive), waits briefly
19
+ * to let DNS/TLS warm up, then tries once more with a longer timeout.
20
+ */
21
+ export async function validateProfileWithStartupRetry(
22
+ validate: ProfileValidator,
23
+ options?: {
24
+ firstTimeoutMs?: number;
25
+ retryTimeoutMs?: number;
26
+ retryDelayMs?: number;
27
+ },
28
+ ): Promise<{ status: AuthStatus; latencyMs?: number }> {
29
+ const firstTimeoutMs = options?.firstTimeoutMs ?? STARTUP_FIRST_TIMEOUT_MS;
30
+ const retryTimeoutMs = options?.retryTimeoutMs ?? STARTUP_RETRY_TIMEOUT_MS;
31
+ const retryDelayMs = options?.retryDelayMs ?? STARTUP_RETRY_DELAY_MS;
32
+
33
+ const first = await validate({ timeoutMs: firstTimeoutMs });
34
+ if (first.status !== "offline") return first;
35
+
36
+ if (retryDelayMs > 0) {
37
+ await new Promise(resolve => setTimeout(resolve, retryDelayMs));
38
+ }
39
+ return await validate({ timeoutMs: retryTimeoutMs });
40
+ }
41
+
7
42
  export type ModelCheckState = "no_provider" | "connected" | "auth_error";
8
43
 
9
44
  export interface ModelStatus {
@@ -117,7 +152,7 @@ async function checkProfileStatus(): Promise<WelcomeProfileStatus> {
117
152
  }
118
153
 
119
154
  const name = status.activeProfileName ?? "default";
120
- const result = await profileService.validateToken();
155
+ const result = await validateProfileWithStartupRetry(opts => profileService.validateToken(opts));
121
156
 
122
157
  switch (result.status) {
123
158
  case "connected":
@@ -1,6 +1,7 @@
1
1
  import { type Component, padding, truncateToWidth, visibleWidth } from "@f5xc-salesdemos/pi-tui";
2
2
  import { APP_NAME } from "@f5xc-salesdemos/pi-utils";
3
3
  import { theme } from "../../modes/theme/theme";
4
+ import { formatStatusIcon } from "../../services/f5xc-profile-indicators";
4
5
  import type { ModelStatus, WelcomeProfileStatus } from "./welcome-checks";
5
6
 
6
7
  export interface UpdateStatus {
@@ -200,15 +201,17 @@ export class WelcomeComponent implements Component {
200
201
  const p = provider ?? "unknown";
201
202
  switch (state) {
202
203
  case "connected":
203
- return [` ✅ ${theme.fg("muted", p)} ${theme.fg("dim", `\u2014 connected (${latencyMs ?? "?"}ms)`)}`];
204
+ return [
205
+ ` ${formatStatusIcon("connected")} ${theme.fg("muted", p)} ${theme.fg("dim", `\u2014 connected (${latencyMs ?? "?"}ms)`)}`,
206
+ ];
204
207
  case "auth_error":
205
208
  return [
206
- ` ${theme.fg("muted", p)} ${theme.fg("error", "\u2014 connection failed")}`,
209
+ ` ${formatStatusIcon("error")} ${theme.fg("muted", p)} ${theme.fg("error", "\u2014 connection failed")}`,
207
210
  ` ${theme.fg("dim", "Run /login to reconnect")}`,
208
211
  ];
209
212
  case "no_provider":
210
213
  return [
211
- ` ${theme.fg("error", "No model provider configured")}`,
214
+ ` ${formatStatusIcon("error")} ${theme.fg("error", "No model provider configured")}`,
212
215
  ` ${theme.fg("dim", "Run /login to connect")}`,
213
216
  ];
214
217
  }
@@ -220,20 +223,22 @@ export class WelcomeComponent implements Component {
220
223
  const n = name ?? "default";
221
224
  switch (state) {
222
225
  case "connected":
223
- return [` ✅ ${theme.fg("muted", n)} ${theme.fg("dim", `\u2014 connected (${latencyMs ?? "?"}ms)`)}`];
226
+ return [
227
+ ` ${formatStatusIcon("connected")} ${theme.fg("muted", n)} ${theme.fg("dim", `\u2014 connected (${latencyMs ?? "?"}ms)`)}`,
228
+ ];
224
229
  case "auth_error":
225
230
  return [
226
- ` ${theme.fg("muted", n)} ${theme.fg("error", "\u2014 token invalid")}`,
231
+ ` ${formatStatusIcon("error")} ${theme.fg("muted", n)} ${theme.fg("error", "\u2014 token invalid")}`,
227
232
  ` ${theme.fg("dim", "Run /profile to update")}`,
228
233
  ];
229
234
  case "offline":
230
235
  return [
231
- ` ⚠️ ${theme.fg("muted", n)} ${theme.fg("warning", "\u2014 unreachable")}`,
236
+ ` ${formatStatusIcon("warning")} ${theme.fg("muted", n)} ${theme.fg("warning", "\u2014 unreachable")}`,
232
237
  ` ${theme.fg("dim", "Check network, /profile")}`,
233
238
  ];
234
239
  case "no_profile":
235
240
  return [
236
- ` ⚠️ ${theme.fg("warning", "No profile configured")}`,
241
+ ` ${formatStatusIcon("warning")} ${theme.fg("warning", "No profile configured")}`,
237
242
  ` ${theme.fg("dim", "Run /profile create <name> <url> <token>")}`,
238
243
  ];
239
244
  }
@@ -105,7 +105,7 @@
105
105
  "statusLineContextPctWarningBg": 22,
106
106
  "statusLineContextPctPurpleBg": 94,
107
107
  "statusLineContextPctErrorBg": 88,
108
- "statusLineProfileF5xcBg": 52,
108
+ "statusLineProfileF5xcBg": "f5Red",
109
109
  "statusLineProfileF5xcFg": 231,
110
110
  "pythonMode": "#f0c040",
111
111
  "syntaxControl": "#569CD6"
@@ -107,7 +107,7 @@
107
107
  "statusLineContextPctWarningBg": 22,
108
108
  "statusLineContextPctPurpleBg": 94,
109
109
  "statusLineContextPctErrorBg": 88,
110
- "statusLineProfileF5xcBg": 52,
110
+ "statusLineProfileF5xcBg": "f5Red",
111
111
  "statusLineProfileF5xcFg": 231,
112
112
  "pythonMode": "warningAmber",
113
113
  "syntaxControl": "f5Red"
@@ -1894,9 +1894,33 @@ function detectTerminalBackground(): "dark" | "light" {
1894
1894
  return "dark";
1895
1895
  }
1896
1896
 
1897
+ /**
1898
+ * Theme-slot override mode. `"auto"` uses terminal-appearance detection to
1899
+ * pick between the dark and light slots. `"dark"` / `"light"` skip detection
1900
+ * and always load that slot — useful when running inside tmux where OSC 11
1901
+ * queries and `COLORFGBG` don't reliably surface (see issue #228).
1902
+ */
1903
+ export type ThemeForceSlot = "auto" | "dark" | "light";
1904
+
1905
+ /**
1906
+ * Pure slot-resolution helper. Exported so it can be unit-tested without a
1907
+ * terminal. When `forceSlot` is `"dark"` or `"light"`, returns that slot's
1908
+ * theme name unconditionally. Otherwise maps `detected` bg to the matching
1909
+ * slot.
1910
+ */
1911
+ export function resolveThemeSlot(
1912
+ forceSlot: ThemeForceSlot,
1913
+ detected: "dark" | "light",
1914
+ darkTheme: string,
1915
+ lightTheme: string,
1916
+ ): string {
1917
+ if (forceSlot === "dark") return darkTheme;
1918
+ if (forceSlot === "light") return lightTheme;
1919
+ return detected === "light" ? lightTheme : darkTheme;
1920
+ }
1921
+
1897
1922
  function getDefaultTheme(): string {
1898
- const bg = detectTerminalBackground();
1899
- return bg === "light" ? autoLightTheme : autoDarkTheme;
1923
+ return resolveThemeSlot(currentForceSlot, detectTerminalBackground(), autoDarkTheme, autoLightTheme);
1900
1924
  }
1901
1925
 
1902
1926
  // ============================================================================
@@ -1918,6 +1942,7 @@ var sigwinchHandler: (() => void) | undefined;
1918
1942
  var autoDetectedTheme: boolean = false;
1919
1943
  var autoDarkTheme: string = "xcsh-dark";
1920
1944
  var autoLightTheme: string = "xcsh-light";
1945
+ var currentForceSlot: ThemeForceSlot = "auto";
1921
1946
  var onThemeChangeCallback: (() => void) | undefined;
1922
1947
  var themeLoadRequestId: number = 0;
1923
1948
 
@@ -1934,10 +1959,12 @@ export async function initTheme(
1934
1959
  colorBlindMode?: boolean,
1935
1960
  darkTheme?: string,
1936
1961
  lightTheme?: string,
1962
+ forceSlot?: ThemeForceSlot,
1937
1963
  ): Promise<void> {
1938
1964
  autoDetectedTheme = true;
1939
1965
  autoDarkTheme = darkTheme ?? "xcsh-dark";
1940
1966
  autoLightTheme = lightTheme ?? "xcsh-light";
1967
+ currentForceSlot = forceSlot ?? "auto";
1941
1968
  const name = getDefaultTheme();
1942
1969
  currentThemeName = name;
1943
1970
  currentSymbolPresetOverride = symbolPreset;
@@ -2032,6 +2059,19 @@ export function setAutoThemeMapping(mode: "dark" | "light", themeName: string):
2032
2059
  reevaluateAutoTheme("setAutoThemeMapping");
2033
2060
  }
2034
2061
 
2062
+ /**
2063
+ * Update the forced-slot override. Callers pass `"auto"` to restore the
2064
+ * detection chain, or `"dark"` / `"light"` to skip detection entirely and
2065
+ * always load that slot. Re-evaluates the active theme immediately.
2066
+ *
2067
+ * Primary use case: tmux sessions where OSC 11 and COLORFGBG don't surface
2068
+ * a reliable bg signal (issue #228).
2069
+ */
2070
+ export function setForceSlot(slot: ThemeForceSlot): void {
2071
+ currentForceSlot = slot;
2072
+ reevaluateAutoTheme("setForceSlot");
2073
+ }
2074
+
2035
2075
  /**
2036
2076
  * Called when the terminal detects a dark/light appearance change.
2037
2077
  * The terminal layer queries OSC 11 (background color) and computes luminance;
@@ -189,6 +189,18 @@ Most tools resolve custom protocol URLs to internal resources (not web URLs):
189
189
 
190
190
  In `bash`, URIs auto-resolve to filesystem paths (e.g., `python skill://my-skill/scripts/init.py`).
191
191
 
192
+ # Product knowledge
193
+
194
+ For F5 Distributed Cloud product questions (capabilities, demos, APIs, configuration),
195
+ you **MUST** start at the live knowledge index:
196
+
197
+ `https://f5xc-salesdemos.github.io/docs/llms.txt`
198
+
199
+ Follow links from there to the specific product's own `llms.txt`, then fetch only the
200
+ tier you need: a custom set (`/_llms-txt/{topic}.txt`), a single page (`/{slug}.md`),
201
+ or `llms-small.txt` / `llms-full.txt` when breadth is required. Content is live —
202
+ never assume a cached snapshot is current.
203
+
192
204
  # Skills
193
205
 
194
206
  Specialized knowledge packs loaded for this session. Relative paths in skill files resolve against the skill directory.
@@ -0,0 +1,16 @@
1
+ import { theme } from "../modes/theme/theme";
2
+
3
+ export type StatusCategory = "connected" | "error" | "warning" | "unknown";
4
+
5
+ export function formatStatusIcon(status: StatusCategory): string {
6
+ switch (status) {
7
+ case "connected":
8
+ return theme.fg("success", "●");
9
+ case "error":
10
+ return theme.fg("error", "○");
11
+ case "warning":
12
+ return theme.fg("warning", "⚠");
13
+ case "unknown":
14
+ return theme.fg("dim", "○");
15
+ }
16
+ }
@@ -1,9 +1,8 @@
1
1
  import type { AuthStatus } from "./f5xc-profile";
2
+ import { formatStatusIcon } from "./f5xc-profile-indicators";
2
3
 
3
4
  // F5 Brand Red — same as welcome.ts line 203
4
5
  const F5_RED = "\x1b[38;5;160m";
5
- const GREEN = "\x1b[38;5;34m";
6
- const RED_TEXT = "\x1b[38;5;196m";
7
6
  const RESET = "\x1b[0m";
8
7
  const BOLD = "\x1b[1m";
9
8
 
@@ -25,13 +24,13 @@ export function formatAuthIndicator(status: AuthStatus, latencyMs?: number): str
25
24
  const ms = latencyMs !== undefined ? ` (${latencyMs}ms)` : "";
26
25
  switch (status) {
27
26
  case "connected":
28
- return `${GREEN}\u25CF${RESET} Connected${ms}`;
27
+ return `${formatStatusIcon("connected")} Connected${ms}`;
29
28
  case "auth_error":
30
- return `${RED_TEXT}\u25CB${RESET} Auth Error${ms}`;
29
+ return `${formatStatusIcon("error")} Auth Error${ms}`;
31
30
  case "offline":
32
- return `${RED_TEXT}\u25CB${RESET} Offline`;
31
+ return `${formatStatusIcon("warning")} Offline`;
33
32
  default:
34
- return `\u25CB Unknown`;
33
+ return `${formatStatusIcon("unknown")} Unknown`;
35
34
  }
36
35
  }
37
36