Skip to content

Focus queue: recently-finished-first sort, walk the finished pile, await-input priority, toggleable auto-advance, visited/unvisited split, drop Controller Mode (#333) - #338

Open
germanescobar wants to merge 5 commits into
mainfrom
issue-333
Open

Focus queue: recently-finished-first sort, walk the finished pile, await-input priority, toggleable auto-advance, visited/unvisited split, drop Controller Mode (#333)#338
germanescobar wants to merge 5 commits into
mainfrom
issue-333

Conversation

@germanescobar

@germanescobar germanescobar commented Aug 20, 2026

Copy link
Copy Markdown
Owner

Closes #333.

What changed

Radar ordering and attention

  • Sessions awaiting structured input or a tool approval appear first.
  • Finished, unvisited sessions follow in FIFO order.
  • Finished sessions already handled by the user sink below the unvisited pile.
  • Active sessions remain last, oldest-running first.
  • Runtime attention state is tracked consistently for Codex, Claude, and Anita, including paused or inactive sessions.

Navigation semantics

Next and auto-advance now perform two explicit operations:

  1. Mark the current radar session as handled and re-sort the queue.
  2. Open the new first session in the rendered queue.

Simply clicking through conversations no longer changes their queue position. This fixes cases where Next selected the last or second-to-last row instead of the first actionable row.

Conversation controls

  • Replaced the standalone countdown toast with one responsive control surface.
  • Desktop uses a floating panel at the top-right of the conversation.
  • Mobile uses a compact row.
  • Radar sessions expose Done and Next.
  • Other sessions expose Add to radar and Next.
  • The auto-advance switch and countdown live in the same panel.
  • Desktop shows configurable shortcuts; mobile omits shortcut chips.
  • Stay uses a pause icon and the same action styling as Done and Next.
  • Removed the radar icon from the conversation header and removed auto-advance from the radar sidebar.

Mobile composer

  • The idle mobile composer collapses to one line.
  • It retains the contextual placeholder and attach, stop, and send or queue actions.
  • Focusing the textarea expands the existing full composer with provider, model, mode, skill, mention, and attachment controls.
  • Desktop composer behavior remains unchanged.
  • Fixed a new-session crash caused by reading countdown fields from a null countdown.

Acceptance criteria

  • Awaiting-input sessions are always surfaced first.
  • Finished unvisited sessions are processed FIFO before visited or active sessions.
  • Next and auto-advance open the first session after reordering the current session.
  • Normal conversation clicks do not reorder the radar.
  • Auto-advance can be toggled from the conversation controls or its shortcut.
  • Countdown navigation and Stay are rendered inside the same controls.
  • Desktop and mobile expose the appropriate responsive control variants.
  • Mobile composer stays one line while idle without losing essential actions.
  • New-session views render without a countdown crash.

Validation

  • npm test — 923 tests passed.
  • npm run build — passed.
  • Focus navigation, responsive controls, mobile composer, countdown null-safety, runtime attention, and route behavior all have regression coverage.

@germanescobar germanescobar changed the title Focus queue: prioritize recently-finished agents above running ones (#333) Focus queue: recently-finished-first sort, walk the finished pile, drop Controller Mode (#333) Sep 1, 2026
@germanescobar
germanescobar force-pushed the issue-333 branch 3 times, most recently from 03a9cc7 to 8d3bcbd Compare September 1, 2026 22:39
@germanescobar germanescobar changed the title Focus queue: recently-finished-first sort, walk the finished pile, drop Controller Mode (#333) Focus queue: recently-finished-first sort, walk the finished pile, await-input priority, drop Controller Mode (#333) Sep 1, 2026
@germanescobar germanescobar changed the title Focus queue: recently-finished-first sort, walk the finished pile, await-input priority, drop Controller Mode (#333) Focus queue: recently-finished-first sort, walk the finished pile, await-input priority, toggleable auto-advance, drop Controller Mode (#333) Sep 2, 2026
@germanescobar germanescobar changed the title Focus queue: recently-finished-first sort, walk the finished pile, await-input priority, toggleable auto-advance, drop Controller Mode (#333) Focus queue: recently-finished-first sort, walk the finished pile, await-input priority, toggleable auto-advance, visited/unvisited split, drop Controller Mode (#333) Sep 2, 2026
@germanescobar
germanescobar force-pushed the issue-333 branch 7 times, most recently from 6a3b291 to c00a3b7 Compare September 4, 2026 06:02
germanescobar and others added 2 commits September 5, 2026 23:44
…ait-input priority, toggleable auto-advance, visited/unvisited split, drop Controller Mode (#333)

The on-radar (focus queue) sidebar used to sort pinned sessions by
`focusPinnedAt ?? createdAt` ascending (oldest-pin-first). That buried
the agent that just stopped on its own — the one that actually needs
attention — under long-idle sessions the user forgot to unpin.

This change brings the radar in line with how it actually gets used:
"triage awaiting-input and recently-finished agents, with optional
auto-advance, before checking on running ones." Six parts:

1. **Sort: five buckets, awaiting-input on top, visited/unvisited split in the finished block.**

   The single ascending sort is replaced with a partition + per-bucket
   sort. From top to bottom:

   - **Awaiting input** — items whose agent has paused on a
     `user.input_requested` prompt or has at least one pending tool
     approval. The user owes a reply to these, so they sit at the
     very top regardless of `active`. (Claude's structured-input
     pause kills the child so an awaiting session can be `active:
     false`.)
   - **Finished, unvisited** — the triage pile. Items whose agent
     finished and the user has not yet landed on via any navigation
     (Next, auto-advance, mark-done follow-up, sidebar click,
     conversation link). Oldest-arrival first (`lastActiveAt` asc)
     so the user walks the pile in the order the agents finished.
     Visiting a session sinks it into the next bucket so the user
     isn't bounced back to it on every cycle.
   - **Finished, visited** — items the user has already looked at.
     Most-recently-visited at the very bottom of this sub-bucket
     (`lastVisitedAt` asc) so the freshest look sits closest to the
     running pile below.
   - **Running (active)** — sessions where the agent is still
     working. Oldest-running first, so the most recently started
     running session lands at the very bottom of the queue.

   The radar-inclusion filter (`Boolean(session.focusPinnedAt)`) is
   unchanged, so manually unpinned sessions still don't appear.
   Visit timestamps are tracked in-memory only (lost on reload) —
   a reload resumes the queue with everything in "unvisited"
   again, which is fine: the user re-triages from the top.

2. **Awaiting-input detection on the server.**

   The runtime map now carries `awaitingUserInput` (Claude's
   `user.input_requested`) alongside the existing `pendingApprovals`
   map. The bulk `/api/runtimes` snapshot reports
   `awaitingInput: boolean` derived from
   `pendingApprovals.size > 0 || awaitingUserInput`. The flag is
   flipped by the stream handler when it processes a
   `user.input_requested` or `tool.approval_requested` event and
   cleared when the run resumes with a non-approval event (or when
   a new stream starts via `markSessionActive`). The flag survives
   `markSessionInactive` so a paused session keeps its
   awaiting-input state across navigation.

3. **Toggleable auto-advance (default on).**

   Every reply triggers a 4-second countdown → auto-advance to the
   next focus item. A new `focusAutoAdvance` chord (default Ctrl+T,
   which we vacated when Controller Mode was dropped) toggles the
   post-reply countdown on or off. When off, replies stay on the
   current session until the user hits **Next** (manual skip) or
   **Mark Done** (removes from queue) — those gestures always work
   regardless of the toggle, so Next is the manual escape hatch for
   the careful triager.

   The setting persists to `localStorage` under
   `controller.focus.autoAdvance` so the choice survives reloads.
   Toggling OFF also cancels any in-flight countdown — the user has
   just said "I want to stay on this session," so honoring a
   4-second-old schedule contradicts that intent.

   The watermark (`lastInteractionAt`) still bumps on every reply
   regardless of the toggle, so the next manual Next press correctly
   surfaces the just-replied session's "recently finished."

   The sidebar shows a Play/Pause toggle button in the **On radar**
   header (with the chord hint in the tooltip), so the toggle is
   one click away when the user wants it.

4. **Recently-finished bucket** in the advance algorithm.

   `lastInteractionAt` (an in-memory watermark bumped on every
   Next / Reply / Mark Done) splits the queue into three
   conceptual buckets in priority order:

   - **Awaiting input** — checked first, always wins.
   - **Recently finished** — items whose `lastActiveAt` is at or
     after the watermark. Fresh finishes the user hasn't answered
     yet; the algorithm walks the finished pile in arrival order.
   - **Plain circular advance** — when the two priority buckets are
     empty, advance from index N to N+1.

   The navigation algorithm doesn't see visit timestamps directly:
   the visual sort puts unvisited-finished above visited-finished,
   so the algorithm naturally surfaces unvisited first via array
   order. Once all unvisited are visited, the algorithm falls
   through to plain circular, and visited items re-emerge via
   `lastActiveAt` order.

   The watermark is stamped *after* the target is computed and
   after navigation, so the just-replied session's server-side
   `lastActiveAt` update doesn't immediately look "fresh" and
   bounce the user back.

5. **Drop Controller Mode.**

   Controller Mode as a toggle was a UX layer on top of the same
   auto-advance behaviour, with its own blue banner, on/off state,
   and toggle shortcut. Removing the toggle makes "auto-advance to
   the next focus item, unless the user cancels the countdown" the
   default behaviour. Concretely:

   - The `controllerMode` state, the sidebar's Controller Mode
     button, the blue Controller Mode banner in the session view,
     and the toggle handler are all gone.
   - Every reply auto-advances by default (see §3); the **Stay**
     chord (Ctrl+S) cancels the countdown, the **Next** chord
     (Ctrl+N) commits it.
   - **Mark Done** (Ctrl+D) keeps working unchanged.
   - The composer auto-focuses whenever the active session changes,
     so the keyboard-driven triage loop still works without a
     "mode" the user has to remember to enable.
   - Shortcuts are renamed (drop the `controllerMode*` prefix):
     - `controllerModeNext` → `focusAdvanceNext` (Ctrl+N)
     - `controllerModeStay` → `focusStay` (Ctrl+S)
     - `controllerModeDone` → `focusDone` (Ctrl+D)
     - New `focusAutoAdvance` (Ctrl+T) for the auto-advance toggle.
   - The `controllerModeToggle` action (the old Ctrl+T) is removed.
   - The `useControllerModeShortcuts` hook is renamed to
     `useFocusShortcuts` (and gutted of Controller Mode logic).

6. **Migrate legacy `controllerMode*` overrides on read.**

   When a user upgrades across the Controller Mode removal, their
   persisted overrides file may still contain `controllerMode*`
   keys. `normalizeStore` translates them to the new ids in
   memory (preserving the user's chord) and rewrites the file in
   the cleaned shape on first read, so the migration is
   self-healing and never has to run again. `controllerModeToggle`
   (no longer a real action) is silently dropped. A new-id override
   already on file wins over a legacy alias for the same action
   (no clobbering).

## Acceptance criteria

- [x] Given a mix of finished and running sessions on the radar,
      finished sessions appear above all running sessions.
- [x] Within the finished section, the oldest-arrival finished is
      at the top and the newest-arrival is at the bottom of that
      block (FIFO).
- [x] Within the running section, the most recently started running
      session is at the very bottom of the queue; older-running
      sessions stack above it.
- [x] Sessions awaiting user input surface at the top of the queue
      regardless of `active` or freshness.
- [x] Sessions the user has already visited sink below the
      unvisited triage pile so a Next-then-Next-then-Next cycle
      doesn't keep bouncing them to the top.
- [x] No regression for sessions where `focusPinnedAt` is unset
      (still hidden from radar) or where the user has manually
      unpinned (`userUnpinned === true`).
- [x] Existing user rebinds for `controllerMode*` actions are
      migrated to the new ids without dropping the user's chord.
- [x] The user can toggle the post-reply auto-advance countdown on
      or off via the sidebar button or the `focusAutoAdvance`
      chord (default Ctrl+T). **Next** and **Mark Done** keep
      working regardless of the toggle. The choice persists across
      reloads.

## Validation

- `client/src/lib/focus-advance.test.ts` — **14/14 pass**,
  covering the awaiting-input bucket, the recently-finished FIFO
  walking, the sent-from skip rule, and the fall-through to plain
  circular advance.
- `client/src/components/__tests__/sidebar-sort-focus-queue.test.tsx`
  — **15/15 pass**, including the four-bucket sort with awaiting
  at the top, finished-unvisited FIFO at the top of the finished
  block, finished-visited sinking below, and running-oldest-first.
- `client/src/components/__tests__/focus-advance-toast.test.tsx` —
  **2/2 pass**, covering the renamed stay/next chords.
- `server/lib/__tests__/shortcut-settings.test.ts` — **12/12
  pass**, including 5 migration cases (legacy translation,
  file-shape rewrite, removed-action drop, no-clobber of new-id
  overrides, unknown-id drop).
- Full client + shared test suite: **251/251 pass**.
- Full server + CLI test suite: **632/632 pass**.
- Smoke-import of every renamed module (`sidebar`,
  `focus-advance`, `useFocusShortcuts`,
  `focus-advance-toast`, `shortcuts-section`, etc.) succeeds.

## Out of scope / non-goals

- `focusDoneAt` does **not** enter the sort. Sessions marked done
  clear `focusPinnedAt` on the server and the sidebar filter
  removes them, so they never reach the queue.
- No time-decay window — "recent" is bounded by the user's
  Next / Reply / Mark Done interactions, not by a clock.
- Within the awaiting and finished buckets, ties on `lastActiveAt`
  fall back to array order (Array#sort is stable in modern engines).
- Within the visited sub-bucket, ties on `lastVisitedAt` fall
  back to array order too.
- The CLI counterpart (related #322, list on-radar sessions) is
  not implemented yet; when it lands it should mirror this
  ordering.

Refs #333.
@germanescobar

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 6, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-06T14:04:30.171315Z 20ca315 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 20ca31582a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread client/src/App.tsx
Comment on lines +305 to +306
const next = pickFirstFocusItem(reordered, sessionId);
if (next) openFocusItem(next);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use the recent-finish selector in the live advance path

When a previously handled pinned session runs again, its persisted lastVisitedAt remains set, so after completion sortFocusQueue keeps it in the visited bucket and orders it by the old handled time rather than the new finish time. This live path then always opens the first sorted row; the newly added pickNextFocusItem(..., lastInteractionAt) logic is only referenced by tests. With multiple visited sessions, Next can therefore open a stale result ahead of a session that just finished.

Useful? React with 👍 / 👎.

@germanescobar germanescobar Sep 6, 2026

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 171640a. I kept the intended two-step semantics (mark/re-sort the current session, then open the visual first row) and changed the canonical sort so a completion newer than the recorded handled timestamp re-enters the unvisited triage bucket. That makes a previously handled session that finishes again become the new first row, which the live advance path then opens. Added a regression test for this case.

Comment on lines +487 to +492
setAwaitingInputSessionIds(
new Set(
entries
.filter((entry) => entry.awaitingInput)
.map((entry) => entry.sessionId),
),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep refreshing while an inactive session awaits input

For a Claude structured-input pause, a refresh records the session in awaitingInputSessionIds while also making activeSessionIds empty; the polling effect at lines 538-544 then stops because it is gated only on active sessions. If the user subsequently dismisses the prompt, the server clears the flag but no sidebar refresh is triggered, so this set remains stale and the resolved session stays pinned at the highest-priority queue position indefinitely. Poll while either active or awaiting sessions exist, or explicitly refresh after resolving input.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 171640a. The sidebar now keeps polling while there are active sessions OR sessions awaiting input, so an inactive structured-input prompt is refreshed and removed after it is resolved elsewhere.

Comment on lines 150 to +157
for (const [sessionId, state] of runtimes) {
const awaitingInput =
state.awaitingUserInput === true ||
(state.pendingApprovals?.size ?? 0) > 0;
summaries.push({
sessionId,
active: state.active,
awaitingInput: awaitingInput || undefined,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Rebuild persisted input attention after a server restart

The new attention summary is derived exclusively from the process-local runtimes map. After Controller restarts, that map is empty even though user.input_requested and response events are persisted and SessionView reconstructs an unanswered prompt from them, so the same session displays an input card but is no longer marked awaitingInput or prioritized in the focus queue. The runtime listing needs to reconstruct unresolved user-input attention from persisted events, or the attention state itself must be persisted.

Useful? React with 👍 / 👎.

@germanescobar germanescobar Sep 6, 2026

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 171640a. The bulk runtimes endpoint now merges process-local state with unresolved attention reconstructed from persisted JSONL event logs. The scanner caches by file mtime and size, so cold starts recover pending input/approvals while unchanged transcripts are not reparsed on every poll. Added persistence reconstruction tests.

Comment on lines +5767 to +5770
{!streaming && canAttachMore && (
<button
type="button"
onClick={() => fileInputRef.current?.click()}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reveal attachments selected from the compact composer

On a mobile-width viewport the collapsed composer renders this paperclip, but clicking it does not set composerHasFocus; after a file is selected, the attachment chips remain hidden behind showComposerDetails at line 6800. The attachment is nevertheless included in the pending send and enables the Send button, leaving users unable to see or remove what they selected unless they discover that tapping the textarea expands the composer. Selecting a file should expand the composer or show attachment state in compact mode.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 171640a. Selecting a file from the compact mobile composer now expands the composer after the picker returns, making the selected attachment and its remove action visible. Cancelling the picker leaves the composer compact.

Comment thread shared/shortcuts.ts
Comment on lines 23 to +27
export type ShortcutActionId =
| "controllerModeToggle"
| "controllerModeNext"
| "controllerModeDone"
| "controllerModeStay"
| "focusAdvanceNext"
| "focusStay"
| "focusDone"
| "focusAutoAdvance"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Update help pages for the removed Controller Mode

These action IDs remove the Controller Mode toggle and replace it with always-on focus controls, but docs/content/docs/sidebar.mdx still tells users to click a Controller Mode toggle and docs/content/docs/controller-mode.mdx still documents the removed mode and toast. Since the in-app Help link opens these pages, users are directed through a workflow that no longer exists; update or replace the affected help content as part of this removal.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 171640a. Updated the sidebar guide, the former Controller Mode page (now Focus navigation), and the first-project link to describe On radar, Next/Done, the conversation panel, responsive shortcut behavior, and auto-advance.

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.

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

1 participant