diff --git a/ui-opentui/src/test/agentsTray.test.tsx b/ui-opentui/src/test/agentsTray.test.tsx index eb007a2b772..92b6926be96 100644 --- a/ui-opentui/src/test/agentsTray.test.tsx +++ b/ui-opentui/src/test/agentsTray.test.tsx @@ -25,7 +25,6 @@ import { isTrayAgent } from '../view/agentsTray.tsx' import { ThemeProvider } from '../view/theme.tsx' import { renderProbe, type RenderProbe } from './lib/render.ts' -const INDICATOR = 'running — ↓ to inspect' const EXPANDED_HINT = 'Enter inspect' /** Fake gateway catalog (what `complete.slash` would return for a `/` prefix). */ @@ -95,19 +94,19 @@ describe('agents tray — visibility', () => { test('0 running agents → the tray renders nothing', async () => { const h = await mountApp() try { - expect(h.probe.frame()).not.toContain(INDICATOR) + expect(h.probe.frame()).not.toContain('⚡') } finally { h.probe.destroy() } }) - test('2 running agents → a one-line indicator with the count', async () => { + test('2 running agents → a ⚡ chip in the status bar (no persistent tray line)', async () => { const h = await mountApp() try { spawn(h.store, 'a1', 'research X') spawn(h.store, 'a2', 'compile Y') - const frame = await h.probe.waitForFrame(f => f.includes(INDICATOR)) - expect(frame).toContain(`⚡ 2 agents ${INDICATOR}`) + const frame = await h.probe.waitForFrame(f => f.includes('⚡')) + expect(frame).toContain(`⚡ 2`) expect(frame).not.toContain(EXPANDED_HINT) // collapsed until focused } finally { h.probe.destroy() @@ -119,12 +118,12 @@ describe('agents tray — visibility', () => { try { spawn(h.store, 'a1', 'research X') spawn(h.store, 'a2', 'compile Y') - await h.probe.waitForFrame(f => f.includes('⚡ 2 agents')) + await h.probe.waitForFrame(f => f.includes('⚡ 2')) complete(h.store, 'a1') - const one = await h.probe.waitForFrame(f => f.includes('⚡ 1 agent ')) - expect(one).toContain(`⚡ 1 agent ${INDICATOR}`) + const one = await h.probe.waitForFrame(f => f.includes('⚡ 1')) + expect(one).toContain(`⚡ 1`) complete(h.store, 'a2') - const none = await h.probe.waitForFrame(f => !f.includes(INDICATOR)) + const none = await h.probe.waitForFrame(f => !f.includes('⚡')) expect(none).not.toContain('⚡') } finally { h.probe.destroy() @@ -138,7 +137,7 @@ describe('agents tray — Down-arrow focus routing', () => { try { spawn(h.store, 'a1', 'research X') spawn(h.store, 'a2', 'compile Y') - await h.probe.waitForFrame(f => f.includes(INDICATOR)) + await h.probe.waitForFrame(f => f.includes('⚡')) h.probe.keys.pressArrow('down') const frame = await h.probe.waitForFrame(f => f.includes(EXPANDED_HINT)) // rows show goal + status, with the first row selected @@ -146,7 +145,7 @@ describe('agents tray — Down-arrow focus routing', () => { expect(frame).toContain('compile Y') expect(frame).toContain('● running') expect(frame).toMatch(/▸ ● running\s+research X/) - expect(frame).not.toContain(INDICATOR) // collapsed line replaced by rows + expect(frame).not.toContain('↓ to inspect') // the old persistent tray hint is gone (folded to the status bar) } finally { h.probe.destroy() } @@ -156,7 +155,7 @@ describe('agents tray — Down-arrow focus routing', () => { const h = await mountApp() try { spawn(h.store, 'a1', 'research X') - await h.probe.waitForFrame(f => f.includes(INDICATOR)) + await h.probe.waitForFrame(f => f.includes('⚡')) await h.probe.keys.typeText('hello') await h.probe.settle() h.probe.keys.pressArrow('down') @@ -164,7 +163,7 @@ describe('agents tray — Down-arrow focus routing', () => { const frame = h.probe.frame() expect(frame).toContain('hello') // text untouched expect(frame).not.toContain(EXPANDED_HINT) - expect(frame).toContain(INDICATOR) // still just the indicator + expect(frame).toContain('⚡') // still just the indicator } finally { h.probe.destroy() } @@ -174,7 +173,7 @@ describe('agents tray — Down-arrow focus routing', () => { const h = await mountApp() try { spawn(h.store, 'a1', 'research X') - await h.probe.waitForFrame(f => f.includes(INDICATOR)) + await h.probe.waitForFrame(f => f.includes('⚡')) await h.probe.keys.typeText('/c') await h.probe.settle() await h.probe.waitForFrame(f => f.includes('/copy')) @@ -210,12 +209,12 @@ describe('agents tray — Down-arrow focus routing', () => { const h = await mountApp() try { spawn(h.store, 'a1', 'research X') - await h.probe.waitForFrame(f => f.includes(INDICATOR)) + await h.probe.waitForFrame(f => f.includes('⚡')) h.probe.keys.pressArrow('down') await h.probe.waitForFrame(f => f.includes(EXPANDED_HINT)) h.probe.keys.pressEscape() const frame = await h.probe.waitForFrame(f => !f.includes(EXPANDED_HINT)) - expect(frame).toContain(INDICATOR) // back to the collapsed line + expect(frame).toContain('⚡') // back to the collapsed line await h.probe.keys.typeText('hi') // composer has focus again await h.probe.settle() expect(h.probe.frame()).toContain('hi') @@ -228,12 +227,12 @@ describe('agents tray — Down-arrow focus routing', () => { const h = await mountApp() try { spawn(h.store, 'a1', 'research X') - await h.probe.waitForFrame(f => f.includes(INDICATOR)) + await h.probe.waitForFrame(f => f.includes('⚡')) h.probe.keys.pressArrow('down') await h.probe.waitForFrame(f => f.includes(EXPANDED_HINT)) await h.probe.keys.typeText('x') const frame = await h.probe.waitForFrame(f => !f.includes(EXPANDED_HINT)) - expect(frame).toContain(INDICATOR) // tray collapsed (textarea reclaimed focus) + expect(frame).toContain('⚡') // tray collapsed (textarea reclaimed focus) expect(frame).toContain('x') // …and the char landed in the composer } finally { h.probe.destroy() @@ -247,7 +246,7 @@ describe('agents tray — Enter opens the dashboard preselected', () => { try { spawn(h.store, 'a1', 'research X') spawn(h.store, 'a2', 'compile Y') - await h.probe.waitForFrame(f => f.includes(INDICATOR)) + await h.probe.waitForFrame(f => f.includes('⚡')) h.probe.keys.pressArrow('down') // focus the tray (row 0) await h.probe.waitForFrame(f => f.includes(EXPANDED_HINT)) h.probe.keys.pressArrow('down') // select row 1 (compile Y) diff --git a/ui-opentui/src/view/agentsTray.tsx b/ui-opentui/src/view/agentsTray.tsx index 820defbe61f..b4b7e6b84ab 100644 --- a/ui-opentui/src/view/agentsTray.tsx +++ b/ui-opentui/src/view/agentsTray.tsx @@ -74,7 +74,6 @@ export function AgentsTray(props: { /** Receives the focus-handoff API once (the App wires it to the composer's Down). */ bind?: ((api: AgentsTrayApi) => void) | undefined }) { - const theme = useTheme() const running = createMemo(() => props.subagents.filter(isTrayAgent)) const [focused, setFocused] = createSignal(false) const [sel, setSel] = createSignal(0) @@ -140,17 +139,14 @@ export function AgentsTray(props: { } }) + // Collapsed = NOTHING under the composer (P4 input-density fold, glitch + // 2026-06-13): the running count moved to the status bar's `⚡ N` chip, so the + // tray no longer keeps a persistent line here. The box stays mounted + focusable + // so composer-Down still hands focus over and EXPANDS it into the rows. return ( 0}> - - {`⚡ ${running().length} agent${running().length === 1 ? '' : 's'} running — ↓ to inspect`} - - } - > + diff --git a/ui-opentui/src/view/statusBar.tsx b/ui-opentui/src/view/statusBar.tsx index f00f9b31b50..f76febb4ac3 100644 --- a/ui-opentui/src/view/statusBar.tsx +++ b/ui-opentui/src/view/statusBar.tsx @@ -43,6 +43,7 @@ import { createEffect, createMemo, createSignal, onCleanup, Show } from 'solid-j import { runningCount } from '../logic/backgroundActivity.ts' import type { SessionStore } from '../logic/store.ts' +import { isTrayAgent } from './agentsTray.tsx' import { useDimensions } from './dimensions.tsx' import { elapsedSeconds, useElapsedTick } from './elapsed.ts' import { useTheme } from './theme.tsx' @@ -265,6 +266,13 @@ export function StatusBar(props: { store: SessionStore }) { const n = runningCount([], props.store.state.backgroundProcesses) return segs().bg && n > 0 ? `bg: ${n}` : '' }) + // `⚡ N` — running subagents. The ambient count lives HERE now (P4 input-density + // fold): the agents tray no longer keeps a persistent collapsed line under the + // composer — Down still expands it; this chip is the at-a-glance signal. + const agentsText = createMemo(() => { + const n = props.store.state.subagents.filter(isTrayAgent).length + return n > 0 && dims().width >= 60 ? `⚡ ${n}` : '' + }) // The cwd flows LAST on the same line (not right-pinned): its budget is the // row width minus every segment before it; it tail-truncates into that, and @@ -272,7 +280,7 @@ export function StatusBar(props: { store: SessionStore }) { const leftLen = createMemo(() => { let len = 1 // dot if (model()) len += 1 + model().length + effort().length - for (const seg of [ctxText(), costText(), upText(), cmpText(), profileText(), bgText(), mcpText()]) { + for (const seg of [agentsText(), ctxText(), costText(), upText(), cmpText(), profileText(), bgText(), mcpText()]) { if (seg) len += SEP.length + seg.length } return len @@ -329,6 +337,7 @@ export function StatusBar(props: { store: SessionStore }) { {` ${model()}`} {effort()} + {SEP} {'ctx: '}