diff --git a/ui/web/src/pages/agents/agent-detail/agent-instance-display-utils.test.ts b/ui/web/src/pages/agents/agent-detail/agent-instance-display-utils.test.ts new file mode 100644 index 00000000..2eba4404 --- /dev/null +++ b/ui/web/src/pages/agents/agent-detail/agent-instance-display-utils.test.ts @@ -0,0 +1,93 @@ +import { describe, expect, it } from "vitest"; +import type { ChannelContact } from "@/types/contact"; +import { buildAgentInstanceDisplay, getAgentInstanceResolveIds } from "./agent-instance-display-utils"; + +function resolver(contacts: Record>) { + return (id: string): ChannelContact | null => (contacts[id] as ChannelContact | undefined) ?? null; +} + +describe("agent instance display utils", () => { + it("formats group instances as channel, chat name, and raw instance id", () => { + const display = buildAgentInstanceDisplay( + { + user_id: "group:lark-cppai-pm:oc_123", + metadata: { chat_title: "LVC Online" }, + }, + resolver({}), + ); + + expect(display.label).toBe("Lark - LVC Online - oc_123"); + expect(display.instanceId).toBe("oc_123"); + }); + + it("uses raw group contact names when profile metadata has no title", () => { + const display = buildAgentInstanceDisplay( + { user_id: "group:lark-cppai-pm:oc_456" }, + resolver({ + oc_456: { + sender_id: "oc_456", + channel_type: "feishu", + display_name: "Peacefullfie", + }, + }), + ); + + expect(display.label).toBe("Lark - Peacefullfie - oc_456"); + }); + + it("does not use sender display names as group names", () => { + const display = buildAgentInstanceDisplay( + { + user_id: "group:lark-cppai-pm:oc_456", + metadata: { display_name: "Last Sender" }, + }, + resolver({}), + ); + + expect(display.label).toBe("Lark - oc_456"); + }); + + it("formats direct channel contacts when contact metadata is available", () => { + const display = buildAgentInstanceDisplay( + { user_id: "ou_789" }, + resolver({ + ou_789: { + sender_id: "ou_789", + channel_type: "feishu", + channel_instance: "lark-cppai-pm", + display_name: "Duc Nguyen", + }, + }), + ); + + expect(display.label).toBe("Lark - Duc Nguyen - ou_789"); + }); + + it("falls back to a clean raw id when no channel or name is known", () => { + const display = buildAgentInstanceDisplay({ user_id: "system" }, resolver({})); + + expect(display.label).toBe("system"); + }); + + it("detects underscore channel prefixes", () => { + const display = buildAgentInstanceDisplay({ user_id: "group:zalo_ol:chat4878" }, resolver({})); + + expect(display.label).toBe("Zalo - chat4878"); + }); + + it("uses raw group ids for contact resolver inputs", () => { + expect( + getAgentInstanceResolveIds([ + { user_id: "group:telegram:-100123" }, + { user_id: "group:telegram:-100123" }, + { user_id: "ou_abc" }, + ]), + ).toEqual(["-100123", "ou_abc"]); + }); + + it("maps tele group instance prefixes to Telegram", () => { + const display = buildAgentInstanceDisplay({ user_id: "group:tele-itsdd-local-goclaw:-5104" }, resolver({})); + + expect(display.label).toBe("Telegram - -5104"); + }); +}); diff --git a/ui/web/src/pages/agents/agent-detail/agent-instance-display-utils.ts b/ui/web/src/pages/agents/agent-detail/agent-instance-display-utils.ts new file mode 100644 index 00000000..50c6ed3c --- /dev/null +++ b/ui/web/src/pages/agents/agent-detail/agent-instance-display-utils.ts @@ -0,0 +1,136 @@ +import type { ChannelContact } from "@/types/contact"; + +interface AgentInstanceLike { + user_id: string; + metadata?: Record; +} + +export interface AgentInstanceDisplay { + label: string; + instanceId: string; + channelLabel: string | null; + chatName: string | null; + rawUserId: string; +} + +type ContactResolver = (id: string) => ChannelContact | null; + +const CHANNEL_PREFIX_LABELS: Array<[string, string]> = [ + ["zalo_personal", "Zalo Personal"], + ["zalo-personal", "Zalo Personal"], + ["zalo_oa", "Zalo OA"], + ["zalo-oa", "Zalo OA"], + ["bitrix24", "Bitrix24"], + ["bitrix", "Bitrix24"], + ["telegram", "Telegram"], + ["tele", "Telegram"], + ["tg", "Telegram"], + ["discord", "Discord"], + ["whatsapp", "WhatsApp"], + ["facebook", "Facebook"], + ["pancake", "Pancake"], + ["feishu", "Feishu"], + ["lark", "Lark"], + ["slack", "Slack"], + ["zalo", "Zalo"], +]; + +export function getAgentInstanceResolveIds(instances: AgentInstanceLike[]): string[] { + const ids = new Set(); + + for (const instance of instances) { + const groupTarget = parseGroupTarget(instance.user_id); + ids.add(groupTarget?.instanceId || instance.user_id); + } + + return [...ids]; +} + +export function buildAgentInstanceDisplay( + instance: AgentInstanceLike, + resolve: ContactResolver, +): AgentInstanceDisplay { + const groupTarget = parseGroupTarget(instance.user_id); + const isGroupInstance = Boolean(groupTarget); + const contact = resolve(instance.user_id); + const rawContact = groupTarget ? resolve(groupTarget.instanceId) : null; + const metadata = instance.metadata ?? {}; + + const instanceId = groupTarget?.instanceId || contact?.sender_id || instance.user_id; + const channelLabel = + channelLabelFromSource(groupTarget?.channelSource) ?? + channelLabelFromSource(metadata.channel_instance) ?? + channelLabelFromSource(rawContact?.channel_instance) ?? + channelLabelFromSource(contact?.channel_instance) ?? + channelLabelFromSource(metadata.channel_type) ?? + channelLabelFromSource(metadata.channel) ?? + channelLabelFromSource(metadata.platform) ?? + channelLabelFromSource(rawContact?.channel_type) ?? + channelLabelFromSource(contact?.channel_type); + + const metadataTitle = + clean(metadata.chat_title) ?? + clean(metadata.group_title) ?? + clean(metadata.chat_name); + const chatName = isGroupInstance + ? metadataTitle ?? clean(rawContact?.display_name) ?? clean(rawContact?.username) + : metadataTitle ?? + clean(metadata.display_name) ?? + clean(rawContact?.display_name) ?? + clean(rawContact?.username) ?? + clean(contact?.display_name) ?? + clean(contact?.username); + + return { + label: formatInstanceLabel(channelLabel, chatName, instanceId), + instanceId, + channelLabel, + chatName, + rawUserId: instance.user_id, + }; +} + +function parseGroupTarget(userID: string): { channelSource: string; instanceId: string } | null { + if (!userID.startsWith("group:")) return null; + + const rest = userID.slice("group:".length); + const separatorIndex = rest.indexOf(":"); + if (separatorIndex < 0) return null; + + const channelSource = rest.slice(0, separatorIndex); + const instanceId = rest.slice(separatorIndex + 1); + if (!channelSource || !instanceId) return null; + + return { channelSource, instanceId }; +} + +function channelLabelFromSource(source?: unknown): string | null { + const value = clean(source)?.toLowerCase(); + if (!value) return null; + + for (const [prefix, label] of CHANNEL_PREFIX_LABELS) { + if ( + value === prefix || + value.startsWith(`${prefix}-`) || + value.startsWith(`${prefix}_`) || + value.startsWith(`${prefix}:`) + ) { + return label; + } + } + + return null; +} + +function formatInstanceLabel(channelLabel: string | null, chatName: string | null, instanceId: string): string { + if (channelLabel && chatName) return `${channelLabel} - ${chatName} - ${instanceId}`; + if (channelLabel) return `${channelLabel} - ${instanceId}`; + if (chatName) return chatName; + return instanceId; +} + +function clean(value?: unknown): string | null { + if (typeof value !== "string") return null; + const trimmed = value.trim(); + return trimmed ? trimmed : null; +} diff --git a/ui/web/src/pages/agents/agent-detail/agent-instances-tab.tsx b/ui/web/src/pages/agents/agent-detail/agent-instances-tab.tsx index 330a5196..55de1b27 100644 --- a/ui/web/src/pages/agents/agent-detail/agent-instances-tab.tsx +++ b/ui/web/src/pages/agents/agent-detail/agent-instances-tab.tsx @@ -9,6 +9,7 @@ import { Textarea } from "@/components/ui/textarea"; import { useContactResolver } from "@/hooks/use-contact-resolver"; import { useAgentInstances, type UserInstance } from "../hooks/use-agent-instances"; import { UserPickerCombobox } from "@/components/shared/user-picker-combobox"; +import { buildAgentInstanceDisplay, getAgentInstanceResolveIds } from "./agent-instance-display-utils"; interface AgentInstancesTabProps { agentId: string; @@ -53,13 +54,18 @@ export function AgentInstancesTab({ agentId }: AgentInstancesTabProps) { const isDirty = content !== originalContent; // Resolve user_ids to contact names for instances without metadata - const instanceUserIDs = useMemo(() => instances.map((i) => i.user_id), [instances]); + const instanceUserIDs = useMemo(() => getAgentInstanceResolveIds(instances), [instances]); const { resolve } = useContactResolver(instanceUserIDs); // Existing instance user_ids for deduplication const existingIDs = useMemo(() => new Set(instances.map((i) => i.user_id)), [instances]); const [addUserId, setAddUserId] = useState(""); + const selectedInstanceDisplay = useMemo(() => { + if (!selected) return null; + const selectedInstance = instances.find((inst) => inst.user_id === selected) ?? { user_id: selected }; + return buildAgentInstanceDisplay(selectedInstance, resolve); + }, [instances, resolve, selected]); const handleAddUser = (val: string) => { setAddUserId(val); @@ -75,7 +81,7 @@ export function AgentInstancesTab({ agentId }: AgentInstancesTabProps) { return (
{/* Instance list */} -
+
USER.md - — {selected} + + — {selectedInstanceDisplay?.label ?? selected} +