hermes-agent/apps/desktop/src/lib/session-branch-tree.ts
Brooklyn Nicholson 62fe9fd101 style(desktop,tui): fix all lint/type/formatting issues
Bring apps/desktop and ui-tui to a clean state for typecheck, eslint,
and prettier:

- Run prettier across both trees (printWidth/wrap drift; prettier is not
  CI-enforced for these JS projects, so main had accumulated drift).
- Apply eslint --fix for padding-line-between-statements and perfectionist
  import/export sorting.
- Manual fixes for non-auto-fixable rules:
  - remove unused node:net import in electron/main.cjs (uses Electron net)
  - replace inline `typeof import(...)` annotations with top-level
    `import type * as EnvModule` in two ui-tui test files
  - scoped eslint-disable no-control-regex on intentional sentinel/ANSI
    regexes (mathUnicode.ts, text.ts)
  - resolve react-hooks/exhaustive-deps per-case: correct swapped/missing
    deps, collapse redundant session.* members, and justified disables on
    settings mount-only data-load effects to preserve run-once behavior

No behavior changes; test pass/fail counts are unchanged from the main
baseline.
2026-06-26 01:04:33 -05:00

108 lines
3.1 KiB
TypeScript

import type { SessionInfo } from '@/types/hermes'
export interface SidebarSessionEntry {
branchStem?: string
session: SessionInfo
}
const recency = (session: SessionInfo): number => session.last_active || session.started_at || 0
/** Flat list with branch/fork sessions nested visually under their parent. */
export function flattenSessionsWithBranches(sessions: readonly SessionInfo[]): SidebarSessionEntry[] {
if (sessions.length < 2) {
return sessions.map(session => ({ session }))
}
const byVisibleId = new Map<string, SessionInfo>()
for (const session of sessions) {
byVisibleId.set(session.id, session)
const rootId = session._lineage_root_id?.trim()
if (rootId) {
byVisibleId.set(rootId, session)
}
}
const childrenByParent = new Map<string, SessionInfo[]>()
const nestedIds = new Set<string>()
for (const session of sessions) {
const parentId = session.parent_session_id?.trim()
if (!parentId) {
continue
}
const parent = byVisibleId.get(parentId)
if (!parent || parent.id === session.id) {
continue
}
nestedIds.add(session.id)
const siblings = childrenByParent.get(parent.id) ?? []
siblings.push(session)
childrenByParent.set(parent.id, siblings)
}
for (const siblings of childrenByParent.values()) {
siblings.sort((left, right) => recency(right) - recency(left))
}
// A group sorts by its freshest member, so activity on any branch lifts the
// whole parent→branches cluster together instead of stranding the parent at
// its own stale timestamp. Memoized — each subtree is folded at most once.
const groupRecencyMemo = new Map<string, number>()
const groupRecency = (session: SessionInfo): number => {
const cached = groupRecencyMemo.get(session.id)
if (cached !== undefined) {
return cached
}
groupRecencyMemo.set(session.id, recency(session)) // cycle guard
const max = (childrenByParent.get(session.id) ?? []).reduce(
(acc, child) => Math.max(acc, groupRecency(child)),
recency(session)
)
groupRecencyMemo.set(session.id, max)
return max
}
// Depth-first so a branch-of-a-branch still renders under its own parent. The
// `seen` set guards against pathological parent cycles, and the trailing sweep
// emits anything the walk somehow missed — nothing in the input is ever dropped.
const out: SidebarSessionEntry[] = []
const seen = new Set<string>()
const emit = (session: SessionInfo, branchStem?: string) => {
if (seen.has(session.id)) {
return
}
seen.add(session.id)
out.push(branchStem ? { branchStem, session } : { session })
const children = childrenByParent.get(session.id)
children?.forEach((child, index) => emit(child, index === children.length - 1 ? '└─ ' : '├─ '))
}
sessions
.filter(session => !nestedIds.has(session.id))
.map((session, index) => ({ index, session }))
.sort((a, b) => groupRecency(b.session) - groupRecency(a.session) || a.index - b.index)
.forEach(({ session }) => emit(session))
for (const session of sessions) {
if (!seen.has(session.id)) {
out.push({ session })
}
}
return out
}