@f5xc-salesdemos/xcsh 17.4.1 → 17.4.2

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": "17.4.1",
4
+ "version": "17.4.2",
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",
@@ -46,12 +46,12 @@
46
46
  "dependencies": {
47
47
  "@agentclientprotocol/sdk": "0.16.1",
48
48
  "@mozilla/readability": "^0.6",
49
- "@f5xc-salesdemos/xcsh-stats": "17.4.1",
50
- "@f5xc-salesdemos/pi-agent-core": "17.4.1",
51
- "@f5xc-salesdemos/pi-ai": "17.4.1",
52
- "@f5xc-salesdemos/pi-natives": "17.4.1",
53
- "@f5xc-salesdemos/pi-tui": "17.4.1",
54
- "@f5xc-salesdemos/pi-utils": "17.4.1",
49
+ "@f5xc-salesdemos/xcsh-stats": "17.4.2",
50
+ "@f5xc-salesdemos/pi-agent-core": "17.4.2",
51
+ "@f5xc-salesdemos/pi-ai": "17.4.2",
52
+ "@f5xc-salesdemos/pi-natives": "17.4.2",
53
+ "@f5xc-salesdemos/pi-tui": "17.4.2",
54
+ "@f5xc-salesdemos/pi-utils": "17.4.2",
55
55
  "@sinclair/typebox": "^0.34",
56
56
  "@xterm/headless": "^6.0",
57
57
  "ajv": "^8.18",
@@ -15,7 +15,7 @@
15
15
  */
16
16
  import * as fs from "node:fs";
17
17
  import * as path from "node:path";
18
- import { $env, logger } from "@f5xc-salesdemos/pi-utils";
18
+ import { $env, logger, readProviderFromModelsYml } from "@f5xc-salesdemos/pi-utils";
19
19
 
20
20
  /** Current config schema version. Bump when the generated format changes. */
21
21
  export const CURRENT_CONFIG_VERSION = 2;
@@ -89,60 +89,28 @@ export interface LiteLLMConfig {
89
89
  * Read existing models.yml and extract the anthropic provider's base URL and API key.
90
90
  *
91
91
  * - baseUrl: strips `/anthropic` suffix to recover the root proxy URL
92
- * - apiKey: if it matches /^[A-Z][A-Z0-9_]+$/ (env var name pattern), resolves via
93
- * process.env; otherwise uses the literal value
94
- * - Falls back to getLiteLLMBaseUrl() for baseUrl and $env.LITELLM_API_KEY for apiKey
95
- * when the file is missing or the anthropic block is incomplete
92
+ * - apiKey: env var name process.env lookup; shell-backed fall back to env;
93
+ * otherwise literal
94
+ * - Falls back to getLiteLLMBaseUrl() and $env.LITELLM_API_KEY when the file is
95
+ * missing or the anthropic block is incomplete
96
96
  */
97
97
  export function readLiteLLMConfig(modelsPath: string): LiteLLMConfig | undefined {
98
98
  if (!fs.existsSync(modelsPath)) return undefined;
99
99
 
100
- let rawBaseUrl: string | undefined;
101
- let rawApiKey: string | undefined;
100
+ const block = readProviderFromModelsYml("anthropic", modelsPath);
102
101
 
103
- // Line-by-line state machine: locate the anthropic provider block
104
- const content = fs.readFileSync(modelsPath, "utf8");
105
- const lines = content.split("\n");
106
- let inAnthropicBlock = false;
102
+ const resolvedBaseUrl = block?.baseUrl ? block.baseUrl.replace(/\/anthropic\/?$/, "") : getLiteLLMBaseUrl();
107
103
 
108
- for (const line of lines) {
109
- // Detect entry into anthropic provider block
110
- if (/^\s{2}anthropic\s*:/.test(line)) {
111
- inAnthropicBlock = true;
112
- continue;
113
- }
114
- // Detect leaving the block: a new sibling provider key (2-space indent, non-empty)
115
- if (inAnthropicBlock && /^\s{2}\S/.test(line)) {
116
- inAnthropicBlock = false;
117
- }
118
-
119
- if (inAnthropicBlock) {
120
- const baseUrlMatch = line.match(/^\s+baseUrl\s*:\s*"?([^"]+)"?\s*$/);
121
- if (baseUrlMatch) {
122
- rawBaseUrl = baseUrlMatch[1].trim();
123
- }
124
- const apiKeyMatch = line.match(/^\s+apiKey\s*:\s*(.+)\s*$/);
125
- if (apiKeyMatch) {
126
- rawApiKey = apiKeyMatch[1].trim().replace(/^["']|["']$/g, "");
127
- }
128
- }
129
- }
130
-
131
- // Resolve base URL: strip /anthropic suffix
132
- const resolvedBaseUrl = rawBaseUrl ? rawBaseUrl.replace(/\/anthropic\/?$/, "") : getLiteLLMBaseUrl();
133
-
134
- // Resolve API key: env var name → process.env lookup; command-backed → skip; otherwise literal
135
104
  let resolvedApiKey: string | undefined;
136
- if (!rawApiKey) {
105
+ const apiKey = block?.apiKey;
106
+ if (!apiKey) {
137
107
  resolvedApiKey = $env.LITELLM_API_KEY;
138
- } else if (/^[A-Z][A-Z0-9_]+$/.test(rawApiKey)) {
139
- // Looks like an env var name resolve it
140
- resolvedApiKey = process.env[rawApiKey] ?? $env.LITELLM_API_KEY;
141
- } else if (rawApiKey.startsWith("!")) {
142
- // Command-backed secrets (e.g. "!command ...") can't be resolved here — fall back to env
108
+ } else if (apiKey.kind === "envVar") {
109
+ resolvedApiKey = process.env[apiKey.name] ?? $env.LITELLM_API_KEY;
110
+ } else if (apiKey.kind === "shellSecret") {
143
111
  resolvedApiKey = $env.LITELLM_API_KEY;
144
112
  } else {
145
- resolvedApiKey = rawApiKey;
113
+ resolvedApiKey = apiKey.value;
146
114
  }
147
115
 
148
116
  if (!resolvedBaseUrl || !resolvedApiKey) return undefined;
@@ -159,6 +127,7 @@ export function generateConfigYml(): string {
159
127
  "",
160
128
  "providers:",
161
129
  " image: openai",
130
+ " webSearch: anthropic",
162
131
  "",
163
132
  "generate_image:",
164
133
  " enabled: true",
@@ -200,31 +169,18 @@ export function healConfigYmlModelRoles(configPath: string): void {
200
169
  // ---------------------------------------------------------------------------
201
170
 
202
171
  /**
203
- * Extract a quoted literal API key from an existing models.yml file.
204
- * Only looks within the anthropic or litellm provider blocks to avoid
205
- * accidentally picking up keys from unrelated providers.
206
- * Returns undefined if the file uses an env var reference (unquoted) or doesn't exist.
172
+ * Extract a double-quoted literal API key from an existing models.yml file.
173
+ * Checks the anthropic and litellm provider blocks; returns undefined if
174
+ * neither has a literal (e.g., both use env var references) or the file is
175
+ * missing.
207
176
  */
208
177
  export function readApiKeyLiteral(modelsPath: string): string | undefined {
209
- try {
210
- const content = fs.readFileSync(modelsPath, "utf-8");
211
- const lines = content.split("\n");
212
- let inTargetBlock = false;
213
-
214
- for (const line of lines) {
215
- if (/^\s{2}(?:anthropic|litellm)\s*:/.test(line)) {
216
- inTargetBlock = true;
217
- continue;
218
- }
219
- if (inTargetBlock && /^\s{2}\S/.test(line)) {
220
- inTargetBlock = false;
221
- }
222
- if (inTargetBlock) {
223
- const match = line.match(/^\s+apiKey:\s*"([^"]+)"/);
224
- if (match) return match[1].replace(/\\"/g, '"').replace(/\\\\/g, "\\");
225
- }
178
+ for (const name of ["anthropic", "litellm"]) {
179
+ const block = readProviderFromModelsYml(name, modelsPath);
180
+ if (block?.apiKey?.kind === "literal" && block.apiKey.wasQuoted) {
181
+ return block.apiKey.value;
226
182
  }
227
- } catch {}
183
+ }
228
184
  return undefined;
229
185
  }
230
186
 
@@ -662,12 +662,6 @@ export const SETTINGS_SCHEMA = {
662
662
  },
663
663
  },
664
664
 
665
- collapseChangelog: {
666
- type: "boolean",
667
- default: true,
668
- ui: { tab: "interaction", label: "Collapse Changelog", description: "Show condensed changelog after updates" },
669
- },
670
-
671
665
  // Notifications
672
666
  "completion.notify": {
673
667
  type: "enum",
package/src/main.ts CHANGED
@@ -144,10 +144,12 @@ export async function submitInteractiveInput(
144
144
  }
145
145
  }
146
146
 
147
+ const INITIAL_UPDATE_CHECK_TIMEOUT_MS = 500;
148
+
147
149
  async function runInteractiveMode(
148
150
  session: AgentSession,
149
151
  version: string,
150
- changelogMarkdown: string | undefined,
152
+ changelogStatus: { hasNew: boolean; version: string } | undefined,
151
153
  notifs: (InteractiveModeNotify | null)[],
152
154
  versionCheckPromise: Promise<string | undefined>,
153
155
  initialMessages: string[],
@@ -158,10 +160,19 @@ async function runInteractiveMode(
158
160
  initialMessage?: string,
159
161
  initialImages?: ImageContent[],
160
162
  ): Promise<void> {
163
+ const initialUpdateVersion = await Promise.race([
164
+ versionCheckPromise.catch(() => undefined),
165
+ new Promise<string | undefined>(resolve => setTimeout(() => resolve(undefined), INITIAL_UPDATE_CHECK_TIMEOUT_MS)),
166
+ ]);
167
+ const initialUpdateStatus = initialUpdateVersion
168
+ ? { available: true, latestVersion: initialUpdateVersion }
169
+ : undefined;
170
+
161
171
  const mode = new InteractiveMode(
162
172
  session,
163
173
  version,
164
- changelogMarkdown,
174
+ changelogStatus,
175
+ initialUpdateStatus,
165
176
  setExtensionUIContext,
166
177
  lspServers,
167
178
  mcpManager,
@@ -170,16 +181,18 @@ async function runInteractiveMode(
170
181
 
171
182
  await mode.init();
172
183
 
173
- versionCheckPromise
174
- .then(newVersion => {
175
- if (!settings.get("startup.checkUpdate")) {
176
- return;
177
- }
178
- if (newVersion) {
179
- mode.showNewVersionNotification(newVersion);
180
- }
181
- })
182
- .catch(() => {});
184
+ if (!initialUpdateVersion) {
185
+ versionCheckPromise
186
+ .then(newVersion => {
187
+ if (!settings.get("startup.checkUpdate")) {
188
+ return;
189
+ }
190
+ if (newVersion) {
191
+ mode.setUpdateStatus({ available: true, latestVersion: newVersion });
192
+ }
193
+ })
194
+ .catch(() => {});
195
+ }
183
196
 
184
197
  mode.renderInitialMessages();
185
198
 
@@ -243,7 +256,7 @@ async function promptForkSession(session: SessionInfo): Promise<boolean> {
243
256
  }
244
257
  }
245
258
 
246
- async function getChangelogForDisplay(parsed: Args): Promise<string | undefined> {
259
+ async function getChangelogForDisplay(parsed: Args): Promise<{ hasNew: boolean; version: string } | undefined> {
247
260
  if (parsed.continue || parsed.resume) {
248
261
  return undefined;
249
262
  }
@@ -255,13 +268,13 @@ async function getChangelogForDisplay(parsed: Args): Promise<string | undefined>
255
268
  if (!lastVersion) {
256
269
  if (entries.length > 0) {
257
270
  settings.set("lastChangelogVersion", VERSION);
258
- return entries.map(e => e.content).join("\n\n");
271
+ return { hasNew: true, version: VERSION };
259
272
  }
260
273
  } else {
261
274
  const newEntries = getNewEntries(entries, lastVersion);
262
275
  if (newEntries.length > 0) {
263
276
  settings.set("lastChangelogVersion", VERSION);
264
- return newEntries.map(e => e.content).join("\n\n");
277
+ return { hasNew: true, version: VERSION };
265
278
  }
266
279
  }
267
280
 
@@ -877,7 +890,7 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
877
890
  } else if (isInteractive) {
878
891
  const versionCheckPromise = checkForNewVersion(VERSION).catch(() => undefined);
879
892
  logger.time("main:getChangelogForDisplay");
880
- const changelogMarkdown = await getChangelogForDisplay(parsedArgs);
893
+ const changelogStatus = await getChangelogForDisplay(parsedArgs);
881
894
 
882
895
  const scopedModelsForDisplay = sessionOptions.scopedModels ?? scopedModels;
883
896
  if (scopedModelsForDisplay.length > 0) {
@@ -901,7 +914,7 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
901
914
  await runInteractiveMode(
902
915
  session,
903
916
  VERSION,
904
- changelogMarkdown,
917
+ changelogStatus,
905
918
  notifs,
906
919
  versionCheckPromise,
907
920
  parsedArgs.messages,
@@ -3,11 +3,23 @@ import { APP_NAME } from "@f5xc-salesdemos/pi-utils";
3
3
  import { theme } from "../../modes/theme/theme";
4
4
  import type { ModelStatus, WelcomeProfileStatus } from "./welcome-checks";
5
5
 
6
+ export interface UpdateStatus {
7
+ available: boolean;
8
+ latestVersion?: string;
9
+ }
10
+
11
+ export interface ChangelogStatus {
12
+ hasNew: boolean;
13
+ version: string;
14
+ }
15
+
6
16
  export class WelcomeComponent implements Component {
7
17
  constructor(
8
18
  private readonly version: string,
9
19
  private modelStatus: ModelStatus,
10
20
  private profileStatus?: WelcomeProfileStatus,
21
+ private updateStatus?: UpdateStatus,
22
+ private changelogStatus?: ChangelogStatus,
11
23
  ) {}
12
24
  invalidate(): void {}
13
25
  setModelStatus(status: ModelStatus): void {
@@ -16,6 +28,12 @@ export class WelcomeComponent implements Component {
16
28
  setProfileStatus(status: WelcomeProfileStatus | undefined): void {
17
29
  this.profileStatus = status;
18
30
  }
31
+ setUpdateStatus(status: UpdateStatus | undefined): void {
32
+ this.updateStatus = status;
33
+ }
34
+ setChangelogStatus(status: ChangelogStatus | undefined): void {
35
+ this.changelogStatus = status;
36
+ }
19
37
 
20
38
  render(termWidth: number): string[] {
21
39
  const minLeftCol = 48;
@@ -111,26 +129,72 @@ export class WelcomeComponent implements Component {
111
129
  if (this.profileStatus) {
112
130
  lines.push(" F5 XC Profile", ...this.#renderProfileStatus());
113
131
  }
132
+ if (this.#showUpdateSection()) {
133
+ lines.push(" Update Available", ...this.#renderUpdateStatus());
134
+ }
135
+ if (this.#showChangelogSection()) {
136
+ lines.push(" What's New", ...this.#renderChangelogStatus());
137
+ }
114
138
  return Math.max(...lines.map(l => visibleWidth(l)));
115
139
  }
116
140
 
117
141
  #buildStatusLines(rightCol: number): string[] {
118
142
  const lines: string[] = [];
119
143
  const separatorWidth = Math.max(0, rightCol - 2);
144
+ const separator = ` ${theme.fg("muted", theme.boxRound.horizontal.repeat(separatorWidth))}`;
120
145
  lines.push("");
121
146
  lines.push(` ${theme.bold(theme.fg("contentAccent", "Model Provider"))}`);
122
147
  lines.push(...this.#renderModelStatus());
123
148
  lines.push("");
124
149
  if (this.profileStatus) {
125
- lines.push(` ${theme.fg("muted", theme.boxRound.horizontal.repeat(separatorWidth))}`);
150
+ lines.push(separator);
126
151
  lines.push("");
127
152
  lines.push(` ${theme.bold(theme.fg("contentAccent", "F5 XC Profile"))}`);
128
153
  lines.push(...this.#renderProfileStatus());
129
154
  lines.push("");
130
155
  }
156
+ if (this.#showUpdateSection()) {
157
+ lines.push(separator);
158
+ lines.push("");
159
+ lines.push(` ${theme.bold(theme.fg("contentAccent", "Update Available"))}`);
160
+ lines.push(...this.#renderUpdateStatus());
161
+ lines.push("");
162
+ }
163
+ if (this.#showChangelogSection()) {
164
+ lines.push(separator);
165
+ lines.push("");
166
+ lines.push(` ${theme.bold(theme.fg("contentAccent", "What's New"))}`);
167
+ lines.push(...this.#renderChangelogStatus());
168
+ lines.push("");
169
+ }
131
170
  return lines;
132
171
  }
133
172
 
173
+ #showUpdateSection(): boolean {
174
+ return this.updateStatus?.available === true;
175
+ }
176
+
177
+ #showChangelogSection(): boolean {
178
+ return this.changelogStatus?.hasNew === true;
179
+ }
180
+
181
+ #renderUpdateStatus(): string[] {
182
+ const latest = this.updateStatus?.latestVersion;
183
+ const label = latest ? `v${latest}` : "new version";
184
+ return [
185
+ ` ${theme.fg("warning", "\u2191")} ${theme.fg("muted", label)}`,
186
+ ` ${theme.fg("dim", "Run")} ${theme.fg("contentAccent", "xcsh update")}`,
187
+ ];
188
+ }
189
+
190
+ #renderChangelogStatus(): string[] {
191
+ const v = this.changelogStatus?.version ?? this.version;
192
+ return [
193
+ ` ${theme.fg("success", "\u2605")} ${theme.fg("muted", `v${v}`)}`,
194
+ ` ${theme.fg("dim", "Run")} ${theme.fg("contentAccent", "/changelog")}`,
195
+ ];
196
+ }
197
+
134
198
  #renderModelStatus(): string[] {
135
199
  const { state, provider, latencyMs } = this.modelStatus;
136
200
  const p = provider ?? "unknown";
@@ -49,7 +49,7 @@ import type { HookSelectorComponent } from "./components/hook-selector";
49
49
  import type { PythonExecutionComponent } from "./components/python-execution";
50
50
  import { StatusLineComponent } from "./components/status-line";
51
51
  import type { ToolExecutionHandle } from "./components/tool-execution";
52
- import { WelcomeComponent } from "./components/welcome";
52
+ import { type ChangelogStatus, type UpdateStatus, WelcomeComponent } from "./components/welcome";
53
53
  import { runWelcomeChecks } from "./components/welcome-checks";
54
54
  import { BtwController } from "./controllers/btw-controller";
55
55
  import { CommandController } from "./controllers/command-controller";
@@ -161,7 +161,8 @@ export class InteractiveMode implements InteractiveModeContext {
161
161
  #pendingSlashCommands: SlashCommand[] = [];
162
162
  #cleanupUnsubscribe?: () => void;
163
163
  readonly #version: string;
164
- readonly #changelogMarkdown: string | undefined;
164
+ readonly #changelogStatus: ChangelogStatus | undefined;
165
+ readonly #initialUpdateStatus: UpdateStatus | undefined;
165
166
  #planModePreviousTools: string[] | undefined;
166
167
  #planModePreviousModelState: { model: Model; thinkingLevel?: ThinkingLevel } | undefined;
167
168
  #pendingModelSwitch: { model: Model; thinkingLevel?: ThinkingLevel } | undefined;
@@ -192,7 +193,8 @@ export class InteractiveMode implements InteractiveModeContext {
192
193
  constructor(
193
194
  session: AgentSession,
194
195
  version: string,
195
- changelogMarkdown: string | undefined = undefined,
196
+ changelogStatus: ChangelogStatus | undefined = undefined,
197
+ initialUpdateStatus: UpdateStatus | undefined = undefined,
196
198
  setToolUIContext: (uiContext: ExtensionUIContext, hasUI: boolean) => void = () => {},
197
199
  lspServers?: import("../tools").LspStartupServerInfo[],
198
200
  mcpManager?: import("../mcp").MCPManager,
@@ -204,7 +206,8 @@ export class InteractiveMode implements InteractiveModeContext {
204
206
  this.keybindings = KeybindingsManager.inMemory();
205
207
  this.agent = session.agent;
206
208
  this.#version = version;
207
- this.#changelogMarkdown = changelogMarkdown;
209
+ this.#changelogStatus = changelogStatus;
210
+ this.#initialUpdateStatus = initialUpdateStatus;
208
211
  this.#toolUiContextSetter = setToolUIContext;
209
212
  this.lspServers = lspServers;
210
213
  this.mcpManager = mcpManager;
@@ -321,28 +324,19 @@ export class InteractiveMode implements InteractiveModeContext {
321
324
  }
322
325
 
323
326
  if (!startupQuiet) {
324
- // Add welcome header
325
- this.#welcomeComponent = new WelcomeComponent(this.#version, welcomeResult.model, welcomeResult.profile);
327
+ // Welcome box owns all startup notifications (model, profile, update, changelog)
328
+ this.#welcomeComponent = new WelcomeComponent(
329
+ this.#version,
330
+ welcomeResult.model,
331
+ welcomeResult.profile,
332
+ this.#initialUpdateStatus,
333
+ this.#changelogStatus,
334
+ );
326
335
 
327
336
  // Setup UI layout
328
337
  this.ui.addChild(new Spacer(1));
329
338
  this.ui.addChild(this.#welcomeComponent);
330
339
  this.ui.addChild(new Spacer(1));
331
-
332
- // Add changelog if provided
333
- if (this.#changelogMarkdown) {
334
- this.ui.addChild(new DynamicBorder());
335
- if (settings.get("collapseChangelog")) {
336
- const condensedText = `Updated to v${this.#version}. Use ${theme.bold("/changelog")} to view full changelog.`;
337
- this.ui.addChild(new Text(condensedText, 1, 0));
338
- } else {
339
- this.ui.addChild(new Text(theme.bold(theme.fg("contentAccent", "What's New")), 1, 0));
340
- this.ui.addChild(new Spacer(1));
341
- this.ui.addChild(new Markdown(this.#changelogMarkdown.trim(), 1, 0, getMarkdownTheme()));
342
- this.ui.addChild(new Spacer(1));
343
- }
344
- this.ui.addChild(new DynamicBorder());
345
- }
346
340
  }
347
341
 
348
342
  this.ui.addChild(this.chatContainer);
@@ -1130,8 +1124,9 @@ export class InteractiveMode implements InteractiveModeContext {
1130
1124
  this.setWorkingMessage(message);
1131
1125
  }
1132
1126
 
1133
- showNewVersionNotification(newVersion: string): void {
1134
- this.#uiHelpers.showNewVersionNotification(newVersion);
1127
+ setUpdateStatus(status: UpdateStatus | undefined): void {
1128
+ this.#welcomeComponent?.setUpdateStatus(status);
1129
+ this.ui.requestRender();
1135
1130
  }
1136
1131
 
1137
1132
  clearEditor(): void {
@@ -137,7 +137,6 @@ 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
- showNewVersionNotification(newVersion: string): void;
141
140
  clearEditor(): void;
142
141
  updatePendingMessagesDisplay(): void;
143
142
  queueCompactionMessage(text: string, mode: "steer" | "followUp"): void;
@@ -7,7 +7,6 @@ import { BashExecutionComponent } from "../../modes/components/bash-execution";
7
7
  import { BranchSummaryMessageComponent } from "../../modes/components/branch-summary-message";
8
8
  import { CompactionSummaryMessageComponent } from "../../modes/components/compaction-summary-message";
9
9
  import { CustomMessageComponent } from "../../modes/components/custom-message";
10
- import { DynamicBorder } from "../../modes/components/dynamic-border";
11
10
  import {
12
11
  createSystemGutter,
13
12
  createTextGutter,
@@ -456,23 +455,6 @@ export class UiHelpers {
456
455
  this.ctx.ui.requestRender();
457
456
  }
458
457
 
459
- showNewVersionNotification(newVersion: string): void {
460
- this.ctx.chatContainer.addChild(new Spacer(1));
461
- this.ctx.chatContainer.addChild(new DynamicBorder(text => theme.fg("warning", text)));
462
- this.ctx.chatContainer.addChild(
463
- new Text(
464
- theme.bold(theme.fg("warning", "Update Available")) +
465
- "\n" +
466
- theme.fg("muted", `New version ${newVersion} is available. Run: `) +
467
- theme.fg("contentAccent", "xcsh update"),
468
- 1,
469
- 0,
470
- ),
471
- );
472
- this.ctx.chatContainer.addChild(new DynamicBorder(text => theme.fg("warning", text)));
473
- this.ctx.ui.requestRender();
474
- }
475
-
476
458
  updatePendingMessagesDisplay(): void {
477
459
  this.ctx.pendingMessagesContainer.clear();
478
460
  const queuedMessages = this.ctx.session.getQueuedMessages() as QueuedMessages;