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:
@ -30,7 +30,7 @@
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import type { LayoutInfo } from "@/domain";
|
||||
import type { DomainEvent, LayoutInfo } from "@/domain";
|
||||
import { LayoutGrid, LayoutTabs } from "@/features/layout";
|
||||
import { AgentsPanel } from "@/features/agents";
|
||||
import { TemplatesPanel } from "@/features/templates";
|
||||
@ -69,6 +69,26 @@ const SIDEBAR_TABS: { id: SidebarTab; label: string }[] = [
|
||||
{ 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() {
|
||||
const vm = useProjects();
|
||||
const { system } = useGateways();
|
||||
@ -86,6 +106,7 @@ export function ProjectsView() {
|
||||
const [viewerConversationId, setViewerConversationId] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const [taskToasts, setTaskToasts] = useState<BackgroundTaskToast[]>([]);
|
||||
|
||||
const active = vm.openTabs.find((t) => t.id === vm.activeTabId) ?? null;
|
||||
|
||||
@ -99,6 +120,32 @@ export function ProjectsView() {
|
||||
setViewerConversationId(null);
|
||||
}, [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 canCreate = name.trim().length > 0 && root.trim().length > 0 && !vm.busy;
|
||||
@ -394,6 +441,32 @@ export function ProjectsView() {
|
||||
)}
|
||||
</main>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user