- 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.
110 lines
3.3 KiB
TypeScript
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;
|
|
}
|