mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-11 13:41:53 +00:00
opentui(v6): fix /bg — it launches a background PROMPT, not the process panel
I conflated two "background" concepts. In hermes, /bg (aliases /background, /btw) launches a background PROMPT (prompt.background → background.complete), and the `bg: N` badge counts in-flight prompt tasks — but P3 hijacked /bg + bg: for the OS-process registry and never handled background.complete (so completions were silent). Corrected: - /bg <prompt> now launches a background prompt (Ink parity): prompt.background, echoes "bg <id> started", tracks the task in store.bgTasks. - background.complete → drops the task + renders a distinct inline completion CARD with the result (the missing completion notification; a completion-ish kind also fires the OSC desktop ping). - `bg: N` badge counts store.bgTasks (in-flight background prompts), not OS procs. - the OS-process panel moved off /bg to /processes (+ /procs); its header count uses runningCount. Dropped the ambient agents.list poll — the badge is now event-driven and the panel fetches on open. Gate green; new store test for background.complete (card + badge decrement).
This commit is contained in:
parent
76eab10b14
commit
8e853e3ff8
8 changed files with 92 additions and 36 deletions
|
|
@ -538,6 +538,7 @@ export const run = Effect.fn('Tui.run')(function* (input: TuiInput) {
|
|||
.map(e => `${e.scope}: ${e.msg}`),
|
||||
openDashboard: () => store.openDashboard(),
|
||||
openBackgroundPanel: () => store.openBackgroundPanel(),
|
||||
addBgTask: id => store.addBgTask(id),
|
||||
openPager: (title, text) => store.openPager(title, text),
|
||||
openPicker: picker => store.openPicker(picker),
|
||||
openSessionPicker: tab => store.openSessionPicker(tab),
|
||||
|
|
@ -594,26 +595,9 @@ export const run = Effect.fn('Tui.run')(function* (input: TuiInput) {
|
|||
// Live backend: drive a session (create + optional initial prompt) concurrently.
|
||||
if (!input.fake) yield* Effect.forkScoped(bootstrapSession(gateway, store, input))
|
||||
|
||||
// Ambient `bg:` badge (A): poll the OS-process registry so the status bar
|
||||
// reflects running background processes even with the panel closed. Cheap
|
||||
// local RPC; scoped fiber → auto-cancelled on shutdown. Adaptive interval:
|
||||
// most sessions have ZERO background processes, so idle-poll slowly (30s)
|
||||
// and tighten to 8s only once something is running.
|
||||
if (!input.fake)
|
||||
yield* Effect.forkScoped(
|
||||
Effect.gen(function* () {
|
||||
while (true) {
|
||||
const idle = store.state.backgroundProcesses.length === 0
|
||||
yield* Effect.sleep(idle ? '30 seconds' : '8 seconds')
|
||||
yield* Effect.promise(() =>
|
||||
backgroundOps
|
||||
.list()
|
||||
.then(procs => store.setBackgroundProcesses(procs))
|
||||
.catch(() => {})
|
||||
)
|
||||
}
|
||||
})
|
||||
)
|
||||
// (No ambient OS-process poll: the `bg:` badge now counts in-flight
|
||||
// background-PROMPT tasks from the event stream, and the /processes panel
|
||||
// fetches `agents.list` on open. Nothing to poll for.)
|
||||
|
||||
// Contact point #1: the single render bridge. After this, the screen is Solid's.
|
||||
// The theme is sourced reactively from the store (skin events update it).
|
||||
|
|
|
|||
|
|
@ -73,8 +73,10 @@ export interface SlashContext {
|
|||
readonly openPicker: (picker: PickerState) => void
|
||||
/** Open the agents dashboard (/agents, /tasks). */
|
||||
readonly openDashboard: () => void
|
||||
/** Open the background-process panel (/bg). */
|
||||
/** Open the OS background-process panel (/processes). */
|
||||
readonly openBackgroundPanel: () => void
|
||||
/** Track an in-flight background-prompt task id (`/bg` → prompt.background). */
|
||||
readonly addBgTask: (id: string) => void
|
||||
/** Cached `/model` picker rows (Epic 7 instant open); undefined until prefetched. */
|
||||
readonly modelItems: () => PickerItem[] | undefined
|
||||
/** Update the cached `/model` picker rows. */
|
||||
|
|
@ -212,7 +214,8 @@ const CLIENT_HELP_LINES = [
|
|||
'/clear, /new — clear the transcript (confirm)',
|
||||
'/compact [on|off|toggle] — compact transcript spacing',
|
||||
'/details [hidden|collapsed|expanded|cycle] — tool/reasoning detail',
|
||||
'/bg — background processes (list + stop all)',
|
||||
'/bg <prompt> — launch a background prompt',
|
||||
'/processes — OS background processes (list + stop all)',
|
||||
'/replay [n|path] — inspect an archived spawn tree',
|
||||
'/mem — live memory stats (diag)',
|
||||
'/heapdump — write a V8 heap snapshot (diag)',
|
||||
|
|
@ -663,11 +666,36 @@ const toolsCmd: ClientHandler = async (arg, ctx) => {
|
|||
}
|
||||
}
|
||||
|
||||
/** `/bg <prompt>` (aliases /background, /btw) — launch a background PROMPT via
|
||||
* `prompt.background` (Ink parity): echo "bg <id> started" and track the task so
|
||||
* the `bg: N` badge counts it until `background.complete` clears it. NOT the OS
|
||||
* process panel (that's /processes). */
|
||||
const backgroundCmd: ClientHandler = async (arg, ctx) => {
|
||||
const text = arg.trim()
|
||||
if (!text) {
|
||||
ctx.pushSystem('/bg <prompt> — launch a background prompt')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const r = await ctx.request('prompt.background', { session_id: ctx.sessionId(), text })
|
||||
const taskId = readStr(r, 'task_id')
|
||||
if (taskId) {
|
||||
ctx.addBgTask(taskId)
|
||||
ctx.pushSystem(`bg ${taskId} started`)
|
||||
} else {
|
||||
ctx.pushSystem('/bg: no task id returned')
|
||||
}
|
||||
} catch (error) {
|
||||
ctx.pushSystem(`/bg: ${error instanceof Error ? error.message : 'failed'}`)
|
||||
}
|
||||
}
|
||||
|
||||
/** The TUI-only client commands (run in-process, never hit the gateway). */
|
||||
const CLIENT: Record<string, ClientHandler> = {
|
||||
agents: (_arg, ctx) => ctx.openDashboard(),
|
||||
background: (_arg, ctx) => ctx.openBackgroundPanel(),
|
||||
bg: (_arg, ctx) => ctx.openBackgroundPanel(),
|
||||
background: backgroundCmd,
|
||||
bg: backgroundCmd,
|
||||
btw: backgroundCmd,
|
||||
clear: (_arg, ctx) => ctx.confirm('Clear the transcript?', ctx.clearTranscript),
|
||||
compact: compactCmd,
|
||||
copy: (arg, ctx) => {
|
||||
|
|
@ -678,8 +706,9 @@ const CLIENT: Record<string, ClientHandler> = {
|
|||
details: detailsCmd,
|
||||
exit: (_arg, ctx) => ctx.quit(),
|
||||
heapdump: heapdumpCmd,
|
||||
jobs: (_arg, ctx) => ctx.openBackgroundPanel(),
|
||||
mem: memCmd,
|
||||
processes: (_arg, ctx) => ctx.openBackgroundPanel(),
|
||||
procs: (_arg, ctx) => ctx.openBackgroundPanel(),
|
||||
model: modelCmd,
|
||||
replay: replayCmd,
|
||||
resume: resumeCmd,
|
||||
|
|
|
|||
|
|
@ -257,10 +257,13 @@ export interface StoreState {
|
|||
dashboard: boolean
|
||||
/** Subagent id the dashboard should preselect on open (tray Enter — Epic 2.7). */
|
||||
dashboardAgent: string | undefined
|
||||
/** Whether the background-process panel overlay is open (/bg). */
|
||||
/** Whether the OS background-process panel overlay is open (/processes). */
|
||||
backgroundPanel: boolean
|
||||
/** OS background processes (polled from `agents.list`) — the panel + the `bg:` badge. */
|
||||
/** OS background processes (from `agents.list`) — shown in the /processes panel. */
|
||||
backgroundProcesses: BackgroundProcess[]
|
||||
/** In-flight background-PROMPT task ids (`/bg` → `prompt.background`, cleared on
|
||||
* `background.complete`) — drives the `bg: N` status-bar badge. */
|
||||
bgTasks: string[]
|
||||
/** Transient busy indicator (the kaomoji face/verb from `thinking.delta`/`status.update`);
|
||||
* shown above the composer WHILE a turn runs, cleared on `message.complete`. NOT transcript. */
|
||||
status: string | undefined
|
||||
|
|
@ -474,6 +477,7 @@ export function createSessionStore(options?: SessionStoreOptions) {
|
|||
dashboardAgent: undefined,
|
||||
backgroundPanel: false,
|
||||
backgroundProcesses: [],
|
||||
bgTasks: [],
|
||||
lastNotification: undefined,
|
||||
status: undefined,
|
||||
// startedAt is set ONCE here (store creation ≈ session start) — the status
|
||||
|
|
@ -649,10 +653,20 @@ export function createSessionStore(options?: SessionStoreOptions) {
|
|||
function closeBackgroundPanel() {
|
||||
setState('backgroundPanel', false)
|
||||
}
|
||||
/** Replace the polled OS-process snapshot (drives the panel + the `bg:` badge). */
|
||||
/** Replace the OS-process snapshot (drives the /processes panel). */
|
||||
function setBackgroundProcesses(procs: BackgroundProcess[]) {
|
||||
setState('backgroundProcesses', procs)
|
||||
}
|
||||
/** Track an in-flight background-prompt task (`/bg` start) — drives the `bg:` badge. */
|
||||
function addBgTask(id: string) {
|
||||
if (!state.bgTasks.includes(id)) setState('bgTasks', [...state.bgTasks, id])
|
||||
}
|
||||
function removeBgTask(id: string) {
|
||||
setState(
|
||||
'bgTasks',
|
||||
state.bgTasks.filter(t => t !== id)
|
||||
)
|
||||
}
|
||||
|
||||
/** Open a local Y/N confirm dialog (non-gateway; e.g. /clear). */
|
||||
function setConfirm(message: string, onConfirm: () => void) {
|
||||
|
|
@ -825,6 +839,19 @@ export function createSessionStore(options?: SessionStoreOptions) {
|
|||
if (key) clearNotificationCards(key)
|
||||
break
|
||||
}
|
||||
// A background PROMPT (`/bg`) finished: drop it from the in-flight set (the
|
||||
// `bg:` badge) and surface its result as a distinct inline completion card
|
||||
// (a completion-ish kind → also fires the OSC desktop ping).
|
||||
case 'background.complete': {
|
||||
removeBgTask(event.payload.task_id)
|
||||
pushNotification({
|
||||
id: `bg:${event.payload.task_id}`,
|
||||
kind: 'background task complete',
|
||||
level: 'info',
|
||||
text: `bg ${event.payload.task_id} → ${event.payload.text}`
|
||||
})
|
||||
break
|
||||
}
|
||||
// reasoning.delta is the model's actual reasoning — a (dim) transcript part.
|
||||
case 'reasoning.delta': {
|
||||
const text = event.payload?.text ?? ''
|
||||
|
|
@ -1142,6 +1169,7 @@ export function createSessionStore(options?: SessionStoreOptions) {
|
|||
openBackgroundPanel,
|
||||
closeBackgroundPanel,
|
||||
setBackgroundProcesses,
|
||||
addBgTask,
|
||||
hydrate,
|
||||
beginBuffer,
|
||||
commitSnapshot,
|
||||
|
|
|
|||
|
|
@ -43,6 +43,19 @@ describe('notification store wiring', () => {
|
|||
store.apply({ type: 'notification.show', payload: { level: 'warn', kind: 'x' } })
|
||||
expect(store.state.messages.some(m => m.role === 'notification')).toBe(false)
|
||||
})
|
||||
|
||||
test('background.complete → a completion card + drops the bg task from the badge count', () => {
|
||||
const store = createSessionStore()
|
||||
store.apply({ type: 'gateway.ready' })
|
||||
store.addBgTask('bg_abc')
|
||||
expect(store.state.bgTasks).toEqual(['bg_abc'])
|
||||
store.apply({ type: 'background.complete', payload: { task_id: 'bg_abc', text: 'all done' } })
|
||||
expect(store.state.bgTasks).toEqual([]) // untracked → bg: badge decrements
|
||||
const last = store.state.messages.at(-1)
|
||||
expect(last?.role).toBe('notification')
|
||||
expect(last?.notification?.text).toContain('bg_abc')
|
||||
expect(last?.notification?.text).toContain('all done')
|
||||
})
|
||||
})
|
||||
|
||||
describe('NotificationCard frame', () => {
|
||||
|
|
|
|||
|
|
@ -227,6 +227,7 @@ function makeCtx(request: (method: string, params: Record<string, unknown>) => P
|
|||
setModelItems: items => (modelCache.value = items),
|
||||
openDashboard: () => (dashboard.value = true),
|
||||
openBackgroundPanel: () => {},
|
||||
addBgTask: () => {},
|
||||
openPager: (title, text) => paged.push({ text, title }),
|
||||
openPicker: p => pickers.push(p),
|
||||
openSessionPicker: tab => sessionPickers.push(tab),
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ function makeCtx(request: (method: string, params: Record<string, unknown>) => P
|
|||
setModelItems: () => {},
|
||||
openDashboard: () => {},
|
||||
openBackgroundPanel: () => {},
|
||||
addBgTask: () => {},
|
||||
openPager: (title, text) => paged.push({ text, title }),
|
||||
openPicker: () => {},
|
||||
openSessionPicker: () => {},
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { type BoxRenderable } from '@opentui/core'
|
|||
import { useKeyboard } from '@opentui/solid'
|
||||
import { For, type JSXElement, onMount, Show } from 'solid-js'
|
||||
|
||||
import { procIsRunning, type BackgroundProcess } from '../../logic/backgroundActivity.ts'
|
||||
import { procIsRunning, runningCount, type BackgroundProcess } from '../../logic/backgroundActivity.ts'
|
||||
import { truncRight } from '../../logic/truncate.ts'
|
||||
import { useDimensions } from '../dimensions.tsx'
|
||||
import { useCloseLayer } from '../keymap.tsx'
|
||||
|
|
@ -46,7 +46,7 @@ export function BackgroundPanel(props: {
|
|||
const dims = useDimensions()
|
||||
let rootRef: BoxRenderable | undefined
|
||||
|
||||
const running = () => props.processes.filter(p => procIsRunning(p.status)).length
|
||||
const running = () => runningCount(props.processes)
|
||||
|
||||
// Focus the root so the focus-within close layer is active; refresh once on
|
||||
// open so the list is fresh.
|
||||
|
|
|
|||
|
|
@ -29,8 +29,9 @@
|
|||
* metrics + labels, ok/warn dot, level-tinted ctx bar and cmp count.
|
||||
*
|
||||
* Background-activity chrome (glitch 2026-06-13): `⚡ N` = running subagents
|
||||
* (folded here from the old agents-tray line), `bg: N` = running OS background
|
||||
* processes (polled from `agents.list`). Both hidden at zero.
|
||||
* (folded here from the old agents-tray line), `bg: N` = in-flight background
|
||||
* PROMPTS (`/bg` → prompt.background, cleared on background.complete). Both
|
||||
* hidden at zero. (OS background processes live in the /processes panel.)
|
||||
*
|
||||
* Parity notes (data that does not reach this TUI yet — reported, not faked):
|
||||
* - `display.show_cost`: Ink reads it from its `config.get` polling loop,
|
||||
|
|
@ -42,7 +43,6 @@
|
|||
import { useKeyboard } from '@opentui/solid'
|
||||
import { createEffect, createMemo, createSignal, onCleanup, Show } from 'solid-js'
|
||||
|
||||
import { runningCount } from '../logic/backgroundActivity.ts'
|
||||
import type { SessionStore } from '../logic/store.ts'
|
||||
import { truncLeft, truncRight } from '../logic/truncate.ts'
|
||||
import { isTrayAgent } from './agentsTray.tsx'
|
||||
|
|
@ -253,10 +253,10 @@ export function StatusBar(props: { store: SessionStore }) {
|
|||
const n = info().mcpServers ?? 0
|
||||
return segs().mcp && n > 0 ? `mcp: ${n}` : ''
|
||||
})
|
||||
// `bg: N` — running OS background processes (polled into the store); the
|
||||
// ambient half of the background-activity notifications (glitch 2026-06-13).
|
||||
// `bg: N` — in-flight background-PROMPT tasks (`/bg` → prompt.background,
|
||||
// cleared on background.complete), mirroring Ink's bgTasks count.
|
||||
const bgText = createMemo(() => {
|
||||
const n = runningCount(props.store.state.backgroundProcesses)
|
||||
const n = props.store.state.bgTasks.length
|
||||
return segs().bg && n > 0 ? `bg: ${n}` : ''
|
||||
})
|
||||
// `⚡ N` — running subagents. The ambient count lives HERE now (P4 input-density
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue