From 1c154fc5887ec1adf75caa67a7147f0c8b0b54dc Mon Sep 17 00:00:00 2001 From: Daman Singh Date: Sat, 18 Apr 2026 15:25:42 -0500 Subject: [PATCH] feat(session-observer): cycle through sub-agent sessions with [/] or Tab While in viewer mode, press ] or Tab to jump to next sub-agent session, [ or Shift+Tab for previous. Wraps around at boundaries. - Header shows position indicator (2/5) when multiple sub-agents exist - Footer hints updated to show [/]:prev/next agent - Cycling resets scroll/selection and auto-skips to first non-user entry - Clears transcript cache on switch for clean reload --- .../components/session-observer-overlay.ts | 52 ++++++++++++++++++- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/packages/coding-agent/src/modes/components/session-observer-overlay.ts b/packages/coding-agent/src/modes/components/session-observer-overlay.ts index e3eb3e3f0..188e0b286 100644 --- a/packages/coding-agent/src/modes/components/session-observer-overlay.ts +++ b/packages/coding-agent/src/modes/components/session-observer-overlay.ts @@ -200,7 +200,12 @@ export class SessionObserverOverlayComponent extends Container { const statusColor = session.status === "active" ? "success" : session.status === "failed" ? "error" : "dim"; const statusText = theme.fg(statusColor, `[${session.status}]`); const agentTag = session.agent ? theme.fg("dim", ` ${session.agent}`) : ""; - this.#viewerHeaderLines.push(`${theme.bold(session.label)} ${statusText}${agentTag}`); + // Session position indicator for cycling + const subagentIds = this.#getSubagentSessionIds(); + const posIdx = subagentIds.indexOf(this.#selectedSessionId ?? ""); + const posLabel = + subagentIds.length > 1 && posIdx >= 0 ? theme.fg("dim", ` (${posIdx + 1}/${subagentIds.length})`) : ""; + this.#viewerHeaderLines.push(`${theme.bold(session.label)} ${statusText}${agentTag}${posLabel}`); } // Content @@ -229,7 +234,7 @@ export class SessionObserverOverlayComponent extends Container { const statsLine = this.#buildStatsLine(session); if (statsLine) this.#viewerFooterLines.push(statsLine); this.#viewerFooterLines.push( - theme.fg("dim", "j/k:navigate Enter:expand/collapse Esc:back PgUp/PgDn:page g/G:top/bottom"), + theme.fg("dim", "j/k:navigate Enter:expand [/]:prev/next agent Esc:back g/G:top/bottom"), ); // Auto-scroll to bottom if we were at bottom @@ -754,6 +759,49 @@ export class SessionObserverOverlayComponent extends Container { this.#rebuildAndScroll(); return; } + + // ] or Tab — next sub-agent session + if (keyData === "]" || matchesKey(keyData, "tab")) { + this.#cycleSession(1); + return; + } + + // [ or Shift+Tab — previous sub-agent session + if (keyData === "[" || matchesKey(keyData, "shift+tab")) { + this.#cycleSession(-1); + return; + } + } + + /** Get the ordered list of sub-agent session IDs (excludes main) */ + #getSubagentSessionIds(): string[] { + return this.#registry + .getSessions() + .filter(s => s.kind === "subagent") + .map(s => s.id); + } + + /** Cycle to next (+1) or previous (-1) sub-agent session */ + #cycleSession(direction: 1 | -1): void { + const ids = this.#getSubagentSessionIds(); + if (ids.length <= 1) return; + const currentIdx = ids.indexOf(this.#selectedSessionId ?? ""); + if (currentIdx < 0) return; + const nextIdx = (currentIdx + direction + ids.length) % ids.length; + this.#selectedSessionId = ids[nextIdx]; + this.#transcriptCache = undefined; + this.#scrollOffset = 0; + this.#selectedEntryIndex = 0; + this.#expandedEntries.clear(); + this.#wasAtBottom = true; + this.#rebuildViewerContent(); + // Auto-skip to first non-user entry + const firstNonUser = this.#viewerEntries.findIndex(e => e.kind !== "user"); + if (firstNonUser >= 0) { + this.#selectedEntryIndex = firstNonUser; + this.#rebuildViewerContent(); + this.#scrollToSelectedEntry(); + } } /** Rebuild transcript lines (which depend on selectedEntryIndex/expandedEntries) and scroll to selection */