@f5-sales-demo/xcsh 19.58.0 → 19.58.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@f5-sales-demo/xcsh",
4
- "version": "19.58.0",
4
+ "version": "19.58.1",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://github.com/f5-sales-demo/xcsh",
7
7
  "author": "Can Boluk",
@@ -55,13 +55,13 @@
55
55
  "dependencies": {
56
56
  "@agentclientprotocol/sdk": "0.16.1",
57
57
  "@mozilla/readability": "^0.6",
58
- "@f5-sales-demo/xcsh-stats": "19.58.0",
59
- "@f5-sales-demo/pi-agent-core": "19.58.0",
60
- "@f5-sales-demo/pi-ai": "19.58.0",
61
- "@f5-sales-demo/pi-natives": "19.58.0",
62
- "@f5-sales-demo/pi-resource-management": "19.58.0",
63
- "@f5-sales-demo/pi-tui": "19.58.0",
64
- "@f5-sales-demo/pi-utils": "19.58.0",
58
+ "@f5-sales-demo/xcsh-stats": "19.58.1",
59
+ "@f5-sales-demo/pi-agent-core": "19.58.1",
60
+ "@f5-sales-demo/pi-ai": "19.58.1",
61
+ "@f5-sales-demo/pi-natives": "19.58.1",
62
+ "@f5-sales-demo/pi-resource-management": "19.58.1",
63
+ "@f5-sales-demo/pi-tui": "19.58.1",
64
+ "@f5-sales-demo/pi-utils": "19.58.1",
65
65
  "@sinclair/typebox": "^0.34",
66
66
  "@xterm/headless": "^6.0",
67
67
  "ajv": "^8.20",
@@ -17,17 +17,17 @@ export interface BuildInfo {
17
17
  }
18
18
 
19
19
  export const BUILD_INFO: BuildInfo = {
20
- "version": "19.58.0",
21
- "commit": "def3667fcf2f20c14de7b9ab37b29e85b390e5c4",
22
- "shortCommit": "def3667",
20
+ "version": "19.58.1",
21
+ "commit": "f72a3fa845f24642e4db4d84aa427bb2e2d64538",
22
+ "shortCommit": "f72a3fa",
23
23
  "branch": "main",
24
- "tag": "v19.58.0",
25
- "commitDate": "2026-07-05T03:37:49Z",
26
- "buildDate": "2026-07-05T04:00:08.049Z",
24
+ "tag": "v19.58.1",
25
+ "commitDate": "2026-07-05T13:33:19Z",
26
+ "buildDate": "2026-07-05T13:53:37.495Z",
27
27
  "dirty": true,
28
28
  "prNumber": "",
29
29
  "repoUrl": "https://github.com/f5-sales-demo/xcsh",
30
30
  "repoSlug": "f5-sales-demo/xcsh",
31
- "commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/def3667fcf2f20c14de7b9ab37b29e85b390e5c4",
32
- "releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v19.58.0"
31
+ "commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/f72a3fa845f24642e4db4d84aa427bb2e2d64538",
32
+ "releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v19.58.1"
33
33
  };
package/src/main.ts CHANGED
@@ -57,6 +57,7 @@ import type { SubmittedUserInput } from "./modes/types";
57
57
  import { type CreateAgentSessionOptions, createAgentSession, discoverAuthStorage } from "./sdk";
58
58
  import type { AgentSession } from "./session/agent-session";
59
59
  import { resolveResumableSession, type SessionInfo, SessionManager } from "./session/session-manager";
60
+ import { profileDump, profileMark } from "./startup-profile";
60
61
  import { resolvePromptInput } from "./system-prompt";
61
62
  import type { LspStartupServerInfo } from "./tools";
62
63
  import type { EventBus } from "./utils/event-bus";
@@ -146,8 +147,6 @@ export async function submitInteractiveInput(
146
147
  }
147
148
  }
148
149
 
149
- const INITIAL_UPDATE_CHECK_TIMEOUT_MS = 500;
150
-
151
150
  async function runInteractiveMode(
152
151
  session: AgentSession,
153
152
  version: string,
@@ -161,23 +160,26 @@ async function runInteractiveMode(
161
160
  initialMessage?: string,
162
161
  initialImages?: ImageContent[],
163
162
  ): Promise<void> {
164
- const initialUpdateVersion = await Promise.race([
165
- versionCheckPromise.catch(() => undefined),
166
- new Promise<string | undefined>(resolve => setTimeout(() => resolve(undefined), INITIAL_UPDATE_CHECK_TIMEOUT_MS)),
167
- ]);
163
+ profileMark("interactive: enter runInteractiveMode");
168
164
 
169
165
  const mode = new InteractiveMode(session, version, setExtensionUIContext, lspServers, mcpManager, eventBus);
170
166
 
171
167
  await mode.init();
168
+ profileMark("interactive: mode.init() done");
172
169
 
173
- // Non-blocking, one-shot update notice: only surface it if the background check
174
- // already resolved within the startup race (no waiting, no live re-render). If it
175
- // resolves later, the update is available on demand via /plugins.
176
- if (initialUpdateVersion && settings.get("startup.checkUpdate")) {
177
- mode.showStatus(`Update available: v${initialUpdateVersion} — run: xcsh update`, { dim: true });
170
+ // Update notice: fully non-blocking. Startup never waits on the network version
171
+ // check; surface the notice whenever it resolves (typically after the prompt is
172
+ // already up). If it never resolves, the update is available on demand via /plugins.
173
+ if (settings.get("startup.checkUpdate")) {
174
+ void versionCheckPromise
175
+ .then(latest => {
176
+ if (latest) mode.showStatus(`Update available: v${latest} — run: xcsh update`, { dim: true });
177
+ })
178
+ .catch(() => {});
178
179
  }
179
180
 
180
181
  mode.renderInitialMessages();
182
+ profileMark("interactive: initial render requested");
181
183
 
182
184
  for (const notify of notifs) {
183
185
  if (!notify) {
@@ -210,6 +212,8 @@ async function runInteractiveMode(
210
212
  }
211
213
  }
212
214
 
215
+ profileMark("interactive: READY — awaiting first input");
216
+ profileDump();
213
217
  while (true) {
214
218
  const input = await mode.getUserInput();
215
219
  await submitInteractiveInput(mode, session, input);
@@ -551,6 +555,7 @@ async function buildSessionOptions(
551
555
 
552
556
  export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<void> {
553
557
  logger.startTiming();
558
+ profileMark("entry: runtime + module-graph loaded (pre-main)");
554
559
 
555
560
  // Initialize theme early with defaults (CLI commands need symbols)
556
561
  // Will be re-initialized with user preferences later
@@ -895,6 +900,7 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
895
900
  sessionOptions,
896
901
  );
897
902
  logger.time("main:afterCreateSession");
903
+ profileMark("createAgentSession done");
898
904
  if (parsedArgs.apiKey && !sessionOptions.model && session.model) {
899
905
  authStorage.setRuntimeApiKey(session.model.provider, parsedArgs.apiKey);
900
906
  }
@@ -274,10 +274,12 @@ export class ExtensionUiController {
274
274
  this.showExtensionError(error.extensionPath, error.error);
275
275
  });
276
276
 
277
- // Emit session_start event
278
- await extensionRunner.emit({
279
- type: "session_start",
280
- });
277
+ // Emit session_start in the BACKGROUND so a slow hook (e.g. a plugin doing a
278
+ // network/CLI check) can never block the TUI paint. The runner discards
279
+ // session_start handler results, so nothing downstream depends on completion;
280
+ // hooks that update the UI (widgets/status) render progressively as they finish.
281
+ // Handler errors are still surfaced via the onError subscription above.
282
+ void extensionRunner.emit({ type: "session_start" });
281
283
  }
282
284
 
283
285
  setHookWidget(key: string, content: ExtensionWidgetContent, options?: ExtensionWidgetOptions): void {
@@ -35,6 +35,7 @@ import planModeApprovedPrompt from "../prompts/system/plan-mode-approved.md" wit
35
35
  import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
36
36
  import { HistoryStorage } from "../session/history-storage";
37
37
  import type { SessionContext, SessionManager } from "../session/session-manager";
38
+ import { profileMark } from "../startup-profile";
38
39
  import { STTController, type SttState } from "../stt";
39
40
  import type { ExitPlanModeDetails } from "../tools";
40
41
  import type { EventBus } from "../utils/event-bus";
@@ -294,6 +295,7 @@ export class InteractiveMode implements InteractiveModeContext {
294
295
  async init(): Promise<void> {
295
296
  if (this.isInitialized) return;
296
297
 
298
+ profileMark("init: start");
297
299
  logger.time("InteractiveMode.init:keybindings");
298
300
  this.keybindings = KeybindingsManager.create();
299
301
 
@@ -305,6 +307,7 @@ export class InteractiveMode implements InteractiveModeContext {
305
307
  this.refreshSlashCommandState.bind(this),
306
308
  getProjectDir(),
307
309
  );
310
+ profileMark("init: refreshSlashCommandState done");
308
311
 
309
312
  // Refresh user profile in background — fire and forget
310
313
  reconcileFromCollectors().catch(err => logger.warn("Background profile refresh failed", { error: String(err) }));
@@ -368,10 +371,12 @@ export class InteractiveMode implements InteractiveModeContext {
368
371
 
369
372
  // Load initial todos
370
373
  await this.#loadTodoList();
374
+ profileMark("init: loadTodoList done");
371
375
 
372
376
  // Start the UI
373
377
  const clearScreen = settings.get("startup.clearScreen");
374
378
  this.ui.start(clearScreen);
379
+ profileMark("init: ui.start done");
375
380
  pushTerminalTitle();
376
381
  setSessionTerminalTitle(this.sessionManager.getSessionName(), this.sessionManager.getCwd());
377
382
  this.#syncEditorMaxHeight();
@@ -379,9 +384,11 @@ export class InteractiveMode implements InteractiveModeContext {
379
384
 
380
385
  // Initialize hooks with TUI-based UI context
381
386
  await this.initHooksAndCustomTools();
387
+ profileMark("init: initHooksAndCustomTools done");
382
388
 
383
389
  // Restore mode from session (e.g. plan mode on resume)
384
390
  await this.#restoreModeFromSession();
391
+ profileMark("init: restoreModeFromSession done");
385
392
 
386
393
  // Subscribe to agent events
387
394
  this.#subscribeToAgent();
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Opt-in startup profiler — `PI_STARTUP_PROFILE=1`.
3
+ *
4
+ * PI_TIMING exits before the TUI paints, so it cannot measure `runInteractiveMode`
5
+ * or the pre-`main` module-graph load. This records labeled timestamps (ms since
6
+ * process start, via `performance.now()`, which is measured from runtime start) and
7
+ * dumps the full boot timeline to a FILE at the input-ready point — so nothing
8
+ * corrupts the live TUI screen. Zero cost when the env var is unset.
9
+ *
10
+ * The FIRST mark's timestamp is the time from process start to that mark, i.e. it
11
+ * captures the runtime init + evaluation of the embedded module graph that happens
12
+ * before any of our code runs. Later marks show the per-step deltas through paint.
13
+ *
14
+ * Usage: PI_STARTUP_PROFILE=1 xcsh (then quit; read /tmp/xcsh-startup-profile.txt,
15
+ * or set PI_STARTUP_PROFILE_FILE to choose the path).
16
+ */
17
+ import { writeFileSync } from "node:fs";
18
+
19
+ const ENABLED = !!process.env.PI_STARTUP_PROFILE;
20
+ const marks: Array<[label: string, atMs: number]> = [];
21
+
22
+ /** Record a labeled timestamp (no-op unless PI_STARTUP_PROFILE is set). */
23
+ export function profileMark(label: string): void {
24
+ if (ENABLED) marks.push([label, performance.now()]);
25
+ }
26
+
27
+ /** Write the collected timeline to a file (no-op unless enabled / no marks). */
28
+ export function profileDump(): void {
29
+ if (!ENABLED || marks.length === 0) return;
30
+ const lines = ["=== xcsh startup profile (ms since process start) ===", ""];
31
+ let prev = 0;
32
+ for (const [label, at] of marks) {
33
+ const delta = at - prev;
34
+ lines.push(` ${at.toFixed(1).padStart(9)} ms (+${delta.toFixed(1).padStart(8)} ms) ${label}`);
35
+ prev = at;
36
+ }
37
+ lines.push(
38
+ "",
39
+ ` first mark = runtime + module-graph load; total to ready = ${marks[marks.length - 1][1].toFixed(1)} ms`,
40
+ );
41
+ const file = process.env.PI_STARTUP_PROFILE_FILE || "/tmp/xcsh-startup-profile.txt";
42
+ try {
43
+ writeFileSync(file, `${lines.join("\n")}\n`);
44
+ process.stderr.write(`[xcsh] startup profile written to ${file}\n`);
45
+ } catch {
46
+ /* best-effort diagnostic */
47
+ }
48
+ }