lineage.ts 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081
  1. // flattenLineage: summaries -> flat list with lineage indentation (pure function).
  2. // The input order is authoritative; lineage only makes each child adjacent to its parent.
  3. // Orphaned lineage degrades to root level; cycles fail soft and emit as roots.
  4. import type { SessionId } from '@deepseek-ai/dsh-session/types'
  5. import type { SessionProjectionMap } from '@deepseek-ai/dsh-session-projection/types'
  6. import type { SessionSummary } from '../../types.ts'
  7. /** Host list summary enriched with the latest Session Controller title projection. */
  8. export interface TitledSessionSummary extends SessionSummary {
  9. title?: string
  10. /** Current host-computed projection values for list consumers. */
  11. projectionValues?: Readonly<Partial<SessionProjectionMap>>
  12. }
  13. /** One flattened session-list row with lineage depth. */
  14. export interface SessionListEntry {
  15. sessionId: SessionId
  16. title?: string
  17. updatedAt: number
  18. running: boolean
  19. /** Empty-log bit mirrored from the summary; lists hide blank sessions (filtering stays with the consumer). */
  20. blank: boolean
  21. parentSessionId?: SessionId
  22. /** Coarse durable origin for navigation filtering; not a continuation capability. */
  23. origin?: 'subagent'
  24. cwd?: string
  25. /** Current host-computed projection values for list consumers. */
  26. projectionValues?: Readonly<Partial<SessionProjectionMap>>
  27. /** Lineage indent depth: root = 0; the UI just multiplies by the indent width. */
  28. depth: number
  29. }
  30. /**
  31. * Summaries -> flat list with lineage indentation. Root and sibling order
  32. * follows the established input order; this projection never re-sorts a
  33. * hydrated list from mutable timestamps.
  34. * @param summaries - the host's session.list items.
  35. * @returns display rows in render order.
  36. */
  37. export function flattenLineage(
  38. summaries: readonly TitledSessionSummary[],
  39. ): SessionListEntry[] {
  40. const byId = new Map<SessionId, TitledSessionSummary>()
  41. for (const s of summaries) byId.set(s.sessionId, s)
  42. const children = new Map<SessionId, TitledSessionSummary[]>()
  43. const roots: TitledSessionSummary[] = []
  44. for (const s of summaries) {
  45. if (s.parentSessionId !== undefined && byId.has(s.parentSessionId)) {
  46. const list = children.get(s.parentSessionId) ?? []
  47. list.push(s)
  48. children.set(s.parentSessionId, list)
  49. } else {
  50. roots.push(s) // root, or an orphan whose parent is absent from summaries (degrade to root, never drop)
  51. }
  52. }
  53. const out: SessionListEntry[] = []
  54. const visited = new Set<SessionId>()
  55. const walk = (s: TitledSessionSummary, depth: number): void => {
  56. if (visited.has(s.sessionId)) {
  57. console.warn(`[session-controller] lineage cycle at ${s.sessionId}; emitting as root`)
  58. return
  59. }
  60. visited.add(s.sessionId)
  61. out.push({
  62. ...s,
  63. depth,
  64. })
  65. const kids = children.get(s.sessionId)
  66. if (kids === undefined) return
  67. for (const kid of kids) walk(kid, depth + 1)
  68. }
  69. for (const root of roots) walk(root, 0)
  70. // Cycle members (unreachable from any root): emit as roots so no entry is lost.
  71. for (const s of summaries) {
  72. if (!visited.has(s.sessionId)) walk(s, 0)
  73. }
  74. return out
  75. }