@f5xc-salesdemos/xcsh 19.43.0 → 19.43.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": "19.43.0",
4
+ "version": "19.43.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",
@@ -52,13 +52,13 @@
52
52
  "dependencies": {
53
53
  "@agentclientprotocol/sdk": "0.16.1",
54
54
  "@mozilla/readability": "^0.6",
55
- "@f5xc-salesdemos/xcsh-stats": "19.43.0",
56
- "@f5xc-salesdemos/pi-agent-core": "19.43.0",
57
- "@f5xc-salesdemos/pi-ai": "19.43.0",
58
- "@f5xc-salesdemos/pi-natives": "19.43.0",
59
- "@f5xc-salesdemos/pi-resource-management": "19.43.0",
60
- "@f5xc-salesdemos/pi-tui": "19.43.0",
61
- "@f5xc-salesdemos/pi-utils": "19.43.0",
55
+ "@f5xc-salesdemos/xcsh-stats": "19.43.2",
56
+ "@f5xc-salesdemos/pi-agent-core": "19.43.2",
57
+ "@f5xc-salesdemos/pi-ai": "19.43.2",
58
+ "@f5xc-salesdemos/pi-natives": "19.43.2",
59
+ "@f5xc-salesdemos/pi-resource-management": "19.43.2",
60
+ "@f5xc-salesdemos/pi-tui": "19.43.2",
61
+ "@f5xc-salesdemos/pi-utils": "19.43.2",
62
62
  "@sinclair/typebox": "^0.34",
63
63
  "@xterm/headless": "^6.0",
64
64
  "ajv": "^8.20",
@@ -17,17 +17,17 @@ export interface BuildInfo {
17
17
  }
18
18
 
19
19
  export const BUILD_INFO: BuildInfo = {
20
- "version": "19.43.0",
21
- "commit": "d8fefbb60bd96f414bb9c21da7df270bd10876eb",
22
- "shortCommit": "d8fefbb",
20
+ "version": "19.43.2",
21
+ "commit": "09742430243eddeb0ed597e94079c21ceca02d7f",
22
+ "shortCommit": "0974243",
23
23
  "branch": "main",
24
- "tag": "v19.43.0",
25
- "commitDate": "2026-06-24T04:38:55Z",
26
- "buildDate": "2026-06-24T05:01:00.600Z",
24
+ "tag": "v19.43.2",
25
+ "commitDate": "2026-06-24T16:28:28Z",
26
+ "buildDate": "2026-06-24T16:56:22.826Z",
27
27
  "dirty": true,
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/d8fefbb60bd96f414bb9c21da7df270bd10876eb",
32
- "releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v19.43.0"
31
+ "commitUrl": "https://github.com/f5xc-salesdemos/xcsh/commit/09742430243eddeb0ed597e94079c21ceca02d7f",
32
+ "releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v19.43.2"
33
33
  };
@@ -41,6 +41,33 @@ export interface RenderMermaidThemedOptions {
41
41
  colorMode?: MermaidColorMode;
42
42
  /** Extra layout options (padding, useAscii, …). */
43
43
  render?: MermaidAsciiRenderOptions;
44
+ /**
45
+ * Available content width. When set (and paddingX isn't overridden), node spacing
46
+ * is expanded toward this width so the diagram uses the available space instead of
47
+ * rendering tiny — capped so small diagrams don't sprawl.
48
+ */
49
+ targetWidth?: number;
50
+ }
51
+
52
+ const DEFAULT_PADDING_X = 2;
53
+ const DEFAULT_PADDING_Y = 1;
54
+ // Candidate horizontal spacings (ascending). Widening paddingX grows width WITHOUT
55
+ // growing height, so we can use much of the terminal; capped so a tiny diagram
56
+ // doesn't sprawl into absurdly long edges on a very wide terminal.
57
+ const PADDING_X_CANDIDATES = [1, 2, 4, 6, 8, 10, 12, 16, 20, 24];
58
+
59
+ /** Largest candidate paddingX whose rendered width still fits targetWidth (>= smallest). */
60
+ function pickPaddingXForWidth(source: string, targetWidth: number): number {
61
+ let best = PADDING_X_CANDIDATES[0]!;
62
+ for (const px of PADDING_X_CANDIDATES) {
63
+ const out = renderMermaidAsciiSafe(source, { colorMode: "none", paddingX: px, paddingY: DEFAULT_PADDING_Y });
64
+ if (out == null) break;
65
+ let w = 0;
66
+ for (const line of out.split("\n")) w = Math.max(w, Bun.stringWidth(line));
67
+ if (w <= targetWidth) best = px;
68
+ else break; // wider candidates only grow; stop early (also avoids costly large-padding renders)
69
+ }
70
+ return best;
44
71
  }
45
72
 
46
73
  /**
@@ -54,25 +81,27 @@ export function renderMermaidThemed(source: string, theme: Theme, opts?: RenderM
54
81
  if (mermaidSourceExceedsLimit(source)) return null;
55
82
 
56
83
  const mode = colorModeFor(theme, opts?.colorMode);
57
- // Layout options (useAscii, padding, …) change the output, so they must be part
58
- // of the key. Omitted when absent so the key matches mermaidThemeSignature() —
59
- // the no-options form used by the inline-markdown prerender/lookup path.
60
- const optsSig = opts?.render && Object.keys(opts.render).length > 0 ? `|${JSON.stringify(opts.render)}` : "";
84
+ const baseRender = opts?.render ?? {};
85
+ // Resolve horizontal spacing: explicit override wins; else expand toward the target
86
+ // width (capped) so the diagram uses the space; else the compact default.
87
+ const paddingX =
88
+ baseRender.paddingX ??
89
+ (opts?.targetWidth !== undefined ? pickPaddingXForWidth(source, opts.targetWidth) : DEFAULT_PADDING_X);
90
+ const renderOpts: MermaidAsciiRenderOptions = { paddingY: DEFAULT_PADDING_Y, ...baseRender, paddingX };
91
+
92
+ // Layout options change the output, so they must be part of the key — but the
93
+ // compact DEFAULT is treated as "no options" so the key still matches
94
+ // mermaidThemeSignature()/getMermaidAscii() (the inline prerender/lookup path).
95
+ const sigObj: Record<string, unknown> = { ...renderOpts };
96
+ if (sigObj.paddingX === DEFAULT_PADDING_X) delete sigObj.paddingX;
97
+ if (sigObj.paddingY === DEFAULT_PADDING_Y) delete sigObj.paddingY;
98
+ const optsSig = Object.keys(sigObj).length > 0 ? `|${JSON.stringify(sigObj)}` : "";
61
99
  const key = `${Bun.hash(source.trim())}|${mode}:${paletteSignature(theme)}${optsSig}`;
62
100
  const cached = cache.get(key);
63
101
  if (cached !== undefined) return cached;
64
102
 
65
103
  const asciiTheme = buildMermaidAsciiTheme(theme);
66
- // Compact defaults: beautiful-mermaid's paddingX/paddingY of 5 spreads nodes far
67
- // apart and bloats the diagram. Tighter spacing reads more elegantly and helps it
68
- // fit the transcript width. Callers can still override via opts.render.
69
- const colored = renderMermaidAsciiSafe(source, {
70
- paddingX: 2,
71
- paddingY: 1,
72
- ...opts?.render,
73
- colorMode: mode,
74
- theme: asciiTheme,
75
- });
104
+ const colored = renderMermaidAsciiSafe(source, { ...renderOpts, colorMode: mode, theme: asciiTheme });
76
105
  if (colored == null) {
77
106
  cache.set(key, null);
78
107
  return null;
@@ -1,7 +1,8 @@
1
1
  /**
2
- * Map the active xcsh UI theme onto a mermaid `AsciiTheme` (per-role hues) and a
3
- * cycling list of node-accent ANSI escapes for the per-node tint pass. Reading the
4
- * theme keeps diagrams on-brand and dark/light adaptive.
2
+ * Map the active xcsh UI theme onto a mermaid `AsciiTheme`. The palette is
3
+ * deliberately restrained and professional: neutral gray structure with a single
4
+ * F5-brand accent on the arrowheads — no rainbow of per-node colors. Reading the
5
+ * theme keeps it dark/light adaptive and on-brand.
5
6
  */
6
7
  import type { MermaidAsciiRenderOptions } from "@f5xc-salesdemos/pi-utils";
7
8
  import type { Theme } from "./theme";
@@ -9,35 +10,30 @@ import type { Theme } from "./theme";
9
10
  type AsciiTheme = NonNullable<MermaidAsciiRenderOptions["theme"]>;
10
11
 
11
12
  /**
12
- * Per-role color palette: red node borders, gold labels, blue edges, green
13
- * arrowheads. These show through wherever the per-node tint pass doesn't apply
14
- * (edges, arrows, subgraph frames, and as the fallback when tinting is skipped).
13
+ * Restrained per-role palette neutral grays for the diagram structure, with one
14
+ * brand accent on the arrowheads:
15
+ * labels → neutral text gray
16
+ * borders → soft gray
17
+ * edges → dim gray (recede behind the boxes)
18
+ * arrows → F5 brand accent (the single pop of color)
19
+ * Nodes render uniformly (see buildNodeAccents — no per-node tint).
15
20
  */
16
21
  export function buildMermaidAsciiTheme(theme: Theme): AsciiTheme {
17
22
  return {
18
- fg: theme.getFgHex("mdHeading", "#febc38"), // node/edge labels
19
- border: theme.getFgHex("accent", "#ca260a"), // node + subgraph borders
20
- line: theme.getFgHex("mdLink", "#0088fa"), // edge lines
21
- arrow: theme.getFgHex("success", "#00ff88"), // arrowheads
22
- corner: theme.getFgHex("mdLink", "#0088fa"),
23
- junction: theme.getFgHex("accent", "#ca260a"),
23
+ fg: theme.getFgHex("toolOutput", "#9ca3b0"), // node + edge labels
24
+ border: theme.getFgHex("muted", "#9ca3b0"), // node + subgraph borders
25
+ line: theme.getFgHex("dim", "#6b7280"), // edge lines — subtle
26
+ arrow: theme.getFgHex("accent", "#ca260a"), // arrowheads — single brand accent
27
+ corner: theme.getFgHex("dim", "#6b7280"),
28
+ junction: theme.getFgHex("muted", "#9ca3b0"),
24
29
  };
25
30
  }
26
31
 
27
32
  /**
28
- * ANSI foreground escapes cycled across detected node boxes so each node reads as
29
- * a distinct hue. Drawn from vivid, well-separated theme roles.
33
+ * Per-node accent colors for the tint pass. Intentionally EMPTY: a professional
34
+ * diagram reads as one consistent palette, not a different bright hue per box.
35
+ * With no accents, the tint pass is a no-op and nodes use the role colors above.
30
36
  */
31
- export function buildNodeAccents(theme: Theme): string[] {
32
- const roles = ["chromeAccent", "success", "warning", "mdLink", "accent"] as const;
33
- const seen = new Set<string>();
34
- const accents: string[] = [];
35
- for (const role of roles) {
36
- const ansi = theme.getFgAnsi(role);
37
- if (!seen.has(ansi)) {
38
- seen.add(ansi);
39
- accents.push(ansi);
40
- }
41
- }
42
- return accents;
37
+ export function buildNodeAccents(_theme: Theme): string[] {
38
+ return [];
43
39
  }
@@ -10,13 +10,15 @@ import {
10
10
  supportsLanguage as nativeSupportsLanguage,
11
11
  } from "@f5xc-salesdemos/pi-natives";
12
12
  import type { EditorTheme, MarkdownTheme, SelectListTheme, SymbolTheme } from "@f5xc-salesdemos/pi-tui";
13
- import { adjustHsv, getCustomThemesDir, isEnoent, logger } from "@f5xc-salesdemos/pi-utils";
13
+ import { adjustHsv, detectDiagramType, getCustomThemesDir, isEnoent, logger } from "@f5xc-salesdemos/pi-utils";
14
14
  import { type Static, Type } from "@sinclair/typebox";
15
15
  import { TypeCompiler } from "@sinclair/typebox/compiler";
16
16
  import chalk from "chalk";
17
+ import { buildMermaidBlockOptions, diagramTypeLabel } from "../../tools/mermaid-renderer";
18
+ import { renderOutputBlock } from "../../tui";
17
19
  // Embed theme JSON files at build time
18
20
  import { defaultThemes } from "./defaults";
19
- import { getMermaidAscii, mermaidThemeSignature } from "./mermaid-cache";
21
+ import { getMermaidAscii, mermaidThemeSignature, renderMermaidThemed } from "./mermaid-cache";
20
22
 
21
23
  export { getLanguageFromPath } from "../../utils/lang-from-path";
22
24
 
@@ -2510,6 +2512,14 @@ export function getMarkdownTheme(): MarkdownTheme {
2510
2512
  strikethrough: (text: string) => chalk.strikethrough(text),
2511
2513
  symbols: getSymbolTheme(),
2512
2514
  getMermaidAscii: (hash: bigint | number) => getMermaidAscii(hash, mermaidThemeSignature(theme)),
2515
+ renderMermaidBlock: (source: string, width: number): string[] | null => {
2516
+ const diagram = renderMermaidThemed(source, theme, { targetWidth: Math.max(20, width - 4) });
2517
+ if (!diagram) return null;
2518
+ return renderOutputBlock(
2519
+ buildMermaidBlockOptions(diagram, { width, theme, typeLabel: diagramTypeLabel(detectDiagramType(source)) }),
2520
+ theme,
2521
+ );
2522
+ },
2513
2523
  highlightCode: (code: string, lang?: string): string[] => {
2514
2524
  const validLang = lang && nativeSupportsLanguage(lang) ? lang : undefined;
2515
2525
  try {
@@ -1,18 +1,18 @@
1
- /** TUI renderer for the render_mermaid tool — bordered, themed, colorized diagram output. */
1
+ /** TUI renderer for the render_mermaid tool — single F5-framed, themed, colorized diagram. */
2
2
  import type { Component } from "@f5xc-salesdemos/pi-tui";
3
- import { Text } from "@f5xc-salesdemos/pi-tui";
3
+ import { Text, visibleWidth } from "@f5xc-salesdemos/pi-tui";
4
4
  import { detectDiagramType, type MermaidDiagramType } from "@f5xc-salesdemos/pi-utils";
5
5
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
6
6
  import { renderMermaidThemed } from "../modes/theme/mermaid-cache";
7
7
  import type { Theme } from "../modes/theme/theme";
8
- import { CachedOutputBlock, F5_TOOL_BORDER_COLOR, renderStatusLine } from "../tui";
8
+ import { CachedOutputBlock, F5_TOOL_BORDER_COLOR, type OutputBlockOptions, renderStatusLine } from "../tui";
9
9
  import type { RenderMermaidToolDetails } from "./render-mermaid";
10
- import { addSection, formatErrorMessage, replaceTabs } from "./render-utils";
10
+ import { formatErrorMessage, replaceTabs } from "./render-utils";
11
11
 
12
12
  const TOOL_TITLE = "Mermaid";
13
13
 
14
- /** Human-friendly caption for the diagram-type badge in the block header. */
15
- function diagramTypeLabel(type: MermaidDiagramType): string {
14
+ /** Human-friendly caption for the diagram type, shown in the F5 frame header. */
15
+ export function diagramTypeLabel(type: MermaidDiagramType): string {
16
16
  switch (type) {
17
17
  case "flowchart":
18
18
  return "flowchart";
@@ -31,6 +31,41 @@ function diagramTypeLabel(type: MermaidDiagramType): string {
31
31
  }
32
32
  }
33
33
 
34
+ /**
35
+ * Build the F5 output-block options that frame a themed/colored mermaid diagram.
36
+ * Shared by the render_mermaid tool result AND inline ```mermaid markdown blocks, so
37
+ * every mermaid render gets the SAME single F5 frame — and the diagram is clipped
38
+ * (wrapContent: false), never wrapped, so a wide diagram can't reflow or overflow.
39
+ */
40
+ export function buildMermaidBlockOptions(
41
+ diagramText: string,
42
+ opts: { width: number; theme: Theme; typeLabel: string; artifactId?: string },
43
+ ): OutputBlockOptions {
44
+ // Unlabeled section: the header already reads "Mermaid · <type>", so a "Diagram"
45
+ // bar would be redundant. The diagram keeps its own colors; we only normalize tabs.
46
+ const lines = diagramText.split("\n").map(line => replaceTabs(line));
47
+ const meta = opts.artifactId ? [opts.theme.fg("dim", `artifact:${opts.artifactId.slice(0, 8)}`)] : undefined;
48
+ const header = renderStatusLine(
49
+ { title: TOOL_TITLE, titleColor: "dim", description: opts.theme.fg("muted", opts.typeLabel), meta },
50
+ opts.theme,
51
+ );
52
+ // Size the frame SNUGLY to the diagram (+ one space of padding each side), capped at
53
+ // the available width and kept wide enough for the header caption — so the diagram
54
+ // feels enclosed in a small padded frame instead of spanning the whole terminal
55
+ // (tiny diagram) or clipping against the right border (wide diagram).
56
+ const maxDiagramWidth = lines.reduce((max, line) => Math.max(max, visibleWidth(line)), 0);
57
+ const desired = Math.max(maxDiagramWidth + 4, visibleWidth(header) + 8);
58
+ const width = Math.min(opts.width, Math.max(1, desired));
59
+ return {
60
+ header,
61
+ state: "success",
62
+ sections: [{ lines }],
63
+ width,
64
+ borderColor: F5_TOOL_BORDER_COLOR,
65
+ wrapContent: false,
66
+ };
67
+ }
68
+
34
69
  type MermaidRenderArgs = {
35
70
  mermaid?: string;
36
71
  };
@@ -50,63 +85,39 @@ export const mermaidRenderer = {
50
85
  details?: RenderMermaidToolDetails;
51
86
  isError?: boolean;
52
87
  },
53
- options: RenderResultOptions,
88
+ _options: RenderResultOptions,
54
89
  uiTheme: Theme,
55
90
  args?: MermaidRenderArgs,
56
91
  ): Component {
57
- const isError = result.isError === true;
58
-
59
- if (isError) {
92
+ if (result.isError === true) {
60
93
  const errorText = result.content?.find(c => c.type === "text")?.text;
61
94
  return new Text(formatErrorMessage(errorText, uiTheme), 0, 0);
62
95
  }
63
96
 
64
- const sections: Array<{ label?: string; lines: string[] }> = [];
65
- const meta: string[] = [];
66
97
  const rawText = result.content?.find(c => c.type === "text")?.text ?? "";
67
-
68
98
  // Strip artifact reference from the plain result text (fallback path).
69
99
  const artifactIdx = rawText.indexOf("\n\nSaved artifact:");
70
100
  const plainText = artifactIdx >= 0 ? rawText.slice(0, artifactIdx) : rawText;
71
101
 
72
- // Prefer re-rendering from the original source: this yields the themed,
73
- // per-role-colored, node-tinted diagram. Fall back to the plain result text.
102
+ // Prefer re-rendering from the original source: themed, per-node-tinted, colored.
74
103
  const source = args?.mermaid?.trim();
75
- const diagramText = (source ? renderMermaidThemed(source, uiTheme) : null) ?? plainText;
76
- const caption = source ? diagramTypeLabel(detectDiagramType(source)) : "diagram";
77
-
78
- // Keep the diagram's own colors — do NOT recolor each line a flat tool color.
79
- // Show the FULL diagram: truncating a diagram with "… N more lines" defeats the purpose.
80
- const diagramLines = diagramText.split("\n").map(line => replaceTabs(line));
81
- addSection(sections, "Diagram", diagramLines, uiTheme);
82
-
83
- if (result.details?.artifactId) {
84
- meta.push(uiTheme.fg("dim", `artifact:${result.details.artifactId.slice(0, 8)}`));
85
- }
86
-
87
- const header = renderStatusLine(
88
- {
89
- title: TOOL_TITLE,
90
- titleColor: "dim",
91
- description: uiTheme.fg("muted", caption),
92
- meta: meta.length > 0 ? meta : undefined,
93
- },
94
- uiTheme,
95
- );
104
+ const typeLabel = source ? diagramTypeLabel(detectDiagramType(source)) : "diagram";
105
+ const artifactId = result.details?.artifactId;
96
106
 
97
- const outputBlock = new CachedOutputBlock();
107
+ const block = new CachedOutputBlock();
98
108
  return {
99
109
  render(width: number): string[] {
100
- const state = options.isPartial ? "pending" : "success";
101
- return outputBlock.render(
102
- // Clip, don't wrap: a diagram is a fixed grid — word-wrapping reflows it
103
- // and breaks alignment. Wide diagrams are truncated to the block width.
104
- { header, state, sections, width, borderColor: F5_TOOL_BORDER_COLOR, wrapContent: false },
110
+ // Expand the diagram toward the available width (inside the frame), then
111
+ // size the frame snugly around the result.
112
+ const targetWidth = Math.max(20, width - 4);
113
+ const diagramText = (source ? renderMermaidThemed(source, uiTheme, { targetWidth }) : null) ?? plainText;
114
+ return block.render(
115
+ buildMermaidBlockOptions(diagramText, { width, theme: uiTheme, typeLabel, artifactId }),
105
116
  uiTheme,
106
117
  );
107
118
  },
108
119
  invalidate() {
109
- outputBlock.invalidate();
120
+ block.invalidate();
110
121
  },
111
122
  };
112
123
  },