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: '}