Skip to content

feat: order the session list by attention, not by when you created it - #300

Merged
saucam merged 1 commit into
mainfrom
feat/session-list-attention-ordering
Aug 23, 2026
Merged

feat: order the session list by attention, not by when you created it#300
saucam merged 1 commit into
mainfrom
feat/session-list-attention-ordering

Conversation

@saucam

@saucam saucam commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator

Fixes the "session panel order seems mostly random" report.

Why it looked random

web/src/state/sessions.ts sorted by createdAt — under a comment reading "Sorted list — most recent activity first." The intent was always recency; the code delivered creation order. A session made last week but driven all morning sat at the bottom.

It could not be fixed client-side. The daemon has always tracked #lastActivityAt — it already orders the resumed session list by it — but never put it on the wire; SessionInfo carried only createdAt. There is a lastActivityAt in web/src/state/messages.ts, but it's derived from messages this client received, so it's 0 for every session you haven't opened. That's a trap for the obvious fix, so it's worth knowing it's there.

The design

Adds lastActivityAt to SessionInfo (optional — clients fall back to createdAt, so an older daemon doesn't sink every session to epoch 0) and bands the list:

NEEDS YOU   waiting_approval, error
WORKING     thinking, tool_running
IDLE        everything else, most recently active first

Bands rather than one flat multi-key sort, for three reasons:

  1. It's already this project's answer. conductor-frontends-design.md §4 locks a state-grouped list as the fleet view's primary, partly because it renders identically in Solid and ratatui. A second ordering vocabulary in the same client would be a bug in itself.
  2. It absorbs thrash. thinking and tool_running alternate several times a second; both are WORKING, so nothing moves. A flat sort keyed on status would jitter continuously.
  3. It makes movement legible. A row crossing under a NEEDS YOU header reads as a state change; the same row silently rising in a flat list reads as a glitch.

error bands as NEEDS YOU, not IDLE — a failed session is the other thing wanting a human, and burying it under idle sessions is how a failure goes unnoticed for an hour.

The two details that matter more than the sort

  • A fleet bands and dates as ONE unit — most urgent state, most recent activity anywhere in it. An orchestrator sits idle while its children work, so banding on the lead alone would file a fleet whose child is blocked on an approval under IDLE: exactly the case NEEDS YOU exists to surface.
  • Ordering is held while the pointer is over the list, applied on leave. A row that moves between aiming and clicking opens the wrong session. Membership is deliberately not held — a destroyed session must disappear, since leaving it clickable trades a misclick for a worse one.

Ordering stays client-side: the daemon owns state and exposes the field, each client decides how to render it. That's what lets the TUI and mobile band differently later.

Companion

highflame-ai/codeoid-ui — mirrors the field. The TUI doesn't order by it yet; that lands with P5.1 so web and TUI adopt one vocabulary rather than drifting.

Verification

  • 13 new tests over the pure ordering functions (web/src/lib/session-order.test.ts), covering band assignment, fleet roll-up, the createdAt fallback, an unparseable timestamp, determinism for same-millisecond sessions, and hold-under-pointer including the membership carve-out
  • daemon 2313 pass, 0 fail · web 422 pass · Rust 7 suites
  • lint / typecheck / cargo fmt / clippy clean; web build clean

Follow-ups I did not fold in

Pinning (a manual override for long-lived sessions) and a collapsed "N idle" roll-up at fleet scale are both natural next steps, but they're UI features rather than fixes to the reported problem.

The sidebar sorted by createdAt — under a comment that read "Sorted list — most
recent activity first". The intent was always recency; the code delivered
creation order. A session made last week but driven all morning sat at the
bottom, which is why the order looked arbitrary.

It could not be fixed client-side. The daemon has always tracked
`#lastActivityAt` (it orders the resumed session list by it) but never put it on
the wire, and `SessionInfo` carried only createdAt. web/src/state/messages.ts has
a `lastActivityAt`, but it is derived from messages THIS client received, so it
is 0 for every session you have not opened — a trap for the obvious fix.

Adds `lastActivityAt` to SessionInfo (optional; clients fall back to createdAt so
an older daemon does not sink every session to epoch 0) and bands the list:

  NEEDS YOU   waiting_approval, error
  WORKING     thinking, tool_running
  IDLE        everything else, most recently active first

Bands rather than one flat multi-key sort, for three reasons. It is already this
project's answer — conductor-frontends-design.md §4 locks a state-grouped list as
the fleet view's primary, and a second ordering vocabulary in the same client
would be a bug in itself. It absorbs thrash: thinking and tool_running alternate
several times a second and both are WORKING, so nothing moves, where a flat sort
keyed on status would jitter continuously. And it makes movement legible — a row
crossing under a NEEDS YOU header reads as a state change rather than a glitch.

Two details that matter more than the sort:

- A fleet bands and dates as ONE unit, by the most urgent state and the most
  recent activity anywhere in it. An orchestrator sits idle while its children
  work, so banding on the lead alone would file a fleet whose child is blocked
  on an approval under IDLE — exactly the case NEEDS YOU exists to surface.
- Ordering is held while the pointer is over the list and applied on leave. A
  row that moves between aiming and clicking opens the wrong session. Membership
  is deliberately NOT held: a destroyed session must disappear, since leaving it
  clickable trades a misclick for a worse one.

Ordering stays client-side. The daemon owns the state and exposes the field;
each client decides how to render it, which is what lets the TUI and mobile band
differently later.

Mirrored in the Rust crate. 13 new tests over the pure ordering functions.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@saucam
saucam merged commit c98755f into main Aug 23, 2026
4 checks passed
saucam added a commit that referenced this pull request Aug 29, 2026
#306)

Both timestamps were stamped unconditionally in the Session constructor, so
every restart re-dated every resumed session to the restart moment:

  this.createdAt = new Date().toISOString();
  this.#lastActivityAt = this.createdAt;

Two consequences. A weeks-old session reported as brand new. And because the
session list's recency key is `lastActivityAt ?? createdAt`, every session tied
on the same instant — so the attention ordering added in #300 collapsed back to
insertion order on the first restart, which is exactly what it was built to
replace.

The values were already persisted in TranscriptMeta (createdAt,
lastActivityAt) and already read: SessionManager's `resumeSortKey` sorts the
resume pass by `meta.lastActivityAt`. So the manager iterated in the right
order and each Session then overwrote the timestamps a moment later — the
correct data was on disk, read, and discarded.

SessionCreateOptions now carries both as optional fields, the constructor
prefers them over `now`, and the resume call site passes the meta values. A new
session is unaffected (no opts -> stamped fresh, activity falls back to
creation), and meta written before `lastActivityAt` existed degrades to
`createdAt` rather than to `now` — an ancient session must not sort as the most
recently active one.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants