refactor(coding-agent): cleaner tree gutter/indent logic

- Only indent when parent has siblings (branch point)
- gutterLevels array tracks which levels need │ vs spaces
- connector: none/branch/last for ├─/└─ display
This commit is contained in:
Mario Zechner 2025-12-29 14:30:48 +01:00
parent 1d90592df1
commit 6fbc3a01ef

View file

@ -21,12 +21,10 @@ import { DynamicBorder } from "./dynamic-border.js";
/** Flattened tree node for navigation */ /** Flattened tree node for navigation */
interface FlatNode { interface FlatNode {
node: SessionTreeNode; node: SessionTreeNode;
depth: number; /** For each indent level, true = show │ gutter, false = show spaces */
isLast: boolean; gutterLevels: boolean[];
/** Prefix chars showing tree structure (│ and spaces for gutter) */ /** none = no connector, branch = ├─, last = └─ */
prefix: string; connector: "none" | "branch" | "last";
/** Whether to show ├─/└─ connector (true at branch points) */
showConnector: boolean;
} }
/** Filter mode for tree display */ /** Filter mode for tree display */
@ -74,18 +72,26 @@ class TreeList implements Component {
const result: FlatNode[] = []; const result: FlatNode[] = [];
this.toolCallMap.clear(); this.toolCallMap.clear();
// Use iterative approach to avoid stack overflow on deep trees // Iterative traversal to avoid stack overflow
// Stack items: [node, prefix, isLast, showConnector] // Stack items: [node, gutterLevels, connector]
const stack: [SessionTreeNode, string, boolean, boolean][] = []; // gutterLevels: for each indent level, true = show │, false = show spaces
// connector: none/branch/last
type StackItem = [SessionTreeNode, boolean[], "none" | "branch" | "last"];
const stack: StackItem[] = [];
// Add roots in reverse order so first root is processed first // Add roots in reverse order
const multipleRoots = roots.length > 1; const multipleRoots = roots.length > 1;
for (let i = roots.length - 1; i >= 0; i--) { for (let i = roots.length - 1; i >= 0; i--) {
stack.push([roots[i], "", i === roots.length - 1, multipleRoots]); const connector: "none" | "branch" | "last" = multipleRoots
? i === roots.length - 1
? "last"
: "branch"
: "none";
stack.push([roots[i], [], connector]);
} }
while (stack.length > 0) { while (stack.length > 0) {
const [node, prefix, isLast, showConnector] = stack.pop()!; const [node, gutterLevels, connector] = stack.pop()!;
// Extract tool calls from assistant messages for later lookup // Extract tool calls from assistant messages for later lookup
const entry = node.entry; const entry = node.entry;
@ -101,27 +107,32 @@ class TreeList implements Component {
} }
} }
const depth = prefix.length / 3 + (showConnector ? 1 : 0); result.push({ node, gutterLevels, connector });
result.push({ node, depth, isLast, prefix, showConnector });
const children = node.children; const children = node.children;
const multipleChildren = children.length > 1; const multipleChildren = children.length > 1;
// Build prefix for children // Build gutterLevels for children
let childPrefix: string; // Only add a gutter level if THIS node had a connector (was in a branch)
if (showConnector) { let childGutterLevels: boolean[];
childPrefix = prefix + (isLast ? " " : "│ "); if (connector !== "none") {
} else if (multipleChildren) { // We showed a connector, so children get a gutter level
childPrefix = prefix; // If we're not last, show │; if we are last, show spaces
childGutterLevels = [...gutterLevels, connector !== "last"];
} else { } else {
childPrefix = prefix; // No connector, children inherit same gutter levels
childGutterLevels = gutterLevels;
} }
// Add children in reverse order so first child is processed first // Add children in reverse order
for (let i = children.length - 1; i >= 0; i--) { for (let i = children.length - 1; i >= 0; i--) {
const child = children[i]; const child = children[i];
const childIsLast = i === children.length - 1; const childConnector: "none" | "branch" | "last" = multipleChildren
stack.push([child, childPrefix, childIsLast, multipleChildren]); ? i === children.length - 1
? "last"
: "branch"
: "none";
stack.push([child, childGutterLevels, childConnector]);
} }
} }
@ -301,13 +312,14 @@ class TreeList implements Component {
// Build line: cursor + gutter + connector + label + content + suffix // Build line: cursor + gutter + connector + label + content + suffix
const cursor = isSelected ? theme.fg("accent", " ") : " "; const cursor = isSelected ? theme.fg("accent", " ") : " ";
const gutter = flatNode.prefix ? theme.fg("dim", flatNode.prefix) : ""; const gutter = flatNode.gutterLevels.map((show) => (show ? "│ " : " ")).join("");
const connector = flatNode.showConnector ? theme.fg("dim", flatNode.isLast ? "└─ " : "├─ ") : ""; const connector = flatNode.connector === "branch" ? "├─ " : flatNode.connector === "last" ? "└─ " : "";
const prefix = gutter || connector ? theme.fg("dim", gutter + connector) : "";
const label = flatNode.node.label ? theme.fg("warning", `[${flatNode.node.label}] `) : ""; const label = flatNode.node.label ? theme.fg("warning", `[${flatNode.node.label}] `) : "";
const content = this.getEntryDisplayText(flatNode.node, isSelected); const content = this.getEntryDisplayText(flatNode.node, isSelected);
const suffix = isCurrentLeaf ? theme.fg("accent", " *") : ""; const suffix = isCurrentLeaf ? theme.fg("accent", " *") : "";
const line = cursor + gutter + connector + label + content + suffix; const line = cursor + prefix + label + content + suffix;
lines.push(truncateToWidth(line, width)); lines.push(truncateToWidth(line, width));
} }