diff --git a/apps/desktop/src/renderer/features/todo-agent/TodoButton/TodoButton.tsx b/apps/desktop/src/renderer/features/todo-agent/TodoButton/TodoButton.tsx index 4f74e5a5474..d337a7fa70c 100644 --- a/apps/desktop/src/renderer/features/todo-agent/TodoButton/TodoButton.tsx +++ b/apps/desktop/src/renderer/features/todo-agent/TodoButton/TodoButton.tsx @@ -1,6 +1,6 @@ import { Button } from "@superset/ui/button"; import { cn } from "@superset/ui/utils"; -import { memo, useCallback, useState } from "react"; +import { memo, useCallback, useMemo, useState } from "react"; import { HiMiniListBullet } from "react-icons/hi2"; import { electronTrpc } from "renderer/lib/electron-trpc"; import { TodoManager } from "../TodoManager"; @@ -12,6 +12,46 @@ interface TodoButtonProps { worktreePath?: string | null; } +type StatusCategory = "running" | "queued" | "failed" | "paused"; + +interface StatusBadgeConfig { + label: string; + dot: string; + badge: string; + pulse?: boolean; +} + +const STATUS_BADGE_ORDER: StatusCategory[] = [ + "running", + "queued", + "failed", + "paused", +]; + +const STATUS_BADGE_META: Record = { + running: { + label: "実行中", + dot: "bg-amber-500", + badge: "bg-amber-500/15 text-amber-600 dark:text-amber-400", + pulse: true, + }, + queued: { + label: "待機中", + dot: "bg-primary", + badge: "bg-primary/15 text-primary", + }, + failed: { + label: "失敗/要確認", + dot: "bg-rose-500", + badge: "bg-rose-500/15 text-rose-600 dark:text-rose-400", + }, + paused: { + label: "一時停止", + dot: "bg-muted-foreground/60", + badge: "bg-muted text-muted-foreground", + }, +}; + /** * Entry point for the fork-local TODO autonomous agent feature. Sits * immediately left of the WorkspaceRunButton in PresetsBar. @@ -32,16 +72,51 @@ export const TodoButton = memo(function TodoButton({ { refetchInterval: 3000 }, ); - const runningCount = (allSessions ?? []).filter( - (s) => - s.status === "preparing" || - s.status === "running" || - s.status === "verifying", - ).length; - const queuedCount = (allSessions ?? []).filter( - (s) => s.status === "queued", - ).length; - const activeCount = runningCount + queuedCount; + const counts = useMemo(() => { + const acc: Record = { + running: 0, + queued: 0, + failed: 0, + paused: 0, + }; + for (const s of allSessions ?? []) { + switch (s.status) { + case "preparing": + case "running": + case "verifying": + acc.running += 1; + break; + case "queued": + case "waiting": + // `waiting` は ScheduleWakeup で一時停止中のセッション。 + // scheduler が waitingUntil 経過後に自動で queued に戻すため、 + // slot を占有している扱いとして queued と同じバッジで集計する。 + acc.queued += 1; + break; + case "failed": + case "escalated": + acc.failed += 1; + break; + case "paused": + acc.paused += 1; + break; + default: + break; + } + } + return acc; + }, [allSessions]); + + const activeCount = + counts.running + counts.queued + counts.failed + counts.paused; + + const tooltip = useMemo(() => { + const parts = STATUS_BADGE_ORDER.filter((key) => counts[key] > 0).map( + (key) => `${STATUS_BADGE_META[key].label}: ${counts[key]}`, + ); + if (parts.length === 0) return "自律 TODO Agent Manager を開く"; + return `自律 TODO Agent Manager を開く (${parts.join(" / ")})`; + }, [counts]); const handleRequestNewTodo = useCallback(() => { setModalOpen(true); @@ -55,25 +130,47 @@ export const TodoButton = memo(function TodoButton({ variant="ghost" className={cn( "h-7 gap-1 px-2 text-xs", - activeCount > 0 && "text-primary", + counts.running > 0 && "text-primary", )} onClick={() => setManagerOpen(true)} - title="自律 TODO Agent Manager を開く" + title={tooltip} > TODO - {runningCount > 0 && ( - - - - - - {runningCount} - - )} - {queuedCount > 0 && ( - - +{queuedCount} + {activeCount > 0 && ( + + {STATUS_BADGE_ORDER.map((key) => { + const count = counts[key]; + if (count <= 0) return null; + const meta = STATUS_BADGE_META[key]; + return ( + + + {meta.pulse && ( + + )} + + + {count} + + ); + })} )}