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
This commit is contained in:
Daman Singh
2026-04-18 15:25:42 -05:00
parent 9fa037d394
commit 1c154fc588
@@ -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 */