Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion crates/agent-gui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { WindowsSettingsHeader } from "./components/WindowsSettingsHeader";
import { WindowsTitleBar } from "./components/WindowsTitleBar";
import { useAppUpdateController } from "./lib/appUpdates";
import { isKBrainBrowserHost } from "./lib/host";
import { HistoryMigrationNoticeBanner } from "./lib/kbrain/HistoryMigrationNoticeBanner";
import { requestPlanning } from "./lib/planning/kbrain";
import { TimeZoneNoticeBanner } from "./lib/planning/TimeZoneNoticeBanner";
import { setRetryErrorExtension } from "./lib/providers/runtime/streamRetry";
Expand Down Expand Up @@ -799,7 +800,14 @@ export default function App() {
return (
<LocaleContext.Provider value={localeContextValue}>
<Toaster />
<AppChrome notice={<TimeZoneNoticeBanner onOpenSettings={() => openSettings("system")} />}>
<AppChrome
notice={
<>
<HistoryMigrationNoticeBanner onOpenSettings={() => openSettings("system")} />
<TimeZoneNoticeBanner onOpenSettings={() => openSettings("system")} />
</>
}
>
{backgroundHostsReady ? (
<Suspense fallback={null}>
<CronPromptRunner settings={settings} />
Expand Down
34 changes: 34 additions & 0 deletions crates/agent-gui/src/i18n/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,22 @@ export const GUI_TRANSLATION_OVERRIDES: Record<Locale, Record<string, string>> =
"historyImport.empty": "未发现可导入的旧对话;已明确删除的记录会跳过。",
"historyImport.partial": "{count} 条对话正文已导入,但文件回滚记录尚不完整,可稍后重试。",
"historyImport.failures": "查看失败详情(可再次点击导入重试)",
"historyImport.failedList": "{count} 条旧对话没能迁移",
"historyImport.reason.too_large":
"对话超过当前 K-brain 的单次导入大小限制。升级后端后,可点「导入旧版对话」重试。",
"historyImport.reason.conflict": "K-brain 里已有同一条对话,但内容不一样,K-brain 拒绝覆盖。",
"historyImport.reason.rejected": "K-brain 认为这条对话的数据无效,拒绝导入。",
"historyImport.reason.transient": "连接中断或后端出错,下次启动会自动重试。",
"historyImport.notice.title": "{count} 条旧对话没能迁移到 K-brain",
"historyImport.notice.description":
"这些对话在当前版本里看不到。旧数据没有删除,启动时也不会再反复上传。",
"historyImport.notice.view": "查看详情",
"historyImport.notice.dismiss": "关闭提示",
"historyImport.notice.dismissTitle": "关闭这条提示?",
"historyImport.notice.dismissDescription":
"关闭后不再提示这些对话的迁移失败,此操作不可撤销。设置里的失败清单仍会保留;对话内容变化后如果再次失败,会重新提示。",
"historyImport.notice.dismissConfirm": "关闭并不再提示",
"historyImport.notice.cancel": "取消",
"app.windowPinned": "已置顶",
"app.windowPinnedHint": "窗口已置顶,浮在其他应用上方。点击取消置顶。",
"app.settingsLoadFailed": "加载设置失败,已回退到默认配置。",
Expand Down Expand Up @@ -330,6 +346,24 @@ export const GUI_TRANSLATION_OVERRIDES: Record<Locale, Record<string, string>> =
"historyImport.partial":
"{count} conversation transcripts were imported, but file rollback records are incomplete. You can retry later.",
"historyImport.failures": "View failures (click Import again to retry)",
"historyImport.failedList": "{count} old conversation(s) could not be migrated",
"historyImport.reason.too_large":
"The conversation exceeds the current K-brain import size limit. After upgrading the backend, retry with Import old conversations.",
"historyImport.reason.conflict":
"K-brain already holds this conversation with different content and refused to overwrite it.",
"historyImport.reason.rejected": "K-brain rejected this conversation's data as invalid.",
"historyImport.reason.transient":
"The connection dropped or the backend failed; it is retried on the next launch.",
"historyImport.notice.title": "{count} old conversation(s) were not migrated into K-brain",
"historyImport.notice.description":
"They are not visible in this version. The old data is kept and is no longer re-uploaded on every launch.",
"historyImport.notice.view": "View details",
"historyImport.notice.dismiss": "Dismiss",
"historyImport.notice.dismissTitle": "Dismiss this notice?",
"historyImport.notice.dismissDescription":
"These migration failures will no longer be announced. This cannot be undone. Settings keeps the list; a conversation that changes and fails again is announced anew.",
"historyImport.notice.dismissConfirm": "Dismiss permanently",
"historyImport.notice.cancel": "Cancel",
"app.windowPinned": "Pinned",
"app.windowPinnedHint": "Window is pinned above other apps. Click to unpin.",
"app.settingsLoadFailed": "Failed to load settings. Default settings have been restored.",
Expand Down
66 changes: 66 additions & 0 deletions crates/agent-gui/src/lib/kbrain/HistoryMigrationNoticeBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { X } from "@liveagent/ui/components/IconSet";
import { SettingsNotice } from "@liveagent/ui/components/settings/SettingsNotice";
import { Button } from "@liveagent/ui/components/ui/button";
import { useConfirmDialog } from "@liveagent/ui/components/ui/confirm-dialog";
import { useLocale } from "@liveagent/ui/i18n";
import { useSyncExternalStore } from "react";
import {
dismissHistoryMigrationFailures,
getHistoryMigrationFailures,
subscribeHistoryMigrationFailures,
} from "./historyMigrationFailures";
import { kBrainStorageScope } from "./mapping";

/** App-level notice for legacy conversations that could not be migrated into K-brain. */
export function HistoryMigrationNoticeBanner({ onOpenSettings }: { onOpenSettings: () => void }) {
const { t } = useLocale();
const scope = kBrainStorageScope();
const { undismissed } = useSyncExternalStore(subscribeHistoryMigrationFailures, () =>
getHistoryMigrationFailures(scope),
);
const { confirm, dialog } = useConfirmDialog();
if (undismissed.length === 0) return dialog;

async function dismiss() {
const confirmed = await confirm({
title: t("historyImport.notice.dismissTitle"),
description: t("historyImport.notice.dismissDescription"),
confirmLabel: t("historyImport.notice.dismissConfirm"),
cancelLabel: t("historyImport.notice.cancel"),
preferCancel: true,
});
if (confirmed) dismissHistoryMigrationFailures(scope, undismissed);
}

return (
<>
<SettingsNotice
variant="warning"
role="status"
aria-live="polite"
data-testid="history-migration-notice"
className="relative m-2 flex shrink-0 flex-wrap items-center gap-x-3 gap-y-2 pr-9"
>
<div className="min-w-0 flex-1 basis-64 break-words">
<p className="font-medium">
{t("historyImport.notice.title").replace("{count}", String(undismissed.length))}
</p>
<p>{t("historyImport.notice.description")}</p>
</div>
<Button variant="ghost" size="sm" onClick={onOpenSettings}>
{t("historyImport.notice.view")}
</Button>
<button
type="button"
className="absolute top-1.5 right-1.5 rounded p-1 opacity-70 has-hover:hover:bg-amber-500/10 has-hover:hover:opacity-100"
aria-label={t("historyImport.notice.dismiss")}
title={t("historyImport.notice.dismiss")}
onClick={() => void dismiss()}
>
<X className="size-3.5" />
</button>
</SettingsNotice>
{dialog}
</>
);
}
67 changes: 54 additions & 13 deletions crates/agent-gui/src/lib/kbrain/LegacyHistoryImportSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,53 @@ import { useLocale } from "@liveagent/ui/i18n";
import { SettingsGroup, SettingsRow } from "@liveagent/ui/pages/settings/shared";
import { useSyncExternalStore } from "react";
import { isTauriHost } from "../host";
import {
getHistoryMigrationFailures,
type HistoryMigrationFailureRecord,
subscribeHistoryMigrationFailures,
} from "./historyMigrationFailures";
import {
getHistoryImportState,
importOldConversations,
subscribeHistoryImport,
} from "./manualHistoryImport";
import { kBrainStorageScope } from "./mapping";

export function formatMigrationBytes(bytes: number | undefined) {
return bytes === undefined ? "" : `${(bytes / 1048576).toFixed(1)} MB`;
}

export function HistoryMigrationFailureList({
records,
}: {
records: readonly HistoryMigrationFailureRecord[];
}) {
const { t } = useLocale();
return (
<ul className="max-h-60 space-y-1.5 overflow-y-auto" data-testid="history-migration-failures">
{records.map((record) => (
<li key={record.sourceId} className="break-words">
<span className="font-medium">{record.title || record.sourceId}</span>
<span className="text-muted-foreground"> · {record.sourceId.slice(0, 8)}</span>
{record.bytes !== undefined && (
<span className="text-muted-foreground"> · {formatMigrationBytes(record.bytes)}</span>
)}
<div className="text-muted-foreground text-xs">
{t(`historyImport.reason.${record.kind}`)}
</div>
</li>
))}
</ul>
);
}

export function LegacyHistoryImportSettings() {
const { t } = useLocale();
const state = useSyncExternalStore(subscribeHistoryImport, getHistoryImportState);
const scope = kBrainStorageScope();
const recorded = useSyncExternalStore(subscribeHistoryMigrationFailures, () =>
getHistoryMigrationFailures(scope),
);
const desktop = isTauriHost();
const result = state.result;
const imported = result?.results.filter((item) => item.status === "imported").length ?? 0;
Expand Down Expand Up @@ -55,21 +93,24 @@ export function LegacyHistoryImportSettings() {
{incomplete > 0 && (
<p>{t("historyImport.partial").replace("{count}", String(incomplete))}</p>
)}
{result.failures.length > 0 && (
<details>
<summary>{t("historyImport.failures")}</summary>
<ul className="max-h-48 overflow-y-auto">
{result.failures.map((failure, index) => (
// biome-ignore lint/suspicious/noArrayIndexKey: a static report of one import run; rows never reorder
<li key={`${failure.sourceId}:${index}`}>
{failure.sourceId}: {failure.error}
</li>
))}
</ul>
</details>
)}
{result.failures
.filter((failure) => !failure.kind)
.map((failure) => (
// Source-level paging errors have no conversation record to list.
<p key={failure.sourceId}>
{failure.sourceId}: {failure.error}
</p>
))}
</>
)}
{recorded.all.length > 0 && (
<details open={!result}>
<summary>
{t("historyImport.failedList").replace("{count}", String(recorded.all.length))}
</summary>
<HistoryMigrationFailureList records={recorded.all} />
</details>
)}
{state.error && <p>{state.error}</p>}
</div>
</SettingsGroup>
Expand Down
Loading
Loading