feat(workstate): panneau Work state simplifié — état + intitulé simples, détails et background tasks repliés par défaut — #235
This commit is contained in:
@ -138,6 +138,27 @@ function canRetryTask(status: BackgroundCompletion["status"]): boolean {
|
||||
return status === "failed" || status === "cancelFailed" || status === "cancelled";
|
||||
}
|
||||
|
||||
function agentStatusLabel(live: boolean, busy: boolean): string {
|
||||
if (busy) return "Busy";
|
||||
return live ? "Idle" : "Offline";
|
||||
}
|
||||
|
||||
function agentStatusClass(live: boolean, busy: boolean): string {
|
||||
if (busy) return "bg-warning/15 text-warning";
|
||||
if (live) return "bg-success/10 text-success";
|
||||
return "bg-raised text-muted";
|
||||
}
|
||||
|
||||
function backgroundTaskSummary(tasks: BackgroundCompletion[]): string | null {
|
||||
if (tasks.length === 0) return null;
|
||||
const running = tasks.filter((task) => canCancelTask(task.status)).length;
|
||||
const failed = tasks.filter((task) => canRetryTask(task.status)).length;
|
||||
if (running > 0 && failed > 0) return `${tasks.length} background tasks (${running} active, ${failed} failed)`;
|
||||
if (running > 0) return `${running} background task${running > 1 ? "s" : ""} active`;
|
||||
if (failed > 0) return `${failed} background task${failed > 1 ? "s" : ""} need attention`;
|
||||
return `${tasks.length} background task${tasks.length > 1 ? "s" : ""}`;
|
||||
}
|
||||
|
||||
function requesterLabel(ticket: AgentTicketState): string {
|
||||
if (ticket.source.kind === "human") return "Human";
|
||||
const id = shortTicket(ticket.source.agentId);
|
||||
@ -554,6 +575,8 @@ function AgentRow({
|
||||
const { agent: agentGateway } = useGateways();
|
||||
const [message, setMessage] = useState<string | null>(null);
|
||||
const [actionBusy, setActionBusy] = useState(false);
|
||||
const [detailsOpen, setDetailsOpen] = useState(false);
|
||||
const [backgroundOpen, setBackgroundOpen] = useState(false);
|
||||
const live = agent.live !== undefined;
|
||||
const busyState = agent.busy ?? { state: "idle" };
|
||||
const busy = busyState.state === "busy";
|
||||
@ -568,6 +591,22 @@ function AgentRow({
|
||||
const attachTarget = visibleLeaves.find((leaf) => !leaf.session && !leaf.agent);
|
||||
const currentTurn =
|
||||
busy || tickets.some((ticket) => ticket.status === "inProgress");
|
||||
const activeTicket = tickets.find((ticket) => ticket.status === "inProgress");
|
||||
const statusLabel = agentStatusLabel(live, busy);
|
||||
const taskSummary = backgroundTaskSummary(backgroundTasks);
|
||||
const activityLine =
|
||||
activeTicket?.taskPreview ??
|
||||
(busyState.state === "busy" ? `Ticket ${shortTicket(busyState.ticket)}` : null) ??
|
||||
(inboxDepth > 0 ? `${inboxDepth} queued` : null) ??
|
||||
taskSummary;
|
||||
const hasActions = Boolean(agent.live);
|
||||
const hasDetails =
|
||||
hasActions ||
|
||||
tickets.length > 0 ||
|
||||
inbox.length > 0 ||
|
||||
backgroundTasks.length > 0 ||
|
||||
message ||
|
||||
(agent.live && !liveVisible && !attachTarget);
|
||||
|
||||
async function refreshAfterAction(): Promise<void> {
|
||||
await onRefresh();
|
||||
@ -623,141 +662,148 @@ function AgentRow({
|
||||
|
||||
return (
|
||||
<li className="py-2 first:pt-0 last:pb-0">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="flex min-w-0 flex-col gap-0.5">
|
||||
<span className="truncate text-sm font-medium text-content">
|
||||
<div className="flex min-w-0 items-start justify-between gap-3">
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block truncate text-sm font-semibold text-content">
|
||||
{agent.name}
|
||||
</span>
|
||||
<code className="truncate text-xs text-muted">{agent.profileId}</code>
|
||||
</span>
|
||||
<span className="flex shrink-0 items-center gap-1.5">
|
||||
<span
|
||||
className={cn(
|
||||
"rounded-full px-2 py-0.5 text-xs font-medium",
|
||||
live
|
||||
? "bg-success/15 text-success"
|
||||
: "bg-raised text-muted",
|
||||
)}
|
||||
>
|
||||
{live ? "Live" : "Offline"}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
"rounded-full px-2 py-0.5 text-xs font-medium",
|
||||
busy
|
||||
? "bg-warning/15 text-warning"
|
||||
: "bg-raised text-muted",
|
||||
)}
|
||||
>
|
||||
{busy ? "Busy" : "Idle"}
|
||||
</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}
|
||||
{activityLine && (
|
||||
<span className="mt-0.5 block truncate text-xs text-muted">
|
||||
{activityLine}
|
||||
</span>
|
||||
)}
|
||||
{busyState.state === "busy" && (
|
||||
<code
|
||||
aria-label={`busy ticket ${shortTicket(busyState.ticket)}`}
|
||||
title={busyState.ticket}
|
||||
className="rounded bg-raised px-1.5 py-0.5 text-xs text-muted"
|
||||
</span>
|
||||
<span className="flex shrink-0 items-center gap-2">
|
||||
<span
|
||||
className={cn(
|
||||
"rounded-full px-2 py-0.5 text-xs font-medium",
|
||||
agentStatusClass(live, busy),
|
||||
)}
|
||||
>
|
||||
{statusLabel}
|
||||
</span>
|
||||
{hasDetails && (
|
||||
<button
|
||||
type="button"
|
||||
className="rounded px-1.5 py-0.5 text-xs text-muted hover:bg-raised hover:text-content"
|
||||
aria-expanded={detailsOpen}
|
||||
aria-controls={`work-agent-details-${agent.agentId}`}
|
||||
onClick={() => setDetailsOpen((value) => !value)}
|
||||
>
|
||||
{shortTicket(busyState.ticket)}
|
||||
</code>
|
||||
)}
|
||||
{agent.live && liveVisible && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => goToCell(agent.live!.nodeId)}
|
||||
>
|
||||
Open
|
||||
</Button>
|
||||
)}
|
||||
{agent.live && !liveVisible && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
disabled={!attachTarget || !agentGateway?.attachLiveAgent}
|
||||
loading={actionBusy}
|
||||
title={
|
||||
attachTarget
|
||||
? undefined
|
||||
: "No empty visible cell available"
|
||||
}
|
||||
onClick={() => void attach()}
|
||||
>
|
||||
Attach
|
||||
</Button>
|
||||
)}
|
||||
{agent.live && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="danger"
|
||||
loading={actionBusy}
|
||||
disabled={!agentGateway?.stopLiveAgent}
|
||||
onClick={() => void stop()}
|
||||
>
|
||||
Stop
|
||||
</Button>
|
||||
{detailsOpen ? "Hide" : "Details"}
|
||||
</button>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
{agent.live && !liveVisible && !attachTarget && (
|
||||
<p className="mt-1 text-xs text-muted">No empty visible cell.</p>
|
||||
)}
|
||||
{message && (
|
||||
<p role="alert" className="mt-1 text-xs text-danger">
|
||||
{message}
|
||||
</p>
|
||||
)}
|
||||
{tickets.length > 0 && (
|
||||
<ul
|
||||
aria-label={`${agent.name} tickets`}
|
||||
className="mt-2 flex flex-col gap-1"
|
||||
|
||||
{detailsOpen && (
|
||||
<div
|
||||
id={`work-agent-details-${agent.agentId}`}
|
||||
className="mt-2 space-y-2 border-l border-border pl-3"
|
||||
>
|
||||
{tickets.map((ticket) => (
|
||||
<TicketRow
|
||||
key={ticket.ticketId}
|
||||
ticket={ticket}
|
||||
summary={conversations.get(ticket.conversationId)}
|
||||
onOpenConversation={onOpenConversation}
|
||||
/>
|
||||
))}
|
||||
</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}
|
||||
onRefresh={refreshAfterAction}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
{agent.live && (
|
||||
<div className="flex min-w-0 flex-wrap gap-1.5">
|
||||
{agent.live && liveVisible && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => goToCell(agent.live!.nodeId)}
|
||||
>
|
||||
Open
|
||||
</Button>
|
||||
)}
|
||||
{agent.live && !liveVisible && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
disabled={!attachTarget || !agentGateway?.attachLiveAgent}
|
||||
loading={actionBusy}
|
||||
title={
|
||||
attachTarget
|
||||
? undefined
|
||||
: "No empty visible cell available"
|
||||
}
|
||||
onClick={() => void attach()}
|
||||
>
|
||||
Attach
|
||||
</Button>
|
||||
)}
|
||||
{agent.live && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="danger"
|
||||
loading={actionBusy}
|
||||
disabled={!agentGateway?.stopLiveAgent}
|
||||
onClick={() => void stop()}
|
||||
>
|
||||
Stop
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{agent.live && !liveVisible && !attachTarget && (
|
||||
<p className="text-xs text-muted">No empty visible cell.</p>
|
||||
)}
|
||||
{message && (
|
||||
<p role="alert" className="text-xs text-danger">
|
||||
{message}
|
||||
</p>
|
||||
)}
|
||||
{tickets.length > 0 && (
|
||||
<ul
|
||||
aria-label={`${agent.name} tickets`}
|
||||
className="flex flex-col gap-1"
|
||||
>
|
||||
{tickets.map((ticket) => (
|
||||
<TicketRow
|
||||
key={ticket.ticketId}
|
||||
ticket={ticket}
|
||||
summary={conversations.get(ticket.conversationId)}
|
||||
onOpenConversation={onOpenConversation}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
{inbox.length > 0 && (
|
||||
<div>
|
||||
<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>
|
||||
<button
|
||||
type="button"
|
||||
className="text-xs font-medium text-muted hover:text-content hover:underline"
|
||||
aria-expanded={backgroundOpen}
|
||||
aria-controls={`work-agent-background-${agent.agentId}`}
|
||||
onClick={() => setBackgroundOpen((value) => !value)}
|
||||
>
|
||||
{backgroundOpen ? "Hide background tasks" : taskSummary}
|
||||
</button>
|
||||
{backgroundOpen && (
|
||||
<ul
|
||||
id={`work-agent-background-${agent.agentId}`}
|
||||
aria-label={`${agent.name} background tasks`}
|
||||
className="mt-1 flex flex-col gap-1"
|
||||
>
|
||||
{backgroundTasks.map((task) => (
|
||||
<BackgroundTaskRow
|
||||
key={task.taskId}
|
||||
task={task}
|
||||
onRefresh={refreshAfterAction}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
|
||||
Reference in New Issue
Block a user