mirror of
https://github.com/multica-ai/multica.git
synced 2026-06-17 19:59:20 +02:00
Compare commits
2 Commits
quick-crea
...
agent/lamb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42c77406f6 | ||
|
|
b9bd54d6d5 |
@@ -1376,17 +1376,20 @@ function AgentDetail({
|
||||
onUpdate,
|
||||
onArchive,
|
||||
onRestore,
|
||||
onDelete,
|
||||
}: {
|
||||
agent: Agent;
|
||||
runtimes: RuntimeDevice[];
|
||||
onUpdate: (id: string, data: Partial<Agent>) => Promise<void>;
|
||||
onArchive: (id: string) => Promise<void>;
|
||||
onRestore: (id: string) => Promise<void>;
|
||||
onDelete: (id: string) => Promise<void>;
|
||||
}) {
|
||||
const st = statusConfig[agent.status];
|
||||
const runtimeDevice = getRuntimeDevice(agent, runtimes);
|
||||
const [activeTab, setActiveTab] = useState<DetailTab>("instructions");
|
||||
const [confirmArchive, setConfirmArchive] = useState(false);
|
||||
const [confirmDelete, setConfirmDelete] = useState(false);
|
||||
const isArchived = !!agent.archived_at;
|
||||
|
||||
return (
|
||||
@@ -1428,8 +1431,7 @@ function AgentDetail({
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{!isArchived && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<Button variant="ghost" size="icon-sm" />
|
||||
@@ -1438,16 +1440,21 @@ function AgentDetail({
|
||||
<MoreHorizontal className="h-4 w-4 text-muted-foreground" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
{!isArchived && (
|
||||
<DropdownMenuItem onClick={() => setConfirmArchive(true)}>
|
||||
<Archive className="h-3.5 w-3.5" />
|
||||
Archive Agent
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
className="text-destructive"
|
||||
onClick={() => setConfirmArchive(true)}
|
||||
onClick={() => setConfirmDelete(true)}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
Archive Agent
|
||||
Delete Agent
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
@@ -1533,6 +1540,39 @@ function AgentDetail({
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
|
||||
{/* Delete Confirmation */}
|
||||
{confirmDelete && (
|
||||
<Dialog open onOpenChange={(v) => { if (!v) setConfirmDelete(false); }}>
|
||||
<DialogContent className="max-w-sm" showCloseButton={false}>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-destructive/10">
|
||||
<AlertCircle className="h-5 w-5 text-destructive" />
|
||||
</div>
|
||||
<DialogHeader className="flex-1 gap-1">
|
||||
<DialogTitle className="text-sm font-semibold">Delete agent?</DialogTitle>
|
||||
<DialogDescription className="text-xs">
|
||||
This will permanently delete "{agent.name}" and all its configuration.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="ghost" onClick={() => setConfirmDelete(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => {
|
||||
setConfirmDelete(false);
|
||||
onDelete(agent.id);
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1610,6 +1650,20 @@ export default function AgentsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
try {
|
||||
await api.deleteAgent(id);
|
||||
if (selectedId === id) {
|
||||
const remaining = agents.filter((a) => a.id !== id);
|
||||
setSelectedId(remaining[0]?.id ?? "");
|
||||
}
|
||||
await refreshAgents();
|
||||
toast.success("Agent deleted");
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : "Failed to delete agent");
|
||||
}
|
||||
};
|
||||
|
||||
const selected = agents.find((a) => a.id === selectedId) ?? null;
|
||||
|
||||
if (isLoading) {
|
||||
@@ -1728,6 +1782,7 @@ export default function AgentsPage() {
|
||||
onUpdate={handleUpdate}
|
||||
onArchive={handleArchive}
|
||||
onRestore={handleRestore}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-full flex-col items-center justify-center text-muted-foreground">
|
||||
|
||||
@@ -513,7 +513,7 @@ export function IssueDetail({ issueId, onDelete, defaultSidebarOpen = true, layo
|
||||
{issue.assignee_type === "member" && issue.assignee_id === m.user_id && <span className="ml-auto text-xs text-muted-foreground">✓</span>}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
{agents.filter((a) => canAssignAgent(a, user?.id, currentMemberRole)).map((a) => (
|
||||
{agents.filter((a) => !a.archived_at && canAssignAgent(a, user?.id, currentMemberRole)).map((a) => (
|
||||
<DropdownMenuItem
|
||||
key={a.id}
|
||||
onClick={() => handleUpdateField({ assignee_type: "agent", assignee_id: a.id })}
|
||||
@@ -742,9 +742,9 @@ export function IssueDetail({ issueId, onDelete, defaultSidebarOpen = true, layo
|
||||
})}
|
||||
</CommandGroup>
|
||||
)}
|
||||
{agents.length > 0 && (
|
||||
{agents.filter((a) => !a.archived_at).length > 0 && (
|
||||
<CommandGroup heading="Agents">
|
||||
{agents.map((a) => {
|
||||
{agents.filter((a) => !a.archived_at).map((a) => {
|
||||
const sub = subscribers.find((s) => s.user_type === "agent" && s.user_id === a.id);
|
||||
const isSubbed = !!sub;
|
||||
return (
|
||||
|
||||
@@ -162,7 +162,7 @@ function ActorSubContent({
|
||||
m.name.toLowerCase().includes(query),
|
||||
);
|
||||
const filteredAgents = agents.filter((a) =>
|
||||
a.name.toLowerCase().includes(query),
|
||||
!a.archived_at && a.name.toLowerCase().includes(query),
|
||||
);
|
||||
|
||||
const isSelected = (type: "member" | "agent", id: string) =>
|
||||
|
||||
@@ -99,7 +99,7 @@ export function CreateIssueModal({ onClose, data }: { onClose: () => void; data?
|
||||
|
||||
const assigneeQuery = assigneeFilter.toLowerCase();
|
||||
const filteredMembers = members.filter((m) => m.name.toLowerCase().includes(assigneeQuery));
|
||||
const filteredAgents = agents.filter((a) => a.name.toLowerCase().includes(assigneeQuery));
|
||||
const filteredAgents = agents.filter((a) => !a.archived_at && a.name.toLowerCase().includes(assigneeQuery));
|
||||
|
||||
const assigneeLabel =
|
||||
assigneeType && assigneeId
|
||||
|
||||
@@ -325,6 +325,10 @@ export class ApiClient {
|
||||
return this.fetch(`/api/agents/${id}/restore`, { method: "POST" });
|
||||
}
|
||||
|
||||
async deleteAgent(id: string): Promise<void> {
|
||||
await this.fetch(`/api/agents/${id}`, { method: "DELETE" });
|
||||
}
|
||||
|
||||
async listRuntimes(params?: { workspace_id?: string }): Promise<AgentRuntime[]> {
|
||||
const search = new URLSearchParams();
|
||||
const wsId = params?.workspace_id ?? this.workspaceId;
|
||||
|
||||
@@ -71,10 +71,10 @@ export interface Agent {
|
||||
skills: Skill[];
|
||||
tools: AgentTool[];
|
||||
triggers: AgentTrigger[];
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
archived_at: string | null;
|
||||
archived_by: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface CreateAgentRequest {
|
||||
|
||||
@@ -60,10 +60,10 @@ export const mockAgents: Agent[] = [
|
||||
skills: [],
|
||||
tools: [],
|
||||
triggers: [],
|
||||
created_at: "2026-01-01T00:00:00Z",
|
||||
updated_at: "2026-01-01T00:00:00Z",
|
||||
archived_at: null,
|
||||
archived_by: null,
|
||||
created_at: "2026-01-01T00:00:00Z",
|
||||
updated_at: "2026-01-01T00:00:00Z",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -197,6 +197,7 @@ func NewRouter(pool *pgxpool.Pool, hub *realtime.Hub, bus *events.Bus) chi.Route
|
||||
r.Route("/{id}", func(r chi.Router) {
|
||||
r.Get("/", h.GetAgent)
|
||||
r.Put("/", h.UpdateAgent)
|
||||
r.Delete("/", h.DeleteAgent)
|
||||
r.Post("/archive", h.ArchiveAgent)
|
||||
r.Post("/restore", h.RestoreAgent)
|
||||
r.Get("/tasks", h.ListAgentTasks)
|
||||
|
||||
@@ -30,10 +30,10 @@ type AgentResponse struct {
|
||||
Skills []SkillResponse `json:"skills"`
|
||||
Tools any `json:"tools"`
|
||||
Triggers any `json:"triggers"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
ArchivedAt *string `json:"archived_at"`
|
||||
ArchivedBy *string `json:"archived_by"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
func agentToResponse(a db.Agent) AgentResponse {
|
||||
@@ -78,10 +78,10 @@ func agentToResponse(a db.Agent) AgentResponse {
|
||||
Skills: []SkillResponse{},
|
||||
Tools: tools,
|
||||
Triggers: triggers,
|
||||
CreatedAt: timestampToString(a.CreatedAt),
|
||||
UpdatedAt: timestampToString(a.UpdatedAt),
|
||||
ArchivedAt: timestampToPtr(a.ArchivedAt),
|
||||
ArchivedBy: uuidToPtr(a.ArchivedBy),
|
||||
CreatedAt: timestampToString(a.CreatedAt),
|
||||
UpdatedAt: timestampToString(a.UpdatedAt),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -493,6 +493,32 @@ func (h *Handler) RestoreAgent(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, resp)
|
||||
}
|
||||
|
||||
func (h *Handler) DeleteAgent(w http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, "id")
|
||||
agent, ok := h.loadAgentForUser(w, r, id)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
wsID := uuidToString(agent.WorkspaceID)
|
||||
|
||||
if !h.canManageAgent(w, r, agent) {
|
||||
return
|
||||
}
|
||||
|
||||
err := h.Queries.DeleteAgent(r.Context(), parseUUID(id))
|
||||
if err != nil {
|
||||
slog.Warn("delete agent failed", append(logger.RequestAttrs(r), "error", err, "agent_id", id)...)
|
||||
writeError(w, http.StatusInternalServerError, "failed to delete agent")
|
||||
return
|
||||
}
|
||||
|
||||
slog.Info("agent deleted", append(logger.RequestAttrs(r), "agent_id", id, "workspace_id", wsID)...)
|
||||
userID := requestUserID(r)
|
||||
actorType, actorID := h.resolveActor(r, userID, wsID)
|
||||
h.publish(protocol.EventAgentDeleted, wsID, actorType, actorID, map[string]any{"agent_id": id, "workspace_id": wsID})
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (h *Handler) ListAgentTasks(w http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, "id")
|
||||
if _, ok := h.loadAgentForUser(w, r, id); !ok {
|
||||
|
||||
@@ -313,6 +313,15 @@ func (q *Queries) CreateAgentTask(ctx context.Context, arg CreateAgentTaskParams
|
||||
return i, err
|
||||
}
|
||||
|
||||
const deleteAgent = `-- name: DeleteAgent :exec
|
||||
DELETE FROM agent WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteAgent(ctx context.Context, id pgtype.UUID) error {
|
||||
_, err := q.db.Exec(ctx, deleteAgent, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const failAgentTask = `-- name: FailAgentTask :one
|
||||
UPDATE agent_task_queue
|
||||
SET status = 'failed', completed_at = now(), error = $2
|
||||
|
||||
@@ -52,6 +52,9 @@ UPDATE agent SET archived_at = NULL, archived_by = NULL, updated_at = now()
|
||||
WHERE id = $1
|
||||
RETURNING *;
|
||||
|
||||
-- name: DeleteAgent :exec
|
||||
DELETE FROM agent WHERE id = $1;
|
||||
|
||||
-- name: ListAgentTasks :many
|
||||
SELECT * FROM agent_task_queue
|
||||
WHERE agent_id = $1
|
||||
|
||||
@@ -19,6 +19,7 @@ const (
|
||||
// Agent events
|
||||
EventAgentStatus = "agent:status"
|
||||
EventAgentCreated = "agent:created"
|
||||
EventAgentDeleted = "agent:deleted"
|
||||
EventAgentArchived = "agent:archived"
|
||||
EventAgentRestored = "agent:restored"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user