| 1 | import { asArray } from "./array"; |
| 2 | import { projectSessionRowKey } from "./projectSessionIdentity"; |
| 3 | import { topicIsActive } from "./projectTreeTopic"; |
| 4 | import type { ProjectNode } from "./types"; |
| 5 | |
| 6 | export function projectTreeNodeKey(node: ProjectNode, depth: number): string { |
| 7 | if (node.session || node.sessionPath || node.source || node.remoteSession || node.tabId) return projectSessionRowKey(node); |
| 8 | return node.key || `${node.kind}-${node.root ?? ""}-${node.topicId ?? ""}-${node.sessionPath ?? ""}-${depth}`; |
| 9 | } |
| 10 | |
| 11 | export function collapsibleProjectTreeFolderKeys(nodes: ProjectNode[], depth = 0): string[] { |
| 12 | const keys: string[] = []; |
| 13 | for (const node of nodes) { |
| 14 | if (!node) continue; |
| 15 | const children = asArray(node.children); |
| 16 | if ((node.kind === "project" || node.kind === "global_folder") && children.length > 0) keys.push(projectTreeNodeKey(node, depth)); |
| 17 | keys.push(...collapsibleProjectTreeFolderKeys(children, depth + 1)); |
| 18 | } |
| 19 | return keys; |
| 20 | } |
| 21 | |
| 22 | export function activeSessionAncestorKeys( |
| 23 | nodes: ProjectNode[], |
| 24 | activeScope?: string, |
| 25 | activeWorkspaceRoot?: string, |
| 26 | activeTopicId?: string, |
| 27 | activeSessionPath?: string, |
| 28 | ): string[] { |
| 29 | const walk = (nodeList: ProjectNode[], ancestors: string[]): string[] | null => { |
| 30 | for (const node of nodeList) { |
| 31 | if (!node) continue; |
| 32 | if (topicIsActive(node, activeScope, activeWorkspaceRoot, activeTopicId, activeSessionPath)) return ancestors; |
| 33 | const children = asArray(node.children); |
| 34 | if (children.length > 0) { |
| 35 | const next = walk(children, [...ancestors, projectTreeNodeKey(node, ancestors.length)]); |
| 36 | if (next) return next; |
| 37 | } |
| 38 | } |
| 39 | return null; |
| 40 | }; |
| 41 | const found = walk(nodes, []); |
| 42 | if (found) return found; |
| 43 | // Shell-only snapshots no longer embed topics. Expand the matching project or |
| 44 | // Global folder by workspace identity so the first lazy page can load. |
| 45 | const scope = (activeScope ?? "").trim(); |
| 46 | const root = (activeWorkspaceRoot ?? "").trim(); |
| 47 | for (const node of nodes) { |
| 48 | if (!node) continue; |
| 49 | if (scope === "global" && node.kind === "global_folder") return [projectTreeNodeKey(node, 0)]; |
| 50 | if (node.kind === "project" && root && (node.root === root || node.root === activeWorkspaceRoot)) return [projectTreeNodeKey(node, 0)]; |
| 51 | if (!scope && !root && activeTopicId && (node.kind === "project" || node.kind === "global_folder")) return [projectTreeNodeKey(node, 0)]; |
| 52 | } |
| 53 | return []; |
| 54 | } |
| 55 | |
| 56 | export function defaultExpandedProjectTreeKeys( |
| 57 | nodes: ProjectNode[], |
| 58 | activeScope?: string, |
| 59 | activeWorkspaceRoot?: string, |
| 60 | activeTopicId?: string, |
| 61 | activeSessionPath?: string, |
| 62 | ): string[] { |
| 63 | const keys = activeSessionAncestorKeys(nodes, activeScope, activeWorkspaceRoot, activeTopicId, activeSessionPath); |
| 64 | if (keys.length > 0 || [activeScope, activeWorkspaceRoot, activeTopicId, activeSessionPath].some((v) => (v ?? "").trim())) return keys; |
| 65 | // With nothing open, a collapsed Global folder never requests its first page |
| 66 | // and reads as an empty history, so it starts expanded. |
| 67 | const global = nodes.find((node) => node?.kind === "global_folder"); |
| 68 | return global ? [projectTreeNodeKey(global, 0)] : []; |
| 69 | } |
| 70 |