mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 13:03:42 +00:00
fix(coding-agent): fix tree indentation after filtering
This commit is contained in:
parent
f74f48660f
commit
3a89ebbe7c
3 changed files with 268 additions and 1 deletions
|
|
@ -2,6 +2,10 @@
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Session tree now preserves branch connectors and indentation when filters hide intermediate entries so descendants attach to the nearest visible ancestor and sibling branches align. Fixed in both TUI and HTML export ([#739](https://github.com/badlogic/pi-mono/pull/739) by [@w-winter](https://github.com/w-winter))
|
||||
|
||||
## [0.46.0] - 2026-01-15
|
||||
|
||||
### Fixed
|
||||
|
|
|
|||
|
|
@ -321,7 +321,7 @@
|
|||
function filterNodes(flatNodes, currentLeafId) {
|
||||
const searchTokens = searchQuery.toLowerCase().split(/\s+/).filter(Boolean);
|
||||
|
||||
return flatNodes.filter(flatNode => {
|
||||
const filtered = flatNodes.filter(flatNode => {
|
||||
const entry = flatNode.node.entry;
|
||||
const label = flatNode.node.label;
|
||||
const isCurrentLeaf = entry.id === currentLeafId;
|
||||
|
|
@ -369,6 +369,139 @@
|
|||
|
||||
return true;
|
||||
});
|
||||
|
||||
// Recalculate visual structure based on visible tree
|
||||
recalculateVisualStructure(filtered, flatNodes);
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recompute indentation/connectors for the filtered view
|
||||
*
|
||||
* Filtering can hide intermediate entries; descendants attach to the nearest visible ancestor.
|
||||
* Keep indentation semantics aligned with flattenTree() so single-child chains don't drift right.
|
||||
*/
|
||||
function recalculateVisualStructure(filteredNodes, allFlatNodes) {
|
||||
if (filteredNodes.length === 0) return;
|
||||
|
||||
const visibleIds = new Set(filteredNodes.map(n => n.node.entry.id));
|
||||
|
||||
// Build entry map for parent lookup (using full tree)
|
||||
const entryMap = new Map();
|
||||
for (const flatNode of allFlatNodes) {
|
||||
entryMap.set(flatNode.node.entry.id, flatNode);
|
||||
}
|
||||
|
||||
// Find nearest visible ancestor for a node
|
||||
function findVisibleAncestor(nodeId) {
|
||||
let currentId = entryMap.get(nodeId)?.node.entry.parentId;
|
||||
while (currentId != null) {
|
||||
if (visibleIds.has(currentId)) {
|
||||
return currentId;
|
||||
}
|
||||
currentId = entryMap.get(currentId)?.node.entry.parentId;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Build visible tree structure
|
||||
const visibleParent = new Map();
|
||||
const visibleChildren = new Map();
|
||||
visibleChildren.set(null, []); // root-level nodes
|
||||
|
||||
for (const flatNode of filteredNodes) {
|
||||
const nodeId = flatNode.node.entry.id;
|
||||
const ancestorId = findVisibleAncestor(nodeId);
|
||||
visibleParent.set(nodeId, ancestorId);
|
||||
|
||||
if (!visibleChildren.has(ancestorId)) {
|
||||
visibleChildren.set(ancestorId, []);
|
||||
}
|
||||
visibleChildren.get(ancestorId).push(nodeId);
|
||||
}
|
||||
|
||||
// Update multipleRoots based on visible roots
|
||||
const visibleRootIds = visibleChildren.get(null);
|
||||
const multipleRoots = visibleRootIds.length > 1;
|
||||
|
||||
// Build a map for quick lookup: nodeId → FlatNode
|
||||
const filteredNodeMap = new Map();
|
||||
for (const flatNode of filteredNodes) {
|
||||
filteredNodeMap.set(flatNode.node.entry.id, flatNode);
|
||||
}
|
||||
|
||||
// DFS traversal of visible tree, applying same indentation rules as flattenTree()
|
||||
// Stack items: [nodeId, indent, justBranched, showConnector, isLast, gutters, isVirtualRootChild]
|
||||
const stack = [];
|
||||
|
||||
// Add visible roots in reverse order (to process in forward order via stack)
|
||||
for (let i = visibleRootIds.length - 1; i >= 0; i--) {
|
||||
const isLast = i === visibleRootIds.length - 1;
|
||||
stack.push([
|
||||
visibleRootIds[i],
|
||||
multipleRoots ? 1 : 0,
|
||||
multipleRoots,
|
||||
multipleRoots,
|
||||
isLast,
|
||||
[],
|
||||
multipleRoots
|
||||
]);
|
||||
}
|
||||
|
||||
while (stack.length > 0) {
|
||||
const [nodeId, indent, justBranched, showConnector, isLast, gutters, isVirtualRootChild] = stack.pop();
|
||||
|
||||
const flatNode = filteredNodeMap.get(nodeId);
|
||||
if (!flatNode) continue;
|
||||
|
||||
// Update this node's visual properties
|
||||
flatNode.indent = indent;
|
||||
flatNode.showConnector = showConnector;
|
||||
flatNode.isLast = isLast;
|
||||
flatNode.gutters = gutters;
|
||||
flatNode.isVirtualRootChild = isVirtualRootChild;
|
||||
flatNode.multipleRoots = multipleRoots;
|
||||
|
||||
// Get visible children of this node
|
||||
const children = visibleChildren.get(nodeId) || [];
|
||||
const multipleChildren = children.length > 1;
|
||||
|
||||
// Calculate child indent using same rules as flattenTree():
|
||||
// - Parent branches (multiple children): children get +1
|
||||
// - Just branched and indent > 0: children get +1 for visual grouping
|
||||
// - Single-child chain: stay flat
|
||||
let childIndent;
|
||||
if (multipleChildren) {
|
||||
childIndent = indent + 1;
|
||||
} else if (justBranched && indent > 0) {
|
||||
childIndent = indent + 1;
|
||||
} else {
|
||||
childIndent = indent;
|
||||
}
|
||||
|
||||
// Build gutters for children (same logic as flattenTree)
|
||||
const connectorDisplayed = showConnector && !isVirtualRootChild;
|
||||
const currentDisplayIndent = multipleRoots ? Math.max(0, indent - 1) : indent;
|
||||
const connectorPosition = Math.max(0, currentDisplayIndent - 1);
|
||||
const childGutters = connectorDisplayed
|
||||
? [...gutters, { position: connectorPosition, show: !isLast }]
|
||||
: gutters;
|
||||
|
||||
// Add children in reverse order (to process in forward order via stack)
|
||||
for (let i = children.length - 1; i >= 0; i--) {
|
||||
const childIsLast = i === children.length - 1;
|
||||
stack.push([
|
||||
children[i],
|
||||
childIndent,
|
||||
multipleChildren,
|
||||
multipleChildren,
|
||||
childIsLast,
|
||||
childGutters,
|
||||
false
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
|
|
|
|||
|
|
@ -302,6 +302,9 @@ class TreeList implements Component {
|
|||
return true;
|
||||
});
|
||||
|
||||
// Recalculate visual structure (indent, connectors, gutters) based on visible tree
|
||||
this.recalculateVisualStructure();
|
||||
|
||||
// Try to preserve cursor on the same node after filtering
|
||||
if (previouslySelectedId) {
|
||||
const newIndex = this.filteredNodes.findIndex((n) => n.node.entry.id === previouslySelectedId);
|
||||
|
|
@ -317,6 +320,133 @@ class TreeList implements Component {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recompute indentation/connectors for the filtered view
|
||||
*
|
||||
* Filtering can hide intermediate entries; descendants attach to the nearest visible ancestor.
|
||||
* Keep indentation semantics aligned with flattenTree() so single-child chains don't drift right.
|
||||
*/
|
||||
private recalculateVisualStructure(): void {
|
||||
if (this.filteredNodes.length === 0) return;
|
||||
|
||||
const visibleIds = new Set(this.filteredNodes.map((n) => n.node.entry.id));
|
||||
|
||||
// Build entry map for efficient parent lookup (using full tree)
|
||||
const entryMap = new Map<string, FlatNode>();
|
||||
for (const flatNode of this.flatNodes) {
|
||||
entryMap.set(flatNode.node.entry.id, flatNode);
|
||||
}
|
||||
|
||||
// Find nearest visible ancestor for a node
|
||||
const findVisibleAncestor = (nodeId: string): string | null => {
|
||||
let currentId = entryMap.get(nodeId)?.node.entry.parentId ?? null;
|
||||
while (currentId !== null) {
|
||||
if (visibleIds.has(currentId)) {
|
||||
return currentId;
|
||||
}
|
||||
currentId = entryMap.get(currentId)?.node.entry.parentId ?? null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// Build visible tree structure:
|
||||
// - visibleParent: nodeId → nearest visible ancestor (or null for roots)
|
||||
// - visibleChildren: parentId → list of visible children (in filteredNodes order)
|
||||
const visibleParent = new Map<string, string | null>();
|
||||
const visibleChildren = new Map<string | null, string[]>();
|
||||
visibleChildren.set(null, []); // root-level nodes
|
||||
|
||||
for (const flatNode of this.filteredNodes) {
|
||||
const nodeId = flatNode.node.entry.id;
|
||||
const ancestorId = findVisibleAncestor(nodeId);
|
||||
visibleParent.set(nodeId, ancestorId);
|
||||
|
||||
if (!visibleChildren.has(ancestorId)) {
|
||||
visibleChildren.set(ancestorId, []);
|
||||
}
|
||||
visibleChildren.get(ancestorId)!.push(nodeId);
|
||||
}
|
||||
|
||||
// Update multipleRoots based on visible roots
|
||||
const visibleRootIds = visibleChildren.get(null)!;
|
||||
this.multipleRoots = visibleRootIds.length > 1;
|
||||
|
||||
// Build a map for quick lookup: nodeId → FlatNode
|
||||
const filteredNodeMap = new Map<string, FlatNode>();
|
||||
for (const flatNode of this.filteredNodes) {
|
||||
filteredNodeMap.set(flatNode.node.entry.id, flatNode);
|
||||
}
|
||||
|
||||
// DFS over the visible tree using flattenTree() indentation semantics
|
||||
// Stack items: [nodeId, indent, justBranched, showConnector, isLast, gutters, isVirtualRootChild]
|
||||
type StackItem = [string, number, boolean, boolean, boolean, GutterInfo[], boolean];
|
||||
const stack: StackItem[] = [];
|
||||
|
||||
// Add visible roots in reverse order (to process in forward order via stack)
|
||||
for (let i = visibleRootIds.length - 1; i >= 0; i--) {
|
||||
const isLast = i === visibleRootIds.length - 1;
|
||||
stack.push([
|
||||
visibleRootIds[i],
|
||||
this.multipleRoots ? 1 : 0,
|
||||
this.multipleRoots,
|
||||
this.multipleRoots,
|
||||
isLast,
|
||||
[],
|
||||
this.multipleRoots,
|
||||
]);
|
||||
}
|
||||
|
||||
while (stack.length > 0) {
|
||||
const [nodeId, indent, justBranched, showConnector, isLast, gutters, isVirtualRootChild] = stack.pop()!;
|
||||
|
||||
const flatNode = filteredNodeMap.get(nodeId);
|
||||
if (!flatNode) continue;
|
||||
|
||||
// Update this node's visual properties
|
||||
flatNode.indent = indent;
|
||||
flatNode.showConnector = showConnector;
|
||||
flatNode.isLast = isLast;
|
||||
flatNode.gutters = gutters;
|
||||
flatNode.isVirtualRootChild = isVirtualRootChild;
|
||||
|
||||
// Get visible children of this node
|
||||
const children = visibleChildren.get(nodeId) || [];
|
||||
const multipleChildren = children.length > 1;
|
||||
|
||||
// Child indent follows flattenTree(): branch points (and first generation after a branch) shift +1
|
||||
let childIndent: number;
|
||||
if (multipleChildren) {
|
||||
childIndent = indent + 1;
|
||||
} else if (justBranched && indent > 0) {
|
||||
childIndent = indent + 1;
|
||||
} else {
|
||||
childIndent = indent;
|
||||
}
|
||||
|
||||
// Child gutters follow flattenTree() connector/gutter rules
|
||||
const connectorDisplayed = showConnector && !isVirtualRootChild;
|
||||
const currentDisplayIndent = this.multipleRoots ? Math.max(0, indent - 1) : indent;
|
||||
const connectorPosition = Math.max(0, currentDisplayIndent - 1);
|
||||
const childGutters: GutterInfo[] = connectorDisplayed
|
||||
? [...gutters, { position: connectorPosition, show: !isLast }]
|
||||
: gutters;
|
||||
|
||||
// Add children in reverse order (to process in forward order via stack)
|
||||
for (let i = children.length - 1; i >= 0; i--) {
|
||||
const childIsLast = i === children.length - 1;
|
||||
stack.push([
|
||||
children[i],
|
||||
childIndent,
|
||||
multipleChildren,
|
||||
multipleChildren,
|
||||
childIsLast,
|
||||
childGutters,
|
||||
false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Get searchable text content from a node */
|
||||
private getSearchableText(node: SessionTreeNode): string {
|
||||
const entry = node.entry;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue