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:
@ -1,9 +1,11 @@
|
||||
import type {
|
||||
AgentTicketState,
|
||||
AgentWorkState,
|
||||
BackgroundCompletion,
|
||||
ConversationPreviewStatus,
|
||||
ConversationTurnWorkPreview,
|
||||
ConversationWorkSummary,
|
||||
InboxItem,
|
||||
ProjectWorkState,
|
||||
TicketWorkSource,
|
||||
TicketWorkStatus,
|
||||
@ -24,10 +26,22 @@ function numberValue(value: unknown, fallback = 0): number {
|
||||
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[] {
|
||||
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 {
|
||||
if (!isRecord(value)) return { kind: "human" };
|
||||
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 {
|
||||
const agent = isRecord(value) ? value : {};
|
||||
const liveKind: "pty" | "structured" =
|
||||
@ -78,13 +143,26 @@ function normalizeAgent(value: unknown, index: number): AgentWorkState {
|
||||
}
|
||||
: 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 {
|
||||
agentId: stringValue(agent.agentId, `legacy-agent-${index}`),
|
||||
agentId,
|
||||
name: stringValue(agent.name, "Unnamed agent"),
|
||||
profileId: stringValue(agent.profileId, "unknown"),
|
||||
...(live ? { live } : {}),
|
||||
busy: normalizeBusy(agent.busy),
|
||||
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(
|
||||
value: unknown,
|
||||
index: number,
|
||||
@ -138,8 +212,33 @@ function normalizeConversation(
|
||||
|
||||
export function normalizeProjectWorkState(value: unknown): ProjectWorkState {
|
||||
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 {
|
||||
agents: arrayValue(state.agents).map(normalizeAgent),
|
||||
agents,
|
||||
conversations: arrayValue(state.conversations).map(normalizeConversation),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user