Merge remote-tracking branch 'origin/main' into feat/tanstack-query-migration
# Conflicts: # apps/web/app/(dashboard)/agents/page.tsx
This commit is contained in:
commit
8cf78b7a47
17 changed files with 36 additions and 803 deletions
|
|
@ -8,15 +8,10 @@ import {
|
|||
Monitor,
|
||||
Plus,
|
||||
ListTodo,
|
||||
Wrench,
|
||||
FileText,
|
||||
BookOpenText,
|
||||
MessageSquare,
|
||||
Timer,
|
||||
Trash2,
|
||||
Save,
|
||||
Key,
|
||||
Link2,
|
||||
Clock,
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
|
|
@ -35,9 +30,6 @@ import type {
|
|||
Agent,
|
||||
AgentStatus,
|
||||
AgentVisibility,
|
||||
AgentTool,
|
||||
AgentTrigger,
|
||||
AgentTriggerType,
|
||||
AgentTask,
|
||||
RuntimeDevice,
|
||||
CreateAgentRequest,
|
||||
|
|
@ -151,10 +143,6 @@ function CreateAgentDialog({
|
|||
description: description.trim(),
|
||||
runtime_id: selectedRuntime.id,
|
||||
visibility,
|
||||
triggers: [
|
||||
{ id: generateId(), type: "on_assign", enabled: true, config: {} },
|
||||
{ id: generateId(), type: "on_comment", enabled: true, config: {} },
|
||||
],
|
||||
});
|
||||
onClose();
|
||||
} catch (err) {
|
||||
|
|
@ -600,466 +588,6 @@ function SkillsTab({
|
|||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tools Tab
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function AddToolDialog({
|
||||
onClose,
|
||||
onAdd,
|
||||
}: {
|
||||
onClose: () => void;
|
||||
onAdd: (tool: AgentTool) => void;
|
||||
}) {
|
||||
const [name, setName] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [authType, setAuthType] = useState<"oauth" | "api_key" | "none">("api_key");
|
||||
|
||||
const handleAdd = () => {
|
||||
if (!name.trim()) return;
|
||||
onAdd({
|
||||
id: generateId(),
|
||||
name: name.trim(),
|
||||
description: description.trim(),
|
||||
auth_type: authType,
|
||||
connected: false,
|
||||
config: {},
|
||||
});
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open onOpenChange={(v) => { if (!v) onClose(); }}>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-sm">Add Tool</DialogTitle>
|
||||
<DialogDescription className="text-xs">
|
||||
Connect an external tool for this agent to use.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<Label className="text-xs text-muted-foreground">Tool Name</Label>
|
||||
<Input
|
||||
autoFocus
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="e.g. Google Search, Slack, GitHub"
|
||||
className="mt-1"
|
||||
onKeyDown={(e) => e.key === "Enter" && handleAdd()}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs text-muted-foreground">Description</Label>
|
||||
<Input
|
||||
type="text"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder="What does this tool do?"
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs text-muted-foreground">Authentication</Label>
|
||||
<div className="mt-1.5 flex gap-2">
|
||||
{(["api_key", "oauth", "none"] as const).map((type) => (
|
||||
<Button
|
||||
key={type}
|
||||
variant={authType === type ? "outline" : "ghost"}
|
||||
size="xs"
|
||||
onClick={() => setAuthType(type)}
|
||||
className={`flex-1 ${
|
||||
authType === type
|
||||
? "border-primary bg-primary/5 font-medium"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{type === "api_key" ? "API Key" : type === "oauth" ? "OAuth" : "None"}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="ghost" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleAdd}
|
||||
disabled={!name.trim()}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function ToolsTab({
|
||||
agent,
|
||||
onSave,
|
||||
}: {
|
||||
agent: Agent;
|
||||
onSave: (tools: AgentTool[]) => Promise<void>;
|
||||
}) {
|
||||
const [tools, setTools] = useState<AgentTool[]>(agent.tools ?? []);
|
||||
const [showAdd, setShowAdd] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setTools(agent.tools ?? []);
|
||||
}, [agent.id, agent.tools]);
|
||||
|
||||
const isDirty = JSON.stringify(tools) !== JSON.stringify(agent.tools ?? []);
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
await onSave(tools);
|
||||
} catch {
|
||||
// toast handled by parent
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleConnect = (toolId: string) => {
|
||||
setTools((prev) =>
|
||||
prev.map((t) => (t.id === toolId ? { ...t, connected: !t.connected } : t)),
|
||||
);
|
||||
};
|
||||
|
||||
const removeTool = (toolId: string) => {
|
||||
setTools((prev) => prev.filter((t) => t.id !== toolId));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold">Tools</h3>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
External tools and APIs this agent can use during task execution.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{isDirty && (
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
size="xs"
|
||||
>
|
||||
<Save className="h-3 w-3" />
|
||||
{saving ? "Saving..." : "Save"}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="xs"
|
||||
onClick={() => setShowAdd(true)}
|
||||
>
|
||||
<Plus className="h-3 w-3" />
|
||||
Add Tool
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{tools.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed py-12">
|
||||
<Wrench className="h-8 w-8 text-muted-foreground/40" />
|
||||
<p className="mt-3 text-sm text-muted-foreground">No tools configured</p>
|
||||
<Button
|
||||
onClick={() => setShowAdd(true)}
|
||||
size="xs"
|
||||
className="mt-3"
|
||||
>
|
||||
<Plus className="h-3 w-3" />
|
||||
Add Tool
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{tools.map((tool) => (
|
||||
<div
|
||||
key={tool.id}
|
||||
className="flex items-center gap-3 rounded-lg border px-4 py-3"
|
||||
>
|
||||
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-muted">
|
||||
{tool.auth_type === "oauth" ? (
|
||||
<Link2 className="h-4 w-4 text-muted-foreground" />
|
||||
) : tool.auth_type === "api_key" ? (
|
||||
<Key className="h-4 w-4 text-muted-foreground" />
|
||||
) : (
|
||||
<Wrench className="h-4 w-4 text-muted-foreground" />
|
||||
)}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-sm font-medium">{tool.name}</div>
|
||||
{tool.description && (
|
||||
<div className="text-xs text-muted-foreground truncate">
|
||||
{tool.description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
onClick={() => toggleConnect(tool.id)}
|
||||
className={
|
||||
tool.connected
|
||||
? "bg-success/10 text-success"
|
||||
: "bg-muted text-muted-foreground hover:bg-accent"
|
||||
}
|
||||
>
|
||||
{tool.connected ? "Connected" : "Connect"}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
onClick={() => removeTool(tool.id)}
|
||||
className="text-muted-foreground hover:text-destructive"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showAdd && (
|
||||
<AddToolDialog
|
||||
onClose={() => setShowAdd(false)}
|
||||
onAdd={(tool) => setTools((prev) => [...prev, tool])}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Triggers Tab
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function TriggersTab({
|
||||
agent,
|
||||
onSave,
|
||||
}: {
|
||||
agent: Agent;
|
||||
onSave: (triggers: AgentTrigger[]) => Promise<void>;
|
||||
}) {
|
||||
const [triggers, setTriggers] = useState<AgentTrigger[]>(agent.triggers ?? []);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setTriggers(agent.triggers ?? []);
|
||||
}, [agent.id, agent.triggers]);
|
||||
|
||||
const isDirty = JSON.stringify(triggers) !== JSON.stringify(agent.triggers ?? []);
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
await onSave(triggers);
|
||||
} catch {
|
||||
// toast handled by parent
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleTrigger = (triggerId: string) => {
|
||||
setTriggers((prev) =>
|
||||
prev.map((t) => (t.id === triggerId ? { ...t, enabled: !t.enabled } : t)),
|
||||
);
|
||||
};
|
||||
|
||||
const removeTrigger = (triggerId: string) => {
|
||||
setTriggers((prev) => prev.filter((t) => t.id !== triggerId));
|
||||
};
|
||||
|
||||
const addTrigger = (type: AgentTriggerType) => {
|
||||
const newTrigger: AgentTrigger = {
|
||||
id: generateId(),
|
||||
type,
|
||||
enabled: true,
|
||||
config: type === "scheduled" ? { cron: "0 9 * * 1-5", timezone: "UTC" } : {},
|
||||
};
|
||||
setTriggers((prev) => [...prev, newTrigger]);
|
||||
};
|
||||
|
||||
const updateTriggerConfig = (triggerId: string, config: Record<string, unknown>) => {
|
||||
setTriggers((prev) =>
|
||||
prev.map((t) => (t.id === triggerId ? { ...t, config } : t)),
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold">Triggers</h3>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
Configure when this agent should start working.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{isDirty && (
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
size="xs"
|
||||
>
|
||||
<Save className="h-3 w-3" />
|
||||
{saving ? "Saving..." : "Save"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{triggers.map((trigger) => {
|
||||
const scheduledConfig = (trigger.config ?? {}) as {
|
||||
cron?: string;
|
||||
timezone?: string;
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
key={trigger.id}
|
||||
className="rounded-lg border px-4 py-3"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-muted">
|
||||
{trigger.type === "on_assign" ? (
|
||||
<Bot className="h-4 w-4 text-muted-foreground" />
|
||||
) : trigger.type === "on_comment" ? (
|
||||
<MessageSquare className="h-4 w-4 text-muted-foreground" />
|
||||
) : (
|
||||
<Timer className="h-4 w-4 text-muted-foreground" />
|
||||
)}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-sm font-medium">
|
||||
{trigger.type === "on_assign"
|
||||
? "On Issue Assign"
|
||||
: trigger.type === "on_comment"
|
||||
? "On Comment"
|
||||
: "Scheduled"}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{trigger.type === "on_assign"
|
||||
? "Runs when an issue is assigned to this agent"
|
||||
: trigger.type === "on_comment"
|
||||
? "Runs when a member comments on the agent's issue"
|
||||
: `Cron: ${scheduledConfig.cron ?? "Not set"}`}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => toggleTrigger(trigger.id)}
|
||||
className={`relative h-5 w-9 rounded-full transition-colors ${
|
||||
trigger.enabled ? "bg-primary" : "bg-muted"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`absolute top-0.5 h-4 w-4 rounded-full bg-white shadow-sm transition-transform ${
|
||||
trigger.enabled ? "left-4.5" : "left-0.5"
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
onClick={() => removeTrigger(trigger.id)}
|
||||
className="text-muted-foreground hover:text-destructive"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{trigger.type === "scheduled" && (
|
||||
<div className="mt-3 grid grid-cols-2 gap-3 pl-12">
|
||||
<div>
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
Cron Expression
|
||||
</Label>
|
||||
<Input
|
||||
type="text"
|
||||
value={scheduledConfig.cron ?? ""}
|
||||
onChange={(e) =>
|
||||
updateTriggerConfig(trigger.id, {
|
||||
...(trigger.config ?? {}),
|
||||
cron: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder="0 9 * * 1-5"
|
||||
className="mt-1 text-xs font-mono"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
Timezone
|
||||
</Label>
|
||||
<Input
|
||||
type="text"
|
||||
value={scheduledConfig.timezone ?? ""}
|
||||
onChange={(e) =>
|
||||
updateTriggerConfig(trigger.id, {
|
||||
...(trigger.config ?? {}),
|
||||
timezone: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder="UTC"
|
||||
className="mt-1 text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="xs"
|
||||
onClick={() => addTrigger("on_assign")}
|
||||
className="border-dashed text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<Bot className="h-3 w-3" />
|
||||
Add On Assign
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="xs"
|
||||
onClick={() => addTrigger("on_comment")}
|
||||
className="border-dashed text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<MessageSquare className="h-3 w-3" />
|
||||
Add On Comment
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="xs"
|
||||
onClick={() => addTrigger("scheduled")}
|
||||
className="border-dashed text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<Timer className="h-3 w-3" />
|
||||
Add Scheduled
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tasks Tab
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -1371,13 +899,11 @@ function SettingsTab({
|
|||
// Agent Detail
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type DetailTab = "instructions" | "skills" | "tools" | "triggers" | "tasks" | "settings";
|
||||
type DetailTab = "instructions" | "skills" | "tasks" | "settings";
|
||||
|
||||
const detailTabs: { id: DetailTab; label: string; icon: typeof FileText }[] = [
|
||||
{ id: "instructions", label: "Instructions", icon: FileText },
|
||||
{ id: "skills", label: "Skills", icon: BookOpenText },
|
||||
{ id: "tools", label: "Tools", icon: Wrench },
|
||||
{ id: "triggers", label: "Triggers", icon: Timer },
|
||||
{ id: "tasks", label: "Tasks", icon: ListTodo },
|
||||
{ id: "settings", label: "Settings", icon: Settings },
|
||||
];
|
||||
|
|
@ -1491,18 +1017,6 @@ function AgentDetail({
|
|||
{activeTab === "skills" && (
|
||||
<SkillsTab agent={agent} />
|
||||
)}
|
||||
{activeTab === "tools" && (
|
||||
<ToolsTab
|
||||
agent={agent}
|
||||
onSave={(tools) => onUpdate(agent.id, { tools })}
|
||||
/>
|
||||
)}
|
||||
{activeTab === "triggers" && (
|
||||
<TriggersTab
|
||||
agent={agent}
|
||||
onSave={(triggers) => onUpdate(agent.id, { triggers })}
|
||||
/>
|
||||
)}
|
||||
{activeTab === "tasks" && <TasksTab agent={agent} />}
|
||||
{activeTab === "settings" && (
|
||||
<SettingsTab
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@ export const en: LandingDict = {
|
|||
{
|
||||
title: "Create your first agent",
|
||||
description:
|
||||
"Give it a name, write instructions, attach skills, and set triggers. Choose when it activates: on assignment, on comment, or on mention.",
|
||||
"Give it a name, write instructions, and attach skills. Agents automatically activate on assignment, on comment, or on mention.",
|
||||
},
|
||||
{
|
||||
title: "Assign an issue and watch it work",
|
||||
|
|
@ -385,7 +385,7 @@ export const en: LandingDict = {
|
|||
title: "Core Platform",
|
||||
changes: [
|
||||
"Multi-workspace switching and creation",
|
||||
"Agent management UI with skills, tools, and triggers",
|
||||
"Agent management UI with skills",
|
||||
"Unified agent SDK supporting Claude Code and Codex backends",
|
||||
"Comment CRUD with real-time WebSocket updates",
|
||||
"Task service layer and daemon REST protocol",
|
||||
|
|
|
|||
|
|
@ -4,8 +4,6 @@ export type AgentRuntimeMode = "local" | "cloud";
|
|||
|
||||
export type AgentVisibility = "workspace" | "private";
|
||||
|
||||
export type AgentTriggerType = "on_assign" | "on_comment" | "scheduled";
|
||||
|
||||
export interface RuntimeDevice {
|
||||
id: string;
|
||||
workspace_id: string;
|
||||
|
|
@ -23,22 +21,6 @@ export interface RuntimeDevice {
|
|||
|
||||
export type AgentRuntime = RuntimeDevice;
|
||||
|
||||
export interface AgentTool {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
auth_type: "oauth" | "api_key" | "none";
|
||||
connected: boolean;
|
||||
config: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface AgentTrigger {
|
||||
id: string;
|
||||
type: AgentTriggerType;
|
||||
enabled: boolean;
|
||||
config: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
export interface AgentTask {
|
||||
id: string;
|
||||
agent_id: string;
|
||||
|
|
@ -69,8 +51,6 @@ export interface Agent {
|
|||
max_concurrent_tasks: number;
|
||||
owner_id: string | null;
|
||||
skills: Skill[];
|
||||
tools: AgentTool[];
|
||||
triggers: AgentTrigger[];
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
archived_at: string | null;
|
||||
|
|
@ -86,8 +66,6 @@ export interface CreateAgentRequest {
|
|||
runtime_config?: Record<string, unknown>;
|
||||
visibility?: AgentVisibility;
|
||||
max_concurrent_tasks?: number;
|
||||
tools?: AgentTool[];
|
||||
triggers?: AgentTrigger[];
|
||||
}
|
||||
|
||||
export interface UpdateAgentRequest {
|
||||
|
|
@ -100,8 +78,6 @@ export interface UpdateAgentRequest {
|
|||
visibility?: AgentVisibility;
|
||||
status?: AgentStatus;
|
||||
max_concurrent_tasks?: number;
|
||||
tools?: AgentTool[];
|
||||
triggers?: AgentTrigger[];
|
||||
}
|
||||
|
||||
// Skills
|
||||
|
|
|
|||
|
|
@ -4,9 +4,6 @@ export type {
|
|||
AgentStatus,
|
||||
AgentRuntimeMode,
|
||||
AgentVisibility,
|
||||
AgentTriggerType,
|
||||
AgentTool,
|
||||
AgentTrigger,
|
||||
AgentTask,
|
||||
AgentRuntime,
|
||||
RuntimeDevice,
|
||||
|
|
|
|||
|
|
@ -58,8 +58,6 @@ export const mockAgents: Agent[] = [
|
|||
max_concurrent_tasks: 3,
|
||||
owner_id: null,
|
||||
skills: [],
|
||||
tools: [],
|
||||
triggers: [],
|
||||
created_at: "2026-01-01T00:00:00Z",
|
||||
updated_at: "2026-01-01T00:00:00Z",
|
||||
archived_at: null,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue