diff --git a/ui/web/src/pages/agents/agent-detail/agent-detail-page.tsx b/ui/web/src/pages/agents/agent-detail/agent-detail-page.tsx index 6692a235..ee2aebcf 100644 --- a/ui/web/src/pages/agents/agent-detail/agent-detail-page.tsx +++ b/ui/web/src/pages/agents/agent-detail/agent-detail-page.tsx @@ -10,6 +10,7 @@ import { AgentOverviewTab } from "./agent-overview-tab"; import { AgentFilesTab } from "./agent-files-tab"; import { AgentInstancesTab } from "./agent-instances-tab"; import { AgentPermissionsTab } from "./agent-permissions-tab"; +import { AgentSharesTab } from "./agent-shares-tab"; import { AgentEvolutionTab } from "./evolution-tab/agent-evolution-tab"; import { AgentHooksTab } from "./agent-hooks-tab"; import { SummoningModal } from "../summoning-modal"; @@ -80,6 +81,7 @@ export function AgentDetailPage({ agentId, onBack }: AgentDetailPageProps) { {t("detail.tabs.agent")} {t("detail.tabs.files")} {t("detail.tabs.permissions")} + {t("detail.tabs.shares")} {t("detail.tabs.evolution")} {t("detail.tabs.hooks")} {agent.agent_type === "predefined" && ( @@ -118,6 +120,10 @@ export function AgentDetailPage({ agentId, onBack }: AgentDetailPageProps) { + + + + (null); + + // Contact picker for the add form + const listContacts = useCallback( + async (search: string): Promise => { + const res = await http.get<{ contacts: ChannelContact[] }>("/v1/contacts", { + search, + limit: "20", + }); + return res.contacts ?? []; + }, + [http], + ); + const { options, searchContacts } = useContactPicker(listContacts); + + // Resolve display names for existing shares + const shareUserIDs = useMemo(() => shares.map((s) => s.user_id), [shares]); + const { resolve } = useContactResolver(shareUserIDs); + + const handleUserIdChange = (val: string) => { + setNewUserId(val); + searchContacts(val); + }; + + const handleAddShare = async () => { + if (!newUserId.trim()) return; + try { + await addShare(newUserId.trim(), newRole); + setNewUserId(""); + setNewRole("user"); + } catch { + // ignore + } + }; + + return ( +
+ {/* Add share form */} +
+

{t("shares.grantAccess")}

+
+
+ + +
+
+ + +
+ +
+
+ + {/* Share list */} + {loading && shares.length === 0 ? ( +
{t("shares.loadingShares")}
+ ) : shares.length === 0 ? ( +
+ +

{t("shares.noShares")}

+

+ {t("shares.noSharesDesc")} +

+
+ ) : ( +
+
+ {t("shares.user")} + {t("shares.role")} + +
+ {shares.map((share) => { + const contact = resolve(share.user_id); + const contactByGrant = share.granted_by ? resolve(share.granted_by) : null; + return ( +
+
+ + {contact?.display_name ?? share.user_id} + + {contact?.display_name && ( + + {share.user_id} + + )} + {contact?.username && ( + + @{contact.username} + + )} + {share.granted_by && ( + + by {contactByGrant?.display_name ?? share.granted_by} + + )} +
+ {share.role} + +
+ ); + })} +
+ )} + + setRevokeTarget(null)} + title={t("shares.revokeTitle")} + description={t("shares.revokeDesc", { userId: revokeTarget })} + confirmLabel={t("shares.revoke")} + variant="destructive" + onConfirm={async () => { + if (revokeTarget) { + try { + await revokeShare(revokeTarget); + } catch { + // ignore + } + setRevokeTarget(null); + } + }} + /> +
+ ); +} diff --git a/ui/web/src/pages/agents/hooks/use-agent-shares.ts b/ui/web/src/pages/agents/hooks/use-agent-shares.ts new file mode 100644 index 00000000..1a4d2ffa --- /dev/null +++ b/ui/web/src/pages/agents/hooks/use-agent-shares.ts @@ -0,0 +1,48 @@ +import { useState, useEffect, useCallback } from "react"; +import { useHttp } from "@/hooks/use-ws"; +import type { AgentShareData } from "@/types/agent"; + +export function useAgentShares(agentId: string) { + const http = useHttp(); + const [shares, setShares] = useState([]); + const [loading, setLoading] = useState(false); + + const loadShares = useCallback(async () => { + setLoading(true); + try { + const res = await http.get<{ shares: AgentShareData[] }>( + `/v1/agents/${agentId}/shares`, + ); + setShares(res.shares ?? []); + } catch { + // ignore + } finally { + setLoading(false); + } + }, [http, agentId]); + + useEffect(() => { + loadShares(); + }, [loadShares]); + + const addShare = useCallback( + async (userId: string, role: string) => { + await http.post(`/v1/agents/${agentId}/shares`, { + user_id: userId, + role, + }); + loadShares(); + }, + [http, agentId, loadShares], + ); + + const revokeShare = useCallback( + async (userId: string) => { + await http.delete(`/v1/agents/${agentId}/shares/${userId}`); + loadShares(); + }, + [http, agentId, loadShares], + ); + + return { shares, loading, refresh: loadShares, addShare, revokeShare }; +}