feat(intelligence): add architecture diagram viewer (#2687)

Signed-off-by: sunilkumarvalmiki <g.sunilkumarvalmiki@gmail.com>
Co-authored-by: sanil-23 <sanil@vezures.xyz>
This commit is contained in:
Sunil Kumar
2026-05-29 04:12:32 +05:30
committed by GitHub
co-authored by sanil-23
parent a6fd156131
commit f407065635
42 changed files with 747 additions and 40 deletions
@@ -0,0 +1,73 @@
import { fireEvent, screen } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import { renderWithProviders } from '../../test/test-utils';
import DiagramViewerTab, { buildDiagramImageUrl } from './DiagramViewerTab';
vi.mock('../../utils/tauriCommands/config', () => ({
openhumanGetDashboardSettings: vi
.fn()
.mockResolvedValue({
result: {
diagram_viewer: {
enabled: true,
source_url: 'http://localhost:8787/workspace/diagrams/latest.png',
refresh_interval_seconds: 10,
},
},
logs: [],
}),
}));
describe('buildDiagramImageUrl', () => {
it('adds a cache-busting refresh parameter to absolute URLs', () => {
expect(buildDiagramImageUrl('http://localhost:8787/latest.png?format=png', 4)).toBe(
'http://localhost:8787/latest.png?format=png&openhuman_refresh=4'
);
});
it('adds a cache-busting refresh parameter to relative URLs', () => {
expect(buildDiagramImageUrl('/workspace/diagrams/latest.png', 2)).toBe(
'/workspace/diagrams/latest.png?openhuman_refresh=2'
);
});
});
describe('DiagramViewerTab', () => {
it('refreshes the diagram image URL on demand', async () => {
renderWithProviders(<DiagramViewerTab />);
const image = await screen.findByRole('img', {
name: 'Latest generated OpenHuman architecture diagram',
});
expect(image).toHaveAttribute('src', expect.stringContaining('openhuman_refresh=0'));
fireEvent.click(screen.getByRole('button', { name: 'Refresh diagram' }));
expect(
screen.getByRole('img', { name: 'Latest generated OpenHuman architecture diagram' })
).toHaveAttribute('src', expect.stringContaining('openhuman_refresh=1'));
});
it('shows an empty state instead of a broken image after load failure', async () => {
renderWithProviders(<DiagramViewerTab />);
const image = await screen.findByRole('img', {
name: 'Latest generated OpenHuman architecture diagram',
});
fireEvent.error(image);
expect(screen.getByText('No diagram available yet')).toBeInTheDocument();
expect(
screen.getByText('npx skills add yizhiyanhua-ai/fireworks-tech-graph')
).toBeInTheDocument();
expect(
screen.getByText(
'Generate an architecture diagram of the current swarm in dark terminal style'
)
).toBeInTheDocument();
expect(
screen.queryByRole('img', { name: 'Latest generated OpenHuman architecture diagram' })
).not.toBeInTheDocument();
});
});
@@ -0,0 +1,160 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { LuImage, LuRefreshCw } from 'react-icons/lu';
import { useT } from '../../lib/i18n/I18nContext';
import {
type DiagramViewerSettings,
openhumanGetDashboardSettings,
} from '../../utils/tauriCommands/config';
const DEFAULT_SETTINGS: DiagramViewerSettings = {
enabled: true,
source_url: 'http://localhost:8787/workspace/diagrams/latest.png',
refresh_interval_seconds: 10,
};
type ImageState = 'idle' | 'loaded' | 'error';
function normalizeSettings(
settings?: Partial<DiagramViewerSettings> | null
): DiagramViewerSettings {
const sourceUrl = settings?.source_url?.trim() || DEFAULT_SETTINGS.source_url;
const refreshInterval = Number(settings?.refresh_interval_seconds);
return {
enabled: settings?.enabled ?? DEFAULT_SETTINGS.enabled,
source_url: sourceUrl,
refresh_interval_seconds:
Number.isFinite(refreshInterval) && refreshInterval > 0
? Math.round(refreshInterval)
: DEFAULT_SETTINGS.refresh_interval_seconds,
};
}
export function buildDiagramImageUrl(sourceUrl: string, refreshKey: number): string {
try {
const url = new URL(sourceUrl);
url.searchParams.set('openhuman_refresh', String(refreshKey));
return url.toString();
} catch {
const separator = sourceUrl.includes('?') ? '&' : '?';
return `${sourceUrl}${separator}openhuman_refresh=${refreshKey}`;
}
}
export default function DiagramViewerTab() {
const { t } = useT();
const [settings, setSettings] = useState<DiagramViewerSettings>(DEFAULT_SETTINGS);
const [refreshKey, setRefreshKey] = useState(0);
const [imageState, setImageState] = useState<ImageState>('idle');
useEffect(() => {
let alive = true;
openhumanGetDashboardSettings()
.then(response => {
if (!alive) return;
setSettings(normalizeSettings(response.result.diagram_viewer));
})
.catch(() => {
if (!alive) return;
setSettings(DEFAULT_SETTINGS);
});
return () => {
alive = false;
};
}, []);
const refreshDiagram = useCallback(() => {
setImageState('idle');
setRefreshKey(prev => prev + 1);
}, []);
useEffect(() => {
if (!settings.enabled || settings.refresh_interval_seconds <= 0) return undefined;
const interval = window.setInterval(refreshDiagram, settings.refresh_interval_seconds * 1000);
return () => window.clearInterval(interval);
}, [refreshDiagram, settings.enabled, settings.refresh_interval_seconds]);
const sourceUrl = settings.source_url.trim();
const imageUrl = useMemo(
() => (sourceUrl ? buildDiagramImageUrl(sourceUrl, refreshKey) : ''),
[refreshKey, sourceUrl]
);
const showImage = settings.enabled && sourceUrl.length > 0 && imageState !== 'error';
const showEmptyState = !settings.enabled || sourceUrl.length === 0 || imageState === 'error';
return (
<section className="space-y-5" aria-labelledby="diagram-viewer-title">
<div className="flex flex-wrap items-start justify-between gap-3">
<div>
<h2
id="diagram-viewer-title"
className="text-lg font-semibold text-stone-900 dark:text-neutral-100">
{t('intelligence.diagram.title')}
</h2>
<p className="mt-1 text-sm text-stone-500 dark:text-neutral-400">
{t('intelligence.diagram.description')}
</p>
</div>
<button
type="button"
onClick={refreshDiagram}
className="inline-flex items-center gap-2 rounded-md border border-stone-200 bg-white px-3 py-2 text-sm font-medium text-stone-700 transition-colors hover:bg-stone-50 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-200 dark:hover:bg-neutral-800"
aria-label={t('intelligence.diagram.refreshAria')}>
<LuRefreshCw aria-hidden="true" className="h-4 w-4" />
{t('intelligence.diagram.refresh')}
</button>
</div>
{showEmptyState && (
<div className="flex min-h-72 flex-col items-center justify-center gap-3 rounded-lg border border-dashed border-stone-300 bg-stone-50 px-6 py-10 text-center dark:border-neutral-700 dark:bg-neutral-950/60">
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-primary-50 text-primary-600 dark:bg-primary-500/10 dark:text-primary-300">
<LuImage aria-hidden="true" className="h-6 w-6" />
</div>
<div>
<h3 className="text-sm font-semibold text-stone-900 dark:text-neutral-100">
{t('intelligence.diagram.emptyTitle')}
</h3>
<p className="mt-1 max-w-md text-sm text-stone-500 dark:text-neutral-400">
{t('intelligence.diagram.emptyDescription')}
</p>
</div>
<div className="flex max-w-full flex-col gap-2">
<code className="max-w-full overflow-x-auto rounded-md bg-white px-3 py-2 text-xs text-stone-600 dark:bg-neutral-900 dark:text-neutral-300">
{t('intelligence.diagram.skillInstallCommand')}
</code>
<code className="max-w-full overflow-x-auto rounded-md bg-white px-3 py-2 text-xs text-stone-600 dark:bg-neutral-900 dark:text-neutral-300">
{t('intelligence.diagram.promptExample')}
</code>
</div>
</div>
)}
{showImage && (
<figure className="space-y-3">
<img
key={imageUrl}
src={imageUrl}
alt={t('intelligence.diagram.imageAlt')}
className="block w-full rounded-lg border border-stone-200 bg-white object-contain dark:border-neutral-800 dark:bg-neutral-950"
onLoad={() => setImageState('loaded')}
onError={() => setImageState('error')}
/>
<figcaption className="flex flex-wrap items-center justify-between gap-2 text-xs text-stone-500 dark:text-neutral-400">
<span>
{t('intelligence.diagram.refreshesEvery').replace(
'{seconds}',
String(settings.refresh_interval_seconds)
)}
</span>
<span className="max-w-full truncate">{sourceUrl}</span>
</figcaption>
</figure>
)}
</section>
);
}
+1
View File
@@ -178,6 +178,7 @@ const ar1: TranslationMap = {
'memory.tab.subconscious': 'اللاوعي',
'memory.tab.dreams': 'الأحلام',
'memory.tab.calls': 'المكالمات',
'memory.tab.diagram': 'Diagram',
'memory.tab.settings': 'الإعدادات',
'memory.analyzeNow': 'تحليل الآن',
'memoryTree.status.title': 'Memory Tree',
+13
View File
@@ -102,6 +102,19 @@ const ar4: TranslationMap = {
'intelligence.memoryChunk.scoreBars.dropped': 'مُسقَط',
'intelligence.memoryChunk.scoreBars.heading': 'س ب ب ا ل ح ف ظ',
'intelligence.memoryChunk.scoreBars.kept': 'محفوظ',
'intelligence.diagram.title': 'Architecture Diagram',
'intelligence.diagram.description':
'Latest local architecture output from the configured diagram endpoint.',
'intelligence.diagram.refresh': 'Refresh',
'intelligence.diagram.refreshAria': 'Refresh diagram',
'intelligence.diagram.emptyTitle': 'No diagram available yet',
'intelligence.diagram.emptyDescription':
'Generate an architecture diagram from the orchestrator and this panel will refresh from the configured local endpoint.',
'intelligence.diagram.skillInstallCommand': 'npx skills add yizhiyanhua-ai/fireworks-tech-graph',
'intelligence.diagram.promptExample':
'Generate an architecture diagram of the current swarm in dark terminal style',
'intelligence.diagram.imageAlt': 'Latest generated OpenHuman architecture diagram',
'intelligence.diagram.refreshesEvery': 'Refreshes every {seconds}s',
'intelligence.memoryText.entityTypePrefix': 'نوع الكيان',
'intelligence.screenDebug.active': 'نشط',
'intelligence.screenDebug.app': 'التطبيق',
+1
View File
@@ -180,6 +180,7 @@ const bn1: TranslationMap = {
'memory.tab.subconscious': 'সাবকনশাস',
'memory.tab.dreams': 'স্বপ্ন',
'memory.tab.calls': 'কল',
'memory.tab.diagram': 'Diagram',
'memory.tab.settings': 'সেটিংস',
'memory.analyzeNow': 'এখনই বিশ্লেষণ করুন',
'memoryTree.status.title': 'Memory Tree',
+13
View File
@@ -103,6 +103,19 @@ const bn4: TranslationMap = {
'intelligence.memoryChunk.scoreBars.dropped': 'বাদ দেওয়া হয়েছে',
'intelligence.memoryChunk.scoreBars.heading': 'কে ন রা খা',
'intelligence.memoryChunk.scoreBars.kept': 'রাখা হয়েছে',
'intelligence.diagram.title': 'Architecture Diagram',
'intelligence.diagram.description':
'Latest local architecture output from the configured diagram endpoint.',
'intelligence.diagram.refresh': 'Refresh',
'intelligence.diagram.refreshAria': 'Refresh diagram',
'intelligence.diagram.emptyTitle': 'No diagram available yet',
'intelligence.diagram.emptyDescription':
'Generate an architecture diagram from the orchestrator and this panel will refresh from the configured local endpoint.',
'intelligence.diagram.skillInstallCommand': 'npx skills add yizhiyanhua-ai/fireworks-tech-graph',
'intelligence.diagram.promptExample':
'Generate an architecture diagram of the current swarm in dark terminal style',
'intelligence.diagram.imageAlt': 'Latest generated OpenHuman architecture diagram',
'intelligence.diagram.refreshesEvery': 'Refreshes every {seconds}s',
'intelligence.memoryText.entityTypePrefix': 'এনটিটি ধরন',
'intelligence.screenDebug.active': 'সক্রিয়',
'intelligence.screenDebug.app': 'অ্যাপ',
+1
View File
@@ -219,6 +219,7 @@ const de1: TranslationMap = {
'memory.tab.subconscious': 'Unterbewusstsein',
'memory.tab.dreams': 'Träume',
'memory.tab.calls': 'Anrufe',
'memory.tab.diagram': 'Diagram',
'memory.tab.settings': 'Einstellungen',
'memory.analyzeNow': 'Jetzt analysieren',
'memoryTree.status.title': 'Memory Tree',
+13
View File
@@ -103,6 +103,19 @@ const de4: TranslationMap = {
'intelligence.memoryChunk.scoreBars.dropped': 'fallen gelassen',
'intelligence.memoryChunk.scoreBars.heading': 'Warum hast du es behalten?',
'intelligence.memoryChunk.scoreBars.kept': 'gehalten',
'intelligence.diagram.title': 'Architecture Diagram',
'intelligence.diagram.description':
'Latest local architecture output from the configured diagram endpoint.',
'intelligence.diagram.refresh': 'Refresh',
'intelligence.diagram.refreshAria': 'Refresh diagram',
'intelligence.diagram.emptyTitle': 'No diagram available yet',
'intelligence.diagram.emptyDescription':
'Generate an architecture diagram from the orchestrator and this panel will refresh from the configured local endpoint.',
'intelligence.diagram.skillInstallCommand': 'npx skills add yizhiyanhua-ai/fireworks-tech-graph',
'intelligence.diagram.promptExample':
'Generate an architecture diagram of the current swarm in dark terminal style',
'intelligence.diagram.imageAlt': 'Latest generated OpenHuman architecture diagram',
'intelligence.diagram.refreshesEvery': 'Refreshes every {seconds}s',
'intelligence.memoryText.entityTypePrefix': 'Entitätstyp',
'intelligence.screenDebug.active': 'Aktiv',
'intelligence.screenDebug.app': 'App',
+1
View File
@@ -487,6 +487,7 @@ const en1: TranslationMap = {
'memory.tab.subconscious': 'Subconscious',
'memory.tab.dreams': 'Dreams',
'memory.tab.calls': 'Calls',
'memory.tab.diagram': 'Diagram',
'memory.tab.settings': 'Settings',
'memory.analyzeNow': 'Analyze Now',
'memoryTree.status.title': 'Memory Tree',
+13
View File
@@ -111,6 +111,19 @@ const en4: TranslationMap = {
'intelligence.memoryChunk.scoreBars.dropped': 'dropped',
'intelligence.memoryChunk.scoreBars.heading': 'w h y k e p t',
'intelligence.memoryChunk.scoreBars.kept': 'kept',
'intelligence.diagram.title': 'Architecture Diagram',
'intelligence.diagram.description':
'Latest local architecture output from the configured diagram endpoint.',
'intelligence.diagram.refresh': 'Refresh',
'intelligence.diagram.refreshAria': 'Refresh diagram',
'intelligence.diagram.emptyTitle': 'No diagram available yet',
'intelligence.diagram.emptyDescription':
'Generate an architecture diagram from the orchestrator and this panel will refresh from the configured local endpoint.',
'intelligence.diagram.skillInstallCommand': 'npx skills add yizhiyanhua-ai/fireworks-tech-graph',
'intelligence.diagram.promptExample':
'Generate an architecture diagram of the current swarm in dark terminal style',
'intelligence.diagram.imageAlt': 'Latest generated OpenHuman architecture diagram',
'intelligence.diagram.refreshesEvery': 'Refreshes every {seconds}s',
'intelligence.memoryText.entityTypePrefix': 'Entity type',
'intelligence.screenDebug.active': 'Active',
'intelligence.screenDebug.app': 'App',
+1
View File
@@ -186,6 +186,7 @@ const es1: TranslationMap = {
'memory.tab.subconscious': 'Subconsciente',
'memory.tab.dreams': 'Sueños',
'memory.tab.calls': 'Llamadas',
'memory.tab.diagram': 'Diagram',
'memory.tab.settings': 'Configuración',
'memory.analyzeNow': 'Analizar ahora',
'memoryTree.status.title': 'Memory Tree',
+13
View File
@@ -103,6 +103,19 @@ const es4: TranslationMap = {
'intelligence.memoryChunk.scoreBars.dropped': 'descartado',
'intelligence.memoryChunk.scoreBars.heading': 'p o r q u é s e c o n s e r v ó',
'intelligence.memoryChunk.scoreBars.kept': 'conservado',
'intelligence.diagram.title': 'Architecture Diagram',
'intelligence.diagram.description':
'Latest local architecture output from the configured diagram endpoint.',
'intelligence.diagram.refresh': 'Refresh',
'intelligence.diagram.refreshAria': 'Refresh diagram',
'intelligence.diagram.emptyTitle': 'No diagram available yet',
'intelligence.diagram.emptyDescription':
'Generate an architecture diagram from the orchestrator and this panel will refresh from the configured local endpoint.',
'intelligence.diagram.skillInstallCommand': 'npx skills add yizhiyanhua-ai/fireworks-tech-graph',
'intelligence.diagram.promptExample':
'Generate an architecture diagram of the current swarm in dark terminal style',
'intelligence.diagram.imageAlt': 'Latest generated OpenHuman architecture diagram',
'intelligence.diagram.refreshesEvery': 'Refreshes every {seconds}s',
'intelligence.memoryText.entityTypePrefix': 'Tipo de entidad',
'intelligence.screenDebug.active': 'Activo',
'intelligence.screenDebug.app': 'Aplicación',
+1
View File
@@ -186,6 +186,7 @@ const fr1: TranslationMap = {
'memory.tab.subconscious': 'Subconscient',
'memory.tab.dreams': 'Rêves',
'memory.tab.calls': 'Appels',
'memory.tab.diagram': 'Diagram',
'memory.tab.settings': 'Paramètres',
'memory.analyzeNow': 'Analyser maintenant',
'memoryTree.status.title': 'Memory Tree',
+13
View File
@@ -103,6 +103,19 @@ const fr4: TranslationMap = {
'intelligence.memoryChunk.scoreBars.dropped': 'abandonné',
'intelligence.memoryChunk.scoreBars.heading': 'p o u r q u o i c o n s e r v é',
'intelligence.memoryChunk.scoreBars.kept': 'conservé',
'intelligence.diagram.title': 'Architecture Diagram',
'intelligence.diagram.description':
'Latest local architecture output from the configured diagram endpoint.',
'intelligence.diagram.refresh': 'Refresh',
'intelligence.diagram.refreshAria': 'Refresh diagram',
'intelligence.diagram.emptyTitle': 'No diagram available yet',
'intelligence.diagram.emptyDescription':
'Generate an architecture diagram from the orchestrator and this panel will refresh from the configured local endpoint.',
'intelligence.diagram.skillInstallCommand': 'npx skills add yizhiyanhua-ai/fireworks-tech-graph',
'intelligence.diagram.promptExample':
'Generate an architecture diagram of the current swarm in dark terminal style',
'intelligence.diagram.imageAlt': 'Latest generated OpenHuman architecture diagram',
'intelligence.diagram.refreshesEvery': 'Refreshes every {seconds}s',
'intelligence.memoryText.entityTypePrefix': "Type d'entité",
'intelligence.screenDebug.active': 'Actif',
'intelligence.screenDebug.app': 'Application',
+1
View File
@@ -178,6 +178,7 @@ const hi1: TranslationMap = {
'memory.tab.subconscious': 'सबकॉन्शस',
'memory.tab.dreams': 'ड्रीम्स',
'memory.tab.calls': 'कॉल्स',
'memory.tab.diagram': 'Diagram',
'memory.tab.settings': 'सेटिंग्स',
'memory.analyzeNow': 'अभी एनालाइज़ करें',
'memoryTree.status.title': 'Memory Tree',
+13
View File
@@ -103,6 +103,19 @@ const hi4: TranslationMap = {
'intelligence.memoryChunk.scoreBars.dropped': 'हटाया गया',
'intelligence.memoryChunk.scoreBars.heading': 'क्यों रखा',
'intelligence.memoryChunk.scoreBars.kept': 'रखा गया',
'intelligence.diagram.title': 'Architecture Diagram',
'intelligence.diagram.description':
'Latest local architecture output from the configured diagram endpoint.',
'intelligence.diagram.refresh': 'Refresh',
'intelligence.diagram.refreshAria': 'Refresh diagram',
'intelligence.diagram.emptyTitle': 'No diagram available yet',
'intelligence.diagram.emptyDescription':
'Generate an architecture diagram from the orchestrator and this panel will refresh from the configured local endpoint.',
'intelligence.diagram.skillInstallCommand': 'npx skills add yizhiyanhua-ai/fireworks-tech-graph',
'intelligence.diagram.promptExample':
'Generate an architecture diagram of the current swarm in dark terminal style',
'intelligence.diagram.imageAlt': 'Latest generated OpenHuman architecture diagram',
'intelligence.diagram.refreshesEvery': 'Refreshes every {seconds}s',
'intelligence.memoryText.entityTypePrefix': 'इकाई प्रकार',
'intelligence.screenDebug.active': 'एक्टिव',
'intelligence.screenDebug.app': 'ऐप',
+1
View File
@@ -179,6 +179,7 @@ const id1: TranslationMap = {
'memory.tab.subconscious': 'Bawah sadar',
'memory.tab.dreams': 'Mimpi',
'memory.tab.calls': 'Panggilan',
'memory.tab.diagram': 'Diagram',
'memory.tab.settings': 'Pengaturan',
'memory.analyzeNow': 'Analisis Sekarang',
'memoryTree.status.title': 'Memory Tree',
+13
View File
@@ -103,6 +103,19 @@ const id4: TranslationMap = {
'intelligence.memoryChunk.scoreBars.dropped': 'dibuang',
'intelligence.memoryChunk.scoreBars.heading': 'm e n g a p a d i s i m p a n',
'intelligence.memoryChunk.scoreBars.kept': 'dipertahankan',
'intelligence.diagram.title': 'Architecture Diagram',
'intelligence.diagram.description':
'Latest local architecture output from the configured diagram endpoint.',
'intelligence.diagram.refresh': 'Refresh',
'intelligence.diagram.refreshAria': 'Refresh diagram',
'intelligence.diagram.emptyTitle': 'No diagram available yet',
'intelligence.diagram.emptyDescription':
'Generate an architecture diagram from the orchestrator and this panel will refresh from the configured local endpoint.',
'intelligence.diagram.skillInstallCommand': 'npx skills add yizhiyanhua-ai/fireworks-tech-graph',
'intelligence.diagram.promptExample':
'Generate an architecture diagram of the current swarm in dark terminal style',
'intelligence.diagram.imageAlt': 'Latest generated OpenHuman architecture diagram',
'intelligence.diagram.refreshesEvery': 'Refreshes every {seconds}s',
'intelligence.memoryText.entityTypePrefix': 'Tipe entitas',
'intelligence.screenDebug.active': 'Aktif',
'intelligence.screenDebug.app': 'Aplikasi',
+1
View File
@@ -184,6 +184,7 @@ const it1: TranslationMap = {
'memory.tab.subconscious': 'Subconscio',
'memory.tab.dreams': 'Sogni',
'memory.tab.calls': 'Chiamate',
'memory.tab.diagram': 'Diagram',
'memory.tab.settings': 'Impostazioni',
'memory.analyzeNow': 'Analizza ora',
'memoryTree.status.title': 'Memory Tree',
+13
View File
@@ -103,6 +103,19 @@ const it4: TranslationMap = {
'intelligence.memoryChunk.scoreBars.dropped': 'scartati',
'intelligence.memoryChunk.scoreBars.heading': 'p e r c h é t e n u t i',
'intelligence.memoryChunk.scoreBars.kept': 'tenuti',
'intelligence.diagram.title': 'Architecture Diagram',
'intelligence.diagram.description':
'Latest local architecture output from the configured diagram endpoint.',
'intelligence.diagram.refresh': 'Refresh',
'intelligence.diagram.refreshAria': 'Refresh diagram',
'intelligence.diagram.emptyTitle': 'No diagram available yet',
'intelligence.diagram.emptyDescription':
'Generate an architecture diagram from the orchestrator and this panel will refresh from the configured local endpoint.',
'intelligence.diagram.skillInstallCommand': 'npx skills add yizhiyanhua-ai/fireworks-tech-graph',
'intelligence.diagram.promptExample':
'Generate an architecture diagram of the current swarm in dark terminal style',
'intelligence.diagram.imageAlt': 'Latest generated OpenHuman architecture diagram',
'intelligence.diagram.refreshesEvery': 'Refreshes every {seconds}s',
'intelligence.memoryText.entityTypePrefix': 'Tipo di entità',
'intelligence.screenDebug.active': 'Attivo',
'intelligence.screenDebug.app': 'App',
+1
View File
@@ -178,6 +178,7 @@ const ko1: TranslationMap = {
'memory.tab.subconscious': '잠재의식',
'memory.tab.dreams': '꿈',
'memory.tab.calls': '통화',
'memory.tab.diagram': 'Diagram',
'memory.tab.settings': '설정',
'memory.analyzeNow': '지금 분석',
'memoryTree.status.title': 'Memory Tree',
+13
View File
@@ -94,6 +94,19 @@ const ko4: TranslationMap = {
'intelligence.memoryChunk.scoreBars.dropped': '제외됨',
'intelligence.memoryChunk.scoreBars.heading': '유지된 이유',
'intelligence.memoryChunk.scoreBars.kept': '유지됨',
'intelligence.diagram.title': 'Architecture Diagram',
'intelligence.diagram.description':
'Latest local architecture output from the configured diagram endpoint.',
'intelligence.diagram.refresh': 'Refresh',
'intelligence.diagram.refreshAria': 'Refresh diagram',
'intelligence.diagram.emptyTitle': 'No diagram available yet',
'intelligence.diagram.emptyDescription':
'Generate an architecture diagram from the orchestrator and this panel will refresh from the configured local endpoint.',
'intelligence.diagram.skillInstallCommand': 'npx skills add yizhiyanhua-ai/fireworks-tech-graph',
'intelligence.diagram.promptExample':
'Generate an architecture diagram of the current swarm in dark terminal style',
'intelligence.diagram.imageAlt': 'Latest generated OpenHuman architecture diagram',
'intelligence.diagram.refreshesEvery': 'Refreshes every {seconds}s',
'intelligence.memoryText.entityTypePrefix': '엔터티 유형',
'intelligence.screenDebug.active': '활성',
'intelligence.screenDebug.app': '앱',
+1
View File
@@ -185,6 +185,7 @@ const pt1: TranslationMap = {
'memory.tab.subconscious': 'Subconsciente',
'memory.tab.dreams': 'Sonhos',
'memory.tab.calls': 'Chamadas',
'memory.tab.diagram': 'Diagram',
'memory.tab.settings': 'Configurações',
'memory.analyzeNow': 'Analisar Agora',
'memoryTree.status.title': 'Memory Tree',
+13
View File
@@ -104,6 +104,19 @@ const pt4: TranslationMap = {
'intelligence.memoryChunk.scoreBars.dropped': 'descartado',
'intelligence.memoryChunk.scoreBars.heading': 'p o r q u ê m a n t i d o',
'intelligence.memoryChunk.scoreBars.kept': 'mantido',
'intelligence.diagram.title': 'Architecture Diagram',
'intelligence.diagram.description':
'Latest local architecture output from the configured diagram endpoint.',
'intelligence.diagram.refresh': 'Refresh',
'intelligence.diagram.refreshAria': 'Refresh diagram',
'intelligence.diagram.emptyTitle': 'No diagram available yet',
'intelligence.diagram.emptyDescription':
'Generate an architecture diagram from the orchestrator and this panel will refresh from the configured local endpoint.',
'intelligence.diagram.skillInstallCommand': 'npx skills add yizhiyanhua-ai/fireworks-tech-graph',
'intelligence.diagram.promptExample':
'Generate an architecture diagram of the current swarm in dark terminal style',
'intelligence.diagram.imageAlt': 'Latest generated OpenHuman architecture diagram',
'intelligence.diagram.refreshesEvery': 'Refreshes every {seconds}s',
'intelligence.memoryText.entityTypePrefix': 'Tipo de entidade',
'intelligence.screenDebug.active': 'Ativo',
'intelligence.screenDebug.app': 'Aplicativo',
+1
View File
@@ -179,6 +179,7 @@ const ru1: TranslationMap = {
'memory.tab.subconscious': 'Подсознание',
'memory.tab.dreams': 'Сны',
'memory.tab.calls': 'Звонки',
'memory.tab.diagram': 'Diagram',
'memory.tab.settings': 'Настройки',
'memory.analyzeNow': 'Анализировать сейчас',
'memoryTree.status.title': 'Memory Tree',
+13
View File
@@ -103,6 +103,19 @@ const ru4: TranslationMap = {
'intelligence.memoryChunk.scoreBars.dropped': 'отброшено',
'intelligence.memoryChunk.scoreBars.heading': 'п о ч е м у с о х р а н е н о',
'intelligence.memoryChunk.scoreBars.kept': 'сохранено',
'intelligence.diagram.title': 'Architecture Diagram',
'intelligence.diagram.description':
'Latest local architecture output from the configured diagram endpoint.',
'intelligence.diagram.refresh': 'Refresh',
'intelligence.diagram.refreshAria': 'Refresh diagram',
'intelligence.diagram.emptyTitle': 'No diagram available yet',
'intelligence.diagram.emptyDescription':
'Generate an architecture diagram from the orchestrator and this panel will refresh from the configured local endpoint.',
'intelligence.diagram.skillInstallCommand': 'npx skills add yizhiyanhua-ai/fireworks-tech-graph',
'intelligence.diagram.promptExample':
'Generate an architecture diagram of the current swarm in dark terminal style',
'intelligence.diagram.imageAlt': 'Latest generated OpenHuman architecture diagram',
'intelligence.diagram.refreshesEvery': 'Refreshes every {seconds}s',
'intelligence.memoryText.entityTypePrefix': 'Тип сущности',
'intelligence.screenDebug.active': 'Активно',
'intelligence.screenDebug.app': 'Приложение',
+1
View File
@@ -174,6 +174,7 @@ const zhCN1: TranslationMap = {
'memory.tab.subconscious': '潜意识',
'memory.tab.dreams': '梦境',
'memory.tab.calls': '调用记录',
'memory.tab.diagram': 'Diagram',
'memory.tab.settings': '设置',
'memory.analyzeNow': '立即分析',
'memoryTree.status.title': 'Memory Tree',
+13
View File
@@ -100,6 +100,19 @@ const zhCN4: TranslationMap = {
'intelligence.memoryChunk.scoreBars.dropped': '已丢弃',
'intelligence.memoryChunk.scoreBars.heading': '保留原因',
'intelligence.memoryChunk.scoreBars.kept': '已保留',
'intelligence.diagram.title': 'Architecture Diagram',
'intelligence.diagram.description':
'Latest local architecture output from the configured diagram endpoint.',
'intelligence.diagram.refresh': 'Refresh',
'intelligence.diagram.refreshAria': 'Refresh diagram',
'intelligence.diagram.emptyTitle': 'No diagram available yet',
'intelligence.diagram.emptyDescription':
'Generate an architecture diagram from the orchestrator and this panel will refresh from the configured local endpoint.',
'intelligence.diagram.skillInstallCommand': 'npx skills add yizhiyanhua-ai/fireworks-tech-graph',
'intelligence.diagram.promptExample':
'Generate an architecture diagram of the current swarm in dark terminal style',
'intelligence.diagram.imageAlt': 'Latest generated OpenHuman architecture diagram',
'intelligence.diagram.refreshesEvery': 'Refreshes every {seconds}s',
'intelligence.memoryText.entityTypePrefix': '实体类型',
'intelligence.screenDebug.active': '活跃',
'intelligence.screenDebug.app': '应用',
+14
View File
@@ -295,6 +295,7 @@ const en: TranslationMap = {
'memory.tab.subconscious': 'Subconscious',
'memory.tab.dreams': 'Dreams',
'memory.tab.calls': 'Calls',
'memory.tab.diagram': 'Diagram',
'memory.tab.settings': 'Settings',
'memory.analyzeNow': 'Analyze Now',
@@ -2431,6 +2432,19 @@ const en: TranslationMap = {
'intelligence.memoryChunk.scoreBars.dropped': 'dropped',
'intelligence.memoryChunk.scoreBars.heading': 'w h y k e p t',
'intelligence.memoryChunk.scoreBars.kept': 'kept',
'intelligence.diagram.title': 'Architecture Diagram',
'intelligence.diagram.description':
'Latest local architecture output from the configured diagram endpoint.',
'intelligence.diagram.refresh': 'Refresh',
'intelligence.diagram.refreshAria': 'Refresh diagram',
'intelligence.diagram.emptyTitle': 'No diagram available yet',
'intelligence.diagram.emptyDescription':
'Generate an architecture diagram from the orchestrator and this panel will refresh from the configured local endpoint.',
'intelligence.diagram.skillInstallCommand': 'npx skills add yizhiyanhua-ai/fireworks-tech-graph',
'intelligence.diagram.promptExample':
'Generate an architecture diagram of the current swarm in dark terminal style',
'intelligence.diagram.imageAlt': 'Latest generated OpenHuman architecture diagram',
'intelligence.diagram.refreshesEvery': 'Refreshes every {seconds}s',
'intelligence.memoryText.entityTypePrefix': 'Entity type',
'intelligence.screenDebug.active': 'Active',
'intelligence.screenDebug.app': 'App',
+5 -1
View File
@@ -1,6 +1,7 @@
import { useCallback, useEffect, useState } from 'react';
import { ConfirmationModal } from '../components/intelligence/ConfirmationModal';
import DiagramViewerTab from '../components/intelligence/DiagramViewerTab';
import IntelligenceCallsTab from '../components/intelligence/IntelligenceCallsTab';
import IntelligenceDreamsTab from '../components/intelligence/IntelligenceDreamsTab';
import IntelligenceSubconsciousTab from '../components/intelligence/IntelligenceSubconsciousTab';
@@ -19,7 +20,7 @@ import type {
ToastNotification,
} from '../types/intelligence';
type IntelligenceTab = 'memory' | 'subconscious' | 'calls' | 'dreams' | 'tasks';
type IntelligenceTab = 'memory' | 'subconscious' | 'calls' | 'dreams' | 'tasks' | 'diagram';
export default function Intelligence() {
const { t } = useT();
@@ -91,6 +92,7 @@ export default function Intelligence() {
{ id: 'memory', label: t('memory.tab.memory') },
{ id: 'subconscious', label: t('memory.tab.subconscious') },
{ id: 'tasks', label: 'Tasks' },
{ id: 'diagram', label: t('memory.tab.diagram') },
{ id: 'calls', label: t('memory.tab.calls') },
{ id: 'dreams', label: t('memory.tab.dreams') },
];
@@ -170,6 +172,8 @@ export default function Intelligence() {
{activeTab === 'tasks' && <IntelligenceTasksTab />}
{activeTab === 'diagram' && <DiagramViewerTab />}
{activeTab === 'calls' && <IntelligenceCallsTab onToast={addToast} />}
{activeTab === 'dreams' && <IntelligenceDreamsTab />}
@@ -0,0 +1,72 @@
import { fireEvent, screen } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import { renderWithProviders } from '../../test/test-utils';
import Intelligence from '../Intelligence';
vi.mock('../../components/intelligence/MemoryWorkspace', () => ({
MemoryWorkspace: () => <div>Memory workspace</div>,
}));
vi.mock('../../components/intelligence/IntelligenceSubconsciousTab', () => ({
default: () => <div>Subconscious tab</div>,
}));
vi.mock('../../components/intelligence/IntelligenceTasksTab', () => ({
default: () => <div>Tasks tab</div>,
}));
vi.mock('../../components/intelligence/IntelligenceCallsTab', () => ({
default: () => <div>Calls tab</div>,
}));
vi.mock('../../components/intelligence/IntelligenceDreamsTab', () => ({
default: () => <div>Dreams tab</div>,
}));
vi.mock('../../hooks/useConsciousItems', () => ({
useConsciousItems: () => ({ isRunning: false }),
}));
vi.mock('../../hooks/useIntelligenceStats', () => ({
useIntelligenceStats: () => ({ aiStatus: 'ready' }),
}));
vi.mock('../../hooks/useMemoryIngestionStatus', () => ({
useMemoryIngestionStatus: () => ({ status: { running: false, queueDepth: 0 } }),
}));
const connectMock = vi.fn();
vi.mock('../../hooks/useIntelligenceSocket', () => ({
useIntelligenceSocket: () => ({ isConnected: true }),
useIntelligenceSocketManager: () => ({ connect: connectMock }),
}));
vi.mock('../../hooks/useSubconscious', () => ({
useSubconscious: () => ({
tasks: [],
escalations: [],
logEntries: [],
status: 'idle',
loading: false,
triggering: false,
triggerTick: vi.fn(),
addTask: vi.fn(),
removeTask: vi.fn(),
toggleTask: vi.fn(),
approveEscalation: vi.fn(),
dismissEscalation: vi.fn(),
}),
}));
describe('Intelligence diagram tab', () => {
it('shows an architecture diagram viewer from the Intelligence tabs', () => {
renderWithProviders(<Intelligence />);
fireEvent.click(screen.getByRole('tab', { name: 'Diagram' }));
expect(screen.getByRole('heading', { name: 'Architecture Diagram' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Refresh diagram' })).toBeInTheDocument();
});
});
+2
View File
@@ -3,6 +3,7 @@ export const CORE_RPC_METHODS = {
configGetAnalyticsSettings: 'openhuman.config_get_analytics_settings',
configGetAutonomySettings: 'openhuman.config_get_autonomy_settings',
configGetComposioTriggerSettings: 'openhuman.config_get_composio_trigger_settings',
configGetDashboardSettings: 'openhuman.config_get_dashboard_settings',
configGetRuntimeFlags: 'openhuman.config_get_runtime_flags',
configGetSearchSettings: 'openhuman.config_get_search_settings',
configUpdateSearchSettings: 'openhuman.config_update_search_settings',
@@ -53,6 +54,7 @@ export const LEGACY_METHOD_ALIASES: Record<string, CoreRpcMethod> = {
'openhuman.tool_registry_call': CORE_RPC_METHODS.mcpClientsToolCall,
'openhuman.get_analytics_settings': CORE_RPC_METHODS.configGetAnalyticsSettings,
'openhuman.get_composio_trigger_settings': CORE_RPC_METHODS.configGetComposioTriggerSettings,
'openhuman.get_dashboard_settings': CORE_RPC_METHODS.configGetDashboardSettings,
'openhuman.get_config': CORE_RPC_METHODS.configGet,
'openhuman.get_runtime_flags': CORE_RPC_METHODS.configGetRuntimeFlags,
'openhuman.ping': CORE_RPC_METHODS.corePing,
+19
View File
@@ -450,6 +450,25 @@ export interface SearchSettings {
allow_all: boolean;
}
export interface DiagramViewerSettings {
enabled: boolean;
source_url: string;
refresh_interval_seconds: number;
}
export interface DashboardSettings {
diagram_viewer: DiagramViewerSettings;
}
export async function openhumanGetDashboardSettings(): Promise<CommandResponse<DashboardSettings>> {
if (!isTauri()) {
throw new Error('Not running in Tauri');
}
return await callCoreRpc<CommandResponse<DashboardSettings>>({
method: CORE_RPC_METHODS.configGetDashboardSettings,
});
}
export async function openhumanGetSearchSettings(): Promise<CommandResponse<SearchSettings>> {
if (!isTauri()) {
throw new Error('Not running in Tauri');
+4
View File
@@ -36,6 +36,10 @@ const LEGACY_ALIASES: &[(&str, &str)] = &[
"openhuman.get_composio_trigger_settings",
"openhuman.config_get_composio_trigger_settings",
),
(
"openhuman.get_dashboard_settings",
"openhuman.config_get_dashboard_settings",
),
("openhuman.get_config", "openhuman.config_get"),
(
"openhuman.get_runtime_flags",
+16 -16
View File
@@ -27,22 +27,22 @@ pub use schema::{
set_runtime_proxy_config, AgentConfig, AuditConfig, AutocompleteConfig, AutonomyConfig,
BrowserComputerUseConfig, BrowserConfig, CapabilityProviderConfig,
CapabilityProviderTrustState, ChannelsConfig, ComposioConfig, Config, ContextConfig,
CostConfig, CronConfig, CurlConfig, DelegateAgentConfig, DictationActivationMode,
DictationConfig, DiscordConfig, DockerRuntimeConfig, EmbeddingRouteConfig, GitbooksConfig,
HeartbeatConfig, HttpRequestConfig, IMessageConfig, IntegrationToggle, IntegrationsConfig,
LarkConfig, LearningConfig, LlmBackend, LocalAiConfig, MatrixConfig, McpAuthConfig,
McpClientConfig, McpClientIdentityConfig, McpServerConfig, MeetConfig, MemoryConfig,
MemoryTreeConfig, ModelRouteConfig, MultimodalConfig, ObservabilityConfig,
OrchestratorModelConfig, PolymarketClobCredentials, PolymarketConfig, ProxyConfig, ProxyScope,
ReflectionSource, ReliabilityConfig, ResourceLimitsConfig, RuntimeConfig, SandboxBackend,
SandboxConfig, SchedulerConfig, SchedulerGateConfig, SchedulerGateMode,
ScreenIntelligenceConfig, SearchConfig, SearchEngine, SearchEngineCredentials, SearxngConfig,
SecretsConfig, SecurityConfig, SlackConfig, StorageConfig, StorageProviderConfig,
StorageProviderSection, StreamMode, TeamModelConfig, TelegramConfig, UpdateConfig,
UpdateRestartStrategy, VoiceActivationMode, VoiceServerConfig, WebSearchConfig, WebhookConfig,
DEFAULT_CLOUD_LLM_MODEL, DEFAULT_MODEL, MODEL_AGENTIC_V1, MODEL_CHAT_V1, MODEL_CODING_V1,
MODEL_REASONING_QUICK_V1, MODEL_REASONING_V1, MODEL_SUMMARIZATION_V1, SEARCH_ENGINE_BRAVE,
SEARCH_ENGINE_MANAGED, SEARCH_ENGINE_PARALLEL,
CostConfig, CronConfig, CurlConfig, DashboardConfig, DelegateAgentConfig, DiagramViewerConfig,
DictationActivationMode, DictationConfig, DiscordConfig, DockerRuntimeConfig,
EmbeddingRouteConfig, GitbooksConfig, HeartbeatConfig, HttpRequestConfig, IMessageConfig,
IntegrationToggle, IntegrationsConfig, LarkConfig, LearningConfig, LlmBackend, LocalAiConfig,
MatrixConfig, McpAuthConfig, McpClientConfig, McpClientIdentityConfig, McpServerConfig,
MeetConfig, MemoryConfig, MemoryTreeConfig, ModelRouteConfig, MultimodalConfig,
ObservabilityConfig, OrchestratorModelConfig, PolymarketClobCredentials, PolymarketConfig,
ProxyConfig, ProxyScope, ReflectionSource, ReliabilityConfig, ResourceLimitsConfig,
RuntimeConfig, SandboxBackend, SandboxConfig, SchedulerConfig, SchedulerGateConfig,
SchedulerGateMode, ScreenIntelligenceConfig, SearchConfig, SearchEngine,
SearchEngineCredentials, SearxngConfig, SecretsConfig, SecurityConfig, SlackConfig,
StorageConfig, StorageProviderConfig, StorageProviderSection, StreamMode, TeamModelConfig,
TelegramConfig, UpdateConfig, UpdateRestartStrategy, VoiceActivationMode, VoiceServerConfig,
WebSearchConfig, WebhookConfig, DEFAULT_CLOUD_LLM_MODEL, DEFAULT_MODEL, MODEL_AGENTIC_V1,
MODEL_CHAT_V1, MODEL_CODING_V1, MODEL_REASONING_QUICK_V1, MODEL_REASONING_V1,
MODEL_SUMMARIZATION_V1, SEARCH_ENGINE_BRAVE, SEARCH_ENGINE_MANAGED, SEARCH_ENGINE_PARALLEL,
};
pub use schema::{
clear_active_user, default_projects_dir, default_root_openhuman_dir, pre_login_user_dir,
+57
View File
@@ -1129,6 +1129,63 @@ pub async fn get_search_settings() -> Result<RpcOutcome<serde_json::Value>, Stri
))
}
/// Reads dashboard settings exposed to the desktop UI.
pub async fn get_dashboard_settings() -> Result<RpcOutcome<serde_json::Value>, String> {
let request_id = uuid::Uuid::new_v4().to_string();
tracing::debug!(
target: "openhuman_core::config",
request_id = %request_id,
method = "openhuman.config_get_dashboard_settings",
"OPENHUMAN: get_dashboard_settings entry"
);
tracing::debug!(
target: "openhuman_core::config",
request_id = %request_id,
method = "openhuman.config_get_dashboard_settings",
"OPENHUMAN: get_dashboard_settings loading config"
);
let config = load_config_with_timeout().await.map_err(|error| {
tracing::warn!(
target: "openhuman_core::config",
request_id = %request_id,
method = "openhuman.config_get_dashboard_settings",
error = %error,
"OPENHUMAN: get_dashboard_settings config load failed"
);
error
})?;
tracing::debug!(
target: "openhuman_core::config",
request_id = %request_id,
method = "openhuman.config_get_dashboard_settings",
"OPENHUMAN: get_dashboard_settings serializing dashboard settings"
);
let result = serde_json::to_value(&config.dashboard).map_err(|error| {
let message = error.to_string();
tracing::warn!(
target: "openhuman_core::config",
request_id = %request_id,
method = "openhuman.config_get_dashboard_settings",
error = %message,
"OPENHUMAN: get_dashboard_settings serialization failed"
);
message
})?;
tracing::debug!(
target: "openhuman_core::config",
request_id = %request_id,
method = "openhuman.config_get_dashboard_settings",
"OPENHUMAN: get_dashboard_settings exit"
);
Ok(RpcOutcome::new(
result,
vec!["dashboard settings read".to_string()],
))
}
/// Loads the configuration, applies browser settings updates, and saves it.
pub async fn load_and_apply_browser_settings(
update: BrowserSettingsPatch,
+62
View File
@@ -1,12 +1,26 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
fn default_diagram_viewer_enabled() -> bool {
true
}
fn default_diagram_viewer_source_url() -> String {
"http://localhost:8787/workspace/diagrams/latest.png".to_string()
}
fn default_diagram_viewer_refresh_interval_seconds() -> u64 {
10
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(default)]
pub struct DashboardConfig {
pub event_stream: EventStreamConfig,
#[serde(default)]
pub model_health: ModelHealthConfig,
#[serde(default)]
pub diagram_viewer: DiagramViewerConfig,
}
impl Default for DashboardConfig {
@@ -14,6 +28,7 @@ impl Default for DashboardConfig {
Self {
event_stream: EventStreamConfig::default(),
model_health: ModelHealthConfig::default(),
diagram_viewer: DiagramViewerConfig::default(),
}
}
}
@@ -72,10 +87,57 @@ impl Default for ModelHealthConfig {
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(default)]
pub struct DiagramViewerConfig {
#[serde(default = "default_diagram_viewer_enabled")]
pub enabled: bool,
#[serde(default = "default_diagram_viewer_source_url")]
pub source_url: String,
#[serde(default = "default_diagram_viewer_refresh_interval_seconds")]
pub refresh_interval_seconds: u64,
}
impl Default for DiagramViewerConfig {
fn default() -> Self {
Self {
enabled: default_diagram_viewer_enabled(),
source_url: default_diagram_viewer_source_url(),
refresh_interval_seconds: default_diagram_viewer_refresh_interval_seconds(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dashboard_config_defaults_enable_local_diagram_viewer() {
let config = DashboardConfig::default();
assert!(config.diagram_viewer.enabled);
assert_eq!(
config.diagram_viewer.source_url,
"http://localhost:8787/workspace/diagrams/latest.png"
);
assert_eq!(config.diagram_viewer.refresh_interval_seconds, 10);
}
#[test]
fn diagram_viewer_partial_toml_uses_missing_defaults() {
let config: DashboardConfig =
toml::from_str("[diagram_viewer]\nsource_url = \"http://localhost:9000/latest.svg\"")
.expect("dashboard config should deserialize");
assert!(config.diagram_viewer.enabled);
assert_eq!(
config.diagram_viewer.source_url,
"http://localhost:9000/latest.svg"
);
assert_eq!(config.diagram_viewer.refresh_interval_seconds, 10);
}
#[test]
fn model_health_defaults_match_spec() {
let mh = ModelHealthConfig::default();
+2 -2
View File
@@ -14,6 +14,7 @@ mod autonomy;
mod capability_providers;
mod channels;
mod context;
mod dashboard;
mod defaults;
mod dictation;
mod heartbeat_cron;
@@ -54,6 +55,7 @@ pub use channels::{
TelegramConfig, WebhookConfig, WhatsAppConfig,
};
pub use context::ContextConfig;
pub use dashboard::{DashboardConfig, DiagramViewerConfig, EventStreamConfig, ModelHealthConfig};
pub use dictation::{DictationActivationMode, DictationConfig};
pub use heartbeat_cron::{CronConfig, HeartbeatConfig};
pub use identity_cost::{CostConfig, ModelPricing};
@@ -92,7 +94,5 @@ pub use voice_providers::{
generate_voice_provider_id, is_voice_slug_reserved, BuiltinVoiceProvider, SttApiStyle,
TtsApiStyle, VoiceCapability, VoiceProviderCreds, BUILTIN_VOICE_PROVIDERS,
};
mod dashboard;
pub use dashboard::{DashboardConfig, EventStreamConfig, ModelHealthConfig};
mod types;
pub use types::*;
+4 -4
View File
@@ -89,6 +89,9 @@ pub struct Config {
#[serde(default)]
pub observability: ObservabilityConfig,
#[serde(default)]
pub dashboard: DashboardConfig,
#[serde(default)]
pub autonomy: AutonomyConfig,
@@ -366,9 +369,6 @@ pub struct Config {
#[serde(default)]
pub chat_onboarding_completed: bool,
#[serde(default)]
pub dashboard: DashboardConfig,
#[serde(default)]
pub model_registry: Vec<ModelRegistryEntry>,
}
@@ -618,6 +618,7 @@ impl Default for Config {
output_language: None,
temperature_unsupported_models: default_temperature_unsupported_models(),
observability: ObservabilityConfig::default(),
dashboard: DashboardConfig::default(),
autonomy: AutonomyConfig::default(),
runtime: RuntimeConfig::default(),
screen_intelligence: ScreenIntelligenceConfig::default(),
@@ -680,7 +681,6 @@ impl Default for Config {
meet: MeetConfig::default(),
onboarding_completed: false,
chat_onboarding_completed: false,
dashboard: DashboardConfig::default(),
model_registry: Vec::new(),
}
}
+21
View File
@@ -236,6 +236,7 @@ pub fn all_controller_schemas() -> Vec<ControllerSchema> {
schemas("workspace_onboarding_flag_set"),
schemas("update_analytics_settings"),
schemas("get_analytics_settings"),
schemas("get_dashboard_settings"),
schemas("update_meet_settings"),
schemas("get_meet_settings"),
schemas("agent_server_status"),
@@ -318,6 +319,10 @@ pub fn all_registered_controllers() -> Vec<RegisteredController> {
schema: schemas("get_analytics_settings"),
handler: handle_get_analytics_settings,
},
RegisteredController {
schema: schemas("get_dashboard_settings"),
handler: handle_get_dashboard_settings,
},
RegisteredController {
schema: schemas("update_meet_settings"),
handler: handle_update_meet_settings,
@@ -769,6 +774,18 @@ pub fn schemas(function: &str) -> ControllerSchema {
required: true,
}],
},
"get_dashboard_settings" => ControllerSchema {
namespace: "config",
function: "get_dashboard_settings",
description: "Read dashboard settings, including the local architecture diagram viewer.",
inputs: vec![],
outputs: vec![FieldSchema {
name: "dashboard",
ty: TypeSchema::Json,
comment: "Current [dashboard] config block.",
required: true,
}],
},
"update_meet_settings" => ControllerSchema {
namespace: "config",
function: "update_meet_settings",
@@ -1321,6 +1338,10 @@ fn handle_get_analytics_settings(_params: Map<String, Value>) -> ControllerFutur
})
}
fn handle_get_dashboard_settings(_params: Map<String, Value>) -> ControllerFuture {
Box::pin(async { to_json(config_rpc::get_dashboard_settings().await?) })
}
fn handle_update_meet_settings(params: Map<String, Value>) -> ControllerFuture {
Box::pin(async move {
log::debug!("[config][rpc] update_meet_settings enter");
+4 -3
View File
@@ -217,9 +217,10 @@ mod tests {
let (_provider, model) = build_chat_runtime(&cfg).unwrap();
assert_eq!(model, DEFAULT_CLOUD_LLM_MODEL);
// build_chat_runtime resolves the "summarization" workload role,
// which routes to the dedicated `summarization-v1` tier (PR #2690)
// rather than the generic `reasoning-v1` fallback.
assert_eq!(model, "summarization-v1");
// which routes to the dedicated DEFAULT_CLOUD_LLM_MODEL
// (`summarization-v1`, PR #2690) rather than the generic
// `reasoning-v1` fallback.
assert_eq!(model, DEFAULT_CLOUD_LLM_MODEL);
}
#[test]
@@ -243,13 +243,10 @@ fn pop_response(
routes: &mut HashMap<String, VecDeque<MockResponse>>,
target: &str,
) -> MockResponse {
if let Some(response) = pop_from_queue(routes.get_mut(target)) {
return response;
}
let path_only = target.split('?').next().unwrap_or(target);
if let Some(response) = pop_from_queue(routes.get_mut(path_only)) {
return response;
for key in route_lookup_keys(target) {
if let Some(response) = pop_from_queue(routes.get_mut(&key)) {
return response;
}
}
MockResponse {
@@ -259,6 +256,36 @@ fn pop_response(
}
}
fn route_lookup_keys(target: &str) -> Vec<String> {
let mut keys = Vec::new();
push_lookup_key(&mut keys, target.to_string());
push_path_only_key(&mut keys, target);
if let Ok(url) = reqwest::Url::parse(target) {
let mut normalized = url.path().to_string();
if let Some(query) = url.query() {
normalized.push('?');
normalized.push_str(query);
}
push_lookup_key(&mut keys, normalized.clone());
push_path_only_key(&mut keys, &normalized);
}
keys
}
fn push_path_only_key(keys: &mut Vec<String>, target: &str) {
if let Some(path_only) = target.split('?').next() {
push_lookup_key(keys, path_only.to_string());
}
}
fn push_lookup_key(keys: &mut Vec<String>, key: String) {
if !keys.iter().any(|existing| existing == &key) {
keys.push(key);
}
}
fn pop_from_queue(queue: Option<&mut VecDeque<MockResponse>>) -> Option<MockResponse> {
let queue = queue?;
if queue.len() <= 1 {
@@ -281,6 +308,16 @@ fn reason_phrase(status: u16) -> &'static str {
}
}
#[test]
fn route_lookup_keys_normalizes_absolute_targets() {
let keys = route_lookup_keys("http://127.0.0.1:1234/nonce?user=0xabc");
assert!(keys.contains(&"http://127.0.0.1:1234/nonce?user=0xabc".to_string()));
assert!(keys.contains(&"http://127.0.0.1:1234/nonce".to_string()));
assert!(keys.contains(&"/nonce?user=0xabc".to_string()));
assert!(keys.contains(&"/nonce".to_string()));
}
fn parse_tool_output(result: &ToolResult) -> Value {
serde_json::from_str::<Value>(&result.output()).expect("tool output should be valid json")
}
@@ -770,10 +807,9 @@ async fn place_order_happy_path_posts_signed_order() {
.expect("wallet setup");
let mut routes = HashMap::new();
routes.insert(
format!("/nonce?user={user}"),
vec![MockResponse::body(200, r#"{"nonce": 42}"#)],
);
let nonce_response = vec![MockResponse::body(200, r#"{"nonce": 42}"#)];
routes.insert(format!("/nonce?user={user}"), nonce_response.clone());
routes.insert("/nonce".to_string(), nonce_response);
routes.insert(
"/order".to_string(),
vec![MockResponse::body(
@@ -812,10 +848,11 @@ async fn place_order_happy_path_posts_signed_order() {
.await
.unwrap();
let requests = captured.lock().unwrap().clone();
assert!(
!result.is_error,
"expected success, got: {}",
result.output()
"expected success, got: {}\nobserved requests: {requests:#?}",
result.output(),
);
let output = parse_tool_output(&result);
assert_eq!(output["action"], "place_order");
@@ -823,7 +860,6 @@ async fn place_order_happy_path_posts_signed_order() {
assert_eq!(output["data"]["success"], true);
assert_eq!(output["data"]["orderID"], "ord-test-1");
let requests = captured.lock().unwrap().clone();
assert_eq!(requests.len(), 2);
let nonce_request = &requests[0];