@f5xc-salesdemos/xcsh 18.70.0 → 18.75.3

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 CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.75.0] - 2026-05-23
6
+
7
+ ### Added
8
+
9
+ - Auto-validate credentials on context switch: SDK context change listener now fires a background `validateToken()` after emitting the switch notification, sending a follow-up `context_validation_result` custom message so the LLM knows whether the new context's credentials are valid
10
+
5
11
  ## [18.64.2] - 2026-05-16
6
12
 
7
13
  ### Fixed
@@ -20,11 +26,13 @@
20
26
  ### Changed
21
27
 
22
28
  - Regenerated API spec index from catalog v2.1.82: http_loadbalancer CRUD verification corrections (6 new server defaults, corrected minimum configs, cross-field dependencies, default_pool inline pool discovery, 5 composable routing approaches) and tcp_loadbalancer minimum config corrections (listen_port, origin_pools_weights, do_not_advertise format, 9 server defaults, forced hash_policy default) ([#753](https://github.com/f5xc-salesdemos/xcsh/issues/753), [#757](https://github.com/f5xc-salesdemos/xcsh/issues/757))
29
+
23
30
  ## [18.53.0] - 2026-05-09
24
31
 
25
32
  ### Changed
26
33
 
27
34
  - Autoresearch subsystem code quality: -513 lines (18.8% reduction), ~13% faster type checking. Un-exported internal symbols, relocated types, consolidated duplicate patterns, replaced manual deep copies with `structuredClone`, replaced `while(exec)` with `matchAll`, compressed control flow, extracted shared interfaces ([#734](https://github.com/f5xc-salesdemos/xcsh/pull/734))
35
+
28
36
  ## [18.53.0] - 2026-05-09
29
37
 
30
38
  ### Fixed
@@ -79,13 +87,11 @@
79
87
 
80
88
  ## [18.18.4] - 2026-04-26
81
89
 
82
-
83
90
  ### Fixed
84
91
 
85
92
  - Fixed gutter width propagation in the fallback tool renderer: `#formatToolExecution()` now receives the actual available width at render-time and uses it for line truncation instead of a hardcoded 80-column limit. On narrow terminals (<82 cols) this prevents content wider than the gutter-adjusted viewport; on wide terminals it allows longer output lines. ([#117](https://github.com/f5xc-salesdemos/xcsh/issues/117))
86
93
  - Fixed `resolveConfigValue` returning literal env var names (e.g. `"LITELLM_API_KEY"`) as API keys when the env var is unset, causing 401 errors on first launch. The resolver now rejects unresolved `ALL_CAPS_WITH_UNDERSCORES` patterns, matching the existing guard in `resolveYamlApiKeyConfig`. ([#241](https://github.com/f5xc-salesdemos/xcsh/issues/241))
87
94
 
88
-
89
95
  ### Changed
90
96
 
91
97
  - 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))
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@f5xc-salesdemos/xcsh",
4
- "version": "18.70.0",
4
+ "version": "18.75.3",
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",
@@ -48,12 +48,12 @@
48
48
  "dependencies": {
49
49
  "@agentclientprotocol/sdk": "0.16.1",
50
50
  "@mozilla/readability": "^0.6",
51
- "@f5xc-salesdemos/xcsh-stats": "18.70.0",
52
- "@f5xc-salesdemos/pi-agent-core": "18.70.0",
53
- "@f5xc-salesdemos/pi-ai": "18.70.0",
54
- "@f5xc-salesdemos/pi-natives": "18.70.0",
55
- "@f5xc-salesdemos/pi-tui": "18.70.0",
56
- "@f5xc-salesdemos/pi-utils": "18.70.0",
51
+ "@f5xc-salesdemos/xcsh-stats": "workspace:*",
52
+ "@f5xc-salesdemos/pi-agent-core": "workspace:*",
53
+ "@f5xc-salesdemos/pi-ai": "workspace:*",
54
+ "@f5xc-salesdemos/pi-natives": "workspace:*",
55
+ "@f5xc-salesdemos/pi-tui": "workspace:*",
56
+ "@f5xc-salesdemos/pi-utils": "workspace:*",
57
57
  "@sinclair/typebox": "^0.34",
58
58
  "@xterm/headless": "^6.0",
59
59
  "ajv": "^8.18",
@@ -17,17 +17,17 @@ export interface BuildInfo {
17
17
  }
18
18
 
19
19
  export const BUILD_INFO: BuildInfo = {
20
- "version": "18.70.0",
21
- "commit": "34f16142c582f937c72feae361239b82a67657df",
22
- "shortCommit": "34f1614",
20
+ "version": "18.75.3",
21
+ "commit": "6856457e251f8583b237e72702ac395bc7173f42",
22
+ "shortCommit": "6856457",
23
23
  "branch": "main",
24
- "tag": "v18.70.0",
25
- "commitDate": "2026-05-19T06:13:53Z",
26
- "buildDate": "2026-05-19T06:39:53.619Z",
24
+ "tag": "v18.75.3",
25
+ "commitDate": "2026-05-23T08:23:41Z",
26
+ "buildDate": "2026-05-23T17:15:18.019Z",
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/34f16142c582f937c72feae361239b82a67657df",
32
- "releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v18.70.0"
31
+ "commitUrl": "https://github.com/f5xc-salesdemos/xcsh/commit/6856457e251f8583b237e72702ac395bc7173f42",
32
+ "releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v18.75.3"
33
33
  };
@@ -10,6 +10,18 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
10
10
  path: { abbreviate: true, maxLength: 40, stripWorkPrefix: true },
11
11
  git: { showBranch: true, showStaged: true, showUnstaged: true, showUntracked: true },
12
12
  },
13
+ dropOrder: [
14
+ "pr",
15
+ "token_total",
16
+ "cost",
17
+ "git",
18
+ "path",
19
+ "context_pct",
20
+ "plan_mode",
21
+ "model",
22
+ "pi",
23
+ "context_f5xc",
24
+ ],
13
25
  },
14
26
 
15
27
  minimal: {
@@ -20,6 +32,7 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
20
32
  path: { abbreviate: true, maxLength: 30 },
21
33
  git: { showBranch: true, showStaged: false, showUnstaged: false, showUntracked: false },
22
34
  },
35
+ dropOrder: ["git", "plan_mode", "context_pct", "path", "context_f5xc"],
23
36
  },
24
37
 
25
38
  compact: {
@@ -30,6 +43,7 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
30
43
  model: { showThinkingLevel: false },
31
44
  git: { showBranch: true, showStaged: true, showUnstaged: true, showUntracked: false },
32
45
  },
46
+ dropOrder: ["pr", "cost", "git", "context_pct", "plan_mode", "model", "context_f5xc"],
33
47
  },
34
48
 
35
49
  full: {
@@ -52,6 +66,25 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
52
66
  git: { showBranch: true, showStaged: true, showUnstaged: true, showUntracked: true },
53
67
  time: { format: "24h", showSeconds: false },
54
68
  },
69
+ dropOrder: [
70
+ "time",
71
+ "time_spent",
72
+ "cache_read",
73
+ "token_rate",
74
+ "token_out",
75
+ "token_in",
76
+ "subagents",
77
+ "pr",
78
+ "git",
79
+ "path",
80
+ "cost",
81
+ "context_pct",
82
+ "plan_mode",
83
+ "hostname",
84
+ "model",
85
+ "pi",
86
+ "context_f5xc",
87
+ ],
55
88
  },
56
89
 
57
90
  nerd: {
@@ -77,6 +110,28 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
77
110
  git: { showBranch: true, showStaged: true, showUnstaged: true, showUntracked: true },
78
111
  time: { format: "24h", showSeconds: true },
79
112
  },
113
+ dropOrder: [
114
+ "context_total",
115
+ "cache_write",
116
+ "session",
117
+ "time",
118
+ "time_spent",
119
+ "cache_read",
120
+ "token_rate",
121
+ "token_out",
122
+ "token_in",
123
+ "subagents",
124
+ "pr",
125
+ "git",
126
+ "path",
127
+ "cost",
128
+ "context_pct",
129
+ "plan_mode",
130
+ "hostname",
131
+ "model",
132
+ "pi",
133
+ "context_f5xc",
134
+ ],
80
135
  },
81
136
 
82
137
  ascii: {
@@ -89,6 +144,7 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
89
144
  path: { abbreviate: true, maxLength: 40 },
90
145
  git: { showBranch: true, showStaged: true, showUnstaged: true, showUntracked: true },
91
146
  },
147
+ dropOrder: ["pr", "token_total", "cost", "git", "path", "context_pct", "model", "context_f5xc"],
92
148
  },
93
149
 
94
150
  xcsh: {
@@ -101,6 +157,7 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
101
157
  git: { showBranch: true, showStaged: true, showUnstaged: true, showUntracked: true },
102
158
  context_pct: { compact: true },
103
159
  },
160
+ dropOrder: ["plan_mode", "git", "path", "context_pct", "context_f5xc"],
104
161
  },
105
162
 
106
163
  custom: {
@@ -461,6 +461,25 @@ export const SEGMENTS: Record<StatusLineSegmentId, StatusLineSegment> = {
461
461
  return { content: "", visible: false };
462
462
  }
463
463
  },
464
+ truncate(maxWidth: number, _ctx: SegmentContext): RenderedSegment | null {
465
+ try {
466
+ const { truncateF5XCContextSegment } = require("../../../services/f5xc-context-segment");
467
+ const result = truncateF5XCContextSegment(maxWidth);
468
+ if (!result) return null;
469
+ let bg = theme.fgColorAsBg("statusLineContextF5xcBg");
470
+ let fg = theme.getFgAnsi("statusLineContextF5xcFg");
471
+ if (result.tokenHealth === "expiring") {
472
+ bg = theme.fgColorAsBg("statusLineGitDirtyBg");
473
+ fg = theme.getFgAnsi("statusLineGitDirtyFg");
474
+ } else if (result.tokenHealth === "expired") {
475
+ bg = theme.fgColorAsBg("statusLineGitConflictBg");
476
+ fg = theme.getFgAnsi("statusLineGitConflictFg");
477
+ }
478
+ return { ...result, bg, fg };
479
+ } catch {
480
+ return null;
481
+ }
482
+ },
464
483
  },
465
484
  };
466
485
 
@@ -66,6 +66,7 @@ export interface RenderedSegment {
66
66
  export interface StatusLineSegment {
67
67
  id: StatusLineSegmentId;
68
68
  render(ctx: SegmentContext): RenderedSegment;
69
+ truncate?(maxWidth: number, ctx: SegmentContext): RenderedSegment | null;
69
70
  }
70
71
 
71
72
  // ═══════════════════════════════════════════════════════════════════════════
@@ -91,4 +92,5 @@ export interface PresetDef {
91
92
  rightSegments: StatusLineSegmentId[];
92
93
  separator: StatusLineSeparatorStyle;
93
94
  segmentOptions?: StatusLineSegmentOptions;
95
+ dropOrder?: StatusLineSegmentId[];
94
96
  }
@@ -20,7 +20,7 @@ import {
20
20
  type PrCacheContext,
21
21
  } from "./status-line/git-utils";
22
22
  import { getPreset } from "./status-line/presets";
23
- import { renderSegment, type SegmentContext } from "./status-line/segments";
23
+ import { renderSegment, SEGMENTS, type SegmentContext } from "./status-line/segments";
24
24
  import { getSeparator } from "./status-line/separators";
25
25
  import { calculateTokensPerSecond } from "./status-line/token-rate";
26
26
 
@@ -490,8 +490,9 @@ export class StatusLineComponent implements Component {
490
490
 
491
491
  // Collect visible segments (preserving bg/fg metadata)
492
492
  type SegPart = { content: string; bg: string; fg: string };
493
- const collectParts = (segIds: readonly string[]): SegPart[] => {
493
+ const collectParts = (segIds: readonly string[]): { parts: SegPart[]; ids: string[] } => {
494
494
  const parts: SegPart[] = [];
495
+ const ids: string[] = [];
495
496
  for (const segId of segIds) {
496
497
  const rendered = renderSegment(segId as any, ctx);
497
498
  if (rendered.visible && rendered.content) {
@@ -500,13 +501,14 @@ export class StatusLineComponent implements Component {
500
501
  bg: rendered.bg || defaultBg,
501
502
  fg: rendered.fg || defaultFg,
502
503
  });
504
+ ids.push(segId);
503
505
  }
504
506
  }
505
- return parts;
507
+ return { parts, ids };
506
508
  };
507
509
 
508
- const leftParts = collectParts(effectiveSettings.leftSegments);
509
- const rightParts = collectParts(effectiveSettings.rightSegments);
510
+ const { parts: leftParts, ids: leftIds } = collectParts(effectiveSettings.leftSegments);
511
+ const { parts: rightParts, ids: rightIds } = collectParts(effectiveSettings.rightSegments);
510
512
 
511
513
  const runningBackgroundJobs = this.session.getAsyncJobSnapshot()?.running.length ?? 0;
512
514
  if (runningBackgroundJobs > 0) {
@@ -534,6 +536,8 @@ export class StatusLineComponent implements Component {
534
536
  const topFillWidth = Math.max(0, width);
535
537
  const left = [...leftParts];
536
538
  const right = [...rightParts];
539
+ const leftSegIds = [...leftIds];
540
+ const rightSegIds = [...rightIds];
537
541
 
538
542
  const sepWidth = visibleWidth(separatorDef.left);
539
543
  const capWidth = separatorDef.endCaps ? visibleWidth(separatorDef.endCaps.right) : 0;
@@ -541,23 +545,74 @@ export class StatusLineComponent implements Component {
541
545
  const groupWidth = (parts: SegPart[]): number => {
542
546
  if (parts.length === 0) return 0;
543
547
  const partsWidth = parts.reduce((sum, p) => sum + visibleWidth(p.content), 0);
544
- // Each segment gets 1 char padding on each side, separators between segments
545
548
  const sepTotal = Math.max(0, parts.length - 1) * sepWidth;
546
549
  return partsWidth + parts.length * 2 + sepTotal + capWidth;
547
550
  };
548
551
 
549
552
  let leftWidth = groupWidth(left);
550
553
  let rightWidth = groupWidth(right);
551
- const totalWidth = () => leftWidth + rightWidth + (left.length > 0 && right.length > 0 ? 1 : 0);
554
+ const totalWidth = () => leftWidth + rightWidth;
555
+
556
+ if (topFillWidth > 0 && totalWidth() > topFillWidth) {
557
+ const preset = getPreset(effectiveSettings.preset ?? "default");
558
+ const dropOrder = preset.dropOrder;
559
+
560
+ if (dropOrder) {
561
+ type SegRef = { group: "left" | "right"; part: SegPart; segId: string };
562
+ const segRefs: SegRef[] = [
563
+ ...left.map((part, i) => ({ group: "left" as const, part, segId: leftSegIds[i] })),
564
+ ...right.map((part, i) => ({ group: "right" as const, part, segId: rightSegIds[i] })),
565
+ ];
566
+
567
+ const inOrder: SegRef[] = [];
568
+ const notInOrder: SegRef[] = [];
569
+ for (const ref of segRefs) {
570
+ const orderIdx = dropOrder.indexOf(ref.segId as any);
571
+ if (orderIdx === -1) notInOrder.push(ref);
572
+ else inOrder.push(ref);
573
+ }
574
+ inOrder.sort((a, b) => dropOrder.indexOf(a.segId as any) - dropOrder.indexOf(b.segId as any));
575
+ const dropQueue = [...notInOrder, ...inOrder];
576
+
577
+ for (const ref of dropQueue) {
578
+ if (totalWidth() <= topFillWidth) break;
579
+
580
+ const segDef = SEGMENTS[ref.segId as StatusLineSegmentId];
581
+ const group = ref.group === "left" ? left : right;
582
+
583
+ const currentIdx = group.indexOf(ref.part);
584
+ if (currentIdx === -1) continue;
585
+
586
+ if (segDef?.truncate) {
587
+ const currentContentWidth = visibleWidth(ref.part.content);
588
+ const deficit = totalWidth() - topFillWidth;
589
+ const targetWidth = Math.max(1, currentContentWidth - deficit);
590
+ const truncated = segDef.truncate(targetWidth, ctx);
591
+ if (truncated?.visible && truncated.content) {
592
+ group[currentIdx] = {
593
+ content: truncated.content,
594
+ bg: truncated.bg || ref.part.bg,
595
+ fg: truncated.fg || ref.part.fg,
596
+ };
597
+ if (ref.group === "left") leftWidth = groupWidth(left);
598
+ else rightWidth = groupWidth(right);
599
+ continue;
600
+ }
601
+ }
552
602
 
553
- if (topFillWidth > 0) {
554
- while (totalWidth() > topFillWidth && right.length > 0) {
555
- right.pop();
556
- rightWidth = groupWidth(right);
557
- }
558
- while (totalWidth() > topFillWidth && left.length > 0) {
559
- left.pop();
560
- leftWidth = groupWidth(left);
603
+ group.splice(currentIdx, 1);
604
+ if (ref.group === "left") leftWidth = groupWidth(left);
605
+ else rightWidth = groupWidth(right);
606
+ }
607
+ } else {
608
+ while (totalWidth() > topFillWidth && right.length > 0) {
609
+ right.pop();
610
+ rightWidth = groupWidth(right);
611
+ }
612
+ while (totalWidth() > topFillWidth && left.length > 0) {
613
+ left.pop();
614
+ leftWidth = groupWidth(left);
615
+ }
561
616
  }
562
617
  }
563
618
 
@@ -636,13 +691,16 @@ export class StatusLineComponent implements Component {
636
691
  const rightGroup = renderGroup(right, "right");
637
692
  if (!leftGroup && !rightGroup) return "";
638
693
 
639
- if (topFillWidth === 0 || left.length === 0 || right.length === 0) {
694
+ if (topFillWidth === 0 || (left.length === 0 && right.length === 0)) {
640
695
  return leftGroup + (leftGroup && rightGroup ? " " : "") + rightGroup;
641
696
  }
642
697
 
643
- leftWidth = groupWidth(left);
644
- rightWidth = groupWidth(right);
645
- const gapWidth = Math.max(1, topFillWidth - leftWidth - rightWidth);
698
+ const actualLeftWidth = visibleWidth(leftGroup);
699
+ const actualRightWidth = visibleWidth(rightGroup);
700
+ const gapWidth = Math.max(0, topFillWidth - actualLeftWidth - actualRightWidth);
701
+ if (gapWidth === 0) {
702
+ return leftGroup + rightGroup;
703
+ }
646
704
  const gapFill = theme.fg("border", theme.boxRound.horizontal.repeat(gapWidth));
647
705
  return leftGroup + gapFill + rightGroup;
648
706
  }