Skip to content

On-radar (focus queue): prioritize recently-finished agents above running ones #333

Description

@germanescobar

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

  • Given a mix of finished and running sessions on the radar, finished sessions appear above all running sessions.
  • Within the finished section, the most recently finished session is at the top; the least recently finished is at the bottom of that section.
  • Within the running section, the most recently started running session is at the very bottom of the queue; older-running sessions stack above it.
  • No regression for sessions where focusPinnedAt is unset (still hidden from radar) or where the user has manually unpinned (userUnpinned === true).
  • Behavior is unchanged on the server side; only the client-side sort changes.
  • Unit tests cover: all-finished, all-running, mixed, ties on lastActiveAt, and the active-state transition (a session that flips from active to inactive moves from bottom to top in the same render).

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions