lineage.ts 3.5 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
  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. /** Agent preset the session's agent was composed from (summary passthrough). */
  26. agentPreset?: string
  27. /** Current host-computed projection values for list consumers. */
  28. projectionValues?: Readonly<Partial<SessionProjectionMap>>
  29. /** Finished running while not selected and not yet opened — the sidebar's green "done" reminder (clears on select or the next run). */
  30. completed: boolean
  31. /** Lineage indent depth: root = 0; the UI just multiplies by the indent width. */
  32. depth: number
  33. }
  34. /**
  35. * Summaries -> flat list with lineage indentation. Root and sibling order
  36. * follows the established input order; this projection never re-sorts a
  37. * hydrated list from mutable timestamps.
  38. * @param summaries - the host's session.list items.
  39. * @param completed - sessions with a pending completion reminder (manager-owned live fact; absent = false).
  40. * @returns display rows in render order.
  41. */
  42. export function flattenLineage(
  43. summaries: readonly TitledSessionSummary[],
  44. completed?: ReadonlySet<SessionId>,
  45. ): SessionListEntry[] {
  46. const byId = new Map<SessionId, TitledSessionSummary>()
  47. for (const s of summaries) byId.set(s.sessionId, s)
  48. const children = new Map<SessionId, TitledSessionSummary[]>()
  49. const roots: TitledSessionSummary[] = []
  50. for (const s of summaries) {
  51. if (s.parentSessionId !== undefined && byId.has(s.parentSessionId)) {
  52. const list = children.get(s.parentSessionId) ?? []
  53. list.push(s)
  54. children.set(s.parentSessionId, list)
  55. } else {
  56. roots.push(s) // root, or an orphan whose parent is absent from summaries (degrade to root, never drop)
  57. }
  58. }
  59. const out: SessionListEntry[] = []
  60. const visited = new Set<SessionId>()
  61. const walk = (s: TitledSessionSummary, depth: number): void => {
  62. if (visited.has(s.sessionId)) {
  63. console.warn(`[session-controller] lineage cycle at ${s.sessionId}; emitting as root`)
  64. return
  65. }
  66. visited.add(s.sessionId)
  67. out.push({
  68. ...s,
  69. completed: completed?.has(s.sessionId) ?? false,
  70. depth,
  71. })
  72. const kids = children.get(s.sessionId)
  73. if (kids === undefined) return
  74. for (const kid of kids) walk(kid, depth + 1)
  75. }
  76. for (const root of roots) walk(root, 0)
  77. // Cycle members (unreachable from any root): emit as roots so no entry is lost.
  78. for (const s of summaries) {
  79. if (!visited.has(s.sessionId)) walk(s, 0)
  80. }
  81. return out
  82. }