rootAncestor / countSubagentDescendants / treeSessionIds 三份「沿 subagent-origin 链上行 + seen 集合防环 fail-soft」实现收敛到新模块 src/client/subagent-lineage.ts:核心走链是单个私有生成器 subagentOriginChain()(seen 防环、只在节点为 subagent 且有 parent 时 前行),三个公开函数迁入并基于它重写;isSideThreadSummary 一并迁入 (countSubagentDescendants 依赖它,迁入可避免 detect↔lineage 循环 依赖),subagent-detect.ts / subagent-jobs.ts re-export 全部迁出 符号,既有 import 调用点零变化。 语义逐字保留的等价性论证: - rootAncestor:链为空返回 start.id(原循环条件不满足即返回 current?.id);链非空返回 byId[末节点.parentId]?.id ?? sessionId (原循环退出时 current 恰为该行,断链/行缺失回退 sessionId 不变)。 - countSubagentDescendants:生成器在 seen.add 后 yield、消费方判 parentId === sessionId 即 break——与原循环体内判断顺序一致; descendant 的 running 判定、side-thread 过滤位置不变。 - treeSessionIds:根行缺失(byId[rootId] 无行)时任何上行都不可能 「访问到」根(原实现只匹配真实访问过的行),故提前返回空集; 根行自身(非 subagent)经 summary.id === rootId 特判入集;其余 行经链节点 parentId === rootId 判定,等价于原「上行走到 id === rootId 的行」——两种表述指向同一行(链上节点的 next 行)。 directSubagentCount 不迁移:它是对直接子行的单层过滤,没有上行 walk,无共享逻辑(背景侦查将其归入 walk 家族与实际代码不符)。 collectBranchIds 遍历的是 catalog 结构而非 session 链,保持不动。
79 lines
2.8 KiB
TypeScript
79 lines
2.8 KiB
TypeScript
/**
|
|
* Pure subagent-membership helpers over the sessions list feed (structural
|
|
* mirror world — no runtime imports). Used by the sidebar's auto-activation
|
|
* effect and the Subagent page:
|
|
*
|
|
* - {@link directSubagentCount}: direct durable children of one session,
|
|
* - {@link detectNewDirectSubagent}: the 0 → N transition that means "a new
|
|
* subagent just spawned under the current session" (the auto-open trigger),
|
|
* - {@link countSubagentDescendants}: uninterrupted subagent-origin lineage
|
|
* totals (mirror of the official `indexSubagentDescendants` over the
|
|
* plugin's own summary rows).
|
|
*
|
|
* The lineage walks themselves ({@link isSideThreadSummary}, {@link
|
|
* rootAncestor}, {@link countSubagentDescendants}) live in
|
|
* ./subagent-lineage.ts — the single shared walk implementation — and are
|
|
* re-exported below for their established import sites.
|
|
*/
|
|
import type {
|
|
SidebarSessionList,
|
|
SidebarSubagentCatalog,
|
|
} from '../context-types.ts'
|
|
import { countSubagentDescendants, isSideThreadSummary, rootAncestor } from './subagent-lineage.ts'
|
|
|
|
export { countSubagentDescendants, isSideThreadSummary, rootAncestor }
|
|
export type { SubagentDescendantTotals } from './subagent-lineage.ts'
|
|
|
|
/** Count the direct subagent children of one session (durable `origin` rows). */
|
|
export function directSubagentCount(
|
|
byId: SidebarSessionList['byId'],
|
|
sessionId: string,
|
|
): number {
|
|
let count = 0
|
|
for (const summary of Object.values(byId)) {
|
|
if (summary.origin === 'subagent' && summary.parentId === sessionId
|
|
&& !isSideThreadSummary(summary)) count += 1
|
|
}
|
|
return count
|
|
}
|
|
|
|
/**
|
|
* Collect every catalog branch (an entry with `hasChildren`) reachable from
|
|
* the root — the set of catalogs the always-expanded topology consumes.
|
|
* Cycles fail soft.
|
|
*/
|
|
export function collectBranchIds(
|
|
catalogs: Readonly<Record<string, SidebarSubagentCatalog>>,
|
|
rootId: string | undefined,
|
|
): string[] {
|
|
const out: string[] = []
|
|
const seen = new Set<string>()
|
|
const visit = (parentId: string): void => {
|
|
if (seen.has(parentId)) return
|
|
seen.add(parentId)
|
|
for (const entry of catalogs[parentId]?.entries ?? []) {
|
|
if (entry.kind === 'child' && entry.hasChildren) {
|
|
out.push(entry.id)
|
|
visit(entry.id)
|
|
}
|
|
}
|
|
}
|
|
if (rootId !== undefined) visit(rootId)
|
|
return out
|
|
}
|
|
|
|
/**
|
|
* Whether a new direct subagent appeared under `sessionId` between two
|
|
* consecutive list snapshots (the count crossed 0 → >0). Switching to a
|
|
* session that already has subagents yields `false` (its baseline starts at
|
|
* the current count), so the auto-open never fights an existing layout.
|
|
*/
|
|
export function detectNewDirectSubagent(
|
|
prev: SidebarSessionList,
|
|
next: SidebarSessionList,
|
|
sessionId: string,
|
|
): boolean {
|
|
return directSubagentCount(prev.byId, sessionId) === 0
|
|
&& directSubagentCount(next.byId, sessionId) > 0
|
|
}
|