@f5-sales-demo/xcsh 21.43.1 → 21.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 +8 -8
- package/src/internal-urls/build-info.generated.ts +8 -8
- package/src/modes/components/tool-execution.ts +12 -3
- package/src/task/render.ts +49 -27
- package/src/tools/calculator.ts +2 -1
- package/src/tools/find.ts +8 -2
- package/src/tools/search-tool-bm25.ts +16 -12
- package/src/tools/todo-write.ts +26 -22
- package/src/tui/file-list.ts +3 -1
- package/src/tui/output-block.ts +6 -2
- package/src/tui/tree-list.ts +49 -5
- package/src/web/search/render.ts +7 -5
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5-sales-demo/xcsh",
|
|
4
|
-
"version": "21.43.
|
|
4
|
+
"version": "21.43.2",
|
|
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",
|
|
@@ -64,13 +64,13 @@
|
|
|
64
64
|
},
|
|
65
65
|
"dependencies": {
|
|
66
66
|
"@agentclientprotocol/sdk": "1.4.0",
|
|
67
|
-
"@f5-sales-demo/pi-agent-core": "21.43.
|
|
68
|
-
"@f5-sales-demo/pi-ai": "21.43.
|
|
69
|
-
"@f5-sales-demo/pi-natives": "21.43.
|
|
70
|
-
"@f5-sales-demo/pi-resource-management": "21.43.
|
|
71
|
-
"@f5-sales-demo/pi-tui": "21.43.
|
|
72
|
-
"@f5-sales-demo/pi-utils": "21.43.
|
|
73
|
-
"@f5-sales-demo/xcsh-stats": "21.43.
|
|
67
|
+
"@f5-sales-demo/pi-agent-core": "21.43.2",
|
|
68
|
+
"@f5-sales-demo/pi-ai": "21.43.2",
|
|
69
|
+
"@f5-sales-demo/pi-natives": "21.43.2",
|
|
70
|
+
"@f5-sales-demo/pi-resource-management": "21.43.2",
|
|
71
|
+
"@f5-sales-demo/pi-tui": "21.43.2",
|
|
72
|
+
"@f5-sales-demo/pi-utils": "21.43.2",
|
|
73
|
+
"@f5-sales-demo/xcsh-stats": "21.43.2",
|
|
74
74
|
"@mozilla/readability": "^0.6",
|
|
75
75
|
"@sinclair/typebox": "0.34.52",
|
|
76
76
|
"@tobilu/qmd": "2.8.3",
|
|
@@ -17,17 +17,17 @@ export interface BuildInfo {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export const BUILD_INFO: BuildInfo = {
|
|
20
|
-
"version": "21.43.
|
|
21
|
-
"commit": "
|
|
22
|
-
"shortCommit": "
|
|
20
|
+
"version": "21.43.2",
|
|
21
|
+
"commit": "117d89d2d2ba9619e4c652d28b6db7c7d17eafd9",
|
|
22
|
+
"shortCommit": "117d89d",
|
|
23
23
|
"branch": "main",
|
|
24
|
-
"tag": "v21.43.
|
|
25
|
-
"commitDate": "2026-09-
|
|
26
|
-
"buildDate": "2026-09-
|
|
24
|
+
"tag": "v21.43.2",
|
|
25
|
+
"commitDate": "2026-09-24T04:11:20+00:00",
|
|
26
|
+
"buildDate": "2026-09-24T05:21:13.289Z",
|
|
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/
|
|
32
|
-
"releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v21.43.
|
|
31
|
+
"commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/117d89d2d2ba9619e4c652d28b6db7c7d17eafd9",
|
|
32
|
+
"releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v21.43.2"
|
|
33
33
|
};
|
|
@@ -33,6 +33,7 @@ import { PYTHON_DEFAULT_PREVIEW_LINES } from "../../tools/python";
|
|
|
33
33
|
import { formatExpandHint, replaceTabs, resolveImageOptions, truncateToWidth } from "../../tools/render-utils";
|
|
34
34
|
import { toolRenderers } from "../../tools/renderers";
|
|
35
35
|
import { renderStatusLine } from "../../tui";
|
|
36
|
+
import { renderStructuredRow, renderStructuredTreeLine } from "../../tui/tree-list";
|
|
36
37
|
import { convertToPng } from "../../utils/image-convert";
|
|
37
38
|
import { sanitizeWithImagePassthrough } from "../../utils/image-passthrough";
|
|
38
39
|
import { renderDiff } from "./diff";
|
|
@@ -699,6 +700,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
699
700
|
*/
|
|
700
701
|
#formatToolExecution(width = 80): string {
|
|
701
702
|
const lines: string[] = [];
|
|
703
|
+
const contentWidth = Math.max(1, width - 2); // Text has one cell of padding on each side.
|
|
702
704
|
const icon = this.#isPartial ? "pending" : undefined;
|
|
703
705
|
lines.push(renderStatusLine({ icon, title: this.#toolLabel }, theme));
|
|
704
706
|
|
|
@@ -706,7 +708,14 @@ export class ToolExecutionComponent extends Container {
|
|
|
706
708
|
if (!this.#expanded && argsObject && Object.keys(argsObject).length > 0) {
|
|
707
709
|
const preview = formatArgsInline(argsObject, Math.max(20, width - 10));
|
|
708
710
|
if (preview) {
|
|
709
|
-
lines.push(
|
|
711
|
+
lines.push(
|
|
712
|
+
...renderStructuredRow(
|
|
713
|
+
theme.fg("dim", preview),
|
|
714
|
+
` ${theme.fg("dim", theme.tree.last)} `,
|
|
715
|
+
" ",
|
|
716
|
+
contentWidth,
|
|
717
|
+
),
|
|
718
|
+
);
|
|
710
719
|
}
|
|
711
720
|
}
|
|
712
721
|
|
|
@@ -722,7 +731,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
722
731
|
JSON_TREE_MAX_LINES_EXPANDED,
|
|
723
732
|
JSON_TREE_SCALAR_LEN_EXPANDED,
|
|
724
733
|
);
|
|
725
|
-
lines.push(...tree.lines);
|
|
734
|
+
lines.push(...tree.lines.flatMap(line => renderStructuredTreeLine(line, contentWidth)));
|
|
726
735
|
if (tree.truncated) {
|
|
727
736
|
lines.push(theme.fg("dim", "…"));
|
|
728
737
|
}
|
|
@@ -748,7 +757,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
748
757
|
const tree = renderJsonTreeLines(parsed, theme, maxDepth, maxLines, maxScalarLen);
|
|
749
758
|
|
|
750
759
|
if (tree.lines.length > 0) {
|
|
751
|
-
lines.push(...tree.lines);
|
|
760
|
+
lines.push(...tree.lines.flatMap(line => renderStructuredTreeLine(line, contentWidth)));
|
|
752
761
|
if (!this.#expanded) {
|
|
753
762
|
lines.push(formatExpandHint(theme, this.#expanded, true));
|
|
754
763
|
} else if (tree.truncated) {
|
package/src/task/render.ts
CHANGED
|
@@ -26,7 +26,8 @@ import {
|
|
|
26
26
|
type ReportFindingDetails,
|
|
27
27
|
type SubmitReviewDetails,
|
|
28
28
|
} from "../tools/review";
|
|
29
|
-
import {
|
|
29
|
+
import { Hasher, type RenderCache, renderStatusLine } from "../tui";
|
|
30
|
+
import { renderStructuredRow, renderStructuredTreeLine } from "../tui/tree-list";
|
|
30
31
|
import { subprocessToolRegistry } from "./subprocess-tool-registry";
|
|
31
32
|
import type { AgentProgress, SingleResult, TaskParams, TaskToolDetails } from "./types";
|
|
32
33
|
|
|
@@ -404,7 +405,7 @@ function renderTaskSection(
|
|
|
404
405
|
lines.push(`${continuePrefix}${theme.fg("dim", "Task")}`);
|
|
405
406
|
const taskLines = trimmed.split("\n");
|
|
406
407
|
for (const line of taskLines.slice(0, maxExpanded)) {
|
|
407
|
-
lines.push(`${continuePrefix} ${theme.fg("dim",
|
|
408
|
+
lines.push(`${continuePrefix} ${theme.fg("dim", replaceTabs(line))}`);
|
|
408
409
|
}
|
|
409
410
|
if (taskLines.length > maxExpanded) {
|
|
410
411
|
lines.push(`${continuePrefix} ${theme.fg("dim", formatMoreItems(taskLines.length - maxExpanded, "line"))}`);
|
|
@@ -476,9 +477,6 @@ function formatOutputInline(data: unknown, theme: Theme, maxWidth = 80): string
|
|
|
476
477
|
* Render the tool call arguments.
|
|
477
478
|
*/
|
|
478
479
|
export function renderCall(args: TaskParams, _options: RenderResultOptions, theme: Theme): Component {
|
|
479
|
-
const lines: string[] = [];
|
|
480
|
-
lines.push(renderStatusLine({ icon: "pending", title: "Task", description: args.agent }, theme));
|
|
481
|
-
|
|
482
480
|
const contextTemplate = args.context ?? "";
|
|
483
481
|
const context = contextTemplate.trim();
|
|
484
482
|
const hasContext = context.length > 0;
|
|
@@ -487,26 +485,50 @@ export function renderCall(args: TaskParams, _options: RenderResultOptions, them
|
|
|
487
485
|
const vertical = theme.fg("dim", theme.tree.vertical);
|
|
488
486
|
const showIsolated = "isolated" in args && args.isolated === true;
|
|
489
487
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
488
|
+
return {
|
|
489
|
+
render(width) {
|
|
490
|
+
const lines = [renderStatusLine({ icon: "pending", title: "Task", description: args.agent }, theme)];
|
|
491
|
+
if (hasContext) {
|
|
492
|
+
lines.push(...renderStructuredRow(theme.fg("dim", "Context"), ` ${branch} `, ` ${vertical} `, width));
|
|
493
|
+
for (const line of context.split("\n")) {
|
|
494
|
+
lines.push(...renderStructuredRow(theme.fg("muted", line), ` ${vertical} `, ` ${vertical} `, width));
|
|
495
|
+
}
|
|
496
|
+
const taskPrefix = showIsolated ? branch : last;
|
|
497
|
+
lines.push(
|
|
498
|
+
...renderStructuredRow(
|
|
499
|
+
`${theme.fg("dim", "Tasks")}: ${theme.fg("muted", `${args.tasks.length} agents`)}`,
|
|
500
|
+
` ${taskPrefix} `,
|
|
501
|
+
` ${showIsolated ? vertical : " "} `,
|
|
502
|
+
width,
|
|
503
|
+
),
|
|
504
|
+
);
|
|
505
|
+
if (showIsolated)
|
|
506
|
+
lines.push(
|
|
507
|
+
...renderStructuredRow(
|
|
508
|
+
`${theme.fg("dim", "Isolated")}: ${theme.fg("muted", "true")}`,
|
|
509
|
+
` ${last} `,
|
|
510
|
+
" ",
|
|
511
|
+
width,
|
|
512
|
+
),
|
|
513
|
+
);
|
|
514
|
+
} else {
|
|
515
|
+
lines.push(
|
|
516
|
+
...renderStructuredRow(
|
|
517
|
+
`${theme.fg("dim", "Tasks")}: ${theme.fg("muted", `${args.tasks.length} agents`)}`,
|
|
518
|
+
"",
|
|
519
|
+
"",
|
|
520
|
+
width,
|
|
521
|
+
),
|
|
522
|
+
);
|
|
523
|
+
if (showIsolated)
|
|
524
|
+
lines.push(
|
|
525
|
+
...renderStructuredRow(`${theme.fg("dim", "Isolated")}: ${theme.fg("muted", "true")}`, "", "", width),
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
return lines;
|
|
529
|
+
},
|
|
530
|
+
invalidate() {},
|
|
531
|
+
};
|
|
510
532
|
}
|
|
511
533
|
|
|
512
534
|
/**
|
|
@@ -996,8 +1018,8 @@ export function renderResult(
|
|
|
996
1018
|
}
|
|
997
1019
|
}
|
|
998
1020
|
|
|
999
|
-
const indented = lines.
|
|
1000
|
-
line.length > 0 ?
|
|
1021
|
+
const indented = lines.flatMap(line =>
|
|
1022
|
+
line.length > 0 ? renderStructuredTreeLine(line, Math.max(1, width - 3)).map(row => ` ${row}`) : [""],
|
|
1001
1023
|
);
|
|
1002
1024
|
cached = { key, lines: indented };
|
|
1003
1025
|
return indented;
|
package/src/tools/calculator.ts
CHANGED
|
@@ -523,6 +523,7 @@ export const calculatorToolRenderer = {
|
|
|
523
523
|
const treeLines = renderTreeList(
|
|
524
524
|
{
|
|
525
525
|
items: outputs,
|
|
526
|
+
viewportWidth: width,
|
|
526
527
|
expanded,
|
|
527
528
|
maxCollapsed: COLLAPSED_LIST_LIMIT,
|
|
528
529
|
itemType: "result",
|
|
@@ -530,7 +531,7 @@ export const calculatorToolRenderer = {
|
|
|
530
531
|
},
|
|
531
532
|
uiTheme,
|
|
532
533
|
);
|
|
533
|
-
const lines = [
|
|
534
|
+
const lines = [truncateToWidth(header, width, Ellipsis.Omit), ...treeLines];
|
|
534
535
|
cached = { key, lines };
|
|
535
536
|
return lines;
|
|
536
537
|
},
|
package/src/tools/find.ts
CHANGED
|
@@ -366,6 +366,7 @@ export const findToolRenderer = {
|
|
|
366
366
|
const listLines = renderTreeList(
|
|
367
367
|
{
|
|
368
368
|
items: lines,
|
|
369
|
+
viewportWidth: width,
|
|
369
370
|
expanded,
|
|
370
371
|
maxCollapsed: COLLAPSED_LIST_LIMIT,
|
|
371
372
|
itemType: "file",
|
|
@@ -373,7 +374,7 @@ export const findToolRenderer = {
|
|
|
373
374
|
},
|
|
374
375
|
uiTheme,
|
|
375
376
|
);
|
|
376
|
-
const result = [
|
|
377
|
+
const result = [truncateToWidth(header, width, Ellipsis.Omit), ...listLines];
|
|
377
378
|
cached = { key, lines: result };
|
|
378
379
|
return result;
|
|
379
380
|
},
|
|
@@ -419,12 +420,17 @@ export const findToolRenderer = {
|
|
|
419
420
|
const fileLines = renderFileList(
|
|
420
421
|
{
|
|
421
422
|
files: files.map(entry => ({ path: entry, isDirectory: entry.endsWith("/") })),
|
|
423
|
+
viewportWidth: width,
|
|
422
424
|
expanded,
|
|
423
425
|
maxCollapsed: COLLAPSED_LIST_LIMIT,
|
|
424
426
|
},
|
|
425
427
|
uiTheme,
|
|
426
428
|
);
|
|
427
|
-
const result = [
|
|
429
|
+
const result = [
|
|
430
|
+
truncateToWidth(header, width, Ellipsis.Omit),
|
|
431
|
+
...fileLines,
|
|
432
|
+
...extraLines.map(l => truncateToWidth(l, width, Ellipsis.Omit)),
|
|
433
|
+
];
|
|
428
434
|
cached = { key, lines: result };
|
|
429
435
|
return result;
|
|
430
436
|
},
|
|
@@ -292,19 +292,23 @@ export const searchToolBm25Renderer = {
|
|
|
292
292
|
return new Text(`${header}\n${uiTheme.fg("muted", emptyMessage)}`, 0, 0);
|
|
293
293
|
}
|
|
294
294
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
295
|
+
return {
|
|
296
|
+
render(width) {
|
|
297
|
+
const treeLines = renderTreeList(
|
|
298
|
+
{
|
|
299
|
+
items: details.tools,
|
|
300
|
+
viewportWidth: width,
|
|
301
|
+
expanded: options.expanded,
|
|
302
|
+
maxCollapsed: COLLAPSED_MATCH_LIMIT,
|
|
303
|
+
itemType: "tool",
|
|
304
|
+
renderItem: match => renderMatchLines(match, uiTheme),
|
|
305
|
+
},
|
|
306
|
+
uiTheme,
|
|
307
|
+
);
|
|
308
|
+
return [header, ...treeLines];
|
|
303
309
|
},
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
lines.push(...treeLines);
|
|
307
|
-
return new Text(lines.join("\n"), 0, 0);
|
|
310
|
+
invalidate() {},
|
|
311
|
+
};
|
|
308
312
|
},
|
|
309
313
|
|
|
310
314
|
mergeCallAndResult: true,
|
package/src/tools/todo-write.ts
CHANGED
|
@@ -412,29 +412,33 @@ export const todoWriteToolRenderer = {
|
|
|
412
412
|
}
|
|
413
413
|
|
|
414
414
|
const { expanded } = options;
|
|
415
|
-
const lines: string[] = [header];
|
|
416
415
|
const indent = phases.length > 1 ? " " : "";
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
lines
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
416
|
+
return {
|
|
417
|
+
render(width) {
|
|
418
|
+
const lines: string[] = [header];
|
|
419
|
+
for (const phase of phases) {
|
|
420
|
+
if (phases.length > 1) {
|
|
421
|
+
lines.push(uiTheme.fg("contentAccent", `${indent}${uiTheme.tree.hook} ${phase.name}`));
|
|
422
|
+
}
|
|
423
|
+
const treeLines = renderTreeList(
|
|
424
|
+
{
|
|
425
|
+
items: phase.tasks,
|
|
426
|
+
viewportWidth: Math.max(1, width - indent.length),
|
|
427
|
+
expanded,
|
|
428
|
+
maxCollapsed: PREVIEW_LIMITS.COLLAPSED_ITEMS,
|
|
429
|
+
itemType: "todo",
|
|
430
|
+
renderItem: todo => formatTodoLine(todo, uiTheme, ""),
|
|
431
|
+
},
|
|
432
|
+
uiTheme,
|
|
433
|
+
);
|
|
434
|
+
for (const line of treeLines) lines.push(`${indent}${line}`);
|
|
435
|
+
}
|
|
436
|
+
const summary = renderTodoSummary(allTasks, uiTheme);
|
|
437
|
+
if (summary !== null) lines.push(`${indent}${summary}`);
|
|
438
|
+
return lines;
|
|
439
|
+
},
|
|
440
|
+
invalidate() {},
|
|
441
|
+
};
|
|
438
442
|
},
|
|
439
443
|
mergeCallAndResult: true,
|
|
440
444
|
};
|
package/src/tui/file-list.ts
CHANGED
|
@@ -13,17 +13,19 @@ export interface FileEntry {
|
|
|
13
13
|
|
|
14
14
|
export interface FileListOptions {
|
|
15
15
|
files: FileEntry[];
|
|
16
|
+
viewportWidth?: number;
|
|
16
17
|
expanded?: boolean;
|
|
17
18
|
maxCollapsed?: number;
|
|
18
19
|
showIcons?: boolean;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
export function renderFileList(options: FileListOptions, theme: Theme): string[] {
|
|
22
|
-
const { files, expanded = false, maxCollapsed = 8, showIcons = true } = options;
|
|
23
|
+
const { files, expanded = false, maxCollapsed = 8, showIcons = true, viewportWidth } = options;
|
|
23
24
|
|
|
24
25
|
return renderTreeList(
|
|
25
26
|
{
|
|
26
27
|
items: files,
|
|
28
|
+
viewportWidth,
|
|
27
29
|
expanded,
|
|
28
30
|
maxCollapsed,
|
|
29
31
|
itemType: "file",
|
package/src/tui/output-block.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { ImageProtocol, padding, TERMINAL, visibleWidth, wrapTextWithAnsi } from "@f5-sales-demo/pi-tui";
|
|
5
5
|
import type { Theme, ThemeColor } from "../modes/theme/theme";
|
|
6
6
|
import { getImageLineMask } from "../utils/image-passthrough";
|
|
7
|
+
import { renderStructuredTreeLine } from "./tree-list";
|
|
7
8
|
import type { State } from "./types";
|
|
8
9
|
import type { RenderCache } from "./utils";
|
|
9
10
|
import { Ellipsis, getStateBgColor, Hasher, padToWidth, truncateToWidth } from "./utils";
|
|
@@ -18,7 +19,7 @@ export interface OutputBlockOptions {
|
|
|
18
19
|
header?: string;
|
|
19
20
|
headerMeta?: string;
|
|
20
21
|
state?: State;
|
|
21
|
-
sections?: Array<{ label?: string; lines: string[] }>;
|
|
22
|
+
sections?: Array<{ label?: string; lines: string[]; structured?: boolean }>;
|
|
22
23
|
width: number;
|
|
23
24
|
applyBg?: boolean;
|
|
24
25
|
/** Override the state-derived border color. Always takes precedence, including on error. Use for branded core tools. */
|
|
@@ -108,7 +109,9 @@ export function renderOutputBlock(options: OutputBlockOptions, theme: Theme): st
|
|
|
108
109
|
continue;
|
|
109
110
|
}
|
|
110
111
|
const wrappedLines = wrapContent
|
|
111
|
-
?
|
|
112
|
+
? section.structured
|
|
113
|
+
? renderStructuredTreeLine(line.trimEnd(), contentWidth)
|
|
114
|
+
: wrapTextWithAnsi(line.trimEnd(), contentWidth)
|
|
112
115
|
: // Clip cleanly with no trailing ellipsis — a column of "…" down the
|
|
113
116
|
// right edge of a clipped diagram is clutter, not information.
|
|
114
117
|
[truncateToWidth(line.trimEnd(), contentWidth, Ellipsis.Omit)];
|
|
@@ -165,6 +168,7 @@ export class CachedOutputBlock {
|
|
|
165
168
|
if (options.sections) {
|
|
166
169
|
for (const s of options.sections) {
|
|
167
170
|
h.optional(s.label);
|
|
171
|
+
h.bool(s.structured ?? false);
|
|
168
172
|
for (const line of s.lines) {
|
|
169
173
|
h.str(line);
|
|
170
174
|
}
|
package/src/tui/tree-list.ts
CHANGED
|
@@ -2,12 +2,38 @@
|
|
|
2
2
|
* Hierarchical tree list rendering helper.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { sanitizeText } from "@f5-sales-demo/pi-natives";
|
|
6
|
+
import { replaceTabs, sliceWithWidth, visibleWidth, wrapTextWithAnsi } from "@f5-sales-demo/pi-tui";
|
|
6
7
|
import type { Theme } from "../modes/theme/theme";
|
|
7
8
|
import { formatMoreItems } from "../tools/render-utils";
|
|
8
9
|
import type { TreeContext } from "./types";
|
|
9
10
|
import { getTreeBranch, getTreeContinuePrefix } from "./utils";
|
|
10
11
|
|
|
12
|
+
/** Wrap a structured transcript row while keeping its tree rail on every line. */
|
|
13
|
+
export function renderStructuredRow(
|
|
14
|
+
content: string,
|
|
15
|
+
firstPrefix: string,
|
|
16
|
+
continuationPrefix: string,
|
|
17
|
+
width: number,
|
|
18
|
+
): string[] {
|
|
19
|
+
const normalized = replaceTabs(content);
|
|
20
|
+
const firstWidth = visibleWidth(firstPrefix);
|
|
21
|
+
const continuationWidth = visibleWidth(continuationPrefix);
|
|
22
|
+
const available = Math.max(1, width - Math.max(firstWidth, continuationWidth));
|
|
23
|
+
const wrapped = normalized ? wrapTextWithAnsi(normalized, available) : [""];
|
|
24
|
+
return wrapped.map((line, index) => `${index === 0 ? firstPrefix : continuationPrefix}${line}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Wrap an existing tree row without discarding ANSI styling in its prefix. */
|
|
28
|
+
export function renderStructuredTreeLine(line: string, width: number): string[] {
|
|
29
|
+
const prefix = sanitizeText(line).match(/^[ │├└╰─]*/)?.[0] ?? "";
|
|
30
|
+
const prefixWidth = prefix.length;
|
|
31
|
+
const firstPrefix = sliceWithWidth(line, 0, prefixWidth, true).text;
|
|
32
|
+
const content = sliceWithWidth(line, prefixWidth, Math.max(0, visibleWidth(line) - prefixWidth), true).text;
|
|
33
|
+
const continuationPrefix = prefix.replaceAll("├", "│").replaceAll(/[└╰─]/g, " ");
|
|
34
|
+
return renderStructuredRow(content, firstPrefix, continuationPrefix, width);
|
|
35
|
+
}
|
|
36
|
+
|
|
11
37
|
export interface TreeListOptions<T> {
|
|
12
38
|
items: T[];
|
|
13
39
|
expanded?: boolean;
|
|
@@ -17,13 +43,23 @@ export interface TreeListOptions<T> {
|
|
|
17
43
|
*/
|
|
18
44
|
maxCollapsedLines?: number;
|
|
19
45
|
itemType?: string;
|
|
46
|
+
/** Available terminal columns when list rows should wrap with their tree rails. */
|
|
47
|
+
viewportWidth?: number;
|
|
20
48
|
/** Called once per item with `isLast: false` during budget calculation;
|
|
21
49
|
* line count MUST NOT vary based on `isLast`. */
|
|
22
50
|
renderItem: (item: T, context: TreeContext) => string | string[];
|
|
23
51
|
}
|
|
24
52
|
|
|
25
53
|
export function renderTreeList<T>(options: TreeListOptions<T>, theme: Theme): string[] {
|
|
26
|
-
const {
|
|
54
|
+
const {
|
|
55
|
+
items,
|
|
56
|
+
expanded = false,
|
|
57
|
+
maxCollapsed = 8,
|
|
58
|
+
maxCollapsedLines,
|
|
59
|
+
itemType = "item",
|
|
60
|
+
renderItem,
|
|
61
|
+
viewportWidth,
|
|
62
|
+
} = options;
|
|
27
63
|
const maxItems = expanded ? items.length : Math.min(items.length, maxCollapsed);
|
|
28
64
|
const linesBudget = !expanded && maxCollapsedLines !== undefined ? maxCollapsedLines : Infinity;
|
|
29
65
|
|
|
@@ -49,7 +85,13 @@ export function renderTreeList<T>(options: TreeListOptions<T>, theme: Theme): st
|
|
|
49
85
|
if (linesBudget !== Infinity) {
|
|
50
86
|
fittingCount = 0;
|
|
51
87
|
for (let i = 0; i < maxItems; i++) {
|
|
52
|
-
const count =
|
|
88
|
+
const count =
|
|
89
|
+
viewportWidth === undefined
|
|
90
|
+
? preRendered[i]!.length
|
|
91
|
+
: preRendered[i]!.reduce(
|
|
92
|
+
(total, line) => total + renderStructuredRow(line, "├─ ", "│ ", viewportWidth).length,
|
|
93
|
+
0,
|
|
94
|
+
);
|
|
53
95
|
const remainingAfter = items.length - (i + 1);
|
|
54
96
|
const reservedSummaryLines = remainingAfter > 0 ? 1 : 0;
|
|
55
97
|
if (fittedLineCount + count + reservedSummaryLines > linesBudget) break;
|
|
@@ -70,9 +112,11 @@ export function renderTreeList<T>(options: TreeListOptions<T>, theme: Theme): st
|
|
|
70
112
|
const continuePrefix = `${theme.fg("dim", getTreeContinuePrefix(isLast, theme))}`;
|
|
71
113
|
const itemLines = preRendered[i]!;
|
|
72
114
|
if (itemLines.length === 0) continue;
|
|
73
|
-
lines.push(`${prefix}${replaceTabs(itemLines[0]!)}`);
|
|
115
|
+
if (viewportWidth === undefined) lines.push(`${prefix}${replaceTabs(itemLines[0]!)}`);
|
|
116
|
+
else lines.push(...renderStructuredRow(itemLines[0]!, prefix, continuePrefix, viewportWidth));
|
|
74
117
|
for (let j = 1; j < itemLines.length; j++) {
|
|
75
|
-
lines.push(`${continuePrefix}${replaceTabs(itemLines[j]!)}`);
|
|
118
|
+
if (viewportWidth === undefined) lines.push(`${continuePrefix}${replaceTabs(itemLines[j]!)}`);
|
|
119
|
+
else lines.push(...renderStructuredRow(itemLines[j]!, continuePrefix, continuePrefix, viewportWidth));
|
|
76
120
|
}
|
|
77
121
|
}
|
|
78
122
|
|
package/src/web/search/render.ts
CHANGED
|
@@ -216,6 +216,7 @@ export function renderSearchResult(
|
|
|
216
216
|
const sourceTree = renderTreeList(
|
|
217
217
|
{
|
|
218
218
|
items: sources,
|
|
219
|
+
viewportWidth: Math.max(1, width - 3),
|
|
219
220
|
expanded,
|
|
220
221
|
maxCollapsed: MAX_COLLAPSED_ITEMS,
|
|
221
222
|
itemType: "source",
|
|
@@ -226,7 +227,7 @@ export function renderSearchResult(
|
|
|
226
227
|
: typeof src.url === "string" && src.url.trim()
|
|
227
228
|
? src.url
|
|
228
229
|
: "Untitled";
|
|
229
|
-
const title =
|
|
230
|
+
const title = titleText;
|
|
230
231
|
const url = typeof src.url === "string" ? src.url : "";
|
|
231
232
|
const domain = url ? getDomain(url) : "";
|
|
232
233
|
const age =
|
|
@@ -240,9 +241,7 @@ export function renderSearchResult(
|
|
|
240
241
|
if (age) metaParts.push(theme.fg("muted", age));
|
|
241
242
|
const metaSep = theme.fg("dim", theme.sep.dot);
|
|
242
243
|
const metaSuffix = metaParts.length > 0 ? ` ${metaParts.join(metaSep)}` : "";
|
|
243
|
-
const srcLines: string[] = [
|
|
244
|
-
truncateToWidth(`${theme.fg("contentAccent", title)}${metaSuffix}`, width),
|
|
245
|
-
];
|
|
244
|
+
const srcLines: string[] = [`${theme.fg("contentAccent", title)}${metaSuffix}`];
|
|
246
245
|
const snippetText = typeof src.snippet === "string" ? src.snippet : "";
|
|
247
246
|
if (snippetText.trim()) {
|
|
248
247
|
const snippetLines = getPreviewLines(snippetText, MAX_SNIPPET_LINES, width);
|
|
@@ -250,7 +249,7 @@ export function renderSearchResult(
|
|
|
250
249
|
srcLines.push(theme.fg("muted", `${theme.format.dash} ${snippetLine}`));
|
|
251
250
|
}
|
|
252
251
|
}
|
|
253
|
-
if (url) srcLines.push(theme.fg("mdLinkUrl",
|
|
252
|
+
if (url) srcLines.push(theme.fg("mdLinkUrl", url));
|
|
254
253
|
return srcLines;
|
|
255
254
|
},
|
|
256
255
|
},
|
|
@@ -268,6 +267,7 @@ export function renderSearchResult(
|
|
|
268
267
|
const answerTree = renderTreeList(
|
|
269
268
|
{
|
|
270
269
|
items: answerTreeLines,
|
|
270
|
+
viewportWidth: Math.max(1, contentWidth),
|
|
271
271
|
expanded: true,
|
|
272
272
|
maxCollapsed: answerTreeLines.length,
|
|
273
273
|
itemType: "line",
|
|
@@ -303,10 +303,12 @@ export function renderSearchResult(
|
|
|
303
303
|
{
|
|
304
304
|
label: theme.fg("toolTitle", "Answer"),
|
|
305
305
|
lines: answerTree,
|
|
306
|
+
structured: true,
|
|
306
307
|
},
|
|
307
308
|
{
|
|
308
309
|
label: theme.fg("toolTitle", "Sources"),
|
|
309
310
|
lines: sourceTree.length > 0 ? sourceTree : [theme.fg("muted", "No sources returned")],
|
|
311
|
+
structured: true,
|
|
310
312
|
},
|
|
311
313
|
{ label: theme.fg("toolTitle", "Metadata"), lines: allMetaLines },
|
|
312
314
|
],
|