mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
feat(migration): OpenClaw import surface + unblock unified-core Apply (#2087)
This commit is contained in:
@@ -0,0 +1,309 @@
|
||||
import debug from 'debug';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { useT } from '../../../lib/i18n/I18nContext';
|
||||
import { type MigrationReport, openhumanMigrateOpenclaw } from '../../../utils/tauriCommands/core';
|
||||
import SettingsHeader from '../components/SettingsHeader';
|
||||
import { useSettingsNavigation } from '../hooks/useSettingsNavigation';
|
||||
|
||||
const log = debug('migration-panel');
|
||||
|
||||
type Vendor = 'openclaw' | 'hermes';
|
||||
|
||||
const HERMES_TRACKING_URL = 'https://github.com/tinyhumansai/openhuman/issues/1440';
|
||||
|
||||
interface MigrationPanelProps {
|
||||
/** When true, render without the SettingsHeader chrome (used when embedded
|
||||
* inside the onboarding custom wizard). Mirrors the embed contract used
|
||||
* by VoicePanel / MemoryDataPanel. */
|
||||
embedded?: boolean;
|
||||
}
|
||||
|
||||
const MigrationPanel = ({ embedded = false }: MigrationPanelProps = {}) => {
|
||||
const { t } = useT();
|
||||
const { navigateBack, breadcrumbs } = useSettingsNavigation();
|
||||
|
||||
const [vendor, setVendor] = useState<Vendor>('openclaw');
|
||||
const [sourcePath, setSourcePath] = useState<string>('');
|
||||
const [previewReport, setPreviewReport] = useState<MigrationReport | null>(null);
|
||||
// Snapshot of `{ vendor, source }` that produced `previewReport`. Apply
|
||||
// must match these exactly — otherwise the user could preview path A,
|
||||
// edit the field to path B, and apply against B without ever seeing
|
||||
// the diff. CodeRabbit flagged this on PR #2087.
|
||||
const [previewInput, setPreviewInput] = useState<{
|
||||
vendor: Vendor;
|
||||
source: string | undefined;
|
||||
} | null>(null);
|
||||
const [appliedReport, setAppliedReport] = useState<MigrationReport | null>(null);
|
||||
const [isPreviewing, setIsPreviewing] = useState(false);
|
||||
const [isApplying, setIsApplying] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const normalizedSource = sourcePath.trim() || undefined;
|
||||
|
||||
// Apply is only enabled after a successful Preview *of the same input*.
|
||||
// Without that gate the user can mutate their workspace without ever
|
||||
// seeing what would change for the currently-typed path — exactly the
|
||||
// surprise issue #1440 calls out about the existing RPC's `dry_run=true`
|
||||
// default, and the regression CodeRabbit flagged on PR #2087.
|
||||
const canApply =
|
||||
previewReport != null &&
|
||||
previewInput != null &&
|
||||
previewInput.vendor === vendor &&
|
||||
previewInput.source === normalizedSource &&
|
||||
!isApplying &&
|
||||
!isPreviewing;
|
||||
|
||||
const runPreview = useCallback(async () => {
|
||||
setError(null);
|
||||
setIsPreviewing(true);
|
||||
setAppliedReport(null);
|
||||
try {
|
||||
log('[migration] preview start vendor=%s source=%s', vendor, normalizedSource ?? '<default>');
|
||||
const response = await openhumanMigrateOpenclaw(normalizedSource, true);
|
||||
// `openhumanMigrateOpenclaw` returns `CommandResponse<MigrationReport>`
|
||||
// — `.result` is the actual report.
|
||||
setPreviewReport(response.result);
|
||||
setPreviewInput({ vendor, source: normalizedSource });
|
||||
log(
|
||||
'[migration] preview ok stats=%o warnings=%d',
|
||||
response.result.stats,
|
||||
response.result.warnings.length
|
||||
);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
log('[migration] preview failed: %s', message);
|
||||
setError(message);
|
||||
setPreviewReport(null);
|
||||
setPreviewInput(null);
|
||||
} finally {
|
||||
setIsPreviewing(false);
|
||||
}
|
||||
}, [vendor, normalizedSource]);
|
||||
|
||||
const runApply = useCallback(async () => {
|
||||
if (!canApply || previewReport == null) return;
|
||||
const summary = previewReport.stats;
|
||||
const totalPlanned = summary.from_sqlite + summary.from_markdown - summary.skipped_unchanged;
|
||||
const template = t(
|
||||
totalPlanned === 1 ? 'migration.confirmImport.singular' : 'migration.confirmImport.plural'
|
||||
);
|
||||
const ok = window.confirm(
|
||||
template
|
||||
.replace('{count}', String(totalPlanned))
|
||||
.replace('{source}', previewReport.source_workspace)
|
||||
.replace('{target}', previewReport.target_workspace)
|
||||
);
|
||||
if (!ok) return;
|
||||
|
||||
setError(null);
|
||||
setIsApplying(true);
|
||||
try {
|
||||
log('[migration] apply start vendor=%s source=%s', vendor, normalizedSource ?? '<default>');
|
||||
const response = await openhumanMigrateOpenclaw(normalizedSource, false);
|
||||
setAppliedReport(response.result);
|
||||
// Clear preview so the operator can't accidentally re-apply the same
|
||||
// dry-run a second time without re-previewing the new on-disk state.
|
||||
setPreviewReport(null);
|
||||
setPreviewInput(null);
|
||||
log('[migration] apply ok stats=%o', response.result.stats);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
log('[migration] apply failed: %s', message);
|
||||
setError(message);
|
||||
} finally {
|
||||
setIsApplying(false);
|
||||
}
|
||||
}, [vendor, normalizedSource, previewReport, canApply, t]);
|
||||
|
||||
const reportToRender = appliedReport ?? previewReport;
|
||||
|
||||
return (
|
||||
<div className="z-10 relative">
|
||||
{!embedded && (
|
||||
<SettingsHeader
|
||||
title={t('migration.title')}
|
||||
showBackButton={true}
|
||||
onBack={navigateBack}
|
||||
breadcrumbs={breadcrumbs}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="max-w-3xl space-y-6 p-6">
|
||||
<p className="text-sm text-stone-600 dark:text-neutral-300">{t('migration.description')}</p>
|
||||
|
||||
<section
|
||||
className="bg-stone-50 dark:bg-neutral-900/40 rounded-lg border border-stone-200 dark:border-neutral-800 p-4 space-y-4"
|
||||
data-testid="migration-form">
|
||||
<label className="block space-y-1">
|
||||
<span className="text-xs font-medium text-stone-600 dark:text-neutral-300">
|
||||
{t('migration.vendorLabel')}
|
||||
</span>
|
||||
<select
|
||||
aria-label={t('migration.vendorLabel')}
|
||||
data-testid="migration-vendor-select"
|
||||
value={vendor}
|
||||
onChange={e => setVendor(e.target.value as Vendor)}
|
||||
className="w-full rounded-md border border-stone-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 px-3 py-2 text-sm text-stone-900 dark:text-neutral-100 focus:outline-none focus:ring-1 focus:ring-primary-400">
|
||||
<option value="openclaw">OpenClaw</option>
|
||||
<option value="hermes" disabled>
|
||||
Hermes Agent (coming soon)
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
{vendor === 'hermes' && (
|
||||
<p
|
||||
className="text-xs text-stone-500 dark:text-neutral-400"
|
||||
data-testid="migration-hermes-coming-soon">
|
||||
{t('migration.hermesComingSoonPrefix')}
|
||||
<a
|
||||
href={HERMES_TRACKING_URL}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-primary-600 dark:text-primary-300 underline">
|
||||
{t('migration.hermesLinkText')}
|
||||
</a>
|
||||
{t('migration.hermesComingSoonSuffix')}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<label className="block space-y-1">
|
||||
<span className="text-xs font-medium text-stone-600 dark:text-neutral-300">
|
||||
{t('migration.sourceLabel')}
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
data-testid="migration-source-input"
|
||||
value={sourcePath}
|
||||
onChange={e => setSourcePath(e.target.value)}
|
||||
placeholder={t('migration.sourcePlaceholder')}
|
||||
className="w-full rounded-md border border-stone-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 px-3 py-2 text-sm text-stone-900 dark:text-neutral-100 placeholder:text-stone-400 dark:placeholder:text-neutral-500 focus:outline-none focus:ring-1 focus:ring-primary-400"
|
||||
/>
|
||||
<p className="text-[11px] text-stone-500 dark:text-neutral-400">
|
||||
{t('migration.sourceHint')}
|
||||
</p>
|
||||
</label>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
data-testid="migration-preview-button"
|
||||
onClick={runPreview}
|
||||
disabled={vendor !== 'openclaw' || isPreviewing}
|
||||
className="px-3 py-1.5 text-xs rounded-md bg-primary-600 hover:bg-primary-700 disabled:opacity-60 text-white">
|
||||
{isPreviewing ? t('migration.previewRunning') : t('migration.previewAction')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="migration-apply-button"
|
||||
onClick={runApply}
|
||||
disabled={!canApply || vendor !== 'openclaw'}
|
||||
className="px-3 py-1.5 text-xs rounded-md bg-amber-600 hover:bg-amber-700 disabled:opacity-60 text-white">
|
||||
{isApplying ? t('migration.applyRunning') : t('migration.applyAction')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p className="text-[11px] text-stone-500 dark:text-neutral-400">
|
||||
{t('migration.applyDisclaimer')}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{error != null && (
|
||||
<div
|
||||
data-testid="migration-error"
|
||||
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">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{reportToRender != null && (
|
||||
<section
|
||||
data-testid={
|
||||
appliedReport != null ? 'migration-report-applied' : 'migration-report-preview'
|
||||
}
|
||||
className="bg-white dark:bg-neutral-900/40 rounded-lg border border-stone-200 dark:border-neutral-800 p-4 space-y-3">
|
||||
<h3 className="text-sm font-semibold text-stone-900 dark:text-neutral-100">
|
||||
{appliedReport != null
|
||||
? t('migration.reportTitleApplied')
|
||||
: t('migration.reportTitlePreview')}
|
||||
</h3>
|
||||
<dl className="grid grid-cols-1 sm:grid-cols-2 gap-x-4 gap-y-1 text-xs">
|
||||
<dt className="text-stone-500 dark:text-neutral-400">
|
||||
{t('migration.report.source')}
|
||||
</dt>
|
||||
<dd
|
||||
className="text-stone-900 dark:text-neutral-100 break-all"
|
||||
data-testid="migration-report-source">
|
||||
{reportToRender.source_workspace}
|
||||
</dd>
|
||||
<dt className="text-stone-500 dark:text-neutral-400">
|
||||
{t('migration.report.target')}
|
||||
</dt>
|
||||
<dd
|
||||
className="text-stone-900 dark:text-neutral-100 break-all"
|
||||
data-testid="migration-report-target">
|
||||
{reportToRender.target_workspace}
|
||||
</dd>
|
||||
<dt className="text-stone-500 dark:text-neutral-400">
|
||||
{t('migration.report.fromSqlite')}
|
||||
</dt>
|
||||
<dd className="text-stone-900 dark:text-neutral-100">
|
||||
{reportToRender.stats.from_sqlite}
|
||||
</dd>
|
||||
<dt className="text-stone-500 dark:text-neutral-400">
|
||||
{t('migration.report.fromMarkdown')}
|
||||
</dt>
|
||||
<dd className="text-stone-900 dark:text-neutral-100">
|
||||
{reportToRender.stats.from_markdown}
|
||||
</dd>
|
||||
<dt className="text-stone-500 dark:text-neutral-400">
|
||||
{t('migration.report.imported')}
|
||||
</dt>
|
||||
<dd
|
||||
className="text-stone-900 dark:text-neutral-100"
|
||||
data-testid="migration-report-imported">
|
||||
{reportToRender.stats.imported}
|
||||
</dd>
|
||||
<dt className="text-stone-500 dark:text-neutral-400">
|
||||
{t('migration.report.skippedUnchanged')}
|
||||
</dt>
|
||||
<dd className="text-stone-900 dark:text-neutral-100">
|
||||
{reportToRender.stats.skipped_unchanged}
|
||||
</dd>
|
||||
<dt className="text-stone-500 dark:text-neutral-400">
|
||||
{t('migration.report.renamedConflicts')}
|
||||
</dt>
|
||||
<dd className="text-stone-900 dark:text-neutral-100">
|
||||
{reportToRender.stats.renamed_conflicts}
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
{reportToRender.warnings.length > 0 && (
|
||||
<div className="space-y-1">
|
||||
<p className="text-xs font-medium text-stone-600 dark:text-neutral-300">
|
||||
{t('migration.report.warnings')}
|
||||
</p>
|
||||
<ul
|
||||
data-testid="migration-report-warnings"
|
||||
className="text-xs text-stone-700 dark:text-neutral-300 list-disc list-inside space-y-0.5">
|
||||
{reportToRender.warnings.map((w, i) => (
|
||||
<li key={i}>{w}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="text-[11px] text-stone-500 dark:text-neutral-400">
|
||||
{appliedReport != null
|
||||
? t('migration.report.appliedHint')
|
||||
: t('migration.report.previewHint')}
|
||||
</p>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MigrationPanel;
|
||||
@@ -0,0 +1,183 @@
|
||||
/**
|
||||
* Tests for the OpenClaw migration panel (#1440).
|
||||
*
|
||||
* Pins the dry-run-then-apply contract the issue calls out:
|
||||
* - Apply is disabled until a successful Preview lands.
|
||||
* - Apply requires an explicit window.confirm.
|
||||
* - The Hermes branch is visibly "coming soon" with a tracker link
|
||||
* (acceptance criterion in #1440 — "do not leave Hermes unmentioned
|
||||
* in UX").
|
||||
* - RPC errors render inline without nuking the form state.
|
||||
*/
|
||||
import { fireEvent, screen, waitFor } from '@testing-library/react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { renderWithProviders } from '../../../../test/test-utils';
|
||||
import {
|
||||
type MigrationReport,
|
||||
openhumanMigrateOpenclaw,
|
||||
} from '../../../../utils/tauriCommands/core';
|
||||
import MigrationPanel from '../MigrationPanel';
|
||||
|
||||
vi.mock('../../../../utils/tauriCommands/core', async () => {
|
||||
const actual = await vi.importActual<typeof import('../../../../utils/tauriCommands/core')>(
|
||||
'../../../../utils/tauriCommands/core'
|
||||
);
|
||||
return { ...actual, openhumanMigrateOpenclaw: vi.fn() };
|
||||
});
|
||||
|
||||
vi.mock('../../hooks/useSettingsNavigation', () => ({
|
||||
useSettingsNavigation: () => ({ navigateBack: vi.fn(), breadcrumbs: [] }),
|
||||
}));
|
||||
|
||||
function makeReport(
|
||||
overrides: Partial<MigrationReport> = {},
|
||||
statsOverrides: Partial<MigrationReport['stats']> = {}
|
||||
): MigrationReport {
|
||||
return {
|
||||
source_workspace: '/home/u/.openclaw/workspace',
|
||||
target_workspace: '/home/u/.openhuman/workspace',
|
||||
dry_run: true,
|
||||
stats: {
|
||||
from_sqlite: 4,
|
||||
from_markdown: 2,
|
||||
imported: 0,
|
||||
skipped_unchanged: 0,
|
||||
renamed_conflicts: 0,
|
||||
...statsOverrides,
|
||||
},
|
||||
warnings: ['No conflicts detected'],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('MigrationPanel (#1440)', () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(openhumanMigrateOpenclaw).mockReset();
|
||||
});
|
||||
|
||||
it('renders OpenClaw as the active vendor and Hermes as coming-soon with the tracker link', () => {
|
||||
renderWithProviders(<MigrationPanel />);
|
||||
const select = screen.getByTestId('migration-vendor-select') as HTMLSelectElement;
|
||||
expect(select.value).toBe('openclaw');
|
||||
const hermesOption = Array.from(select.options).find(o => o.value === 'hermes');
|
||||
expect(hermesOption).toBeDefined();
|
||||
expect(hermesOption?.disabled).toBe(true);
|
||||
// Apply must start disabled — the issue (#1440) is explicit that dry-run
|
||||
// vs apply must be visually obvious.
|
||||
expect(screen.getByTestId('migration-apply-button')).toBeDisabled();
|
||||
});
|
||||
|
||||
it('surfaces the Hermes "coming soon" callout (with tracking link) when the user picks Hermes', () => {
|
||||
renderWithProviders(<MigrationPanel />);
|
||||
const select = screen.getByTestId('migration-vendor-select') as HTMLSelectElement;
|
||||
fireEvent.change(select, { target: { value: 'hermes' } });
|
||||
const callout = screen.getByTestId('migration-hermes-coming-soon');
|
||||
expect(callout).toBeInTheDocument();
|
||||
const link = callout.querySelector('a');
|
||||
expect(link?.getAttribute('href')).toContain('issues/1440');
|
||||
});
|
||||
|
||||
it('calls the RPC with dry_run=true on Preview and renders the report', async () => {
|
||||
vi.mocked(openhumanMigrateOpenclaw).mockResolvedValueOnce({
|
||||
result: makeReport({}, { from_sqlite: 7, from_markdown: 3 }),
|
||||
logs: [],
|
||||
});
|
||||
|
||||
renderWithProviders(<MigrationPanel />);
|
||||
fireEvent.click(screen.getByTestId('migration-preview-button'));
|
||||
|
||||
await waitFor(() => expect(screen.getByTestId('migration-report-preview')).toBeInTheDocument());
|
||||
expect(openhumanMigrateOpenclaw).toHaveBeenCalledWith(undefined, true);
|
||||
expect(screen.getByTestId('migration-report-source').textContent).toContain(
|
||||
'/home/u/.openclaw/workspace'
|
||||
);
|
||||
expect(screen.getByTestId('migration-report-warnings').textContent).toContain(
|
||||
'No conflicts detected'
|
||||
);
|
||||
// Apply is unlocked now that we have a preview.
|
||||
expect(screen.getByTestId('migration-apply-button')).not.toBeDisabled();
|
||||
});
|
||||
|
||||
it('passes the user-supplied source path through to the RPC', async () => {
|
||||
vi.mocked(openhumanMigrateOpenclaw).mockResolvedValueOnce({ result: makeReport(), logs: [] });
|
||||
renderWithProviders(<MigrationPanel />);
|
||||
const input = screen.getByTestId('migration-source-input') as HTMLInputElement;
|
||||
fireEvent.change(input, { target: { value: ' /opt/legacy/openclaw ' } });
|
||||
fireEvent.click(screen.getByTestId('migration-preview-button'));
|
||||
await waitFor(() =>
|
||||
expect(openhumanMigrateOpenclaw).toHaveBeenCalledWith('/opt/legacy/openclaw', true)
|
||||
);
|
||||
});
|
||||
|
||||
it('requires window.confirm before Apply and calls the RPC with dry_run=false on yes', async () => {
|
||||
vi.mocked(openhumanMigrateOpenclaw)
|
||||
.mockResolvedValueOnce({ result: makeReport(), logs: [] })
|
||||
.mockResolvedValueOnce({
|
||||
result: makeReport({ dry_run: false }, { from_sqlite: 4, from_markdown: 2, imported: 6 }),
|
||||
logs: [],
|
||||
});
|
||||
|
||||
const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValueOnce(true);
|
||||
renderWithProviders(<MigrationPanel />);
|
||||
fireEvent.click(screen.getByTestId('migration-preview-button'));
|
||||
await waitFor(() => expect(screen.getByTestId('migration-apply-button')).not.toBeDisabled());
|
||||
|
||||
fireEvent.click(screen.getByTestId('migration-apply-button'));
|
||||
await waitFor(() =>
|
||||
expect(openhumanMigrateOpenclaw).toHaveBeenNthCalledWith(2, undefined, false)
|
||||
);
|
||||
expect(confirmSpy).toHaveBeenCalledTimes(1);
|
||||
await waitFor(() => expect(screen.getByTestId('migration-report-applied')).toBeInTheDocument());
|
||||
confirmSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('skips Apply when the user cancels the confirm dialog', async () => {
|
||||
vi.mocked(openhumanMigrateOpenclaw).mockResolvedValueOnce({ result: makeReport(), logs: [] });
|
||||
const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValueOnce(false);
|
||||
renderWithProviders(<MigrationPanel />);
|
||||
fireEvent.click(screen.getByTestId('migration-preview-button'));
|
||||
await waitFor(() => expect(screen.getByTestId('migration-apply-button')).not.toBeDisabled());
|
||||
|
||||
fireEvent.click(screen.getByTestId('migration-apply-button'));
|
||||
// Only the Preview call should have fired — Apply must not call the RPC
|
||||
// when the operator says no.
|
||||
expect(openhumanMigrateOpenclaw).toHaveBeenCalledTimes(1);
|
||||
expect(screen.queryByTestId('migration-report-applied')).toBeNull();
|
||||
confirmSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('re-disables Apply when the source path is edited after a preview', async () => {
|
||||
// CodeRabbit regression guard on PR #2087: previously the Apply button
|
||||
// stayed unlocked after any prior preview, so editing the source path
|
||||
// could run Apply against a workspace that was never previewed.
|
||||
vi.mocked(openhumanMigrateOpenclaw).mockResolvedValueOnce({ result: makeReport(), logs: [] });
|
||||
renderWithProviders(<MigrationPanel />);
|
||||
const input = screen.getByTestId('migration-source-input') as HTMLInputElement;
|
||||
const apply = screen.getByTestId('migration-apply-button');
|
||||
|
||||
// Preview with no source path → Apply unlocks.
|
||||
fireEvent.click(screen.getByTestId('migration-preview-button'));
|
||||
await waitFor(() => expect(apply).not.toBeDisabled());
|
||||
|
||||
// User edits the path after previewing — Apply must re-lock until
|
||||
// they preview the new value.
|
||||
fireEvent.change(input, { target: { value: '/opt/legacy/openclaw' } });
|
||||
expect(apply).toBeDisabled();
|
||||
});
|
||||
|
||||
it('renders inline error on RPC failure without removing the form', async () => {
|
||||
vi.mocked(openhumanMigrateOpenclaw).mockRejectedValueOnce(
|
||||
new Error('OpenClaw workspace not found at /opt/legacy/openclaw')
|
||||
);
|
||||
renderWithProviders(<MigrationPanel />);
|
||||
fireEvent.click(screen.getByTestId('migration-preview-button'));
|
||||
await waitFor(() => expect(screen.getByTestId('migration-error')).toBeInTheDocument());
|
||||
expect(screen.getByTestId('migration-error').textContent).toContain(
|
||||
'OpenClaw workspace not found'
|
||||
);
|
||||
// Form survives so the user can edit + retry.
|
||||
expect(screen.getByTestId('migration-form')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('migration-apply-button')).toBeDisabled();
|
||||
});
|
||||
});
|
||||
@@ -374,6 +374,40 @@ const ar1: TranslationMap = {
|
||||
'settings.about.releasesDesc': 'تصفح ملاحظات الإصدار والإصدارات السابقة على GitHub.',
|
||||
'settings.about.openReleases': 'فتح إصدارات GitHub',
|
||||
'settings.ai.overview': 'نظرة عامة على نظام الذكاء الاصطناعي',
|
||||
'migration.title': 'استيراد من مساعد آخر',
|
||||
'migration.description':
|
||||
'انقل الذاكرة والملاحظات من مساعد محلي آخر إلى مساحة العمل هذه. ابدأ بـ«معاينة» لرؤية ما سيتغير، ثم اضغط «تطبيق» لنسخ البيانات. يتم نسخ ذاكرتك الحالية احتياطيًا أولًا.',
|
||||
'migration.vendorLabel': 'المصدر',
|
||||
'migration.sourceLabel': 'مسار مساحة العمل المصدر (اختياري)',
|
||||
'migration.sourcePlaceholder': 'اتركه فارغًا للاكتشاف التلقائي (مثال: ~/.openclaw/workspace)',
|
||||
'migration.sourceHint':
|
||||
'يستخدم الموقع الافتراضي للمصدر عند تركه فارغًا. حدد مسارًا صريحًا إذا نقلت مساحة العمل إلى مكان آخر.',
|
||||
'migration.previewAction': 'معاينة',
|
||||
'migration.previewRunning': 'جاري المعاينة…',
|
||||
'migration.applyAction': 'تطبيق الاستيراد',
|
||||
'migration.applyRunning': 'جاري الاستيراد…',
|
||||
'migration.applyDisclaimer':
|
||||
'يُفتح التطبيق فقط بعد معاينة ناجحة لنفس المصدر. يتم نسخ الذاكرة الحالية احتياطيًا قبل أي استيراد.',
|
||||
'migration.reportTitlePreview': 'معاينة — لم يُستورد شيء بعد',
|
||||
'migration.reportTitleApplied': 'اكتمل الاستيراد',
|
||||
'migration.report.source': 'مساحة العمل المصدر',
|
||||
'migration.report.target': 'مساحة العمل الهدف',
|
||||
'migration.report.fromSqlite': 'من SQLite (brain.db)',
|
||||
'migration.report.fromMarkdown': 'من Markdown',
|
||||
'migration.report.imported': 'مُستورد',
|
||||
'migration.report.skippedUnchanged': 'تم تخطّيه (دون تغيير)',
|
||||
'migration.report.renamedConflicts': 'تمت إعادة التسمية عند التعارض',
|
||||
'migration.report.warnings': 'تحذيرات',
|
||||
'migration.report.previewHint': 'لم تُستورد أي بيانات بعد. اضغط «تطبيق الاستيراد» للنسخ.',
|
||||
'migration.report.appliedHint':
|
||||
'العناصر المستوردة أصبحت الآن في ذاكرتك. أعد تشغيل «المعاينة» للمقارنة مرة أخرى.',
|
||||
'migration.hermesComingSoonPrefix': 'مستورد Hermes ضمن خارطة الطريق — راجع ',
|
||||
'migration.hermesComingSoonSuffix': ' للسياق. استخدم OpenClaw للترحيل اليوم؛ سيصل Hermes لاحقًا.',
|
||||
'migration.hermesLinkText': '#1440',
|
||||
'migration.confirmImport.singular':
|
||||
'استيراد {count} عنصر إلى مساحة العمل الحالية؟\n\nالمصدر: {source}\nالهدف: {target}\n\nسيتم نسخ الذاكرة الحالية احتياطيًا قبل بدء الاستيراد.',
|
||||
'migration.confirmImport.plural':
|
||||
'استيراد {count} عنصر إلى مساحة العمل الحالية؟\n\nالمصدر: {source}\nالهدف: {target}\n\nسيتم نسخ الذاكرة الحالية احتياطيًا قبل بدء الاستيراد.',
|
||||
};
|
||||
|
||||
export default ar1;
|
||||
|
||||
@@ -361,6 +361,9 @@ const ar4: TranslationMap = {
|
||||
'settings.billing.subscription.paymentConfirmed': 'تم تأكيد الدفع',
|
||||
'settings.billing.subscription.perMonth': 'في الشهر',
|
||||
'settings.billing.subscription.popular': 'شائع',
|
||||
'pages.settings.account.migration': 'استيراد من مساعد آخر',
|
||||
'pages.settings.account.migrationDesc':
|
||||
'انقل الذاكرة والملاحظات من OpenClaw (وقريبًا Hermes) إلى مساحة العمل هذه.',
|
||||
};
|
||||
|
||||
export default ar4;
|
||||
|
||||
@@ -382,6 +382,41 @@ const bn1: TranslationMap = {
|
||||
'settings.about.releasesDesc': 'GitHub-এ রিলিজ নোট ও আগের বিল্ড দেখুন।',
|
||||
'settings.about.openReleases': 'GitHub রিলিজ খুলুন',
|
||||
'settings.ai.overview': 'AI সিস্টেম ওভারভিউ',
|
||||
'migration.title': 'অন্য সহকারী থেকে আমদানি করুন',
|
||||
'migration.description':
|
||||
'অন্য একটি লোকাল সহকারী থেকে মেমরি ও নোট এই ওয়ার্কস্পেসে স্থানান্তর করুন। প্রথমে Preview চালিয়ে দেখুন ঠিক কী বদলাবে, তারপর Apply চাপলে ডেটা কপি হবে। আপনার বর্তমান মেমরি আগে ব্যাকআপ নেওয়া হবে।',
|
||||
'migration.vendorLabel': 'উৎস',
|
||||
'migration.sourceLabel': 'উৎস ওয়ার্কস্পেসের পাথ (ঐচ্ছিক)',
|
||||
'migration.sourcePlaceholder': 'অটো-ডিটেক্টের জন্য খালি রাখুন (যেমন ~/.openclaw/workspace)',
|
||||
'migration.sourceHint':
|
||||
'খালি রাখলে উৎসের ডিফল্ট লোকেশন ব্যবহার হবে। ওয়ার্কস্পেস সরিয়ে থাকলে স্পষ্ট পাথ দিন।',
|
||||
'migration.previewAction': 'প্রিভিউ',
|
||||
'migration.previewRunning': 'প্রিভিউ চলছে…',
|
||||
'migration.applyAction': 'আমদানি প্রয়োগ করুন',
|
||||
'migration.applyRunning': 'আমদানি হচ্ছে…',
|
||||
'migration.applyDisclaimer':
|
||||
'একই উৎসের সফল Preview হলেই Apply খুলবে। যে কোনো আমদানির আগে বর্তমান মেমরির ব্যাকআপ নেওয়া হয়।',
|
||||
'migration.reportTitlePreview': 'প্রিভিউ — এখনো কিছু আমদানি হয়নি',
|
||||
'migration.reportTitleApplied': 'আমদানি সম্পন্ন',
|
||||
'migration.report.source': 'উৎস ওয়ার্কস্পেস',
|
||||
'migration.report.target': 'লক্ষ্য ওয়ার্কস্পেস',
|
||||
'migration.report.fromSqlite': 'SQLite (brain.db) থেকে',
|
||||
'migration.report.fromMarkdown': 'Markdown থেকে',
|
||||
'migration.report.imported': 'আমদানিকৃত',
|
||||
'migration.report.skippedUnchanged': 'এড়ানো হয়েছে (অপরিবর্তিত)',
|
||||
'migration.report.renamedConflicts': 'দ্বন্দ্বে নাম বদলানো হয়েছে',
|
||||
'migration.report.warnings': 'সতর্কতা',
|
||||
'migration.report.previewHint': 'এখনো কোনো ডেটা আমদানি হয়নি। কপি করতে Apply চাপুন।',
|
||||
'migration.report.appliedHint':
|
||||
'আমদানিকৃত এন্ট্রি এখন আপনার মেমরিতে আছে। তুলনা করতে আবার Preview চালান।',
|
||||
'migration.hermesComingSoonPrefix': 'Hermes ইম্পোর্টার রোডম্যাপে আছে — দেখুন ',
|
||||
'migration.hermesComingSoonSuffix':
|
||||
' প্রসঙ্গের জন্য। আজ OpenClaw বেছে নিন; Hermes পরবর্তী রিলিজে আসবে।',
|
||||
'migration.hermesLinkText': '#1440',
|
||||
'migration.confirmImport.singular':
|
||||
'বর্তমান ওয়ার্কস্পেসে {count}টি এন্ট্রি আমদানি করবেন?\n\nউৎস: {source}\nলক্ষ্য: {target}\n\nআমদানির আগে বর্তমান মেমরির ব্যাকআপ নেওয়া হবে।',
|
||||
'migration.confirmImport.plural':
|
||||
'বর্তমান ওয়ার্কস্পেসে {count}টি এন্ট্রি আমদানি করবেন?\n\nউৎস: {source}\nলক্ষ্য: {target}\n\nআমদানির আগে বর্তমান মেমরির ব্যাকআপ নেওয়া হবে।',
|
||||
};
|
||||
|
||||
export default bn1;
|
||||
|
||||
@@ -363,6 +363,9 @@ const bn4: TranslationMap = {
|
||||
'settings.billing.subscription.paymentConfirmed': 'পেমেন্ট নিশ্চিত',
|
||||
'settings.billing.subscription.perMonth': 'প্রতি মাসে',
|
||||
'settings.billing.subscription.popular': 'জনপ্রিয়',
|
||||
'pages.settings.account.migration': 'অন্য সহকারী থেকে আমদানি করুন',
|
||||
'pages.settings.account.migrationDesc':
|
||||
'OpenClaw (এবং শীঘ্রই Hermes) থেকে মেমরি ও নোট এই ওয়ার্কস্পেসে স্থানান্তর করুন।',
|
||||
};
|
||||
|
||||
export default bn4;
|
||||
|
||||
@@ -93,6 +93,42 @@ const en1: TranslationMap = {
|
||||
'settings.account.connectionsDesc': 'Manage linked accounts and services',
|
||||
'settings.account.privacy': 'Privacy',
|
||||
'settings.account.privacyDesc': 'Control what data leaves your computer',
|
||||
'migration.title': 'Import from another assistant',
|
||||
'migration.description':
|
||||
'Migrate memory and notes from another local assistant into this workspace. Start with a Preview to see exactly what would change, then Apply to copy the data over. Your current memory is backed up first.',
|
||||
'migration.vendorLabel': 'Source vendor',
|
||||
'migration.sourceLabel': 'Source workspace path (optional)',
|
||||
'migration.sourcePlaceholder': 'Leave blank to auto-detect (e.g. ~/.openclaw/workspace)',
|
||||
'migration.sourceHint':
|
||||
"Defaults to the vendor's standard location when blank. Set an explicit path if you've moved the workspace elsewhere.",
|
||||
'migration.previewAction': 'Preview',
|
||||
'migration.previewRunning': 'Previewing…',
|
||||
'migration.applyAction': 'Apply import',
|
||||
'migration.applyRunning': 'Importing…',
|
||||
'migration.applyDisclaimer':
|
||||
'Apply is unlocked after a successful Preview of the same source. Existing memory is backed up before any import.',
|
||||
'migration.reportTitlePreview': 'Preview — nothing imported yet',
|
||||
'migration.reportTitleApplied': 'Import complete',
|
||||
'migration.report.source': 'Source workspace',
|
||||
'migration.report.target': 'Target workspace',
|
||||
'migration.report.fromSqlite': 'From SQLite (brain.db)',
|
||||
'migration.report.fromMarkdown': 'From Markdown',
|
||||
'migration.report.imported': 'Imported',
|
||||
'migration.report.skippedUnchanged': 'Skipped (unchanged)',
|
||||
'migration.report.renamedConflicts': 'Renamed on conflict',
|
||||
'migration.report.warnings': 'Warnings',
|
||||
'migration.report.previewHint':
|
||||
'No data has been imported yet. Click Apply import to copy it over.',
|
||||
'migration.report.appliedHint':
|
||||
'Imported entries are now in your memory. Re-run Preview if you want to compare again.',
|
||||
'migration.hermesComingSoonPrefix': 'Hermes importer is on the roadmap — see ',
|
||||
'migration.hermesComingSoonSuffix':
|
||||
' for context. Pick OpenClaw to migrate today; Hermes lands in a follow-up.',
|
||||
'migration.hermesLinkText': '#1440',
|
||||
'migration.confirmImport.singular':
|
||||
'Import {count} entry into the current workspace?\n\nSource: {source}\nTarget: {target}\n\nExisting memory will be backed up before the import runs.',
|
||||
'migration.confirmImport.plural':
|
||||
'Import {count} entries into the current workspace?\n\nSource: {source}\nTarget: {target}\n\nExisting memory will be backed up before the import runs.',
|
||||
'settings.notifications.doNotDisturb': 'Do Not Disturb',
|
||||
'settings.notifications.doNotDisturbDesc': 'Pause all notifications for a set period',
|
||||
'settings.notifications.channelControls': 'Per-Channel Controls',
|
||||
|
||||
@@ -159,6 +159,9 @@ const en4: TranslationMap = {
|
||||
'Manage your BIP39 recovery phrase for encryption and wallet access',
|
||||
'pages.settings.account.team': 'Team',
|
||||
'pages.settings.account.teamDesc': 'Manage your team, members, and invites',
|
||||
'pages.settings.account.migration': 'Import from another assistant',
|
||||
'pages.settings.account.migrationDesc':
|
||||
'Migrate memory and notes from OpenClaw (or, soon, Hermes) into this workspace.',
|
||||
'pages.settings.accountSection.description':
|
||||
'Recovery phrase, team, connections, and privacy settings.',
|
||||
'pages.settings.accountSection.title': 'Account',
|
||||
|
||||
@@ -392,6 +392,43 @@ const es1: TranslationMap = {
|
||||
'Explora las notas de versión y compilaciones anteriores en GitHub.',
|
||||
'settings.about.openReleases': 'Abrir versiones en GitHub',
|
||||
'settings.ai.overview': 'Resumen del sistema de IA',
|
||||
'migration.title': 'Importar desde otro asistente',
|
||||
'migration.description':
|
||||
'Migra la memoria y las notas de otro asistente local a este espacio de trabajo. Empieza con una Vista previa para ver exactamente qué cambiará y luego pulsa Aplicar para copiar los datos. Tu memoria actual se respalda primero.',
|
||||
'migration.vendorLabel': 'Proveedor de origen',
|
||||
'migration.sourceLabel': 'Ruta del espacio de trabajo de origen (opcional)',
|
||||
'migration.sourcePlaceholder':
|
||||
'Déjalo en blanco para detectar automáticamente (p. ej. ~/.openclaw/workspace)',
|
||||
'migration.sourceHint':
|
||||
'Si está vacío, se usa la ubicación predeterminada del proveedor. Indica una ruta explícita si moviste el espacio de trabajo a otro sitio.',
|
||||
'migration.previewAction': 'Vista previa',
|
||||
'migration.previewRunning': 'Previsualizando…',
|
||||
'migration.applyAction': 'Aplicar importación',
|
||||
'migration.applyRunning': 'Importando…',
|
||||
'migration.applyDisclaimer':
|
||||
'Aplicar se desbloquea tras una Vista previa exitosa del mismo origen. La memoria actual se respalda antes de cualquier importación.',
|
||||
'migration.reportTitlePreview': 'Vista previa — aún no se importó nada',
|
||||
'migration.reportTitleApplied': 'Importación completa',
|
||||
'migration.report.source': 'Espacio de trabajo de origen',
|
||||
'migration.report.target': 'Espacio de trabajo de destino',
|
||||
'migration.report.fromSqlite': 'Desde SQLite (brain.db)',
|
||||
'migration.report.fromMarkdown': 'Desde Markdown',
|
||||
'migration.report.imported': 'Importado',
|
||||
'migration.report.skippedUnchanged': 'Omitido (sin cambios)',
|
||||
'migration.report.renamedConflicts': 'Renombrado por conflicto',
|
||||
'migration.report.warnings': 'Advertencias',
|
||||
'migration.report.previewHint':
|
||||
'Todavía no se importó ningún dato. Pulsa Aplicar importación para copiarlo.',
|
||||
'migration.report.appliedHint':
|
||||
'Las entradas importadas ya están en tu memoria. Vuelve a ejecutar Vista previa si quieres comparar de nuevo.',
|
||||
'migration.hermesComingSoonPrefix': 'El importador de Hermes está en la hoja de ruta — ver ',
|
||||
'migration.hermesComingSoonSuffix':
|
||||
' para más contexto. Hoy puedes migrar con OpenClaw; Hermes llegará en una entrega posterior.',
|
||||
'migration.hermesLinkText': '#1440',
|
||||
'migration.confirmImport.singular':
|
||||
'¿Importar {count} entrada al espacio de trabajo actual?\n\nOrigen: {source}\nDestino: {target}\n\nLa memoria existente se respaldará antes de la importación.',
|
||||
'migration.confirmImport.plural':
|
||||
'¿Importar {count} entradas al espacio de trabajo actual?\n\nOrigen: {source}\nDestino: {target}\n\nLa memoria existente se respaldará antes de la importación.',
|
||||
};
|
||||
|
||||
export default es1;
|
||||
|
||||
@@ -365,6 +365,9 @@ const es4: TranslationMap = {
|
||||
'settings.billing.subscription.paymentConfirmed': 'Pago confirmado',
|
||||
'settings.billing.subscription.perMonth': 'Por mes',
|
||||
'settings.billing.subscription.popular': 'Popular',
|
||||
'pages.settings.account.migration': 'Importar desde otro asistente',
|
||||
'pages.settings.account.migrationDesc':
|
||||
'Migra memoria y notas desde OpenClaw (y pronto Hermes) a este espacio de trabajo.',
|
||||
};
|
||||
|
||||
export default es4;
|
||||
|
||||
@@ -394,6 +394,43 @@ const fr1: TranslationMap = {
|
||||
'Parcourir les notes de version et les builds précédentes sur GitHub.',
|
||||
'settings.about.openReleases': 'Ouvrir les versions GitHub',
|
||||
'settings.ai.overview': "Vue d'ensemble du système IA",
|
||||
'migration.title': 'Importer depuis un autre assistant',
|
||||
'migration.description':
|
||||
"Migrez la mémoire et les notes d'un autre assistant local vers cet espace de travail. Commencez par un Aperçu pour voir précisément ce qui changera, puis cliquez sur Appliquer pour copier les données. Votre mémoire actuelle est sauvegardée au préalable.",
|
||||
'migration.vendorLabel': 'Source',
|
||||
'migration.sourceLabel': "Chemin de l'espace de travail source (facultatif)",
|
||||
'migration.sourcePlaceholder':
|
||||
'Laisser vide pour détection automatique (p. ex. ~/.openclaw/workspace)',
|
||||
'migration.sourceHint':
|
||||
"Utilise l'emplacement par défaut du fournisseur si vide. Indiquez un chemin explicite si vous avez déplacé l'espace de travail.",
|
||||
'migration.previewAction': 'Aperçu',
|
||||
'migration.previewRunning': 'Aperçu en cours…',
|
||||
'migration.applyAction': "Appliquer l'import",
|
||||
'migration.applyRunning': 'Importation…',
|
||||
'migration.applyDisclaimer':
|
||||
'Appliquer est débloqué après un Aperçu réussi de la même source. La mémoire existante est sauvegardée avant tout import.',
|
||||
'migration.reportTitlePreview': "Aperçu — rien d'importé pour l'instant",
|
||||
'migration.reportTitleApplied': 'Importation terminée',
|
||||
'migration.report.source': 'Espace de travail source',
|
||||
'migration.report.target': 'Espace de travail cible',
|
||||
'migration.report.fromSqlite': 'Depuis SQLite (brain.db)',
|
||||
'migration.report.fromMarkdown': 'Depuis Markdown',
|
||||
'migration.report.imported': 'Importés',
|
||||
'migration.report.skippedUnchanged': 'Ignorés (inchangés)',
|
||||
'migration.report.renamedConflicts': 'Renommés en cas de conflit',
|
||||
'migration.report.warnings': 'Avertissements',
|
||||
'migration.report.previewHint':
|
||||
"Aucune donnée n'a encore été importée. Cliquez sur Appliquer l'import pour la copier.",
|
||||
'migration.report.appliedHint':
|
||||
'Les entrées importées sont maintenant dans votre mémoire. Relancez Aperçu pour comparer à nouveau.',
|
||||
'migration.hermesComingSoonPrefix': "L'importateur Hermes est sur la feuille de route — voir ",
|
||||
'migration.hermesComingSoonSuffix':
|
||||
" pour le contexte. Choisissez OpenClaw pour migrer aujourd'hui ; Hermes arrivera dans une prochaine itération.",
|
||||
'migration.hermesLinkText': '#1440',
|
||||
'migration.confirmImport.singular':
|
||||
"Importer {count} entrée dans l'espace de travail actuel ?\n\nSource : {source}\nCible : {target}\n\nLa mémoire existante sera sauvegardée avant l'import.",
|
||||
'migration.confirmImport.plural':
|
||||
"Importer {count} entrées dans l'espace de travail actuel ?\n\nSource : {source}\nCible : {target}\n\nLa mémoire existante sera sauvegardée avant l'import.",
|
||||
};
|
||||
|
||||
export default fr1;
|
||||
|
||||
@@ -364,6 +364,9 @@ const fr4: TranslationMap = {
|
||||
'settings.billing.subscription.paymentConfirmed': 'Paiement confirmé',
|
||||
'settings.billing.subscription.perMonth': 'Par mois',
|
||||
'settings.billing.subscription.popular': 'Populaire',
|
||||
'pages.settings.account.migration': 'Importer depuis un autre assistant',
|
||||
'pages.settings.account.migrationDesc':
|
||||
'Migrez la mémoire et les notes depuis OpenClaw (et bientôt Hermes) vers cet espace de travail.',
|
||||
};
|
||||
|
||||
export default fr4;
|
||||
|
||||
@@ -379,6 +379,41 @@ const hi1: TranslationMap = {
|
||||
'settings.about.releasesDesc': 'GitHub पर रिलीज़ नोट्स और पुराने बिल्ड देखें।',
|
||||
'settings.about.openReleases': 'GitHub रिलीज़ खोलें',
|
||||
'settings.ai.overview': 'AI सिस्टम ओवरव्यू',
|
||||
'migration.title': 'किसी अन्य असिस्टेंट से इम्पोर्ट करें',
|
||||
'migration.description':
|
||||
'किसी अन्य लोकल असिस्टेंट से मेमोरी और नोट्स को इस वर्कस्पेस में माइग्रेट करें। पहले Preview से देखें कि क्या बदलेगा, फिर Apply से डेटा कॉपी करें। आपकी मौजूदा मेमोरी पहले बैकअप कर ली जाती है।',
|
||||
'migration.vendorLabel': 'सोर्स वेंडर',
|
||||
'migration.sourceLabel': 'सोर्स वर्कस्पेस पाथ (वैकल्पिक)',
|
||||
'migration.sourcePlaceholder': 'ऑटो-डिटेक्ट के लिए खाली छोड़ें (जैसे ~/.openclaw/workspace)',
|
||||
'migration.sourceHint':
|
||||
'खाली होने पर वेंडर के डिफ़ॉल्ट लोकेशन का उपयोग होता है। अगर आपने वर्कस्पेस हटाया है तो स्पष्ट पाथ दें।',
|
||||
'migration.previewAction': 'प्रीव्यू',
|
||||
'migration.previewRunning': 'प्रीव्यू हो रहा है…',
|
||||
'migration.applyAction': 'इम्पोर्ट लागू करें',
|
||||
'migration.applyRunning': 'इम्पोर्ट हो रहा है…',
|
||||
'migration.applyDisclaimer':
|
||||
'उसी सोर्स के सफल Preview के बाद ही Apply अनलॉक होता है। किसी भी इम्पोर्ट से पहले मौजूदा मेमोरी का बैकअप लिया जाता है।',
|
||||
'migration.reportTitlePreview': 'प्रीव्यू — अभी कुछ इम्पोर्ट नहीं हुआ',
|
||||
'migration.reportTitleApplied': 'इम्पोर्ट पूरा',
|
||||
'migration.report.source': 'सोर्स वर्कस्पेस',
|
||||
'migration.report.target': 'टार्गेट वर्कस्पेस',
|
||||
'migration.report.fromSqlite': 'SQLite (brain.db) से',
|
||||
'migration.report.fromMarkdown': 'Markdown से',
|
||||
'migration.report.imported': 'इम्पोर्ट हुए',
|
||||
'migration.report.skippedUnchanged': 'छोड़े गए (अपरिवर्तित)',
|
||||
'migration.report.renamedConflicts': 'टकराव पर नाम बदला',
|
||||
'migration.report.warnings': 'चेतावनी',
|
||||
'migration.report.previewHint':
|
||||
'अभी तक कोई डेटा इम्पोर्ट नहीं हुआ है। कॉपी करने के लिए Apply पर क्लिक करें।',
|
||||
'migration.report.appliedHint':
|
||||
'इम्पोर्ट की गई एंट्रीज़ अब आपकी मेमोरी में हैं। दोबारा तुलना के लिए Preview फिर से चलाएँ।',
|
||||
'migration.hermesComingSoonPrefix': 'Hermes इम्पोर्टर रोडमैप पर है — देखें ',
|
||||
'migration.hermesComingSoonSuffix': '। आज OpenClaw चुनें; Hermes अगले अपडेट में आएगा।',
|
||||
'migration.hermesLinkText': '#1440',
|
||||
'migration.confirmImport.singular':
|
||||
'{count} एंट्री को मौजूदा वर्कस्पेस में इम्पोर्ट करें?\n\nसोर्स: {source}\nटार्गेट: {target}\n\nइम्पोर्ट से पहले मौजूदा मेमोरी का बैकअप लिया जाएगा।',
|
||||
'migration.confirmImport.plural':
|
||||
'{count} एंट्रीज़ को मौजूदा वर्कस्पेस में इम्पोर्ट करें?\n\nसोर्स: {source}\nटार्गेट: {target}\n\nइम्पोर्ट से पहले मौजूदा मेमोरी का बैकअप लिया जाएगा।',
|
||||
};
|
||||
|
||||
export default hi1;
|
||||
|
||||
@@ -363,6 +363,9 @@ const hi4: TranslationMap = {
|
||||
'settings.billing.subscription.paymentConfirmed': 'पेमेंट कन्फर्म हुई',
|
||||
'settings.billing.subscription.perMonth': 'प्रति माह',
|
||||
'settings.billing.subscription.popular': 'लोकप्रिय',
|
||||
'pages.settings.account.migration': 'किसी अन्य असिस्टेंट से इम्पोर्ट करें',
|
||||
'pages.settings.account.migrationDesc':
|
||||
'OpenClaw (और जल्द ही Hermes) से मेमोरी और नोट्स इस वर्कस्पेस में माइग्रेट करें।',
|
||||
};
|
||||
|
||||
export default hi4;
|
||||
|
||||
@@ -382,6 +382,43 @@ const id1: TranslationMap = {
|
||||
'settings.about.releasesDesc': 'Telusuri catatan rilis dan build sebelumnya di GitHub.',
|
||||
'settings.about.openReleases': 'Buka rilis GitHub',
|
||||
'settings.ai.overview': 'Ringkasan Sistem AI',
|
||||
'migration.title': 'Impor dari asisten lain',
|
||||
'migration.description':
|
||||
'Migrasikan memori dan catatan dari asisten lokal lain ke ruang kerja ini. Mulai dengan Pratinjau untuk melihat persis apa yang akan berubah, lalu klik Terapkan untuk menyalin datanya. Memori Anda saat ini dicadangkan terlebih dahulu.',
|
||||
'migration.vendorLabel': 'Sumber',
|
||||
'migration.sourceLabel': 'Path ruang kerja sumber (opsional)',
|
||||
'migration.sourcePlaceholder':
|
||||
'Biarkan kosong untuk deteksi otomatis (misalnya ~/.openclaw/workspace)',
|
||||
'migration.sourceHint':
|
||||
'Kosong berarti memakai lokasi default sumber. Isi path eksplisit jika ruang kerja sudah dipindahkan.',
|
||||
'migration.previewAction': 'Pratinjau',
|
||||
'migration.previewRunning': 'Memuat pratinjau…',
|
||||
'migration.applyAction': 'Terapkan impor',
|
||||
'migration.applyRunning': 'Mengimpor…',
|
||||
'migration.applyDisclaimer':
|
||||
'Terapkan terbuka setelah Pratinjau berhasil untuk sumber yang sama. Memori yang ada dicadangkan sebelum impor apa pun.',
|
||||
'migration.reportTitlePreview': 'Pratinjau — belum ada yang diimpor',
|
||||
'migration.reportTitleApplied': 'Impor selesai',
|
||||
'migration.report.source': 'Ruang kerja sumber',
|
||||
'migration.report.target': 'Ruang kerja tujuan',
|
||||
'migration.report.fromSqlite': 'Dari SQLite (brain.db)',
|
||||
'migration.report.fromMarkdown': 'Dari Markdown',
|
||||
'migration.report.imported': 'Diimpor',
|
||||
'migration.report.skippedUnchanged': 'Dilewati (tidak berubah)',
|
||||
'migration.report.renamedConflicts': 'Diganti nama saat konflik',
|
||||
'migration.report.warnings': 'Peringatan',
|
||||
'migration.report.previewHint':
|
||||
'Belum ada data yang diimpor. Klik Terapkan impor untuk menyalinnya.',
|
||||
'migration.report.appliedHint':
|
||||
'Entri yang diimpor kini ada di memori Anda. Jalankan Pratinjau lagi untuk membandingkan.',
|
||||
'migration.hermesComingSoonPrefix': 'Pengimpor Hermes ada di peta jalan — lihat ',
|
||||
'migration.hermesComingSoonSuffix':
|
||||
' untuk konteks. Pilih OpenClaw untuk migrasi hari ini; Hermes menyusul.',
|
||||
'migration.hermesLinkText': '#1440',
|
||||
'migration.confirmImport.singular':
|
||||
'Impor {count} entri ke ruang kerja saat ini?\n\nSumber: {source}\nTujuan: {target}\n\nMemori yang ada akan dicadangkan sebelum impor dimulai.',
|
||||
'migration.confirmImport.plural':
|
||||
'Impor {count} entri ke ruang kerja saat ini?\n\nSumber: {source}\nTujuan: {target}\n\nMemori yang ada akan dicadangkan sebelum impor dimulai.',
|
||||
};
|
||||
|
||||
export default id1;
|
||||
|
||||
@@ -364,6 +364,9 @@ const id4: TranslationMap = {
|
||||
'settings.billing.subscription.paymentConfirmed': 'Pembayaran dikonfirmasi',
|
||||
'settings.billing.subscription.perMonth': 'Per bulan',
|
||||
'settings.billing.subscription.popular': 'Populer',
|
||||
'pages.settings.account.migration': 'Impor dari asisten lain',
|
||||
'pages.settings.account.migrationDesc':
|
||||
'Migrasikan memori dan catatan dari OpenClaw (dan, segera, Hermes) ke ruang kerja ini.',
|
||||
};
|
||||
|
||||
export default id4;
|
||||
|
||||
@@ -387,6 +387,43 @@ const it1: TranslationMap = {
|
||||
'settings.about.releasesDesc': 'Sfoglia le note di rilascio e le build precedenti su GitHub.',
|
||||
'settings.about.openReleases': 'Apri release GitHub',
|
||||
'settings.ai.overview': 'Panoramica sistema AI',
|
||||
'migration.title': 'Importa da un altro assistente',
|
||||
'migration.description':
|
||||
'Migra memoria e note da un altro assistente locale in questo spazio di lavoro. Inizia con Anteprima per vedere esattamente cosa cambierebbe, poi premi Applica per copiare i dati. La memoria attuale viene salvata in backup per prima.',
|
||||
'migration.vendorLabel': 'Provider di origine',
|
||||
'migration.sourceLabel': 'Percorso dello spazio di lavoro di origine (facoltativo)',
|
||||
'migration.sourcePlaceholder':
|
||||
'Lascia vuoto per rilevamento automatico (es. ~/.openclaw/workspace)',
|
||||
'migration.sourceHint':
|
||||
'Vuoto usa la posizione predefinita del provider. Inserisci un percorso esplicito se hai spostato lo spazio di lavoro.',
|
||||
'migration.previewAction': 'Anteprima',
|
||||
'migration.previewRunning': 'Anteprima in corso…',
|
||||
'migration.applyAction': "Applica l'import",
|
||||
'migration.applyRunning': 'Importazione in corso…',
|
||||
'migration.applyDisclaimer':
|
||||
"Applica si sblocca dopo un'Anteprima riuscita della stessa origine. La memoria esistente viene salvata in backup prima di qualsiasi import.",
|
||||
'migration.reportTitlePreview': 'Anteprima — nessun import effettuato',
|
||||
'migration.reportTitleApplied': 'Importazione completata',
|
||||
'migration.report.source': 'Spazio di lavoro di origine',
|
||||
'migration.report.target': 'Spazio di lavoro di destinazione',
|
||||
'migration.report.fromSqlite': 'Da SQLite (brain.db)',
|
||||
'migration.report.fromMarkdown': 'Da Markdown',
|
||||
'migration.report.imported': 'Importati',
|
||||
'migration.report.skippedUnchanged': 'Saltati (non modificati)',
|
||||
'migration.report.renamedConflicts': 'Rinominati in caso di conflitto',
|
||||
'migration.report.warnings': 'Avvisi',
|
||||
'migration.report.previewHint':
|
||||
"Nessun dato è ancora stato importato. Premi Applica l'import per copiarlo.",
|
||||
'migration.report.appliedHint':
|
||||
'Le voci importate sono ora nella tua memoria. Riesegui Anteprima per confrontare di nuovo.',
|
||||
'migration.hermesComingSoonPrefix': "L'importatore Hermes è nella roadmap — vedi ",
|
||||
'migration.hermesComingSoonSuffix':
|
||||
' per il contesto. Usa OpenClaw per migrare oggi; Hermes arriverà in una release successiva.',
|
||||
'migration.hermesLinkText': '#1440',
|
||||
'migration.confirmImport.singular':
|
||||
"Importare {count} voce nello spazio di lavoro attuale?\n\nOrigine: {source}\nDestinazione: {target}\n\nLa memoria esistente verrà salvata in backup prima dell'import.",
|
||||
'migration.confirmImport.plural':
|
||||
"Importare {count} voci nello spazio di lavoro attuale?\n\nOrigine: {source}\nDestinazione: {target}\n\nLa memoria esistente verrà salvata in backup prima dell'import.",
|
||||
};
|
||||
|
||||
export default it1;
|
||||
|
||||
@@ -365,6 +365,9 @@ const it4: TranslationMap = {
|
||||
'settings.billing.subscription.paymentConfirmed': 'Pagamento confermato',
|
||||
'settings.billing.subscription.perMonth': 'Al mese',
|
||||
'settings.billing.subscription.popular': 'Popolare',
|
||||
'pages.settings.account.migration': 'Importa da un altro assistente',
|
||||
'pages.settings.account.migrationDesc':
|
||||
'Migra memoria e note da OpenClaw (e presto Hermes) in questo spazio di lavoro.',
|
||||
};
|
||||
|
||||
export default it4;
|
||||
|
||||
@@ -392,6 +392,43 @@ const pt1: TranslationMap = {
|
||||
'Navegar pelas notas de versão e compilações anteriores no GitHub.',
|
||||
'settings.about.openReleases': 'Abrir versões no GitHub',
|
||||
'settings.ai.overview': 'Visão Geral do Sistema de IA',
|
||||
'migration.title': 'Importar de outro assistente',
|
||||
'migration.description':
|
||||
'Migre memória e anotações de outro assistente local para este espaço de trabalho. Comece com uma Pré-visualização para ver exatamente o que mudaria, depois clique em Aplicar para copiar os dados. Sua memória atual é salva primeiro.',
|
||||
'migration.vendorLabel': 'Fornecedor de origem',
|
||||
'migration.sourceLabel': 'Caminho do espaço de trabalho de origem (opcional)',
|
||||
'migration.sourcePlaceholder':
|
||||
'Deixe em branco para detecção automática (ex.: ~/.openclaw/workspace)',
|
||||
'migration.sourceHint':
|
||||
'Em branco usa o local padrão do fornecedor. Informe um caminho explícito se você moveu o espaço de trabalho.',
|
||||
'migration.previewAction': 'Pré-visualizar',
|
||||
'migration.previewRunning': 'Pré-visualizando…',
|
||||
'migration.applyAction': 'Aplicar importação',
|
||||
'migration.applyRunning': 'Importando…',
|
||||
'migration.applyDisclaimer':
|
||||
'Aplicar é desbloqueado após uma Pré-visualização bem-sucedida da mesma origem. A memória existente é salva antes de qualquer importação.',
|
||||
'migration.reportTitlePreview': 'Pré-visualização — nada importado ainda',
|
||||
'migration.reportTitleApplied': 'Importação concluída',
|
||||
'migration.report.source': 'Espaço de trabalho de origem',
|
||||
'migration.report.target': 'Espaço de trabalho de destino',
|
||||
'migration.report.fromSqlite': 'Do SQLite (brain.db)',
|
||||
'migration.report.fromMarkdown': 'Do Markdown',
|
||||
'migration.report.imported': 'Importados',
|
||||
'migration.report.skippedUnchanged': 'Ignorados (sem alteração)',
|
||||
'migration.report.renamedConflicts': 'Renomeados em caso de conflito',
|
||||
'migration.report.warnings': 'Avisos',
|
||||
'migration.report.previewHint':
|
||||
'Nenhum dado foi importado ainda. Clique em Aplicar importação para copiar.',
|
||||
'migration.report.appliedHint':
|
||||
'As entradas importadas já estão na sua memória. Execute Pré-visualizar novamente para comparar.',
|
||||
'migration.hermesComingSoonPrefix': 'O importador Hermes está no roteiro — veja ',
|
||||
'migration.hermesComingSoonSuffix':
|
||||
' para contexto. Use OpenClaw para migrar hoje; o Hermes chega em uma próxima entrega.',
|
||||
'migration.hermesLinkText': '#1440',
|
||||
'migration.confirmImport.singular':
|
||||
'Importar {count} entrada para o espaço de trabalho atual?\n\nOrigem: {source}\nDestino: {target}\n\nA memória existente será salva antes da importação.',
|
||||
'migration.confirmImport.plural':
|
||||
'Importar {count} entradas para o espaço de trabalho atual?\n\nOrigem: {source}\nDestino: {target}\n\nA memória existente será salva antes da importação.',
|
||||
};
|
||||
|
||||
export default pt1;
|
||||
|
||||
@@ -365,6 +365,9 @@ const pt4: TranslationMap = {
|
||||
'settings.billing.subscription.paymentConfirmed': 'Pagamento confirmado',
|
||||
'settings.billing.subscription.perMonth': 'Por mês',
|
||||
'settings.billing.subscription.popular': 'Popular',
|
||||
'pages.settings.account.migration': 'Importar de outro assistente',
|
||||
'pages.settings.account.migrationDesc':
|
||||
'Migre memória e anotações do OpenClaw (e, em breve, do Hermes) para este espaço de trabalho.',
|
||||
};
|
||||
|
||||
export default pt4;
|
||||
|
||||
@@ -382,6 +382,43 @@ const ru1: TranslationMap = {
|
||||
'settings.about.releasesDesc': 'Просмотр заметок к релизам и предыдущих сборок на GitHub.',
|
||||
'settings.about.openReleases': 'Открыть релизы на GitHub',
|
||||
'settings.ai.overview': 'Обзор AI-системы',
|
||||
'migration.title': 'Импорт из другого ассистента',
|
||||
'migration.description':
|
||||
'Перенесите память и заметки из другого локального ассистента в это рабочее пространство. Сначала запустите «Предпросмотр», чтобы увидеть, что изменится, затем нажмите «Применить», чтобы скопировать данные. Текущая память сохраняется в резервную копию первой.',
|
||||
'migration.vendorLabel': 'Источник',
|
||||
'migration.sourceLabel': 'Путь к исходному рабочему пространству (необязательно)',
|
||||
'migration.sourcePlaceholder':
|
||||
'Оставьте пустым для автоопределения (например, ~/.openclaw/workspace)',
|
||||
'migration.sourceHint':
|
||||
'Если пусто, используется стандартное расположение источника. Укажите явный путь, если вы перенесли рабочее пространство.',
|
||||
'migration.previewAction': 'Предпросмотр',
|
||||
'migration.previewRunning': 'Предпросмотр…',
|
||||
'migration.applyAction': 'Применить импорт',
|
||||
'migration.applyRunning': 'Импорт…',
|
||||
'migration.applyDisclaimer':
|
||||
'«Применить» разблокируется только после успешного предпросмотра того же источника. Существующая память сохраняется перед любым импортом.',
|
||||
'migration.reportTitlePreview': 'Предпросмотр — ничего ещё не импортировано',
|
||||
'migration.reportTitleApplied': 'Импорт завершён',
|
||||
'migration.report.source': 'Исходное пространство',
|
||||
'migration.report.target': 'Целевое пространство',
|
||||
'migration.report.fromSqlite': 'Из SQLite (brain.db)',
|
||||
'migration.report.fromMarkdown': 'Из Markdown',
|
||||
'migration.report.imported': 'Импортировано',
|
||||
'migration.report.skippedUnchanged': 'Пропущено (без изменений)',
|
||||
'migration.report.renamedConflicts': 'Переименовано при конфликте',
|
||||
'migration.report.warnings': 'Предупреждения',
|
||||
'migration.report.previewHint':
|
||||
'Данные ещё не импортированы. Нажмите «Применить импорт», чтобы скопировать.',
|
||||
'migration.report.appliedHint':
|
||||
'Импортированные записи теперь в вашей памяти. Запустите «Предпросмотр» снова для сравнения.',
|
||||
'migration.hermesComingSoonPrefix': 'Импортёр Hermes в дорожной карте — см. ',
|
||||
'migration.hermesComingSoonSuffix':
|
||||
' для контекста. Сегодня используйте OpenClaw для переноса; Hermes появится позже.',
|
||||
'migration.hermesLinkText': '#1440',
|
||||
'migration.confirmImport.singular':
|
||||
'Импортировать {count} запись в текущее рабочее пространство?\n\nИсточник: {source}\nЦель: {target}\n\nПеред импортом будет сохранена резервная копия памяти.',
|
||||
'migration.confirmImport.plural':
|
||||
'Импортировать {count} записей в текущее рабочее пространство?\n\nИсточник: {source}\nЦель: {target}\n\nПеред импортом будет сохранена резервная копия памяти.',
|
||||
};
|
||||
|
||||
export default ru1;
|
||||
|
||||
@@ -363,6 +363,9 @@ const ru4: TranslationMap = {
|
||||
'settings.billing.subscription.paymentConfirmed': 'Оплата подтверждена',
|
||||
'settings.billing.subscription.perMonth': 'В месяц',
|
||||
'settings.billing.subscription.popular': 'Популярное',
|
||||
'pages.settings.account.migration': 'Импорт из другого ассистента',
|
||||
'pages.settings.account.migrationDesc':
|
||||
'Перенесите память и заметки из OpenClaw (а вскоре и Hermes) в это рабочее пространство.',
|
||||
};
|
||||
|
||||
export default ru4;
|
||||
|
||||
@@ -364,6 +364,38 @@ const zhCN1: TranslationMap = {
|
||||
'settings.about.releasesDesc': '在 GitHub 上浏览发布说明和早期版本。',
|
||||
'settings.about.openReleases': '打开 GitHub 发布',
|
||||
'settings.ai.overview': 'AI 系统概览',
|
||||
'migration.title': '从其他助手导入',
|
||||
'migration.description':
|
||||
'将记忆和笔记从另一个本地助手迁移到此工作区。先点击「预览」查看将要变更的内容,然后点击「应用」复制数据。当前的记忆会先备份。',
|
||||
'migration.vendorLabel': '来源助手',
|
||||
'migration.sourceLabel': '来源工作区路径(可选)',
|
||||
'migration.sourcePlaceholder': '留空可自动检测(例如 ~/.openclaw/workspace)',
|
||||
'migration.sourceHint':
|
||||
'留空时使用该助手的默认位置。如果你已将工作区移到其他位置,请填写明确路径。',
|
||||
'migration.previewAction': '预览',
|
||||
'migration.previewRunning': '正在预览…',
|
||||
'migration.applyAction': '应用导入',
|
||||
'migration.applyRunning': '正在导入…',
|
||||
'migration.applyDisclaimer': '只有对同一来源完成预览后才能应用。导入前会自动备份现有记忆。',
|
||||
'migration.reportTitlePreview': '预览 — 尚未导入任何数据',
|
||||
'migration.reportTitleApplied': '导入完成',
|
||||
'migration.report.source': '来源工作区',
|
||||
'migration.report.target': '目标工作区',
|
||||
'migration.report.fromSqlite': '来自 SQLite (brain.db)',
|
||||
'migration.report.fromMarkdown': '来自 Markdown',
|
||||
'migration.report.imported': '已导入',
|
||||
'migration.report.skippedUnchanged': '已跳过(未变更)',
|
||||
'migration.report.renamedConflicts': '冲突时已重命名',
|
||||
'migration.report.warnings': '警告',
|
||||
'migration.report.previewHint': '尚未导入任何数据。点击「应用导入」开始复制。',
|
||||
'migration.report.appliedHint': '导入的条目已加入你的记忆。如需再次比较,请重新运行预览。',
|
||||
'migration.hermesComingSoonPrefix': 'Hermes 导入器仍在规划中 — 详见 ',
|
||||
'migration.hermesComingSoonSuffix': '。今天可以先用 OpenClaw 迁移;Hermes 将在后续版本中加入。',
|
||||
'migration.hermesLinkText': '#1440',
|
||||
'migration.confirmImport.singular':
|
||||
'将 {count} 条数据导入当前工作区?\n\n来源:{source}\n目标:{target}\n\n导入前会先备份现有记忆。',
|
||||
'migration.confirmImport.plural':
|
||||
'将 {count} 条数据导入当前工作区?\n\n来源:{source}\n目标:{target}\n\n导入前会先备份现有记忆。',
|
||||
};
|
||||
|
||||
export default zhCN1;
|
||||
|
||||
@@ -359,6 +359,9 @@ const zhCN4: TranslationMap = {
|
||||
'settings.billing.subscription.paymentConfirmed': '支付已确认',
|
||||
'settings.billing.subscription.perMonth': '每月',
|
||||
'settings.billing.subscription.popular': '热门',
|
||||
'pages.settings.account.migration': '从其他助手导入',
|
||||
'pages.settings.account.migrationDesc':
|
||||
'将 OpenClaw(即将支持 Hermes)的记忆和笔记迁移到此工作区。',
|
||||
};
|
||||
|
||||
export default zhCN4;
|
||||
|
||||
+40
-1
@@ -99,6 +99,42 @@ const en: TranslationMap = {
|
||||
'settings.account.connectionsDesc': 'Manage linked accounts and services',
|
||||
'settings.account.privacy': 'Privacy',
|
||||
'settings.account.privacyDesc': 'Control what data leaves your computer',
|
||||
'migration.title': 'Import from another assistant',
|
||||
'migration.description':
|
||||
'Migrate memory and notes from another local assistant into this workspace. Start with a Preview to see exactly what would change, then Apply to copy the data over. Your current memory is backed up first.',
|
||||
'migration.vendorLabel': 'Source vendor',
|
||||
'migration.sourceLabel': 'Source workspace path (optional)',
|
||||
'migration.sourcePlaceholder': 'Leave blank to auto-detect (e.g. ~/.openclaw/workspace)',
|
||||
'migration.sourceHint':
|
||||
"Defaults to the vendor's standard location when blank. Set an explicit path if you've moved the workspace elsewhere.",
|
||||
'migration.previewAction': 'Preview',
|
||||
'migration.previewRunning': 'Previewing…',
|
||||
'migration.applyAction': 'Apply import',
|
||||
'migration.applyRunning': 'Importing…',
|
||||
'migration.applyDisclaimer':
|
||||
'Apply is unlocked after a successful Preview of the same source. Existing memory is backed up before any import.',
|
||||
'migration.reportTitlePreview': 'Preview — nothing imported yet',
|
||||
'migration.reportTitleApplied': 'Import complete',
|
||||
'migration.report.source': 'Source workspace',
|
||||
'migration.report.target': 'Target workspace',
|
||||
'migration.report.fromSqlite': 'From SQLite (brain.db)',
|
||||
'migration.report.fromMarkdown': 'From Markdown',
|
||||
'migration.report.imported': 'Imported',
|
||||
'migration.report.skippedUnchanged': 'Skipped (unchanged)',
|
||||
'migration.report.renamedConflicts': 'Renamed on conflict',
|
||||
'migration.report.warnings': 'Warnings',
|
||||
'migration.report.previewHint':
|
||||
'No data has been imported yet. Click Apply import to copy it over.',
|
||||
'migration.report.appliedHint':
|
||||
'Imported entries are now in your memory. Re-run Preview if you want to compare again.',
|
||||
'migration.hermesComingSoonPrefix': 'Hermes importer is on the roadmap — see ',
|
||||
'migration.hermesComingSoonSuffix':
|
||||
' for context. Pick OpenClaw to migrate today; Hermes lands in a follow-up.',
|
||||
'migration.hermesLinkText': '#1440',
|
||||
'migration.confirmImport.singular':
|
||||
'Import {count} entry into the current workspace?\n\nSource: {source}\nTarget: {target}\n\nExisting memory will be backed up before the import runs.',
|
||||
'migration.confirmImport.plural':
|
||||
'Import {count} entries into the current workspace?\n\nSource: {source}\nTarget: {target}\n\nExisting memory will be backed up before the import runs.',
|
||||
|
||||
// Settings: Notifications
|
||||
'settings.notifications.doNotDisturb': 'Do Not Disturb',
|
||||
@@ -1457,7 +1493,10 @@ const en: TranslationMap = {
|
||||
'overlay.ariaVoiceActive': 'Voice input active',
|
||||
'overlay.orbTitle': 'Drag to move · Double-click to reset position',
|
||||
'pages.settings.account.connections': 'Connections',
|
||||
'pages.settings.account.connectionsDesc': 'Manage linked accounts and services',
|
||||
'pages.settings.account.connectionsDesc': 'Review and manage linked account connections',
|
||||
'pages.settings.account.migration': 'Import from another assistant',
|
||||
'pages.settings.account.migrationDesc':
|
||||
'Bring memory and notes over from OpenClaw (Hermes coming soon)',
|
||||
'pages.settings.account.privacy': 'Privacy',
|
||||
'pages.settings.account.privacyDesc': 'Control what data leaves your computer',
|
||||
'pages.settings.account.recoveryPhrase': 'Recovery phrase',
|
||||
|
||||
@@ -18,6 +18,7 @@ import MascotPanel from '../components/settings/panels/MascotPanel';
|
||||
import MemoryDataPanel from '../components/settings/panels/MemoryDataPanel';
|
||||
import MemoryDebugPanel from '../components/settings/panels/MemoryDebugPanel';
|
||||
import MessagingPanel from '../components/settings/panels/MessagingPanel';
|
||||
import MigrationPanel from '../components/settings/panels/MigrationPanel';
|
||||
import NotificationRoutingPanel from '../components/settings/panels/NotificationRoutingPanel';
|
||||
import NotificationsPanel from '../components/settings/panels/NotificationsPanel';
|
||||
import PrivacyPanel from '../components/settings/panels/PrivacyPanel';
|
||||
@@ -80,6 +81,16 @@ const PrivacyIcon = (
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
const MigrationIcon = (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
const ScreenIcon = (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
@@ -200,6 +211,13 @@ const Settings = () => {
|
||||
route: 'privacy',
|
||||
icon: PrivacyIcon,
|
||||
},
|
||||
{
|
||||
id: 'migration',
|
||||
title: t('pages.settings.account.migration'),
|
||||
description: t('pages.settings.account.migrationDesc'),
|
||||
route: 'migration',
|
||||
icon: MigrationIcon,
|
||||
},
|
||||
];
|
||||
|
||||
const featuresSettingsItems = [
|
||||
@@ -303,6 +321,7 @@ const Settings = () => {
|
||||
{/* BillingPanel intentionally uses its own wider layout. */}
|
||||
<Route path="billing" element={<BillingPanel />} />
|
||||
<Route path="privacy" element={wrapSettingsPage(<PrivacyPanel />)} />
|
||||
<Route path="migration" element={wrapSettingsPage(<MigrationPanel />)} />
|
||||
{/* Features leaf panels */}
|
||||
<Route path="screen-intelligence" element={wrapSettingsPage(<ScreenIntelligencePanel />)} />
|
||||
<Route path="autocomplete" element={wrapSettingsPage(<AutocompletePanel />)} />
|
||||
|
||||
@@ -472,6 +472,7 @@ Canonical mapping of every product feature to its test source(s). Drives gap-fil
|
||||
| 13.5.1 | Clear App Data | WD | `app/test/e2e/specs/settings-data-management.spec.ts` | ✅ | Destructive — confirm-then-reset |
|
||||
| 13.5.2 | Cache Reset | WD | `app/test/e2e/specs/settings-data-management.spec.ts` | ✅ | |
|
||||
| 13.5.3 | Full State Reset | WD | `app/test/e2e/specs/settings-data-management.spec.ts` | ✅ | Restart-and-verify fresh-install state |
|
||||
| 13.5.4 | Migration from another assistant (OpenClaw) | VU+RU | `app/src/components/settings/panels/__tests__/MigrationPanel.test.tsx` (this PR), `src/openhuman/migration/ops.rs` (existing) | ✅ | Was ❌ — UI now wraps the existing `openhuman.migrate_openclaw` RPC with preview-then-apply + confirm. Hermes tracked as follow-up under #1440 (#1440) |
|
||||
|
||||
---
|
||||
|
||||
@@ -479,9 +480,9 @@ Canonical mapping of every product feature to its test source(s). Drives gap-fil
|
||||
|
||||
| Status | Count |
|
||||
| ---------------- | ------------------------------------------------ |
|
||||
| ✅ Covered | 65 |
|
||||
| ✅ Covered | 66 |
|
||||
| 🟡 Partial | 27 |
|
||||
| ❌ Missing | 27 |
|
||||
| ❌ Missing | 26 |
|
||||
| 🚫 Manual smoke | 11 |
|
||||
| **Total leaves** | **130 explicit + nested = 201 product features** |
|
||||
|
||||
|
||||
@@ -457,12 +457,22 @@ fn create_memory_full(
|
||||
|
||||
/// Create a memory instance specifically for migration purposes.
|
||||
///
|
||||
/// NOTE: This is currently disabled for the unified namespace memory core.
|
||||
/// The unified namespace memory core has a single workspace-scoped
|
||||
/// store, so migration writes into the same `UnifiedMemory` instance the
|
||||
/// rest of the app reads from — there is no separate "migration
|
||||
/// backend". This helper delegates to [`create_memory`] so the
|
||||
/// migration importer (`migrate_openclaw_memory`) gets a real, writable
|
||||
/// memory handle and the Apply path can actually run end-to-end.
|
||||
///
|
||||
/// Prior to #1440 this function unconditionally bailed with "memory
|
||||
/// migration is disabled for the unified namespace memory core", which
|
||||
/// left the OpenClaw importer broken even though the rest of the
|
||||
/// pipeline (source discovery, dry-run report, backup) worked.
|
||||
pub fn create_memory_for_migration(
|
||||
_backend: &str,
|
||||
_workspace_dir: &Path,
|
||||
config: &MemoryConfig,
|
||||
workspace_dir: &Path,
|
||||
) -> anyhow::Result<Box<dyn Memory>> {
|
||||
anyhow::bail!("memory migration is disabled for the unified namespace memory core")
|
||||
create_memory(config, workspace_dir)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -619,16 +629,19 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_memory_for_migration_always_errors() {
|
||||
fn create_memory_for_migration_returns_writable_memory_on_unified_core() {
|
||||
// Regression for #1440: prior to that PR this factory unconditionally
|
||||
// bailed with "memory migration is disabled for the unified namespace
|
||||
// memory core", which broke the OpenClaw importer's Apply path even
|
||||
// though the dry-run / preview path worked. Now it delegates to
|
||||
// `create_memory` so the migration importer gets a real workspace-
|
||||
// scoped memory handle. Box<dyn Memory> doesn't impl Debug, so we
|
||||
// match instead of unwrap.
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
// Box<dyn Memory> doesn't impl Debug, so we can't use .unwrap_err().
|
||||
// Use match instead.
|
||||
match create_memory_for_migration("any", tmp.path()) {
|
||||
Ok(_) => panic!("expected error"),
|
||||
Err(e) => assert!(
|
||||
e.to_string().contains("migration is disabled"),
|
||||
"unexpected error: {e}"
|
||||
),
|
||||
let cfg = MemoryConfig::default();
|
||||
match create_memory_for_migration(&cfg, tmp.path()) {
|
||||
Ok(_) => {}
|
||||
Err(e) => panic!("expected Ok for unified namespace core, got: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -118,7 +118,7 @@ pub async fn migrate_openclaw_memory(
|
||||
}
|
||||
|
||||
fn target_memory_backend(config: &Config) -> Result<Box<dyn Memory>> {
|
||||
memory::create_memory_for_migration(&config.memory.backend, &config.workspace_dir)
|
||||
memory::create_memory_for_migration(&config.memory, &config.workspace_dir)
|
||||
}
|
||||
|
||||
fn collect_source_entries(
|
||||
|
||||
@@ -54,6 +54,40 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn migrate_openclaw_apply_imports_markdown_entries_into_target_workspace() {
|
||||
// Regression for #1440: prior to this PR the Apply path
|
||||
// (`dry_run = false`) bailed at `create_memory_for_migration`
|
||||
// because the unified namespace memory core hard-disabled it.
|
||||
// With the disable removed, Apply must actually move markdown
|
||||
// entries from the OpenClaw source workspace into the target.
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let config = test_config(&tmp);
|
||||
|
||||
// Fake OpenClaw workspace with two markdown entries — no
|
||||
// brain.db needed; the migration path reads MEMORY.md + any
|
||||
// memory/*.md files.
|
||||
let source = tmp.path().join("openclaw-src");
|
||||
std::fs::create_dir_all(source.join("memory")).unwrap();
|
||||
std::fs::write(source.join("MEMORY.md"), "# Top-level note\nimport me").unwrap();
|
||||
std::fs::write(
|
||||
source.join("memory").join("sprint.md"),
|
||||
"# Sprint plan\nweek one design",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let outcome = migrate_openclaw(&config, Some(source), false)
|
||||
.await
|
||||
.expect("apply path should succeed on the unified core after #1440");
|
||||
let report = outcome.value;
|
||||
assert!(!report.dry_run, "apply must produce a non-dry-run report");
|
||||
assert!(
|
||||
report.stats.imported >= 1,
|
||||
"apply must import at least one entry; stats={:?}",
|
||||
report.stats
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn migrate_openclaw_returns_error_for_missing_source_workspace() {
|
||||
// Pointing at a non-existent source directory must surface as
|
||||
|
||||
Reference in New Issue
Block a user