diff --git a/app/src/components/intelligence/VaultHealthChecklist.test.tsx b/app/src/components/intelligence/VaultHealthChecklist.test.tsx new file mode 100644 index 000000000..a3e7bd20c --- /dev/null +++ b/app/src/components/intelligence/VaultHealthChecklist.test.tsx @@ -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 { + 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('', () => { + it('shows all checks as passed when the health RPC is fully healthy', async () => { + memoryTreeVaultHealthCheck.mockResolvedValueOnce(health()); + renderWithProviders(); + + 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(); + + 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(); + + 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(); + + 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'); + }); + }); +}); diff --git a/app/src/components/intelligence/VaultHealthChecklist.tsx b/app/src/components/intelligence/VaultHealthChecklist.tsx new file mode 100644 index 000000000..c89bed386 --- /dev/null +++ b/app/src/components/intelligence/VaultHealthChecklist.tsx @@ -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) => 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(null); + const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); + const [error, setError] = useState(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 ( +
+
+
+

+ {resolvedTitle} +

+

+ {t('vaultHealth.workspaceVault')} memory_tree/content +

+
+ +
+ + {health?.content_root_abs ? ( + + {health.content_root_abs} + + ) : null} + +
+ + + +
+ + {loading ? ( +
+ ) : error ? ( +
+ {t('vaultHealth.loadError')} {error} +
+ ) : ( +
+ {checklist.map(item => ( +
+
+ {item.ok ? t('vaultHealth.passed') : t('vaultHealth.needsAttention')} · {item.label} +
+ {!item.ok ?

{item.recovery}

: null} +
+ ))} +

+ {t('vaultHealth.lastSync')} {formatRelativeTime(health?.last_sync_ms ?? 0, t)} +

+
+ )} +
+ ); +} + +export default VaultHealthChecklist; diff --git a/app/src/components/settings/components/MemoryWindowControl.tsx b/app/src/components/settings/components/MemoryWindowControl.tsx index 803e44242..b9d2729be 100644 --- a/app/src/components/settings/components/MemoryWindowControl.tsx +++ b/app/src/components/settings/components/MemoryWindowControl.tsx @@ -154,7 +154,9 @@ const MemoryWindowControl = ({ onError, onSaved }: Props) => { data-testid="memory-window-control">
-

{t('settings.memoryWindow.title')}

+

+ {t('settings.memoryWindow.title')} +

{t('settings.memoryWindow.description')}

diff --git a/app/src/components/settings/panels/MemoryDataPanel.tsx b/app/src/components/settings/panels/MemoryDataPanel.tsx index 18c7ba56e..ebff61471 100644 --- a/app/src/components/settings/panels/MemoryDataPanel.tsx +++ b/app/src/components/settings/panels/MemoryDataPanel.tsx @@ -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 = {}) => { /> )}
+
+

+ {t('memoryData.howItWorks')} +

+
+
+
+ {t('memoryData.workspaceVault')} +
+
+ {t('memoryData.workspaceVaultDesc')} +
+
+
+
+ {t('memoryData.connectedSources')} +
+
+ {t('memoryData.connectedSourcesDesc')} +
+
+
+
+ {t('memoryData.internalFiles')} +
+
+ {t('memoryData.internalFilesDesc')} +
+
+
+
+
diff --git a/app/src/lib/i18n/ar.ts b/app/src/lib/i18n/ar.ts index 19281461d..6604e6518 100644 --- a/app/src/lib/i18n/ar.ts +++ b/app/src/lib/i18n/ar.ts @@ -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; diff --git a/app/src/lib/i18n/bn.ts b/app/src/lib/i18n/bn.ts index 3ff1ba596..82a8e7927 100644 --- a/app/src/lib/i18n/bn.ts +++ b/app/src/lib/i18n/bn.ts @@ -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; diff --git a/app/src/lib/i18n/de.ts b/app/src/lib/i18n/de.ts index 74de5a304..a18cace5c 100644 --- a/app/src/lib/i18n/de.ts +++ b/app/src/lib/i18n/de.ts @@ -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; diff --git a/app/src/lib/i18n/en.ts b/app/src/lib/i18n/en.ts index 79418ced1..97ffe62c5 100644 --- a/app/src/lib/i18n/en.ts +++ b/app/src/lib/i18n/en.ts @@ -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', diff --git a/app/src/lib/i18n/es.ts b/app/src/lib/i18n/es.ts index b96c224da..f569fe936 100644 --- a/app/src/lib/i18n/es.ts +++ b/app/src/lib/i18n/es.ts @@ -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; diff --git a/app/src/lib/i18n/fr.ts b/app/src/lib/i18n/fr.ts index 8f10ebc73..0911764de 100644 --- a/app/src/lib/i18n/fr.ts +++ b/app/src/lib/i18n/fr.ts @@ -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; diff --git a/app/src/lib/i18n/hi.ts b/app/src/lib/i18n/hi.ts index 58994554d..58763d630 100644 --- a/app/src/lib/i18n/hi.ts +++ b/app/src/lib/i18n/hi.ts @@ -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; diff --git a/app/src/lib/i18n/id.ts b/app/src/lib/i18n/id.ts index f7e108344..8601cde50 100644 --- a/app/src/lib/i18n/id.ts +++ b/app/src/lib/i18n/id.ts @@ -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; diff --git a/app/src/lib/i18n/it.ts b/app/src/lib/i18n/it.ts index e1ee38a20..f38ce00f0 100644 --- a/app/src/lib/i18n/it.ts +++ b/app/src/lib/i18n/it.ts @@ -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; diff --git a/app/src/lib/i18n/ko.ts b/app/src/lib/i18n/ko.ts index 51e9b1c26..8e21c34fe 100644 --- a/app/src/lib/i18n/ko.ts +++ b/app/src/lib/i18n/ko.ts @@ -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; diff --git a/app/src/lib/i18n/pl.ts b/app/src/lib/i18n/pl.ts index aabaa78b6..17498c4d9 100644 --- a/app/src/lib/i18n/pl.ts +++ b/app/src/lib/i18n/pl.ts @@ -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; diff --git a/app/src/lib/i18n/pt.ts b/app/src/lib/i18n/pt.ts index 80e64ddc2..e4714637b 100644 --- a/app/src/lib/i18n/pt.ts +++ b/app/src/lib/i18n/pt.ts @@ -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; diff --git a/app/src/lib/i18n/ru.ts b/app/src/lib/i18n/ru.ts index fa5eae82a..cede52528 100644 --- a/app/src/lib/i18n/ru.ts +++ b/app/src/lib/i18n/ru.ts @@ -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; diff --git a/app/src/lib/i18n/zh-CN.ts b/app/src/lib/i18n/zh-CN.ts index a87da4f6b..1ab5107ed 100644 --- a/app/src/lib/i18n/zh-CN.ts +++ b/app/src/lib/i18n/zh-CN.ts @@ -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; diff --git a/app/src/pages/onboarding/Onboarding.tsx b/app/src/pages/onboarding/Onboarding.tsx index 4de345ad9..2c4a160a8 100644 --- a/app/src/pages/onboarding/Onboarding.tsx +++ b/app/src/pages/onboarding/Onboarding.tsx @@ -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 = () => { } /> } /> } /> + } /> {/* } /> */} } /> diff --git a/app/src/pages/onboarding/OnboardingContext.tsx b/app/src/pages/onboarding/OnboardingContext.tsx index 6f4eb7453..db7055106 100644 --- a/app/src/pages/onboarding/OnboardingContext.tsx +++ b/app/src/pages/onboarding/OnboardingContext.tsx @@ -9,7 +9,8 @@ export type CustomStepKey = | 'search' | 'embeddings' | 'memory' - | 'activity'; + | 'activity' + | 'vault'; export type CustomStepChoice = 'default' | 'configure'; export interface OnboardingDraft { diff --git a/app/src/pages/onboarding/customWizardSteps.ts b/app/src/pages/onboarding/customWizardSteps.ts index d32b6a8cc..5e3c71545 100644 --- a/app/src/pages/onboarding/customWizardSteps.ts +++ b/app/src/pages/onboarding/customWizardSteps.ts @@ -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 = { 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 = { embeddings: '/settings/embeddings', activity: '/settings/activity-level', memory: '/settings/memory-data', + vault: '/settings/memory-data', }; diff --git a/app/src/pages/onboarding/pages/VaultSetupStep.test.tsx b/app/src/pages/onboarding/pages/VaultSetupStep.test.tsx new file mode 100644 index 000000000..a375f8158 --- /dev/null +++ b/app/src/pages/onboarding/pages/VaultSetupStep.test.tsx @@ -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(); + return { ...actual, useNavigate: () => navigateMock }; +}); + +vi.mock('../../../components/settings/panels/MemoryDataPanel', () => ({ + default: () =>
Memory Data Panel
, +})); + +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( + + + + + + + + ); +} + +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(); + }); +}); diff --git a/app/src/pages/onboarding/pages/VaultSetupStep.tsx b/app/src/pages/onboarding/pages/VaultSetupStep.tsx new file mode 100644 index 000000000..c23fd1bcf --- /dev/null +++ b/app/src/pages/onboarding/pages/VaultSetupStep.tsx @@ -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(initialChoice); + const [exitError, setExitError] = useState(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(() => , []); + + return ( + <> + 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 ? ( +
+ {t('onboarding.custom.vault.exitError')} +
+ ) : null} + + ); +} diff --git a/app/src/pages/onboarding/steps/CustomWizardStep.tsx b/app/src/pages/onboarding/steps/CustomWizardStep.tsx index b08682eae..82946edae 100644 --- a/app/src/pages/onboarding/steps/CustomWizardStep.tsx +++ b/app/src/pages/onboarding/steps/CustomWizardStep.tsx @@ -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); diff --git a/app/src/utils/tauriCommands/memoryTree.ts b/app/src/utils/tauriCommands/memoryTree.ts index 509437cbd..a0e04de49 100644 --- a/app/src/utils/tauriCommands/memoryTree.ts +++ b/app/src/utils/tauriCommands/memoryTree.ts @@ -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 `/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 (`/memory_tree/content/`). + * + * Backed by `openhuman.memory_tree_vault_health_check`. + */ +export async function memoryTreeVaultHealthCheck( + obsidianConfigDir?: string +): Promise { + console.debug( + '[memory-tree-rpc] memoryTreeVaultHealthCheck: entry override=%s', + obsidianConfigDir ? 'set' : 'none' + ); + const resp = await callCoreRpc>({ + 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 diff --git a/src/openhuman/memory/read_rpc.rs b/src/openhuman/memory/read_rpc.rs index 26d46ca00..f864defd8 100644 --- a/src/openhuman/memory/read_rpc.rs +++ b/src/openhuman/memory/read_rpc.rs @@ -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 `/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 `/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, +) -> Result, 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 diff --git a/src/openhuman/memory/read_rpc_tests.rs b/src/openhuman/memory/read_rpc_tests.rs index c258e0772..88a3fe4c8 100644 --- a/src/openhuman/memory/read_rpc_tests.rs +++ b/src/openhuman/memory/read_rpc_tests.rs @@ -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] + ); +} diff --git a/src/openhuman/memory/schema.rs b/src/openhuman/memory/schema.rs index 970654811..b3d7cfc57 100644 --- a/src/openhuman/memory/schema.rs +++ b/src/openhuman/memory/schema.rs @@ -40,6 +40,7 @@ pub fn all_controller_schemas() -> Vec { 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 { 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 /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 /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) -> ControllerFuture }) } +fn handle_vault_health_check(params: Map) -> ControllerFuture { + Box::pin(async move { + #[derive(serde::Deserialize, Default)] + struct Req { + #[serde(default)] + obsidian_config_dir: Option, + } + let config = config_rpc::load_config_with_timeout().await?; + let req = parse_value::(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) -> ControllerFuture { Box::pin(async move { #[derive(serde::Deserialize)]