mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-03 05:20:01 +00:00
feat(ui): add list view and creator filter to agents page
Add card/list view toggle and owner filter dropdown to the agents page. New AgentListRow component for compact list display. i18n keys added for en, vi, zh locales.
This commit is contained in:
1 parent
ddd4565380
commit
d2bd270968
6 files changed
+244
-164
No files matched your search
@@ -1,136 +0,0 @@
|
||||
# Brainstorm: Discord Guild-User Memory Sharing
|
||||
|
||||
**Date:** 2026-03-12
|
||||
**Branch:** feat/discord-guild-user-memory
|
||||
**Status:** Đề xuất — chờ implement
|
||||
|
||||
---
|
||||
|
||||
## Vấn đề
|
||||
|
||||
GoClaw bot khi được add vào Discord server hiện tại có bộ nhớ tách biệt theo từng user trong cùng server: mỗi khi user chuyển sang channel khác, bot không nhớ tên, cách xưng hô, ngôn ngữ, preferences. User phải tự giới thiệu lại từ đầu.
|
||||
|
||||
**Discord khác các platform khác:**
|
||||
- Telegram: 1 bot = nhiều DM chat riêng biệt, mỗi chat có identity độc lập → không bị vấn đề này
|
||||
- Discord: 1 server → nhiều channels → cùng 1 danh sách members → user mong muốn bot nhớ họ xuyên suốt các channels
|
||||
|
||||
---
|
||||
|
||||
## Root Cause (Cập nhật sau phân tích sâu)
|
||||
|
||||
Ban đầu nghĩ vấn đề là per-channel scoping. Sau khi đọc `cmd/gateway_consumer.go:97-110`, phát hiện code hiện tại **đã** có Discord-specific guild_id handling:
|
||||
|
||||
```go
|
||||
// Group-scoped UserID — For Discord: use guild_id so all channels
|
||||
// in the same server share context files, memory, and seeding.
|
||||
userID := msg.UserID
|
||||
if peerKind == string(sessions.PeerGroup) && msg.ChatID != "" {
|
||||
groupID := msg.ChatID
|
||||
if guildID := msg.Metadata["guild_id"]; guildID != "" {
|
||||
groupID = guildID // Discord: dùng guild_id thay vì channelID
|
||||
}
|
||||
userID = fmt.Sprintf("group:%s:%s", msg.Channel, groupID)
|
||||
}
|
||||
```
|
||||
|
||||
→ Discord groups hiện đang dùng `userID = "group:discord:{guildID}"` — context files đã được **share across channels trong cùng server**.
|
||||
|
||||
**Root cause thực sự:** `"group:discord:{guildID}"` là scope **shared bởi tất cả users** trong server. Không có per-individual separation. User A setup → User B ghi đè USER.md → mất thông tin cá nhân.
|
||||
|
||||
**Scope mapping hiện tại:**
|
||||
|
||||
| Platform | Group userID | Per-individual? |
|
||||
|---|---|---|
|
||||
| Telegram | `group:telegram:{chatID}` | ✗ shared bởi all users |
|
||||
| Discord | `group:discord:{guildID}` | ✗ shared bởi all users |
|
||||
| Zalo, Slack... | `group:{channel}:{chatID}` | ✗ shared bởi all users |
|
||||
|
||||
**Files liên quan:**
|
||||
- `cmd/gateway_consumer.go:97-110` — nơi build userID cho group sessions (điểm thay đổi chính)
|
||||
- `internal/tools/context_file_interceptor.go:250` — permission check dùng `"group:"` prefix
|
||||
- `internal/store/context.go` — `UserIDKey`, `SenderIDKey` context keys
|
||||
|
||||
---
|
||||
|
||||
## Yêu cầu đã xác định
|
||||
|
||||
- **Core problem:** Phải tự giới thiệu lại, bot không nhớ tên/preferences khi sang channel khác trong cùng server
|
||||
- **Memory scope:** Per-user trong cùng guild (Server A ≠ Server B, User A ≠ User B)
|
||||
- **Conversation history:** Giữ riêng từng channel (không share)
|
||||
- **Profile loading:** Active sender (mỗi message load profile của người đang gửi)
|
||||
- **DM ↔ Group:** Share cùng profile (Phase 2)
|
||||
|
||||
---
|
||||
|
||||
## Giải pháp đề xuất
|
||||
|
||||
### Phase 1: Guild-User scoped userID (Core Fix)
|
||||
|
||||
**Thay đổi duy nhất:** `cmd/gateway_consumer.go` (~3 dòng), chỉ ảnh hưởng Discord:
|
||||
|
||||
```go
|
||||
if peerKind == string(sessions.PeerGroup) && msg.ChatID != "" {
|
||||
if guildID := msg.Metadata["guild_id"]; guildID != "" && msg.SenderID != "" {
|
||||
// Discord: per-guild-user (cá nhân, không shared)
|
||||
userID = fmt.Sprintf("guild:%s:user:%s", guildID, msg.SenderID)
|
||||
} else {
|
||||
groupID := msg.ChatID
|
||||
userID = fmt.Sprintf("group:%s:%s", msg.Channel, groupID)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Thêm update nhỏ trong `context_file_interceptor.go:250` để handle `"guild:"` prefix song song với `"group:"` trong permission check.
|
||||
|
||||
**Behavior mới:**
|
||||
|
||||
| Scenario | Session History | Profile (USER.md) |
|
||||
|---|---|---|
|
||||
| User A ở #general | `agent:X:discord:group:{channelA}` | `guild:G1:user:A` ← shared |
|
||||
| User A ở #tech | `agent:X:discord:group:{channelB}` | `guild:G1:user:A` ← same ✓ |
|
||||
| User B ở #general | `agent:X:discord:group:{channelA}` | `guild:G1:user:B` ← isolated ✓ |
|
||||
| User A ở Server khác | `agent:X:discord:group:{channelX}` | `guild:G2:user:A` ← isolated ✓ |
|
||||
|
||||
**Zero new infrastructure** — tận dụng hoàn toàn `user_context_files` table và `ContextFileInterceptor` hiện có.
|
||||
|
||||
**Ảnh hưởng các channel khác: KHÔNG** — Telegram, Zalo, Feishu, Slack không có `guild_id` trong metadata → luồng else → giữ nguyên `"group:{channel}:{chatID}"`.
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: Session Participants Index (Enhancement)
|
||||
|
||||
Mỗi message trong group channel chỉ inject USER.md của active sender. Để bot biết về **tất cả users đã chat** trong session, cần thêm:
|
||||
|
||||
- `participants []string` vào session metadata, cập nhật mỗi khi có message từ user mới
|
||||
- Khi build system context: batch-load USER.mds của tất cả participants → inject dạng:
|
||||
|
||||
```
|
||||
[Known participants in this channel]
|
||||
- Alice (user:123): Người Việt, prefer tiếng Việt, senior developer
|
||||
- Bob (user:456): English speaker, prefer concise answers
|
||||
```
|
||||
|
||||
**Tại sao cần Phase 2 riêng:** `providers.Message` không có `SenderID` field → không thể extract users từ session history. Cần participants metadata riêng.
|
||||
|
||||
**Context overhead:** ~10 users × 300 tokens = 3000 tokens (negligible với 200k context window)
|
||||
|
||||
---
|
||||
|
||||
## Security Analysis
|
||||
|
||||
| Risk | Mitigation |
|
||||
|---|---|
|
||||
| User X đọc profile của User Y | Impossible: key include `senderID` |
|
||||
| Server A leak sang Server B | `guild:{guildID}` prefix đảm bảo isolation |
|
||||
| Unauthorized writes to SOUL.md | `protectedFileSet` check vẫn giữ — thêm `"guild:"` prefix vào điều kiện |
|
||||
| Agent A leak sang Agent B | `agent_id` vẫn là primary key trong `user_context_files` |
|
||||
|
||||
---
|
||||
|
||||
## Unresolved Questions
|
||||
|
||||
1. **Backward compatibility:** Users hiện tại đã setup trong Discord groups → data cũ lưu theo `"group:discord:{guildID}"` (shared). Sau khi đổi sang per-user scope, data cũ sẽ bị "orphan". Cần migration script hoặc fallback lookup?
|
||||
|
||||
2. **DM ↔ Group bridging (Phase 2):** Discord DMs không có `guildID` → userID vẫn là `senderID` cho DMs. Hai scope khác nhau (DM vs group) nhưng user mong muốn nhất quán. Approach: lookup guild profile khi DM?
|
||||
|
||||
3. **Multi-agent Discord:** Cùng 1 Discord server có 2 agents → per-agent isolation via `agent_id` → user phải giới thiệu lại với mỗi agent. Acceptable?
|
||||
@@ -7,6 +7,9 @@
|
||||
"emptyDescription": "Create your first agent to get started.",
|
||||
"noMatchTitle": "No matching agents",
|
||||
"noMatchDescription": "Try a different search term.",
|
||||
"viewCard": "Card view",
|
||||
"viewList": "List view",
|
||||
"allCreators": "All creators",
|
||||
"presets": {
|
||||
"foxSpirit": {
|
||||
"label": "🐱 Fox Spirit",
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
"emptyDescription": "Tạo agent đầu tiên để bắt đầu.",
|
||||
"noMatchTitle": "Không tìm thấy agent phù hợp",
|
||||
"noMatchDescription": "Thử từ khóa tìm kiếm khác.",
|
||||
"viewCard": "Dạng thẻ",
|
||||
"viewList": "Dạng danh sách",
|
||||
"allCreators": "Tất cả người tạo",
|
||||
"presets": {
|
||||
"foxSpirit": {
|
||||
"label": "🐱 Tiểu Hồ",
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
"emptyDescription": "创建您的第一个Agent以开始使用。",
|
||||
"noMatchTitle": "未找到匹配的Agent",
|
||||
"noMatchDescription": "请尝试其他搜索词。",
|
||||
"viewCard": "卡片视图",
|
||||
"viewList": "列表视图",
|
||||
"allCreators": "所有创建者",
|
||||
"presets": {
|
||||
"foxSpirit": {
|
||||
"label": "🐱 小狐",
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import { Bot, Star, Trash2, RotateCcw, Sparkles } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import type { AgentData } from "@/types/agent";
|
||||
|
||||
interface AgentListRowProps {
|
||||
agent: AgentData;
|
||||
ownerName?: string;
|
||||
onClick: () => void;
|
||||
onResummon?: () => void;
|
||||
onDelete?: () => void;
|
||||
}
|
||||
|
||||
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
|
||||
export function AgentListRow({ agent, ownerName, onClick, onResummon, onDelete }: AgentListRowProps) {
|
||||
const { t } = useTranslation("agents");
|
||||
const displayName = agent.display_name
|
||||
|| (UUID_RE.test(agent.agent_key) ? t("card.unnamedAgent") : agent.agent_key);
|
||||
const selfEvolve = agent.agent_type === "predefined" && Boolean((agent.other_config as Record<string, unknown> | null)?.self_evolve);
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className="flex w-full cursor-pointer items-center gap-3 rounded-lg border bg-card px-4 py-3 text-left transition-all hover:border-primary/30 hover:shadow-sm"
|
||||
>
|
||||
{/* Icon */}
|
||||
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary">
|
||||
<Bot className="h-4 w-4" />
|
||||
</div>
|
||||
|
||||
{/* Name + key */}
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="truncate text-sm font-semibold">{displayName}</span>
|
||||
{agent.is_default && <Star className="h-3 w-3 shrink-0 fill-amber-400 text-amber-400" />}
|
||||
</div>
|
||||
{agent.display_name && !UUID_RE.test(agent.agent_key) && (
|
||||
<div className="truncate text-xs text-muted-foreground">{agent.agent_key}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Status */}
|
||||
<div className="hidden shrink-0 sm:block">
|
||||
{agent.status === "summoning" ? (
|
||||
<Badge variant="outline" className="animate-pulse border-violet-400 text-violet-600 dark:text-violet-400">
|
||||
{t("card.summoning")}
|
||||
</Badge>
|
||||
) : agent.status === "summon_failed" ? (
|
||||
<Badge variant="destructive">{t("card.summonFailed")}</Badge>
|
||||
) : (
|
||||
<Badge variant={agent.status === "active" ? "success" : "secondary"}>{agent.status}</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Model */}
|
||||
<div className="hidden shrink-0 text-xs text-muted-foreground md:block md:w-40 md:truncate">
|
||||
{[agent.provider, agent.model].filter(Boolean).join(" / ")}
|
||||
</div>
|
||||
|
||||
{/* Type + evolve */}
|
||||
<div className="hidden shrink-0 items-center gap-1 lg:flex">
|
||||
<Badge variant="outline" className="text-[11px]">{agent.agent_type}</Badge>
|
||||
{selfEvolve && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Badge className="bg-violet-100 text-[11px] text-violet-700 hover:bg-violet-100 dark:bg-violet-900/30 dark:text-violet-300">
|
||||
<Sparkles className="mr-0.5 h-3 w-3" />
|
||||
{t("card.evolving")}
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" className="max-w-[240px] text-xs">
|
||||
{t("card.evolvingTooltip")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Owner */}
|
||||
{ownerName && (
|
||||
<div className="hidden shrink-0 text-xs text-muted-foreground xl:block xl:w-28 xl:truncate">
|
||||
{ownerName}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Context window */}
|
||||
{agent.context_window > 0 && (
|
||||
<span className="hidden shrink-0 text-[11px] text-muted-foreground lg:block">
|
||||
{(agent.context_window / 1000).toFixed(0)}K
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex shrink-0 items-center gap-1">
|
||||
{agent.status === "summon_failed" && onResummon && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="xs"
|
||||
onClick={(e) => { e.stopPropagation(); onResummon(); }}
|
||||
>
|
||||
<RotateCcw className="h-3 w-3" />
|
||||
</Button>
|
||||
)}
|
||||
{onDelete && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
className="text-muted-foreground hover:text-destructive"
|
||||
onClick={(e) => { e.stopPropagation(); onDelete(); }}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { useParams, useNavigate } from "react-router";
|
||||
import { Plus, Bot } from "lucide-react";
|
||||
import { Plus, Bot, LayoutGrid, List } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { PageHeader } from "@/components/shared/page-header";
|
||||
import { EmptyState } from "@/components/shared/empty-state";
|
||||
@@ -9,10 +9,19 @@ import { Pagination } from "@/components/shared/pagination";
|
||||
import { CardSkeleton } from "@/components/shared/loading-skeleton";
|
||||
import { useDeferredLoading } from "@/hooks/use-deferred-loading";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip";
|
||||
import { ConfirmDialog } from "@/components/shared/confirm-dialog";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { useContactResolver } from "@/hooks/use-contact-resolver";
|
||||
import { useAgents } from "./hooks/use-agents";
|
||||
import { AgentCard } from "./agent-card";
|
||||
import { AgentListRow } from "./agent-list-row";
|
||||
import { AgentCreateDialog } from "./agent-create-dialog";
|
||||
import { AgentDetailPage } from "./agent-detail/agent-detail-page";
|
||||
import { SummoningModal } from "./summoning-modal";
|
||||
@@ -26,10 +35,16 @@ export function AgentsPage() {
|
||||
const showSkeleton = useDeferredLoading(loading && agents.length === 0);
|
||||
|
||||
const [search, setSearch] = useState("");
|
||||
const [viewMode, setViewMode] = useState<"card" | "list">("card");
|
||||
const [ownerFilter, setOwnerFilter] = useState<string | undefined>();
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
const [deleteTarget, setDeleteTarget] = useState<string | null>(null);
|
||||
const [summoningAgent, setSummoningAgent] = useState<{ id: string; name: string } | null>(null);
|
||||
|
||||
// Collect unique owner IDs for filter + contact resolution
|
||||
const ownerIDs = useMemo(() => [...new Set(agents.map((a) => a.owner_id).filter(Boolean))], [agents]);
|
||||
const { resolve } = useContactResolver(ownerIDs);
|
||||
|
||||
const handleResummon = async (agent: { id: string; display_name?: string; agent_key: string }) => {
|
||||
try {
|
||||
await resummonAgent(agent.id);
|
||||
@@ -50,6 +65,7 @@ export function AgentsPage() {
|
||||
}
|
||||
|
||||
const filtered = agents.filter((a) => {
|
||||
if (ownerFilter && a.owner_id !== ownerFilter) return false;
|
||||
const q = search.toLowerCase();
|
||||
return (
|
||||
a.agent_key.toLowerCase().includes(q) ||
|
||||
@@ -59,7 +75,20 @@ export function AgentsPage() {
|
||||
|
||||
const { pageItems, pagination, setPage, setPageSize, resetPage } = usePagination(filtered);
|
||||
|
||||
useEffect(() => { resetPage(); }, [search, resetPage]);
|
||||
useEffect(() => { resetPage(); }, [search, ownerFilter, resetPage]);
|
||||
|
||||
const resolveOwnerName = (id: string) => {
|
||||
const contact = resolve(id);
|
||||
return contact?.display_name || contact?.username || id;
|
||||
};
|
||||
|
||||
const handleClick = (agent: { id: string; display_name?: string; agent_key: string; status: string }) => {
|
||||
if (agent.status === "summoning") {
|
||||
setSummoningAgent({ id: agent.id, name: agent.display_name || agent.agent_key });
|
||||
} else {
|
||||
navigate(`/agents/${agent.id}`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-4 sm:p-6">
|
||||
@@ -73,13 +102,66 @@ export function AgentsPage() {
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="mt-4">
|
||||
{/* Toolbar: search + creator filter + view toggle */}
|
||||
<div className="mt-4 flex flex-wrap items-center gap-2">
|
||||
<SearchInput
|
||||
value={search}
|
||||
onChange={setSearch}
|
||||
placeholder={t("searchPlaceholder")}
|
||||
className="max-w-sm"
|
||||
/>
|
||||
|
||||
{/* Creator filter */}
|
||||
{ownerIDs.length > 0 && (
|
||||
<Select
|
||||
value={ownerFilter ?? "__all__"}
|
||||
onValueChange={(v) => setOwnerFilter(v === "__all__" ? undefined : v)}
|
||||
>
|
||||
<SelectTrigger className="h-9 w-44 text-xs">
|
||||
<SelectValue placeholder={t("allCreators")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__all__">{t("allCreators")}</SelectItem>
|
||||
{ownerIDs.map((id) => (
|
||||
<SelectItem key={id} value={id}>
|
||||
{resolveOwnerName(id)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
|
||||
{/* View toggle */}
|
||||
<TooltipProvider>
|
||||
<div className="ml-auto flex items-center gap-0.5 rounded-md border p-0.5">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant={viewMode === "card" ? "default" : "ghost"}
|
||||
size="xs"
|
||||
className="h-7 w-7 p-0"
|
||||
onClick={() => setViewMode("card")}
|
||||
>
|
||||
<LayoutGrid className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t("viewCard")}</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant={viewMode === "list" ? "default" : "ghost"}
|
||||
size="xs"
|
||||
className="h-7 w-7 p-0"
|
||||
onClick={() => setViewMode("list")}
|
||||
>
|
||||
<List className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t("viewList")}</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
@@ -92,9 +174,9 @@ export function AgentsPage() {
|
||||
) : filtered.length === 0 ? (
|
||||
<EmptyState
|
||||
icon={Bot}
|
||||
title={search ? t("noMatchTitle") : t("emptyTitle")}
|
||||
title={search || ownerFilter ? t("noMatchTitle") : t("emptyTitle")}
|
||||
description={
|
||||
search
|
||||
search || ownerFilter
|
||||
? t("noMatchDescription")
|
||||
: t("emptyDescription")
|
||||
}
|
||||
@@ -102,26 +184,32 @@ export function AgentsPage() {
|
||||
) : (
|
||||
<>
|
||||
<TooltipProvider>
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{pageItems.map((agent) => (
|
||||
<AgentCard
|
||||
key={agent.id}
|
||||
agent={agent}
|
||||
onClick={() => {
|
||||
if (agent.status === "summoning") {
|
||||
setSummoningAgent({
|
||||
id: agent.id,
|
||||
name: agent.display_name || agent.agent_key,
|
||||
});
|
||||
} else {
|
||||
navigate(`/agents/${agent.id}`);
|
||||
}
|
||||
}}
|
||||
onResummon={() => handleResummon(agent)}
|
||||
onDelete={() => setDeleteTarget(agent.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{viewMode === "card" ? (
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{pageItems.map((agent) => (
|
||||
<AgentCard
|
||||
key={agent.id}
|
||||
agent={agent}
|
||||
onClick={() => handleClick(agent)}
|
||||
onResummon={() => handleResummon(agent)}
|
||||
onDelete={() => setDeleteTarget(agent.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-2">
|
||||
{pageItems.map((agent) => (
|
||||
<AgentListRow
|
||||
key={agent.id}
|
||||
agent={agent}
|
||||
ownerName={resolveOwnerName(agent.owner_id)}
|
||||
onClick={() => handleClick(agent)}
|
||||
onResummon={() => handleResummon(agent)}
|
||||
onDelete={() => setDeleteTarget(agent.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</TooltipProvider>
|
||||
<div className="mt-4">
|
||||
<Pagination
|
||||
@@ -143,7 +231,6 @@ export function AgentsPage() {
|
||||
onCreate={async (data) => {
|
||||
const created = await createAgent(data);
|
||||
refresh();
|
||||
// Auto-show summoning modal if agent is being summoned
|
||||
if (created && typeof created === "object" && "status" in created && created.status === "summoning") {
|
||||
const ag = created as { id: string; display_name?: string; agent_key: string };
|
||||
setSummoningAgent({ id: ag.id, name: ag.display_name || ag.agent_key });
|
||||
|
||||
Reference in new issue
Block a user