File size: 3,483 Bytes
0bb29ca
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
// Ordering the Overview by when something last happened.
//
// The Overview's own order is the sidebar's: the tree, hand-arranged, groups as
// capsules. That answers "where is this agent" and nothing else. The two
// questions it cannot answer are "where did I last type something" and "who
// answered most recently", and both are a timestamp the digest already carries
// (`lastPromptTs` / `lastAssistantTs`, ms epoch, 0 when unknown).
//
// Three rules, all of them here rather than in the component, because they are
// the part that can be wrong:
//
//  1. **A running agent is not ranked.** Its last message is whatever it said
//     before it started working, so ranking it by that buries the agent that is
//     doing something right now under agents that are done. They come out in
//     their own section, and the caller pins it above the sorted list.
//  2. **A missing timestamp sinks.** `0` means "we do not know" — a session
//     that never started, has no transcript, or runs a harness that writes
//     none. Sorting descending on a number would float those to the bottom
//     anyway, but only by accident of arithmetic; they get their own section so
//     the caller can say what they are instead of implying they are stale.
//  3. **Ties keep the manual order.** The caller passes items in tree order and
//     the sort is stable, so equal timestamps — and the whole undated section —
//     read as the sidebar reads.

import type { OverviewSort } from '../types';

/** What ordering needs to know about a session. Deliberately not `MetaSession`:
 *  the three facts, so this is testable without a digest or a React tree. */
export interface Rankable {
  id: string;
  lastPromptTs: number;     // ms epoch, 0 = unknown
  lastAssistantTs: number;  // ms epoch, 0 = unknown
  running: boolean;
}

/** The timestamp a given sort reads. 0 for the manual order, which reads none. */
export function sortTs(s: Rankable, sort: OverviewSort): number {
  if (sort === 'prompt') return s.lastPromptTs || 0;
  if (sort === 'answer') return s.lastAssistantTs || 0;
  return 0;
}

export interface Sections<T> {
  /** At work now — pinned above the sorted list, in the caller's order. */
  running: T[];
  /** The answer to the question, newest first. */
  dated: T[];
  /** No such message ever: never started, no transcript, unsupported harness. */
  undated: T[];
}

/**
 * Split into the three sections above and sort the middle one, newest first.
 *
 * `manual` is the identity: everything lands in `dated` untouched, so a caller
 * that renders the sections in order still gets the tree's own arrangement.
 */
export function rankSessions<T extends Rankable>(items: T[], sort: OverviewSort): Sections<T> {
  if (sort === 'manual') return { running: [], dated: items.slice(), undated: [] };
  const running: T[] = [];
  const dated: T[] = [];
  const undated: T[] = [];
  for (const it of items) {
    if (it.running) running.push(it);
    else if (sortTs(it, sort) > 0) dated.push(it);
    else undated.push(it);
  }
  // Stable (ES2019+): equal timestamps stay in the order they arrived, which is
  // the tree's.
  dated.sort((a, b) => sortTs(b, sort) - sortTs(a, sort));
  return { running, dated, undated };
}

/** What the sorted block is sorted BY, spelled out above it in the feed. */
export function sortLabel(sort: OverviewSort): string {
  return sort === 'prompt' ? 'by your last message' : sort === 'answer' ? 'by the last reply' : '';
}