feat(events): live domain event stream log panel (#2653)

Co-authored-by: sanil-23 <sanil@vezures.xyz>
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Sathvik Gilakamsetty
2026-05-29 05:04:23 +05:30
committed by GitHub
co-authored by sanil-23 Claude
parent a211cac9fd
commit c5c93c502e
22 changed files with 1097 additions and 5 deletions
@@ -138,6 +138,22 @@ const developerItems = [
</svg>
),
},
{
id: 'event-log',
titleKey: 'settings.developerMenu.eventLog.title',
descriptionKey: 'settings.developerMenu.eventLog.desc',
route: 'event-log',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 6h16M4 10h16M4 14h16M4 18h16"
/>
</svg>
),
},
{
id: 'tool-policy-diagnostics',
titleKey: 'devOptions.diagnostics',
@@ -0,0 +1,326 @@
import { useEffect, useRef, useState } from 'react';
import { useT } from '../../../lib/i18n/I18nContext';
import { getCoreHttpBaseUrl, getCoreRpcToken } from '../../../services/coreRpcClient';
import SettingsHeader from '../components/SettingsHeader';
import { useSettingsNavigation } from '../hooks/useSettingsNavigation';
interface EventEntry {
id: number;
domain: string;
event: string;
agent: string;
timestamp: string;
}
const DOMAIN_BADGE_KEYS: Record<string, string> = {
tool: 'settings.developerMenu.eventLog.badge.tool',
agent: 'settings.developerMenu.eventLog.badge.agent',
system: 'settings.developerMenu.eventLog.badge.info',
memory: 'settings.developerMenu.eventLog.badge.mem',
channel: 'settings.developerMenu.eventLog.badge.chan',
cron: 'settings.developerMenu.eventLog.badge.cron',
webhook: 'settings.developerMenu.eventLog.badge.hook',
approval: 'settings.developerMenu.eventLog.badge.warn',
skill: 'settings.developerMenu.eventLog.badge.skill',
composio: 'settings.developerMenu.eventLog.badge.comp',
mcp_client: 'settings.developerMenu.eventLog.badge.mcp',
};
const DOMAIN_BADGE_COLORS: Record<string, { bg: string; text: string }> = {
tool: { bg: 'bg-blue-500/20', text: 'text-blue-400' },
agent: { bg: 'bg-green-500/20', text: 'text-green-400' },
system: { bg: 'bg-slate-500/20', text: 'text-slate-400' },
memory: { bg: 'bg-purple-500/20', text: 'text-purple-400' },
channel: { bg: 'bg-cyan-500/20', text: 'text-cyan-400' },
cron: { bg: 'bg-orange-500/20', text: 'text-orange-400' },
webhook: { bg: 'bg-indigo-500/20', text: 'text-indigo-400' },
approval: { bg: 'bg-amber-500/20', text: 'text-amber-400' },
skill: { bg: 'bg-teal-500/20', text: 'text-teal-400' },
composio: { bg: 'bg-pink-500/20', text: 'text-pink-400' },
mcp_client: { bg: 'bg-violet-500/20', text: 'text-violet-400' },
};
const MAX_ENTRIES = 200;
const RECONNECT_DELAY_MS = 3000;
const EventLogPanel = () => {
const { t } = useT();
const { navigateBack, breadcrumbs } = useSettingsNavigation();
const [entries, setEntries] = useState<EventEntry[]>([]);
const [isLive, setIsLive] = useState(false);
const [filterType, setFilterType] = useState<string>('');
const [filterText, setFilterText] = useState('');
const [autoScroll, setAutoScroll] = useState(true);
const containerRef = useRef<HTMLDivElement>(null);
const idRef = useRef(0);
const controllerRef = useRef<AbortController | null>(null);
const reconnectRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const unmountedRef = useRef(false);
const maxEntriesRef = useRef(MAX_ENTRIES);
const newEntriesRef = useRef<'top' | 'bottom'>('top');
const connectRef = useRef<(() => Promise<void>) | null>(null);
const connect = async () => {
if (unmountedRef.current) return;
try {
const [baseUrl, token] = await Promise.all([getCoreHttpBaseUrl(), getCoreRpcToken()]);
if (!token) {
setIsLive(false);
return;
}
const url = `${baseUrl}/events/domain`;
const controller = new AbortController();
controllerRef.current = controller;
const response = await fetch(url, {
headers: { Authorization: `Bearer ${token}` },
signal: controller.signal,
});
if (!response.ok || !response.body) {
setIsLive(false);
return;
}
setIsLive(true);
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('event:')) {
const eventType = line.slice(6).trim();
if (eventType === 'config') {
// Next data: line is config — handled below
continue;
}
}
if (line.startsWith('data:')) {
const jsonStr = line.slice(5).trim();
if (!jsonStr) continue;
try {
const data = JSON.parse(jsonStr);
// Config message from server
if (data.max_entries !== undefined) {
maxEntriesRef.current = data.max_entries;
if (data.new_entries === 'top' || data.new_entries === 'bottom') {
newEntriesRef.current = data.new_entries;
}
continue;
}
const entry: EventEntry = {
id: ++idRef.current,
domain: data.domain || 'unknown',
event: data.event || '',
agent: data.agent || '',
timestamp: data.timestamp || '',
};
setEntries(prev => {
const next = newEntriesRef.current === 'top' ? [entry, ...prev] : [...prev, entry];
return next.length > maxEntriesRef.current
? newEntriesRef.current === 'top'
? next.slice(0, maxEntriesRef.current)
: next.slice(-maxEntriesRef.current)
: next;
});
} catch {
// skip malformed
}
}
}
}
setIsLive(false);
} catch {
setIsLive(false);
} finally {
controllerRef.current = null;
// Auto-reconnect unless unmounted
if (!unmountedRef.current) {
reconnectRef.current = setTimeout(() => void connectRef.current?.(), RECONNECT_DELAY_MS);
}
}
};
connectRef.current = connect;
useEffect(() => {
unmountedRef.current = false;
void connectRef.current?.();
return () => {
unmountedRef.current = true;
controllerRef.current?.abort();
controllerRef.current = null;
if (reconnectRef.current) {
clearTimeout(reconnectRef.current);
reconnectRef.current = null;
}
};
}, []);
useEffect(() => {
if (autoScroll && containerRef.current) {
const el = containerRef.current;
el.scrollTop = newEntriesRef.current === 'top' ? 0 : el.scrollHeight;
}
}, [entries, autoScroll]);
const handleScroll = () => {
const el = containerRef.current;
if (!el) return;
const atAnchor =
newEntriesRef.current === 'top'
? el.scrollTop < 10
: el.scrollHeight - el.scrollTop - el.clientHeight < 10;
setAutoScroll(atAnchor);
};
const filteredEntries = entries.filter(e => {
if (filterType && e.domain !== filterType) return false;
if (filterText) {
const q = filterText.toLowerCase();
if (!e.event.toLowerCase().includes(q) && !e.agent.toLowerCase().includes(q)) return false;
}
return true;
});
const exportLog = () => {
const blob = new Blob([filteredEntries.map(e => JSON.stringify(e)).join('\n')], {
type: 'application/x-ndjson',
});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `event-log-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.ndjson`;
a.click();
URL.revokeObjectURL(url);
};
const domains = [...new Set(entries.map(e => e.domain))].sort();
return (
<div data-testid="event-log-panel">
<SettingsHeader
title={t('settings.developerMenu.eventLog.title')}
showBackButton={true}
onBack={navigateBack}
breadcrumbs={breadcrumbs}
/>
<div className="p-4 space-y-4">
{/* Status bar */}
<div className="flex flex-wrap items-center gap-2 text-xs">
<select
className="rounded-lg border border-stone-200 dark:border-neutral-800 bg-stone-50 dark:bg-neutral-800/60 px-3 py-1.5 font-medium text-stone-700 dark:text-neutral-200"
value={filterType}
onChange={e => setFilterType(e.target.value)}>
<option value="">{t('settings.developerMenu.eventLog.allTypes')}</option>
{domains.map(d => (
<option key={d} value={d}>
{d}
</option>
))}
</select>
<input
type="text"
className="rounded-lg border border-stone-200 dark:border-neutral-800 bg-stone-50 dark:bg-neutral-800/60 px-3 py-1.5 font-medium text-stone-700 dark:text-neutral-200 w-40"
placeholder={t('settings.developerMenu.eventLog.filterAgent')}
value={filterText}
onChange={e => setFilterText(e.target.value)}
/>
<button
type="button"
onClick={exportLog}
disabled={filteredEntries.length === 0}
className="rounded-lg border border-stone-200 dark:border-neutral-800 bg-stone-50 dark:bg-neutral-800/60 px-3 py-1.5 font-medium text-stone-700 dark:text-neutral-200 hover:bg-stone-100 dark:hover:bg-neutral-800 disabled:opacity-50">
{t('settings.developerMenu.eventLog.download')}
</button>
<span className="text-stone-500 dark:text-neutral-400">
{filteredEntries.length} {t('settings.developerMenu.eventLog.events')} &middot;{' '}
<span
className={
isLive ? 'text-sage-600 dark:text-sage-300' : 'text-stone-400 dark:text-neutral-500'
}>
{isLive
? t('settings.developerMenu.eventLog.live')
: t('settings.developerMenu.eventLog.disconnected')}
</span>
</span>
</div>
{/* Jump to latest */}
{!autoScroll && (
<button
type="button"
onClick={() => {
setAutoScroll(true);
const el = containerRef.current;
if (el) {
el.scrollTop = newEntriesRef.current === 'top' ? 0 : el.scrollHeight;
}
}}
className="text-xs rounded-lg border border-primary-300 dark:border-primary-500/40 bg-primary-50 dark:bg-primary-500/10 px-3 py-1 text-primary-700 dark:text-primary-300">
{t('settings.developerMenu.eventLog.jumpToLatest')}
</button>
)}
{/* Event stream */}
<section className="space-y-1">
<div
ref={containerRef}
onScroll={handleScroll}
className="max-h-[60vh] overflow-y-auto space-y-1">
{filteredEntries.length === 0 && (
<p className="text-xs text-stone-400 dark:text-neutral-500 py-4 text-center">
{isLive
? t('settings.developerMenu.eventLog.waiting')
: t('settings.developerMenu.eventLog.notConnected')}
</p>
)}
{filteredEntries.map(entry => {
const colors = DOMAIN_BADGE_COLORS[entry.domain] || {
bg: 'bg-stone-500/20',
text: 'text-stone-400',
};
return (
<div
key={entry.id}
className="rounded-xl border border-stone-200 dark:border-neutral-800 bg-stone-50 dark:bg-neutral-800/60 px-3 py-2 flex items-start gap-2">
<span className="text-[10px] text-stone-400 dark:text-neutral-500 font-mono shrink-0 pt-0.5">
{entry.timestamp}
</span>
<span
className={`rounded-full ${colors.bg} px-2 py-0.5 text-[10px] ${colors.text} shrink-0`}>
{DOMAIN_BADGE_KEYS[entry.domain]
? t(DOMAIN_BADGE_KEYS[entry.domain])
: entry.domain.toUpperCase()}
</span>
{entry.agent && (
<span className="text-[10px] text-stone-500 dark:text-neutral-400 shrink-0 font-mono">
{entry.agent}
</span>
)}
<span className="text-xs text-stone-900 dark:text-neutral-100 truncate">
{entry.event}
</span>
</div>
);
})}
</div>
</section>
</div>
</div>
);
};
export default EventLogPanel;
@@ -0,0 +1,180 @@
import { fireEvent, screen, waitFor } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { renderWithProviders } from '../../../../test/test-utils';
import EventLogPanel from '../EventLogPanel';
vi.mock('../../../../services/coreRpcClient', () => ({
getCoreHttpBaseUrl: vi.fn().mockResolvedValue('http://localhost:9999'),
getCoreRpcToken: vi.fn().mockResolvedValue('test-token'),
}));
vi.mock('../../hooks/useSettingsNavigation', () => ({
useSettingsNavigation: () => ({ navigateBack: vi.fn(), breadcrumbs: [] }),
}));
vi.mock('../../../../lib/i18n/I18nContext', () => ({ useT: () => ({ t: (k: string) => k }) }));
function mockFetchSSE(events: Array<{ domain: string; event: string }>) {
const lines = events.map(e => `data:${JSON.stringify({ ...e, timestamp: '12:00:00' })}\n`);
const body = lines.join('');
const encoder = new TextEncoder();
const stream = new ReadableStream({
start(controller) {
controller.enqueue(encoder.encode(body));
controller.close();
},
});
global.fetch = vi.fn().mockResolvedValue({ ok: true, body: stream });
}
describe('EventLogPanel', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('renders the panel with header and filter controls', () => {
global.fetch = vi.fn().mockResolvedValue({ ok: false, body: null });
renderWithProviders(<EventLogPanel />);
expect(screen.getByTestId('event-log-panel')).toBeTruthy();
expect(screen.getByText('settings.developerMenu.eventLog.allTypes')).toBeTruthy();
});
it('shows waiting message when connected but no events', async () => {
const stream = new ReadableStream({
start() {
// never enqueue — stays open
},
});
global.fetch = vi.fn().mockResolvedValue({ ok: true, body: stream });
renderWithProviders(<EventLogPanel />);
await waitFor(() => {
expect(screen.getByText('settings.developerMenu.eventLog.waiting')).toBeTruthy();
});
});
it('renders events from SSE stream with domain badges', async () => {
mockFetchSSE([
{ domain: 'tool', event: 'ToolExecuted' },
{ domain: 'agent', event: 'AgentStarted' },
]);
renderWithProviders(<EventLogPanel />);
await waitFor(() => {
expect(screen.getByText('ToolExecuted')).toBeTruthy();
expect(screen.getByText('AgentStarted')).toBeTruthy();
});
expect(screen.getByText('settings.developerMenu.eventLog.badge.tool')).toBeTruthy();
expect(screen.getByText('settings.developerMenu.eventLog.badge.agent')).toBeTruthy();
});
it('shows disconnected state when fetch fails', async () => {
global.fetch = vi.fn().mockRejectedValue(new Error('network'));
renderWithProviders(<EventLogPanel />);
await waitFor(() => {
expect(screen.getByText('settings.developerMenu.eventLog.disconnected')).toBeTruthy();
});
});
it('filters events by domain type', async () => {
mockFetchSSE([
{ domain: 'tool', event: 'ToolA' },
{ domain: 'agent', event: 'AgentB' },
]);
const { container } = renderWithProviders(<EventLogPanel />);
await waitFor(() => {
expect(screen.getByText('ToolA')).toBeTruthy();
});
const select = container.querySelector('select')!;
fireEvent.change(select, { target: { value: 'tool' } });
await waitFor(() => {
expect(screen.queryByText('AgentB')).toBeNull();
});
expect(screen.getByText('ToolA')).toBeTruthy();
});
it('shows not connected when token is missing', async () => {
const { getCoreRpcToken } = await import('../../../../services/coreRpcClient');
vi.mocked(getCoreRpcToken).mockResolvedValueOnce(null as unknown as string);
global.fetch = vi.fn();
renderWithProviders(<EventLogPanel />);
await waitFor(() => {
expect(screen.getByText('settings.developerMenu.eventLog.notConnected')).toBeTruthy();
});
expect(global.fetch).not.toHaveBeenCalled();
});
it('exports filtered events as ndjson', async () => {
mockFetchSSE([{ domain: 'tool', event: 'ToolExport' }]);
renderWithProviders(<EventLogPanel />);
await waitFor(() => {
expect(screen.getByText('ToolExport')).toBeTruthy();
});
const createObjectURL = vi.fn().mockReturnValue('blob:test');
const revokeObjectURL = vi.fn();
global.URL.createObjectURL = createObjectURL;
global.URL.revokeObjectURL = revokeObjectURL;
const downloadBtn = screen.getByText('settings.developerMenu.eventLog.download');
fireEvent.click(downloadBtn);
expect(createObjectURL).toHaveBeenCalled();
expect(revokeObjectURL).toHaveBeenCalledWith('blob:test');
});
it('filters events by text input', async () => {
mockFetchSSE([
{ domain: 'tool', event: 'ToolMatch' },
{ domain: 'tool', event: 'Other' },
]);
renderWithProviders(<EventLogPanel />);
await waitFor(() => {
expect(screen.getByText('ToolMatch')).toBeTruthy();
});
const input = screen.getByPlaceholderText('settings.developerMenu.eventLog.filterAgent');
fireEvent.change(input, { target: { value: 'Match' } });
await waitFor(() => {
expect(screen.queryByText('Other')).toBeNull();
});
});
it('renders unknown domain as uppercase text', async () => {
mockFetchSSE([{ domain: 'custom_thing', event: 'Evt' }]);
renderWithProviders(<EventLogPanel />);
await waitFor(() => {
expect(screen.getByText('CUSTOM_THING')).toBeTruthy();
});
});
it('handles scroll and shows jump-to-latest button', async () => {
mockFetchSSE([{ domain: 'tool', event: 'ScrollTest' }]);
const { container } = renderWithProviders(<EventLogPanel />);
await waitFor(() => {
expect(screen.getByText('ScrollTest')).toBeTruthy();
});
const scrollDiv = container.querySelector('.max-h-\\[60vh\\]')!;
Object.defineProperty(scrollDiv, 'scrollTop', { value: 100, writable: true });
fireEvent.scroll(scrollDiv);
await waitFor(() => {
expect(screen.getByText('settings.developerMenu.eventLog.jumpToLatest')).toBeTruthy();
});
fireEvent.click(screen.getByText('settings.developerMenu.eventLog.jumpToLatest'));
});
});
+23
View File
@@ -225,6 +225,29 @@ const ar5: TranslationMap = {
'settings.developerMenu.webhooks.title': 'خطافات الويب',
'settings.developerMenu.webhooks.desc':
'فحص تسجيلات خطافات الويب وقت التشغيل وسجلات الطلبات الملتقطة',
'settings.developerMenu.eventLog.title': 'Event Log',
'settings.developerMenu.eventLog.desc':
'Live colour-coded stream of all agent, tool, and system events',
'settings.developerMenu.eventLog.allTypes': 'All types',
'settings.developerMenu.eventLog.filterAgent': 'Filter...',
'settings.developerMenu.eventLog.download': 'Download',
'settings.developerMenu.eventLog.events': 'events',
'settings.developerMenu.eventLog.live': 'Live',
'settings.developerMenu.eventLog.disconnected': 'Disconnected',
'settings.developerMenu.eventLog.waiting': 'Waiting for events...',
'settings.developerMenu.eventLog.notConnected': 'Not connected to core',
'settings.developerMenu.eventLog.jumpToLatest': 'Jump to latest',
'settings.developerMenu.eventLog.badge.tool': 'TOOL',
'settings.developerMenu.eventLog.badge.agent': 'AGENT',
'settings.developerMenu.eventLog.badge.info': 'INFO',
'settings.developerMenu.eventLog.badge.mem': 'MEM',
'settings.developerMenu.eventLog.badge.chan': 'CHAN',
'settings.developerMenu.eventLog.badge.cron': 'CRON',
'settings.developerMenu.eventLog.badge.hook': 'HOOK',
'settings.developerMenu.eventLog.badge.warn': 'WARN',
'settings.developerMenu.eventLog.badge.skill': 'SKILL',
'settings.developerMenu.eventLog.badge.comp': 'COMP',
'settings.developerMenu.eventLog.badge.mcp': 'MCP',
'settings.developerMenu.intelligence.title': 'الذكاء',
'settings.developerMenu.intelligence.desc':
'مساحة عمل الذاكرة، ومحرك اللاوعي، والأحلام، والإعدادات',
+23
View File
@@ -231,6 +231,29 @@ const bn5: TranslationMap = {
'settings.developerMenu.webhooks.title': 'ওয়েবহুক',
'settings.developerMenu.webhooks.desc':
'রানটাইম ওয়েবহুক রেজিস্ট্রেশন এবং ধরা পড়া রিকোয়েস্ট লগ পরিদর্শন করুন',
'settings.developerMenu.eventLog.title': 'Event Log',
'settings.developerMenu.eventLog.desc':
'Live colour-coded stream of all agent, tool, and system events',
'settings.developerMenu.eventLog.allTypes': 'All types',
'settings.developerMenu.eventLog.filterAgent': 'Filter...',
'settings.developerMenu.eventLog.download': 'Download',
'settings.developerMenu.eventLog.events': 'events',
'settings.developerMenu.eventLog.live': 'Live',
'settings.developerMenu.eventLog.disconnected': 'Disconnected',
'settings.developerMenu.eventLog.waiting': 'Waiting for events...',
'settings.developerMenu.eventLog.notConnected': 'Not connected to core',
'settings.developerMenu.eventLog.jumpToLatest': 'Jump to latest',
'settings.developerMenu.eventLog.badge.tool': 'TOOL',
'settings.developerMenu.eventLog.badge.agent': 'AGENT',
'settings.developerMenu.eventLog.badge.info': 'INFO',
'settings.developerMenu.eventLog.badge.mem': 'MEM',
'settings.developerMenu.eventLog.badge.chan': 'CHAN',
'settings.developerMenu.eventLog.badge.cron': 'CRON',
'settings.developerMenu.eventLog.badge.hook': 'HOOK',
'settings.developerMenu.eventLog.badge.warn': 'WARN',
'settings.developerMenu.eventLog.badge.skill': 'SKILL',
'settings.developerMenu.eventLog.badge.comp': 'COMP',
'settings.developerMenu.eventLog.badge.mcp': 'MCP',
'settings.developerMenu.intelligence.title': 'ইন্টেলিজেন্স',
'settings.developerMenu.intelligence.desc':
'মেমরি ওয়ার্কস্পেস, সাবকনশাস ইঞ্জিন, ড্রিমস এবং সেটিংস',
+23
View File
@@ -239,6 +239,29 @@ const de5: TranslationMap = {
'settings.developerMenu.webhooks.title': 'Webhooks',
'settings.developerMenu.webhooks.desc':
'Prüfe Laufzeit-Webhook-Registrierungen und erfasste Anforderungsprotokolle',
'settings.developerMenu.eventLog.title': 'Event Log',
'settings.developerMenu.eventLog.desc':
'Live colour-coded stream of all agent, tool, and system events',
'settings.developerMenu.eventLog.allTypes': 'All types',
'settings.developerMenu.eventLog.filterAgent': 'Filter...',
'settings.developerMenu.eventLog.download': 'Download',
'settings.developerMenu.eventLog.events': 'events',
'settings.developerMenu.eventLog.live': 'Live',
'settings.developerMenu.eventLog.disconnected': 'Disconnected',
'settings.developerMenu.eventLog.waiting': 'Waiting for events...',
'settings.developerMenu.eventLog.notConnected': 'Not connected to core',
'settings.developerMenu.eventLog.jumpToLatest': 'Jump to latest',
'settings.developerMenu.eventLog.badge.tool': 'TOOL',
'settings.developerMenu.eventLog.badge.agent': 'AGENT',
'settings.developerMenu.eventLog.badge.info': 'INFO',
'settings.developerMenu.eventLog.badge.mem': 'MEM',
'settings.developerMenu.eventLog.badge.chan': 'CHAN',
'settings.developerMenu.eventLog.badge.cron': 'CRON',
'settings.developerMenu.eventLog.badge.hook': 'HOOK',
'settings.developerMenu.eventLog.badge.warn': 'WARN',
'settings.developerMenu.eventLog.badge.skill': 'SKILL',
'settings.developerMenu.eventLog.badge.comp': 'COMP',
'settings.developerMenu.eventLog.badge.mcp': 'MCP',
'settings.developerMenu.intelligence.title': 'Intelligenz',
'settings.developerMenu.intelligence.desc':
'Gedächtnisarbeitsbereich, Unterbewusstseinsmotor, Träume und Einstellungen',
+23
View File
@@ -232,6 +232,29 @@ const en5: TranslationMap = {
'settings.developerMenu.webhooks.title': 'Webhooks',
'settings.developerMenu.webhooks.desc':
'Inspect runtime webhook registrations and captured request logs',
'settings.developerMenu.eventLog.title': 'Event Log',
'settings.developerMenu.eventLog.desc':
'Live colour-coded stream of all agent, tool, and system events',
'settings.developerMenu.eventLog.allTypes': 'All types',
'settings.developerMenu.eventLog.filterAgent': 'Filter...',
'settings.developerMenu.eventLog.download': 'Download',
'settings.developerMenu.eventLog.events': 'events',
'settings.developerMenu.eventLog.live': 'Live',
'settings.developerMenu.eventLog.disconnected': 'Disconnected',
'settings.developerMenu.eventLog.waiting': 'Waiting for events...',
'settings.developerMenu.eventLog.notConnected': 'Not connected to core',
'settings.developerMenu.eventLog.jumpToLatest': 'Jump to latest',
'settings.developerMenu.eventLog.badge.tool': 'TOOL',
'settings.developerMenu.eventLog.badge.agent': 'AGENT',
'settings.developerMenu.eventLog.badge.info': 'INFO',
'settings.developerMenu.eventLog.badge.mem': 'MEM',
'settings.developerMenu.eventLog.badge.chan': 'CHAN',
'settings.developerMenu.eventLog.badge.cron': 'CRON',
'settings.developerMenu.eventLog.badge.hook': 'HOOK',
'settings.developerMenu.eventLog.badge.warn': 'WARN',
'settings.developerMenu.eventLog.badge.skill': 'SKILL',
'settings.developerMenu.eventLog.badge.comp': 'COMP',
'settings.developerMenu.eventLog.badge.mcp': 'MCP',
'settings.developerMenu.intelligence.title': 'Intelligence',
'settings.developerMenu.intelligence.desc':
'Memory workspace, subconscious engine, dreams, and settings',
+23
View File
@@ -234,6 +234,29 @@ const es5: TranslationMap = {
'settings.developerMenu.webhooks.title': 'Ganchos web',
'settings.developerMenu.webhooks.desc':
'Inspecciona registros de webhooks en tiempo de ejecución y solicitudes capturadas',
'settings.developerMenu.eventLog.title': 'Event Log',
'settings.developerMenu.eventLog.desc':
'Live colour-coded stream of all agent, tool, and system events',
'settings.developerMenu.eventLog.allTypes': 'All types',
'settings.developerMenu.eventLog.filterAgent': 'Filter...',
'settings.developerMenu.eventLog.download': 'Download',
'settings.developerMenu.eventLog.events': 'events',
'settings.developerMenu.eventLog.live': 'Live',
'settings.developerMenu.eventLog.disconnected': 'Disconnected',
'settings.developerMenu.eventLog.waiting': 'Waiting for events...',
'settings.developerMenu.eventLog.notConnected': 'Not connected to core',
'settings.developerMenu.eventLog.jumpToLatest': 'Jump to latest',
'settings.developerMenu.eventLog.badge.tool': 'TOOL',
'settings.developerMenu.eventLog.badge.agent': 'AGENT',
'settings.developerMenu.eventLog.badge.info': 'INFO',
'settings.developerMenu.eventLog.badge.mem': 'MEM',
'settings.developerMenu.eventLog.badge.chan': 'CHAN',
'settings.developerMenu.eventLog.badge.cron': 'CRON',
'settings.developerMenu.eventLog.badge.hook': 'HOOK',
'settings.developerMenu.eventLog.badge.warn': 'WARN',
'settings.developerMenu.eventLog.badge.skill': 'SKILL',
'settings.developerMenu.eventLog.badge.comp': 'COMP',
'settings.developerMenu.eventLog.badge.mcp': 'MCP',
'settings.developerMenu.intelligence.title': 'Inteligencia',
'settings.developerMenu.intelligence.desc':
'Espacio de trabajo de memoria, motor subconsciente, sueños y ajustes',
+23
View File
@@ -236,6 +236,29 @@ const fr5: TranslationMap = {
'settings.developerMenu.webhooks.title': 'Webhooks',
'settings.developerMenu.webhooks.desc':
"Inspecter les enregistrements de webhooks d'exécution et les journaux de requêtes capturés",
'settings.developerMenu.eventLog.title': 'Event Log',
'settings.developerMenu.eventLog.desc':
'Live colour-coded stream of all agent, tool, and system events',
'settings.developerMenu.eventLog.allTypes': 'All types',
'settings.developerMenu.eventLog.filterAgent': 'Filter...',
'settings.developerMenu.eventLog.download': 'Download',
'settings.developerMenu.eventLog.events': 'events',
'settings.developerMenu.eventLog.live': 'Live',
'settings.developerMenu.eventLog.disconnected': 'Disconnected',
'settings.developerMenu.eventLog.waiting': 'Waiting for events...',
'settings.developerMenu.eventLog.notConnected': 'Not connected to core',
'settings.developerMenu.eventLog.jumpToLatest': 'Jump to latest',
'settings.developerMenu.eventLog.badge.tool': 'TOOL',
'settings.developerMenu.eventLog.badge.agent': 'AGENT',
'settings.developerMenu.eventLog.badge.info': 'INFO',
'settings.developerMenu.eventLog.badge.mem': 'MEM',
'settings.developerMenu.eventLog.badge.chan': 'CHAN',
'settings.developerMenu.eventLog.badge.cron': 'CRON',
'settings.developerMenu.eventLog.badge.hook': 'HOOK',
'settings.developerMenu.eventLog.badge.warn': 'WARN',
'settings.developerMenu.eventLog.badge.skill': 'SKILL',
'settings.developerMenu.eventLog.badge.comp': 'COMP',
'settings.developerMenu.eventLog.badge.mcp': 'MCP',
'settings.developerMenu.intelligence.title': 'Intelligence',
'settings.developerMenu.intelligence.desc':
'Espace mémoire, moteur subconscient, rêves et paramètres',
+23
View File
@@ -231,6 +231,29 @@ const hi5: TranslationMap = {
'settings.developerMenu.webhooks.title': 'वेबहुक्स',
'settings.developerMenu.webhooks.desc':
'रनटाइम वेबहुक रजिस्ट्रेशन और कैप्चर किए गए अनुरोध लॉग देखें',
'settings.developerMenu.eventLog.title': 'Event Log',
'settings.developerMenu.eventLog.desc':
'Live colour-coded stream of all agent, tool, and system events',
'settings.developerMenu.eventLog.allTypes': 'All types',
'settings.developerMenu.eventLog.filterAgent': 'Filter...',
'settings.developerMenu.eventLog.download': 'Download',
'settings.developerMenu.eventLog.events': 'events',
'settings.developerMenu.eventLog.live': 'Live',
'settings.developerMenu.eventLog.disconnected': 'Disconnected',
'settings.developerMenu.eventLog.waiting': 'Waiting for events...',
'settings.developerMenu.eventLog.notConnected': 'Not connected to core',
'settings.developerMenu.eventLog.jumpToLatest': 'Jump to latest',
'settings.developerMenu.eventLog.badge.tool': 'TOOL',
'settings.developerMenu.eventLog.badge.agent': 'AGENT',
'settings.developerMenu.eventLog.badge.info': 'INFO',
'settings.developerMenu.eventLog.badge.mem': 'MEM',
'settings.developerMenu.eventLog.badge.chan': 'CHAN',
'settings.developerMenu.eventLog.badge.cron': 'CRON',
'settings.developerMenu.eventLog.badge.hook': 'HOOK',
'settings.developerMenu.eventLog.badge.warn': 'WARN',
'settings.developerMenu.eventLog.badge.skill': 'SKILL',
'settings.developerMenu.eventLog.badge.comp': 'COMP',
'settings.developerMenu.eventLog.badge.mcp': 'MCP',
'settings.developerMenu.intelligence.title': 'इंटेलिजेंस',
'settings.developerMenu.intelligence.desc':
'मेमोरी वर्कस्पेस, सबकॉन्शस इंजन, ड्रीम्स, और सेटिंग्स',
+23
View File
@@ -231,6 +231,29 @@ const id5: TranslationMap = {
'settings.developerMenu.webhooks.title': 'Webhook',
'settings.developerMenu.webhooks.desc':
'Periksa pendaftaran webhook runtime dan log permintaan yang tertangkap',
'settings.developerMenu.eventLog.title': 'Event Log',
'settings.developerMenu.eventLog.desc':
'Live colour-coded stream of all agent, tool, and system events',
'settings.developerMenu.eventLog.allTypes': 'All types',
'settings.developerMenu.eventLog.filterAgent': 'Filter...',
'settings.developerMenu.eventLog.download': 'Download',
'settings.developerMenu.eventLog.events': 'events',
'settings.developerMenu.eventLog.live': 'Live',
'settings.developerMenu.eventLog.disconnected': 'Disconnected',
'settings.developerMenu.eventLog.waiting': 'Waiting for events...',
'settings.developerMenu.eventLog.notConnected': 'Not connected to core',
'settings.developerMenu.eventLog.jumpToLatest': 'Jump to latest',
'settings.developerMenu.eventLog.badge.tool': 'TOOL',
'settings.developerMenu.eventLog.badge.agent': 'AGENT',
'settings.developerMenu.eventLog.badge.info': 'INFO',
'settings.developerMenu.eventLog.badge.mem': 'MEM',
'settings.developerMenu.eventLog.badge.chan': 'CHAN',
'settings.developerMenu.eventLog.badge.cron': 'CRON',
'settings.developerMenu.eventLog.badge.hook': 'HOOK',
'settings.developerMenu.eventLog.badge.warn': 'WARN',
'settings.developerMenu.eventLog.badge.skill': 'SKILL',
'settings.developerMenu.eventLog.badge.comp': 'COMP',
'settings.developerMenu.eventLog.badge.mcp': 'MCP',
'settings.developerMenu.intelligence.title': 'Kecerdasan',
'settings.developerMenu.intelligence.desc':
'Ruang kerja memori, mesin bawah sadar, mimpi, dan pengaturan',
+23
View File
@@ -234,6 +234,29 @@ const it5: TranslationMap = {
'settings.developerMenu.webhooks.title': 'Webhook',
'settings.developerMenu.webhooks.desc':
'Ispeziona registrazioni webhook di runtime e log delle richieste catturate',
'settings.developerMenu.eventLog.title': 'Event Log',
'settings.developerMenu.eventLog.desc':
'Live colour-coded stream of all agent, tool, and system events',
'settings.developerMenu.eventLog.allTypes': 'All types',
'settings.developerMenu.eventLog.filterAgent': 'Filter...',
'settings.developerMenu.eventLog.download': 'Download',
'settings.developerMenu.eventLog.events': 'events',
'settings.developerMenu.eventLog.live': 'Live',
'settings.developerMenu.eventLog.disconnected': 'Disconnected',
'settings.developerMenu.eventLog.waiting': 'Waiting for events...',
'settings.developerMenu.eventLog.notConnected': 'Not connected to core',
'settings.developerMenu.eventLog.jumpToLatest': 'Jump to latest',
'settings.developerMenu.eventLog.badge.tool': 'TOOL',
'settings.developerMenu.eventLog.badge.agent': 'AGENT',
'settings.developerMenu.eventLog.badge.info': 'INFO',
'settings.developerMenu.eventLog.badge.mem': 'MEM',
'settings.developerMenu.eventLog.badge.chan': 'CHAN',
'settings.developerMenu.eventLog.badge.cron': 'CRON',
'settings.developerMenu.eventLog.badge.hook': 'HOOK',
'settings.developerMenu.eventLog.badge.warn': 'WARN',
'settings.developerMenu.eventLog.badge.skill': 'SKILL',
'settings.developerMenu.eventLog.badge.comp': 'COMP',
'settings.developerMenu.eventLog.badge.mcp': 'MCP',
'settings.developerMenu.intelligence.title': 'Intelligenza',
'settings.developerMenu.intelligence.desc':
'Area di lavoro memoria, motore subconscio, sogni e impostazioni',
+23
View File
@@ -562,6 +562,29 @@ const ko5: TranslationMap = {
'settings.developerMenu.composio.desc': '라우팅 모드, 통합 트리거 및 트리거 기록 아카이브.',
'settings.developerMenu.webhooks.title': '웹훅',
'settings.developerMenu.webhooks.desc': '런타임 웹훅 등록 및 캡처된 요청 로그 검사',
'settings.developerMenu.eventLog.title': 'Event Log',
'settings.developerMenu.eventLog.desc':
'Live colour-coded stream of all agent, tool, and system events',
'settings.developerMenu.eventLog.allTypes': 'All types',
'settings.developerMenu.eventLog.filterAgent': 'Filter...',
'settings.developerMenu.eventLog.download': 'Download',
'settings.developerMenu.eventLog.events': 'events',
'settings.developerMenu.eventLog.live': 'Live',
'settings.developerMenu.eventLog.disconnected': 'Disconnected',
'settings.developerMenu.eventLog.waiting': 'Waiting for events...',
'settings.developerMenu.eventLog.notConnected': 'Not connected to core',
'settings.developerMenu.eventLog.jumpToLatest': 'Jump to latest',
'settings.developerMenu.eventLog.badge.tool': 'TOOL',
'settings.developerMenu.eventLog.badge.agent': 'AGENT',
'settings.developerMenu.eventLog.badge.info': 'INFO',
'settings.developerMenu.eventLog.badge.mem': 'MEM',
'settings.developerMenu.eventLog.badge.chan': 'CHAN',
'settings.developerMenu.eventLog.badge.cron': 'CRON',
'settings.developerMenu.eventLog.badge.hook': 'HOOK',
'settings.developerMenu.eventLog.badge.warn': 'WARN',
'settings.developerMenu.eventLog.badge.skill': 'SKILL',
'settings.developerMenu.eventLog.badge.comp': 'COMP',
'settings.developerMenu.eventLog.badge.mcp': 'MCP',
'settings.developerMenu.intelligence.title': '인텔리전스',
'settings.developerMenu.intelligence.desc': '메모리 작업 공간, 잠재의식 엔진, 드림 및 설정',
'settings.developerMenu.notificationRouting.title': '알림 라우팅',
+23
View File
@@ -235,6 +235,29 @@ const pt5: TranslationMap = {
'settings.developerMenu.webhooks.title': 'Webhooks',
'settings.developerMenu.webhooks.desc':
'Inspecione registros de webhooks em tempo de execução e logs de solicitações capturadas',
'settings.developerMenu.eventLog.title': 'Event Log',
'settings.developerMenu.eventLog.desc':
'Live colour-coded stream of all agent, tool, and system events',
'settings.developerMenu.eventLog.allTypes': 'All types',
'settings.developerMenu.eventLog.filterAgent': 'Filter...',
'settings.developerMenu.eventLog.download': 'Download',
'settings.developerMenu.eventLog.events': 'events',
'settings.developerMenu.eventLog.live': 'Live',
'settings.developerMenu.eventLog.disconnected': 'Disconnected',
'settings.developerMenu.eventLog.waiting': 'Waiting for events...',
'settings.developerMenu.eventLog.notConnected': 'Not connected to core',
'settings.developerMenu.eventLog.jumpToLatest': 'Jump to latest',
'settings.developerMenu.eventLog.badge.tool': 'TOOL',
'settings.developerMenu.eventLog.badge.agent': 'AGENT',
'settings.developerMenu.eventLog.badge.info': 'INFO',
'settings.developerMenu.eventLog.badge.mem': 'MEM',
'settings.developerMenu.eventLog.badge.chan': 'CHAN',
'settings.developerMenu.eventLog.badge.cron': 'CRON',
'settings.developerMenu.eventLog.badge.hook': 'HOOK',
'settings.developerMenu.eventLog.badge.warn': 'WARN',
'settings.developerMenu.eventLog.badge.skill': 'SKILL',
'settings.developerMenu.eventLog.badge.comp': 'COMP',
'settings.developerMenu.eventLog.badge.mcp': 'MCP',
'settings.developerMenu.intelligence.title': 'Inteligência',
'settings.developerMenu.intelligence.desc':
'Espaço de trabalho de memória, motor subconsciente, sonhos e configurações',
+23
View File
@@ -232,6 +232,29 @@ const ru5: TranslationMap = {
'settings.developerMenu.webhooks.title': 'Вебхуки',
'settings.developerMenu.webhooks.desc':
'Проверяйте регистрации runtime-вебхуков и журналы захваченных запросов',
'settings.developerMenu.eventLog.title': 'Event Log',
'settings.developerMenu.eventLog.desc':
'Live colour-coded stream of all agent, tool, and system events',
'settings.developerMenu.eventLog.allTypes': 'All types',
'settings.developerMenu.eventLog.filterAgent': 'Filter...',
'settings.developerMenu.eventLog.download': 'Download',
'settings.developerMenu.eventLog.events': 'events',
'settings.developerMenu.eventLog.live': 'Live',
'settings.developerMenu.eventLog.disconnected': 'Disconnected',
'settings.developerMenu.eventLog.waiting': 'Waiting for events...',
'settings.developerMenu.eventLog.notConnected': 'Not connected to core',
'settings.developerMenu.eventLog.jumpToLatest': 'Jump to latest',
'settings.developerMenu.eventLog.badge.tool': 'TOOL',
'settings.developerMenu.eventLog.badge.agent': 'AGENT',
'settings.developerMenu.eventLog.badge.info': 'INFO',
'settings.developerMenu.eventLog.badge.mem': 'MEM',
'settings.developerMenu.eventLog.badge.chan': 'CHAN',
'settings.developerMenu.eventLog.badge.cron': 'CRON',
'settings.developerMenu.eventLog.badge.hook': 'HOOK',
'settings.developerMenu.eventLog.badge.warn': 'WARN',
'settings.developerMenu.eventLog.badge.skill': 'SKILL',
'settings.developerMenu.eventLog.badge.comp': 'COMP',
'settings.developerMenu.eventLog.badge.mcp': 'MCP',
'settings.developerMenu.intelligence.title': 'Интеллект',
'settings.developerMenu.intelligence.desc':
'Рабочая область памяти, подсознательный движок, сны и настройки',
+23
View File
@@ -218,6 +218,29 @@ const zhCN5: TranslationMap = {
'settings.developerMenu.localModelDebug.desc': 'Ollama 配置、资源下载、模型测试和诊断',
'settings.developerMenu.webhooks.title': 'Webhook',
'settings.developerMenu.webhooks.desc': '检查运行时 Webhook 注册和捕获的请求日志',
'settings.developerMenu.eventLog.title': 'Event Log',
'settings.developerMenu.eventLog.desc':
'Live colour-coded stream of all agent, tool, and system events',
'settings.developerMenu.eventLog.allTypes': 'All types',
'settings.developerMenu.eventLog.filterAgent': 'Filter...',
'settings.developerMenu.eventLog.download': 'Download',
'settings.developerMenu.eventLog.events': 'events',
'settings.developerMenu.eventLog.live': 'Live',
'settings.developerMenu.eventLog.disconnected': 'Disconnected',
'settings.developerMenu.eventLog.waiting': 'Waiting for events...',
'settings.developerMenu.eventLog.notConnected': 'Not connected to core',
'settings.developerMenu.eventLog.jumpToLatest': 'Jump to latest',
'settings.developerMenu.eventLog.badge.tool': 'TOOL',
'settings.developerMenu.eventLog.badge.agent': 'AGENT',
'settings.developerMenu.eventLog.badge.info': 'INFO',
'settings.developerMenu.eventLog.badge.mem': 'MEM',
'settings.developerMenu.eventLog.badge.chan': 'CHAN',
'settings.developerMenu.eventLog.badge.cron': 'CRON',
'settings.developerMenu.eventLog.badge.hook': 'HOOK',
'settings.developerMenu.eventLog.badge.warn': 'WARN',
'settings.developerMenu.eventLog.badge.skill': 'SKILL',
'settings.developerMenu.eventLog.badge.comp': 'COMP',
'settings.developerMenu.eventLog.badge.mcp': 'MCP',
'settings.developerMenu.intelligence.title': '智能',
'settings.developerMenu.intelligence.desc': '记忆工作区、潜意识引擎、梦境和设置',
'settings.developerMenu.notificationRouting.title': '通知路由',
+23
View File
@@ -3262,6 +3262,29 @@ const en: TranslationMap = {
'settings.developerMenu.webhooks.title': 'Webhooks',
'settings.developerMenu.webhooks.desc':
'Inspect runtime webhook registrations and captured request logs',
'settings.developerMenu.eventLog.title': 'Event Log',
'settings.developerMenu.eventLog.desc':
'Live colour-coded stream of all agent, tool, and system events',
'settings.developerMenu.eventLog.allTypes': 'All types',
'settings.developerMenu.eventLog.filterAgent': 'Filter...',
'settings.developerMenu.eventLog.download': 'Download',
'settings.developerMenu.eventLog.events': 'events',
'settings.developerMenu.eventLog.live': 'Live',
'settings.developerMenu.eventLog.disconnected': 'Disconnected',
'settings.developerMenu.eventLog.waiting': 'Waiting for events...',
'settings.developerMenu.eventLog.notConnected': 'Not connected to core',
'settings.developerMenu.eventLog.jumpToLatest': 'Jump to latest',
'settings.developerMenu.eventLog.badge.tool': 'TOOL',
'settings.developerMenu.eventLog.badge.agent': 'AGENT',
'settings.developerMenu.eventLog.badge.info': 'INFO',
'settings.developerMenu.eventLog.badge.mem': 'MEM',
'settings.developerMenu.eventLog.badge.chan': 'CHAN',
'settings.developerMenu.eventLog.badge.cron': 'CRON',
'settings.developerMenu.eventLog.badge.hook': 'HOOK',
'settings.developerMenu.eventLog.badge.warn': 'WARN',
'settings.developerMenu.eventLog.badge.skill': 'SKILL',
'settings.developerMenu.eventLog.badge.comp': 'COMP',
'settings.developerMenu.eventLog.badge.mcp': 'MCP',
'settings.developerMenu.intelligence.title': 'Intelligence',
'settings.developerMenu.intelligence.desc':
'Memory workspace, subconscious engine, dreams, and settings',
+2
View File
@@ -20,6 +20,7 @@ import DeveloperOptionsPanel from '../components/settings/panels/DeveloperOption
import DevicesComingSoonPanel from '../components/settings/panels/DevicesComingSoonPanel';
import DevWorkflowPanel from '../components/settings/panels/DevWorkflowPanel';
import EmbeddingsPanel from '../components/settings/panels/EmbeddingsPanel';
import EventLogPanel from '../components/settings/panels/EventLogPanel';
import HeartbeatPanel from '../components/settings/panels/HeartbeatPanel';
import LedgerUsagePanel from '../components/settings/panels/LedgerUsagePanel';
import LocalModelDebugPanel from '../components/settings/panels/LocalModelDebugPanel';
@@ -467,6 +468,7 @@ const Settings = () => {
<Route path="voice-debug" element={wrapSettingsPage(<VoiceDebugPanel />)} />
<Route path="local-model-debug" element={wrapSettingsPage(<LocalModelDebugPanel />)} />
<Route path="webhooks-debug" element={wrapSettingsPage(<WebhooksDebugPanel />)} />
<Route path="event-log" element={wrapSettingsPage(<EventLogPanel />)} />
<Route
path="model-health"
element={wrapSettingsPage(<ModelHealthPanel />, { maxWidthClass: 'max-w-4xl' })}
+98
View File
@@ -728,6 +728,104 @@ impl DomainEvent {
| Self::McpSetupSecretRequested { .. } => "mcp_client",
}
}
/// Stable variant name without payload (avoids Debug format coupling).
pub fn variant_name(&self) -> &'static str {
match self {
Self::AgentTurnStarted { .. } => "AgentTurnStarted",
Self::AgentTurnCompleted { .. } => "AgentTurnCompleted",
Self::AgentError { .. } => "AgentError",
Self::SubagentSpawned { .. } => "SubagentSpawned",
Self::SubagentCompleted { .. } => "SubagentCompleted",
Self::SubagentFailed { .. } => "SubagentFailed",
Self::MemoryStored { .. } => "MemoryStored",
Self::MemoryRecalled { .. } => "MemoryRecalled",
Self::MemorySyncRequested { .. } => "MemorySyncRequested",
Self::MemorySyncStageChanged { .. } => "MemorySyncStageChanged",
Self::MemoryIngestionStarted { .. } => "MemoryIngestionStarted",
Self::MemoryIngestionCompleted { .. } => "MemoryIngestionCompleted",
Self::DocumentCanonicalized { .. } => "DocumentCanonicalized",
Self::CacheRebuilt { .. } => "CacheRebuilt",
Self::ChannelInboundMessage { .. } => "ChannelInboundMessage",
Self::ChannelMessageReceived { .. } => "ChannelMessageReceived",
Self::ChannelMessageProcessed { .. } => "ChannelMessageProcessed",
Self::ChannelReactionReceived { .. } => "ChannelReactionReceived",
Self::ChannelReactionSent { .. } => "ChannelReactionSent",
Self::ChannelConnected { .. } => "ChannelConnected",
Self::ChannelDisconnected { .. } => "ChannelDisconnected",
Self::CronJobTriggered { .. } => "CronJobTriggered",
Self::CronJobCompleted { .. } => "CronJobCompleted",
Self::CronDeliveryRequested { .. } => "CronDeliveryRequested",
Self::ProactiveMessageRequested { .. } => "ProactiveMessageRequested",
Self::SkillLoaded { .. } => "SkillLoaded",
Self::SkillStopped { .. } => "SkillStopped",
Self::SkillStartFailed { .. } => "SkillStartFailed",
Self::SkillExecuted { .. } => "SkillExecuted",
Self::ToolExecutionStarted { .. } => "ToolExecutionStarted",
Self::ToolExecutionCompleted { .. } => "ToolExecutionCompleted",
Self::WebhookIncomingRequest { .. } => "WebhookIncomingRequest",
Self::WebhookReceived { .. } => "WebhookReceived",
Self::WebhookRegistered { .. } => "WebhookRegistered",
Self::WebhookUnregistered { .. } => "WebhookUnregistered",
Self::WebhookProcessed { .. } => "WebhookProcessed",
Self::ComposioTriggerReceived { .. } => "ComposioTriggerReceived",
Self::ComposioConnectionCreated { .. } => "ComposioConnectionCreated",
Self::ComposioConnectionDeleted { .. } => "ComposioConnectionDeleted",
Self::ComposioActionExecuted { .. } => "ComposioActionExecuted",
Self::ComposioConfigChanged { .. } => "ComposioConfigChanged",
Self::TriggerEvaluated { .. } => "TriggerEvaluated",
Self::TriggerEscalated { .. } => "TriggerEscalated",
Self::TriggerEscalationFailed { .. } => "TriggerEscalationFailed",
Self::TreeSummarizerHourCompleted { .. } => "TreeSummarizerHourCompleted",
Self::TreeSummarizerPropagated { .. } => "TreeSummarizerPropagated",
Self::TreeSummarizerRebuildCompleted { .. } => "TreeSummarizerRebuildCompleted",
Self::NotificationIngested { .. } => "NotificationIngested",
Self::NotificationTriaged { .. } => "NotificationTriaged",
Self::DevicePaired { .. } => "DevicePaired",
Self::DeviceRevoked { .. } => "DeviceRevoked",
Self::DevicePeerOnline { .. } => "DevicePeerOnline",
Self::DevicePeerOffline { .. } => "DevicePeerOffline",
Self::DeviceTunnelFrame { .. } => "DeviceTunnelFrame",
Self::DeviceTunnelRegistered { .. } => "DeviceTunnelRegistered",
Self::CompanionSessionStarted { .. } => "CompanionSessionStarted",
Self::CompanionStateChanged { .. } => "CompanionStateChanged",
Self::CompanionSessionEnded { .. } => "CompanionSessionEnded",
Self::SystemStartup { .. } => "SystemStartup",
Self::SystemShutdown { .. } => "SystemShutdown",
Self::SystemRestartRequested { .. } => "SystemRestartRequested",
Self::SystemShutdownRequested { .. } => "SystemShutdownRequested",
Self::AutonomyConfigChanged => "AutonomyConfigChanged",
Self::HealthChanged { .. } => "HealthChanged",
Self::HealthRestarted { .. } => "HealthRestarted",
Self::SessionExpired { .. } => "SessionExpired",
Self::ApprovalRequested { .. } => "ApprovalRequested",
Self::ApprovalDecided { .. } => "ApprovalDecided",
Self::McpServerInstalled { .. } => "McpServerInstalled",
Self::McpServerConnected { .. } => "McpServerConnected",
Self::McpServerDisconnected { .. } => "McpServerDisconnected",
Self::McpClientToolExecuted { .. } => "McpClientToolExecuted",
Self::McpSetupSecretRequested { .. } => "McpSetupSecretRequested",
Self::EmbeddingModelUnhealthy { .. } => "EmbeddingModelUnhealthy",
}
}
/// Best-effort agent/session hint for display (not all events carry one).
pub fn agent_hint(&self) -> Option<&str> {
match self {
Self::AgentTurnStarted { session_id, .. }
| Self::AgentTurnCompleted { session_id, .. }
| Self::AgentError { session_id, .. } => Some(session_id.as_str()),
Self::SubagentSpawned { agent_id, .. }
| Self::SubagentCompleted { agent_id, .. }
| Self::SubagentFailed { agent_id, .. } => Some(agent_id.as_str()),
Self::ChannelMessageReceived { channel, .. }
| Self::ChannelConnected { channel, .. }
| Self::ChannelDisconnected { channel, .. } => Some(channel.as_str()),
Self::ToolExecutionStarted { tool_name, .. }
| Self::ToolExecutionCompleted { tool_name, .. } => Some(tool_name.as_str()),
_ => None,
}
}
}
#[cfg(test)]
+96
View File
@@ -862,6 +862,7 @@ pub fn build_core_http_router(socketio_enabled: bool) -> Router {
.route("/schema", get(schema_handler))
.route("/events", get(events_handler))
.route("/events/webhooks", get(webhook_events_handler))
.route("/events/domain", get(domain_events_handler))
.route("/rpc", post(rpc_handler))
.route("/ws/dictation", get(dictation_ws_handler))
.route("/auth", get(desktop_auth_handler))
@@ -1186,6 +1187,101 @@ async fn webhook_events_handler() -> Response {
.into_response()
}
/// SSE endpoint streaming DomainEvent bus events for the live event log panel.
///
/// Requires bearer auth. Streams all domain events as JSON with event type
/// set to the domain name (agent, tool, memory, etc.).
async fn domain_events_handler(headers: axum::http::HeaderMap) -> Response {
let bearer = headers
.get(header::AUTHORIZATION)
.and_then(|v| v.to_str().ok())
.and_then(|v| v.strip_prefix("Bearer "))
.map(str::trim)
.filter(|s| !s.is_empty());
let bearer_ok = bearer
.map(crate::core::auth::verify_bearer_token)
.unwrap_or(false);
if !bearer_ok {
log::warn!("[events/domain] reject subscribe: missing or invalid bearer token");
return (
StatusCode::UNAUTHORIZED,
Json(json!({
"ok": false,
"error": "unauthorized",
"message": "Bearer token required for domain event stream"
})),
)
.into_response();
}
// Read dashboard config for event stream settings.
let es_cfg = crate::openhuman::config::rpc::load_config_with_timeout()
.await
.map(|c| c.dashboard.event_stream)
.unwrap_or_default();
if !es_cfg.enabled {
return (
StatusCode::NOT_FOUND,
Json(json!({ "ok": false, "error": "event stream disabled by config" })),
)
.into_response();
}
let bus = match crate::core::event_bus::global() {
Some(bus) => bus,
None => {
log::warn!("[events/domain] event bus not initialized");
return (
StatusCode::SERVICE_UNAVAILABLE,
Json(json!({ "ok": false, "error": "event bus not initialized" })),
)
.into_response();
}
};
log::debug!("[events/domain] client connected, streaming domain events");
// Send config as first SSE event so frontend can apply settings.
let config_event = Event::default().event("config").data(
serde_json::to_string(&json!({
"max_entries": es_cfg.max_entries,
"new_entries": es_cfg.new_entries,
}))
.unwrap_or_default(),
);
let rx = bus.raw_receiver();
let event_stream = tokio_stream::wrappers::BroadcastStream::new(rx).filter_map(
|item| -> Option<Result<Event, std::convert::Infallible>> {
let event = match item {
Ok(ev) => ev,
Err(_) => return None,
};
let domain = event.domain().to_string();
let event_name = event.variant_name();
let agent = event.agent_hint().unwrap_or("").to_string();
let data = json!({
"domain": domain,
"event": event_name,
"agent": agent,
"timestamp": chrono::Utc::now().format("%H:%M:%S").to_string(),
});
let data_str = serde_json::to_string(&data).ok()?;
Some(Ok(Event::default().event(domain).data(data_str)))
},
);
let config_stream =
futures::stream::once(async move { Ok::<_, std::convert::Infallible>(config_event) });
let stream = config_stream.chain(event_stream);
Sse::new(stream)
.keep_alive(KeepAlive::new().interval(std::time::Duration::from_secs(5)))
.into_response()
}
/// Handler for the root endpoint, returning server information and available endpoints.
async fn root_handler() -> impl IntoResponse {
let api_server = match crate::openhuman::config::Config::load_or_init().await {
+55 -3
View File
@@ -1,3 +1,5 @@
//! Dashboard configuration (event stream, model health, future panels).
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -16,6 +18,7 @@ fn default_diagram_viewer_refresh_interval_seconds() -> u64 {
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(default)]
pub struct DashboardConfig {
#[serde(default)]
pub event_stream: EventStreamConfig,
#[serde(default)]
pub model_health: ModelHealthConfig,
@@ -36,17 +39,36 @@ impl Default for DashboardConfig {
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(default)]
pub struct EventStreamConfig {
#[serde(default = "default_es_enabled")]
/// Whether the live event stream endpoint is enabled.
#[serde(default = "default_enabled")]
pub enabled: bool,
/// Maximum number of entries the frontend should retain.
#[serde(default = "default_max_entries")]
pub max_entries: usize,
/// Where new entries appear: "top" (newest first) or "bottom" (oldest first).
#[serde(default = "default_new_entries")]
pub new_entries: String,
}
fn default_es_enabled() -> bool {
fn default_enabled() -> bool {
true
}
fn default_max_entries() -> usize {
200
}
fn default_new_entries() -> String {
"top".to_string()
}
impl Default for EventStreamConfig {
fn default() -> Self {
Self { enabled: true }
Self {
enabled: true,
max_entries: 200,
new_entries: "top".to_string(),
}
}
}
@@ -112,6 +134,36 @@ impl Default for DiagramViewerConfig {
mod tests {
use super::*;
#[test]
fn defaults_match_issue_spec() {
let cfg = DashboardConfig::default();
assert!(cfg.event_stream.enabled);
assert_eq!(cfg.event_stream.max_entries, 200);
assert_eq!(cfg.event_stream.new_entries, "top");
}
#[test]
fn deserialize_from_empty_json() {
let cfg: DashboardConfig = serde_json::from_value(serde_json::json!({})).unwrap();
assert!(cfg.event_stream.enabled);
assert_eq!(cfg.event_stream.max_entries, 200);
}
#[test]
fn deserialize_custom_values() {
let cfg: DashboardConfig = serde_json::from_value(serde_json::json!({
"event_stream": {
"enabled": false,
"max_entries": 500,
"new_entries": "bottom"
}
}))
.unwrap();
assert!(!cfg.event_stream.enabled);
assert_eq!(cfg.event_stream.max_entries, 500);
assert_eq!(cfg.event_stream.new_entries, "bottom");
}
#[test]
fn dashboard_config_defaults_enable_local_diagram_viewer() {
let config = DashboardConfig::default();
+2 -2
View File
@@ -87,10 +87,10 @@ pub struct Config {
pub temperature_unsupported_models: Vec<String>,
#[serde(default)]
pub observability: ObservabilityConfig,
pub dashboard: DashboardConfig,
#[serde(default)]
pub dashboard: DashboardConfig,
pub observability: ObservabilityConfig,
#[serde(default)]
pub autonomy: AutonomyConfig,