@f5xc-salesdemos/xcsh 18.16.0 → 18.18.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/CHANGELOG.md +6 -0
- package/package.json +7 -7
- package/src/config/settings-schema.ts +2 -2
- package/src/internal-urls/build-info-runtime.ts +18 -18
- package/src/internal-urls/build-info.generated.ts +8 -8
- package/src/internal-urls/xcsh-protocol.ts +7 -7
- package/src/main.ts +5 -5
- package/src/modes/components/status-line/presets.ts +8 -8
- package/src/modes/components/status-line/segments.ts +6 -6
- package/src/modes/components/status-line-segment-editor.ts +1 -1
- package/src/modes/components/welcome-checks.ts +25 -25
- package/src/modes/components/welcome.ts +18 -18
- package/src/modes/interactive-mode.ts +3 -3
- package/src/modes/theme/defaults/xcsh-dark.json +2 -2
- package/src/modes/theme/defaults/xcsh-light.json +2 -2
- package/src/modes/theme/theme-schema.json +6 -6
- package/src/modes/theme/theme.ts +6 -6
- package/src/prompts/system/custom-system-prompt.md +5 -4
- package/src/prompts/system/system-prompt.md +5 -4
- package/src/sdk.ts +73 -73
- package/src/services/f5xc-context-command.ts +610 -0
- package/src/services/f5xc-context-display.ts +15 -0
- package/src/services/{f5xc-profile-indicators.ts → f5xc-context-indicators.ts} +1 -1
- package/src/services/f5xc-context-segment.ts +23 -0
- package/src/services/f5xc-context.ts +1115 -0
- package/src/services/f5xc-env.ts +3 -3
- package/src/services/f5xc-table.ts +2 -2
- package/src/session/agent-session.ts +1 -1
- package/src/session/session-manager.ts +26 -26
- package/src/slash-commands/builtin-registry.ts +143 -24
- package/src/system-prompt.ts +4 -4
- package/src/services/f5xc-profile-command.ts +0 -429
- package/src/services/f5xc-profile-display.ts +0 -15
- package/src/services/f5xc-profile-segment.ts +0 -23
- package/src/services/f5xc-profile.ts +0 -728
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.18.0] - 2026-04-25
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Renamed F5 XC credential system from "profile" to "context" to align with kubectl conventions. The `/profile` command is now `/context`, all types/classes use `Context*` naming (`ContextService`, `ContextStatus`, `F5XCContext`, etc.), on-disk paths changed from `profiles/` to `contexts/` and `active_profile` to `active_context`, and the status-line segment ID is now `context_f5xc`. ([#302](https://github.com/f5xc-salesdemos/xcsh/issues/302))
|
|
10
|
+
|
|
5
11
|
## [18.12.0] - 2026-04-23
|
|
6
12
|
|
|
7
13
|
### Changed
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5xc-salesdemos/xcsh",
|
|
4
|
-
"version": "18.
|
|
4
|
+
"version": "18.18.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.
|
|
51
|
-
"@f5xc-salesdemos/pi-agent-core": "18.
|
|
52
|
-
"@f5xc-salesdemos/pi-ai": "18.
|
|
53
|
-
"@f5xc-salesdemos/pi-natives": "18.
|
|
54
|
-
"@f5xc-salesdemos/pi-tui": "18.
|
|
55
|
-
"@f5xc-salesdemos/pi-utils": "18.
|
|
50
|
+
"@f5xc-salesdemos/xcsh-stats": "18.18.0",
|
|
51
|
+
"@f5xc-salesdemos/pi-agent-core": "18.18.0",
|
|
52
|
+
"@f5xc-salesdemos/pi-ai": "18.18.0",
|
|
53
|
+
"@f5xc-salesdemos/pi-natives": "18.18.0",
|
|
54
|
+
"@f5xc-salesdemos/pi-tui": "18.18.0",
|
|
55
|
+
"@f5xc-salesdemos/pi-utils": "18.18.0",
|
|
56
56
|
"@sinclair/typebox": "^0.34",
|
|
57
57
|
"@xterm/headless": "^6.0",
|
|
58
58
|
"ajv": "^8.18",
|
|
@@ -76,7 +76,7 @@ export type StatusLineSegmentId =
|
|
|
76
76
|
| "cache_read"
|
|
77
77
|
| "cache_write"
|
|
78
78
|
| "session_name"
|
|
79
|
-
| "
|
|
79
|
+
| "context_f5xc"
|
|
80
80
|
| "os_icon";
|
|
81
81
|
|
|
82
82
|
interface UiMetadata {
|
|
@@ -1763,7 +1763,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
1763
1763
|
// Fork-specific settings (xcsh / f5xc)
|
|
1764
1764
|
// ────────────────────────────────────────────────────────────────────────
|
|
1765
1765
|
|
|
1766
|
-
/** Per-session environment variables injected into bash (used by f5xc
|
|
1766
|
+
/** Per-session environment variables injected into bash (used by f5xc context system) */
|
|
1767
1767
|
"bash.environment": { type: "record", default: {} as Record<string, string> },
|
|
1768
1768
|
|
|
1769
1769
|
/** Clear terminal on startup */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { $ } from "bun";
|
|
4
|
-
import type {
|
|
4
|
+
import type { ContextStatus } from "../services/f5xc-context";
|
|
5
5
|
import { BUILD_INFO, type BuildInfo } from "./build-info.generated";
|
|
6
6
|
|
|
7
7
|
export type BuildInfoSource = "compiled" | "live-git" | "embedded-fallback";
|
|
@@ -104,46 +104,46 @@ export function formatRelativeTime(epochMs: number, nowMs: number): string {
|
|
|
104
104
|
return `${days} day${days === 1 ? "" : "s"} ago`;
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
function renderAuthStatusLine(
|
|
108
|
-
const base = `**Auth Status:** ${
|
|
109
|
-
if (
|
|
107
|
+
function renderAuthStatusLine(context: ContextStatus, nowMs: number): string {
|
|
108
|
+
const base = `**Auth Status:** ${context.authStatus}`;
|
|
109
|
+
if (context.authLatencyMs === undefined || context.authCheckedAt === undefined) {
|
|
110
110
|
return base;
|
|
111
111
|
}
|
|
112
|
-
const checked = formatRelativeTime(
|
|
113
|
-
return `${base} (latency: ${
|
|
112
|
+
const checked = formatRelativeTime(context.authCheckedAt, nowMs);
|
|
113
|
+
return `${base} (latency: ${context.authLatencyMs}ms, checked: ${checked})`;
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
function renderPlatformContext(
|
|
117
|
-
// xcsh can be connected via a named
|
|
118
|
-
// In the env-only case,
|
|
116
|
+
function renderPlatformContext(context: ContextStatus | null, nowMs: number): string {
|
|
117
|
+
// xcsh can be connected via a named context OR via F5XC_API_URL / F5XC_API_TOKEN env vars.
|
|
118
|
+
// In the env-only case, activeContextName is null but activeContextTenant (derived from the
|
|
119
119
|
// env URL) and credentialSource ("environment") are still populated. Guard on tenant, not
|
|
120
120
|
// name, so env-backed deployments see the configured state instead of the unconfigured copy.
|
|
121
|
-
if (!
|
|
121
|
+
if (!context?.isConfigured || !context.activeContextTenant) {
|
|
122
122
|
return [
|
|
123
123
|
"## Current Platform Context",
|
|
124
124
|
"",
|
|
125
|
-
"No F5 XC
|
|
125
|
+
"No F5 XC context active. Run `/context create` or `/context activate` to connect.",
|
|
126
126
|
"",
|
|
127
127
|
].join("\n");
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
const authLine = renderAuthStatusLine(
|
|
131
|
-
const credentialLine = `**Credential Source:** ${
|
|
132
|
-
|
|
130
|
+
const authLine = renderAuthStatusLine(context, nowMs);
|
|
131
|
+
const credentialLine = `**Credential Source:** ${context.credentialSource}${
|
|
132
|
+
context.credentialSource === "context" && context.activeContextName ? ` (name: ${context.activeContextName})` : ""
|
|
133
133
|
}`;
|
|
134
134
|
|
|
135
135
|
return [
|
|
136
136
|
"## Current Platform Context",
|
|
137
137
|
"",
|
|
138
|
-
`- **Tenant:** ${
|
|
139
|
-
`- **Namespace:** ${
|
|
138
|
+
`- **Tenant:** ${context.activeContextTenant}`,
|
|
139
|
+
`- **Namespace:** ${context.activeContextNamespace ?? "default"}`,
|
|
140
140
|
`- ${authLine}`,
|
|
141
141
|
`- ${credentialLine}`,
|
|
142
142
|
"",
|
|
143
143
|
].join("\n");
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
export function renderAboutDoc(info: RuntimeBuildInfo,
|
|
146
|
+
export function renderAboutDoc(info: RuntimeBuildInfo, context: ContextStatus | null): string {
|
|
147
147
|
return [
|
|
148
148
|
"# xcsh — identity and build fingerprint",
|
|
149
149
|
"",
|
|
@@ -163,7 +163,7 @@ export function renderAboutDoc(info: RuntimeBuildInfo, profile: ProfileStatus |
|
|
|
163
163
|
`- PR that shipped this version: ${info.prNumber ? `#${info.prNumber}` : "unknown (resolve via gh if needed)"}`,
|
|
164
164
|
`- Provenance source: \`${info.source}\` (resolved at ${info.resolvedAt})`,
|
|
165
165
|
"",
|
|
166
|
-
renderPlatformContext(
|
|
166
|
+
renderPlatformContext(context, Date.now()),
|
|
167
167
|
"## Source of truth",
|
|
168
168
|
"",
|
|
169
169
|
`- Repository: ${info.repoUrl}`,
|
|
@@ -17,17 +17,17 @@ export interface BuildInfo {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export const BUILD_INFO: BuildInfo = {
|
|
20
|
-
"version": "18.
|
|
21
|
-
"commit": "
|
|
22
|
-
"shortCommit": "
|
|
20
|
+
"version": "18.18.0",
|
|
21
|
+
"commit": "e903ec539e8c8079df8aa50a8917c5ea2aa4dfe0",
|
|
22
|
+
"shortCommit": "e903ec5",
|
|
23
23
|
"branch": "main",
|
|
24
|
-
"tag": "v18.
|
|
25
|
-
"commitDate": "2026-04-
|
|
26
|
-
"buildDate": "2026-04-
|
|
24
|
+
"tag": "v18.18.0",
|
|
25
|
+
"commitDate": "2026-04-25T06:12:23Z",
|
|
26
|
+
"buildDate": "2026-04-25T06:32:00.724Z",
|
|
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/
|
|
32
|
-
"releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v18.
|
|
31
|
+
"commitUrl": "https://github.com/f5xc-salesdemos/xcsh/commit/e903ec539e8c8079df8aa50a8917c5ea2aa4dfe0",
|
|
32
|
+
"releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v18.18.0"
|
|
33
33
|
};
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* - xcsh://about - Identity fingerprint (version, commit, branch, repo)
|
|
11
11
|
*/
|
|
12
12
|
import * as path from "node:path";
|
|
13
|
-
import type {
|
|
13
|
+
import type { ContextStatus } from "../services/f5xc-context";
|
|
14
14
|
import { getRuntimeBuildInfo, type RuntimeBuildInfo, renderAboutDoc } from "./build-info-runtime";
|
|
15
15
|
import { EMBEDDED_DOC_FILENAMES, EMBEDDED_DOCS } from "./docs-index.generated";
|
|
16
16
|
import type { InternalResource, InternalUrl, ProtocolHandler } from "./types";
|
|
@@ -21,8 +21,8 @@ const ABOUT_ROUTE = "about";
|
|
|
21
21
|
export interface InternalDocsProtocolOptions {
|
|
22
22
|
/** Override runtime build-info resolution. Primarily for tests. */
|
|
23
23
|
readonly resolveBuildInfo?: () => Promise<RuntimeBuildInfo>;
|
|
24
|
-
/** Sync getter returning the current
|
|
25
|
-
readonly
|
|
24
|
+
/** Sync getter returning the current context status (or null if unconfigured / unavailable). */
|
|
25
|
+
readonly getContextStatus?: () => ContextStatus | null;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
/**
|
|
@@ -34,11 +34,11 @@ export interface InternalDocsProtocolOptions {
|
|
|
34
34
|
export class InternalDocsProtocolHandler implements ProtocolHandler {
|
|
35
35
|
readonly scheme = "xcsh";
|
|
36
36
|
readonly #resolveBuildInfo: () => Promise<RuntimeBuildInfo>;
|
|
37
|
-
readonly #
|
|
37
|
+
readonly #getContextStatus: (() => ContextStatus | null) | undefined;
|
|
38
38
|
|
|
39
39
|
constructor(options: InternalDocsProtocolOptions = {}) {
|
|
40
40
|
this.#resolveBuildInfo = options.resolveBuildInfo ?? getRuntimeBuildInfo;
|
|
41
|
-
this.#
|
|
41
|
+
this.#getContextStatus = options.getContextStatus;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
async resolve(url: InternalUrl): Promise<InternalResource> {
|
|
@@ -84,8 +84,8 @@ export class InternalDocsProtocolHandler implements ProtocolHandler {
|
|
|
84
84
|
|
|
85
85
|
if (normalized === ABOUT_ROUTE || normalized === `${ABOUT_ROUTE}.md`) {
|
|
86
86
|
const info = await this.#resolveBuildInfo();
|
|
87
|
-
const
|
|
88
|
-
const content = renderAboutDoc(info,
|
|
87
|
+
const context = this.#getContextStatus?.() ?? null;
|
|
88
|
+
const content = renderAboutDoc(info, context);
|
|
89
89
|
return {
|
|
90
90
|
url: url.href,
|
|
91
91
|
content,
|
package/src/main.ts
CHANGED
|
@@ -638,14 +638,14 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
|
|
|
638
638
|
const cwd = getProjectDir();
|
|
639
639
|
await logger.time("settings:init", Settings.init, { cwd });
|
|
640
640
|
|
|
641
|
-
// F5 XC
|
|
641
|
+
// F5 XC context loading — optional, never blocks startup.
|
|
642
642
|
// NOTE: This runs in the CLI path only. SDK consumers using createAgentSession()
|
|
643
|
-
// directly must call
|
|
643
|
+
// directly must call ContextService.init(configDir).loadActive() themselves.
|
|
644
644
|
try {
|
|
645
|
-
const {
|
|
645
|
+
const { ContextService } = await import("./services/f5xc-context");
|
|
646
646
|
const { getF5XCConfigDir } = await import("@f5xc-salesdemos/pi-utils");
|
|
647
|
-
const
|
|
648
|
-
await
|
|
647
|
+
const contextService = ContextService.init(getF5XCConfigDir());
|
|
648
|
+
await contextService.loadActive();
|
|
649
649
|
} catch {
|
|
650
650
|
// F5 XC auth is optional — silently continue if anything fails
|
|
651
651
|
}
|
|
@@ -3,7 +3,7 @@ import type { PresetDef, StatusLinePreset } from "./types";
|
|
|
3
3
|
export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
|
|
4
4
|
default: {
|
|
5
5
|
leftSegments: ["pi", "model", "plan_mode", "path", "git", "pr", "context_pct", "token_total", "cost"],
|
|
6
|
-
rightSegments: ["
|
|
6
|
+
rightSegments: ["context_f5xc"],
|
|
7
7
|
separator: "powerline",
|
|
8
8
|
segmentOptions: {
|
|
9
9
|
model: { showThinkingLevel: true },
|
|
@@ -14,7 +14,7 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
|
|
|
14
14
|
|
|
15
15
|
minimal: {
|
|
16
16
|
leftSegments: ["path", "git"],
|
|
17
|
-
rightSegments: ["plan_mode", "context_pct", "
|
|
17
|
+
rightSegments: ["plan_mode", "context_pct", "context_f5xc"],
|
|
18
18
|
separator: "slash",
|
|
19
19
|
segmentOptions: {
|
|
20
20
|
path: { abbreviate: true, maxLength: 30 },
|
|
@@ -24,7 +24,7 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
|
|
|
24
24
|
|
|
25
25
|
compact: {
|
|
26
26
|
leftSegments: ["model", "plan_mode", "git", "pr"],
|
|
27
|
-
rightSegments: ["cost", "context_pct", "
|
|
27
|
+
rightSegments: ["cost", "context_pct", "context_f5xc"],
|
|
28
28
|
separator: "powerline",
|
|
29
29
|
segmentOptions: {
|
|
30
30
|
model: { showThinkingLevel: false },
|
|
@@ -43,7 +43,7 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
|
|
|
43
43
|
"context_pct",
|
|
44
44
|
"time_spent",
|
|
45
45
|
"time",
|
|
46
|
-
"
|
|
46
|
+
"context_f5xc",
|
|
47
47
|
],
|
|
48
48
|
separator: "powerline",
|
|
49
49
|
segmentOptions: {
|
|
@@ -68,7 +68,7 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
|
|
|
68
68
|
"context_total",
|
|
69
69
|
"time_spent",
|
|
70
70
|
"time",
|
|
71
|
-
"
|
|
71
|
+
"context_f5xc",
|
|
72
72
|
],
|
|
73
73
|
separator: "powerline",
|
|
74
74
|
segmentOptions: {
|
|
@@ -82,7 +82,7 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
|
|
|
82
82
|
ascii: {
|
|
83
83
|
// No Nerd Font dependencies
|
|
84
84
|
leftSegments: ["model", "plan_mode", "path", "git", "pr"],
|
|
85
|
-
rightSegments: ["token_total", "cost", "context_pct", "
|
|
85
|
+
rightSegments: ["token_total", "cost", "context_pct", "context_f5xc"],
|
|
86
86
|
separator: "ascii",
|
|
87
87
|
segmentOptions: {
|
|
88
88
|
model: { showThinkingLevel: true },
|
|
@@ -93,7 +93,7 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
|
|
|
93
93
|
|
|
94
94
|
xcsh: {
|
|
95
95
|
leftSegments: ["context_pct", "path", "git"],
|
|
96
|
-
rightSegments: ["plan_mode", "
|
|
96
|
+
rightSegments: ["plan_mode", "context_f5xc"],
|
|
97
97
|
separator: "powerline",
|
|
98
98
|
segmentOptions: {
|
|
99
99
|
model: { showThinkingLevel: true },
|
|
@@ -106,7 +106,7 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
|
|
|
106
106
|
custom: {
|
|
107
107
|
// User-defined - these are just defaults that get overridden
|
|
108
108
|
leftSegments: ["model", "plan_mode", "path", "git", "pr"],
|
|
109
|
-
rightSegments: ["token_total", "cost", "context_pct", "
|
|
109
|
+
rightSegments: ["token_total", "cost", "context_pct", "context_f5xc"],
|
|
110
110
|
separator: "powerline",
|
|
111
111
|
segmentOptions: {},
|
|
112
112
|
},
|
|
@@ -440,17 +440,17 @@ export const SEGMENTS: Record<StatusLineSegmentId, StatusLineSegment> = {
|
|
|
440
440
|
return { content: `${ansi}${sanitizeStatusText(name)}\x1b[39m`, visible: true };
|
|
441
441
|
},
|
|
442
442
|
},
|
|
443
|
-
|
|
444
|
-
id: "
|
|
443
|
+
context_f5xc: {
|
|
444
|
+
id: "context_f5xc",
|
|
445
445
|
render() {
|
|
446
446
|
try {
|
|
447
|
-
const {
|
|
448
|
-
const result =
|
|
447
|
+
const { renderF5XCContextSegment } = require("../../../services/f5xc-context-segment");
|
|
448
|
+
const result = renderF5XCContextSegment();
|
|
449
449
|
if (!result.visible) return result;
|
|
450
450
|
return {
|
|
451
451
|
...result,
|
|
452
|
-
bg: theme.fgColorAsBg("
|
|
453
|
-
fg: theme.getFgAnsi("
|
|
452
|
+
bg: theme.fgColorAsBg("statusLineContextF5xcBg"),
|
|
453
|
+
fg: theme.getFgAnsi("statusLineContextF5xcFg"),
|
|
454
454
|
};
|
|
455
455
|
} catch {
|
|
456
456
|
return { content: "", visible: false };
|
|
@@ -38,7 +38,7 @@ const SEGMENT_INFO: Record<StatusLineSegmentId, { label: string; short: string }
|
|
|
38
38
|
cache_read: { label: "Cache ↓", short: "cache read" },
|
|
39
39
|
cache_write: { label: "Cache ↑", short: "cache write" },
|
|
40
40
|
session_name: { label: "Session Name", short: "session name" },
|
|
41
|
-
|
|
41
|
+
context_f5xc: { label: "F5 XC Context", short: "F5 XC tenant" },
|
|
42
42
|
};
|
|
43
43
|
|
|
44
44
|
type Column = "left" | "right" | "disabled";
|
|
@@ -1,27 +1,27 @@
|
|
|
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 { type AuthStatus,
|
|
4
|
+
import { type AuthStatus, ContextService } from "../../services/f5xc-context";
|
|
5
5
|
import type { AuthStorage } from "../../session/auth-storage";
|
|
6
6
|
|
|
7
7
|
// Startup validation budget. These are longer than validateToken's 3000ms default because
|
|
8
8
|
// the welcome path runs during TLS/DNS cold-start — a single 3s shot races against warm-up
|
|
9
|
-
// and falsely reports offline for
|
|
9
|
+
// and falsely reports offline for contexts that reconnect cleanly moments later.
|
|
10
10
|
const STARTUP_FIRST_TIMEOUT_MS = 4000;
|
|
11
11
|
const STARTUP_RETRY_TIMEOUT_MS = 5000;
|
|
12
12
|
const STARTUP_RETRY_DELAY_MS = 500;
|
|
13
13
|
|
|
14
|
-
type
|
|
14
|
+
type ContextValidator = (opts: {
|
|
15
15
|
timeoutMs: number;
|
|
16
16
|
}) => Promise<{ status: AuthStatus; latencyMs?: number; errorClass?: "network" | "credential" }>;
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
|
-
* Runs the
|
|
19
|
+
* Runs the context validator once with a startup-sized timeout; if the result is `offline`
|
|
20
20
|
* (the only transient class — auth_error/connected/unknown are definitive), waits briefly
|
|
21
21
|
* to let DNS/TLS warm up, then tries once more with a longer timeout.
|
|
22
22
|
*/
|
|
23
|
-
export async function
|
|
24
|
-
validate:
|
|
23
|
+
export async function validateContextWithStartupRetry(
|
|
24
|
+
validate: ContextValidator,
|
|
25
25
|
options?: {
|
|
26
26
|
firstTimeoutMs?: number;
|
|
27
27
|
retryTimeoutMs?: number;
|
|
@@ -49,17 +49,17 @@ export interface ModelStatus {
|
|
|
49
49
|
latencyMs?: number;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
export type
|
|
52
|
+
export type ContextCheckState = "no_context" | "connected" | "auth_error" | "offline";
|
|
53
53
|
|
|
54
|
-
export interface
|
|
55
|
-
state:
|
|
54
|
+
export interface WelcomeContextStatus {
|
|
55
|
+
state: ContextCheckState;
|
|
56
56
|
name?: string;
|
|
57
57
|
latencyMs?: number;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
export interface WelcomeCheckResult {
|
|
61
61
|
model: ModelStatus;
|
|
62
|
-
|
|
62
|
+
context?: WelcomeContextStatus;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
/** Providers that don't store API keys (local inference servers) */
|
|
@@ -67,7 +67,7 @@ const KEYLESS_PROVIDERS = new Set(["ollama", "llama.cpp", "lm-studio", "llamafil
|
|
|
67
67
|
|
|
68
68
|
/**
|
|
69
69
|
* Run blocking startup checks for the welcome screen.
|
|
70
|
-
* Model check always runs.
|
|
70
|
+
* Model check always runs. Context check only runs if model is connected.
|
|
71
71
|
*/
|
|
72
72
|
export async function runWelcomeChecks(
|
|
73
73
|
model: Model | undefined,
|
|
@@ -87,9 +87,9 @@ export async function runWelcomeChecks(
|
|
|
87
87
|
return { model: modelStatus };
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
// Step 3:
|
|
91
|
-
const
|
|
92
|
-
return { model: modelStatus,
|
|
90
|
+
// Step 3: Context check (only if model is connected)
|
|
91
|
+
const contextStatus = await checkContextStatus();
|
|
92
|
+
return { model: modelStatus, context: contextStatus };
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
async function validateModelConnection(model: Model | undefined, authStorage: AuthStorage): Promise<ModelStatus> {
|
|
@@ -141,20 +141,20 @@ async function validateModelConnection(model: Model | undefined, authStorage: Au
|
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
async function
|
|
144
|
+
async function checkContextStatus(): Promise<WelcomeContextStatus> {
|
|
145
145
|
try {
|
|
146
|
-
const
|
|
147
|
-
if (!
|
|
148
|
-
return { state: "
|
|
146
|
+
const contextService = ContextService.instance;
|
|
147
|
+
if (!contextService) {
|
|
148
|
+
return { state: "no_context" };
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
const status =
|
|
151
|
+
const status = contextService.getStatus();
|
|
152
152
|
if (!status.isConfigured) {
|
|
153
|
-
return { state: "
|
|
153
|
+
return { state: "no_context" };
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
const name = status.
|
|
157
|
-
const result = await
|
|
156
|
+
const name = status.activeContextName ?? "default";
|
|
157
|
+
const result = await validateContextWithStartupRetry(opts => contextService.validateToken(opts));
|
|
158
158
|
|
|
159
159
|
switch (result.status) {
|
|
160
160
|
case "connected":
|
|
@@ -164,10 +164,10 @@ async function checkProfileStatus(): Promise<WelcomeProfileStatus> {
|
|
|
164
164
|
case "offline":
|
|
165
165
|
return { state: "offline", name };
|
|
166
166
|
default:
|
|
167
|
-
return { state: "
|
|
167
|
+
return { state: "no_context" };
|
|
168
168
|
}
|
|
169
169
|
} catch {
|
|
170
|
-
logger.warn("Welcome
|
|
171
|
-
return { state: "
|
|
170
|
+
logger.warn("Welcome context validation failed");
|
|
171
|
+
return { state: "no_context" };
|
|
172
172
|
}
|
|
173
173
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
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-
|
|
5
|
-
import type { ModelStatus,
|
|
4
|
+
import { formatStatusIcon } from "../../services/f5xc-context-indicators";
|
|
5
|
+
import type { ModelStatus, WelcomeContextStatus } from "./welcome-checks";
|
|
6
6
|
|
|
7
7
|
export interface UpdateStatus {
|
|
8
8
|
available: boolean;
|
|
@@ -18,7 +18,7 @@ export class WelcomeComponent implements Component {
|
|
|
18
18
|
constructor(
|
|
19
19
|
private readonly version: string,
|
|
20
20
|
private modelStatus: ModelStatus,
|
|
21
|
-
private
|
|
21
|
+
private contextStatus?: WelcomeContextStatus,
|
|
22
22
|
private updateStatus?: UpdateStatus,
|
|
23
23
|
private changelogStatus?: ChangelogStatus,
|
|
24
24
|
) {}
|
|
@@ -26,8 +26,8 @@ export class WelcomeComponent implements Component {
|
|
|
26
26
|
setModelStatus(status: ModelStatus): void {
|
|
27
27
|
this.modelStatus = status;
|
|
28
28
|
}
|
|
29
|
-
|
|
30
|
-
this.
|
|
29
|
+
setContextStatus(status: WelcomeContextStatus | undefined): void {
|
|
30
|
+
this.contextStatus = status;
|
|
31
31
|
}
|
|
32
32
|
setUpdateStatus(status: UpdateStatus | undefined): void {
|
|
33
33
|
this.updateStatus = status;
|
|
@@ -127,8 +127,8 @@ export class WelcomeComponent implements Component {
|
|
|
127
127
|
|
|
128
128
|
#measureStatusWidth(): number {
|
|
129
129
|
const lines: string[] = [" Model Provider", ...this.#renderModelStatus()];
|
|
130
|
-
if (this.
|
|
131
|
-
lines.push(" F5 XC
|
|
130
|
+
if (this.contextStatus) {
|
|
131
|
+
lines.push(" F5 XC Context", ...this.#renderContextStatus());
|
|
132
132
|
}
|
|
133
133
|
if (this.#showUpdateSection()) {
|
|
134
134
|
lines.push(" Update Available", ...this.#renderUpdateStatus());
|
|
@@ -147,11 +147,11 @@ export class WelcomeComponent implements Component {
|
|
|
147
147
|
lines.push(` ${theme.bold(theme.fg("contentAccent", "Model Provider"))}`);
|
|
148
148
|
lines.push(...this.#renderModelStatus());
|
|
149
149
|
lines.push("");
|
|
150
|
-
if (this.
|
|
150
|
+
if (this.contextStatus) {
|
|
151
151
|
lines.push(separator);
|
|
152
152
|
lines.push("");
|
|
153
|
-
lines.push(` ${theme.bold(theme.fg("contentAccent", "F5 XC
|
|
154
|
-
lines.push(...this.#
|
|
153
|
+
lines.push(` ${theme.bold(theme.fg("contentAccent", "F5 XC Context"))}`);
|
|
154
|
+
lines.push(...this.#renderContextStatus());
|
|
155
155
|
lines.push("");
|
|
156
156
|
}
|
|
157
157
|
if (this.#showUpdateSection()) {
|
|
@@ -217,9 +217,9 @@ export class WelcomeComponent implements Component {
|
|
|
217
217
|
}
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
-
#
|
|
221
|
-
if (!this.
|
|
222
|
-
const { state, name, latencyMs } = this.
|
|
220
|
+
#renderContextStatus(): string[] {
|
|
221
|
+
if (!this.contextStatus) return [];
|
|
222
|
+
const { state, name, latencyMs } = this.contextStatus;
|
|
223
223
|
const n = name ?? "default";
|
|
224
224
|
switch (state) {
|
|
225
225
|
case "connected":
|
|
@@ -229,17 +229,17 @@ export class WelcomeComponent implements Component {
|
|
|
229
229
|
case "auth_error":
|
|
230
230
|
return [
|
|
231
231
|
` ${formatStatusIcon("error")} ${theme.fg("muted", n)} ${theme.fg("error", "\u2014 token invalid")}`,
|
|
232
|
-
` ${theme.fg("dim", "Run /
|
|
232
|
+
` ${theme.fg("dim", "Run /context to update")}`,
|
|
233
233
|
];
|
|
234
234
|
case "offline":
|
|
235
235
|
return [
|
|
236
236
|
` ${formatStatusIcon("warning")} ${theme.fg("muted", n)} ${theme.fg("warning", "\u2014 unreachable")}`,
|
|
237
|
-
` ${theme.fg("dim", "Check network, /
|
|
237
|
+
` ${theme.fg("dim", "Check network, /context")}`,
|
|
238
238
|
];
|
|
239
|
-
case "
|
|
239
|
+
case "no_context":
|
|
240
240
|
return [
|
|
241
|
-
` ${formatStatusIcon("warning")} ${theme.fg("warning", "No
|
|
242
|
-
` ${theme.fg("dim", "Run /
|
|
241
|
+
` ${formatStatusIcon("warning")} ${theme.fg("warning", "No context configured")}`,
|
|
242
|
+
` ${theme.fg("dim", "Run /context create <name> <url> <token>")}`,
|
|
243
243
|
];
|
|
244
244
|
}
|
|
245
245
|
}
|
|
@@ -310,7 +310,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
310
310
|
getProjectDir(),
|
|
311
311
|
);
|
|
312
312
|
|
|
313
|
-
// Run blocking welcome screen status checks (model +
|
|
313
|
+
// Run blocking welcome screen status checks (model + context)
|
|
314
314
|
const welcomeResult = await logger.time("InteractiveMode.init:welcomeChecks", () =>
|
|
315
315
|
runWelcomeChecks(this.session.model, this.session.modelRegistry.authStorage),
|
|
316
316
|
);
|
|
@@ -324,11 +324,11 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
324
324
|
}
|
|
325
325
|
|
|
326
326
|
if (!startupQuiet) {
|
|
327
|
-
// Welcome box owns all startup notifications (model,
|
|
327
|
+
// Welcome box owns all startup notifications (model, context, update, changelog)
|
|
328
328
|
this.#welcomeComponent = new WelcomeComponent(
|
|
329
329
|
this.#version,
|
|
330
330
|
welcomeResult.model,
|
|
331
|
-
welcomeResult.
|
|
331
|
+
welcomeResult.context,
|
|
332
332
|
this.#initialUpdateStatus,
|
|
333
333
|
this.#changelogStatus,
|
|
334
334
|
);
|
|
@@ -100,8 +100,8 @@
|
|
|
100
100
|
"statusLineGitConflictFg": 7,
|
|
101
101
|
"statusLinePlanModeBg": 236,
|
|
102
102
|
"statusLinePlanModeFg": 117,
|
|
103
|
-
"
|
|
104
|
-
"
|
|
103
|
+
"statusLineContextF5xcBg": "f5Red",
|
|
104
|
+
"statusLineContextF5xcFg": 231,
|
|
105
105
|
"pythonMode": "#f0c040",
|
|
106
106
|
"syntaxControl": "#569CD6"
|
|
107
107
|
},
|
|
@@ -102,8 +102,8 @@
|
|
|
102
102
|
"statusLineGitConflictFg": 255,
|
|
103
103
|
"statusLinePlanModeBg": 223,
|
|
104
104
|
"statusLinePlanModeFg": 238,
|
|
105
|
-
"
|
|
106
|
-
"
|
|
105
|
+
"statusLineContextF5xcBg": "f5Red",
|
|
106
|
+
"statusLineContextF5xcFg": 231,
|
|
107
107
|
"pythonMode": "warningAmber",
|
|
108
108
|
"syntaxControl": "f5Red"
|
|
109
109
|
},
|
|
@@ -123,8 +123,8 @@
|
|
|
123
123
|
"statusLineGitConflictFg",
|
|
124
124
|
"statusLinePlanModeBg",
|
|
125
125
|
"statusLinePlanModeFg",
|
|
126
|
-
"
|
|
127
|
-
"
|
|
126
|
+
"statusLineContextF5xcBg",
|
|
127
|
+
"statusLineContextF5xcFg"
|
|
128
128
|
],
|
|
129
129
|
"properties": {
|
|
130
130
|
"accent": {
|
|
@@ -483,13 +483,13 @@
|
|
|
483
483
|
"$ref": "#/$defs/colorValue",
|
|
484
484
|
"description": "256-color palette index for the powerline plan-mode segment foreground. See issue #242 for the two-domain color model."
|
|
485
485
|
},
|
|
486
|
-
"
|
|
486
|
+
"statusLineContextF5xcBg": {
|
|
487
487
|
"$ref": "#/$defs/colorValue",
|
|
488
|
-
"description": "256-color palette index OR color reference for the powerline F5 XC
|
|
488
|
+
"description": "256-color palette index OR color reference for the powerline F5 XC context segment background. Stays F5 brand red across light and dark themes. See issue #242 for the two-domain color model."
|
|
489
489
|
},
|
|
490
|
-
"
|
|
490
|
+
"statusLineContextF5xcFg": {
|
|
491
491
|
"$ref": "#/$defs/colorValue",
|
|
492
|
-
"description": "256-color palette index for the powerline F5 XC
|
|
492
|
+
"description": "256-color palette index for the powerline F5 XC context segment foreground. See issue #242 for the two-domain color model."
|
|
493
493
|
}
|
|
494
494
|
},
|
|
495
495
|
"additionalProperties": false
|