Problem
The on-radar (focus queue) sidebar currently sorts pinned sessions by focusPinnedAt ?? createdAt ascending — i.e. oldest-pin-first (client/src/components/sidebar.tsx around lines 335–343).
This means an agent that just stopped on its own — the session that actually needs my attention right now — sits wherever it landed when it was auto-pinned, while long-idle sessions I forgot to unpin float to the top. The queue gives me no signal about what's fresh.
Desired behavior
For sessions where focusPinnedAt is set (i.e. currently on the radar):
- Inactive (
active === false) — the agent stopped on its own and is waiting for the user's reply — sit at the top, ordered by finishing order (most recently finished first, by lastActiveAt desc).
- Active (
active === true) — the agent is still working — sit at the bottom, ordered by lastActiveAt ascending so the most recently started running session ends up at the very bottom.
Concretely, from top to bottom:
1. most recently finished (lastActiveAt desc)
2. ...
3. oldest finished still on radar
4. oldest still running (lastActiveAt asc, oldest first)
5. ...
6. newest still running (lastActiveAt asc → newest at the bottom)
Signals involved
session.focusPinnedAt — already used as the radar inclusion filter (unchanged).
session.lastActiveAt — already updated by the server on reply and on agent stream close (server/routes/sessions.ts around lines 1530, 1859, 2106, 2157). Doubles as the "finished-at" timestamp without us needing a new field.
item.active — already computed in the sidebar from the runtime poller (activeSessionIds set in client/src/components/sidebar.tsx).
Out of scope / non-goals
focusDoneAt does not enter the sort. Sessions marked done are removed from the radar today (updateSessionFocus in server/lib/sessions.ts clears focusPinnedAt on the "done" action, and the sidebar filter is Boolean(session.focusPinnedAt)), so they never reach the queue. No special-casing needed.
- No time-decay window — "recent" is bounded by manual unpin/mark-done, not by a clock. A session that finished two days ago and hasn't been marked done stays at the top of the finished pile, which is the intended behavior.
- No user-facing setting for now. Hard-code the rule; revisit if it needs to be tunable.
- Within the running bucket, relative order between concurrent sessions is best-effort (
lastActiveAt asc). Two agents finishing in the same polling tick fall back to array order. A strict FIFO finish-order timestamp can be added later if needed.
- Within the finished bucket, ties on
lastActiveAt fall back to array order.
Implementation sketch
client/src/components/sidebar.tsx, in the focusQueue useMemo (currently around lines 320–344):
const focusQueue = useMemo<FocusQueueItem[]>(() => {
const items = projectData.flatMap((project) =>
project.worktrees.flatMap((worktree) =>
worktree.sessions
.filter((session) => Boolean(session.focusPinnedAt))
.map((session) => ({
projectId: project.id,
projectName: project.name,
worktreeId: worktree.id,
worktreeName: worktree.name,
session,
active: activeSessionIds.has(session.id),
})),
),
);
const finished = items
.filter((item) => !item.active)
.sort(
(a, b) =>
new Date(b.session.lastActiveAt).getTime() -
new Date(a.session.lastActiveAt).getTime(),
);
const running = items
.filter((item) => item.active)
.sort(
(a, b) =>
new Date(a.session.lastActiveAt).getTime() -
new Date(b.session.lastActiveAt).getTime(),
);
return [...finished, ...running];
}, [activeSessionIds, projectData]);
This replaces the single .sort(...) with a partition + two sorts. No server changes required.
Acceptance criteria
Related
Problem
The on-radar (focus queue) sidebar currently sorts pinned sessions by
focusPinnedAt ?? createdAtascending — i.e. oldest-pin-first (client/src/components/sidebar.tsxaround lines 335–343).This means an agent that just stopped on its own — the session that actually needs my attention right now — sits wherever it landed when it was auto-pinned, while long-idle sessions I forgot to unpin float to the top. The queue gives me no signal about what's fresh.
Desired behavior
For sessions where
focusPinnedAtis set (i.e. currently on the radar):active === false) — the agent stopped on its own and is waiting for the user's reply — sit at the top, ordered by finishing order (most recently finished first, bylastActiveAtdesc).active === true) — the agent is still working — sit at the bottom, ordered bylastActiveAtascending so the most recently started running session ends up at the very bottom.Concretely, from top to bottom:
Signals involved
session.focusPinnedAt— already used as the radar inclusion filter (unchanged).session.lastActiveAt— already updated by the server on reply and on agent stream close (server/routes/sessions.tsaround lines 1530, 1859, 2106, 2157). Doubles as the "finished-at" timestamp without us needing a new field.item.active— already computed in the sidebar from the runtime poller (activeSessionIdsset inclient/src/components/sidebar.tsx).Out of scope / non-goals
focusDoneAtdoes not enter the sort. Sessions marked done are removed from the radar today (updateSessionFocusinserver/lib/sessions.tsclearsfocusPinnedAton the"done"action, and the sidebar filter isBoolean(session.focusPinnedAt)), so they never reach the queue. No special-casing needed.lastActiveAtasc). Two agents finishing in the same polling tick fall back to array order. A strict FIFO finish-order timestamp can be added later if needed.lastActiveAtfall back to array order.Implementation sketch
client/src/components/sidebar.tsx, in thefocusQueueuseMemo(currently around lines 320–344):This replaces the single
.sort(...)with a partition + two sorts. No server changes required.Acceptance criteria
focusPinnedAtis unset (still hidden from radar) or where the user has manually unpinned (userUnpinned === true).lastActiveAt, and the active-state transition (a session that flips from active to inactive moves from bottom to top in the same render).Related