feat(onboarding): guided memory & vault setup with health checklist (#2910) (#3160)

Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
This commit is contained in:
oxoxDev
2026-06-01 23:07:41 -07:00
committed by GitHub
co-authored by Steven Enamakel
parent 56a31c88bd
commit 9683653211
28 changed files with 1680 additions and 13 deletions
@@ -0,0 +1,92 @@
import { fireEvent, screen, waitFor } from '@testing-library/react';
import { describe, expect, it, type Mock, vi } from 'vitest';
import { renderWithProviders } from '../../test/test-utils';
import type { VaultHealthCheck } from '../../utils/tauriCommands';
import { VaultHealthChecklist } from './VaultHealthChecklist';
vi.mock('../../utils/tauriCommands', () => ({ memoryTreeVaultHealthCheck: vi.fn() }));
vi.mock('../../utils/openUrl', () => ({
openUrl: vi.fn().mockResolvedValue(undefined),
revealPath: vi.fn().mockResolvedValue(undefined),
}));
const { memoryTreeVaultHealthCheck } = (await import('../../utils/tauriCommands')) as unknown as {
memoryTreeVaultHealthCheck: Mock;
};
const { openUrl, revealPath } = (await import('../../utils/openUrl')) as unknown as {
openUrl: Mock;
revealPath: Mock;
};
function health(overrides: Partial<VaultHealthCheck> = {}): VaultHealthCheck {
return {
content_root_abs: '/tmp/workspace/memory_tree/content',
exists: true,
readable: true,
writable: true,
obsidian_registered: true,
pipeline_healthy: true,
last_sync_ms: Date.now() - 60_000,
...overrides,
};
}
describe('<VaultHealthChecklist />', () => {
it('shows all checks as passed when the health RPC is fully healthy', async () => {
memoryTreeVaultHealthCheck.mockResolvedValueOnce(health());
renderWithProviders(<VaultHealthChecklist />);
await waitFor(() => {
expect(screen.getByTestId('vault-health-item-exists')).toBeInTheDocument();
});
expect(screen.getByText(/Passed · Workspace vault path exists/)).toBeInTheDocument();
expect(screen.getByText(/Passed · Vault is writable by OpenHuman/)).toBeInTheDocument();
expect(screen.getByText(/Passed · Vault is registered in Obsidian/)).toBeInTheDocument();
expect(screen.getByText(/Passed · Memory pipeline is healthy/)).toBeInTheDocument();
});
it('surfaces a recovery hint when the vault folder is missing', async () => {
memoryTreeVaultHealthCheck.mockResolvedValueOnce(health({ exists: false, readable: false }));
renderWithProviders(<VaultHealthChecklist />);
await waitFor(() => {
expect(screen.getByTestId('vault-health-item-exists')).toBeInTheDocument();
});
expect(screen.getByText(/Vault folder is missing/)).toBeInTheDocument();
});
it('surfaces a recovery hint when the vault is not writable', async () => {
memoryTreeVaultHealthCheck.mockResolvedValueOnce(health({ writable: false }));
renderWithProviders(<VaultHealthChecklist />);
await waitFor(() => {
expect(screen.getByTestId('vault-health-item-writable')).toBeInTheDocument();
});
expect(screen.getByText(/cannot write to this vault yet/i)).toBeInTheDocument();
});
it('surfaces Obsidian registration guidance and action buttons', async () => {
memoryTreeVaultHealthCheck.mockResolvedValueOnce(health({ obsidian_registered: false }));
renderWithProviders(<VaultHealthChecklist />);
await waitFor(() => {
expect(screen.getByTestId('vault-health-item-obsidian')).toBeInTheDocument();
});
expect(screen.getByText(/Open folder as vault/)).toBeInTheDocument();
fireEvent.click(screen.getByTestId('vault-health-open-obsidian'));
await waitFor(() => {
expect(openUrl).toHaveBeenCalledWith(
'obsidian://open?path=' + encodeURIComponent('/tmp/workspace/memory_tree/content')
);
});
fireEvent.click(screen.getByTestId('vault-health-reveal'));
await waitFor(() => {
expect(revealPath).toHaveBeenCalledWith('/tmp/workspace/memory_tree/content');
});
});
});
@@ -0,0 +1,230 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useT } from '../../lib/i18n/I18nContext';
import type { ToastNotification } from '../../types/intelligence';
import { openUrl, revealPath } from '../../utils/openUrl';
import { memoryTreeVaultHealthCheck, type VaultHealthCheck } from '../../utils/tauriCommands';
const OBSIDIAN_DOWNLOAD_URL = 'https://obsidian.md/download';
interface VaultHealthChecklistProps {
onToast?: (toast: Omit<ToastNotification, 'id'>) => void;
title?: string;
}
function formatRelativeTime(ms: number, t: (key: string, fallback?: string) => string): string {
if (!ms || ms <= 0) return t('vaultHealth.timeNever');
const diff = Date.now() - ms;
if (diff < 0) return t('vaultHealth.timeNever');
const sec = Math.floor(diff / 1000);
if (sec < 45) return t('vaultHealth.timeJustNow');
const min = Math.floor(sec / 60);
if (min < 60) return t('vaultHealth.timeMinAgo').replace('{n}', String(min));
const hr = Math.floor(min / 60);
if (hr < 24) return t('vaultHealth.timeHrAgo').replace('{n}', String(hr));
const day = Math.floor(hr / 24);
return (day === 1 ? t('vaultHealth.timeDayAgo') : t('vaultHealth.timeDaysAgo')).replace(
'{n}',
String(day)
);
}
function dirname(path: string): string {
const normalized = path.replace(/[\\/]+$/, '');
const slash = Math.max(normalized.lastIndexOf('/'), normalized.lastIndexOf('\\'));
if (slash <= 0) return normalized;
return normalized.slice(0, slash);
}
export function VaultHealthChecklist({ onToast, title }: VaultHealthChecklistProps) {
const { t } = useT();
const resolvedTitle = title ?? t('vaultHealth.title');
const [health, setHealth] = useState<VaultHealthCheck | null>(null);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [error, setError] = useState<string | null>(null);
const runCheck = useCallback(async () => {
setRefreshing(true);
try {
const next = await memoryTreeVaultHealthCheck();
setHealth(next);
setError(null);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
setError(message);
} finally {
setRefreshing(false);
setLoading(false);
}
}, []);
useEffect(() => {
void runCheck();
}, [runCheck]);
const revealTarget = useMemo(() => {
if (!health?.content_root_abs) return '';
return health.exists ? health.content_root_abs : dirname(health.content_root_abs);
}, [health]);
const openObsidian = useCallback(() => {
if (!health?.content_root_abs) return;
void (async () => {
try {
await openUrl(`obsidian://open?path=${encodeURIComponent(health.content_root_abs)}`);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
onToast?.({ type: 'error', title: t('vaultHealth.openObsidianError'), message });
}
})();
}, [health, onToast, t]);
const revealVault = useCallback(() => {
if (!revealTarget) return;
void (async () => {
try {
await revealPath(revealTarget);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
onToast?.({ type: 'error', title: t('vaultHealth.revealError'), message });
}
})();
}, [onToast, revealTarget, t]);
const installObsidian = useCallback(() => {
void (async () => {
try {
await openUrl(OBSIDIAN_DOWNLOAD_URL);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
onToast?.({ type: 'error', title: t('vaultHealth.downloadError'), message });
}
})();
}, [onToast, t]);
const checklist = health
? [
{
key: 'exists',
label: t('vaultHealth.existsLabel'),
ok: health.exists,
recovery: t('vaultHealth.existsRecovery'),
},
{
key: 'writable',
label: t('vaultHealth.writableLabel'),
ok: health.writable,
recovery: t('vaultHealth.writableRecovery'),
},
{
key: 'obsidian',
label: t('vaultHealth.obsidianLabel'),
ok: health.obsidian_registered,
recovery: t('vaultHealth.obsidianRecovery'),
},
{
key: 'pipeline',
label: t('vaultHealth.pipelineLabel'),
ok: health.pipeline_healthy,
recovery: t('vaultHealth.pipelineRecovery'),
},
]
: [];
return (
<div
className="rounded-xl border border-stone-200 dark:border-neutral-700 bg-white dark:bg-neutral-900 p-4 space-y-3"
data-testid="vault-health-checklist">
<div className="flex items-start justify-between gap-2">
<div>
<h3 className="text-sm font-semibold text-stone-900 dark:text-neutral-100">
{resolvedTitle}
</h3>
<p className="mt-1 text-xs text-stone-600 dark:text-neutral-300">
{t('vaultHealth.workspaceVault')} <code className="font-mono">memory_tree/content</code>
</p>
</div>
<button
type="button"
onClick={() => {
void runCheck();
}}
disabled={refreshing}
className="rounded-md border border-stone-300 dark:border-neutral-600 bg-white dark:bg-neutral-800 px-2.5 py-1 text-xs font-medium text-stone-700 dark:text-neutral-200 disabled:opacity-60"
data-testid="vault-health-refresh">
{refreshing ? t('vaultHealth.refreshing') : t('vaultHealth.refresh')}
</button>
</div>
{health?.content_root_abs ? (
<code
className="block break-all rounded-md bg-stone-100 dark:bg-neutral-800 px-2 py-1 text-[11px] text-stone-700 dark:text-neutral-200"
data-testid="vault-health-path">
{health.content_root_abs}
</code>
) : null}
<div className="flex flex-wrap gap-2">
<button
type="button"
onClick={revealVault}
disabled={!health?.content_root_abs}
className="rounded-md border border-stone-300 dark:border-neutral-600 bg-white dark:bg-neutral-800 px-3 py-1.5 text-xs font-semibold text-stone-700 dark:text-neutral-200 disabled:opacity-50"
data-testid="vault-health-reveal">
{t('vaultHealth.revealFolder')}
</button>
<button
type="button"
onClick={openObsidian}
disabled={!health?.content_root_abs}
className="rounded-md border border-violet-300 dark:border-violet-500/40 bg-white dark:bg-neutral-800 px-3 py-1.5 text-xs font-semibold text-violet-700 dark:text-violet-300 disabled:opacity-50"
data-testid="vault-health-open-obsidian">
{t('vaultHealth.openInObsidian')}
</button>
<button
type="button"
onClick={installObsidian}
className="rounded-md border border-stone-300 dark:border-neutral-600 bg-white dark:bg-neutral-800 px-3 py-1.5 text-xs font-semibold text-stone-700 dark:text-neutral-200"
data-testid="vault-health-install-obsidian">
{t('vaultHealth.installObsidian')}
</button>
</div>
{loading ? (
<div className="h-16 rounded-md bg-stone-100 dark:bg-neutral-800 animate-pulse" />
) : error ? (
<div
className="rounded-md border border-coral-200 dark:border-coral-500/30 bg-coral-50 dark:bg-coral-500/10 px-3 py-2 text-xs text-coral-700 dark:text-coral-300"
data-testid="vault-health-error">
{t('vaultHealth.loadError')} {error}
</div>
) : (
<div className="space-y-2">
{checklist.map(item => (
<div
key={item.key}
data-testid={`vault-health-item-${item.key}`}
className={`rounded-md border px-3 py-2 text-xs ${
item.ok
? 'border-sage-200 dark:border-sage-500/30 bg-sage-50 dark:bg-sage-500/10 text-sage-800 dark:text-sage-200'
: 'border-amber-200 dark:border-amber-500/30 bg-amber-50 dark:bg-amber-500/10 text-amber-800 dark:text-amber-200'
}`}>
<div className="font-semibold">
{item.ok ? t('vaultHealth.passed') : t('vaultHealth.needsAttention')} · {item.label}
</div>
{!item.ok ? <p className="mt-1 leading-relaxed">{item.recovery}</p> : null}
</div>
))}
<p
className="text-[11px] text-stone-600 dark:text-neutral-300"
data-testid="vault-health-last-sync">
{t('vaultHealth.lastSync')} {formatRelativeTime(health?.last_sync_ms ?? 0, t)}
</p>
</div>
)}
</div>
);
}
export default VaultHealthChecklist;
@@ -154,7 +154,9 @@ const MemoryWindowControl = ({ onError, onSaved }: Props) => {
data-testid="memory-window-control">
<div className="flex items-baseline justify-between">
<div>
<h3 className="text-base font-semibold">{t('settings.memoryWindow.title')}</h3>
<h3 className="text-base font-semibold text-stone-900 dark:text-neutral-100">
{t('settings.memoryWindow.title')}
</h3>
<p className="text-sm text-muted-foreground">{t('settings.memoryWindow.description')}</p>
</div>
</div>
@@ -4,6 +4,7 @@ import { useT } from '../../../lib/i18n/I18nContext';
import type { ToastNotification } from '../../../types/intelligence';
import { MemoryWorkspace } from '../../intelligence/MemoryWorkspace';
import { ToastContainer } from '../../intelligence/Toast';
import { VaultHealthChecklist } from '../../intelligence/VaultHealthChecklist';
import MemoryWindowControl from '../components/MemoryWindowControl';
import SettingsHeader from '../components/SettingsHeader';
import { useSettingsNavigation } from '../hooks/useSettingsNavigation';
@@ -30,16 +31,20 @@ const MemoryDataPanel = ({ embedded = false }: MemoryDataPanelProps = {}) => {
const handleWindowError = useCallback(
(message: string) => {
addToast({ type: 'error', title: 'Memory window', message });
addToast({ type: 'error', title: t('memoryData.windowError'), message });
},
[addToast]
[addToast, t]
);
const handleWindowSaved = useCallback(
(window: string) => {
addToast({ type: 'success', title: 'Memory window updated', message: `Set to ${window}.` });
addToast({
type: 'success',
title: t('memoryData.windowUpdated'),
message: t('memoryData.windowUpdatedMsg').replace('{window}', window),
});
},
[addToast]
[addToast, t]
);
return (
@@ -53,6 +58,38 @@ const MemoryDataPanel = ({ embedded = false }: MemoryDataPanelProps = {}) => {
/>
)}
<div className={embedded ? 'space-y-4' : 'p-4 space-y-4'}>
<section className="rounded-xl border border-stone-200 dark:border-neutral-700 bg-white dark:bg-neutral-900 p-4 space-y-3">
<h3 className="text-sm font-semibold text-stone-900 dark:text-neutral-100">
{t('memoryData.howItWorks')}
</h3>
<dl className="space-y-2.5">
<div>
<dt className="text-xs font-semibold text-stone-900 dark:text-neutral-100">
{t('memoryData.workspaceVault')}
</dt>
<dd className="text-xs leading-relaxed text-stone-600 dark:text-neutral-300">
{t('memoryData.workspaceVaultDesc')}
</dd>
</div>
<div>
<dt className="text-xs font-semibold text-stone-900 dark:text-neutral-100">
{t('memoryData.connectedSources')}
</dt>
<dd className="text-xs leading-relaxed text-stone-600 dark:text-neutral-300">
{t('memoryData.connectedSourcesDesc')}
</dd>
</div>
<div>
<dt className="text-xs font-semibold text-stone-900 dark:text-neutral-100">
{t('memoryData.internalFiles')}
</dt>
<dd className="text-xs leading-relaxed text-stone-600 dark:text-neutral-300">
{t('memoryData.internalFilesDesc')}
</dd>
</div>
</dl>
</section>
<VaultHealthChecklist onToast={addToast} title={t('vaultHealth.setupTitle')} />
<MemoryWindowControl onError={handleWindowError} onSaved={handleWindowSaved} />
<MemoryWorkspace onToast={addToast} />
</div>
+57
View File
@@ -4431,11 +4431,68 @@ const messages: TranslationMap = {
// Onboarding: Custom > Activity
'onboarding.custom.stepperActivity': 'النشاط',
'onboarding.custom.stepperVault': 'الخزينة',
'onboarding.custom.activity.title': 'نشاط الوكيل',
'onboarding.custom.activity.subtitle': 'مدى استباقية وكيلك في المراقبة والتصرف في الخلفية.',
'onboarding.custom.activity.defaultDesc': 'نشاط متوسط — مزامنة كل ساعة، ملخص يومي.',
'onboarding.custom.activity.configureDesc':
'اختر مستوى نشاطك الخاص. الإعداد في الإعدادات › مستوى نشاط الوكيل.',
'onboarding.custom.vault.title': 'إعداد الذاكرة والخزينة',
'onboarding.custom.vault.subtitle':
'تأكيد موضع كتابة ملاحظات الذاكرة، وكيفية قراءة البيانات المصدر، وسلامة مسار الخزينة.',
'onboarding.custom.vault.defaultDesc':
'استخدم الإعدادات الافتراضية لذاكرة OpenHuman المُدارة. يمكن مراجعة مسار الخزينة وصحة المزامنة لاحقًا.',
'onboarding.custom.vault.configureDesc':
'راجع ملكية الخزينة، وشغّل فحوصات الصحة، واضبط عناصر التحكم في الذاكرة الآن.',
'onboarding.custom.vault.localDisabledReason':
'يتطلب الإعداد المُدار تسجيل الدخول إلى OpenHuman وغير متاح في الوضع المحلي.',
'onboarding.custom.vault.exitError': 'تعذّر إتمام الإعداد. يُرجى المحاولة مجددًا.',
'vaultHealth.title': 'قائمة فحص صحة الخزينة',
'vaultHealth.setupTitle': 'صحة إعداد الخزينة',
'vaultHealth.workspaceVault': 'خزينة مساحة العمل:',
'vaultHealth.refresh': 'تحديث',
'vaultHealth.refreshing': 'جارٍ التحديث…',
'vaultHealth.revealFolder': 'إظهار المجلد',
'vaultHealth.openInObsidian': 'فتح في Obsidian',
'vaultHealth.installObsidian': 'تثبيت Obsidian',
'vaultHealth.openObsidianError': 'تعذّر فتح Obsidian',
'vaultHealth.revealError': 'تعذّر إظهار مجلد الخزينة',
'vaultHealth.downloadError': 'تعذّر فتح صفحة تنزيل Obsidian',
'vaultHealth.loadError': 'تعذّر تحميل صحة الخزينة:',
'vaultHealth.lastSync': 'آخر مزامنة:',
'vaultHealth.passed': 'ناجح',
'vaultHealth.needsAttention': 'يحتاج إلى انتباه',
'vaultHealth.existsLabel': 'مسار خزينة مساحة العمل موجود',
'vaultHealth.existsRecovery':
'مجلد الخزينة مفقود. ابدأ مزامنة أو أنشئ هذا المجلد، ثم حدّث هذه القائمة.',
'vaultHealth.writableLabel': 'الخزينة قابلة للكتابة بواسطة OpenHuman',
'vaultHealth.writableRecovery':
'لا يستطيع OpenHuman الكتابة في هذه الخزينة بعد. امنح أذونات الكتابة ثم حدّث.',
'vaultHealth.obsidianLabel': 'الخزينة مسجّلة في Obsidian',
'vaultHealth.obsidianRecovery':
'في Obsidian، اختر "فتح المجلد كخزينة" لهذا المسار، ثم حدّث هذه القائمة.',
'vaultHealth.pipelineLabel': 'مسار الذاكرة سليم',
'vaultHealth.pipelineRecovery':
'مسار الذاكرة متوقف أو في حالة خطأ. أعد تفعيل المزامنة التلقائية في حالة شجرة الذاكرة وأعد المحاولة.',
'vaultHealth.timeNever': 'قط',
'vaultHealth.timeJustNow': 'الآن',
'vaultHealth.timeMinAgo': 'منذ {n} دقيقة',
'vaultHealth.timeHrAgo': 'منذ {n} ساعة',
'vaultHealth.timeDayAgo': 'منذ {n} يوم',
'vaultHealth.timeDaysAgo': 'منذ {n} أيام',
'memoryData.howItWorks': 'كيف يعمل تخزين الذاكرة',
'memoryData.workspaceVault': 'خزينة مساحة العمل · كتابة',
'memoryData.workspaceVaultDesc':
'يكتب OpenHuman ملاحظات الذاكرة المُولَّدة إلى memory_tree/content.',
'memoryData.connectedSources': 'المصادر المتصلة · قراءة',
'memoryData.connectedSourcesDesc':
'تُستورد المجلدات وصناديق البريد والمحادثات والمستودعات لفهرسة الذاكرة — ولا تُعاد كتابة ملفاتها الأصلية أبدًا.',
'memoryData.internalFiles': 'ملفات شجرة الذاكرة الداخلية',
'memoryData.internalFilesDesc':
'تُدار الفهارس وحالة قائمة الانتظار والملخصات بواسطة OpenHuman للحفاظ على سلامة الاسترجاع والمزامنة.',
'memoryData.windowError': 'نافذة الذاكرة',
'memoryData.windowUpdated': 'تم تحديث نافذة الذاكرة',
'memoryData.windowUpdatedMsg': 'تم الضبط على {window}.',
};
export default messages;
+56
View File
@@ -4512,6 +4512,7 @@ const messages: TranslationMap = {
// Onboarding: Custom > Activity
'onboarding.custom.stepperActivity': 'কার্যকলাপ',
'onboarding.custom.stepperVault': 'ভল্ট',
'onboarding.custom.activity.title': 'এজেন্টের কার্যকলাপ',
'onboarding.custom.activity.subtitle':
'আপনার এজেন্ট পটভূমিতে কতটা সক্রিয়ভাবে পর্যবেক্ষণ ও কাজ করে।',
@@ -4519,6 +4520,61 @@ const messages: TranslationMap = {
'মাঝারি কার্যকলাপ — প্রতি ঘণ্টায় সিঙ্ক, দৈনিক সারসংক্ষেপ।',
'onboarding.custom.activity.configureDesc':
'নিজের কার্যকলাপের স্তর বেছে নিন। সেটিংস › এজেন্ট কার্যকলাপ স্তরে কনফিগার করুন।',
'onboarding.custom.vault.title': 'মেমোরি ও ভল্ট সেটআপ',
'onboarding.custom.vault.subtitle':
'নিশ্চিত করুন মেমোরি নোট কোথায় লেখা হয়, উৎস ডেটা কীভাবে পড়া হয় এবং আপনার ভল্ট পাইপলাইন সুস্থ কিনা।',
'onboarding.custom.vault.defaultDesc':
'OpenHuman-পরিচালিত মেমোরির ডিফল্ট ব্যবহার করুন। ভল্ট পাথ এবং সিঙ্ক স্বাস্থ্য পরে পর্যালোচনা করা যাবে।',
'onboarding.custom.vault.configureDesc':
'ভল্টের মালিকানা পর্যালোচনা করুন, স্বাস্থ্য পরীক্ষা চালান এবং এখনই মেমোরি নিয়ন্ত্রণ সামঞ্জস্য করুন।',
'onboarding.custom.vault.localDisabledReason':
'পরিচালিত সেটআপের জন্য OpenHuman সাইন-ইন প্রয়োজন এবং লোকাল মোডে উপলব্ধ নয়।',
'onboarding.custom.vault.exitError': 'অনবোর্ডিং শেষ করা যায়নি। আবার চেষ্টা করুন।',
'vaultHealth.title': 'ভল্ট স্বাস্থ্য চেকলিস্ট',
'vaultHealth.setupTitle': 'ভল্ট সেটআপ স্বাস্থ্য',
'vaultHealth.workspaceVault': 'ওয়ার্কস্পেস ভল্ট:',
'vaultHealth.refresh': 'রিফ্রেশ',
'vaultHealth.refreshing': 'রিফ্রেশ হচ্ছে…',
'vaultHealth.revealFolder': 'ফোল্ডার দেখান',
'vaultHealth.openInObsidian': 'Obsidian-এ খুলুন',
'vaultHealth.installObsidian': 'Obsidian ইনস্টল করুন',
'vaultHealth.openObsidianError': 'Obsidian খোলা যায়নি',
'vaultHealth.revealError': 'ভল্ট ফোল্ডার দেখানো যায়নি',
'vaultHealth.downloadError': 'Obsidian ডাউনলোড পৃষ্ঠা খোলা যায়নি',
'vaultHealth.loadError': 'ভল্ট স্বাস্থ্য লোড করা যায়নি:',
'vaultHealth.lastSync': 'শেষ সিঙ্ক:',
'vaultHealth.passed': 'পাস হয়েছে',
'vaultHealth.needsAttention': 'মনোযোগ প্রয়োজন',
'vaultHealth.existsLabel': 'ওয়ার্কস্পেস ভল্ট পাথ বিদ্যমান',
'vaultHealth.existsRecovery':
'ভল্ট ফোল্ডার নেই। একটি সিঙ্ক শুরু করুন বা এই ফোল্ডারটি তৈরি করুন, তারপর এই চেকলিস্ট রিফ্রেশ করুন।',
'vaultHealth.writableLabel': 'ভল্ট OpenHuman দ্বারা লিখনযোগ্য',
'vaultHealth.writableRecovery':
'OpenHuman এখনও এই ভল্টে লিখতে পারছে না। লেখার অনুমতি দিন এবং রিফ্রেশ করুন।',
'vaultHealth.obsidianLabel': 'ভল্ট Obsidian-এ নিবন্ধিত',
'vaultHealth.obsidianRecovery':
'Obsidian-এ এই পাথটির জন্য "ফোল্ডার ভল্ট হিসেবে খুলুন" বেছে নিন, তারপর এই চেকলিস্ট রিফ্রেশ করুন।',
'vaultHealth.pipelineLabel': 'মেমোরি পাইপলাইন সুস্থ',
'vaultHealth.pipelineRecovery':
'মেমোরি পাইপলাইন বিরতিতে আছে বা ত্রুটিতে আছে। মেমোরি ট্রি স্ট্যাটাসে অটো-সিঙ্ক পুনরায় সক্ষম করুন এবং পুনরায় চেষ্টা করুন।',
'vaultHealth.timeNever': 'কখনো না',
'vaultHealth.timeJustNow': 'এইমাত্র',
'vaultHealth.timeMinAgo': '{n} মিনিট আগে',
'vaultHealth.timeHrAgo': '{n} ঘণ্টা আগে',
'vaultHealth.timeDayAgo': '{n} দিন আগে',
'vaultHealth.timeDaysAgo': '{n} দিন আগে',
'memoryData.howItWorks': 'মেমোরি স্টোরেজ কীভাবে কাজ করে',
'memoryData.workspaceVault': 'ওয়ার্কস্পেস ভল্ট · লেখা',
'memoryData.workspaceVaultDesc': 'OpenHuman তৈরি মেমোরি নোট memory_tree/content-এ লেখে।',
'memoryData.connectedSources': 'সংযুক্ত উৎস · পড়া',
'memoryData.connectedSourcesDesc':
'ফোল্ডার, মেইলবক্স, চ্যাট এবং রেপো মেমোরি ইন্ডেক্সিংয়ের জন্য আমদানি করা হয় — তাদের মূল ফাইল কখনো পুনরায় লেখা হয় না।',
'memoryData.internalFiles': 'অভ্যন্তরীণ মেমোরি-ট্রি ফাইল',
'memoryData.internalFilesDesc':
'ইন্ডেক্স, কিউ স্টেট এবং সারসংক্ষেপ OpenHuman দ্বারা পরিচালিত হয় যাতে রিকল ও সিঙ্ক সুস্থ থাকে।',
'memoryData.windowError': 'মেমোরি উইন্ডো',
'memoryData.windowUpdated': 'মেমোরি উইন্ডো আপডেট হয়েছে',
'memoryData.windowUpdatedMsg': '{window}-এ সেট করা হয়েছে।',
};
export default messages;
+64
View File
@@ -4639,6 +4639,70 @@ const messages: TranslationMap = {
'Moderate Aktivität stündliche Synchronisierung, tägliche Zusammenfassung.',
'onboarding.custom.activity.configureDesc':
'Eigene Aktivitätsstufe wählen. Konfigurieren in Einstellungen Agent-Aktivitätsstufe.',
// Onboarding: Custom > Vault
'onboarding.custom.stepperVault': 'Vault',
'onboarding.custom.vault.title': 'Speicher & Vault-Einrichtung',
'onboarding.custom.vault.subtitle':
'Bestätigen Sie, wohin Speichernotizen geschrieben werden, wie Quelldaten gelesen werden und ob Ihre Vault-Pipeline fehlerfrei ist.',
'onboarding.custom.vault.defaultDesc':
'Von OpenHuman verwaltete Standardspeichereinstellungen verwenden. Vault-Pfad und Synchronisierungsstatus können später eingesehen werden.',
'onboarding.custom.vault.configureDesc':
'Vault-Eigentümerschaft prüfen, Integritätsprüfungen durchführen und Speicherkontrollen jetzt anpassen.',
'onboarding.custom.vault.localDisabledReason':
'Die verwaltete Einrichtung erfordert eine OpenHuman-Anmeldung und ist im lokalen Modus nicht verfügbar.',
'onboarding.custom.vault.exitError':
'Onboarding konnte nicht abgeschlossen werden. Bitte versuchen Sie es erneut.',
// Vault Health
'vaultHealth.title': 'Vault-Integritätsprüfliste',
'vaultHealth.setupTitle': 'Vault-Einrichtungsstatus',
'vaultHealth.workspaceVault': 'Arbeitsbereich-Vault:',
'vaultHealth.refresh': 'Aktualisieren',
'vaultHealth.refreshing': 'Wird aktualisiert…',
'vaultHealth.revealFolder': 'Ordner anzeigen',
'vaultHealth.openInObsidian': 'In Obsidian öffnen',
'vaultHealth.installObsidian': 'Obsidian installieren',
'vaultHealth.openObsidianError': 'Obsidian konnte nicht geöffnet werden',
'vaultHealth.revealError': 'Vault-Ordner konnte nicht angezeigt werden',
'vaultHealth.downloadError': 'Obsidian-Downloadseite konnte nicht geöffnet werden',
'vaultHealth.loadError': 'Vault-Status konnte nicht geladen werden:',
'vaultHealth.lastSync': 'Letzte Synchronisierung:',
'vaultHealth.passed': 'Bestanden',
'vaultHealth.needsAttention': 'Aufmerksamkeit erforderlich',
'vaultHealth.existsLabel': 'Arbeitsbereich-Vault-Pfad vorhanden',
'vaultHealth.existsRecovery':
'Vault-Ordner fehlt. Starten Sie eine Synchronisierung oder erstellen Sie diesen Ordner und aktualisieren Sie dann diese Prüfliste.',
'vaultHealth.writableLabel': 'Vault ist durch OpenHuman beschreibbar',
'vaultHealth.writableRecovery':
'OpenHuman kann noch nicht in diesen Vault schreiben. Schreibberechtigungen erteilen und aktualisieren.',
'vaultHealth.obsidianLabel': 'Vault ist in Obsidian registriert',
'vaultHealth.obsidianRecovery':
'Wählen Sie in Obsidian „Ordner als Vault öffnen" für diesen Pfad und aktualisieren Sie dann diese Prüfliste.',
'vaultHealth.pipelineLabel': 'Speicher-Pipeline ist fehlerfrei',
'vaultHealth.pipelineRecovery':
'Speicher-Pipeline ist pausiert oder fehlerhaft. Auto-Synchronisierung im Memory-Tree-Status wieder aktivieren und erneut versuchen.',
'vaultHealth.timeNever': 'Nie',
'vaultHealth.timeJustNow': 'gerade eben',
'vaultHealth.timeMinAgo': 'vor {n} Min.',
'vaultHealth.timeHrAgo': 'vor {n} Std.',
'vaultHealth.timeDayAgo': 'vor {n} Tag',
'vaultHealth.timeDaysAgo': 'vor {n} Tagen',
// Memory Data
'memoryData.howItWorks': 'So funktioniert die Speicherablage',
'memoryData.workspaceVault': 'Arbeitsbereich-Vault · Schreiben',
'memoryData.workspaceVaultDesc':
'OpenHuman schreibt generierte Speichernotizen nach memory_tree/content.',
'memoryData.connectedSources': 'Verbundene Quellen · Lesen',
'memoryData.connectedSourcesDesc':
'Ordner, Postfächer, Chats und Repositories werden für die Speicherindizierung importiert die Originaldateien werden dabei nie überschrieben.',
'memoryData.internalFiles': 'Interne Memory-Tree-Dateien',
'memoryData.internalFilesDesc':
'Indizes, Warteschlangenstatus und Zusammenfassungen werden von OpenHuman verwaltet, um Erinnerung und Synchronisierung fehlerfrei zu halten.',
'memoryData.windowError': 'Speicherfenster',
'memoryData.windowUpdated': 'Speicherfenster aktualisiert',
'memoryData.windowUpdatedMsg': 'Auf {window} gesetzt.',
};
export default messages;
+64 -1
View File
@@ -609,13 +609,14 @@ const en: TranslationMap = {
'onboarding.apiKeys.continue': 'Save and continue',
'onboarding.apiKeys.saving': 'Saving…',
// Onboarding: Custom wizard (Inference / Voice / OAuth / Search / Activity / Memory)
// Onboarding: Custom wizard (Inference / Voice / OAuth / Search / Activity / Vault / Memory)
'onboarding.custom.stepperInference': 'Inference',
'onboarding.custom.stepperVoice': 'Voice',
'onboarding.custom.stepperOAuth': 'OAuth',
'onboarding.custom.stepperSearch': 'Search',
'onboarding.custom.stepperEmbeddings': 'Embeddings',
'onboarding.custom.stepperActivity': 'Activity',
'onboarding.custom.stepperVault': 'Vault',
'onboarding.custom.stepperMemory': 'Memory',
'onboarding.custom.stepCounter': 'Step {n} of {total}',
'onboarding.custom.defaultTitle': 'Default',
@@ -682,6 +683,18 @@ const en: TranslationMap = {
'onboarding.custom.activity.configureDesc':
'Pick your own activity level. Configure in Settings Agent activity level.',
// Onboarding: Custom > Vault
'onboarding.custom.vault.title': 'Memory & Vault Setup',
'onboarding.custom.vault.subtitle':
'Confirm where memory notes are written, how source data is read, and whether your vault pipeline is healthy.',
'onboarding.custom.vault.defaultDesc':
'Use OpenHuman-managed memory defaults. Vault path and sync health can still be reviewed later.',
'onboarding.custom.vault.configureDesc':
'Review vault ownership, run health checks, and tune memory controls now.',
'onboarding.custom.vault.localDisabledReason':
'Managed setup requires OpenHuman sign-in and is unavailable in local mode.',
'onboarding.custom.vault.exitError': 'Could not finish onboarding. Please try again.',
// Onboarding: Custom > Memory
'onboarding.custom.memory.title': 'Memory',
'onboarding.custom.memory.subtitle':
@@ -2122,6 +2135,56 @@ const en: TranslationMap = {
'workspace.trees': 'Trees',
'workspace.contacts': 'Contacts',
// Vault health checklist
'vaultHealth.title': 'Vault Health Checklist',
'vaultHealth.setupTitle': 'Vault setup health',
'vaultHealth.workspaceVault': 'Workspace vault:',
'vaultHealth.refresh': 'Refresh',
'vaultHealth.refreshing': 'Refreshing…',
'vaultHealth.revealFolder': 'Reveal Folder',
'vaultHealth.openInObsidian': 'Open in Obsidian',
'vaultHealth.installObsidian': 'Install Obsidian',
'vaultHealth.openObsidianError': 'Could not open Obsidian',
'vaultHealth.revealError': 'Could not reveal vault folder',
'vaultHealth.downloadError': 'Could not open Obsidian download page',
'vaultHealth.loadError': 'Could not load vault health:',
'vaultHealth.lastSync': 'Last sync:',
'vaultHealth.passed': 'Passed',
'vaultHealth.needsAttention': 'Needs attention',
'vaultHealth.existsLabel': 'Workspace vault path exists',
'vaultHealth.existsRecovery':
'Vault folder is missing. Start a sync or create this folder, then refresh this checklist.',
'vaultHealth.writableLabel': 'Vault is writable by OpenHuman',
'vaultHealth.writableRecovery':
'OpenHuman cannot write to this vault yet. Grant write permissions and refresh.',
'vaultHealth.obsidianLabel': 'Vault is registered in Obsidian',
'vaultHealth.obsidianRecovery':
'In Obsidian, choose "Open folder as vault" for this path, then refresh this checklist.',
'vaultHealth.pipelineLabel': 'Memory pipeline is healthy',
'vaultHealth.pipelineRecovery':
'Memory pipeline is paused or in error. Re-enable Auto-sync in Memory Tree status and retry.',
'vaultHealth.timeNever': 'Never',
'vaultHealth.timeJustNow': 'just now',
'vaultHealth.timeMinAgo': '{n} min ago',
'vaultHealth.timeHrAgo': '{n} hr ago',
'vaultHealth.timeDayAgo': '{n} day ago',
'vaultHealth.timeDaysAgo': '{n} days ago',
// Memory data panel (storage explainer)
'memoryData.howItWorks': 'How memory storage works',
'memoryData.workspaceVault': 'Workspace vault · write',
'memoryData.workspaceVaultDesc':
'OpenHuman writes generated memory notes to memory_tree/content.',
'memoryData.connectedSources': 'Connected sources · read',
'memoryData.connectedSourcesDesc':
'Folders, mailboxes, chats, and repos are imported for memory indexing — their original files are never rewritten.',
'memoryData.internalFiles': 'Internal memory-tree files',
'memoryData.internalFilesDesc':
'Indexes, queue state, and summaries are managed by OpenHuman to keep recall and sync healthy.',
'memoryData.windowError': 'Memory window',
'memoryData.windowUpdated': 'Memory window updated',
'memoryData.windowUpdatedMsg': 'Set to {window}.',
// Graph
'graph.noContactMentions': 'No contact mentions',
'graph.noMemory': 'No memory',
+64
View File
@@ -4596,6 +4596,7 @@ const messages: TranslationMap = {
// Onboarding: Custom > Activity
'onboarding.custom.stepperActivity': 'Actividad',
'onboarding.custom.stepperVault': 'Bóveda',
'onboarding.custom.activity.title': 'Actividad del agente',
'onboarding.custom.activity.subtitle':
'Qué tan proactivamente monitorea y actúa tu agente en segundo plano.',
@@ -4603,6 +4604,69 @@ const messages: TranslationMap = {
'Actividad moderada — sincronización por hora, resumen diario.',
'onboarding.custom.activity.configureDesc':
'Elige tu propio nivel de actividad. Configurar en Ajustes Nivel de actividad del agente.',
// Onboarding: Custom > Vault
'onboarding.custom.vault.title': 'Configuración de memoria y bóveda',
'onboarding.custom.vault.subtitle':
'Confirma dónde se escriben las notas de memoria, cómo se leen los datos de origen y si el flujo de bóveda está en buen estado.',
'onboarding.custom.vault.defaultDesc':
'Usa los valores predeterminados de memoria administrada por OpenHuman. La ruta de la bóveda y el estado de sincronización se pueden revisar más adelante.',
'onboarding.custom.vault.configureDesc':
'Revisa la propiedad de la bóveda, ejecuta comprobaciones de estado y ajusta los controles de memoria ahora.',
'onboarding.custom.vault.localDisabledReason':
'La configuración administrada requiere inicio de sesión en OpenHuman y no está disponible en modo local.',
'onboarding.custom.vault.exitError':
'No se pudo completar el proceso de incorporación. Por favor, inténtalo de nuevo.',
// Vault Health
'vaultHealth.title': 'Lista de verificación de salud de la bóveda',
'vaultHealth.setupTitle': 'Estado de configuración de la bóveda',
'vaultHealth.workspaceVault': 'Bóveda del espacio de trabajo:',
'vaultHealth.refresh': 'Actualizar',
'vaultHealth.refreshing': 'Actualizando…',
'vaultHealth.revealFolder': 'Mostrar carpeta',
'vaultHealth.openInObsidian': 'Abrir en Obsidian',
'vaultHealth.installObsidian': 'Instalar Obsidian',
'vaultHealth.openObsidianError': 'No se pudo abrir Obsidian',
'vaultHealth.revealError': 'No se pudo mostrar la carpeta de la bóveda',
'vaultHealth.downloadError': 'No se pudo abrir la página de descarga de Obsidian',
'vaultHealth.loadError': 'No se pudo cargar el estado de la bóveda:',
'vaultHealth.lastSync': 'Última sincronización:',
'vaultHealth.passed': 'Superado',
'vaultHealth.needsAttention': 'Requiere atención',
'vaultHealth.existsLabel': 'La ruta de la bóveda del espacio de trabajo existe',
'vaultHealth.existsRecovery':
'La carpeta de la bóveda no existe. Inicia una sincronización o crea esta carpeta y luego actualiza esta lista.',
'vaultHealth.writableLabel': 'OpenHuman puede escribir en la bóveda',
'vaultHealth.writableRecovery':
'OpenHuman aún no puede escribir en esta bóveda. Concede permisos de escritura y actualiza.',
'vaultHealth.obsidianLabel': 'La bóveda está registrada en Obsidian',
'vaultHealth.obsidianRecovery':
'En Obsidian, elige "Abrir carpeta como bóveda" para esta ruta y luego actualiza esta lista.',
'vaultHealth.pipelineLabel': 'El flujo de memoria está en buen estado',
'vaultHealth.pipelineRecovery':
'El flujo de memoria está pausado o en error. Vuelve a habilitar la sincronización automática en el estado del árbol de memoria y reintenta.',
'vaultHealth.timeNever': 'Nunca',
'vaultHealth.timeJustNow': 'ahora mismo',
'vaultHealth.timeMinAgo': 'hace {n} min',
'vaultHealth.timeHrAgo': 'hace {n} h',
'vaultHealth.timeDayAgo': 'hace {n} día',
'vaultHealth.timeDaysAgo': 'hace {n} días',
// Memory Data
'memoryData.howItWorks': 'Cómo funciona el almacenamiento de memoria',
'memoryData.workspaceVault': 'Bóveda del espacio de trabajo · escritura',
'memoryData.workspaceVaultDesc':
'OpenHuman escribe las notas de memoria generadas en memory_tree/content.',
'memoryData.connectedSources': 'Fuentes conectadas · lectura',
'memoryData.connectedSourcesDesc':
'Carpetas, buzones, chats y repositorios se importan para la indexación de memoria; sus archivos originales nunca se reescriben.',
'memoryData.internalFiles': 'Archivos internos del árbol de memoria',
'memoryData.internalFilesDesc':
'Los índices, el estado de la cola y los resúmenes son administrados por OpenHuman para mantener en buen estado la recuperación y la sincronización.',
'memoryData.windowError': 'Ventana de memoria',
'memoryData.windowUpdated': 'Ventana de memoria actualizada',
'memoryData.windowUpdatedMsg': 'Establecida en {window}.',
};
export default messages;
+63
View File
@@ -4613,6 +4613,7 @@ const messages: TranslationMap = {
// Onboarding: Custom > Activity
'onboarding.custom.stepperActivity': 'Activité',
'onboarding.custom.stepperVault': 'Coffre',
'onboarding.custom.activity.title': "Activité de l'agent",
'onboarding.custom.activity.subtitle':
'À quel point votre agent surveille et agit en arrière-plan.',
@@ -4620,6 +4621,68 @@ const messages: TranslationMap = {
'Activité modérée — synchronisation horaire, résumé quotidien.',
'onboarding.custom.activity.configureDesc':
"Choisissez votre propre niveau d'activité. Configurer dans Paramètres Niveau d'activité de l'agent.",
// Onboarding: Custom > Vault
'onboarding.custom.vault.title': 'Configuration de la mémoire et du coffre',
'onboarding.custom.vault.subtitle':
'Confirmez où les notes de mémoire sont écrites, comment les données sources sont lues et si votre pipeline de coffre est en bon état.',
'onboarding.custom.vault.defaultDesc':
"Utiliser les paramètres de mémoire gérés par OpenHuman. Le chemin du coffre et l'état de synchronisation peuvent encore être consultés ultérieurement.",
'onboarding.custom.vault.configureDesc':
'Vérifiez la propriété du coffre, exécutez des contrôles de santé et affinez les contrôles de mémoire maintenant.',
'onboarding.custom.vault.localDisabledReason':
"La configuration gérée nécessite une connexion à OpenHuman et n'est pas disponible en mode local.",
'onboarding.custom.vault.exitError': "Impossible de terminer l'intégration. Veuillez réessayer.",
// Vault Health
'vaultHealth.title': 'Liste de contrôle de santé du coffre',
'vaultHealth.setupTitle': 'Santé de la configuration du coffre',
'vaultHealth.workspaceVault': "Coffre de l'espace de travail :",
'vaultHealth.refresh': 'Actualiser',
'vaultHealth.refreshing': 'Actualisation…',
'vaultHealth.revealFolder': 'Afficher le dossier',
'vaultHealth.openInObsidian': 'Ouvrir dans Obsidian',
'vaultHealth.installObsidian': 'Installer Obsidian',
'vaultHealth.openObsidianError': "Impossible d'ouvrir Obsidian",
'vaultHealth.revealError': "Impossible d'afficher le dossier du coffre",
'vaultHealth.downloadError': "Impossible d'ouvrir la page de téléchargement d'Obsidian",
'vaultHealth.loadError': 'Impossible de charger la santé du coffre :',
'vaultHealth.lastSync': 'Dernière synchronisation :',
'vaultHealth.passed': 'Réussi',
'vaultHealth.needsAttention': 'Attention requise',
'vaultHealth.existsLabel': "Le chemin du coffre de l'espace de travail existe",
'vaultHealth.existsRecovery':
'Le dossier du coffre est manquant. Lancez une synchronisation ou créez ce dossier, puis actualisez cette liste de contrôle.',
'vaultHealth.writableLabel': 'Le coffre est accessible en écriture par OpenHuman',
'vaultHealth.writableRecovery':
"OpenHuman ne peut pas encore écrire dans ce coffre. Accordez les autorisations d'écriture et actualisez.",
'vaultHealth.obsidianLabel': 'Le coffre est enregistré dans Obsidian',
'vaultHealth.obsidianRecovery':
'Dans Obsidian, choisissez « Ouvrir le dossier comme coffre » pour ce chemin, puis actualisez cette liste de contrôle.',
'vaultHealth.pipelineLabel': 'Le pipeline de mémoire est en bon état',
'vaultHealth.pipelineRecovery':
"Le pipeline de mémoire est en pause ou en erreur. Réactivez la synchronisation automatique dans le statut de l'arbre de mémoire et réessayez.",
'vaultHealth.timeNever': 'Jamais',
'vaultHealth.timeJustNow': "à l'instant",
'vaultHealth.timeMinAgo': 'il y a {n} min',
'vaultHealth.timeHrAgo': 'il y a {n} h',
'vaultHealth.timeDayAgo': 'il y a {n} jour',
'vaultHealth.timeDaysAgo': 'il y a {n} jours',
// Memory Data
'memoryData.howItWorks': 'Fonctionnement du stockage de la mémoire',
'memoryData.workspaceVault': "Coffre de l'espace de travail · écriture",
'memoryData.workspaceVaultDesc':
'OpenHuman écrit les notes de mémoire générées dans memory_tree/content.',
'memoryData.connectedSources': 'Sources connectées · lecture',
'memoryData.connectedSourcesDesc':
"Les dossiers, boîtes mail, conversations et dépôts sont importés pour l'indexation de la mémoire — leurs fichiers originaux ne sont jamais réécrits.",
'memoryData.internalFiles': "Fichiers internes de l'arbre de mémoire",
'memoryData.internalFilesDesc':
"Les index, l'état de la file d'attente et les résumés sont gérés par OpenHuman pour maintenir la mémoire et la synchronisation en bon état.",
'memoryData.windowError': 'Fenêtre de mémoire',
'memoryData.windowUpdated': 'Fenêtre de mémoire mise à jour',
'memoryData.windowUpdatedMsg': 'Définie sur {window}.',
};
export default messages;
+57
View File
@@ -4517,12 +4517,69 @@ const messages: TranslationMap = {
// Onboarding: Custom > Activity
'onboarding.custom.stepperActivity': 'गतिविधि',
'onboarding.custom.stepperVault': 'वॉल्ट',
'onboarding.custom.activity.title': 'एजेंट गतिविधि',
'onboarding.custom.activity.subtitle':
'आपका एजेंट पृष्ठभूमि में कितनी सक्रियता से निगरानी और कार्य करता है।',
'onboarding.custom.activity.defaultDesc': 'मध्यम गतिविधि — प्रति घंटे सिंक, दैनिक सारांश।',
'onboarding.custom.activity.configureDesc':
'अपना गतिविधि स्तर चुनें। सेटिंग्स › एजेंट गतिविधि स्तर में कॉन्फ़िगर करें।',
'onboarding.custom.vault.title': 'मेमोरी और वॉल्ट सेटअप',
'onboarding.custom.vault.subtitle':
'पुष्टि करें कि मेमोरी नोट्स कहाँ लिखे जाते हैं, स्रोत डेटा कैसे पढ़ा जाता है, और आपका वॉल्ट पाइपलाइन स्वस्थ है या नहीं।',
'onboarding.custom.vault.defaultDesc':
'OpenHuman-प्रबंधित मेमोरी डिफ़ॉल्ट का उपयोग करें। वॉल्ट पाथ और सिंक स्वास्थ्य बाद में भी देखे जा सकते हैं।',
'onboarding.custom.vault.configureDesc':
'वॉल्ट स्वामित्व की समीक्षा करें, स्वास्थ्य जाँच चलाएँ और अभी मेमोरी नियंत्रण ठीक करें।',
'onboarding.custom.vault.localDisabledReason':
'प्रबंधित सेटअप के लिए OpenHuman साइन-इन आवश्यक है और लोकल मोड में उपलब्ध नहीं है।',
'onboarding.custom.vault.exitError': 'ऑनबोर्डिंग पूरी नहीं हो सकी। कृपया पुनः प्रयास करें।',
'vaultHealth.title': 'वॉल्ट स्वास्थ्य चेकलिस्ट',
'vaultHealth.setupTitle': 'वॉल्ट सेटअप स्वास्थ्य',
'vaultHealth.workspaceVault': 'वर्कस्पेस वॉल्ट:',
'vaultHealth.refresh': 'रिफ्रेश',
'vaultHealth.refreshing': 'रिफ्रेश हो रहा है…',
'vaultHealth.revealFolder': 'फ़ोल्डर दिखाएँ',
'vaultHealth.openInObsidian': 'Obsidian में खोलें',
'vaultHealth.installObsidian': 'Obsidian इंस्टॉल करें',
'vaultHealth.openObsidianError': 'Obsidian नहीं खोला जा सका',
'vaultHealth.revealError': 'वॉल्ट फ़ोल्डर नहीं दिखाया जा सका',
'vaultHealth.downloadError': 'Obsidian डाउनलोड पृष्ठ नहीं खोला जा सका',
'vaultHealth.loadError': 'वॉल्ट स्वास्थ्य लोड नहीं हो सका:',
'vaultHealth.lastSync': 'अंतिम सिंक:',
'vaultHealth.passed': 'पास',
'vaultHealth.needsAttention': 'ध्यान आवश्यक',
'vaultHealth.existsLabel': 'वर्कस्पेस वॉल्ट पाथ मौजूद है',
'vaultHealth.existsRecovery':
'वॉल्ट फ़ोल्डर गायब है। एक सिंक शुरू करें या यह फ़ोल्डर बनाएँ, फिर इस चेकलिस्ट को रिफ्रेश करें।',
'vaultHealth.writableLabel': 'वॉल्ट OpenHuman द्वारा लिखने योग्य है',
'vaultHealth.writableRecovery':
'OpenHuman अभी इस वॉल्ट में नहीं लिख सकता। लिखने की अनुमति दें और रिफ्रेश करें।',
'vaultHealth.obsidianLabel': 'वॉल्ट Obsidian में पंजीकृत है',
'vaultHealth.obsidianRecovery':
'Obsidian में इस पाथ के लिए "फ़ोल्डर को वॉल्ट के रूप में खोलें" चुनें, फिर इस चेकलिस्ट को रिफ्रेश करें।',
'vaultHealth.pipelineLabel': 'मेमोरी पाइपलाइन स्वस्थ है',
'vaultHealth.pipelineRecovery':
'मेमोरी पाइपलाइन रुकी हुई है या त्रुटि में है। मेमोरी ट्री स्थिति में ऑटो-सिंक पुनः सक्षम करें और पुनः प्रयास करें।',
'vaultHealth.timeNever': 'कभी नहीं',
'vaultHealth.timeJustNow': 'अभी-अभी',
'vaultHealth.timeMinAgo': '{n} मिनट पहले',
'vaultHealth.timeHrAgo': '{n} घंटे पहले',
'vaultHealth.timeDayAgo': '{n} दिन पहले',
'vaultHealth.timeDaysAgo': '{n} दिन पहले',
'memoryData.howItWorks': 'मेमोरी स्टोरेज कैसे काम करता है',
'memoryData.workspaceVault': 'वर्कस्पेस वॉल्ट · लिखना',
'memoryData.workspaceVaultDesc':
'OpenHuman जनरेट किए गए मेमोरी नोट्स को memory_tree/content में लिखता है।',
'memoryData.connectedSources': 'कनेक्टेड स्रोत · पढ़ना',
'memoryData.connectedSourcesDesc':
'फ़ोल्डर, मेलबॉक्स, चैट और रेपो मेमोरी इंडेक्सिंग के लिए आयात किए जाते हैं — उनकी मूल फ़ाइलें कभी नहीं बदली जातीं।',
'memoryData.internalFiles': 'आंतरिक मेमोरी-ट्रि फ़ाइलें',
'memoryData.internalFilesDesc':
'इंडेक्स, क्यू स्थिति और सारांश OpenHuman द्वारा प्रबंधित किए जाते हैं ताकि रिकॉल और सिंक स्वस्थ रहे।',
'memoryData.windowError': 'मेमोरी विंडो',
'memoryData.windowUpdated': 'मेमोरी विंडो अपडेट हुई',
'memoryData.windowUpdatedMsg': '{window} पर सेट किया गया।',
};
export default messages;
+57
View File
@@ -4530,6 +4530,7 @@ const messages: TranslationMap = {
// Onboarding: Custom > Activity
'onboarding.custom.stepperActivity': 'Aktivitas',
'onboarding.custom.stepperVault': 'Vault',
'onboarding.custom.activity.title': 'Aktivitas agen',
'onboarding.custom.activity.subtitle':
'Seberapa proaktif agen Anda memantau dan bertindak di latar belakang.',
@@ -4537,6 +4538,62 @@ const messages: TranslationMap = {
'Aktivitas sedang — sinkronisasi per jam, ringkasan harian.',
'onboarding.custom.activity.configureDesc':
'Pilih tingkat aktivitas Anda sendiri. Konfigurasi di Pengaturan Tingkat aktivitas agen.',
'onboarding.custom.vault.title': 'Pengaturan Memori & Vault',
'onboarding.custom.vault.subtitle':
'Konfirmasi di mana catatan memori ditulis, bagaimana data sumber dibaca, dan apakah pipeline vault Anda sehat.',
'onboarding.custom.vault.defaultDesc':
'Gunakan default memori yang dikelola OpenHuman. Jalur vault dan kondisi sinkronisasi tetap dapat ditinjau nanti.',
'onboarding.custom.vault.configureDesc':
'Tinjau kepemilikan vault, jalankan pemeriksaan kesehatan, dan sesuaikan kontrol memori sekarang.',
'onboarding.custom.vault.localDisabledReason':
'Pengaturan terkelola memerlukan masuk OpenHuman dan tidak tersedia dalam mode lokal.',
'onboarding.custom.vault.exitError': 'Tidak dapat menyelesaikan orientasi. Silakan coba lagi.',
'vaultHealth.title': 'Daftar Periksa Kesehatan Vault',
'vaultHealth.setupTitle': 'Kesehatan pengaturan vault',
'vaultHealth.workspaceVault': 'Vault ruang kerja:',
'vaultHealth.refresh': 'Segarkan',
'vaultHealth.refreshing': 'Menyegarkan…',
'vaultHealth.revealFolder': 'Tampilkan Folder',
'vaultHealth.openInObsidian': 'Buka di Obsidian',
'vaultHealth.installObsidian': 'Pasang Obsidian',
'vaultHealth.openObsidianError': 'Tidak dapat membuka Obsidian',
'vaultHealth.revealError': 'Tidak dapat menampilkan folder vault',
'vaultHealth.downloadError': 'Tidak dapat membuka halaman unduhan Obsidian',
'vaultHealth.loadError': 'Tidak dapat memuat kesehatan vault:',
'vaultHealth.lastSync': 'Sinkronisasi terakhir:',
'vaultHealth.passed': 'Lulus',
'vaultHealth.needsAttention': 'Perlu perhatian',
'vaultHealth.existsLabel': 'Jalur vault ruang kerja ada',
'vaultHealth.existsRecovery':
'Folder vault tidak ditemukan. Mulai sinkronisasi atau buat folder ini, lalu segarkan daftar periksa ini.',
'vaultHealth.writableLabel': 'Vault dapat ditulis oleh OpenHuman',
'vaultHealth.writableRecovery':
'OpenHuman belum dapat menulis ke vault ini. Berikan izin tulis dan segarkan.',
'vaultHealth.obsidianLabel': 'Vault terdaftar di Obsidian',
'vaultHealth.obsidianRecovery':
'Di Obsidian, pilih "Buka folder sebagai vault" untuk jalur ini, lalu segarkan daftar periksa ini.',
'vaultHealth.pipelineLabel': 'Pipeline memori sehat',
'vaultHealth.pipelineRecovery':
'Pipeline memori dijeda atau mengalami kesalahan. Aktifkan kembali Sinkronisasi Otomatis di status Pohon Memori dan coba lagi.',
'vaultHealth.timeNever': 'Tidak pernah',
'vaultHealth.timeJustNow': 'baru saja',
'vaultHealth.timeMinAgo': '{n} menit lalu',
'vaultHealth.timeHrAgo': '{n} jam lalu',
'vaultHealth.timeDayAgo': '{n} hari lalu',
'vaultHealth.timeDaysAgo': '{n} hari lalu',
'memoryData.howItWorks': 'Cara kerja penyimpanan memori',
'memoryData.workspaceVault': 'Vault ruang kerja · tulis',
'memoryData.workspaceVaultDesc':
'OpenHuman menulis catatan memori yang dihasilkan ke memory_tree/content.',
'memoryData.connectedSources': 'Sumber terhubung · baca',
'memoryData.connectedSourcesDesc':
'Folder, kotak surat, obrolan, dan repositori diimpor untuk pengindeksan memori — file aslinya tidak pernah ditulis ulang.',
'memoryData.internalFiles': 'File pohon memori internal',
'memoryData.internalFilesDesc':
'Indeks, status antrean, dan ringkasan dikelola oleh OpenHuman agar pemanggilan dan sinkronisasi tetap sehat.',
'memoryData.windowError': 'Jendela memori',
'memoryData.windowUpdated': 'Jendela memori diperbarui',
'memoryData.windowUpdatedMsg': 'Diatur ke {window}.',
};
export default messages;
+63
View File
@@ -4590,6 +4590,7 @@ const messages: TranslationMap = {
// Onboarding: Custom > Activity
'onboarding.custom.stepperActivity': 'Attività',
'onboarding.custom.stepperVault': 'Vault',
'onboarding.custom.activity.title': "Attività dell'agente",
'onboarding.custom.activity.subtitle':
'Quanto proattivamente il tuo agente monitora e agisce in background.',
@@ -4597,6 +4598,68 @@ const messages: TranslationMap = {
'Attività moderata — sincronizzazione oraria, riepilogo giornaliero.',
'onboarding.custom.activity.configureDesc':
"Scegli il tuo livello di attività. Configura in Impostazioni Livello di attività dell'agente.",
// Onboarding: Custom > Vault
'onboarding.custom.vault.title': 'Configurazione memoria e Vault',
'onboarding.custom.vault.subtitle':
'Conferma dove vengono scritte le note di memoria, come vengono letti i dati sorgente e se la pipeline del vault è integra.',
'onboarding.custom.vault.defaultDesc':
'Usa le impostazioni di memoria predefinite gestite da OpenHuman. Il percorso del vault e la salute della sincronizzazione possono essere verificati in seguito.',
'onboarding.custom.vault.configureDesc':
'Verifica la proprietà del vault, esegui controlli di integrità e ottimizza i controlli della memoria ora.',
'onboarding.custom.vault.localDisabledReason':
'La configurazione gestita richiede il login a OpenHuman e non è disponibile in modalità locale.',
'onboarding.custom.vault.exitError': "Impossibile completare l'onboarding. Riprova.",
// Vault Health
'vaultHealth.title': 'Checklist integrità vault',
'vaultHealth.setupTitle': 'Integrità configurazione vault',
'vaultHealth.workspaceVault': 'Vault workspace:',
'vaultHealth.refresh': 'Aggiorna',
'vaultHealth.refreshing': 'Aggiornamento in corso…',
'vaultHealth.revealFolder': 'Mostra cartella',
'vaultHealth.openInObsidian': 'Apri in Obsidian',
'vaultHealth.installObsidian': 'Installa Obsidian',
'vaultHealth.openObsidianError': 'Impossibile aprire Obsidian',
'vaultHealth.revealError': 'Impossibile mostrare la cartella del vault',
'vaultHealth.downloadError': 'Impossibile aprire la pagina di download di Obsidian',
'vaultHealth.loadError': "Impossibile caricare l'integrità del vault:",
'vaultHealth.lastSync': 'Ultima sincronizzazione:',
'vaultHealth.passed': 'Superato',
'vaultHealth.needsAttention': 'Richiede attenzione',
'vaultHealth.existsLabel': 'Il percorso del vault workspace esiste',
'vaultHealth.existsRecovery':
'La cartella del vault è assente. Avvia una sincronizzazione o crea questa cartella, poi aggiorna la checklist.',
'vaultHealth.writableLabel': 'Il vault è scrivibile da OpenHuman',
'vaultHealth.writableRecovery':
'OpenHuman non riesce ancora a scrivere in questo vault. Concedi i permessi di scrittura e aggiorna.',
'vaultHealth.obsidianLabel': 'Il vault è registrato in Obsidian',
'vaultHealth.obsidianRecovery':
'In Obsidian, scegli "Apri cartella come vault" per questo percorso, poi aggiorna la checklist.',
'vaultHealth.pipelineLabel': 'La pipeline di memoria è integra',
'vaultHealth.pipelineRecovery':
'La pipeline di memoria è in pausa o in errore. Riattiva la sincronizzazione automatica nello stato del Memory Tree e riprova.',
'vaultHealth.timeNever': 'Mai',
'vaultHealth.timeJustNow': 'poco fa',
'vaultHealth.timeMinAgo': '{n} min fa',
'vaultHealth.timeHrAgo': '{n} ora fa',
'vaultHealth.timeDayAgo': '{n} giorno fa',
'vaultHealth.timeDaysAgo': '{n} giorni fa',
// Memory Data
'memoryData.howItWorks': 'Come funziona la memorizzazione',
'memoryData.workspaceVault': 'Vault workspace · scrittura',
'memoryData.workspaceVaultDesc':
'OpenHuman scrive le note di memoria generate in memory_tree/content.',
'memoryData.connectedSources': 'Fonti collegate · lettura',
'memoryData.connectedSourcesDesc':
"Cartelle, caselle di posta, chat e repository vengono importati per l'indicizzazione della memoria — i file originali non vengono mai riscritti.",
'memoryData.internalFiles': 'File interni del memory-tree',
'memoryData.internalFilesDesc':
'Indici, stato della coda e riepiloghi sono gestiti da OpenHuman per mantenere efficiente il richiamo e la sincronizzazione.',
'memoryData.windowError': 'Finestra di memoria',
'memoryData.windowUpdated': 'Finestra di memoria aggiornata',
'memoryData.windowUpdatedMsg': 'Impostata su {window}.',
};
export default messages;
+57
View File
@@ -4473,12 +4473,69 @@ const messages: TranslationMap = {
// Onboarding: Custom > Activity
'onboarding.custom.stepperActivity': '활동',
'onboarding.custom.stepperVault': '볼트',
'onboarding.custom.activity.title': '에이전트 활동',
'onboarding.custom.activity.subtitle':
'에이전트가 백그라운드에서 얼마나 능동적으로 모니터링하고 행동하는지.',
'onboarding.custom.activity.defaultDesc': '보통 활동 — 매시간 동기화, 일일 요약.',
'onboarding.custom.activity.configureDesc':
'자신만의 활동 수준을 선택하세요. 설정 › 에이전트 활동 수준에서 구성하세요.',
'onboarding.custom.vault.title': '메모리 및 볼트 설정',
'onboarding.custom.vault.subtitle':
'메모리 노트가 기록되는 위치, 소스 데이터를 읽는 방법, 볼트 파이프라인이 정상인지 확인하세요.',
'onboarding.custom.vault.defaultDesc':
'OpenHuman 관리형 메모리 기본값을 사용합니다. 볼트 경로와 동기화 상태는 나중에 검토할 수 있습니다.',
'onboarding.custom.vault.configureDesc':
'볼트 소유권을 검토하고 상태 검사를 실행하며 메모리 컨트롤을 지금 조정하세요.',
'onboarding.custom.vault.localDisabledReason':
'관리형 설정은 OpenHuman 로그인이 필요하며 로컬 모드에서는 사용할 수 없습니다.',
'onboarding.custom.vault.exitError': '온보딩을 완료할 수 없습니다. 다시 시도해 주세요.',
'vaultHealth.title': '볼트 상태 체크리스트',
'vaultHealth.setupTitle': '볼트 설정 상태',
'vaultHealth.workspaceVault': '워크스페이스 볼트:',
'vaultHealth.refresh': '새로 고침',
'vaultHealth.refreshing': '새로 고침 중…',
'vaultHealth.revealFolder': '폴더 표시',
'vaultHealth.openInObsidian': 'Obsidian에서 열기',
'vaultHealth.installObsidian': 'Obsidian 설치',
'vaultHealth.openObsidianError': 'Obsidian을 열 수 없습니다',
'vaultHealth.revealError': '볼트 폴더를 표시할 수 없습니다',
'vaultHealth.downloadError': 'Obsidian 다운로드 페이지를 열 수 없습니다',
'vaultHealth.loadError': '볼트 상태를 불러올 수 없습니다:',
'vaultHealth.lastSync': '마지막 동기화:',
'vaultHealth.passed': '통과',
'vaultHealth.needsAttention': '주의 필요',
'vaultHealth.existsLabel': '워크스페이스 볼트 경로가 존재합니다',
'vaultHealth.existsRecovery':
'볼트 폴더가 없습니다. 동기화를 시작하거나 이 폴더를 만든 후 체크리스트를 새로 고침하세요.',
'vaultHealth.writableLabel': 'OpenHuman이 볼트에 쓸 수 있습니다',
'vaultHealth.writableRecovery':
'OpenHuman이 아직 이 볼트에 쓸 수 없습니다. 쓰기 권한을 부여하고 새로 고침하세요.',
'vaultHealth.obsidianLabel': '볼트가 Obsidian에 등록되어 있습니다',
'vaultHealth.obsidianRecovery':
'Obsidian에서 이 경로에 대해 "폴더를 볼트로 열기"를 선택한 후 체크리스트를 새로 고침하세요.',
'vaultHealth.pipelineLabel': '메모리 파이프라인이 정상입니다',
'vaultHealth.pipelineRecovery':
'메모리 파이프라인이 일시 중지되었거나 오류 상태입니다. 메모리 트리 상태에서 자동 동기화를 다시 활성화하고 재시도하세요.',
'vaultHealth.timeNever': '없음',
'vaultHealth.timeJustNow': '방금 전',
'vaultHealth.timeMinAgo': '{n}분 전',
'vaultHealth.timeHrAgo': '{n}시간 전',
'vaultHealth.timeDayAgo': '{n}일 전',
'vaultHealth.timeDaysAgo': '{n}일 전',
'memoryData.howItWorks': '메모리 저장 방식',
'memoryData.workspaceVault': '워크스페이스 볼트 · 쓰기',
'memoryData.workspaceVaultDesc':
'OpenHuman이 생성된 메모리 노트를 memory_tree/content에 기록합니다.',
'memoryData.connectedSources': '연결된 소스 · 읽기',
'memoryData.connectedSourcesDesc':
'폴더, 사서함, 채팅, 저장소가 메모리 인덱싱을 위해 가져와집니다 — 원본 파일은 절대 수정되지 않습니다.',
'memoryData.internalFiles': '내부 메모리 트리 파일',
'memoryData.internalFilesDesc':
'인덱스, 큐 상태, 요약은 OpenHuman이 관리하여 회상과 동기화를 정상 상태로 유지합니다.',
'memoryData.windowError': '메모리 창',
'memoryData.windowUpdated': '메모리 창 업데이트됨',
'memoryData.windowUpdatedMsg': '{window}(으)로 설정되었습니다.',
};
export default messages;
+57
View File
@@ -4587,12 +4587,69 @@ const messages: TranslationMap = {
// Onboarding: Custom > Activity
'onboarding.custom.stepperActivity': 'Aktywność',
'onboarding.custom.stepperVault': 'Skarbiec',
'onboarding.custom.activity.title': 'Aktywność agenta',
'onboarding.custom.activity.subtitle': 'Jak proaktywnie Twój agent monitoruje i działa w tle.',
'onboarding.custom.activity.defaultDesc':
'Umiarkowana aktywność — synchronizacja co godzinę, codzienny skrót.',
'onboarding.custom.activity.configureDesc':
'Wybierz własny poziom aktywności. Skonfiguruj w Ustawieniach Poziom aktywności agenta.',
'onboarding.custom.vault.title': 'Konfiguracja pamięci i skarbca',
'onboarding.custom.vault.subtitle':
'Potwierdź, gdzie zapisywane są notatki pamięci, jak odczytywane są dane źródłowe i czy potok skarbca działa poprawnie.',
'onboarding.custom.vault.defaultDesc':
'Użyj domyślnych ustawień pamięci zarządzanej przez OpenHuman. Ścieżkę skarbca i stan synchronizacji można sprawdzić później.',
'onboarding.custom.vault.configureDesc':
'Sprawdź właściciela skarbca, uruchom kontrole stanu i dostosuj teraz ustawienia pamięci.',
'onboarding.custom.vault.localDisabledReason':
'Konfiguracja zarządzana wymaga logowania do OpenHuman i jest niedostępna w trybie lokalnym.',
'onboarding.custom.vault.exitError': 'Nie udało się zakończyć wdrożenia. Spróbuj ponownie.',
'vaultHealth.title': 'Lista kontrolna stanu skarbca',
'vaultHealth.setupTitle': 'Stan konfiguracji skarbca',
'vaultHealth.workspaceVault': 'Skarbiec obszaru roboczego:',
'vaultHealth.refresh': 'Odśwież',
'vaultHealth.refreshing': 'Odświeżanie…',
'vaultHealth.revealFolder': 'Pokaż folder',
'vaultHealth.openInObsidian': 'Otwórz w Obsidian',
'vaultHealth.installObsidian': 'Zainstaluj Obsidian',
'vaultHealth.openObsidianError': 'Nie można otworzyć Obsidian',
'vaultHealth.revealError': 'Nie można wyświetlić folderu skarbca',
'vaultHealth.downloadError': 'Nie można otworzyć strony pobierania Obsidian',
'vaultHealth.loadError': 'Nie można załadować stanu skarbca:',
'vaultHealth.lastSync': 'Ostatnia synchronizacja:',
'vaultHealth.passed': 'Zaliczone',
'vaultHealth.needsAttention': 'Wymaga uwagi',
'vaultHealth.existsLabel': 'Ścieżka skarbca obszaru roboczego istnieje',
'vaultHealth.existsRecovery':
'Folder skarbca jest niedostępny. Rozpocznij synchronizację lub utwórz ten folder, a następnie odśwież listę kontrolną.',
'vaultHealth.writableLabel': 'Skarbiec jest zapisywalny przez OpenHuman',
'vaultHealth.writableRecovery':
'OpenHuman nie może jeszcze zapisywać do tego skarbca. Nadaj uprawnienia do zapisu i odśwież.',
'vaultHealth.obsidianLabel': 'Skarbiec jest zarejestrowany w Obsidian',
'vaultHealth.obsidianRecovery':
'W Obsidian wybierz „Otwórz folder jako skarbiec" dla tej ścieżki, a następnie odśwież listę kontrolną.',
'vaultHealth.pipelineLabel': 'Potok pamięci działa poprawnie',
'vaultHealth.pipelineRecovery':
'Potok pamięci jest wstrzymany lub w stanie błędu. Włącz ponownie automatyczną synchronizację w statusie drzewa pamięci i spróbuj ponownie.',
'vaultHealth.timeNever': 'Nigdy',
'vaultHealth.timeJustNow': 'przed chwilą',
'vaultHealth.timeMinAgo': '{n} min temu',
'vaultHealth.timeHrAgo': '{n} godz. temu',
'vaultHealth.timeDayAgo': '{n} dzień temu',
'vaultHealth.timeDaysAgo': '{n} dni temu',
'memoryData.howItWorks': 'Jak działa przechowywanie pamięci',
'memoryData.workspaceVault': 'Skarbiec obszaru roboczego · zapis',
'memoryData.workspaceVaultDesc':
'OpenHuman zapisuje wygenerowane notatki pamięci do memory_tree/content.',
'memoryData.connectedSources': 'Połączone źródła · odczyt',
'memoryData.connectedSourcesDesc':
'Foldery, skrzynki pocztowe, czaty i repozytoria są importowane do indeksowania pamięci — ich oryginalne pliki nigdy nie są nadpisywane.',
'memoryData.internalFiles': 'Wewnętrzne pliki drzewa pamięci',
'memoryData.internalFilesDesc':
'Indeksy, stan kolejki i podsumowania są zarządzane przez OpenHuman, aby utrzymać poprawne działanie przywołania i synchronizacji.',
'memoryData.windowError': 'Okno pamięci',
'memoryData.windowUpdated': 'Okno pamięci zaktualizowane',
'memoryData.windowUpdatedMsg': 'Ustawiono na {window}.',
};
export default messages;
+64
View File
@@ -4592,6 +4592,70 @@ const messages: TranslationMap = {
'Atividade moderada — sincronização por hora, resumo diário.',
'onboarding.custom.activity.configureDesc':
'Escolha seu próprio nível de atividade. Configurar em Configurações Nível de atividade do agente.',
// Onboarding: Custom > Vault
'onboarding.custom.stepperVault': 'Vault',
'onboarding.custom.vault.title': 'Configuração de Memória e Vault',
'onboarding.custom.vault.subtitle':
'Confirme onde as notas de memória são gravadas, como os dados de origem são lidos e se o pipeline do vault está saudável.',
'onboarding.custom.vault.defaultDesc':
'Use os padrões de memória gerenciados pelo OpenHuman. O caminho do vault e a integridade da sincronização ainda podem ser revisados depois.',
'onboarding.custom.vault.configureDesc':
'Revise a propriedade do vault, execute verificações de integridade e ajuste os controles de memória agora.',
'onboarding.custom.vault.localDisabledReason':
'A configuração gerenciada requer login no OpenHuman e não está disponível no modo local.',
'onboarding.custom.vault.exitError':
'Não foi possível concluir o processo de integração. Por favor, tente novamente.',
// Vault Health
'vaultHealth.title': 'Checklist de integridade do vault',
'vaultHealth.setupTitle': 'Integridade da configuração do vault',
'vaultHealth.workspaceVault': 'Vault do espaço de trabalho:',
'vaultHealth.refresh': 'Atualizar',
'vaultHealth.refreshing': 'Atualizando…',
'vaultHealth.revealFolder': 'Revelar pasta',
'vaultHealth.openInObsidian': 'Abrir no Obsidian',
'vaultHealth.installObsidian': 'Instalar Obsidian',
'vaultHealth.openObsidianError': 'Não foi possível abrir o Obsidian',
'vaultHealth.revealError': 'Não foi possível revelar a pasta do vault',
'vaultHealth.downloadError': 'Não foi possível abrir a página de download do Obsidian',
'vaultHealth.loadError': 'Não foi possível carregar a integridade do vault:',
'vaultHealth.lastSync': 'Última sincronização:',
'vaultHealth.passed': 'Aprovado',
'vaultHealth.needsAttention': 'Requer atenção',
'vaultHealth.existsLabel': 'O caminho do vault do espaço de trabalho existe',
'vaultHealth.existsRecovery':
'A pasta do vault está ausente. Inicie uma sincronização ou crie esta pasta e atualize este checklist.',
'vaultHealth.writableLabel': 'O vault pode ser gravado pelo OpenHuman',
'vaultHealth.writableRecovery':
'O OpenHuman ainda não consegue gravar neste vault. Conceda permissões de escrita e atualize.',
'vaultHealth.obsidianLabel': 'O vault está registrado no Obsidian',
'vaultHealth.obsidianRecovery':
'No Obsidian, escolha "Abrir pasta como vault" para este caminho e atualize este checklist.',
'vaultHealth.pipelineLabel': 'O pipeline de memória está saudável',
'vaultHealth.pipelineRecovery':
'O pipeline de memória está pausado ou com erro. Reative a Sincronização automática no status da Árvore de Memória e tente novamente.',
'vaultHealth.timeNever': 'Nunca',
'vaultHealth.timeJustNow': 'agora mesmo',
'vaultHealth.timeMinAgo': 'há {n} min',
'vaultHealth.timeHrAgo': 'há {n} h',
'vaultHealth.timeDayAgo': 'há {n} dia',
'vaultHealth.timeDaysAgo': 'há {n} dias',
// Memory Data
'memoryData.howItWorks': 'Como o armazenamento de memória funciona',
'memoryData.workspaceVault': 'Vault do espaço de trabalho · gravação',
'memoryData.workspaceVaultDesc':
'O OpenHuman grava as notas de memória geradas em memory_tree/content.',
'memoryData.connectedSources': 'Fontes conectadas · leitura',
'memoryData.connectedSourcesDesc':
'Pastas, caixas de correio, chats e repositórios são importados para indexação de memória — seus arquivos originais nunca são reescritos.',
'memoryData.internalFiles': 'Arquivos internos da árvore de memória',
'memoryData.internalFilesDesc':
'Índices, estado da fila e resumos são gerenciados pelo OpenHuman para manter a recuperação e a sincronização saudáveis.',
'memoryData.windowError': 'Janela de memória',
'memoryData.windowUpdated': 'Janela de memória atualizada',
'memoryData.windowUpdatedMsg': 'Definida para {window}.',
};
export default messages;
+58
View File
@@ -4556,6 +4556,7 @@ const messages: TranslationMap = {
// Onboarding: Custom > Activity
'onboarding.custom.stepperActivity': 'Активность',
'onboarding.custom.stepperVault': 'Хранилище',
'onboarding.custom.activity.title': 'Активность агента',
'onboarding.custom.activity.subtitle':
'Насколько проактивно агент отслеживает события и действует в фоне.',
@@ -4563,6 +4564,63 @@ const messages: TranslationMap = {
'Умеренная активность — синхронизация каждый час, ежедневная сводка.',
'onboarding.custom.activity.configureDesc':
'Выберите свой уровень активности. Настройка в Параметры › Уровень активности агента.',
'onboarding.custom.vault.title': 'Настройка памяти и хранилища',
'onboarding.custom.vault.subtitle':
'Подтвердите, куда записываются заметки памяти, как считываются исходные данные и исправно ли работает конвейер хранилища.',
'onboarding.custom.vault.defaultDesc':
'Использовать настройки памяти по умолчанию, управляемые OpenHuman. Путь к хранилищу и состояние синхронизации можно проверить позже.',
'onboarding.custom.vault.configureDesc':
'Проверьте владельца хранилища, выполните проверки состояния и настройте параметры памяти прямо сейчас.',
'onboarding.custom.vault.localDisabledReason':
'Управляемая настройка требует входа в OpenHuman и недоступна в локальном режиме.',
'onboarding.custom.vault.exitError':
'Не удалось завершить настройку. Пожалуйста, попробуйте ещё раз.',
'vaultHealth.title': 'Контрольный список состояния хранилища',
'vaultHealth.setupTitle': 'Состояние настройки хранилища',
'vaultHealth.workspaceVault': 'Хранилище рабочего пространства:',
'vaultHealth.refresh': 'Обновить',
'vaultHealth.refreshing': 'Обновление…',
'vaultHealth.revealFolder': 'Показать папку',
'vaultHealth.openInObsidian': 'Открыть в Obsidian',
'vaultHealth.installObsidian': 'Установить Obsidian',
'vaultHealth.openObsidianError': 'Не удалось открыть Obsidian',
'vaultHealth.revealError': 'Не удалось показать папку хранилища',
'vaultHealth.downloadError': 'Не удалось открыть страницу загрузки Obsidian',
'vaultHealth.loadError': 'Не удалось загрузить состояние хранилища:',
'vaultHealth.lastSync': 'Последняя синхронизация:',
'vaultHealth.passed': 'Пройдено',
'vaultHealth.needsAttention': 'Требует внимания',
'vaultHealth.existsLabel': 'Путь к хранилищу рабочего пространства существует',
'vaultHealth.existsRecovery':
'Папка хранилища отсутствует. Запустите синхронизацию или создайте эту папку, затем обновите контрольный список.',
'vaultHealth.writableLabel': 'Хранилище доступно для записи OpenHuman',
'vaultHealth.writableRecovery':
'OpenHuman пока не может записывать в это хранилище. Предоставьте права на запись и обновите страницу.',
'vaultHealth.obsidianLabel': 'Хранилище зарегистрировано в Obsidian',
'vaultHealth.obsidianRecovery':
'В Obsidian выберите «Открыть папку как хранилище» для этого пути, затем обновите контрольный список.',
'vaultHealth.pipelineLabel': 'Конвейер памяти работает исправно',
'vaultHealth.pipelineRecovery':
'Конвейер памяти приостановлен или находится в состоянии ошибки. Повторно включите автосинхронизацию в статусе дерева памяти и повторите попытку.',
'vaultHealth.timeNever': 'Никогда',
'vaultHealth.timeJustNow': 'только что',
'vaultHealth.timeMinAgo': '{n} мин назад',
'vaultHealth.timeHrAgo': '{n} ч назад',
'vaultHealth.timeDayAgo': '{n} день назад',
'vaultHealth.timeDaysAgo': '{n} дн назад',
'memoryData.howItWorks': 'Как работает хранение памяти',
'memoryData.workspaceVault': 'Хранилище рабочего пространства · запись',
'memoryData.workspaceVaultDesc':
'OpenHuman записывает сгенерированные заметки памяти в memory_tree/content.',
'memoryData.connectedSources': 'Подключённые источники · чтение',
'memoryData.connectedSourcesDesc':
'Папки, почтовые ящики, чаты и репозитории импортируются для индексирования памяти — исходные файлы никогда не перезаписываются.',
'memoryData.internalFiles': 'Внутренние файлы дерева памяти',
'memoryData.internalFilesDesc':
'Индексы, состояние очереди и сводки управляются OpenHuman для поддержания исправного восстановления и синхронизации.',
'memoryData.windowError': 'Окно памяти',
'memoryData.windowUpdated': 'Окно памяти обновлено',
'memoryData.windowUpdatedMsg': 'Установлено значение {window}.',
};
export default messages;
+58
View File
@@ -4293,11 +4293,69 @@ const messages: TranslationMap = {
// Onboarding: Custom > Activity
'onboarding.custom.stepperActivity': '活动',
'onboarding.custom.stepperVault': '保险库',
'onboarding.custom.activity.title': '智能体活动',
'onboarding.custom.activity.subtitle': '您的智能体在后台监控和行动的主动程度。',
'onboarding.custom.activity.defaultDesc': '适中活动——每小时同步,每日摘要。',
'onboarding.custom.activity.configureDesc':
'选择您自己的活动级别。在设置 › 智能体活动级别中配置。',
// Onboarding: Custom > Vault
'onboarding.custom.vault.title': '记忆与保险库设置',
'onboarding.custom.vault.subtitle':
'确认记忆笔记的写入位置、源数据的读取方式,以及保险库管道是否正常运行。',
'onboarding.custom.vault.defaultDesc':
'使用 OpenHuman 托管的记忆默认设置。保险库路径和同步健康状态仍可稍后查看。',
'onboarding.custom.vault.configureDesc': '立即查看保险库所有权、运行健康检查并调整记忆控制。',
'onboarding.custom.vault.localDisabledReason': '托管设置需要登录 OpenHuman,在本地模式下不可用。',
'onboarding.custom.vault.exitError': '无法完成引导流程,请重试。',
// Vault Health
'vaultHealth.title': '保险库健康检查清单',
'vaultHealth.setupTitle': '保险库设置健康状态',
'vaultHealth.workspaceVault': '工作区保险库:',
'vaultHealth.refresh': '刷新',
'vaultHealth.refreshing': '刷新中…',
'vaultHealth.revealFolder': '显示文件夹',
'vaultHealth.openInObsidian': '在 Obsidian 中打开',
'vaultHealth.installObsidian': '安装 Obsidian',
'vaultHealth.openObsidianError': '无法打开 Obsidian',
'vaultHealth.revealError': '无法显示保险库文件夹',
'vaultHealth.downloadError': '无法打开 Obsidian 下载页面',
'vaultHealth.loadError': '无法加载保险库健康状态:',
'vaultHealth.lastSync': '上次同步:',
'vaultHealth.passed': '通过',
'vaultHealth.needsAttention': '需要关注',
'vaultHealth.existsLabel': '工作区保险库路径存在',
'vaultHealth.existsRecovery': '保险库文件夹缺失。请启动同步或创建该文件夹,然后刷新此检查清单。',
'vaultHealth.writableLabel': 'OpenHuman 可写入保险库',
'vaultHealth.writableRecovery': 'OpenHuman 暂时无法写入此保险库。请授予写入权限后刷新。',
'vaultHealth.obsidianLabel': '保险库已在 Obsidian 中注册',
'vaultHealth.obsidianRecovery':
'请在 Obsidian 中选择"将文件夹作为保险库打开",然后刷新此检查清单。',
'vaultHealth.pipelineLabel': '记忆管道运行正常',
'vaultHealth.pipelineRecovery':
'记忆管道已暂停或出现错误。请在记忆树状态中重新启用自动同步并重试。',
'vaultHealth.timeNever': '从未',
'vaultHealth.timeJustNow': '刚刚',
'vaultHealth.timeMinAgo': '{n} 分钟前',
'vaultHealth.timeHrAgo': '{n} 小时前',
'vaultHealth.timeDayAgo': '{n} 天前',
'vaultHealth.timeDaysAgo': '{n} 天前',
// Memory Data
'memoryData.howItWorks': '记忆存储原理',
'memoryData.workspaceVault': '工作区保险库 · 写入',
'memoryData.workspaceVaultDesc': 'OpenHuman 将生成的记忆笔记写入 memory_tree/content。',
'memoryData.connectedSources': '已连接来源 · 读取',
'memoryData.connectedSourcesDesc':
'文件夹、邮箱、聊天记录和代码仓库会导入用于记忆索引——其原始文件不会被改写。',
'memoryData.internalFiles': '内部记忆树文件',
'memoryData.internalFilesDesc':
'索引、队列状态和摘要由 OpenHuman 管理,以保持召回和同步的正常运行。',
'memoryData.windowError': '记忆时间窗口',
'memoryData.windowUpdated': '记忆时间窗口已更新',
'memoryData.windowUpdatedMsg': '已设置为 {window}。',
};
export default messages;
+3 -4
View File
@@ -4,13 +4,11 @@ import OnboardingLayout from './OnboardingLayout';
import CustomActivityPage from './pages/CustomActivityPage';
import CustomEmbeddingsPage from './pages/CustomEmbeddingsPage';
import CustomInferencePage from './pages/CustomInferencePage';
// Memory step is hidden from the flow for now (file kept on disk;
// uncomment alongside customWizardSteps.ts to re-enable):
// import CustomMemoryPage from './pages/CustomMemoryPage';
import CustomOAuthPage from './pages/CustomOAuthPage';
import CustomSearchPage from './pages/CustomSearchPage';
import CustomVoicePage from './pages/CustomVoicePage';
import RuntimeChoicePage from './pages/RuntimeChoicePage';
import VaultSetupStep from './pages/VaultSetupStep';
import WelcomePage from './pages/WelcomePage';
/**
@@ -18,7 +16,7 @@ import WelcomePage from './pages/WelcomePage';
*
* welcome → runtime-choice
* ├── cloud → /home
* └── custom → /custom/inference → voice → oauth → search → embeddings → memory → /home
* └── custom → /custom/inference → voice → oauth → search → embeddings → vault → /home
*
* Each custom step asks Default (let OpenHuman manage it) vs Configure
* (let me pick). Default is a one-click pick; Configure renders inline
@@ -41,6 +39,7 @@ const Onboarding = () => {
<Route path="custom/search" element={<CustomSearchPage />} />
<Route path="custom/embeddings" element={<CustomEmbeddingsPage />} />
<Route path="custom/activity" element={<CustomActivityPage />} />
<Route path="custom/vault" element={<VaultSetupStep />} />
{/* <Route path="custom/memory" element={<CustomMemoryPage />} /> */}
<Route path="*" element={<Navigate to="welcome" replace />} />
</Route>
@@ -9,7 +9,8 @@ export type CustomStepKey =
| 'search'
| 'embeddings'
| 'memory'
| 'activity';
| 'activity'
| 'vault';
export type CustomStepChoice = 'default' | 'configure';
export interface OnboardingDraft {
@@ -1,8 +1,7 @@
import type { CustomStepKey } from './OnboardingContext';
/** Ordered list of custom-wizard steps. Index drives the step counter UI and
* the back/continue navigation. `search` and `memory` are commented out for
* now — their pages still exist and route in case we want to re-enable. */
* the back/continue navigation. */
export const CUSTOM_WIZARD_STEPS: CustomStepKey[] = [
'inference',
'voice',
@@ -10,6 +9,7 @@ export const CUSTOM_WIZARD_STEPS: CustomStepKey[] = [
'search',
'embeddings',
'activity',
'vault',
// 'memory',
];
@@ -21,6 +21,7 @@ export const CUSTOM_WIZARD_ROUTES: Record<CustomStepKey, string> = {
embeddings: '/onboarding/custom/embeddings',
activity: '/onboarding/custom/activity',
memory: '/onboarding/custom/memory',
vault: '/onboarding/custom/vault',
};
/** Deep-link target inside Settings for users who pick "Configure" and want
@@ -33,4 +34,5 @@ export const CUSTOM_WIZARD_SETTINGS_ROUTES: Record<CustomStepKey, string> = {
embeddings: '/settings/embeddings',
activity: '/settings/activity-level',
memory: '/settings/memory-data',
vault: '/settings/memory-data',
};
@@ -0,0 +1,79 @@
import { configureStore } from '@reduxjs/toolkit';
import { render, screen } from '@testing-library/react';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { I18nProvider } from '../../../lib/i18n/I18nContext';
import type { Locale } from '../../../lib/i18n/types';
import localeReducer from '../../../store/localeSlice';
import VaultSetupStep from './VaultSetupStep';
const navigateMock = vi.fn();
const setDraftMock = vi.fn();
const completeAndExitMock = vi.fn();
let sessionToken = 'header.payload.local';
vi.mock('react-router-dom', async importOriginal => {
const actual = await importOriginal<typeof import('react-router-dom')>();
return { ...actual, useNavigate: () => navigateMock };
});
vi.mock('../../../components/settings/panels/MemoryDataPanel', () => ({
default: () => <div data-testid="memory-data-panel">Memory Data Panel</div>,
}));
vi.mock('../../../providers/CoreStateProvider', () => ({
useCoreState: () => ({ snapshot: { sessionToken } }),
}));
vi.mock('../OnboardingContext', () => ({
useOnboardingContext: () => ({
draft: { connectedSources: [], customChoices: {} },
setDraft: setDraftMock,
completeAndExit: completeAndExitMock,
}),
}));
function renderPage() {
const store = configureStore({
reducer: { locale: localeReducer },
preloadedState: { locale: { current: 'en' as Locale } },
});
return render(
<Provider store={store}>
<MemoryRouter>
<I18nProvider>
<VaultSetupStep />
</I18nProvider>
</MemoryRouter>
</Provider>
);
}
describe('VaultSetupStep', () => {
beforeEach(() => {
navigateMock.mockReset();
setDraftMock.mockReset();
completeAndExitMock.mockReset();
sessionToken = 'header.payload.local';
});
it('forces configure mode and hides chooser cards for local sessions', () => {
renderPage();
expect(screen.getByTestId('memory-data-panel')).toBeInTheDocument();
expect(screen.queryByTestId('onboarding-custom-vault-step-default')).not.toBeInTheDocument();
expect(screen.queryByTestId('onboarding-custom-vault-step-configure')).not.toBeInTheDocument();
});
it('shows default/configure chooser cards for managed sessions', () => {
sessionToken = 'header.payload.remote';
renderPage();
expect(screen.getByTestId('onboarding-custom-vault-step-default')).toBeInTheDocument();
expect(screen.getByTestId('onboarding-custom-vault-step-configure')).toBeInTheDocument();
});
});
@@ -0,0 +1,93 @@
import { useCallback, useMemo, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import MemoryDataPanel from '../../../components/settings/panels/MemoryDataPanel';
import { useT } from '../../../lib/i18n/I18nContext';
import { useCoreState } from '../../../providers/CoreStateProvider';
import { trackEvent } from '../../../services/analytics';
import { isLocalSessionToken } from '../../../utils/localSession';
import { CUSTOM_WIZARD_ROUTES, CUSTOM_WIZARD_STEPS } from '../customWizardSteps';
import { type CustomStepChoice, useOnboardingContext } from '../OnboardingContext';
import CustomWizardStep from '../steps/CustomWizardStep';
const STEP_KEY = 'vault' as const;
export default function VaultSetupStep() {
const { t } = useT();
const navigate = useNavigate();
const { snapshot } = useCoreState();
const { draft, setDraft, completeAndExit } = useOnboardingContext();
const stepIndex = CUSTOM_WIZARD_STEPS.indexOf(STEP_KEY);
const isLocalSession = isLocalSessionToken(snapshot.sessionToken);
const appliedLocalRef = useRef(false);
const initialChoice = isLocalSession ? 'configure' : (draft.customChoices?.[STEP_KEY] ?? null);
const [choice, setChoice] = useState<CustomStepChoice | null>(initialChoice);
const [exitError, setExitError] = useState<string | null>(null);
if (isLocalSession && !appliedLocalRef.current) {
appliedLocalRef.current = true;
if (choice !== 'configure') {
setChoice('configure');
}
setDraft(prev => ({
...prev,
customChoices: { ...prev.customChoices, [STEP_KEY]: 'configure' },
}));
}
const persistChoice = useCallback(
(next: CustomStepChoice) => {
setChoice(next);
setDraft(prev => ({ ...prev, customChoices: { ...prev.customChoices, [STEP_KEY]: next } }));
},
[setDraft]
);
const configureContent = useMemo(() => <MemoryDataPanel embedded />, []);
return (
<>
<CustomWizardStep
testId="onboarding-custom-vault-step"
stepIndex={stepIndex}
stepCount={CUSTOM_WIZARD_STEPS.length}
title={t('onboarding.custom.vault.title')}
subtitle={t('onboarding.custom.vault.subtitle')}
defaultDescription={t('onboarding.custom.vault.defaultDesc')}
configureDescription={t('onboarding.custom.vault.configureDesc')}
configureContent={configureContent}
defaultDisabled={isLocalSession}
defaultDisabledReason={
isLocalSession ? t('onboarding.custom.vault.localDisabledReason') : undefined
}
hideChoiceCards={isLocalSession}
choice={choice}
onChoiceChange={persistChoice}
onBack={() => navigate(CUSTOM_WIZARD_ROUTES[CUSTOM_WIZARD_STEPS[stepIndex - 1]])}
onContinue={async () => {
setExitError(null);
trackEvent('onboarding_step_complete', {
step_name: 'custom_vault',
choice: choice ?? 'default',
});
try {
await completeAndExit();
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
console.error('[onboarding:custom-vault] completeAndExit failed', err);
setExitError(message);
}
}}
continueLabel={t('onboarding.custom.finish')}
/>
{exitError ? (
<div
className="mt-3 rounded-xl border border-coral-200 dark:border-coral-500/30 bg-coral-50 dark:bg-coral-500/10 px-4 py-3 text-sm text-coral-700 dark:text-coral-300"
data-testid="onboarding-vault-exit-error">
{t('onboarding.custom.vault.exitError')}
</div>
) : null}
</>
);
}
@@ -115,6 +115,7 @@ const CustomWizardStep = ({
t('onboarding.custom.stepperSearch'),
t('onboarding.custom.stepperEmbeddings'),
t('onboarding.custom.stepperActivity'),
t('onboarding.custom.stepperVault'),
t('onboarding.custom.stepperMemory'),
].slice(0, stepCount);
+47
View File
@@ -671,6 +671,53 @@ export async function memoryTreeObsidianVaultStatus(
return out;
}
/** Response shape for `memory_tree_vault_health_check`. */
export interface VaultHealthCheck {
/** Absolute filesystem path to `<workspace>/memory_tree/content/`. */
content_root_abs: string;
/** True when the vault directory exists on disk. */
exists: boolean;
/** True when the vault directory is readable. */
readable: boolean;
/** True when a temp-file create+delete probe succeeds in the vault. */
writable: boolean;
/** True when Obsidian has this folder (or an ancestor) registered as a vault. */
obsidian_registered: boolean;
/** True when pipeline status is not paused and not in error. */
pipeline_healthy: boolean;
/** Epoch ms of newest chunk timestamp; zero when no chunks exist yet. */
last_sync_ms: number;
}
/**
* Consolidated onboarding/settings health snapshot for the workspace memory
* vault (`<workspace>/memory_tree/content/`).
*
* Backed by `openhuman.memory_tree_vault_health_check`.
*/
export async function memoryTreeVaultHealthCheck(
obsidianConfigDir?: string
): Promise<VaultHealthCheck> {
console.debug(
'[memory-tree-rpc] memoryTreeVaultHealthCheck: entry override=%s',
obsidianConfigDir ? 'set' : 'none'
);
const resp = await callCoreRpc<VaultHealthCheck | ResultEnvelope<VaultHealthCheck>>({
method: 'openhuman.memory_tree_vault_health_check',
params: obsidianConfigDir ? { obsidian_config_dir: obsidianConfigDir } : {},
});
const out = unwrapResult(resp);
console.debug(
'[memory-tree-rpc] memoryTreeVaultHealthCheck: exit exists=%s readable=%s writable=%s obsidian_registered=%s pipeline_healthy=%s',
out.exists,
out.readable,
out.writable,
out.obsidian_registered,
out.pipeline_healthy
);
return out;
}
/**
* #1574 §4b: per-model embedding re-embed backfill status. The AI settings
* panel polls this after an embedder change to warn that semantic recall
+120
View File
@@ -28,6 +28,7 @@
use anyhow::{Context, Result};
use rusqlite::params;
use serde::{Deserialize, Serialize};
use std::io::Write;
use crate::openhuman::config::Config;
use crate::openhuman::memory_store::chunks::store::{self as chunk_store, with_connection};
@@ -1071,6 +1072,125 @@ pub async fn obsidian_vault_status_rpc(
Ok(RpcOutcome::single_log(resp, log))
}
/// Response shape for [`vault_health_check_rpc`].
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct VaultHealthCheckResponse {
/// Absolute path to `<workspace>/memory_tree/content/`.
pub content_root_abs: String,
/// `true` when the content-root directory exists on disk.
pub exists: bool,
/// `true` when the content-root directory is readable.
pub readable: bool,
/// `true` when a temp file can be created + removed under content-root.
pub writable: bool,
/// `true` when the content root (or an ancestor) is a registered Obsidian
/// vault in Obsidian's `obsidian.json`.
pub obsidian_registered: bool,
/// `true` when the Memory Tree pipeline is neither paused nor in an error
/// state.
pub pipeline_healthy: bool,
/// Epoch ms of the most-recent chunk timestamp. Zero when empty.
pub last_sync_ms: i64,
}
/// `memory_tree_vault_health_check` — consolidated onboarding/settings health
/// snapshot for the workspace vault.
///
/// Combines:
/// - filesystem reachability checks over `<workspace>/memory_tree/content/`
/// - Obsidian registration check (same logic as `obsidian_vault_status_rpc`)
/// - pipeline health signals from `memory_tree_pipeline_status`
///
/// `obsidian_config_dir` is optional and mirrors
/// [`obsidian_vault_status_rpc`]: it overrides where we probe for
/// `obsidian.json` for non-standard installs.
pub async fn vault_health_check_rpc(
config: &Config,
obsidian_config_dir: Option<String>,
) -> Result<RpcOutcome<VaultHealthCheckResponse>, String> {
let cfg = config.clone();
let fs_probe = tokio::task::spawn_blocking(move || {
let content_root = cfg.memory_tree_content_root();
let content_root_abs = content_root.to_string_lossy().to_string();
let exists = content_root.is_dir();
let readable = exists && std::fs::read_dir(&content_root).is_ok();
let writable = exists && probe_directory_writable(&content_root);
let extra = obsidian_config_dir
.as_deref()
.map(str::trim)
.filter(|s| !s.is_empty())
.map(std::path::Path::new);
let obsidian_registered =
obsidian_registry::vault_registration_status(&content_root, extra).registered;
(
content_root_abs,
exists,
readable,
writable,
obsidian_registered,
)
})
.await
.map_err(|e| format!("vault_health_check fs probe join error: {e}"))?;
let pipeline = crate::openhuman::memory_tree::tree::rpc::pipeline_status_rpc(config)
.await
.map_err(|e| format!("vault_health_check pipeline_status: {e}"))?;
let (content_root_abs, exists, readable, writable, obsidian_registered) = fs_probe;
let pipeline_healthy = pipeline.value.status != "error" && !pipeline.value.is_paused;
let last_sync_ms = pipeline.value.last_sync_ms.max(0);
let resp = VaultHealthCheckResponse {
content_root_abs,
exists,
readable,
writable,
obsidian_registered,
pipeline_healthy,
last_sync_ms,
};
let log = format!(
"memory_tree::read: vault_health_check exists={} readable={} writable={} obsidian_registered={} pipeline_healthy={} last_sync_ms={} root_hash={}",
resp.exists,
resp.readable,
resp.writable,
resp.obsidian_registered,
resp.pipeline_healthy,
resp.last_sync_ms,
crate::openhuman::memory::util::redact::redact(&resp.content_root_abs),
);
Ok(RpcOutcome::single_log(resp, log))
}
fn probe_directory_writable(dir: &std::path::Path) -> bool {
let ts = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let probe = dir.join(format!(
".openhuman-vault-writecheck-{}-{ts}.tmp",
std::process::id()
));
match std::fs::OpenOptions::new()
.create_new(true)
.write(true)
.open(&probe)
{
Ok(mut file) => {
let write_ok = file.write_all(b"ok").is_ok();
if let Err(e) = std::fs::remove_file(&probe) {
log::debug!("[memory] vault write-probe cleanup failed: {e}");
}
write_ok
}
Err(_) => false,
}
}
/// Tree mode: summary nodes joined to their owning tree for the
/// human-readable scope, plus the leaf chunks that hang off them. Edges
/// are encoded implicitly via `GraphNode.parent_id` (a chunk's
+48
View File
@@ -899,3 +899,51 @@ async fn obsidian_status_blank_override_is_treated_as_none() {
.unwrap();
assert!(!outcome.value.registered);
}
#[tokio::test]
async fn vault_health_check_reports_missing_content_root_for_fresh_workspace() {
let (_tmp, cfg) = test_config();
let outcome = vault_health_check_rpc(&cfg, None).await.unwrap();
assert!(!outcome.value.exists);
assert!(!outcome.value.readable);
assert!(!outcome.value.writable);
assert!(!outcome.value.obsidian_registered);
assert!(outcome.value.pipeline_healthy);
assert_eq!(outcome.value.last_sync_ms, 0);
}
#[tokio::test]
async fn vault_health_check_reports_writable_and_obsidian_registered_when_ready() {
let (_tmp, cfg) = test_config();
seed_chat_chunk(
&cfg,
"slack:#eng",
"Vault health seed chunk so content_root exists and last_sync_ms > 0",
)
.await;
let content_root = cfg.memory_tree_content_root();
let cfg_dir = TempDir::new().unwrap();
let body = format!(
"{{ \"vaults\": {{ \"id0\": {{ \"path\": {}, \"open\": true }} }} }}",
serde_json::to_string(&content_root.to_string_lossy().to_string()).unwrap()
);
std::fs::write(cfg_dir.path().join("obsidian.json"), body).unwrap();
let outcome = vault_health_check_rpc(&cfg, Some(cfg_dir.path().to_string_lossy().to_string()))
.await
.unwrap();
assert!(outcome.value.exists);
assert!(outcome.value.readable);
assert!(outcome.value.writable);
assert!(outcome.value.obsidian_registered);
assert!(outcome.value.pipeline_healthy);
assert!(outcome.value.last_sync_ms > 0);
assert!(
!outcome.logs[0].contains(content_root.to_str().unwrap()),
"log leaked content root: {}",
outcome.logs[0]
);
}
+78
View File
@@ -40,6 +40,7 @@ pub fn all_controller_schemas() -> Vec<ControllerSchema> {
schemas("delete_chunk"),
schemas("graph_export"),
schemas("obsidian_vault_status"),
schemas("vault_health_check"),
schemas("flush_now"),
schemas("flush_source"),
schemas("wipe_all"),
@@ -110,6 +111,10 @@ pub fn all_registered_controllers() -> Vec<RegisteredController> {
schema: schemas("obsidian_vault_status"),
handler: handle_obsidian_vault_status,
},
RegisteredController {
schema: schemas("vault_health_check"),
handler: handle_vault_health_check,
},
RegisteredController {
schema: schemas("flush_now"),
handler: handle_flush_now,
@@ -693,6 +698,66 @@ pub fn schemas(function: &str) -> ControllerSchema {
},
],
},
"vault_health_check" => ControllerSchema {
namespace: NAMESPACE,
function: "vault_health_check",
description: "Consolidated workspace-vault health snapshot for onboarding and \
settings. Checks whether <workspace>/memory_tree/content exists, is \
readable, and is writable (via temp-file probe), whether Obsidian has \
the vault registered, and whether the Memory Tree pipeline is healthy.",
inputs: vec![FieldSchema {
name: "obsidian_config_dir",
ty: TypeSchema::Option(Box::new(TypeSchema::String)),
comment: "Optional override for Obsidian's config directory (where \
obsidian.json lives). Omitted ⇒ standard per-OS probe.",
required: false,
}],
outputs: vec![
FieldSchema {
name: "content_root_abs",
ty: TypeSchema::String,
comment: "Absolute path to <workspace>/memory_tree/content/.",
required: true,
},
FieldSchema {
name: "exists",
ty: TypeSchema::Bool,
comment: "True when the workspace vault directory exists on disk.",
required: true,
},
FieldSchema {
name: "readable",
ty: TypeSchema::Bool,
comment: "True when the workspace vault directory can be read.",
required: true,
},
FieldSchema {
name: "writable",
ty: TypeSchema::Bool,
comment: "True when the vault accepts a create+delete temp-file probe.",
required: true,
},
FieldSchema {
name: "obsidian_registered",
ty: TypeSchema::Bool,
comment: "True when Obsidian has this folder (or an ancestor) registered \
as a vault.",
required: true,
},
FieldSchema {
name: "pipeline_healthy",
ty: TypeSchema::Bool,
comment: "True when Memory Tree pipeline is not paused and not in error.",
required: true,
},
FieldSchema {
name: "last_sync_ms",
ty: TypeSchema::I64,
comment: "Epoch ms of the newest chunk timestamp; 0 when empty.",
required: true,
},
],
},
"pipeline_status" => ControllerSchema {
namespace: NAMESPACE,
function: "pipeline_status",
@@ -1077,6 +1142,19 @@ fn handle_obsidian_vault_status(params: Map<String, Value>) -> ControllerFuture
})
}
fn handle_vault_health_check(params: Map<String, Value>) -> ControllerFuture {
Box::pin(async move {
#[derive(serde::Deserialize, Default)]
struct Req {
#[serde(default)]
obsidian_config_dir: Option<String>,
}
let config = config_rpc::load_config_with_timeout().await?;
let req = parse_value::<Req>(Value::Object(params)).unwrap_or_default();
to_json(read_rpc::vault_health_check_rpc(&config, req.obsidian_config_dir).await?)
})
}
fn handle_flush_source(params: Map<String, Value>) -> ControllerFuture {
Box::pin(async move {
#[derive(serde::Deserialize)]