Spaces:
Running
Running
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' : '';
}
|