mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-27 11:22:03 +00:00
fix(desktop): make Agents indicator match the Spawn-tree panel
The status-bar "Agents" item conflated three unrelated signals — running subagents (aggregated across all sessions), in-flight session turns, and failed background *system* actions (gateway restarts, toolset installs, computer-use grants via $desktopActionTasks/preview restart) — yet clicking it opens AgentsView, which renders only subagents. A failed gateway restart therefore showed "Agents (1 Failed)" over an empty "No live subagents" tree. AgentsView also filtered to the active session, so a subagent running in a background session showed "Agents N running" with nothing in the tree (the desync reported in #49808). Unify the scope both surfaces speak: - AgentsView aggregates subagents across every session (salvages #49819). - The indicator's running/failed counts come from subagents only (aggregated), never background system actions — those keep their own surfaces in settings / command center. So "Agents (N …)" now always points at a populated Spawn tree. Supersedes #49819. Fixes #49808.
This commit is contained in:
parent
404b06ac4f
commit
a268dfff0a
4 changed files with 51 additions and 40 deletions
|
|
@ -9,9 +9,9 @@ import { type Translations, useI18n } from '@/i18n'
|
|||
import { AlertCircle, CheckCircle2, Sparkles } from '@/lib/icons'
|
||||
import { useEnterAnimation } from '@/lib/use-enter-animation'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { $activeSessionId } from '@/store/session'
|
||||
import {
|
||||
$subagentsBySession,
|
||||
allSubagents,
|
||||
buildSubagentTree,
|
||||
type SubagentNode,
|
||||
type SubagentStatus,
|
||||
|
|
@ -77,15 +77,12 @@ interface AgentsViewProps {
|
|||
|
||||
export function AgentsView({ onClose }: AgentsViewProps) {
|
||||
const { t } = useI18n()
|
||||
const activeSessionId = useStore($activeSessionId)
|
||||
const subagentsBySession = useStore($subagentsBySession)
|
||||
|
||||
const activeSubagents = useMemo(
|
||||
() => (activeSessionId ? (subagentsBySession[activeSessionId] ?? []) : []),
|
||||
[activeSessionId, subagentsBySession]
|
||||
)
|
||||
|
||||
const tree = useMemo(() => buildSubagentTree(activeSubagents), [activeSubagents])
|
||||
// Aggregate every session, matching the status-bar indicator — a subagent
|
||||
// running in a background session must still be visible here, or the two
|
||||
// desync ("Agents N running" vs an empty tree).
|
||||
const tree = useMemo(() => buildSubagentTree(allSubagents(subagentsBySession)), [subagentsBySession])
|
||||
|
||||
return (
|
||||
<OverlayView
|
||||
|
|
|
|||
|
|
@ -22,8 +22,6 @@ import type { RuntimeReadinessResult } from '@/lib/runtime-readiness'
|
|||
import { contextBarLabel, LiveDuration, usageContextLabel } from '@/lib/statusbar'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { setGlobalYolo, setSessionYolo } from '@/lib/yolo-session'
|
||||
import { $desktopActionTasks } from '@/store/activity'
|
||||
import { $previewServerRestartStatus } from '@/store/preview'
|
||||
import {
|
||||
$activeSessionId,
|
||||
$busy,
|
||||
|
|
@ -31,11 +29,10 @@ import {
|
|||
$currentUsage,
|
||||
$sessionStartedAt,
|
||||
$turnStartedAt,
|
||||
$workingSessionIds,
|
||||
$yoloActive,
|
||||
setYoloActive
|
||||
} from '@/store/session'
|
||||
import { $subagentsBySession, activeSubagentCount } from '@/store/subagents'
|
||||
import { $subagentsBySession, activeSubagentCount, failedSubagentCount } from '@/store/subagents'
|
||||
import { $gatewayRestarting } from '@/store/system-actions'
|
||||
import {
|
||||
$backendUpdateApply,
|
||||
|
|
@ -90,12 +87,9 @@ export function useStatusbarItems({
|
|||
const yoloActive = useStore($yoloActive)
|
||||
const busy = useStore($busy)
|
||||
const currentUsage = useStore($currentUsage)
|
||||
const desktopActionTasks = useStore($desktopActionTasks)
|
||||
const gatewayRestarting = useStore($gatewayRestarting)
|
||||
const previewServerRestartStatus = useStore($previewServerRestartStatus)
|
||||
const sessionStartedAt = useStore($sessionStartedAt)
|
||||
const turnStartedAt = useStore($turnStartedAt)
|
||||
const workingSessionIds = useStore($workingSessionIds)
|
||||
const subagentsBySession = useStore($subagentsBySession)
|
||||
const updateStatus = useStore($updateStatus)
|
||||
const updateApply = useStore($updateApply)
|
||||
|
|
@ -159,24 +153,17 @@ export function useStatusbarItems({
|
|||
[gatewayLogLines, gatewayState, inferenceStatus, openCommandCenterSection, statusSnapshot]
|
||||
)
|
||||
|
||||
const { bgFailed, bgRunning, subagentsRunning } = useMemo(() => {
|
||||
const actions = Object.values(desktopActionTasks)
|
||||
const running = actions.filter(t => t.status.running).length
|
||||
const failed = actions.filter(t => !t.status.running && (t.status.exit_code ?? 0) !== 0).length
|
||||
const previewRunning = previewServerRestartStatus === 'running' ? 1 : 0
|
||||
const previewFailed = previewServerRestartStatus === 'error' ? 1 : 0
|
||||
|
||||
const subagentsRunning = Object.values(subagentsBySession).reduce(
|
||||
(sum, items) => sum + activeSubagentCount(items),
|
||||
0
|
||||
)
|
||||
// The indicator must speak the same scope as the Spawn-tree panel it opens:
|
||||
// every session's subagents, never background system actions (gateway
|
||||
// restarts, toolset installs) which surface in their own panels.
|
||||
const { subagentsFailed, subagentsRunning } = useMemo(() => {
|
||||
const lists = Object.values(subagentsBySession)
|
||||
|
||||
return {
|
||||
bgFailed: failed + previewFailed,
|
||||
bgRunning: workingSessionIds.length + running + previewRunning,
|
||||
subagentsRunning
|
||||
subagentsFailed: lists.reduce((sum, items) => sum + failedSubagentCount(items), 0),
|
||||
subagentsRunning: lists.reduce((sum, items) => sum + activeSubagentCount(items), 0)
|
||||
}
|
||||
}, [desktopActionTasks, previewServerRestartStatus, subagentsBySession, workingSessionIds])
|
||||
}, [subagentsBySession])
|
||||
|
||||
const gatewayOpen = gatewayState === 'open'
|
||||
const gatewayConnecting = gatewayState === 'connecting'
|
||||
|
|
@ -321,20 +308,18 @@ export function useStatusbarItems({
|
|||
{
|
||||
className: cn(
|
||||
agentsOpen && 'bg-accent/55 text-foreground',
|
||||
bgFailed > 0 && 'text-destructive hover:text-destructive'
|
||||
subagentsFailed > 0 && 'text-destructive hover:text-destructive'
|
||||
),
|
||||
detail:
|
||||
subagentsRunning > 0
|
||||
? copy.subagents(subagentsRunning)
|
||||
: bgFailed > 0
|
||||
? copy.failed(bgFailed)
|
||||
: bgRunning > 0
|
||||
? copy.running(bgRunning)
|
||||
: undefined,
|
||||
: subagentsFailed > 0
|
||||
? copy.failed(subagentsFailed)
|
||||
: undefined,
|
||||
icon:
|
||||
bgFailed > 0 ? (
|
||||
subagentsFailed > 0 ? (
|
||||
<AlertCircle className="size-3" />
|
||||
) : bgRunning > 0 || subagentsRunning > 0 ? (
|
||||
) : subagentsRunning > 0 ? (
|
||||
<Loader2 className="size-3 animate-spin" />
|
||||
) : (
|
||||
<Sparkles className="size-3" />
|
||||
|
|
@ -356,8 +341,6 @@ export function useStatusbarItems({
|
|||
],
|
||||
[
|
||||
agentsOpen,
|
||||
bgFailed,
|
||||
bgRunning,
|
||||
commandCenterOpen,
|
||||
copy,
|
||||
gatewayMenuContent,
|
||||
|
|
@ -367,6 +350,7 @@ export function useStatusbarItems({
|
|||
inferenceReady,
|
||||
inferenceStatus?.reason,
|
||||
openAgents,
|
||||
subagentsFailed,
|
||||
subagentsRunning,
|
||||
toggleCommandCenter
|
||||
]
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|||
import {
|
||||
$subagentsBySession,
|
||||
activeSubagentCount,
|
||||
allSubagents,
|
||||
buildSubagentTree,
|
||||
clearSessionSubagents,
|
||||
failedSubagentCount,
|
||||
pruneDelegateFallbackSubagents,
|
||||
upsertSubagent
|
||||
} from './subagents'
|
||||
|
|
@ -99,6 +101,27 @@ describe('subagent store', () => {
|
|||
expect(listFor('s1').map(item => item.id)).toEqual(['sa-0-xyz'])
|
||||
})
|
||||
|
||||
// Contract: the status-bar "Agents" indicator and the Spawn-tree panel read
|
||||
// the same scope — every session's subagents — so a count can never point at
|
||||
// an empty tree (the desync behind "Agents (N)" vs "No live subagents").
|
||||
it('counts running/failed across every session, matching the aggregated tree', () => {
|
||||
upsertSubagent('s1', { goal: 'a', status: 'running', subagent_id: 'a', task_index: 0 })
|
||||
upsertSubagent('s1', { goal: 'b', status: 'failed', subagent_id: 'b', task_index: 1 })
|
||||
upsertSubagent('s2', { goal: 'c', status: 'running', subagent_id: 'c', task_index: 0 })
|
||||
upsertSubagent('s2', { goal: 'd', status: 'failed', subagent_id: 'd', task_index: 1 })
|
||||
|
||||
const flat = allSubagents($subagentsBySession.get())
|
||||
const indicatorRunning = Object.values($subagentsBySession.get()).reduce((n, l) => n + activeSubagentCount(l), 0)
|
||||
const indicatorFailed = Object.values($subagentsBySession.get()).reduce((n, l) => n + failedSubagentCount(l), 0)
|
||||
const tree = buildSubagentTree(flat)
|
||||
|
||||
// The active-session-only filter would have reported 1/1 here, not 2/2.
|
||||
expect(indicatorRunning).toBe(2)
|
||||
expect(indicatorFailed).toBe(2)
|
||||
expect(tree).toHaveLength(4)
|
||||
expect(indicatorRunning + indicatorFailed).toBe(tree.length)
|
||||
})
|
||||
|
||||
it('clears one session without touching another', () => {
|
||||
upsertSubagent('s1', { goal: 'one', status: 'running', subagent_id: 'a1', task_index: 0 })
|
||||
upsertSubagent('s2', { goal: 'two', status: 'running', subagent_id: 'a2', task_index: 0 })
|
||||
|
|
|
|||
|
|
@ -261,3 +261,10 @@ export function buildSubagentTree(items: readonly SubagentProgress[]): SubagentN
|
|||
|
||||
export const activeSubagentCount = (items: readonly SubagentProgress[]) =>
|
||||
items.filter(item => item.status === 'queued' || item.status === 'running').length
|
||||
|
||||
export const failedSubagentCount = (items: readonly SubagentProgress[]) =>
|
||||
items.filter(item => item.status === 'failed' || item.status === 'interrupted').length
|
||||
|
||||
/** Flatten every session's subagents — the scope the Spawn-tree panel and the
|
||||
* status-bar indicator must agree on. */
|
||||
export const allSubagents = (bySession: Record<string, SubagentProgress[]>) => Object.values(bySession).flat()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue