@f5xc-salesdemos/xcsh 19.42.0 → 19.42.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.42.0",
4
+ "version": "19.42.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",
@@ -39,6 +39,7 @@
39
39
  "fix": "biome check --write --unsafe . && bun run format-prompts && bun run generate-docs-index && bun run generate-api-spec-index && bun run generate-build-info",
40
40
  "fmt": "biome format --write . && bun run format-prompts",
41
41
  "format-prompts": "bun scripts/format-prompts.ts",
42
+ "mermaid:gallery": "bun scripts/mermaid-gallery.ts",
42
43
  "generate-docs-index": "bun scripts/generate-docs-index.ts",
43
44
  "generate-api-spec-index": "bun scripts/generate-api-spec-index.ts",
44
45
  "generate-branding-index": "bun scripts/generate-branding-index.ts",
@@ -51,13 +52,13 @@
51
52
  "dependencies": {
52
53
  "@agentclientprotocol/sdk": "0.16.1",
53
54
  "@mozilla/readability": "^0.6",
54
- "@f5xc-salesdemos/xcsh-stats": "19.42.0",
55
- "@f5xc-salesdemos/pi-agent-core": "19.42.0",
56
- "@f5xc-salesdemos/pi-ai": "19.42.0",
57
- "@f5xc-salesdemos/pi-natives": "19.42.0",
58
- "@f5xc-salesdemos/pi-resource-management": "19.42.0",
59
- "@f5xc-salesdemos/pi-tui": "19.42.0",
60
- "@f5xc-salesdemos/pi-utils": "19.42.0",
55
+ "@f5xc-salesdemos/xcsh-stats": "19.42.2",
56
+ "@f5xc-salesdemos/pi-agent-core": "19.42.2",
57
+ "@f5xc-salesdemos/pi-ai": "19.42.2",
58
+ "@f5xc-salesdemos/pi-natives": "19.42.2",
59
+ "@f5xc-salesdemos/pi-resource-management": "19.42.2",
60
+ "@f5xc-salesdemos/pi-tui": "19.42.2",
61
+ "@f5xc-salesdemos/pi-utils": "19.42.2",
61
62
  "@sinclair/typebox": "^0.34",
62
63
  "@xterm/headless": "^6.0",
63
64
  "ajv": "^8.20",
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * F5 XC Mermaid — visual gallery harness for human UAT.
4
+ *
5
+ * Renders the F5 Distributed Cloud mermaid sample library through the real
6
+ * display pipeline (themed per-role palette + per-node tint) so you can visually
7
+ * inspect the colorized output in your own terminal and give feedback.
8
+ *
9
+ * Usage:
10
+ * bun scripts/mermaid-gallery.ts # all samples, dark theme, truecolor
11
+ * bun scripts/mermaid-gallery.ts --theme light
12
+ * bun scripts/mermaid-gallery.ts --color ansi256 # none | ansi256 | truecolor
13
+ * bun scripts/mermaid-gallery.ts --ascii # ASCII box chars instead of Unicode
14
+ * bun scripts/mermaid-gallery.ts --type flowchart # filter by diagram type
15
+ * bun scripts/mermaid-gallery.ts --filter waf # regex over id/category/prompt
16
+ * bun scripts/mermaid-gallery.ts --list # list samples, don't render
17
+ *
18
+ * Tip: pipe through `less -R` to page while keeping color.
19
+ */
20
+ import type { MermaidColorMode } from "@f5xc-salesdemos/pi-utils";
21
+ import { renderMermaidThemed } from "../src/modes/theme/mermaid-cache";
22
+ import { getThemeByName } from "../src/modes/theme/theme";
23
+ import { XC_MERMAID_SAMPLES, type XcMermaidSample } from "../test/fixtures/xc-mermaid-samples";
24
+
25
+ interface Args {
26
+ theme: string;
27
+ color: MermaidColorMode;
28
+ ascii: boolean;
29
+ type?: string;
30
+ filter?: RegExp;
31
+ list: boolean;
32
+ }
33
+
34
+ function parseArgs(argv: string[]): Args {
35
+ const args: Args = { theme: "xcsh-dark", color: "truecolor", ascii: false, list: false };
36
+ for (let i = 0; i < argv.length; i++) {
37
+ const a = argv[i];
38
+ const next = () => argv[++i] ?? "";
39
+ switch (a) {
40
+ case "--theme":
41
+ args.theme = next() === "light" ? "xcsh-light" : "xcsh-dark";
42
+ break;
43
+ case "--color":
44
+ args.color = next() as MermaidColorMode;
45
+ break;
46
+ case "--ascii":
47
+ args.ascii = true;
48
+ break;
49
+ case "--type":
50
+ args.type = next();
51
+ break;
52
+ case "--filter":
53
+ args.filter = new RegExp(next(), "i");
54
+ break;
55
+ case "--list":
56
+ args.list = true;
57
+ break;
58
+ case "--help":
59
+ case "-h":
60
+ console.log(
61
+ "bun scripts/mermaid-gallery.ts [--theme dark|light] [--color none|ansi256|truecolor] [--ascii] [--type <t>] [--filter <regex>] [--list]",
62
+ );
63
+ process.exit(0);
64
+ }
65
+ }
66
+ return args;
67
+ }
68
+
69
+ function selectSamples(args: Args): XcMermaidSample[] {
70
+ return XC_MERMAID_SAMPLES.filter(s => {
71
+ if (args.type && s.type !== args.type) return false;
72
+ if (args.filter && !args.filter.test(`${s.id} ${s.category} ${s.prompt}`)) return false;
73
+ return true;
74
+ });
75
+ }
76
+
77
+ async function main(): Promise<void> {
78
+ const args = parseArgs(process.argv.slice(2));
79
+ const theme = await getThemeByName(args.theme);
80
+ if (!theme) {
81
+ console.error(`Unknown theme: ${args.theme}`);
82
+ process.exit(1);
83
+ }
84
+ const samples = selectSamples(args);
85
+ const width = Math.min(process.stdout.columns ?? 100, 110);
86
+ const rule = (ch: string) => theme.fg("accent", ch.repeat(width));
87
+
88
+ console.log(`\n${rule("━")}`);
89
+ console.log(
90
+ theme.fg("mdHeading", " F5 XC Mermaid Gallery") +
91
+ theme.fg(
92
+ "muted",
93
+ ` theme=${args.theme} color=${args.color} ascii=${args.ascii} samples=${samples.length}`,
94
+ ),
95
+ );
96
+ console.log(rule("━"));
97
+
98
+ if (args.list) {
99
+ for (const [i, s] of samples.entries()) {
100
+ console.log(
101
+ ` ${theme.fg("dim", String(i + 1).padStart(2, "0"))} ` +
102
+ `${theme.fg("accent", s.type.padEnd(10))} ${theme.fg("mdHeading", s.id.padEnd(28))} ${theme.fg("muted", s.category)}`,
103
+ );
104
+ }
105
+ console.log("");
106
+ return;
107
+ }
108
+
109
+ for (const [i, s] of samples.entries()) {
110
+ console.log(
111
+ `\n${theme.fg("accent", "━━━")} ${theme.fg("dim", `[${i + 1}/${samples.length}]`)} ${theme.fg("mdHeading", s.category)} ${theme.fg("muted", "·")} ${theme.fg("success", s.type)} ${theme.fg("accent", "━".repeat(Math.max(0, width - s.category.length - s.type.length - 18)))}`,
112
+ );
113
+ console.log(` ${theme.fg("dim", "▸")} ${theme.fg("mdLink", s.id)}`);
114
+ console.log(` ${theme.fg("muted", `"${s.prompt}"`)}\n`);
115
+
116
+ const out = renderMermaidThemed(s.source, theme, { colorMode: args.color, render: { useAscii: args.ascii } });
117
+ if (out == null) {
118
+ console.log(` ${theme.fg("error", "⚠ failed to render")}\n`);
119
+ continue;
120
+ }
121
+ for (const line of out.split("\n")) console.log(` ${line}`);
122
+ }
123
+
124
+ console.log(`\n${rule("━")}`);
125
+ console.log(
126
+ theme.fg("muted", ` Rendered ${samples.length} sample(s). Inspect the colors above and note anything to tune `) +
127
+ theme.fg("dim", "(borders, labels, edges, arrows, per-node tints, contrast)."),
128
+ );
129
+ console.log(rule("━") + "\n");
130
+ }
131
+
132
+ await main();
@@ -17,17 +17,17 @@ export interface BuildInfo {
17
17
  }
18
18
 
19
19
  export const BUILD_INFO: BuildInfo = {
20
- "version": "19.42.0",
21
- "commit": "5959e8371dc1aaa802e7a5b3bc51642328816b2a",
22
- "shortCommit": "5959e83",
20
+ "version": "19.42.2",
21
+ "commit": "a018af25c2adec4357b1aa63213417ef9331ef18",
22
+ "shortCommit": "a018af2",
23
23
  "branch": "main",
24
- "tag": "v19.42.0",
25
- "commitDate": "2026-06-24T01:36:24Z",
26
- "buildDate": "2026-06-24T02:03:24.964Z",
24
+ "tag": "v19.42.2",
25
+ "commitDate": "2026-06-24T03:15:55Z",
26
+ "buildDate": "2026-06-24T03:45:49.205Z",
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/5959e8371dc1aaa802e7a5b3bc51642328816b2a",
32
- "releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v19.42.0"
31
+ "commitUrl": "https://github.com/f5xc-salesdemos/xcsh/commit/a018af25c2adec4357b1aa63213417ef9331ef18",
32
+ "releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v19.42.2"
33
33
  };
@@ -3,6 +3,7 @@ import {
3
3
  logger,
4
4
  type MermaidAsciiRenderOptions,
5
5
  type MermaidColorMode,
6
+ mermaidSourceExceedsLimit,
6
7
  pickColorMode,
7
8
  renderMermaidAsciiSafe,
8
9
  tintMermaidNodes,
@@ -48,13 +49,30 @@ export interface RenderMermaidThemedOptions {
48
49
  * Result is memoized per (source, theme, colorMode). Returns null on parse failure.
49
50
  */
50
51
  export function renderMermaidThemed(source: string, theme: Theme, opts?: RenderMermaidThemedOptions): string | null {
52
+ // Oversized graphs can hang the synchronous pathfinder for tens of seconds; skip
53
+ // them so the display falls back to the raw code block instead of freezing the UI.
54
+ if (mermaidSourceExceedsLimit(source)) return null;
55
+
51
56
  const mode = colorModeFor(theme, opts?.colorMode);
52
- const key = `${Bun.hash(source.trim())}|${mode}:${paletteSignature(theme)}`;
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)}` : "";
61
+ const key = `${Bun.hash(source.trim())}|${mode}:${paletteSignature(theme)}${optsSig}`;
53
62
  const cached = cache.get(key);
54
63
  if (cached !== undefined) return cached;
55
64
 
56
65
  const asciiTheme = buildMermaidAsciiTheme(theme);
57
- const colored = renderMermaidAsciiSafe(source, { ...opts?.render, colorMode: mode, theme: asciiTheme });
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
+ });
58
76
  if (colored == null) {
59
77
  cache.set(key, null);
60
78
  return null;
@@ -10,7 +10,6 @@ import type { RenderMermaidToolDetails } from "./render-mermaid";
10
10
  import { addSection, formatErrorMessage, replaceTabs } from "./render-utils";
11
11
 
12
12
  const TOOL_TITLE = "Mermaid";
13
- const MAX_DIAGRAM_LINES = 40;
14
13
 
15
14
  /** Human-friendly caption for the diagram-type badge in the block header. */
16
15
  function diagramTypeLabel(type: MermaidDiagramType): string {
@@ -77,8 +76,9 @@ export const mermaidRenderer = {
77
76
  const caption = source ? diagramTypeLabel(detectDiagramType(source)) : "diagram";
78
77
 
79
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
80
  const diagramLines = diagramText.split("\n").map(line => replaceTabs(line));
81
- addSection(sections, "Diagram", diagramLines, uiTheme, MAX_DIAGRAM_LINES);
81
+ addSection(sections, "Diagram", diagramLines, uiTheme);
82
82
 
83
83
  if (result.details?.artifactId) {
84
84
  meta.push(uiTheme.fg("dim", `artifact:${result.details.artifactId.slice(0, 8)}`));
@@ -98,7 +98,12 @@ export const mermaidRenderer = {
98
98
  return {
99
99
  render(width: number): string[] {
100
100
  const state = options.isPartial ? "pending" : "success";
101
- return outputBlock.render({ header, state, sections, width, borderColor: F5_TOOL_BORDER_COLOR }, uiTheme);
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 },
105
+ uiTheme,
106
+ );
102
107
  },
103
108
  invalidate() {
104
109
  outputBlock.invalidate();
@@ -4,7 +4,12 @@ import type {
4
4
  AgentToolResult,
5
5
  AgentToolUpdateCallback,
6
6
  } from "@f5xc-salesdemos/pi-agent-core";
7
- import { type MermaidAsciiRenderOptions, prompt, renderMermaidAscii } from "@f5xc-salesdemos/pi-utils";
7
+ import {
8
+ type MermaidAsciiRenderOptions,
9
+ mermaidSourceExceedsLimit,
10
+ prompt,
11
+ renderMermaidAsciiSafe,
12
+ } from "@f5xc-salesdemos/pi-utils";
8
13
  import { type Static, Type } from "@sinclair/typebox";
9
14
  import renderMermaidDescription from "../prompts/tools/render-mermaid.md" with { type: "text" };
10
15
  import type { ToolSession } from "./index";
@@ -55,7 +60,24 @@ export class RenderMermaidTool implements AgentTool<typeof renderMermaidSchema,
55
60
  _onUpdate?: AgentToolUpdateCallback<RenderMermaidToolDetails>,
56
61
  _context?: AgentToolContext,
57
62
  ): Promise<AgentToolResult<RenderMermaidToolDetails>> {
58
- const ascii = renderMermaidAscii(params.mermaid, sanitizeRenderConfig(params.config));
63
+ // Reject oversized graphs up front: beautiful-mermaid's pathfinder can hang
64
+ // for tens of seconds and then throw `RangeError: Out of memory` on them.
65
+ // Throwing here (fast) surfaces a clean tool error the agent can recover from.
66
+ if (mermaidSourceExceedsLimit(params.mermaid)) {
67
+ throw new Error(
68
+ "Mermaid diagram is too large to render safely. Reduce the number of nodes/edges and try again.",
69
+ );
70
+ }
71
+
72
+ // Safe variant: a parse failure or memory blow-up becomes null rather than a
73
+ // raw RangeError, so we can throw a clear, actionable message instead.
74
+ const ascii = renderMermaidAsciiSafe(params.mermaid, sanitizeRenderConfig(params.config));
75
+ if (ascii == null || ascii.trim() === "") {
76
+ throw new Error(
77
+ "Failed to render the Mermaid diagram — the syntax may be unsupported or too complex. Use newline-separated statements (no ';') and try a simpler diagram.",
78
+ );
79
+ }
80
+
59
81
  const { path: artifactPath, id: artifactId } =
60
82
  (await this.session.allocateOutputArtifact?.("render_mermaid")) ?? {};
61
83
  if (artifactPath) {
@@ -6,7 +6,7 @@ import type { Theme, ThemeColor } from "../modes/theme/theme";
6
6
  import { getImageLineMask } from "../utils/image-passthrough";
7
7
  import type { State } from "./types";
8
8
  import type { RenderCache } from "./utils";
9
- import { getStateBgColor, Hasher, padToWidth, truncateToWidth } from "./utils";
9
+ import { Ellipsis, getStateBgColor, Hasher, padToWidth, truncateToWidth } from "./utils";
10
10
 
11
11
  /**
12
12
  * Border color override for F5-branded tool renderers.
@@ -23,6 +23,12 @@ export interface OutputBlockOptions {
23
23
  applyBg?: boolean;
24
24
  /** Override the state-derived border color. Always takes precedence, including on error. Use for branded core tools. */
25
25
  borderColor?: ThemeColor;
26
+ /**
27
+ * Word-wrap content lines to fit the inner width (default). Set false to CLIP
28
+ * (truncate) instead — required for pre-formatted grid content like mermaid
29
+ * diagrams, where wrapping reflows characters and destroys alignment.
30
+ */
31
+ wrapContent?: boolean;
26
32
  }
27
33
 
28
34
  export function renderOutputBlock(options: OutputBlockOptions, theme: Theme): string[] {
@@ -34,6 +40,7 @@ export function renderOutputBlock(options: OutputBlockOptions, theme: Theme): st
34
40
  width,
35
41
  applyBg = true,
36
42
  borderColor: borderColorOverride,
43
+ wrapContent = true,
37
44
  } = options;
38
45
  const h = theme.boxSharp.horizontal;
39
46
  const v = theme.boxSharp.vertical;
@@ -100,7 +107,11 @@ export function renderOutputBlock(options: OutputBlockOptions, theme: Theme): st
100
107
  lines.push(line);
101
108
  continue;
102
109
  }
103
- const wrappedLines = wrapTextWithAnsi(line.trimEnd(), contentWidth);
110
+ const wrappedLines = wrapContent
111
+ ? wrapTextWithAnsi(line.trimEnd(), contentWidth)
112
+ : // Clip cleanly with no trailing ellipsis — a column of "…" down the
113
+ // right edge of a clipped diagram is clutter, not information.
114
+ [truncateToWidth(line.trimEnd(), contentWidth, Ellipsis.Omit)];
104
115
  for (const wrappedLine of wrappedLines) {
105
116
  const innerPadding = padding(Math.max(0, contentWidth - visibleWidth(wrappedLine)));
106
117
  const fullLine = `${contentPrefix}${wrappedLine}${innerPadding}${contentSuffix}`;
@@ -150,6 +161,7 @@ export class CachedOutputBlock {
150
161
  h.optional(options.state);
151
162
  h.bool(options.applyBg ?? true);
152
163
  h.optional(options.borderColor);
164
+ h.bool(options.wrapContent ?? true);
153
165
  if (options.sections) {
154
166
  for (const s of options.sections) {
155
167
  h.optional(s.label);