agent-ecosystem/src/renderer/utils/fileTreeBuilder.ts
iliya 85684b59e8 feat: enhance TeamMemberLogsFinder with team detection and task handling improvements
- Updated fileMentionsTaskId method to include an 'assumeTeam' parameter for better team detection in logs.
- Added logic to extract team information from process entries and message content, improving task attribution accuracy.
- Enhanced handling of task updates without explicit team names, ensuring correct task processing in solo scenarios.
- Improved overall robustness of log parsing and team identification, facilitating better task management in team environments.
2026-03-04 17:08:34 +02:00

110 lines
3.3 KiB
TypeScript

/**
* Generic tree builder — converts a flat list of items with paths
* into a hierarchical tree structure with single-child directory collapsing.
*
* Used by ReviewFileTree (FileChangeSummary) and EditorFileTree (FileTreeEntry).
*/
import { splitPath as splitPathCrossPlatform } from '@shared/utils/platformPath';
export interface TreeNode<T> {
name: string;
fullPath: string;
isFile: boolean;
data?: T;
children: TreeNode<T>[];
}
/**
* Build a hierarchical tree from a flat list of items.
*
* @param items - Flat list of items (files/entries)
* @param getPath - Extract relative path from item (using '/' separator)
* @param options.collapse - Merge single-child intermediate directories (default: true)
*/
export function buildTree<T>(
items: T[],
getPath: (item: T) => string,
options?: { collapse?: boolean }
): TreeNode<T>[] {
const root: TreeNode<T> = { name: '', fullPath: '', isFile: false, children: [] };
for (const item of items) {
const parts = splitPathCrossPlatform(getPath(item));
let current = root;
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
const isLast = i === parts.length - 1;
const fullPath = parts.slice(0, i + 1).join('/');
let child = current.children.find((c) => c.name === part);
if (!child) {
child = {
name: part,
fullPath,
isFile: isLast,
data: isLast ? item : undefined,
children: [],
};
current.children.push(child);
}
current = child;
}
}
if (options?.collapse === false) {
return root.children;
}
// Collapse children individually — root itself has empty name and must not participate
return root.children.map(collapseTree);
}
/** Merge single-child intermediate directories: a/ → b/ → c becomes a/b/c */
function collapseTree<T>(node: TreeNode<T>): TreeNode<T> {
const collapsed: TreeNode<T> = { ...node, children: node.children.map(collapseTree) };
if (!collapsed.isFile && collapsed.children.length === 1 && !collapsed.children[0].isFile) {
const child = collapsed.children[0];
return {
...child,
name: `${collapsed.name}/${child.name}`,
children: child.children,
};
}
return collapsed;
}
/** Sort tree nodes: directories first, then alphabetical */
export function sortTreeNodes<T>(nodes: TreeNode<T>[]): TreeNode<T>[] {
return [...nodes].sort((a, b) => {
if (a.isFile !== b.isFile) return a.isFile ? 1 : -1;
return a.name.localeCompare(b.name);
});
}
/**
* Flatten a sorted tree into a list of leaf items in display order.
* Mirrors the visual order of ReviewFileTree (directories first, then alphabetical at each level).
*/
function collectLeaves<T>(nodes: TreeNode<T>[], out: T[]): void {
for (const node of sortTreeNodes(nodes)) {
if (node.isFile && node.data != null) {
out.push(node.data);
} else {
collectLeaves(node.children, out);
}
}
}
/**
* Sort a flat list of items to match the visual order of the file tree
* (directories first, then alphabetical at each level).
*/
export function sortItemsAsTree<T>(items: T[], getPath: (item: T) => string): T[] {
if (items.length <= 1) return items;
const tree = buildTree(items, getPath);
const result: T[] = [];
collectLeaves(tree, result);
return result;
}