opentui(v6): background-activity P4 — fold the agents tray line into a status-bar chip

Input-zone density (rpiw): the agents tray kept a persistent collapsed line under
the composer (` N agents running — ↓ to inspect`), stacking with the status bar +
composer. The running count now lives in a status-bar ` N` chip (next to bg:/mcp:),
and the tray renders NOTHING when collapsed — one fewer persistent line under the
transcript. The tray stays mounted + focusable, so composer-Down still hands focus
over and expands it into the rows (focus-routing tests unchanged + green).

Gate green; agentsTray tests repointed from the removed tray line to the chip; the
Down/Esc/printable focus-routing coverage is intact.
This commit is contained in:
alt-glitch 2026-06-13 22:18:08 +05:30
parent 7016fa4902
commit 5c5a1fec4b
3 changed files with 33 additions and 29 deletions

View file

@ -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)

View file

@ -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 (
<Show when={running().length > 0}>
<box ref={attach} focusable style={{ flexDirection: 'column', flexShrink: 0 }}>
<Show
when={focused()}
fallback={
<text selectable={false} fg={theme().color.muted}>
{`${running().length} agent${running().length === 1 ? '' : 's'} running — ↓ to inspect`}
</text>
}
>
<Show when={focused()}>
<TrayRows agents={running()} selected={selected()} firstSeen={firstSeen} />
</Show>
</box>

View file

@ -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 }) {
<span style={{ fg: theme().color.statusFg }}>{` ${model()}`}</span>
<span style={{ fg: theme().color.muted }}>{effort()}</span>
</Show>
<Seg text={agentsText()} fg={theme().color.accent} />
<Show when={ctxText()}>
<span style={{ fg: theme().color.border }}>{SEP}</span>
<span style={{ fg: theme().color.muted }}>{'ctx: '}</span>