返回 DeepSeek-Reasonix
projectTreePresentation.ts
根目录 / desktop / frontend / src / lib / projectTreePresentation.ts
1 import { asArray } from "./array";
2 import { isRuntimeSessionNode, isTopicNode, type WorkbenchSortMode } from "./projectTreeTopic";
3 import { topicActivityTime } from "./session";
4 import type { ProjectNode } from "./types";
5 import { projectSessionIdentity } from "./projectSessionIdentity";
6
7 export type PinnedTreeSections = {
8 pinned: ProjectNode[];
9 projects: ProjectNode[];
10 };
11
12 function topicSortValue(node: ProjectNode, sortMode: WorkbenchSortMode): number {
13 if (sortMode === "created") return node.createdAt || node.lastActivityAt || 0;
14 return topicActivityTime(node);
15 }
16
17 function projectSortValue(node: ProjectNode, sortMode: WorkbenchSortMode): number {
18 return asArray(node.children).reduce((max, child) => {
19 if (!isTopicNode(child)) return max;
20 return Math.max(max, topicSortValue(child, sortMode));
21 }, 0);
22 }
23
24 function manualTopicOrder(a: ProjectNode, b: ProjectNode): number {
25 const aOrder = typeof a.sortOrder === "number" && a.sortOrder >= 0 ? a.sortOrder : Number.MAX_SAFE_INTEGER;
26 const bOrder = typeof b.sortOrder === "number" && b.sortOrder >= 0 ? b.sortOrder : Number.MAX_SAFE_INTEGER;
27 return aOrder === bOrder ? 0 : aOrder - bOrder;
28 }
29
30 function sortWorkbenchChildren(children: ProjectNode[], sortMode: WorkbenchSortMode): ProjectNode[] {
31 return [...children].sort((a, b) => {
32 if (!isTopicNode(a) || !isTopicNode(b)) return 0;
33 if (Boolean(a.pinned) !== Boolean(b.pinned)) return a.pinned ? -1 : 1;
34 const manualOrder = manualTopicOrder(a, b);
35 if (manualOrder !== 0) return manualOrder;
36 const activityOrder = topicSortValue(b, sortMode) - topicSortValue(a, sortMode);
37 if (activityOrder !== 0) return activityOrder;
38 const aKey = projectSessionIdentity(a);
39 const bKey = projectSessionIdentity(b);
40 return aKey < bKey ? -1 : aKey > bKey ? 1 : 0;
41 });
42 }
43
44 export function arrangeWorkbenchTree(
45 nodes: ProjectNode[],
46 sortMode: WorkbenchSortMode,
47 ): ProjectNode[] {
48 return nodes.map((node) => {
49 if (node.kind !== "project" && node.kind !== "global_folder") return node;
50 return { ...node, children: sortWorkbenchChildren(asArray(node.children), sortMode) };
51 });
52 }
53
54 function projectTreeTopicIdentity(node: ProjectNode): string | null {
55 if (!isTopicNode(node) && !isRuntimeSessionNode(node)) return null;
56 const global = node.kind === "global_topic" || node.kind === "global_session";
57 return `${global ? "global" : "project"}\u001f${global ? "" : node.root ?? ""}\u001f${projectSessionIdentity(node)}`;
58 }
59
60 export function splitPinnedProjectTree(
61 nodes: ProjectNode[],
62 sortMode: WorkbenchSortMode,
63 includePinnedProjects = true,
64 ): PinnedTreeSections {
65 const pinnedTopics: ProjectNode[] = [];
66 const pinnedProjects: ProjectNode[] = [];
67 const projects: ProjectNode[] = [];
68 // A pinned topic shell can coexist briefly with a runtime session projection.
69 // Collect identities first so its source folder cannot paint it twice.
70 const pinnedTopicIdentities = new Set<string>();
71 for (const node of nodes) {
72 const identity = projectTreeTopicIdentity(node);
73 if (identity && node.pinned) pinnedTopicIdentities.add(identity);
74 if (node.kind !== "project" && node.kind !== "global_folder") continue;
75 for (const child of asArray(node.children)) {
76 const childIdentity = projectTreeTopicIdentity(child);
77 if (childIdentity && child.pinned) pinnedTopicIdentities.add(childIdentity);
78 }
79 }
80
81 for (const node of nodes) {
82 if (!node) continue;
83 const isFolder = node.kind === "project" || node.kind === "global_folder";
84 if (!isFolder) {
85 const identity = projectTreeTopicIdentity(node);
86 if (node.pinned) pinnedTopics.push(node);
87 else if (!identity || !pinnedTopicIdentities.has(identity)) projects.push(node);
88 continue;
89 }
90
91 if (includePinnedProjects && node.pinned && node.kind === "project") {
92 pinnedProjects.push(node);
93 continue;
94 }
95
96 const nextChildren: ProjectNode[] = [];
97 for (const child of asArray(node.children)) {
98 const identity = projectTreeTopicIdentity(child);
99 if (isTopicNode(child) && child.pinned) {
100 pinnedTopics.push(child);
101 continue;
102 }
103 if (identity && pinnedTopicIdentities.has(identity)) continue;
104 nextChildren.push(child);
105 }
106 projects.push({ ...node, children: nextChildren });
107 }
108
109 pinnedTopics.sort((a, b) => topicSortValue(b, sortMode) - topicSortValue(a, sortMode));
110 pinnedProjects.sort((a, b) => projectSortValue(b, sortMode) - projectSortValue(a, sortMode));
111 return { pinned: [...pinnedTopics, ...pinnedProjects], projects };
112 }
113
113 lines TYPESCRIPT