feat(frontend): surface UI des tâches de fond (F1-F4)

Expose les tâches de fond côté front : modèle domaine et
normalisation du work state, panneau ProjectWorkStatePanel,
intégration dans LayoutGrid et ProjectsView, et abonnement aux
événements backgroundTaskChanged / agentInboxChanged /
agentWakeChanged pour rafraîchir l'état. npm run build + npm test
(449) verts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 21:33:51 +02:00
parent e05edc6863
commit 5d88c952ec
6 changed files with 450 additions and 9 deletions

View File

@ -1,9 +1,11 @@
import type { import type {
AgentTicketState, AgentTicketState,
AgentWorkState, AgentWorkState,
BackgroundCompletion,
ConversationPreviewStatus, ConversationPreviewStatus,
ConversationTurnWorkPreview, ConversationTurnWorkPreview,
ConversationWorkSummary, ConversationWorkSummary,
InboxItem,
ProjectWorkState, ProjectWorkState,
TicketWorkSource, TicketWorkSource,
TicketWorkStatus, TicketWorkStatus,
@ -24,10 +26,22 @@ function numberValue(value: unknown, fallback = 0): number {
return typeof value === "number" && Number.isFinite(value) ? value : fallback; return typeof value === "number" && Number.isFinite(value) ? value : fallback;
} }
function nullableNumber(value: unknown): number | null {
return typeof value === "number" && Number.isFinite(value) ? value : null;
}
function arrayValue(value: unknown): unknown[] { function arrayValue(value: unknown): unknown[] {
return Array.isArray(value) ? value : []; return Array.isArray(value) ? value : [];
} }
function optionalString(value: unknown): string | undefined {
return typeof value === "string" ? value : undefined;
}
function nullableString(value: unknown): string | null {
return typeof value === "string" ? value : null;
}
function normalizeSource(value: unknown): TicketWorkSource { function normalizeSource(value: unknown): TicketWorkSource {
if (!isRecord(value)) return { kind: "human" }; if (!isRecord(value)) return { kind: "human" };
if (value.kind === "agent") { if (value.kind === "agent") {
@ -66,6 +80,57 @@ function normalizeTicket(value: unknown, index: number): AgentTicketState {
}; };
} }
function normalizeBackgroundStatus(
value: unknown,
): BackgroundCompletion["status"] {
const raw = typeof value === "string" ? value.toLowerCase() : "";
if (raw === "completed" || raw === "success" || raw === "succeeded") return "completed";
if (raw === "failed" || raw === "failure") return "failed";
if (raw === "cancelled" || raw === "canceled" || raw === "expired") return "cancelled";
if (raw === "running" || raw === "started") return "running";
if (raw === "delivered") return "delivered";
return "pending";
}
function normalizeBackgroundTask(
value: unknown,
index: number,
fallbackAgentId: string,
): BackgroundCompletion {
const task = isRecord(value) ? value : {};
return {
taskId: stringValue(task.taskId, `legacy-task-${index}`),
ownerAgentId: stringValue(task.ownerAgentId, fallbackAgentId),
projectId: stringValue(task.projectId),
kind: stringValue(task.kind, "background"),
status: normalizeBackgroundStatus(task.status ?? task.state),
exitCode: nullableNumber(task.exitCode),
stdoutTail: nullableString(task.stdoutTail),
stderrTail: nullableString(task.stderrTail),
finishedAtMs: nullableNumber(task.finishedAtMs),
};
}
function normalizeInboxItem(
value: unknown,
index: number,
fallbackAgentId: string,
): InboxItem {
const item = isRecord(value) ? value : {};
return {
id: stringValue(item.id, `legacy-inbox-${index}`),
agentId: stringValue(item.agentId ?? item.agent_id, fallbackAgentId),
source: stringValue(item.source, "unknown"),
kind: stringValue(item.kind, "message"),
body: stringValue(item.body),
createdAtMs: numberValue(item.createdAtMs ?? item.created_at_ms, 0),
...(optionalString(item.correlationId ?? item.correlation_id)
? { correlationId: optionalString(item.correlationId ?? item.correlation_id) }
: {}),
priority: numberValue(item.priority, 0),
};
}
function normalizeAgent(value: unknown, index: number): AgentWorkState { function normalizeAgent(value: unknown, index: number): AgentWorkState {
const agent = isRecord(value) ? value : {}; const agent = isRecord(value) ? value : {};
const liveKind: "pty" | "structured" = const liveKind: "pty" | "structured" =
@ -78,13 +143,26 @@ function normalizeAgent(value: unknown, index: number): AgentWorkState {
} }
: undefined; : undefined;
const agentId = stringValue(agent.agentId, `legacy-agent-${index}`);
const inbox = arrayValue(agent.inbox).map((item, itemIndex) =>
normalizeInboxItem(item, itemIndex, agentId),
);
const backgroundTasks = arrayValue(
agent.backgroundTasks ?? agent.backgroundCompletions,
).map((task, taskIndex) =>
normalizeBackgroundTask(task, taskIndex, agentId),
);
return { return {
agentId: stringValue(agent.agentId, `legacy-agent-${index}`), agentId,
name: stringValue(agent.name, "Unnamed agent"), name: stringValue(agent.name, "Unnamed agent"),
profileId: stringValue(agent.profileId, "unknown"), profileId: stringValue(agent.profileId, "unknown"),
...(live ? { live } : {}), ...(live ? { live } : {}),
busy: normalizeBusy(agent.busy), busy: normalizeBusy(agent.busy),
tickets: arrayValue(agent.tickets).map(normalizeTicket), tickets: arrayValue(agent.tickets).map(normalizeTicket),
inboxDepth: numberValue(agent.inboxDepth, inbox.length),
inbox,
backgroundTasks,
}; };
} }
@ -116,10 +194,6 @@ function normalizeTurn(value: unknown, index: number): ConversationTurnWorkPrevi
}; };
} }
function nullableString(value: unknown): string | null {
return typeof value === "string" ? value : null;
}
function normalizeConversation( function normalizeConversation(
value: unknown, value: unknown,
index: number, index: number,
@ -138,8 +212,33 @@ function normalizeConversation(
export function normalizeProjectWorkState(value: unknown): ProjectWorkState { export function normalizeProjectWorkState(value: unknown): ProjectWorkState {
const state = isRecord(value) ? value : {}; const state = isRecord(value) ? value : {};
const agents = arrayValue(state.agents).map(normalizeAgent);
const topLevelTasks = arrayValue(
state.backgroundTasks ?? state.backgroundCompletions,
).map((task, index) => normalizeBackgroundTask(task, index, ""));
for (const task of topLevelTasks) {
const owner = agents.find((agent) => agent.agentId === task.ownerAgentId);
if (!owner) continue;
owner.backgroundTasks ??= [];
if (!owner.backgroundTasks.some((item) => item.taskId === task.taskId)) {
owner.backgroundTasks.push(task);
}
}
const topLevelInbox = arrayValue(state.inboxItems).map((item, index) =>
normalizeInboxItem(item, index, ""),
);
for (const item of topLevelInbox) {
const owner = agents.find((agent) => agent.agentId === item.agentId);
if (!owner) continue;
owner.inbox ??= [];
owner.inboxDepth ??= 0;
if (!owner.inbox.some((entry) => entry.id === item.id)) {
owner.inbox.push(item);
owner.inboxDepth = Math.max(owner.inboxDepth, owner.inbox.length);
}
}
return { return {
agents: arrayValue(state.agents).map(normalizeAgent), agents,
conversations: arrayValue(state.conversations).map(normalizeConversation), conversations: arrayValue(state.conversations).map(normalizeConversation),
}; };
} }

View File

@ -20,6 +20,26 @@ export type DomainEvent =
| { type: "agentExited"; agentId: string; code: number } | { type: "agentExited"; agentId: string; code: number }
| { type: "agentProfileChanged"; agentId: string; profileId: string } | { type: "agentProfileChanged"; agentId: string; profileId: string }
| { type: "agentBusyChanged"; agentId: string; busy: boolean } | { type: "agentBusyChanged"; agentId: string; busy: boolean }
| {
type: "backgroundTaskChanged";
projectId: string;
taskId: string;
agentId: string;
state: string;
}
| {
type: "agentInboxChanged";
agentId: string;
depth: number;
action: string;
}
| {
type: "agentWakeChanged";
projectId: string;
agentId: string;
action: string;
reason?: string;
}
| { | {
/** /**
* A delegation is ready to be injected into the agent's native terminal * A delegation is ready to be injected into the agent's native terminal
@ -170,6 +190,31 @@ export interface AgentTicketState {
taskLen: number; taskLen: number;
} }
/** One first-class background task visible in the work-state read-model. */
export interface BackgroundCompletion {
taskId: string;
ownerAgentId: string;
projectId: string;
kind: string;
status: "running" | "completed" | "failed" | "cancelled" | "pending" | "delivered";
exitCode: number | null;
stdoutTail: string | null;
stderrTail: string | null;
finishedAtMs: number | null;
}
/** One item currently pending in an agent's inbox. */
export interface InboxItem {
id: string;
agentId: string;
source: string;
kind: string;
body: string;
createdAtMs: number;
correlationId?: string;
priority: number;
}
/** Availability of a compact conversation summary in the work-state read-model. */ /** Availability of a compact conversation summary in the work-state read-model. */
export type ConversationPreviewStatus = export type ConversationPreviewStatus =
| "ready" | "ready"
@ -205,6 +250,9 @@ export interface AgentWorkState {
live?: LiveWorkSession; live?: LiveWorkSession;
busy: WorkBusyState; busy: WorkBusyState;
tickets: AgentTicketState[]; tickets: AgentTicketState[];
inboxDepth?: number;
inbox?: InboxItem[];
backgroundTasks?: BackgroundCompletion[];
} }
/** Minimal read-only live-state surface for a project. */ /** Minimal read-only live-state surface for a project. */

View File

@ -317,6 +317,16 @@ function LeafView({
? workState?.agents.find((row) => row.agentId === agentId) ? workState?.agents.find((row) => row.agentId === agentId)
: undefined; : undefined;
const activeTicket = agentWork?.tickets.find((ticket) => ticket.status === "inProgress"); const activeTicket = agentWork?.tickets.find((ticket) => ticket.status === "inProgress");
const queuedTickets = agentWork?.tickets.filter((ticket) => ticket.status === "queued") ?? [];
const inboxDepth = Math.max(
agentWork?.inboxDepth ?? 0,
agentWork?.inbox?.length ?? 0,
queuedTickets.length,
);
const completedBackgroundTasks =
agentWork?.backgroundTasks?.filter((task) =>
task.status === "completed" || task.status === "failed" || task.status === "cancelled",
) ?? [];
const busyByWorkState = agentWork?.busy.state === "busy" || Boolean(activeTicket); const busyByWorkState = agentWork?.busy.state === "busy" || Boolean(activeTicket);
const delegatedTicket = activeTicket?.source.kind === "agent" ? activeTicket : undefined; const delegatedTicket = activeTicket?.source.kind === "agent" ? activeTicket : undefined;
const historyConversationId = const historyConversationId =
@ -661,7 +671,7 @@ function LeafView({
</button> </button>
)} )}
</div> </div>
{agentId && (busyByWorkState || historyConversationId) && ( {agentId && (busyByWorkState || inboxDepth > 0 || completedBackgroundTasks.length > 0 || historyConversationId) && (
<div <div
role="status" role="status"
aria-live="polite" aria-live="polite"
@ -699,6 +709,60 @@ function LeafView({
: "Busy"} : "Busy"}
</span> </span>
)} )}
{!busyByWorkState && inboxDepth > 0 && (
<span
title={`${inboxDepth} pending message${inboxDepth === 1 ? "" : "s"}`}
style={{
maxWidth: 180,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
border: "1px solid var(--color-primary, #5b9bd5)",
borderRadius: 3,
background: "rgba(91, 155, 213, 0.14)",
color: "var(--color-primary, #5b9bd5)",
padding: "1px 6px",
fontSize: 11,
fontWeight: 600,
}}
>
Queued · {inboxDepth}
</span>
)}
{busyByWorkState && inboxDepth > 0 && (
<span
title={`${inboxDepth} queued message${inboxDepth === 1 ? "" : "s"}`}
style={{
flexShrink: 0,
border: "1px solid var(--color-primary, #5b9bd5)",
borderRadius: 3,
background: "rgba(91, 155, 213, 0.14)",
color: "var(--color-primary, #5b9bd5)",
padding: "1px 6px",
fontSize: 11,
fontWeight: 600,
}}
>
Queued {inboxDepth}
</span>
)}
{completedBackgroundTasks.length > 0 && (
<span
title={`${completedBackgroundTasks.length} background task result${completedBackgroundTasks.length === 1 ? "" : "s"}`}
style={{
flexShrink: 0,
border: "1px solid var(--color-success, #57b36a)",
borderRadius: 3,
background: "rgba(87, 179, 106, 0.14)",
color: "var(--color-success, #57b36a)",
padding: "1px 6px",
fontSize: 11,
fontWeight: 600,
}}
>
Task done {completedBackgroundTasks.length}
</span>
)}
{busyByWorkState && ( {busyByWorkState && (
<button <button
type="button" type="button"

View File

@ -30,7 +30,7 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import type { LayoutInfo } from "@/domain"; import type { DomainEvent, LayoutInfo } from "@/domain";
import { LayoutGrid, LayoutTabs } from "@/features/layout"; import { LayoutGrid, LayoutTabs } from "@/features/layout";
import { AgentsPanel } from "@/features/agents"; import { AgentsPanel } from "@/features/agents";
import { TemplatesPanel } from "@/features/templates"; import { TemplatesPanel } from "@/features/templates";
@ -69,6 +69,26 @@ const SIDEBAR_TABS: { id: SidebarTab; label: string }[] = [
{ id: "git", label: "Git" }, { id: "git", label: "Git" },
]; ];
interface BackgroundTaskToast {
id: string;
projectId: string;
agentId: string;
taskId: string;
state: string;
}
function isTerminalBackgroundTaskEvent(
event: DomainEvent,
): event is Extract<DomainEvent, { type: "backgroundTaskChanged" }> {
return (
event.type === "backgroundTaskChanged" &&
(event.state === "completed" ||
event.state === "failed" ||
event.state === "cancelled" ||
event.state === "delivered")
);
}
export function ProjectsView() { export function ProjectsView() {
const vm = useProjects(); const vm = useProjects();
const { system } = useGateways(); const { system } = useGateways();
@ -86,6 +106,7 @@ export function ProjectsView() {
const [viewerConversationId, setViewerConversationId] = useState< const [viewerConversationId, setViewerConversationId] = useState<
string | null string | null
>(null); >(null);
const [taskToasts, setTaskToasts] = useState<BackgroundTaskToast[]>([]);
const active = vm.openTabs.find((t) => t.id === vm.activeTabId) ?? null; const active = vm.openTabs.find((t) => t.id === vm.activeTabId) ?? null;
@ -99,6 +120,32 @@ export function ProjectsView() {
setViewerConversationId(null); setViewerConversationId(null);
}, [active?.id]); }, [active?.id]);
useEffect(() => {
let unsubscribe: (() => void) | undefined;
let cancelled = false;
void system.onDomainEvent((event) => {
if (!isTerminalBackgroundTaskEvent(event)) return;
const toast: BackgroundTaskToast = {
id: `${event.taskId}-${event.state}-${Date.now()}`,
projectId: event.projectId,
agentId: event.agentId,
taskId: event.taskId,
state: event.state,
};
setTaskToasts((prev) => [...prev.slice(-2), toast]);
window.setTimeout(() => {
setTaskToasts((prev) => prev.filter((item) => item.id !== toast.id));
}, 7000);
}).then((u) => {
if (cancelled) u();
else unsubscribe = u;
});
return () => {
cancelled = true;
unsubscribe?.();
};
}, [system]);
const activeLayoutKind = activeLayout?.kind ?? "terminal"; const activeLayoutKind = activeLayout?.kind ?? "terminal";
const canCreate = name.trim().length > 0 && root.trim().length > 0 && !vm.busy; const canCreate = name.trim().length > 0 && root.trim().length > 0 && !vm.busy;
@ -394,6 +441,32 @@ export function ProjectsView() {
)} )}
</main> </main>
</div> </div>
{taskToasts.length > 0 && (
<div className="fixed bottom-4 right-4 z-50 flex w-80 max-w-[calc(100vw-2rem)] flex-col gap-2">
{taskToasts.map((toast) => (
<button
key={toast.id}
type="button"
className="rounded-md border border-border bg-surface px-3 py-2 text-left shadow-lg hover:border-border-strong"
onClick={() => {
const projectOpen = vm.openTabs.some((tab) => tab.id === toast.projectId);
if (projectOpen) vm.activateTab(toast.projectId);
setSidebarCollapsed(false);
setSidebarTab("work");
setViewerConversationId(null);
setTaskToasts((prev) => prev.filter((item) => item.id !== toast.id));
}}
>
<span className="block text-sm font-medium text-content">
Background task {toast.state}
</span>
<span className="mt-0.5 block truncate text-xs text-muted">
{toast.agentId.slice(0, 8)} · {toast.taskId.slice(0, 8)}
</span>
</button>
))}
</div>
)}
</div> </div>
); );
} }

View File

@ -8,8 +8,10 @@ import { Component, type ReactNode, useState } from "react";
import type { import type {
AgentTicketState, AgentTicketState,
AgentWorkState, AgentWorkState,
BackgroundCompletion,
ConversationPreviewStatus, ConversationPreviewStatus,
ConversationWorkSummary, ConversationWorkSummary,
InboxItem,
LeafCell, LeafCell,
} from "@/domain"; } from "@/domain";
import { useGateways } from "@/app/di"; import { useGateways } from "@/app/di";
@ -85,6 +87,30 @@ function formatStatus(status: AgentTicketState["status"]): string {
return status === "inProgress" ? "In progress" : "Queued"; return status === "inProgress" ? "In progress" : "Queued";
} }
function formatTaskStatus(status: BackgroundCompletion["status"]): string {
switch (status) {
case "running":
return "Running";
case "completed":
return "Completed";
case "failed":
return "Failed";
case "cancelled":
return "Cancelled";
case "delivered":
return "Delivered";
case "pending":
return "Pending";
}
}
function taskStatusClass(status: BackgroundCompletion["status"]): string {
if (status === "running" || status === "pending") return "bg-warning/15 text-warning";
if (status === "completed" || status === "delivered") return "bg-success/10 text-success";
if (status === "failed" || status === "cancelled") return "bg-danger/10 text-danger";
return "bg-raised text-muted";
}
function requesterLabel(ticket: AgentTicketState): string { function requesterLabel(ticket: AgentTicketState): string {
if (ticket.source.kind === "human") return "Human"; if (ticket.source.kind === "human") return "Human";
const id = shortTicket(ticket.source.agentId); const id = shortTicket(ticket.source.agentId);
@ -278,6 +304,92 @@ function TicketRow({
); );
} }
function InboxRow({ item }: { item: InboxItem }) {
return (
<li className="min-w-0 text-xs text-muted">
<div className="flex min-w-0 items-start gap-2">
<span className="mt-0.5 shrink-0 rounded-full bg-raised px-1.5 py-0.5 font-medium text-muted">
{item.kind}
</span>
<span className="min-w-0 flex-1 break-words">{item.body}</span>
<code
title={item.id}
className="mt-0.5 shrink-0 rounded bg-raised px-1.5 py-0.5 text-[11px] text-muted"
>
{shortTicket(item.id)}
</code>
</div>
</li>
);
}
function BackgroundTaskRow({ task }: { task: BackgroundCompletion }) {
const [open, setOpen] = useState(false);
const hasOutput = Boolean(task.stdoutTail || task.stderrTail);
const canCancel = task.status === "running" || task.status === "pending";
const canRetry = task.status === "failed" || task.status === "cancelled";
return (
<li className="min-w-0 text-xs text-muted">
<div className="flex min-w-0 items-start gap-2">
<span
className={cn(
"mt-0.5 shrink-0 rounded-full px-1.5 py-0.5 font-medium",
taskStatusClass(task.status),
)}
>
{formatTaskStatus(task.status)}
</span>
<span className="min-w-0 flex-1">
<span className="font-medium text-content">{task.kind}</span>
{task.exitCode !== null && (
<span className="text-muted"> · exit {task.exitCode}</span>
)}
</span>
<Button
size="sm"
variant="ghost"
disabled={!canCancel}
title="Backend command not exposed yet"
>
Cancel
</Button>
<Button
size="sm"
variant="ghost"
disabled={!hasOutput}
onClick={() => setOpen((v) => !v)}
>
Output
</Button>
<Button
size="sm"
variant="ghost"
disabled={!canRetry}
title="Backend command not exposed yet"
>
Retry
</Button>
<code
title={task.taskId}
className="mt-0.5 shrink-0 rounded bg-raised px-1.5 py-0.5 text-[11px] text-muted"
>
{shortTicket(task.taskId)}
</code>
</div>
{open && hasOutput && (
<pre
aria-label={`task ${shortTicket(task.taskId)} output`}
className="mt-1 max-h-40 overflow-auto whitespace-pre-wrap rounded border border-border bg-canvas p-2 text-[11px] text-muted"
>
{task.stdoutTail ? `stdout\n${task.stdoutTail}` : ""}
{task.stdoutTail && task.stderrTail ? "\n\n" : ""}
{task.stderrTail ? `stderr\n${task.stderrTail}` : ""}
</pre>
)}
</li>
);
}
function AgentRow({ function AgentRow({
agent, agent,
conversations, conversations,
@ -302,6 +414,13 @@ function AgentRow({
const busyState = agent.busy ?? { state: "idle" }; const busyState = agent.busy ?? { state: "idle" };
const busy = busyState.state === "busy"; const busy = busyState.state === "busy";
const tickets = [...(agent.tickets ?? [])].sort((a, b) => a.position - b.position); const tickets = [...(agent.tickets ?? [])].sort((a, b) => a.position - b.position);
const inbox = [...(agent.inbox ?? [])].sort((a, b) => a.createdAtMs - b.createdAtMs);
const backgroundTasks = [...(agent.backgroundTasks ?? [])].sort((a, b) => {
const aDone = a.finishedAtMs ?? 0;
const bDone = b.finishedAtMs ?? 0;
return bDone - aDone || a.taskId.localeCompare(b.taskId);
});
const inboxDepth = Math.max(agent.inboxDepth ?? 0, inbox.length, tickets.length);
const visibleNodeIds = new Set(visibleLeaves.map((leaf) => leaf.id)); const visibleNodeIds = new Set(visibleLeaves.map((leaf) => leaf.id));
const liveVisible = live && visibleNodeIds.has(agent.live!.nodeId); const liveVisible = live && visibleNodeIds.has(agent.live!.nodeId);
const attachTarget = visibleLeaves.find((leaf) => !leaf.session && !leaf.agent); const attachTarget = visibleLeaves.find((leaf) => !leaf.session && !leaf.agent);
@ -390,6 +509,14 @@ function AgentRow({
> >
{busy ? "Busy" : "Idle"} {busy ? "Busy" : "Idle"}
</span> </span>
{inboxDepth > 0 && (
<span
aria-label={`${agent.name} queue depth ${inboxDepth}`}
className="rounded-full bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary"
>
Queue {inboxDepth}
</span>
)}
{busyState.state === "busy" && ( {busyState.state === "busy" && (
<code <code
aria-label={`busy ticket ${shortTicket(busyState.ticket)}`} aria-label={`busy ticket ${shortTicket(busyState.ticket)}`}
@ -460,6 +587,33 @@ function AgentRow({
))} ))}
</ul> </ul>
)} )}
{inbox.length > 0 && (
<div className="mt-2">
<p className="text-[11px] font-semibold uppercase tracking-wide text-faint">
Inbox
</p>
<ul aria-label={`${agent.name} inbox`} className="mt-1 flex flex-col gap-1">
{inbox.map((item) => (
<InboxRow key={item.id} item={item} />
))}
</ul>
</div>
)}
{backgroundTasks.length > 0 && (
<div className="mt-2">
<p className="text-[11px] font-semibold uppercase tracking-wide text-faint">
Background tasks
</p>
<ul
aria-label={`${agent.name} background tasks`}
className="mt-1 flex flex-col gap-1"
>
{backgroundTasks.map((task) => (
<BackgroundTaskRow key={task.taskId} task={task} />
))}
</ul>
</div>
)}
</li> </li>
); );
} }

View File

@ -56,7 +56,10 @@ export function useProjectWorkState(projectId: string): ProjectWorkStateViewMode
event.type === "agentExited" || event.type === "agentExited" ||
event.type === "agentBusyChanged" || event.type === "agentBusyChanged" ||
event.type === "delegationReady" || event.type === "delegationReady" ||
event.type === "orchestratorRequestProcessed" event.type === "orchestratorRequestProcessed" ||
event.type === "backgroundTaskChanged" ||
event.type === "agentInboxChanged" ||
event.type === "agentWakeChanged"
) { ) {
void refresh(); void refresh();
} }