From 57754a569b246e33e6d1d130a109c2aa8601a78f Mon Sep 17 00:00:00 2001 From: viettranx Date: Tue, 17 Mar 2026 22:28:00 +0700 Subject: [PATCH] =?UTF-8?q?refactor(ui):=20simplify=20agent=20detail=20pag?= =?UTF-8?q?e=20=E2=80=94=20remove=20Links,=20Shares=20tabs=20and=20Other?= =?UTF-8?q?=20Config=20section?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove unused UI sections from agent detail: - Links tab + link-sections components + use-agent-links hook - Shares tab + use-agent-shares hook - "Quality & Advanced" config group + OtherConfigSection Also fix skills tab sort (ungranted non-system first) and harden use-contact-search with Array.isArray guard. --- .../agents/agent-detail/agent-config-tab.tsx | 35 +--- .../agents/agent-detail/agent-detail-page.tsx | 12 -- .../agents/agent-detail/agent-links-tab.tsx | 100 --------- .../agents/agent-detail/agent-shares-tab.tsx | 198 ------------------ .../agents/agent-detail/agent-skills-tab.tsx | 18 +- .../agent-detail/config-sections/index.ts | 1 - .../config-sections/other-config-section.tsx | 59 ------ .../agent-detail/link-sections/index.ts | 11 - .../link-sections/link-create-form.tsx | 175 ---------------- .../link-sections/link-edit-dialog.tsx | 146 ------------- .../agent-detail/link-sections/link-list.tsx | 120 ----------- .../agent-detail/link-sections/link-utils.ts | 47 ----- .../src/pages/agents/hooks/use-agent-links.ts | 67 ------ .../pages/agents/hooks/use-agent-shares.ts | 48 ----- .../pages/agents/hooks/use-contact-search.ts | 2 +- 15 files changed, 14 insertions(+), 1025 deletions(-) delete mode 100644 ui/web/src/pages/agents/agent-detail/agent-links-tab.tsx delete mode 100644 ui/web/src/pages/agents/agent-detail/agent-shares-tab.tsx delete mode 100644 ui/web/src/pages/agents/agent-detail/config-sections/other-config-section.tsx delete mode 100644 ui/web/src/pages/agents/agent-detail/link-sections/index.ts delete mode 100644 ui/web/src/pages/agents/agent-detail/link-sections/link-create-form.tsx delete mode 100644 ui/web/src/pages/agents/agent-detail/link-sections/link-edit-dialog.tsx delete mode 100644 ui/web/src/pages/agents/agent-detail/link-sections/link-list.tsx delete mode 100644 ui/web/src/pages/agents/agent-detail/link-sections/link-utils.ts delete mode 100644 ui/web/src/pages/agents/hooks/use-agent-links.ts delete mode 100644 ui/web/src/pages/agents/hooks/use-agent-shares.ts diff --git a/ui/web/src/pages/agents/agent-detail/agent-config-tab.tsx b/ui/web/src/pages/agents/agent-detail/agent-config-tab.tsx index 6ed7fd74..e528b737 100644 --- a/ui/web/src/pages/agents/agent-detail/agent-config-tab.tsx +++ b/ui/web/src/pages/agents/agent-detail/agent-config-tab.tsx @@ -19,7 +19,6 @@ import { ContextPruningSection, SandboxSection, MemorySection, - OtherConfigSection, ThinkingSection, WorkspaceSharingSection, } from "./config-sections"; @@ -52,27 +51,11 @@ export function AgentConfigTab({ agent, onUpdate }: AgentConfigTabProps) { const otherObj = (agent.other_config ?? {}) as Record; const initialThinkingLevel = (typeof otherObj.thinking_level === "string" ? otherObj.thinking_level : "off"); const initialWsSharing = (otherObj.workspace_sharing ?? {}) as WorkspaceSharingConfig; - // Strip all managed keys so they don't leak into the raw JSON editor. - // General tab manages: emoji, self_evolve, description, skill_evolve, skill_nudge_interval - // Config tab manages: workspace_sharing, thinking_level - const { - workspace_sharing: _ws, thinking_level: _tl, - emoji: _emoji, self_evolve: _se, description: _desc, - skill_evolve: _ske, skill_nudge_interval: _sni, - ...otherWithoutManaged - } = otherObj; const [wsSharing, setWsSharing] = useState(initialWsSharing); const [thinkingLevel, setThinkingLevel] = useState(initialThinkingLevel); - const [otherEnabled, setOtherEnabled] = useState( - agent.other_config != null && Object.keys(otherWithoutManaged).length > 0, - ); - const [otherJson, setOtherJson] = useState( - Object.keys(otherWithoutManaged).length > 0 ? JSON.stringify(otherWithoutManaged, null, 2) : "{}", - ); - const [saving, setSaving] = useState(false); const [saveError, setSaveError] = useState(null); const [saved, setSaved] = useState(false); @@ -94,13 +77,10 @@ export function AgentConfigTab({ agent, onUpdate }: AgentConfigTabProps) { }; // Preserve existing other_config fields not managed by this tab (e.g. emoji from General tab). const existing = (agent.other_config as Record | null) ?? {}; - let otherBase: Record = { ...existing }; + const otherBase: Record = { ...existing }; // Strip fields managed by this tab — they'll be re-added below from local state. delete otherBase.thinking_level; delete otherBase.workspace_sharing; - if (otherEnabled) { - try { Object.assign(otherBase, JSON.parse(otherJson)); } catch { /* keep existing */ } - } if (thinkingLevel && thinkingLevel !== "off") { otherBase.thinking_level = thinkingLevel; } @@ -182,19 +162,6 @@ export function AgentConfigTab({ agent, onUpdate }: AgentConfigTabProps) { /> - {/* Advanced */} - -
- { setOtherEnabled(v); if (!v) setOtherJson("{}"); }} - onChange={setOtherJson} - /> -
{t("detail.tabs.general")} {t("detail.tabs.config")} {t("detail.tabs.files")} - {t("detail.tabs.shares")} - {t("detail.tabs.links")} {t("detail.tabs.skills")} {agent.agent_type === "predefined" && ( {t("detail.tabs.instances")} @@ -182,14 +178,6 @@ export function AgentDetailPage({ agentId, onBack }: AgentDetailPageProps) { /> - - - - - - - - diff --git a/ui/web/src/pages/agents/agent-detail/agent-links-tab.tsx b/ui/web/src/pages/agents/agent-detail/agent-links-tab.tsx deleted file mode 100644 index 96a9a7ce..00000000 --- a/ui/web/src/pages/agents/agent-detail/agent-links-tab.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import { useState, useEffect, useMemo } from "react"; -import { Link } from "react-router"; -import { TriangleAlert } from "lucide-react"; -import { useTranslation } from "react-i18next"; -import { ConfirmDialog } from "@/components/shared/confirm-dialog"; -import { useAgentLinks } from "../hooks/use-agent-links"; -import { useAgents } from "../hooks/use-agents"; -import type { AgentLinkData } from "@/types/agent"; -import { LinkCreateForm, LinkList, LinkEditDialog, linkTargetName } from "./link-sections"; -import { ROUTES } from "@/lib/constants"; - -interface AgentLinksTabProps { - agentId: string; -} - -export function AgentLinksTab({ agentId }: AgentLinksTabProps) { - const { t } = useTranslation("agents"); - const { links, loading, load, createLink, updateLink, deleteLink } = - useAgentLinks(agentId); - const { agents } = useAgents(); - - const [editLink, setEditLink] = useState(null); - const [deleteTarget, setDeleteTarget] = useState<{ - id: string; - name: string; - } | null>(null); - - useEffect(() => { - load(); - }, [load]); - - const agentOptions = useMemo( - () => - agents - .filter((a) => a.id !== agentId && a.agent_type === "predefined") - .map((a) => ({ - value: a.id, - label: a.display_name || a.agent_key, - })), - [agents, agentId], - ); - - const handleStatusToggle = async (link: AgentLinkData) => { - const newStatus = link.status === "active" ? "disabled" : "active"; - await updateLink(link.id, { status: newStatus }); - }; - - return ( -
-
- -

- {t("links.teamsHint").split("Agent Teams")[0]} - - Agent Teams - - {t("links.teamsHint").split("Agent Teams")[1]} -

-
- - - - - setDeleteTarget({ id: link.id, name: linkTargetName(link, agentId) }) - } - /> - - setEditLink(null)} - onSave={updateLink} - /> - - setDeleteTarget(null)} - title={t("links.deleteTitle")} - description={t("links.deleteDesc", { name: deleteTarget?.name })} - confirmLabel={t("delete.confirmLabel")} - variant="destructive" - onConfirm={async () => { - if (deleteTarget) { - try { - await deleteLink(deleteTarget.id); - } catch { - // ignore - } - setDeleteTarget(null); - } - }} - /> -
- ); -} diff --git a/ui/web/src/pages/agents/agent-detail/agent-shares-tab.tsx b/ui/web/src/pages/agents/agent-detail/agent-shares-tab.tsx deleted file mode 100644 index 66b309fc..00000000 --- a/ui/web/src/pages/agents/agent-detail/agent-shares-tab.tsx +++ /dev/null @@ -1,198 +0,0 @@ -import { useState, useCallback, useMemo } from "react"; -import { Plus, Trash2, Users } from "lucide-react"; -import { useTranslation } from "react-i18next"; -import { Button } from "@/components/ui/button"; -import { Label } from "@/components/ui/label"; -import { Badge } from "@/components/ui/badge"; -import { Combobox } from "@/components/ui/combobox"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { ConfirmDialog } from "@/components/shared/confirm-dialog"; -import { useHttp } from "@/hooks/use-ws"; -import { useContactPicker } from "@/hooks/use-contact-picker"; -import { useContactResolver } from "@/hooks/use-contact-resolver"; -import { useAgentShares } from "../hooks/use-agent-shares"; -import type { ChannelContact } from "@/types/contact"; - -interface AgentSharesTabProps { - agentId: string; -} - -const ROLE_OPTIONS = [ - { value: "user" }, - { value: "viewer" }, -] as const; - -function roleBadgeVariant(role: string) { - switch (role) { - case "owner": return "success" as const; - case "user": return "info" as const; - default: return "outline" as const; - } -} - -export function AgentSharesTab({ agentId }: AgentSharesTabProps) { - const { t } = useTranslation("agents"); - const http = useHttp(); - const { shares, loading, addShare, revokeShare } = useAgentShares(agentId); - const [newUserId, setNewUserId] = useState(""); - const [newRole, setNewRole] = useState("user"); - const [revokeTarget, setRevokeTarget] = useState(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/agent-detail/agent-skills-tab.tsx b/ui/web/src/pages/agents/agent-detail/agent-skills-tab.tsx index 6c70fc6e..2e883c64 100644 --- a/ui/web/src/pages/agents/agent-detail/agent-skills-tab.tsx +++ b/ui/web/src/pages/agents/agent-detail/agent-skills-tab.tsx @@ -31,12 +31,18 @@ export function AgentSkillsTab({ agentId }: AgentSkillsTabProps) { const [search, setSearch] = useState(""); const [toggling, setToggling] = useState(null); - const filtered = skills.filter( - (s) => - s.name.toLowerCase().includes(search.toLowerCase()) || - s.slug.toLowerCase().includes(search.toLowerCase()) || - s.description.toLowerCase().includes(search.toLowerCase()), - ); + const filtered = skills + .filter( + (s) => + s.name.toLowerCase().includes(search.toLowerCase()) || + s.slug.toLowerCase().includes(search.toLowerCase()) || + s.description.toLowerCase().includes(search.toLowerCase()), + ) + .sort((a, b) => { + // Priority: ungranted non-system (0) → system (1) → granted (2) + const rank = (s: typeof a) => s.is_system ? 1 : s.granted ? 2 : 0; + return rank(a) - rank(b); + }); const handleToggle = async (skillId: string, granted: boolean) => { setToggling(skillId); diff --git a/ui/web/src/pages/agents/agent-detail/config-sections/index.ts b/ui/web/src/pages/agents/agent-detail/config-sections/index.ts index b5cde489..6a9adf67 100644 --- a/ui/web/src/pages/agents/agent-detail/config-sections/index.ts +++ b/ui/web/src/pages/agents/agent-detail/config-sections/index.ts @@ -5,6 +5,5 @@ export { CompactionSection } from "./compaction-section"; export { ContextPruningSection } from "./context-pruning-section"; export { SandboxSection } from "./sandbox-section"; export { MemorySection } from "./memory-section"; -export { OtherConfigSection } from "./other-config-section"; export { ThinkingSection } from "./thinking-section"; export { WorkspaceSharingSection } from "./workspace-sharing-section"; diff --git a/ui/web/src/pages/agents/agent-detail/config-sections/other-config-section.tsx b/ui/web/src/pages/agents/agent-detail/config-sections/other-config-section.tsx deleted file mode 100644 index 65a7284f..00000000 --- a/ui/web/src/pages/agents/agent-detail/config-sections/other-config-section.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { useState, useEffect } from "react"; -import { useTranslation } from "react-i18next"; -import { Textarea } from "@/components/ui/textarea"; -import { Button } from "@/components/ui/button"; -import { ConfigSection } from "./config-section"; - -interface OtherConfigSectionProps { - enabled: boolean; - value: string; - onToggle: (v: boolean) => void; - onChange: (v: string) => void; -} - -export function OtherConfigSection({ enabled, value, onToggle, onChange }: OtherConfigSectionProps) { - const { t } = useTranslation("agents"); - const s = "configSections.otherConfig"; - const [validJson, setValidJson] = useState(true); - - useEffect(() => { - try { - JSON.parse(value || "{}"); - setValidJson(true); - } catch { - setValidJson(false); - } - }, [value]); - - const handleFormat = () => { - try { - const parsed = JSON.parse(value); - onChange(JSON.stringify(parsed, null, 2)); - } catch { - // can't format invalid JSON - } - }; - - return ( - -