From 4787799cd81c4150612201f5d8c69099722d43a2 Mon Sep 17 00:00:00 2001 From: RockChinQ Date: Wed, 26 Aug 2026 14:06:09 +0800 Subject: [PATCH] fix(agents): align debug event selection --- .../app/home/agents/AgentDetailContent.tsx | 37 ++++-- .../agents/components/AgentDebugPanel.tsx | 124 +++++++++++------- .../components/AgentEventPatternPicker.tsx | 36 ++--- .../agents/components/AgentFormComponent.tsx | 28 ++-- .../event-patterns/event-pattern-groups.ts | 22 ++++ .../e2e/processor-detail-workbench.spec.ts | 34 +++++ 6 files changed, 184 insertions(+), 97 deletions(-) diff --git a/web/src/app/home/agents/AgentDetailContent.tsx b/web/src/app/home/agents/AgentDetailContent.tsx index 8ef7bbd06..6f3a3233b 100644 --- a/web/src/app/home/agents/AgentDetailContent.tsx +++ b/web/src/app/home/agents/AgentDetailContent.tsx @@ -49,6 +49,12 @@ export default function AgentDetailContent({ id }: { id: string }) { const [runnerStatus, setRunnerStatus] = useState( null, ); + const [availableEventTypes, setAvailableEventTypes] = useState([ + 'message.received', + ]); + const [supportedEventPatterns, setSupportedEventPatterns] = useState< + string[] + >(['*']); const agentFormRef = useRef(null); useEffect(() => { @@ -70,10 +76,25 @@ export default function AgentDetailContent({ id }: { id: string }) { if (isCreateMode) return; let cancelled = false; setLoading(true); - httpClient - .getAgent(id) - .then((resp) => { - if (!cancelled) setAgent(resp.agent); + Promise.all([ + httpClient.getAgent(id), + httpClient.getAdapters().catch(() => ({ adapters: [] })), + ]) + .then(([resp, adaptersResp]) => { + if (cancelled) return; + const adapterEvents = adaptersResp.adapters.flatMap( + (adapter) => adapter.spec.supported_events ?? [], + ); + setAvailableEventTypes( + adapterEvents.length > 0 + ? Array.from(new Set(adapterEvents)).sort() + : ['message.received'], + ); + setSupportedEventPatterns( + resp.agent.supported_event_patterns ?? + resp.agent.capability?.supported_event_patterns ?? ['*'], + ); + setAgent(resp.agent); }) .finally(() => { if (!cancelled) setLoading(false); @@ -177,6 +198,7 @@ export default function AgentDetailContent({ id }: { id: string }) { { if (updatedAgent) { setAgent((current) => @@ -188,6 +210,7 @@ export default function AgentDetailContent({ id }: { id: string }) { onDirtyChange={setFormDirty} onSavingChange={setFormSaving} onRunnerStatusChange={setRunnerStatus} + onSupportedEventPatternsChange={setSupportedEventPatterns} /> } @@ -201,10 +224,8 @@ export default function AgentDetailContent({ id }: { id: string }) { onOpenRunnerConfig={() => agentFormRef.current?.openSection('runner_config') } - supportedEventPatterns={ - agent.supported_event_patterns ?? - agent.capability?.supported_event_patterns ?? ['*'] - } + supportedEventPatterns={supportedEventPatterns} + availableEventTypes={availableEventTypes} /> ) : undefined } diff --git a/web/src/app/home/agents/components/AgentDebugPanel.tsx b/web/src/app/home/agents/components/AgentDebugPanel.tsx index 37b7b5570..d008670b5 100644 --- a/web/src/app/home/agents/components/AgentDebugPanel.tsx +++ b/web/src/app/home/agents/components/AgentDebugPanel.tsx @@ -17,7 +17,9 @@ import { Label } from '@/components/ui/label'; import { Select, SelectContent, + SelectGroup, SelectItem, + SelectLabel, SelectTrigger, SelectValue, } from '@/components/ui/select'; @@ -28,9 +30,16 @@ import { CollapsibleContent, CollapsibleTrigger, } from '@/components/ui/collapsible'; +import { + eventGroupLabel, + eventPatternDescription, + eventPatternLabel, + groupEventPatterns, +} from '@/app/home/components/event-patterns/event-pattern-groups'; interface AgentDebugPanelProps { agentId: string; + availableEventTypes: string[]; supportedEventPatterns?: string[]; beforeRun?: () => Promise; hasUnsavedChanges?: boolean; @@ -46,16 +55,15 @@ interface DebugEntry { detail?: string; } -const EVENT_PRESETS = [ - { - value: 'message.received', - labelKey: 'agents.debugMessageReceived', +const EVENT_PRESET_DATA: Record< + string, + { text: string; data: Record } +> = { + 'message.received': { text: '', data: {}, }, - { - value: 'group.member.joined', - labelKey: 'agents.debugGroupMemberJoined', + 'group.member_joined': { text: 'A new member joined the group.', data: { group_id: 'debug-group', @@ -63,9 +71,7 @@ const EVENT_PRESETS = [ member_name: 'Debug User', }, }, - { - value: 'group.member.left', - labelKey: 'agents.debugGroupMemberLeft', + 'group.member_left': { text: 'A member left the group.', data: { group_id: 'debug-group', @@ -73,9 +79,7 @@ const EVENT_PRESETS = [ member_name: 'Debug User', }, }, - { - value: 'friend.requested', - labelKey: 'agents.debugFriendRequested', + 'friend.request_received': { text: 'A user sent a friend request.', data: { requester_id: 'debug-user', @@ -83,22 +87,14 @@ const EVENT_PRESETS = [ message: 'Hello', }, }, - { - value: 'feedback.received', - labelKey: 'agents.debugFeedbackReceived', + 'feedback.received': { text: 'The user submitted feedback.', data: { rating: 5, content: 'Debug feedback', }, }, - { - value: 'custom', - labelKey: 'agents.debugCustomEvent', - text: '', - data: {}, - }, -] as const; +}; function createDebugSessionId(agentId: string) { const nonce = globalThis.crypto?.randomUUID?.() ?? String(Date.now()); @@ -112,6 +108,7 @@ function matchesEventPattern(pattern: string, eventType: string) { export default function AgentDebugPanel({ agentId, + availableEventTypes, supportedEventPatterns = ['*'], beforeRun, hasUnsavedChanges = false, @@ -132,27 +129,39 @@ export default function AgentDebugPanel({ () => supportedEventPatterns.join(', '), [supportedEventPatterns], ); - const availablePresets = useMemo( - () => - EVENT_PRESETS.filter( - (item) => - item.value === 'custom' || - supportedEventPatterns.some((pattern) => - matchesEventPattern(pattern, item.value), - ), - ), - [supportedEventPatterns], + const availableEvents = useMemo(() => { + const concretePatterns = supportedEventPatterns.filter( + (pattern) => pattern !== '*' && !pattern.endsWith('.*'), + ); + return Array.from(new Set([...availableEventTypes, ...concretePatterns])) + .filter((candidate) => + supportedEventPatterns.some((pattern) => + matchesEventPattern(pattern, candidate), + ), + ) + .sort(); + }, [availableEventTypes, supportedEventPatterns]); + const eventGroups = useMemo( + () => groupEventPatterns(availableEvents), + [availableEvents], + ); + const supportsCustomEvent = supportedEventPatterns.some( + (pattern) => pattern === '*' || pattern.endsWith('.*'), ); useEffect(() => { - if (availablePresets.some((item) => item.value === preset)) return; - selectPreset(availablePresets[0]?.value ?? 'custom'); - }, [availablePresets, preset]); + if ( + availableEvents.includes(preset) || + (preset === 'custom' && supportsCustomEvent) + ) { + return; + } + selectPreset(availableEvents[0] ?? 'custom'); + }, [availableEvents, preset, supportsCustomEvent]); function selectPreset(value: string) { setPreset(value); - const nextPreset = EVENT_PRESETS.find((item) => item.value === value); - if (!nextPreset) return; + const nextPreset = EVENT_PRESET_DATA[value] ?? { text: '', data: {} }; setInputText(nextPreset.text); setEventDataText(JSON.stringify(nextPreset.data, null, 2)); } @@ -275,15 +284,42 @@ export default function AgentDebugPanel({
diff --git a/web/src/app/home/agents/components/AgentEventPatternPicker.tsx b/web/src/app/home/agents/components/AgentEventPatternPicker.tsx index 55867b7f8..2e6efcfab 100644 --- a/web/src/app/home/agents/components/AgentEventPatternPicker.tsx +++ b/web/src/app/home/agents/components/AgentEventPatternPicker.tsx @@ -20,6 +20,8 @@ import { cn } from '@/lib/utils'; import { eventGroupLabel, eventNamespaces, + eventPatternDescription, + eventPatternLabel, groupEventPatterns, } from '@/app/home/components/event-patterns/event-pattern-groups'; @@ -61,30 +63,6 @@ export default function AgentEventPatternPicker({ }, [events, selectedPatterns]); const optionGroups = useMemo(() => groupEventPatterns(options), [options]); - function eventLabel(pattern: string) { - if (pattern === '*') return t('bots.eventWildcard'); - if (pattern.endsWith('.*')) { - return t('bots.eventNamespaceWildcard', { - namespace: pattern.replace('.*', ''), - }); - } - const key = `bots.eventNames.${pattern.replace(/\./g, '_')}`; - const label = t(key); - return label === key ? pattern : label; - } - - function eventDescription(pattern: string) { - if (pattern === '*') return t('bots.eventDescriptions.all'); - if (pattern.endsWith('.*')) { - return t('bots.eventDescriptions.namespace'); - } - const key = `bots.eventDescriptions.${pattern.replace(/\./g, '_')}`; - const description = t(key); - return description === key - ? t('bots.eventDescriptions.custom') - : description; - } - function togglePattern(pattern: string) { if (pattern === '*') { onChange(['*']); @@ -127,7 +105,9 @@ export default function AgentEventPatternPicker({ variant="secondary" className="max-w-full rounded-md font-normal" > - {eventLabel(pattern)} + + {eventPatternLabel(pattern, t)} + ))} {selectedPatterns.length > 3 && ( @@ -157,7 +137,7 @@ export default function AgentEventPatternPicker({ return ( togglePattern(pattern)} className="items-start gap-2 py-2" > @@ -170,14 +150,14 @@ export default function AgentEventPatternPicker({ - {eventLabel(pattern)} + {eventPatternLabel(pattern, t)} {pattern} - {eventDescription(pattern)} + {eventPatternDescription(pattern, t)} diff --git a/web/src/app/home/agents/components/AgentFormComponent.tsx b/web/src/app/home/agents/components/AgentFormComponent.tsx index c3b6a5b25..0c3c531a9 100644 --- a/web/src/app/home/agents/components/AgentFormComponent.tsx +++ b/web/src/app/home/agents/components/AgentFormComponent.tsx @@ -47,10 +47,12 @@ export interface AgentRunnerStatus { interface AgentFormComponentProps { agentId: string; + availableEventTypes: string[]; onFinish: (agent?: Partial) => void; onDirtyChange?: (dirty: boolean) => void; onSavingChange?: (saving: boolean) => void; onRunnerStatusChange?: (status: AgentRunnerStatus) => void; + onSupportedEventPatternsChange?: (patterns: string[]) => void; } export type AgentConfigSection = 'events' | 'runner' | 'runner_config'; @@ -98,10 +100,12 @@ function isRunnerFieldVisible( function AgentFormComponent( { agentId, + availableEventTypes, onFinish, onDirtyChange, onSavingChange, onRunnerStatusChange, + onSupportedEventPatternsChange, }: AgentFormComponentProps, ref: ForwardedRef, ) { @@ -112,9 +116,6 @@ function AgentFormComponent( useState(null); const [pluginStatusLoading, setPluginStatusLoading] = useState(true); const [pluginStatusError, setPluginStatusError] = useState(false); - const [availableEventTypes, setAvailableEventTypes] = useState([ - 'message.received', - ]); const [activeSection, setActiveSection] = useState('runner'); const isSavingRef = useRef(false); @@ -159,24 +160,17 @@ function AgentFormComponent( onDirtyChange?.(hasUnsavedChanges); }, [hasUnsavedChanges, onDirtyChange]); + const supportedEventPatterns = form.watch('supported_event_patterns'); + useEffect(() => { + onSupportedEventPatternsChange?.(supportedEventPatterns); + }, [onSupportedEventPatternsChange, supportedEventPatterns]); + useEffect(() => { let cancelled = false; - Promise.all([ - httpClient.getAgentMetadata(), - httpClient.getAgent(agentId), - httpClient.getAdapters().catch(() => ({ adapters: [] })), - ]) - .then(([metadata, resp, adaptersResp]) => { + Promise.all([httpClient.getAgentMetadata(), httpClient.getAgent(agentId)]) + .then(([metadata, resp]) => { if (cancelled) return; setRunnerConfigSchema(metadata.runner_config ?? null); - const adapterEvents = adaptersResp.adapters.flatMap( - (adapter) => adapter.spec.supported_events ?? [], - ); - setAvailableEventTypes( - adapterEvents.length > 0 - ? Array.from(new Set(adapterEvents)).sort() - : ['message.received'], - ); const agent = resp.agent; const config = (agent.config ?? {}) as Record; const loadedValues: FormValues = { diff --git a/web/src/app/home/components/event-patterns/event-pattern-groups.ts b/web/src/app/home/components/event-patterns/event-pattern-groups.ts index b1f1f8525..242fc9adb 100644 --- a/web/src/app/home/components/event-patterns/event-pattern-groups.ts +++ b/web/src/app/home/components/event-patterns/event-pattern-groups.ts @@ -55,3 +55,25 @@ export function eventGroupLabel(namespace: string, t: TFunction) { const label = t(key); return label === key ? namespace : label; } + +export function eventPatternLabel(pattern: string, t: TFunction) { + if (pattern === '*') return t('bots.eventWildcard'); + if (pattern.endsWith('.*')) { + return t('bots.eventNamespaceWildcard', { + namespace: pattern.replace('.*', ''), + }); + } + const key = `bots.eventNames.${pattern.replace(/\./g, '_')}`; + const label = t(key); + return label === key ? pattern : label; +} + +export function eventPatternDescription(pattern: string, t: TFunction) { + if (pattern === '*') return t('bots.eventDescriptions.all'); + if (pattern.endsWith('.*')) { + return t('bots.eventDescriptions.namespace'); + } + const key = `bots.eventDescriptions.${pattern.replace(/\./g, '_')}`; + const description = t(key); + return description === key ? t('bots.eventDescriptions.custom') : description; +} diff --git a/web/tests/e2e/processor-detail-workbench.spec.ts b/web/tests/e2e/processor-detail-workbench.spec.ts index f8f5e2c87..343947a29 100644 --- a/web/tests/e2e/processor-detail-workbench.spec.ts +++ b/web/tests/e2e/processor-detail-workbench.spec.ts @@ -27,6 +27,21 @@ test.describe('processor detail workbench', () => { expect(debugBox!.x).toBeLessThan(configBox!.x); expect(configBox!.width).toBeGreaterThan(debugBox!.width); + const debugEventPicker = debugPanel.getByRole('combobox', { + name: 'Event type', + }); + await debugEventPicker.click(); + await expect(page.getByRole('group', { name: 'Messages' })).toBeVisible(); + await expect(page.getByRole('group', { name: 'Groups' })).toBeVisible(); + await expect( + page.getByRole('option').filter({ hasText: 'Member joined group' }), + ).toContainText('A member joins a group where the bot is present.'); + await expect( + page.getByRole('option').filter({ hasText: 'Message edited' }), + ).toContainText('The platform reports that an existing message changed.'); + await expect(page.getByRole('option')).toHaveCount(6); + await page.keyboard.press('Escape'); + const appShell = page.locator('[class*="group/sidebar-wrapper"]'); const sidebarInset = page.locator('[data-slot="sidebar-inset"]'); await expect(appShell).toHaveCSS('overflow', 'clip'); @@ -108,6 +123,25 @@ test.describe('processor detail workbench', () => { ).toBeVisible(); await expect(page.getByRole('group', { name: 'Messages' })).toHaveCount(1); await expect(page.getByRole('group', { name: 'Groups' })).toHaveCount(1); + await page + .getByRole('option') + .filter({ hasText: 'message.*' }) + .first() + .click(); + await page.keyboard.press('Escape'); + + await debugEventPicker.click(); + await expect( + page.getByRole('option').filter({ hasText: 'Message edited' }), + ).toBeVisible(); + await expect( + page.getByRole('option').filter({ hasText: 'Member joined group' }), + ).toHaveCount(0); + await expect(page.getByRole('option')).toHaveCount(3); + await page.keyboard.press('Escape'); + + await eventPicker.click(); + await page.getByRole('option').filter({ hasText: 'All events' }).click(); await page.keyboard.press('Escape'); await flow.getByRole('tab').nth(1).click();