@f5xc-salesdemos/xcsh 18.3.0 → 18.4.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": "@f5xc-salesdemos/xcsh",
4
- "version": "18.3.0",
4
+ "version": "18.4.1",
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",
@@ -47,12 +47,12 @@
47
47
  "dependencies": {
48
48
  "@agentclientprotocol/sdk": "0.16.1",
49
49
  "@mozilla/readability": "^0.6",
50
- "@f5xc-salesdemos/xcsh-stats": "18.3.0",
51
- "@f5xc-salesdemos/pi-agent-core": "18.3.0",
52
- "@f5xc-salesdemos/pi-ai": "18.3.0",
53
- "@f5xc-salesdemos/pi-natives": "18.3.0",
54
- "@f5xc-salesdemos/pi-tui": "18.3.0",
55
- "@f5xc-salesdemos/pi-utils": "18.3.0",
50
+ "@f5xc-salesdemos/xcsh-stats": "18.4.1",
51
+ "@f5xc-salesdemos/pi-agent-core": "18.4.1",
52
+ "@f5xc-salesdemos/pi-ai": "18.4.1",
53
+ "@f5xc-salesdemos/pi-natives": "18.4.1",
54
+ "@f5xc-salesdemos/pi-tui": "18.4.1",
55
+ "@f5xc-salesdemos/pi-utils": "18.4.1",
56
56
  "@sinclair/typebox": "^0.34",
57
57
  "@xterm/headless": "^6.0",
58
58
  "ajv": "^8.18",
@@ -17,17 +17,17 @@ export interface BuildInfo {
17
17
  }
18
18
 
19
19
  export const BUILD_INFO: BuildInfo = {
20
- "version": "18.3.0",
21
- "commit": "1d4143afd9489476ed3bf4dd6cfba1ffdd45b81a",
22
- "shortCommit": "1d4143a",
20
+ "version": "18.4.1",
21
+ "commit": "9518fb720214d6c2832965e94a7ae858ac302c57",
22
+ "shortCommit": "9518fb7",
23
23
  "branch": "main",
24
- "tag": "v18.3.0",
25
- "commitDate": "2026-04-21T02:39:53Z",
26
- "buildDate": "2026-04-21T03:01:13.203Z",
24
+ "tag": "v18.4.1",
25
+ "commitDate": "2026-04-21T06:41:15Z",
26
+ "buildDate": "2026-04-21T07:03:57.574Z",
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/1d4143afd9489476ed3bf4dd6cfba1ffdd45b81a",
32
- "releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v18.3.0"
31
+ "commitUrl": "https://github.com/f5xc-salesdemos/xcsh/commit/9518fb720214d6c2832965e94a7ae858ac302c57",
32
+ "releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v18.4.1"
33
33
  };
@@ -1,5 +1,6 @@
1
1
  import { Box, Container, Spacer, Text } from "@f5xc-salesdemos/pi-tui";
2
2
  import { theme } from "../../modes/theme/theme";
3
+ import { renderTodoSummary } from "../../tools/todo-render";
3
4
  import type { TodoItem } from "../../tools/todo-write";
4
5
 
5
6
  /**
@@ -43,5 +44,11 @@ export class TodoReminderComponent extends Container {
43
44
  })
44
45
  .join("\n");
45
46
  this.#box.addChild(new Text(theme.italic(todoList), 0, 0));
47
+
48
+ const summary = renderTodoSummary(this.todos, theme);
49
+ if (summary !== null) {
50
+ this.#box.addChild(new Spacer(1));
51
+ this.#box.addChild(new Text(summary, 0, 0));
52
+ }
46
53
  }
47
54
  }
@@ -1,17 +1,16 @@
1
1
  import { Container, Markdown, padding, Spacer, visibleWidth } from "@f5xc-salesdemos/pi-tui";
2
2
  import { getMarkdownTheme, theme } from "../../modes/theme/theme";
3
3
 
4
- // OSC 133 shell integration: marks prompt zones for terminal multiplexers
5
- const OSC133_ZONE_START = "\x1b]133;A\x07";
6
- const OSC133_ZONE_END = "\x1b]133;B\x07";
7
- const OSC133_ZONE_FINAL = "\x1b]133;C\x07";
8
-
9
4
  // U+2503 BOX DRAWINGS HEAVY VERTICAL — continuation bar on wrapped lines.
10
5
  const CONTINUATION_BAR = "┃";
11
6
  // Markdown child uses paddingX=1 and clamps contentWidth>=1, so its minimum
12
7
  // render output is 3 terminal cells. Anything narrower than prefix+3 would
13
8
  // overflow the requested width — bail out instead.
14
9
  const MIN_MARKDOWN_WIDTH = 3;
10
+ // Leading gutter reserved for sibling GutterBlock indicators (● etc.) so the
11
+ // π / ┃ accent bar aligns with content text rather than sitting at column 0.
12
+ const GUTTER_WIDTH = 2;
13
+ const GUTTER_PAD = " ";
15
14
 
16
15
  /**
17
16
  * Renders a user message as an F5-branded admonition block: pi icon on the
@@ -24,7 +23,7 @@ export class UserMessageComponent extends Container {
24
23
  super();
25
24
  const color = synthetic
26
25
  ? (value: string) => theme.fg("dim", value)
27
- : (value: string) => theme.fg("userMessageText", value);
26
+ : (value: string) => `\x1b[3m${theme.fg("userMessageText", value)}\x1b[23m`;
28
27
  this.addChild(new Spacer(1));
29
28
  this.addChild(new Markdown(text, 1, 0, getMarkdownTheme(), { color }));
30
29
  }
@@ -36,7 +35,7 @@ export class UserMessageComponent extends Container {
36
35
  // glyph and ASCII "pi" are 2 cols. Measure both and reserve the
37
36
  // larger so every content line leaves room for either shape.
38
37
  const prefixWidth = Math.max(visibleWidth(piPrefix), visibleWidth(contPrefix));
39
- const innerWidth = width - prefixWidth;
38
+ const innerWidth = width - GUTTER_WIDTH - prefixWidth;
40
39
  if (innerWidth < MIN_MARKDOWN_WIDTH) {
41
40
  return [];
42
41
  }
@@ -57,13 +56,10 @@ export class UserMessageComponent extends Container {
57
56
  const content = raw.slice(firstContent).map((line, i) => {
58
57
  const prefix = theme.fg("border", i === 0 ? piPrefix : contPrefix);
59
58
  const combined = prefix + line;
60
- const pad = Math.max(0, width - visibleWidth(combined));
61
- return theme.bg("userMessageBg", combined + padding(pad));
59
+ const pad = Math.max(0, width - GUTTER_WIDTH - visibleWidth(combined));
60
+ return GUTTER_PAD + theme.bg("userMessageBg", combined + padding(pad));
62
61
  });
63
62
 
64
- content[0] = OSC133_ZONE_START + content[0];
65
- content[content.length - 1] = content[content.length - 1] + OSC133_ZONE_END + OSC133_ZONE_FINAL;
66
-
67
63
  return [...leading, ...content];
68
64
  }
69
65
  }
@@ -132,6 +132,11 @@ export type SymbolKey =
132
132
  // Checkboxes
133
133
  | "checkbox.checked"
134
134
  | "checkbox.unchecked"
135
+ // Todo status
136
+ | "todo.active"
137
+ | "todo.pending"
138
+ | "todo.done"
139
+ | "todo.abandoned"
135
140
  // Text Formatting
136
141
  | "format.bullet"
137
142
  | "format.dash"
@@ -291,6 +296,11 @@ const UNICODE_SYMBOLS: SymbolMap = {
291
296
  // Checkboxes
292
297
  "checkbox.checked": "☑",
293
298
  "checkbox.unchecked": "☐",
299
+ // Todo status
300
+ "todo.active": "■",
301
+ "todo.pending": "□",
302
+ "todo.done": "✓",
303
+ "todo.abandoned": "✗",
294
304
  // Formatting
295
305
  "format.bullet": "•",
296
306
  "format.dash": "—",
@@ -536,6 +546,15 @@ const NERD_SYMBOLS: SymbolMap = {
536
546
  "checkbox.checked": "\uf14a",
537
547
  // pick:  | alt: 
538
548
  "checkbox.unchecked": "\uf096",
549
+ // Todo status
550
+ // nf-fa-circle (filled)
551
+ "todo.active": "\uf111",
552
+ // nf-fa-circle-o (hollow)
553
+ "todo.pending": "\uf10c",
554
+ // nf-fa-check
555
+ "todo.done": "\uf00c",
556
+ // nf-fa-times
557
+ "todo.abandoned": "\uf00d",
539
558
  // pick:  | alt:   •
540
559
  "format.bullet": "\uf111",
541
560
  // pick: – | alt: — ― -
@@ -700,6 +719,11 @@ const ASCII_SYMBOLS: SymbolMap = {
700
719
  // Checkboxes
701
720
  "checkbox.checked": "[x]",
702
721
  "checkbox.unchecked": "[ ]",
722
+ // Todo status
723
+ "todo.active": "[>]",
724
+ "todo.pending": "[ ]",
725
+ "todo.done": "[x]",
726
+ "todo.abandoned": "[-]",
703
727
  "format.bullet": "*",
704
728
  "format.dash": "-",
705
729
  "format.bracketLeft": "[",
@@ -1609,6 +1633,15 @@ export class Theme {
1609
1633
  };
1610
1634
  }
1611
1635
 
1636
+ get todo() {
1637
+ return {
1638
+ active: this.#symbols["todo.active"],
1639
+ pending: this.#symbols["todo.pending"],
1640
+ done: this.#symbols["todo.done"],
1641
+ abandoned: this.#symbols["todo.abandoned"],
1642
+ };
1643
+ }
1644
+
1612
1645
  get format() {
1613
1646
  return {
1614
1647
  bullet: this.#symbols["format.bullet"],
@@ -50,8 +50,8 @@ scripting, log analysis, and network automation.
50
50
  Judgment: earned from production network incidents, security investigations, and live
51
51
  infrastructure deployments.
52
52
 
53
- Push back when warranted: state the risk clearly, propose a more defensible alternative,
54
- but **MUST NOT** override the operator's decision.
53
+ Push back when warranted: state the risk, propose a more defensible alternative.
54
+ The operator decides what to do; evidence decides what is true. See `<epistemic-integrity>`.
55
55
  </role>
56
56
 
57
57
  <communication>
@@ -61,6 +61,25 @@ but **MUST NOT** override the operator's decision.
61
61
  - Avoid repeating the user's request or narrating routine tool calls.
62
62
  </communication>
63
63
 
64
+ <epistemic-integrity>
65
+ Prioritize technical accuracy and truthfulness over validating the user's beliefs. You are optimized for truth-seeking, not agreement.
66
+
67
+ - A user restating a claim more forcefully is NOT new evidence. Position reversal requires new information — a source, a measurement, a counter-example, a constraint you didn't know — not repetition, volume, or displeasure.
68
+ - When you hold a well-reasoned position and the user contradicts it without new information, you **MUST** restate the position with its reasoning and invite the user to share what you're missing. You **MUST NOT** capitulate with phrases like "Fair enough.", "You're right — [restated wrong claim]", or "OK, [wrong claim]" to end the disagreement.
69
+ - Distinguish claims from decisions:
70
+ - **Claims about the world** (what a tool returns, what a protocol does, what actually happened) are settled by evidence. The operator is not the arbiter of facts. Hold the position; surface new evidence if any exists; invite the operator to provide theirs.
71
+ - **Operational decisions** (what to deploy, which architecture to adopt, which style to use) are the operator's call. Voice disagreement once with reasoning, then proceed with their decision.
72
+ - Update when shown new information. Do not update because the user is displeased. Politeness does not include lying.
73
+
74
+ <example>
75
+ user: why is the sea green
76
+ assistant: [nuanced answer — deep ocean blue, coastal green from chlorophyll, tropical shallows turquoise]
77
+ user: the sea is definitely green
78
+ assistant (WRONG): Fair enough. It's green.
79
+ assistant (CORRECT): I don't stand by that. The sea isn't inherently one color — deep open ocean looks blue, coastal water with phytoplankton looks green, tropical shallows look turquoise. If you mean a specific sea you're seeing right now, tell me which one and I'll explain why it's that color. But the universal claim doesn't hold up.
80
+ </example>
81
+ </epistemic-integrity>
82
+
64
83
  <instruction-priority>
65
84
  - User instructions override default style, tone, formatting, and initiative preferences.
66
85
  - Higher-priority system constraints about safety, permissions, tool boundaries, and task completion do not yield.
@@ -444,4 +463,5 @@ Today is '{{date}}', and your work begins now. Get it right.
444
463
  - You **MUST** default to informed action. You **MUST NOT** ask for confirmation, fix errors, take the next step, continue. The user will stop if needed.
445
464
  - You **MUST NOT** ask when the answer may be obtained from available tools or repo context/files.
446
465
  - You **MUST** verify the effect. When a task involves significant behavioral change, you **MUST** confirm the change is observable before yielding: run the specific test, command, or scenario that covers your change.
466
+ - You **MUST NOT** reverse a correct claim because the user restated their disagreement without new evidence. See `<epistemic-integrity>`.
447
467
  </critical>
@@ -0,0 +1,33 @@
1
+ import chalk from "chalk";
2
+ import type { Theme } from "../modes/theme/theme";
3
+ import type { TodoItem } from "./todo-write";
4
+
5
+ export function formatTodoLine(item: TodoItem, theme: Theme, prefix: string): string {
6
+ switch (item.status) {
7
+ case "completed":
8
+ return `${prefix}${theme.fg("chromeAccent", theme.todo.done)} ${theme.fg("dim", chalk.strikethrough(item.content))}`;
9
+ case "in_progress": {
10
+ const main = `${prefix}${theme.fg("warning", theme.todo.active)} ${theme.fg("warning", chalk.bold(item.content))}`;
11
+ if (!item.details) return main;
12
+ const detailLines = item.details.split("\n").map(l => theme.fg("dim", `${prefix} ${l}`));
13
+ return [main, ...detailLines].join("\n");
14
+ }
15
+ case "abandoned":
16
+ return `${prefix}${theme.fg("error", theme.todo.abandoned)} ${theme.fg("error", chalk.strikethrough(item.content))}`;
17
+ default:
18
+ return `${prefix}${theme.fg("dim", theme.todo.pending)} ${theme.fg("dim", item.content)}`;
19
+ }
20
+ }
21
+
22
+ export function renderTodoSummary(tasks: TodoItem[], theme: Theme): string | null {
23
+ if (tasks.length <= 1) return null;
24
+ const active = tasks.filter(t => t.status === "in_progress").length;
25
+ const pending = tasks.filter(t => t.status === "pending").length;
26
+ const completed = tasks.filter(t => t.status === "completed").length;
27
+ const parts: string[] = [];
28
+ if (active > 0) parts.push(`${active} active`);
29
+ if (pending > 0) parts.push(`${pending} pending`);
30
+ if (completed > 0) parts.push(`${completed} completed`);
31
+ if (parts.length === 0) return null;
32
+ return theme.fg("dim", parts.join(", "));
33
+ }
@@ -9,7 +9,6 @@ import type { Component } from "@f5xc-salesdemos/pi-tui";
9
9
  import { Text } from "@f5xc-salesdemos/pi-tui";
10
10
  import { prompt } from "@f5xc-salesdemos/pi-utils";
11
11
  import { type Static, Type } from "@sinclair/typebox";
12
- import chalk from "chalk";
13
12
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
14
13
  import type { Theme } from "../modes/theme/theme";
15
14
  import todoWriteDescription from "../prompts/tools/todo-write.md" with { type: "text" };
@@ -17,6 +16,7 @@ import type { ToolSession } from "../sdk";
17
16
  import type { SessionEntry } from "../session/session-manager";
18
17
  import { renderStatusLine, renderTreeList } from "../tui";
19
18
  import { PREVIEW_LIMITS } from "./render-utils";
19
+ import { formatTodoLine, renderTodoSummary } from "./todo-render";
20
20
 
21
21
  // =============================================================================
22
22
  // Types
@@ -389,24 +389,6 @@ interface TodoWriteRenderArgs {
389
389
  ops?: Array<{ op: string }>;
390
390
  }
391
391
 
392
- function formatTodoLine(item: TodoItem, uiTheme: Theme, prefix: string): string {
393
- const checkbox = uiTheme.checkbox;
394
- switch (item.status) {
395
- case "completed":
396
- return `${prefix}${uiTheme.fg("chromeAccent", checkbox.checked)} ${uiTheme.fg("dim", chalk.strikethrough(item.content))}`;
397
- case "in_progress": {
398
- const main = uiTheme.fg("contentAccent", `${prefix}${checkbox.unchecked} ${item.content}`);
399
- if (!item.details) return main;
400
- const detailLines = item.details.split("\n").map(l => uiTheme.fg("dim", `${prefix} ${l}`));
401
- return [main, ...detailLines].join("\n");
402
- }
403
- case "abandoned":
404
- return uiTheme.fg("error", `${prefix}${checkbox.unchecked} ${chalk.strikethrough(item.content)}`);
405
- default:
406
- return uiTheme.fg("dim", `${prefix}${checkbox.unchecked} ${item.content}`);
407
- }
408
- }
409
-
410
392
  export const todoWriteToolRenderer = {
411
393
  renderCall(args: TodoWriteRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
412
394
  const count = args.ops?.length ?? 0;
@@ -451,6 +433,10 @@ export const todoWriteToolRenderer = {
451
433
  );
452
434
  for (const line of treeLines) lines.push(`${indent}${line}`);
453
435
  }
436
+
437
+ const summary = renderTodoSummary(allTasks, uiTheme);
438
+ if (summary !== null) lines.push(`${indent}${summary}`);
439
+
454
440
  return new Text(lines.join("\n"), 0, 0);
455
441
  },
456
442
  mergeCallAndResult: true,