From b4303f9bec4dc123da5e3f9fce6488bb76ecbcff Mon Sep 17 00:00:00 2001 From: Jiayuan Zhang Date: Mon, 23 Mar 2026 18:29:39 +0800 Subject: [PATCH 1/2] feat(agent): add agent management UI, skills/tools/triggers, and issue assignment - Complete agents management page with create dialog, runtime device selector, skills/tools/triggers/tasks tabs, and agent detail view - Add AssigneePicker to issue detail page for assigning to members or agents - Extend agent types with description, skills, tools, triggers, RuntimeDevice - Add SDK methods for agent CRUD and task listing - Add migration 002 for agent config columns (skills, tools, triggers) - Update seed data with realistic agent configurations - Use auth context as single source of truth for agents (fixes state sync) Co-Authored-By: Claude Opus 4.6 --- apps/web/app/(dashboard)/agents/page.tsx | 1138 +++++++++++++++-- apps/web/app/(dashboard)/issues/[id]/page.tsx | 225 +++- apps/web/lib/auth-context.test.tsx | 4 + apps/web/test/helpers.tsx | 4 + packages/sdk/src/api-client.ts | 25 + packages/types/src/agent.ts | 70 + packages/types/src/index.ts | 14 +- server/cmd/seed/main.go | 46 +- server/migrations/002_agent_config.down.sql | 5 + server/migrations/002_agent_config.up.sql | 6 + 10 files changed, 1415 insertions(+), 122 deletions(-) create mode 100644 server/migrations/002_agent_config.down.sql create mode 100644 server/migrations/002_agent_config.up.sql diff --git a/apps/web/app/(dashboard)/agents/page.tsx b/apps/web/app/(dashboard)/agents/page.tsx index c8f03704..4517f1f7 100644 --- a/apps/web/app/(dashboard)/agents/page.tsx +++ b/apps/web/app/(dashboard)/agents/page.tsx @@ -6,11 +6,36 @@ import { Cloud, Monitor, Plus, - Zap, ListTodo, + Wrench, + FileText, + Timer, + Trash2, + Save, + X, + Key, + Link2, + Clock, + CheckCircle2, + XCircle, + Loader2, + AlertCircle, + MoreHorizontal, + Play, + ChevronDown, } from "lucide-react"; -import type { Agent, AgentStatus, AgentStatusPayload } from "@multica/types"; +import type { + Agent, + AgentStatus, + AgentTool, + AgentTrigger, + AgentTask, + RuntimeDevice, + CreateAgentRequest, + UpdateAgentRequest, +} from "@multica/types"; import { api } from "../../../lib/api"; +import { useAuth } from "../../../lib/auth-context"; import { useWSEvent } from "../../../lib/ws-context"; // --------------------------------------------------------------------------- @@ -25,6 +50,15 @@ const statusConfig: Record = { + queued: { label: "Queued", icon: Clock, color: "text-muted-foreground" }, + dispatched: { label: "Dispatched", icon: Play, color: "text-blue-500" }, + running: { label: "Running", icon: Loader2, color: "text-green-500" }, + completed: { label: "Completed", icon: CheckCircle2, color: "text-green-600" }, + failed: { label: "Failed", icon: XCircle, color: "text-red-500" }, + cancelled: { label: "Cancelled", icon: XCircle, color: "text-muted-foreground" }, +}; + function getInitials(name: string): string { return name .split(/[\s-]+/) @@ -34,8 +68,231 @@ function getInitials(name: string): string { .slice(0, 2); } +function generateId(): string { + return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`; +} + // --------------------------------------------------------------------------- -// Components +// Mock Runtime Devices (will be replaced with real daemon registration API) +// --------------------------------------------------------------------------- + +const MOCK_RUNTIME_DEVICES: RuntimeDevice[] = [ + { + id: "runtime-cloud", + name: "Multica Agent", + runtime_mode: "cloud", + status: "online", + device_info: "Cloud", + }, + { + id: "runtime-macbook", + name: "Jiayuan's MacBook Pro", + runtime_mode: "local", + status: "online", + device_info: "macOS 15.4 · Claude Code v1.2", + }, + { + id: "runtime-linux", + name: "Dev Server (gpu-01)", + runtime_mode: "local", + status: "online", + device_info: "Ubuntu 24.04 · Codex v0.8", + }, + { + id: "runtime-ci", + name: "CI Runner", + runtime_mode: "local", + status: "offline", + device_info: "Linux · GitHub Actions", + }, +]; + +function getRuntimeDevice(agent: Agent): RuntimeDevice | undefined { + const runtimeId = agent.runtime_config?.runtime_id as string | undefined; + if (runtimeId) { + return MOCK_RUNTIME_DEVICES.find((d) => d.id === runtimeId); + } + if (agent.runtime_mode === "cloud") { + return MOCK_RUNTIME_DEVICES.find((d) => d.runtime_mode === "cloud"); + } + return undefined; +} + +// --------------------------------------------------------------------------- +// Create Agent Dialog +// --------------------------------------------------------------------------- + +function CreateAgentDialog({ + onClose, + onCreate, +}: { + onClose: () => void; + onCreate: (data: CreateAgentRequest) => Promise; +}) { + const [name, setName] = useState(""); + const [description, setDescription] = useState(""); + const [selectedRuntimeId, setSelectedRuntimeId] = useState(MOCK_RUNTIME_DEVICES[0]!.id); + const [creating, setCreating] = useState(false); + const [runtimeOpen, setRuntimeOpen] = useState(false); + + const selectedRuntime = MOCK_RUNTIME_DEVICES.find((d) => d.id === selectedRuntimeId)!; + + const handleSubmit = async () => { + if (!name.trim()) return; + setCreating(true); + try { + await onCreate({ + name: name.trim(), + description: description.trim(), + runtime_mode: selectedRuntime.runtime_mode, + runtime_config: { + runtime_id: selectedRuntime.id, + runtime_name: selectedRuntime.name, + }, + triggers: [{ id: generateId(), type: "on_assign", enabled: true, config: {} }], + }); + onClose(); + } catch { + setCreating(false); + } + }; + + return ( + <> +
+
+
+

Create Agent

+ +
+

+ Create a new AI agent for your workspace. +

+ +
+
+ + setName(e.target.value)} + placeholder="e.g. Deep Research Agent" + className="mt-1 w-full rounded-md border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring" + onKeyDown={(e) => e.key === "Enter" && handleSubmit()} + /> +
+ +
+ + setDescription(e.target.value)} + placeholder="What does this agent do?" + className="mt-1 w-full rounded-md border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring" + /> +
+ +
+ +
+ + + {runtimeOpen && ( + <> +
setRuntimeOpen(false)} /> +
+ {MOCK_RUNTIME_DEVICES.map((device) => ( + + ))} +
+ + )} +
+
+
+ +
+ + +
+
+ + ); +} + +// --------------------------------------------------------------------------- +// Agent List Item // --------------------------------------------------------------------------- function AgentListItem({ @@ -78,83 +335,717 @@ function AgentListItem({ ); } -function AgentDetail({ agent }: { agent: Agent }) { - const st = statusConfig[agent.status]; +// --------------------------------------------------------------------------- +// Skills Tab +// --------------------------------------------------------------------------- + +function SkillsTab({ + agent, + onSave, +}: { + agent: Agent; + onSave: (skills: string) => Promise; +}) { + const [skills, setSkills] = useState(agent.skills); + const [saving, setSaving] = useState(false); + const isDirty = skills !== agent.skills; + + useEffect(() => { + setSkills(agent.skills); + }, [agent.id, agent.skills]); + + const handleSave = async () => { + setSaving(true); + try { + await onSave(skills); + } finally { + setSaving(false); + } + }; return ( -
+
+
+
+

Skills

+

+ Define what this agent does and how it should accomplish tasks. Supports Markdown. +

+
+ {isDirty && ( + + )} +
+