mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-02 01:45:52 +02:00
- Add CoreProvider to @multica/core/platform — single component for API/stores/WS/QueryClient init - Delete 13 platform files across web (6) and desktop (7), each app keeps only navigation.tsx - Extract AppSidebar + DashboardLayout to @multica/views/layout - Extract LoginPage to @multica/views/auth - Extract AgentsPage (1,279 lines) to @multica/views/agents (11 files) - Extract InboxPage (468 lines) to @multica/views/inbox (5 files) - Extract SettingsPage + 6 tabs (1,277 lines) to @multica/views/settings (9 files) - Fix AppLink to use forwardRef for Base UI render prop compatibility - Fix Tailwind @source to scan .ts files (status config with bg-info/bg-warning) - Suppress next-themes React 19 script tag warning - Add WebProviders wrapper for Server→Client function passing - Wire all desktop routes to shared views, remove PlaceholderPage - Net: +106 / -4,094 lines Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
136 lines
5.3 KiB
TypeScript
136 lines
5.3 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
import { ListTodo } from "lucide-react";
|
|
import type { Agent, AgentTask } from "@multica/core/types";
|
|
import { Skeleton } from "@multica/ui/components/ui/skeleton";
|
|
import { api } from "@multica/core/api";
|
|
import { useWorkspaceId } from "@multica/core/hooks";
|
|
import { issueListOptions } from "@multica/core/issues/queries";
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import { taskStatusConfig } from "../../config";
|
|
|
|
export function TasksTab({ agent }: { agent: Agent }) {
|
|
const [tasks, setTasks] = useState<AgentTask[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const wsId = useWorkspaceId();
|
|
const { data: issues = [] } = useQuery(issueListOptions(wsId));
|
|
|
|
useEffect(() => {
|
|
setLoading(true);
|
|
api
|
|
.listAgentTasks(agent.id)
|
|
.then(setTasks)
|
|
.catch(() => setTasks([]))
|
|
.finally(() => setLoading(false));
|
|
}, [agent.id]);
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="space-y-2">
|
|
{Array.from({ length: 3 }).map((_, i) => (
|
|
<div key={i} className="flex items-center gap-3 rounded-lg border px-4 py-3">
|
|
<Skeleton className="h-4 w-4 rounded shrink-0" />
|
|
<div className="flex-1 space-y-1.5">
|
|
<Skeleton className="h-4 w-1/2" />
|
|
<Skeleton className="h-3 w-1/3" />
|
|
</div>
|
|
<Skeleton className="h-4 w-16" />
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Sort: active tasks (running > dispatched > queued) first, then completed/failed by date
|
|
const activeStatuses = ["running", "dispatched", "queued"];
|
|
const sortedTasks = [...tasks].sort((a, b) => {
|
|
const aActive = activeStatuses.indexOf(a.status);
|
|
const bActive = activeStatuses.indexOf(b.status);
|
|
const aIsActive = aActive !== -1;
|
|
const bIsActive = bActive !== -1;
|
|
if (aIsActive && !bIsActive) return -1;
|
|
if (!aIsActive && bIsActive) return 1;
|
|
if (aIsActive && bIsActive) return aActive - bActive;
|
|
return new Date(b.created_at).getTime() - new Date(a.created_at).getTime();
|
|
});
|
|
|
|
const issueMap = new Map(issues.map((i) => [i.id, i]));
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div>
|
|
<h3 className="text-sm font-semibold">Task Queue</h3>
|
|
<p className="text-xs text-muted-foreground mt-0.5">
|
|
Issues assigned to this agent and their execution status.
|
|
</p>
|
|
</div>
|
|
|
|
{tasks.length === 0 ? (
|
|
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed py-12">
|
|
<ListTodo className="h-8 w-8 text-muted-foreground/40" />
|
|
<p className="mt-3 text-sm text-muted-foreground">No tasks in queue</p>
|
|
<p className="mt-1 text-xs text-muted-foreground">
|
|
Assign an issue to this agent to get started.
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-1.5">
|
|
{sortedTasks.map((task) => {
|
|
const config = taskStatusConfig[task.status] ?? taskStatusConfig.queued!;
|
|
const Icon = config.icon;
|
|
const issue = issueMap.get(task.issue_id);
|
|
const isActive = task.status === "running" || task.status === "dispatched";
|
|
const isRunning = task.status === "running";
|
|
|
|
return (
|
|
<div
|
|
key={task.id}
|
|
className={`flex items-center gap-3 rounded-lg border px-4 py-3 ${
|
|
isRunning
|
|
? "border-success/40 bg-success/5"
|
|
: task.status === "dispatched"
|
|
? "border-info/40 bg-info/5"
|
|
: ""
|
|
}`}
|
|
>
|
|
<Icon
|
|
className={`h-4 w-4 shrink-0 ${config.color} ${
|
|
isRunning ? "animate-spin" : ""
|
|
}`}
|
|
/>
|
|
<div className="min-w-0 flex-1">
|
|
<div className="flex items-center gap-2">
|
|
{issue && (
|
|
<span className="shrink-0 text-xs font-mono text-muted-foreground">
|
|
{issue.identifier}
|
|
</span>
|
|
)}
|
|
<span className={`text-sm truncate ${isActive ? "font-medium" : ""}`}>
|
|
{issue?.title ?? `Issue ${task.issue_id.slice(0, 8)}...`}
|
|
</span>
|
|
</div>
|
|
<div className="text-xs text-muted-foreground mt-0.5">
|
|
{isRunning && task.started_at
|
|
? `Started ${new Date(task.started_at).toLocaleString()}`
|
|
: task.status === "dispatched" && task.dispatched_at
|
|
? `Dispatched ${new Date(task.dispatched_at).toLocaleString()}`
|
|
: task.status === "completed" && task.completed_at
|
|
? `Completed ${new Date(task.completed_at).toLocaleString()}`
|
|
: task.status === "failed" && task.completed_at
|
|
? `Failed ${new Date(task.completed_at).toLocaleString()}`
|
|
: `Queued ${new Date(task.created_at).toLocaleString()}`}
|
|
</div>
|
|
</div>
|
|
<span className={`shrink-0 text-xs font-medium ${config.color}`}>
|
|
{config.label}
|
|
</span>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|