From 72bfc48e63a1a376caad1345da0034633f66fc31 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Tue, 23 Jun 2026 11:32:00 -0700 Subject: [PATCH] feat(tui): track background subagents in the status bar (#51485) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Parity with the classic CLI status bar's ⛓ indicator (PR #51441). The Ink TUI status bar now shows ⛓ N for live background/async subagents (delegate_task batches + background single delegations). - tui_gateway/server.py: _get_usage() embeds active_subagents from tools.async_delegation.active_count() — the same registry the CLI reads — onto the existing per-update usage payload, guarded so a raising active_count() leaves the field off without breaking usage. - ui-tui appChrome: new 'subagents' status segment (breakpoint w>=92, slots between bg and cost in the shed-order), renders ⛓ N from usage.active_subagents. - Usage / SessionUsageResponse types gain active_subagents?. Distinct from the turn-scoped SpawnHud / /agents overlay, which mirror live in-turn subagent.* events; this is the persistent registry count. --- tests/test_tui_gateway_server.py | 42 +++++++++++++++++++ tui_gateway/server.py | 8 ++++ .../__tests__/appChromeStatusRule.test.tsx | 40 ++++++++++++++++++ ui-tui/src/__tests__/statusRule.test.ts | 2 + ui-tui/src/components/appChrome.tsx | 10 +++++ ui-tui/src/gatewayTypes.ts | 1 + ui-tui/src/types.ts | 1 + 7 files changed, 104 insertions(+) diff --git a/tests/test_tui_gateway_server.py b/tests/test_tui_gateway_server.py index 0c70557ce3a..93b2610e293 100644 --- a/tests/test_tui_gateway_server.py +++ b/tests/test_tui_gateway_server.py @@ -7946,3 +7946,45 @@ def test_start_agent_build_passes_session_model_override(monkeypatch): assert session["agent"].model == "claude-sonnet-4.6" finally: server._sessions.clear() + + +# ── _get_usage active_subagents (TUI status-bar ⛓ indicator) ────────────── +# Mirrors the classic CLI status bar: _get_usage embeds a live count of +# background/async subagents from tools.async_delegation.active_count() so the +# Ink status bar can render ⛓ N. Source of truth is the same registry the CLI +# reads; the field rides the existing per-update `usage` payload. + + +class _BareAgent: + """Agent stub with no compressor — exercises the active_subagents path + independent of the `if comp:` context-percent block.""" + + model = "x" + + +def test_get_usage_includes_active_subagents(monkeypatch): + import tools.async_delegation as ad_mod + monkeypatch.setattr(ad_mod, "active_count", lambda: 4) + usage = server._get_usage(_BareAgent()) + assert usage["active_subagents"] == 4 + + +def test_get_usage_active_subagents_zero(monkeypatch): + import tools.async_delegation as ad_mod + monkeypatch.setattr(ad_mod, "active_count", lambda: 0) + usage = server._get_usage(_BareAgent()) + assert usage["active_subagents"] == 0 + + +def test_get_usage_safe_when_active_count_raises(monkeypatch): + """A raising active_count() must not break the usage payload.""" + import tools.async_delegation as ad_mod + + def _boom(): + raise RuntimeError("boom") + + monkeypatch.setattr(ad_mod, "active_count", _boom) + usage = server._get_usage(_BareAgent()) + # Field omitted, but the rest of the payload is intact. + assert "active_subagents" not in usage + assert usage["model"] == "x" diff --git a/tui_gateway/server.py b/tui_gateway/server.py index ad3ea68cdd4..e4bcf1b0bfc 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -2612,6 +2612,14 @@ def _get_usage(agent) -> dict: usage["context_max"] = ctx_max usage["context_percent"] = max(0, min(100, round(ctx_used / ctx_max * 100))) usage["compressions"] = getattr(comp, "compression_count", 0) or 0 + # Live count of background/async subagents still running (delegate_task + # batches + background single delegations). Mirrors the classic CLI status + # bar's ⛓ indicator; sourced from the same async_delegation registry. + try: + from tools.async_delegation import active_count as _async_active_count + usage["active_subagents"] = _async_active_count() + except Exception: + pass try: from agent.usage_pricing import CanonicalUsage, estimate_usage_cost diff --git a/ui-tui/src/__tests__/appChromeStatusRule.test.tsx b/ui-tui/src/__tests__/appChromeStatusRule.test.tsx index 5bbd14bbdce..c7f2a00eefc 100644 --- a/ui-tui/src/__tests__/appChromeStatusRule.test.tsx +++ b/ui-tui/src/__tests__/appChromeStatusRule.test.tsx @@ -105,6 +105,46 @@ const baseProps = { voiceLabel: '' } +describe('StatusRule background-subagent indicator', () => { + it('renders ⛓ N on a wide terminal when subagents are running', () => { + const element = StatusRule({ + ...baseProps, + usage: { ...baseProps.usage, active_subagents: 3 } + }) + + expect(textContent(element)).toContain('⛓ 3') + }) + + it('omits the segment when no subagents are running', () => { + const element = StatusRule({ + ...baseProps, + usage: { ...baseProps.usage, active_subagents: 0 } + }) + + expect(textContent(element)).not.toContain('⛓') + }) + + it('omits the segment when the field is absent', () => { + const element = StatusRule({ ...baseProps }) + + expect(textContent(element)).not.toContain('⛓') + }) + + it('drops the subagent segment before the bg segment on a narrow terminal', () => { + // cols=44 is below the subagents breakpoint (92) but the bg breakpoint + // (88) too — both gone. Assert the lower-priority subagent indicator is + // not shown when space is tight even with a live count. + const element = StatusRule({ + ...baseProps, + cols: 44, + bgCount: 1, + usage: { ...baseProps.usage, active_subagents: 2 } + }) + + expect(textContent(element)).not.toContain('⛓') + }) +}) + describe('StatusRule session count click target', () => { it('makes the live session count itself clickable', () => { const openSwitcher = vi.fn() diff --git a/ui-tui/src/__tests__/statusRule.test.ts b/ui-tui/src/__tests__/statusRule.test.ts index fcba6a96705..6af617a973d 100644 --- a/ui-tui/src/__tests__/statusRule.test.ts +++ b/ui-tui/src/__tests__/statusRule.test.ts @@ -68,6 +68,7 @@ describe('statusBarSegments', () => { compressions: true, voice: true, bg: true, + subagents: true, cost: true }) }) @@ -89,6 +90,7 @@ describe('statusBarSegments', () => { 'compressions', 'voice', 'bg', + 'subagents', 'cost' ] diff --git a/ui-tui/src/components/appChrome.tsx b/ui-tui/src/components/appChrome.tsx index 007fd356355..b3ec8bff21b 100644 --- a/ui-tui/src/components/appChrome.tsx +++ b/ui-tui/src/components/appChrome.tsx @@ -250,6 +250,7 @@ export interface StatusBarSegments { compressions: boolean cost: boolean duration: boolean + subagents: boolean voice: boolean } @@ -263,6 +264,7 @@ export function statusBarSegments(cols: number): StatusBarSegments { compressions: w >= 80, voice: w >= 84, bg: w >= 88, + subagents: w >= 92, cost: w >= 96 } } @@ -512,6 +514,8 @@ export function StatusRule({ const showVoice = segs.voice && !!voiceLabel && fits(SEP + stringWidth(voiceLabel)) const showSessionCount = !!sessionCountText && fits(SEP + stringWidth(sessionCountText)) const showBg = segs.bg && bgCount > 0 && fits(SEP + stringWidth(`${bgCount} bg`)) + const subagentCount = typeof usage.active_subagents === 'number' ? usage.active_subagents : 0 + const showSubagents = segs.subagents && subagentCount > 0 && fits(SEP + stringWidth(`⛓ ${subagentCount}`)) const showCostSeg = segs.cost && showCost && !!costText && fits(SEP + stringWidth(costText)) // No segs flag / no showCost coupling — it's a server-gated dev readout, lowest priority, // so it consumes tail budget LAST and drops first on a narrow terminal. @@ -619,6 +623,12 @@ export function StatusRule({ {bgCount} bg ) : null} + {showSubagents ? ( + + {' │ '} + ⛓ {subagentCount} + + ) : null} {showCostSeg ? ( {' │ '} diff --git a/ui-tui/src/gatewayTypes.ts b/ui-tui/src/gatewayTypes.ts index 74a6f7627d1..1e252e706a3 100644 --- a/ui-tui/src/gatewayTypes.ts +++ b/ui-tui/src/gatewayTypes.ts @@ -310,6 +310,7 @@ export interface SessionUndoResponse { } export interface SessionUsageResponse { + active_subagents?: number cache_read?: number cache_write?: number calls?: number diff --git a/ui-tui/src/types.ts b/ui-tui/src/types.ts index 830e532ce8d..4f7ffa225d2 100644 --- a/ui-tui/src/types.ts +++ b/ui-tui/src/types.ts @@ -167,6 +167,7 @@ export interface SessionInfo { } export interface Usage { + active_subagents?: number calls: number compressions?: number context_max?: number