mirror of
https://github.com/stablyai/orca.git
synced 2026-10-06 08:02:28 +00:00
refactor(native-chat): split the sidebar subagent rows out of this PR
The strip stands alone: the sidebar mapping, its observation plumbing and the AgentStatusEntry.subagents wiring move to a stacked follow-up. No wire field here is sidebar-only - the strip's rows read name, state, elapsed and tokens.
This commit is contained in:
@@ -6,7 +6,6 @@ import type {
|
||||
AgentSessionStatusEvent,
|
||||
AgentSessionStatusSummary
|
||||
} from '../../../../shared/agent-session-wire'
|
||||
import { buildSubagentChildRows } from '../sidebar/worktree-subagent-child-rows'
|
||||
import { resolveAttention } from '../sidebar/smart-attention'
|
||||
import { isExplicitAgentStatusFresh } from '@/lib/pane-agent-evidence'
|
||||
import type { AgentStatusEntry } from '../../../../shared/agent-status-types'
|
||||
@@ -220,116 +219,6 @@ describe('StructuredAgentSessionStatusBridge', () => {
|
||||
expect(statuses()).toEqual([expect.objectContaining({ state: 'blocked' })])
|
||||
})
|
||||
|
||||
it('publishes agent-kind background tasks as the sidebar subagent children', async () => {
|
||||
render(<StructuredAgentSessionStatusBridge />)
|
||||
await waitFor(() => expect(mocks.subscribeStatus).toHaveBeenCalledOnce())
|
||||
|
||||
act(() =>
|
||||
feed().emit({
|
||||
type: 'snapshot',
|
||||
sessions: [
|
||||
summary({
|
||||
backgroundTasks: [
|
||||
{
|
||||
id: 'child-1',
|
||||
kind: 'agent',
|
||||
name: 'deep_review',
|
||||
description: 'Review the diff',
|
||||
state: 'working',
|
||||
startedAt: 500
|
||||
},
|
||||
// A backgrounded shell is not a subagent; kinds stay distinct.
|
||||
{ id: 'shell-1', kind: 'command', description: 'sleep 180', state: 'working' }
|
||||
]
|
||||
})
|
||||
]
|
||||
})
|
||||
)
|
||||
expect(statuses()).toEqual([
|
||||
expect.objectContaining({
|
||||
subagents: [
|
||||
{
|
||||
id: 'child-1',
|
||||
state: 'working',
|
||||
startedAt: 500,
|
||||
agentType: 'deep_review',
|
||||
description: 'Review the diff'
|
||||
}
|
||||
]
|
||||
})
|
||||
])
|
||||
|
||||
// An unchanged roster must not rewrite the store.
|
||||
const writes = mocks.setAgentStatus.mock.calls.length
|
||||
act(() =>
|
||||
feed().emit({
|
||||
type: 'status',
|
||||
session: summary({
|
||||
backgroundTasks: [
|
||||
{
|
||||
id: 'child-1',
|
||||
kind: 'agent',
|
||||
name: 'deep_review',
|
||||
description: 'Review the diff',
|
||||
state: 'working',
|
||||
startedAt: 500
|
||||
},
|
||||
{ id: 'shell-1', kind: 'command', description: 'sleep 180', state: 'working' }
|
||||
]
|
||||
})
|
||||
})
|
||||
)
|
||||
expect(mocks.setAgentStatus.mock.calls.length).toBe(writes)
|
||||
|
||||
act(() =>
|
||||
feed().emit({
|
||||
type: 'status',
|
||||
session: summary({
|
||||
updatedAt: 2,
|
||||
backgroundTasks: [
|
||||
{ id: 'child-1', kind: 'agent', name: 'deep_review', state: 'waiting', startedAt: 500 }
|
||||
]
|
||||
})
|
||||
})
|
||||
)
|
||||
expect(statuses()).toEqual([
|
||||
expect.objectContaining({
|
||||
subagents: [expect.objectContaining({ id: 'child-1', state: 'waiting' })]
|
||||
})
|
||||
])
|
||||
|
||||
// A summary without tasks ends the fan-out: children clear with it.
|
||||
act(() => feed().emit({ type: 'status', session: summary({ status: 'idle', updatedAt: 3 }) }))
|
||||
expect(statuses()).toEqual([expect.objectContaining({ subagents: undefined })])
|
||||
})
|
||||
|
||||
it('keeps quiet live children authoritative and reconfirms them per session after reconnect', async () => {
|
||||
render(<StructuredAgentSessionStatusBridge />)
|
||||
await waitFor(() => expect(mocks.subscribeStatus).toHaveBeenCalledOnce())
|
||||
const live = summary({ backgroundTasks: [{ id: 'child', kind: 'agent', state: 'working' }] })
|
||||
const childState = () =>
|
||||
buildSubagentChildRows({
|
||||
parentEntry: statuses()[0],
|
||||
tab: structuredTab as never,
|
||||
parentIsFresh: false
|
||||
})[0]?.state
|
||||
act(() => feed().emit({ type: 'snapshot', sessions: [live] }))
|
||||
// A hook's evidence window has expired, but the host has not retracted its live task.
|
||||
expect(childState()).toBe('working')
|
||||
act(() => feed().emit({ type: 'end' }))
|
||||
expect(childState()).toBe('unverifiable')
|
||||
await waitFor(() => expect(mocks.subscribeStatus).toHaveBeenCalledTimes(2))
|
||||
act(() => feed(1).emit({ type: 'snapshot', sessions: [] }))
|
||||
expect(childState()).toBe('unverifiable')
|
||||
act(() => feed(1).emit({ type: 'status', session: live }))
|
||||
expect(childState()).toBe('working')
|
||||
const writes = mocks.setAgentStatus.mock.calls.length
|
||||
act(() => feed(1).emit({ type: 'status', session: live }))
|
||||
expect(mocks.setAgentStatus).toHaveBeenCalledTimes(writes)
|
||||
act(() => feed(1).emit({ type: 'status', session: summary({ backgroundTasks: [] }) }))
|
||||
expect(childState()).toBeUndefined()
|
||||
})
|
||||
|
||||
it('carries the model, the running tool line, and the last assistant message', async () => {
|
||||
render(<StructuredAgentSessionStatusBridge />)
|
||||
await waitFor(() => expect(mocks.subscribeStatus).toHaveBeenCalledOnce())
|
||||
|
||||
@@ -1,16 +1,7 @@
|
||||
import { useEffect, useMemo, useSyncExternalStore } from 'react'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import { agentProviderSessionsEqual } from '../../../../shared/agent-session-resume'
|
||||
import type {
|
||||
AgentSessionBackgroundTask,
|
||||
AgentSessionStatusSummary
|
||||
} from '../../../../shared/agent-session-wire'
|
||||
import {
|
||||
AGENT_STATUS_MAX_SUBAGENTS,
|
||||
agentSubagentsEqual,
|
||||
type AgentSubagentSnapshot,
|
||||
type AgentSubagentState
|
||||
} from '../../../../shared/agent-status-types'
|
||||
import type { AgentSessionStatusSummary } from '../../../../shared/agent-session-wire'
|
||||
import {
|
||||
structuredAgentSessionPaneKey,
|
||||
structuredAgentSessionStatusState
|
||||
@@ -58,78 +49,17 @@ export function getStructuredAgentSessionTabs(
|
||||
function useStructuredAgentSessionStatusSummary(
|
||||
sessionId: string,
|
||||
target: RuntimeClientTarget
|
||||
): { summary: AgentSessionStatusSummary | null; observation: 'live' | 'unverifiable' } {
|
||||
): AgentSessionStatusSummary | null {
|
||||
const feed = useMemo(() => getStructuredAgentSessionStatusFeed(target), [target])
|
||||
useEffect(() => feed.activate(), [feed])
|
||||
const summary = useSyncExternalStore(
|
||||
return useSyncExternalStore(
|
||||
feed.subscribe,
|
||||
() => feed.getSnapshot().get(sessionId) ?? null,
|
||||
() => null
|
||||
)
|
||||
const observation = useSyncExternalStore(
|
||||
feed.subscribe,
|
||||
() => feed.getSessionObservation(sessionId),
|
||||
() => 'unverifiable' as const
|
||||
)
|
||||
return { summary, observation }
|
||||
}
|
||||
|
||||
/** Matches the wire-parse bound in `normalizeSubagentSnapshot`. */
|
||||
const SUBAGENT_ID_MAX_LENGTH = 64
|
||||
|
||||
function subagentStateFromTask(task: AgentSessionBackgroundTask): AgentSubagentState {
|
||||
switch (task.state) {
|
||||
case 'waiting':
|
||||
return 'waiting'
|
||||
case 'blocked':
|
||||
return 'blocked'
|
||||
case 'done':
|
||||
case 'idle':
|
||||
case 'unverifiable':
|
||||
return 'idle'
|
||||
// Absent state is an old host's live task; live means working here.
|
||||
case 'working':
|
||||
case 'monitoring':
|
||||
case undefined:
|
||||
return 'working'
|
||||
}
|
||||
}
|
||||
|
||||
/** Sidebar children for a structured session: the agent-kind background tasks
|
||||
* the host publishes, mapped to the sidebar's own subagent vocabulary rather
|
||||
* than widening it. Kinds stay distinct — a backgrounded shell never counts
|
||||
* as a subagent. */
|
||||
function subagentSnapshotsFromTasks(
|
||||
tasks: AgentSessionBackgroundTask[] | undefined
|
||||
): AgentSubagentSnapshot[] | undefined {
|
||||
if (!tasks) {
|
||||
return undefined
|
||||
}
|
||||
const snapshots: AgentSubagentSnapshot[] = []
|
||||
for (const task of tasks) {
|
||||
const id = task.id.trim()
|
||||
if (task.kind !== 'agent' || id.length === 0 || id.length > SUBAGENT_ID_MAX_LENGTH) {
|
||||
continue
|
||||
}
|
||||
snapshots.push({
|
||||
id,
|
||||
state: subagentStateFromTask(task),
|
||||
startedAt: task.startedAt ?? 0,
|
||||
...(task.name ? { agentType: task.name } : {}),
|
||||
...(task.description ? { description: task.description } : {})
|
||||
})
|
||||
if (snapshots.length >= AGENT_STATUS_MAX_SUBAGENTS) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return snapshots.length > 0 ? snapshots : undefined
|
||||
}
|
||||
|
||||
function projectStatus(
|
||||
tab: StructuredTab,
|
||||
summary: AgentSessionStatusSummary | null,
|
||||
observation: 'live' | 'unverifiable'
|
||||
): void {
|
||||
function projectStatus(tab: StructuredTab, summary: AgentSessionStatusSummary | null): void {
|
||||
const paneKey = structuredAgentSessionPaneKey(tab.id, tab.entityId)
|
||||
const store = useAppStore.getState()
|
||||
// No persisted turn yet (or nothing known): the row shows no agent status at all.
|
||||
@@ -139,7 +69,6 @@ function projectStatus(
|
||||
}
|
||||
return
|
||||
}
|
||||
const subagents = subagentSnapshotsFromTasks(summary.backgroundTasks)
|
||||
const desired = {
|
||||
// Shared with `worktree ps`, so the CLI and this row cannot disagree about one session.
|
||||
state: structuredAgentSessionStatusState(summary.status),
|
||||
@@ -151,7 +80,6 @@ function projectStatus(
|
||||
...(summary.toolName ? { toolName: summary.toolName } : {}),
|
||||
...(summary.toolInput ? { toolInput: summary.toolInput } : {}),
|
||||
...(summary.lastAssistantMessage ? { lastAssistantMessage: summary.lastAssistantMessage } : {}),
|
||||
...(subagents ? { subagents, subagentObservation: observation } : {}),
|
||||
sessionBoundary: false
|
||||
} as const
|
||||
const current = store.agentStatusByPaneKey?.[paneKey]
|
||||
@@ -164,8 +92,6 @@ function projectStatus(
|
||||
current.toolName === summary.toolName &&
|
||||
current.toolInput === summary.toolInput &&
|
||||
current.lastAssistantMessage === summary.lastAssistantMessage &&
|
||||
agentSubagentsEqual(current.subagents, subagents) &&
|
||||
current.subagentObservation === desired.subagentObservation &&
|
||||
current.sessionBoundary === desired.sessionBoundary &&
|
||||
current.updatedAt === summary.updatedAt &&
|
||||
current.terminalTitle === tab.label &&
|
||||
@@ -212,10 +138,10 @@ function StructuredAgentSessionStatusProjection({ tab }: { tab: StructuredTab })
|
||||
() => getActiveRuntimeTarget({ activeRuntimeEnvironmentId: environmentId }),
|
||||
[environmentId]
|
||||
)
|
||||
const { summary, observation } = useStructuredAgentSessionStatusSummary(tab.entityId, target)
|
||||
const summary = useStructuredAgentSessionStatusSummary(tab.entityId, target)
|
||||
useEffect(() => {
|
||||
projectStatus(tab, summary, observation)
|
||||
}, [summary, observation, tab])
|
||||
projectStatus(tab, summary)
|
||||
}, [summary, tab])
|
||||
useEffect(
|
||||
() => () =>
|
||||
useAppStore.getState().removeAgentStatus(structuredAgentSessionPaneKey(tab.id, tab.entityId)),
|
||||
|
||||
@@ -29,13 +29,8 @@ export function buildSubagentChildRows(args: {
|
||||
return []
|
||||
}
|
||||
return subagents.map((subagent) => {
|
||||
const observation = args.parentEntry.subagentObservation
|
||||
const fresh = observation === 'live' || (observation === undefined && args.parentIsFresh)
|
||||
const activeState = fresh && subagent.state !== 'idle' ? subagent.state : undefined
|
||||
const state =
|
||||
observation === 'unverifiable' && subagent.state !== 'idle'
|
||||
? 'unverifiable'
|
||||
: (activeState ?? 'idle')
|
||||
const activeState = args.parentIsFresh && subagent.state !== 'idle' ? subagent.state : undefined
|
||||
const state = activeState ?? 'idle'
|
||||
const startedAt = subagent.startedAt > 0 ? subagent.startedAt : args.parentEntry.stateStartedAt
|
||||
const paneKey = subagentRowKey(args.parentEntry.paneKey, subagent.id)
|
||||
const entry: AgentStatusEntry = {
|
||||
|
||||
@@ -125,58 +125,6 @@ describe('structured agent session status feed', () => {
|
||||
expect(feed.getSnapshot().get('session-2')?.status).toBe('idle')
|
||||
})
|
||||
|
||||
it('confirms sessions per connection and rejects late events from a disconnected stream', async () => {
|
||||
const feed = getStructuredAgentSessionStatusFeed(LOCAL)
|
||||
const deactivate = feed.activate()
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
hostEmit()({ type: 'snapshot', sessions: [summary('one'), summary('two')] })
|
||||
expect(feed.getSessionObservation('one')).toBe('live')
|
||||
await vi.advanceTimersByTimeAsync(31 * 60_000)
|
||||
expect(feed.getSessionObservation('one')).toBe('live')
|
||||
expect(mocks.subscribeStatus).toHaveBeenCalledOnce()
|
||||
hostEmit()({ type: 'end' })
|
||||
expect(feed.getSessionObservation('one')).toBe('unverifiable')
|
||||
hostEmit()({ type: 'status', session: summary('one') })
|
||||
expect(feed.getSessionObservation('one')).toBe('unverifiable')
|
||||
await vi.advanceTimersByTimeAsync(300)
|
||||
hostEmit(1)({ type: 'snapshot', sessions: [summary('two')] })
|
||||
expect(feed.getSessionObservation('two')).toBe('live')
|
||||
expect(feed.getSessionObservation('one')).toBe('unverifiable')
|
||||
expect(feed.getSnapshot().has('one')).toBe(true)
|
||||
hostEmit(1)({ type: 'status', session: summary('one') })
|
||||
expect(feed.getSessionObservation('one')).toBe('live')
|
||||
deactivate()
|
||||
expect(feed.getSessionObservation('one')).toBe('unverifiable')
|
||||
expect(vi.getTimerCount()).toBe(0)
|
||||
})
|
||||
|
||||
it.each(['error', 'close', 'reject'] as const)(
|
||||
'revokes confirmation on subscription %s',
|
||||
async (failure) => {
|
||||
let reject!: (error: Error) => void
|
||||
mocks.subscribeStatus.mockImplementationOnce(
|
||||
() =>
|
||||
new Promise((_resolve, rejectPromise) => {
|
||||
reject = rejectPromise
|
||||
})
|
||||
)
|
||||
const feed = getStructuredAgentSessionStatusFeed(LOCAL)
|
||||
feed.activate()
|
||||
hostEmit()({ type: 'status', session: summary('one') })
|
||||
expect(feed.getSessionObservation('one')).toBe('live')
|
||||
if (failure === 'reject') {
|
||||
reject(new Error('disconnected'))
|
||||
} else {
|
||||
mocks.subscribeStatus.mock.calls[0][failure === 'error' ? 2 : 3]()
|
||||
}
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
expect(feed.getSessionObservation('one')).toBe('unverifiable')
|
||||
hostEmit()({ type: 'status', session: summary('one') })
|
||||
expect(feed.getSessionObservation('one')).toBe('unverifiable')
|
||||
expect(vi.getTimerCount()).toBe(1)
|
||||
}
|
||||
)
|
||||
|
||||
it('stops a pending reconnect when the feeds are reset between tests', async () => {
|
||||
getStructuredAgentSessionStatusFeed(LOCAL).activate()
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
@@ -21,7 +21,6 @@ export type StructuredAgentSessionStatusSnapshot = ReadonlyMap<string, AgentSess
|
||||
export type StructuredAgentSessionStatusFeedOwner = {
|
||||
activate: () => () => void
|
||||
getSnapshot: () => StructuredAgentSessionStatusSnapshot
|
||||
getSessionObservation: (sessionId: string) => 'live' | 'unverifiable'
|
||||
subscribe: (listener: () => void) => () => void
|
||||
}
|
||||
|
||||
@@ -38,7 +37,6 @@ export function structuredAgentSessionStatusFeedKey(target: RuntimeClientTarget)
|
||||
|
||||
function createOwner(target: RuntimeClientTarget): OwnedStatusFeed {
|
||||
let snapshot: StructuredAgentSessionStatusSnapshot = new Map()
|
||||
const confirmedSessions = new Set<string>()
|
||||
const listeners = new Set<() => void>()
|
||||
const activations = new Set<symbol>()
|
||||
let generation = 0
|
||||
@@ -62,14 +60,12 @@ function createOwner(target: RuntimeClientTarget): OwnedStatusFeed {
|
||||
// the first snapshot can be empty and dropping those rows flickers every one to no-status.
|
||||
const next = new Map(snapshot)
|
||||
for (const session of event.sessions) {
|
||||
confirmedSessions.add(session.sessionId)
|
||||
next.set(session.sessionId, session)
|
||||
}
|
||||
setSnapshot(next)
|
||||
return
|
||||
}
|
||||
if (event.type === 'status') {
|
||||
confirmedSessions.add(event.session.sessionId)
|
||||
const next = new Map(snapshot)
|
||||
next.set(event.session.sessionId, event.session)
|
||||
setSnapshot(next)
|
||||
@@ -103,6 +99,15 @@ function createOwner(target: RuntimeClientTarget): OwnedStatusFeed {
|
||||
emit()
|
||||
}
|
||||
}
|
||||
const fenceCandidateAndReconnect = (candidate: number): void => {
|
||||
if (candidate !== generation) {
|
||||
return
|
||||
}
|
||||
generation += 1
|
||||
revokeSnapshotOwnership()
|
||||
dropHandle()
|
||||
scheduleReconnect(generation)
|
||||
}
|
||||
let open = (): void => {}
|
||||
const scheduleReconnect = (candidate: number): void => {
|
||||
if (!active(candidate) || reconnectTimer) {
|
||||
@@ -117,19 +122,6 @@ function createOwner(target: RuntimeClientTarget): OwnedStatusFeed {
|
||||
}
|
||||
}, delay)
|
||||
}
|
||||
// Losing contact is never exit: the sessions go unverifiable and this client stops
|
||||
// claiming host-owned execution, but nothing here settles them.
|
||||
const loseConnection = (candidate: number): void => {
|
||||
if (candidate !== generation) {
|
||||
return
|
||||
}
|
||||
generation += 1
|
||||
confirmedSessions.clear()
|
||||
revokeSnapshotOwnership()
|
||||
emit()
|
||||
dropHandle()
|
||||
scheduleReconnect(generation)
|
||||
}
|
||||
const subscribeToHost = (candidate: number): void => {
|
||||
void subscribeStructuredAgentSessionStatus(
|
||||
target,
|
||||
@@ -138,19 +130,19 @@ function createOwner(target: RuntimeClientTarget): OwnedStatusFeed {
|
||||
return
|
||||
}
|
||||
if (event.type === 'end') {
|
||||
loseConnection(candidate)
|
||||
fenceCandidateAndReconnect(candidate)
|
||||
return
|
||||
}
|
||||
applyEvent(event)
|
||||
},
|
||||
() => {
|
||||
if (active(candidate)) {
|
||||
loseConnection(candidate)
|
||||
fenceCandidateAndReconnect(candidate)
|
||||
}
|
||||
},
|
||||
() => {
|
||||
if (active(candidate)) {
|
||||
loseConnection(candidate)
|
||||
fenceCandidateAndReconnect(candidate)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -161,7 +153,13 @@ function createOwner(target: RuntimeClientTarget): OwnedStatusFeed {
|
||||
opened.unsubscribe()
|
||||
}
|
||||
})
|
||||
.catch(() => loseConnection(candidate))
|
||||
.catch(() => {
|
||||
if (active(candidate)) {
|
||||
fenceCandidateAndReconnect(candidate)
|
||||
} else {
|
||||
scheduleReconnect(candidate)
|
||||
}
|
||||
})
|
||||
}
|
||||
open = (): void => {
|
||||
const candidate = ++generation
|
||||
@@ -188,7 +186,7 @@ function createOwner(target: RuntimeClientTarget): OwnedStatusFeed {
|
||||
}
|
||||
console.warn('[structured-session-status] host too old for the status feed', environmentId)
|
||||
})
|
||||
.catch(() => loseConnection(candidate))
|
||||
.catch(() => scheduleReconnect(candidate))
|
||||
}
|
||||
const stop = (): void => {
|
||||
generation += 1
|
||||
@@ -196,9 +194,6 @@ function createOwner(target: RuntimeClientTarget): OwnedStatusFeed {
|
||||
dropHandle()
|
||||
revokeSnapshotOwnership()
|
||||
reconnectAttempt = 0
|
||||
// Teardown only runs once nothing is activated, so re-confirmation is the next
|
||||
// subscribe's job and there is no mounted reader left to notify.
|
||||
confirmedSessions.clear()
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -216,8 +211,6 @@ function createOwner(target: RuntimeClientTarget): OwnedStatusFeed {
|
||||
}
|
||||
},
|
||||
getSnapshot: () => snapshot,
|
||||
getSessionObservation: (sessionId) =>
|
||||
confirmedSessions.has(sessionId) ? 'live' : 'unverifiable',
|
||||
subscribe: (listener) => {
|
||||
listeners.add(listener)
|
||||
return () => listeners.delete(listener)
|
||||
|
||||
@@ -83,7 +83,6 @@ export type AgentLaunchConfigRegistryEntry = {
|
||||
}
|
||||
|
||||
export type AgentStatusPayload = ParsedAgentStatusPayload & {
|
||||
subagentObservation?: AgentStatusEntry['subagentObservation']
|
||||
orchestration?: AgentStatusOrchestrationContext
|
||||
promptInteractionKey?: string
|
||||
restoredUnconfirmed?: boolean
|
||||
|
||||
@@ -256,7 +256,6 @@ export function buildAgentStatusLiveEntry(
|
||||
lastAssistantMessageIsToolOutput: payload.lastAssistantMessageIsToolOutput,
|
||||
...(lastCompletedAssistantMessage ? { lastCompletedAssistantMessage } : {}),
|
||||
orchestration,
|
||||
...(payload.subagentObservation ? { subagentObservation: payload.subagentObservation } : {}),
|
||||
subagents: agentSubagentsEqual(existing?.subagents, payload.subagents)
|
||||
? existing?.subagents
|
||||
: payload.subagents,
|
||||
|
||||
@@ -102,8 +102,6 @@ export type AgentSubagentSnapshot = {
|
||||
}
|
||||
|
||||
export type AgentStatusEntry = {
|
||||
/** Renderer-local status-feed confirmation for children; absent on hook rows. */
|
||||
subagentObservation?: 'live' | 'unverifiable'
|
||||
state: AgentStatusState
|
||||
/** Ongoing work that does not require foreground agent execution. Only valid while working. */
|
||||
workingMode?: AgentWorkingMode
|
||||
|
||||
Reference in New Issue
Block a user