import { type SessionSiteOrigin } from "./session-site-origin.js"; import type { SessionListTotals, SiteConversationGroup } from "@opengeni/sdk"; import type { RailSession as Session } from "./session-list-entry.js"; export { compareSessionActivity, compareSessionPins, groupSessionsForRail, isRunningStatus, partitionPinnedSessions, recencyGroupFor, relativeTimeLabel, SESSION_GROUP_LABELS, SESSION_GROUP_ORDER, sessionActivityTime, type GroupedSessions, type SessionRecencyBucket, type SessionRecencyGroup, } from "../../session-list-model.js"; export type SessionTreeNode = { /** Presentation-only group; never persisted or used for session actions. */ siteGroup?: SessionSiteOrigin | undefined; /** * A Site row's complete server counts, independent of which conversations * are loaded. Absent when the row was built from loaded conversations only. */ siteSummary?: SiteRowSummary | undefined; session: Session; children: SessionTreeNode[]; /** A descendant (any depth, not the node itself) is running/queued/awaiting action. */ hasActiveDescendant: boolean; }; export type RailAggregateStatusKind = "send_failed" | "needs_attention" | "failed" | "active" | "queued" | "waiting" | "unread" | "active_work" | "neutral"; export type RailAggregateStatus = { kind: RailAggregateStatusKind; /** Number of sessions represented by the winning status. */ count: number; /** Total sessions represented by the node or section. */ total: number; label: string; /** * `needs_attention` only: when the longest-waiting represented session * entered `requires_action` (the earliest known `requiresActionSince` / * `treeStats.attentionSince`). Absent when no server reported it. */ attentionSince?: string | undefined; }; export type RailStatusCounts = { total: number; sendFailed: number; attention: number; /** Earliest known requires_action entry across the counted `attention` sessions. */ attentionSince: string | null; failed: number; active: number; queued: number; /** Idle sessions parked on their own wake-up (input or timer), not executing. */ waiting: number; unread: number; activeWork: number; }; /** Complete ordinary project counters; absent groups are authoritative zeroes. */ export declare function sessionProjectTotals(totals: SessionListTotals, channelIds: readonly string[]): Map; /** One status for a collapsed parent or workstream, including all descendants. */ export declare function summarizeRailNodes(nodes: readonly SessionTreeNode[], localDeliveryAttention?: ReadonlyMap, now?: Date): RailAggregateStatus; /** Shared presentation for complete server metadata and loaded node summaries. */ export declare function summarizeRailStatusCounts(counts: RailStatusCounts, now?: Date): RailAggregateStatus; /** * Merge two projections of the same session for the rail. Detail/SSE data is * fresher for lifecycle fields, but detail reads intentionally omit treeStats. * An omitted list-only field must not make the selected row forget its loaded * hierarchy summary (and therefore lose its disclosure control). */ export declare function mergeSessionForRail(current: Session, incoming: T): T; export type SessionForest = { running: SessionTreeNode[]; grouped: { group: string; label: string; sessions: SessionTreeNode[]; }[]; }; export type SessionBrowseGroupBy = "activity" | "project" | "none" | "created" | "creator"; export type SessionBrowseSortBy = "updatedAt" | "createdAt" | "name"; /** The same ordering as the server page, reapplied after live projection merges. */ export declare function compareSessionBrowse(left: Session, right: Session, sortBy: SessionBrowseSortBy): number; export declare function sortSessionForest(forest: SessionForest, sortBy: SessionBrowseSortBy): SessionForest; export type SessionBrowseDateField = "activity" | "created"; export type SessionBrowseDateRange = "any" | "today" | "week" | "month"; export declare function sessionCreatorKey(session: Session): string; export declare function sessionCreatorLabel(session: Session): string; export type SessionCreatorOption = { value: string; label: string; }; /** * Loaded creator choices with identity-specific labels only when frozen display * labels collide. The subject ID remains opaque; it is displayed, not parsed. */ export declare function sessionCreatorLabelMap(sessions: Session[]): ReadonlyMap; export declare function sessionCreatorOptions(sessions: Session[]): SessionCreatorOption[]; /** Drop a flat-search creator selection that is not valid for hierarchical browse roots. */ export declare function normalizeSessionBrowseCreator(creator: string | null, creatorLabels: ReadonlyMap, hierarchical: boolean): string | null; /** Count matching rows in flat search, or matching workstream roots in hierarchical browse. */ export declare function sessionBrowseResultCount(sessions: readonly Session[], hierarchical: boolean): number; export declare function filterSessionsForBrowse(sessions: T[], options: { creator: string | null; dateField: SessionBrowseDateField; dateRange: SessionBrowseDateRange; /** Filter whole loaded workstreams by their root instead of orphaning descendants. */ hierarchical?: boolean | undefined; now?: Date | undefined; }): T[]; /** Regroup already-built workstream roots without flattening their descendants. */ export declare function groupSessionForestForBrowse(forest: SessionForest, groupBy: Exclude, options?: { now?: Date | undefined; creatorLabels?: ReadonlyMap | undefined; }): SessionForest; /** Build and regroup a loaded session set while preserving its lineage tree. */ export declare function groupSessionsForBrowse(sessions: Session[], groupBy: Exclude, options?: { now?: Date | undefined; creatorLabels?: ReadonlyMap | undefined; }): SessionForest; export type PinnedRailSections = { /** Complete loaded hierarchy, used for expansion and lineage lookups. */ complete: SessionForest; /** Every explicit pin is an independently ordered shortcut root. */ pinned: SessionTreeNode[]; /** The ordinary hierarchy with every pin-owned subtree removed. */ ordinary: SessionForest; }; /** Whether the node's own status, or any descendant, is in a live state. */ export declare function nodeIsActive(node: SessionTreeNode): boolean; /** Bucket already-built roots using the rail's activity and recency rules. */ /** * The scheduled task a session was created for, or null. The worker stamps this * onto every session it generates for a run; it is deliberately read from * metadata rather than a column, because that is where the scheduler writes it. * A session a human started never carries the key, so it never groups. */ export declare function scheduledTaskIdOf(session: Session): string | null; /** * Collapse repeat runs of one scheduled task into a single rail entry. * * A schedule that fires hourly would otherwise push every other session out of * the rail, so runs of the same task fold together. This deliberately does NOT * introduce a scheduled-task row type: the newest run IS the entry, and the * older runs become its children, so the group renders and behaves exactly like * an ordinary session with spawned children. Clicking it opens the latest run, * expanding it reveals the rest, and each one stays individually openable. * * A task with a single run is left completely alone - one session is not a * flood, and wrapping it would only add a disclosure with nothing behind it. * * `roots` arrives running-first-then-recency, not purely newest-first, so the * members are re-sorted by activity here: the umbrella must be the latest run, * and the runs behind it must read newest to oldest. The group keeps the list * position of whichever member ranked highest in the incoming order, so folding * never pushes a live run down the rail. */ export declare function groupScheduledRuns(roots: SessionTreeNode[]): SessionTreeNode[]; export type SiteRowSummary = Omit; /** * A Site row's stable id. Rows are per project when the rail shows projects, * so a Site used from two projects appears in each; otherwise one per Site. */ export declare function siteRowKey(siteId: string, channelId?: string | null): string; /** The Site and project a Site row stands for, or null for an ordinary id. */ export declare function parseSiteRowKey(key: string): { siteId: string; channelId?: string | null | undefined; } | null; /** * Fold Site conversations into stable Site rows. `groups` are the complete * rows from the server; loaded conversations (an expanded Site, the open * session, a legacy labelled chat) join their row, so which session is * selected never decides whether a Site or its conversations are listed. * Pins have already been split out. */ export declare function attachSiteRows(roots: SessionTreeNode[], options?: { groups?: readonly SiteConversationGroup[] | undefined; /** Rows per project when the rail shows projects. */ byProject?: boolean | undefined; workspaceId?: string | undefined; creator?: Session["createdBy"] | undefined; }): SessionTreeNode[]; /** * How many conversations a Site row stands for, loaded or not. The server * counts Site conversations; an older chat that only carries a Site label * joins the row when loaded, so the count can grow by those on expansion. */ export declare function siteRowConversationCount(node: SessionTreeNode): number; export declare function categorizeRailRoots(rootNodes: SessionTreeNode[], now?: Date): SessionForest; /** * Build the rail forest: roots (parentSessionId null, or parent absent from the * page) at the top level, spawned children nested beneath their parent, each * subtree ordered most-recent-first. Running roots (self or via a live * descendant) are pinned above the recency buckets. A `seen` guard makes a * pathological parent cycle in the loaded page terminate rather than recurse * forever. */ export declare function buildRailForest(sessions: Session[], now?: Date): SessionForest; /** * Remove explicit pinned roots below `node`. `keepRoot` is used to build one * pin shortcut: that pin stays, but any nested explicit pin is promoted to its * own globally ordered shortcut instead of appearing twice. */ export declare function prunePinnedSubtrees(node: SessionTreeNode, keepRoot?: boolean): SessionTreeNode | null; /** * Rows as the rail will actually render them. * * Hierarchy mode nests spawned sessions under their parent, so lineage stays. * Search results are deliberately flat because a partial match set is not a * tree, so every result there IS top-level and must say so. A row that renders * at the top level while still naming an absent parent is the bug this prevents. */ export declare function projectRailSessions(sessions: T[], hierarchyMode: boolean): T[]; /** Build the complete, explicit-pin, and ordinary rail projections together. */ export declare function buildPinnedRailSections(sessions: Session[], now?: Date, options?: { groupSites?: boolean | undefined; /** Complete Site rows from a grouped root page. */ siteGroups?: readonly SiteConversationGroup[] | undefined; /** Split Site rows by project (the rail's project view). */ siteRowsByProject?: boolean | undefined; workspaceId?: string | undefined; creator?: Session["createdBy"] | undefined; }): PinnedRailSections; /** The selected descendant under a collapsed node, projected one level deeper. */ export declare function selectedDescendantNode(node: SessionTreeNode, selectedSessionId: string | null): SessionTreeNode | null; /** Flatten the forest to the rows currently VISIBLE, given the expanded set — * a node, then its children only when the node is expanded (depth-first). * A collapsed branch keeps its selected descendant visible as one ordinary * child row, matching the rail projection. Keyboard navigation walks this. */ export declare function visibleTreeRows(roots: SessionTreeNode[], expanded: ReadonlySet, selectedSessionId?: string | null): { node: SessionTreeNode; depth: number; }[]; export declare function visibleForestRows(forest: SessionForest, expanded: ReadonlySet, selectedSessionId?: string | null): { node: SessionTreeNode; depth: number; }[]; export type ChannelRailSection = { /** Stable render key: the channel id, or "recents" for unfiled roots. */ key: string; channelId: string | null; /** User-facing workstream name. */ name: string; sessions: SessionTreeNode[]; }; /** * Group an ordinary rail forest's roots by channel. Channels render in the * given (server-defined) order — pinned projects first, then ordinary ones — * including empty ones, so a just-created project is immediately visible. * The catch-all "Default" section for unfiled roots follows them (only when * it has members). Within a section, active roots * keep floating above recency-ordered idle ones because the incoming forest * is flattened in that order. A root whose channel no longer exists folds * into Default rather than disappearing. */ export declare function channelRailSections(forest: SessionForest, channels: readonly { id: string; name: string; }[]): ChannelRailSection[];