mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-11 13:41:53 +00:00
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:
parent
7016fa4902
commit
5c5a1fec4b
3 changed files with 33 additions and 29 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue