Compare commits

..
28 changed files with 300 additions and 384 deletions

No files matched your search

@@ -379,8 +379,8 @@ it("uses user-facing connection confirmation copy without implementation narrati
expect(
screen.queryByText(/选择草稿|按各应用回执|本页任务控制尚未接通/),
).toBeNull();
expect(screen.getByText(/下一步选择记忆模式/)).toBeTruthy();
expect(screen.getByText(/之后仍可在设置中更改/)).toBeTruthy();
expect(screen.queryByText(/下一步选择记忆模式/)).toBeNull();
expect(screen.queryByText(/之后仍可在设置中更改/)).toBeNull();
});
it.each(["paused", "failed", "cancelled", "interrupted"] as const)(
@@ -398,11 +398,7 @@ it.each(["paused", "failed", "cancelled", "interrupted"] as const)(
expect(
screen.getByRole("heading", { name: "对话正在汇入,记忆从这里开始" }),
).toBeTruthy();
expect(
screen.getByText(/已保存的对话还在/, {
selector: ".first-import-r6-footer-note p",
}),
).toBeTruthy();
expect(screen.queryByText(/已保存的对话还在/)).toBeNull();
},
);
@@ -439,9 +435,7 @@ it.each(["stale", "revoked", "missing"])(
onContinue={vi.fn()}
/>,
);
if (kind === "missing")
expect(screen.getByText("导入在后台继续,无需等待。")).toBeTruthy();
else expect(screen.queryByText("导入在后台继续,无需等待。")).toBeNull();
expect(screen.queryByText("导入在后台继续,无需等待。")).toBeNull();
expect(
screen.getByRole("heading", { name: "对话正在汇入,记忆从这里开始" }),
).toBeTruthy();
@@ -199,8 +199,8 @@ it("keeps the saved count when authority or freshness changes, without a convers
expect(view.container.querySelector('[data-running="true"]')).toBeTruthy();
expect(screen.getByText("已保存 7 条消息")).toBeTruthy();
expect(
screen.getAllByText("正在重新核对来源;已保存数量仍按任务回执显示。"),
).toHaveLength(1);
screen.queryByText("正在重新核对来源;已保存数量仍按任务回执显示。"),
).toBeNull();
view.update({
sourceSnapshot: { confirmedAt: "2026-09-14T00:00:02Z", stale: false },
jobs: [{ ...job, stored: 20 }],
+67 -118
View File
@@ -148,31 +148,6 @@ export function FirstImportReview({
const canResume = batch.some((job) =>
["paused", "interrupted", "failed"].includes(job.status),
);
const allCompleted =
batch.length > 0 &&
ids.every((id) => batch.some((job) => job.source_id === id)) &&
batch.every((job) => job.status === "completed") &&
!Object.keys(failedSources).length;
const allPaused =
batch.length > 0 && batch.every((job) => job.status === "paused");
const importing =
!stale &&
batch.some(
(job) =>
["pending", "running"].includes(job.status) &&
sources.some(
(source) => source.source_id === job.source_id && source.authorized,
),
);
const displayImporting =
importing ||
(!stale &&
!allCompleted &&
!allPaused &&
batch.length === 0 &&
ids.some((id) =>
sources.some((source) => source.source_id === id && source.authorized),
));
const title = "对话正在汇入,记忆从这里开始";
const hostEligible = importHostEligible(sources, ids);
const draftReady = !!connectionDraft && !!onConnectionDraftChange;
@@ -282,24 +257,73 @@ export function FirstImportReview({
<R6ColdStartFrame
step={2}
title={text(title)}
eyebrow={text("导入进行中 · 顺手连接应用")}
footer={
<>
<div className="first-import-r6-footer-note">
<p>
{text(
allCompleted
? "对话已保存,记忆尚待生成。"
: displayImporting
? "导入在后台继续,无需等待。"
: stale
? "正在重新核对来源;已保存数量仍按任务回执显示。"
: allPaused
? "本次历史导入已暂停,已保存的对话还在。"
: "导入等待处理,已保存的对话还在。",
<div className="first-import-r6-footer-actions">
{changing ? (
<R6Button
onClick={() => {
stopBatch.current = true;
}}
>
{text("停止后续操作")}
</R6Button>
) : onChangeJob && (canPause || canResume) ? (
<R6Button
disabled={busy || (stale && !canPause)}
onClick={() => void changeBatch(canPause ? "pause" : "resume")}
>
{canPause ? (
<Pause size={16} aria-hidden="true" />
) : (
<Play size={16} aria-hidden="true" />
)}
</p>
{(error || actionMessage) && (
{text(canPause ? "暂停导入" : "继续导入")}
</R6Button>
) : null}
{recovery ? (
<>
<R6Button
disabled={busy}
onClick={() => {
stopBatch.current = true;
recovery.onSkip();
}}
>
{text(
recovery.destination === "sources"
? "仍进入应用页"
: recovery.destination === "settings"
? "仍进入设置"
: "仍进入工作台",
)}
</R6Button>
<R6Button
variant="primary"
disabled={busy}
onClick={() => leave(recovery.destination)}
>
{text("重试保存并进入")}
</R6Button>
</>
) : (
<R6Button
variant="primary"
disabled={busy}
onClick={() => leave("settings")}
>
{text("选择记忆模式")}
</R6Button>
)}
</div>
}
>
<div className="first-import-r6-layout">
<section
className="first-import-r6-main"
aria-label={text("导入与原文")}
>
{(error || actionMessage) && (
<div className="first-import-r6-feedback">
<p
role={
error?.includes("应用连接") ||
@@ -318,83 +342,8 @@ export function FirstImportReview({
>
{error ? text(error) : actionMessage}
</p>
)}
</div>
<div className="first-import-r6-footer-actions">
{changing ? (
<R6Button
onClick={() => {
stopBatch.current = true;
}}
>
{text("停止后续操作")}
</R6Button>
) : onChangeJob && (canPause || canResume) ? (
<R6Button
disabled={busy || (stale && !canPause)}
onClick={() => void changeBatch(canPause ? "pause" : "resume")}
>
{canPause ? (
<Pause size={16} aria-hidden="true" />
) : (
<Play size={16} aria-hidden="true" />
)}
{text(canPause ? "暂停导入" : "继续导入")}
</R6Button>
) : null}
{recovery ? (
<>
<R6Button
disabled={busy}
onClick={() => {
stopBatch.current = true;
recovery.onSkip();
}}
>
{text(
recovery.destination === "sources"
? "仍进入应用页"
: recovery.destination === "settings"
? "仍进入设置"
: "仍进入工作台",
)}
</R6Button>
<R6Button
variant="primary"
disabled={busy}
onClick={() => leave(recovery.destination)}
>
{text("重试保存并进入")}
</R6Button>
</>
) : (
<R6Button
variant="primary"
disabled={busy}
onClick={() => leave("settings")}
>
{text("选择记忆模式")}
</R6Button>
)}
</div>
</>
}
>
<div className="first-import-r6-layout">
<section
className="first-import-r6-main"
aria-label={text("导入与原文")}
>
<p className="first-import-r6-intro">
{text(
allCompleted
? "本次导入已结束。已保存的对话无需重复导入。"
: "对话先保存在本机。你可以同时设置后续连接。",
)}
</p>
<p className="first-import-r6-subintro">
{text("下一步选择记忆模式;之后仍可在设置中更改。")}
</p>
</div>
)}
{historyWindowLimited && (
<p className="first-import-r6-history-window" role="status">
{t("onboardingHistoryWindow")}
+2 -2
View File
@@ -1235,11 +1235,11 @@ export function MemoryMap(props: Props) {
<h2>
{props.simplified
? raw
? t("memory:原始对话")
? text("来源归属")
: text("记忆关联")
: label}
</h2>
{props.simplified && (
{props.simplified && (!raw || !projection.nodes.length) && (
<p className="selected-graph-subtitle">
{projection.nodes.length
? text("来源归属")
@@ -289,6 +289,11 @@ it("shows raw conversations instead of advanced memory in lite", async () => {
render(<MemorySpace simplified />);
expect(await screen.findAllByText("原始对话内容")).toHaveLength(2);
expect(screen.getByText("1 条原始对话")).toBeTruthy();
expect(screen.queryByText("找到 1 条")).toBeNull();
expect(
screen.getByRole("heading", { level: 2, name: "来源归属" }),
).toBeTruthy();
expect(
document.querySelector(".core-memory-map")?.hasAttribute("hidden"),
).toBe(false);
@@ -625,6 +630,7 @@ it("opens a search hit with the complete role transcript", async () => {
await user.click(screen.getByRole("button", { name: "搜索" }));
expect(await screen.findByText("完整问题")).toBeTruthy();
expect(screen.getByText("找到 1 条")).toBeTruthy();
expect(screen.getByText("完整回答")).toBeTruthy();
expect(
document.querySelector(".map-dialogue-reading")?.textContent,
+3 -4
View File
@@ -964,10 +964,9 @@ export function MemorySpace({
? sdkPage
? sdkPage.total_exact === false
? t("memory:原始对话")
: `${t("memory:原始对话")} · ${tn(
"sdk.raw.found",
sdkPage.total,
)}`
: sdkLibrary.settledQuery.trim()
? tn("sdk.raw.found", sdkPage.total)
: tn("sdk.raw.total", sdkPage.total)
: sdkLibrary.failed
? null
: t("memory:正在读取…")
@@ -164,8 +164,7 @@ it("moves from import to the actual third step without completing onboarding", a
expect(completeOnboarding).not.toHaveBeenCalled();
expect(finish).not.toHaveBeenCalled();
await userEvent.click(screen.getByRole("button", { name: "返回第二步" }));
await userEvent.click(screen.getByRole("button", { name: "返回导入" }));
expect(screen.getByText("Import accepted")).toBeTruthy();
expect(screen.getByRole("heading", { name: "选择记忆模式" })).toBeTruthy();
expect(completeOnboarding).not.toHaveBeenCalled();
});
@@ -184,7 +183,7 @@ it("lets an empty first-time user enter without granting or importing a source",
onFinish={finish}
/>,
);
const skip = await screen.findByRole("button", { name: "稍后添加,先进入" });
const skip = await screen.findByRole("button", { name: "选择记忆模式" });
await waitFor(() => expect((skip as HTMLButtonElement).disabled).toBe(false));
await userEvent.click(skip);
await userEvent.click(screen.getByRole("radio", { name: /lite/i }));
@@ -210,7 +209,7 @@ it("retains explicit retry or enter when saving the skipped onboarding fails", a
onFinish={finish}
/>,
);
const skip = await screen.findByRole("button", { name: "稍后添加,先进入" });
const skip = await screen.findByRole("button", { name: "选择记忆模式" });
await waitFor(() => expect((skip as HTMLButtonElement).disabled).toBe(false));
await userEvent.click(skip);
await userEvent.click(screen.getByRole("radio", { name: /lite/i }));
@@ -302,7 +301,9 @@ it("freezes the explicitly submitted source list when a later source arrives", a
resolveRefresh = resolve;
}),
);
await userEvent.click(screen.getByRole("button", { name: "开始导入" }));
await userEvent.click(
await screen.findByRole("button", { name: "开始导入" }),
);
const lateSources = [source("workbuddy"), source("codex")];
rendered.rerender(view({ ...imports, sources: lateSources }));
expect(checkbox("codex").checked).toBe(false);
@@ -211,10 +211,10 @@ it("translates discovery, progress and privacy without importing or authorizing
}),
).toBeTruthy();
expect(
screen.getByText(
screen.queryByText(
"Antes de confirmar, solo se comprueba la información de las aplicaciones. No se leen conversaciones ni se instalan conexiones.",
),
).toBeTruthy();
).toBeNull();
expect(
screen.getByText("/fixture/cursor · Ubicación predeterminada"),
).toBeTruthy();
@@ -289,10 +289,10 @@ it("translates import review and preserves independent permission disclosure", a
}),
).toBeTruthy();
expect(
screen.getByText(
screen.queryByText(
"A continuación, elige un modo de memoria. Podrás cambiarlo más adelante en Configuración.",
),
).toBeTruthy();
).toBeNull();
expect(
screen.getByRole("region", {
name: "Progreso de importación de las aplicaciones",
@@ -324,8 +324,8 @@ it("uses compact Spanish actions and preserves Chinese copy and permissions", as
}),
).toBeTruthy();
expect(
screen.getByText(/A continuación, elige un modo de memoria/),
).toBeTruthy();
screen.queryByText(/A continuación, elige un modo de memoria/),
).toBeNull();
expect(
screen.getByText(/Solo para las apps seleccionadas y compatibles/),
).toBeTruthy();
@@ -333,7 +333,7 @@ it("uses compact Spanish actions and preserves Chinese copy and permissions", as
fireEvent.click(screen.getByText("中文"));
expect(screen.getByRole("button", { name: "选择记忆模式" })).toBeTruthy();
expect(navigation.textContent).toContain("连接应用");
expect(screen.getByText(/下一步选择记忆模式/)).toBeTruthy();
expect(screen.queryByText(/下一步选择记忆模式/)).toBeNull();
expect(next).not.toHaveBeenCalled();
expect(draft).not.toHaveBeenCalled();
});
@@ -425,6 +425,12 @@ it("translates completion recovery live without repeating completion writes", as
new Error("fixture-offline"),
);
const imports = controller();
imports.sources = [];
vi.mocked(imports.refresh).mockResolvedValue({
sources: [],
jobs: [],
health: null as never,
});
const finish = vi.fn();
render(
<LanguageProvider>
@@ -434,11 +440,11 @@ it("translates completion recovery live without repeating completion writes", as
);
await waitFor(() =>
expect(
screen.getByRole("button", { name: "Entrar y añadir más tarde" }),
screen.getByRole("button", { name: "Elegir el modo de memoria" }),
).toHaveProperty("disabled", false),
);
fireEvent.click(
screen.getByRole("button", { name: "Entrar y añadir más tarde" }),
screen.getByRole("button", { name: "Elegir el modo de memoria" }),
);
fireEvent.click(screen.getByRole("radio", { name: /lite/i }));
fireEvent.click(
@@ -347,11 +347,11 @@ it("keeps the single-source submit frozen when another source arrives during fin
it("empty discovery shows zero and allows explicit entry without a connection request", async () => {
await ready();
await waitFor(() => expect(count()).toBe("0"));
expect(screen.getByRole("button", { name: "开始导入" })).toHaveProperty(
expect(screen.getByRole("button", { name: "选择记忆模式" })).toHaveProperty(
"disabled",
true,
false,
);
fireEvent.click(screen.getByRole("button", { name: "稍后添加,先进入" }));
fireEvent.click(screen.getByRole("button", { name: "选择记忆模式" }));
fireEvent.click(screen.getByRole("radio", { name: /lite/i }));
fireEvent.click(await screen.findByRole("button", { name: "确认选择" }));
fireEvent.click(screen.getByRole("button", { name: "使用 Lite 进入" }));
@@ -398,13 +398,7 @@ it("partial acceptance preserves the successful job and failure through model-st
"accepted-job:completed",
);
fireEvent.click(screen.getByRole("button", { name: "继续配置模型" }));
fireEvent.click(await screen.findByRole("button", { name: "返回导入" }));
expect(screen.getByLabelText("保留任务").textContent).toBe(
"accepted-job:completed",
);
expect(screen.getByLabelText("失败来源").textContent).toBe("cursor");
expect(api.completeOnboarding).not.toHaveBeenCalled();
fireEvent.click(screen.getByRole("button", { name: "进入应用" }));
fireEvent.click(screen.getByRole("radio", { name: /lite/i }));
fireEvent.click(await screen.findByRole("button", { name: "确认选择" }));
fireEvent.click(screen.getByRole("button", { name: "使用 Lite 进入" }));
@@ -125,7 +125,7 @@ it("renders the final shell, double title, step one, branded rows and exact sour
expect(
screen.getByRole("heading", { name: "把熟悉的背景,带到这里" }),
).toBeTruthy();
expect(screen.getByText("你的记忆,从这里接上")).toBeTruthy();
expect(screen.queryByText("你的记忆,从这里接上")).toBeNull();
expect(
screen
.getByRole("navigation", { name: "首次设置进度" })
@@ -133,8 +133,8 @@ it("renders the final shell, double title, step one, branded rows and exact sour
).toContain("导入历史");
expect(screen.getByRole("heading", { name: "本机 AI 应用" })).toBeTruthy();
expect(
screen.getByText("确认前只检查应用信息,不读取对话,也不安装连接。"),
).toBeTruthy();
screen.queryByText("确认前只检查应用信息,不读取对话,也不安装连接。"),
).toBeNull();
expect(screen.queryByRole("button", { name: "导入历史包" })).toBeNull();
expect(screen.getByText("/fixture/workbuddy · 默认位置")).toBeTruthy();
expect(
@@ -349,7 +349,7 @@ it("custom source cancel does not grant or select and accepted import retains re
expect(
(screen.getByLabelText("导入 codex 历史") as HTMLInputElement).checked,
).toBe(false);
fireEvent.click(screen.getByRole("button", { name: "开始导入" }));
fireEvent.click(await screen.findByRole("button", { name: "开始导入" }));
await screen.findByText("已受理 workbuddy");
expect(vi.mocked(imports.connectForOnboarding).mock.calls[0]?.[0]).toEqual([
"workbuddy",
@@ -373,7 +373,7 @@ it("does not submit authorization when the first-step owner leaves during final
done = resolve;
}),
);
fireEvent.click(screen.getByRole("button", { name: "开始导入" }));
fireEvent.click(await screen.findByRole("button", { name: "开始导入" }));
view.unmount();
await act(async () =>
done({
+17 -25
View File
@@ -219,7 +219,7 @@ function imports(
const READ_CONSENT = "允许读取以上来源的本次历史";
const START = "开始导入";
const SKIP = "稍后添加,先进入";
const SKIP = "选择记忆模式";
async function openApps(_user?: ReturnType<typeof userEvent.setup>) {
await waitFor(() =>
expect(
@@ -470,15 +470,12 @@ describe("OnboardingFlow S1.3", () => {
modelBoundary,
);
await user.click(screen.getByRole("button", { name: "返回导入" }));
await user.click(screen.getByRole("button", { name: "返回导入" }));
await waitFor(() =>
expect(screen.getByRole("alert").textContent).toContain(
"开始导入的结果尚未确认",
),
);
expect(
screen.getByRole("heading", { name: "对话正在汇入,记忆从这里开始" }),
).toBeTruthy();
expect(screen.getByRole("heading", { name: "选择记忆模式" })).toBeTruthy();
});
it("accepts Lite once while the import submission is still in flight", async () => {
@@ -951,12 +948,10 @@ describe("OnboardingFlow S1.3", () => {
await user.click(screen.getByRole("button", { name: SKIP }));
expect(screen.getByRole("heading", { name: "选择记忆模式" })).toBeTruthy();
expect(screen.getByText("之后可在设置中更改。")).toBeTruthy();
expect(screen.queryByText("之后可在设置中更改。")).toBeNull();
expect(screen.getAllByRole("radio")).toHaveLength(3);
expect(screen.getByRole("radio", { name: /ultra/i })).toBeTruthy();
expect(screen.getByText("推荐").closest("label")?.textContent).toContain(
"pro",
);
expect(screen.queryByText("推荐")).toBeNull();
const modeStep = screen
.getByRole("navigation", { name: "首次设置进度" })
.querySelector('[aria-current="step"]');
@@ -1208,7 +1203,7 @@ describe("OnboardingFlow S1.3", () => {
}),
);
const user = userEvent.setup();
render(
const view = render(
<OnboardingFlow
state={onboarding}
imports={imports({ sources: [] })}
@@ -1219,7 +1214,7 @@ describe("OnboardingFlow S1.3", () => {
await openApps();
await user.click(screen.getByRole("button", { name: SKIP }));
await user.click(screen.getByRole("radio", { name: /lite/i }));
await user.click(screen.getByRole("button", { name: "返回导入" }));
view.unmount();
await act(async () =>
resolvePlan({
current: "pro",
@@ -1238,11 +1233,6 @@ describe("OnboardingFlow S1.3", () => {
);
expect(screen.queryByRole("dialog")).toBeNull();
expect(
screen.getByRole("heading", {
name: "对话正在汇入,记忆从这里开始",
}),
).toBeTruthy();
});
it("persists Lite mode before resumable progress and completion", async () => {
@@ -1758,7 +1748,7 @@ describe("OnboardingFlow S1.3", () => {
it("skipping after a selection does not grant, install, sync or connect", async () => {
const user = userEvent.setup();
const controller = imports();
const controller = imports({ sources: [] });
const onFinish = vi.fn();
render(
<OnboardingFlow
@@ -1767,7 +1757,7 @@ describe("OnboardingFlow S1.3", () => {
onFinish={onFinish}
/>,
);
await choose(user);
await openApps();
await user.click(screen.getByRole("button", { name: SKIP }));
await chooseLiteAndContinue(user);
expect(onFinish).toHaveBeenCalledWith("sources");
@@ -1823,6 +1813,7 @@ describe("OnboardingFlow S1.3", () => {
await user.click(screen.getByRole("button", { name: "重新检测位置" }));
expect(choice()).toHaveProperty("disabled", false);
await waitFor(() => expect(choice()).toHaveProperty("checked", true));
await user.click(choice());
await user.click(screen.getByRole("button", { name: SKIP }));
await chooseLiteAndContinue(user);
expect(onFinish).toHaveBeenCalledWith("sources");
@@ -1847,6 +1838,7 @@ describe("OnboardingFlow S1.3", () => {
expect(screen.getByRole("alert")).toBeTruthy();
expect(controller.connectForOnboarding).not.toHaveBeenCalled();
expect(onFinish).not.toHaveBeenCalled();
await user.click(choice());
await user.click(screen.getByRole("button", { name: SKIP }));
await chooseLiteAndContinue(user);
expect(onFinish).toHaveBeenCalledWith("sources");
@@ -1929,9 +1921,9 @@ describe("OnboardingFlow S1.3", () => {
await user.click(choice());
await user.click(screen.getByRole("button", { name: "重新检测位置" }));
expect(choice()).toHaveProperty("checked", false);
expect(screen.getByRole("button", { name: START })).toHaveProperty(
expect(screen.getByRole("button", { name: SKIP })).toHaveProperty(
"disabled",
true,
false,
);
});
@@ -2738,10 +2730,9 @@ describe("OnboardingFlow S1.3", () => {
"disabled",
true,
);
expect(screen.getByRole("button", { name: SKIP })).toHaveProperty(
"disabled",
true,
);
expect(
screen.getByRole("button", { name: "正在开始导入…" }),
).toHaveProperty("disabled", true);
expect(controller.refresh).toHaveBeenCalledTimes(2);
await act(async () => resolve());
expect(controller.connectForOnboarding).toHaveBeenCalledOnce();
@@ -3084,7 +3075,7 @@ describe("OnboardingFlow S1.3", () => {
it("lets the user leave after completion cannot be persisted without reconnecting", async () => {
const user = userEvent.setup();
completeOnboarding.mockRejectedValue(new Error("disk unavailable"));
const controller = imports();
const controller = imports({ sources: [] });
const onFinish = vi.fn();
render(
<OnboardingFlow
@@ -3093,6 +3084,7 @@ describe("OnboardingFlow S1.3", () => {
onFinish={onFinish}
/>,
);
await openApps();
await user.click(screen.getByRole("button", { name: SKIP }));
await chooseLiteAndContinue(user);
await user.click(screen.getByRole("button", { name: "仍进入应用页" }));
+40 -83
View File
@@ -1148,6 +1148,8 @@ export function OnboardingFlow({
}
const count = selected.length;
const continueWithoutImport =
busy === null && locationCheck !== "checking" && count === 0;
// D51 item 3: only a *confirmed* snapshot may show a number. A failed
// detection must read 未完成 even when sources are listed — otherwise the
// number branch answered first and the summary claimed a count while the
@@ -1251,78 +1253,38 @@ export function OnboardingFlow({
<R6ColdStartFrame
step={3}
title={text("选择记忆模式")}
eyebrow={text("最后一步 · 决定如何处理对话")}
focusToken={modeFocusToken}
footer={
<>
<R6Button
variant="quiet"
disabled={modeSaving || completionBusy}
onClick={() => {
modePlanOperation.current += 1;
setModePlanBusy(false);
setPendingMode(null);
setPendingPlan(null);
setModeError(null);
intentRef.current = modeChoice;
setupStepRef.current = "review";
setSetupStep("review");
void queueProgressWrite(() =>
patchOnboardingProgress({
mode_intent: modeChoice,
setup_step: "review",
}),
).catch((reason) => {
if (
!isCompletedProgressConflict(reason) &&
alive.current
)
setModeError(
"恢复进度未能保存。你的选择已保留,请重试。",
);
});
}}
>
{text("返回导入")}
</R6Button>
<R6Button
className="onboarding-r6-submit"
variant="primary"
pending={modeSaving || modePlanBusy}
disabled={
!modeChoice ||
completionBusy ||
modePlanBusy ||
(modeChoice === "ultra" && !scheduleLoaded)
}
onClick={() => void saveModeChoice()}
>
{text(
modePhase === "saving"
? "正在保存模式…"
: modePhase === "completing"
? "正在完成首次设置…"
: modeChoice === "lite"
? "使用 Lite 进入"
: modeChoice === "pro"
? "继续配置模型"
: modeChoice === "ultra"
? "继续"
: "继续",
)}
</R6Button>
</>
<R6Button
className="onboarding-r6-submit"
variant="primary"
pending={modeSaving || modePlanBusy}
disabled={
!modeChoice ||
completionBusy ||
modePlanBusy ||
(modeChoice === "ultra" && !scheduleLoaded)
}
onClick={() => void saveModeChoice()}
>
{text(
modePhase === "saving"
? "正在保存模式…"
: modePhase === "completing"
? "正在完成首次设置…"
: modeChoice === "lite"
? "使用 Lite 进入"
: modeChoice === "pro"
? "继续配置模型"
: "继续",
)}
</R6Button>
}
>
<section
className="onboarding-r6-mode"
aria-labelledby="onboarding-memory-mode-title"
aria-label={text("记忆模式")}
>
<div className="onboarding-r6-mode-intro">
<p id="onboarding-memory-mode-title">
{text("之后可在设置中更改。")}
</p>
</div>
<MemoryModeChoiceList
aria-label={text("记忆模式")}
modes={(["lite", "pro", "ultra"] as const).map((mode) => ({
@@ -1332,7 +1294,6 @@ export function OnboardingFlow({
}))}
selected={modeChoice}
disabled={modeSaving || completionBusy || modePlanBusy}
recommended={{ mode: "pro", badge: text("推荐") }}
onChange={(mode, control) =>
void requestModeSelection(
mode as OnboardingMemoryMode,
@@ -1502,16 +1463,8 @@ export function OnboardingFlow({
<R6ColdStartFrame
step={1}
title={text("把熟悉的背景,带到这里")}
eyebrow={text("你的记忆,从这里接上")}
footer={
<>
<R6Button
variant="quiet"
disabled={locked}
onClick={() => requestModeChoice("sources")}
>
{text("稍后添加,先进入")}
</R6Button>
{error && (
<p className="onboarding-r6-error" role="alert">
{error && text(error)}
@@ -1520,10 +1473,20 @@ export function OnboardingFlow({
<R6Button
className="onboarding-r6-submit"
variant="primary"
disabled={locked || count === 0}
onClick={() => void startImport()}
disabled={locked}
onClick={() =>
continueWithoutImport
? requestModeChoice("sources")
: void startImport()
}
>
{text(busy === "submitting" ? "正在开始导入…" : "开始导入")}
{text(
continueWithoutImport
? "选择记忆模式"
: busy === "submitting"
? "正在开始导入…"
: "开始导入",
)}
</R6Button>
</>
}
@@ -1533,12 +1496,6 @@ export function OnboardingFlow({
className="onboarding-r6-apps"
aria-labelledby="cold-start-phase"
>
<div className="onboarding-r6-intro">
<p>
{text("不用从头介绍自己。从常用的 App 开始,把已有背景带过来。")}
</p>
<p>{text("确认前只检查应用信息,不读取对话,也不安装连接。")}</p>
</div>
<div className="onboarding-r6-list-heading">
<h2 id="cold-start-phase" ref={heading} tabIndex={-1}>
{text("本机 AI 应用")}
+36 -2
View File
@@ -48,6 +48,40 @@ it("sizes the import action to its label and wraps footer content", () => {
);
});
it("uses content-driven shell, discovery and mode geometry", () => {
const stage = declaration("onboarding-r6.css", ".onboarding-r6-stage");
const shell = declaration("onboarding-r6.css", ".onboarding-r6-shell");
const discovery = declaration(
"onboarding-r6.css",
".onboarding-r6-discovery",
);
const heading = declaration(
"onboarding-r6.css",
".onboarding-r6-list-heading",
);
const mode = declaration("onboarding-r6.css", ".onboarding-r6-mode");
expect(stage).toMatch(/min-height:\s*0/);
expect(stage).not.toMatch(/min-height:\s*852px/);
expect(shell).toMatch(/height:\s*100%/);
expect(shell).not.toMatch(/(?:^|;)\s*height:\s*820px/);
expect(discovery).toMatch(/height:\s*100%/);
expect(heading).toMatch(/position:\s*static/);
expect(mode).toMatch(/width:\s*100%/);
});
it("does not reserve an empty eyebrow row", () => {
render(
<LanguageProvider>
<R6ColdStartFrame step={1} title="Fixture" footer={null}>
<p>Fixture body</p>
</R6ColdStartFrame>
</LanguageProvider>,
);
expect(screen.getByRole("heading", { name: "Fixture" })).toBeTruthy();
expect(screen.queryByText("你的记忆,从这里接上")).toBeNull();
});
it("keeps progress cards content-sized and the step-two footer in normal flow", () => {
const card = declaration(
"r6-import-progress.css",
@@ -62,10 +96,10 @@ it("keeps progress cards content-sized and the step-two footer in normal flow",
),
).toMatch(/flex:\s*0 0 auto/);
expect(declaration("first-import-r6.css", ".first-import-r6-layout")).toMatch(
/height:\s*auto/,
/height:\s*100%/,
);
expect(
declaration("first-import-r6.css", ".first-import-r6-footer-note"),
declaration("first-import-r6.css", ".first-import-r6-feedback"),
).not.toMatch(/max-height|overflow/);
expect(declaration("onboarding-r6.css", ".onboarding-r6-body")).toMatch(
/overflow-y:\s*auto/,
+2 -2
View File
@@ -13,7 +13,7 @@ export function R6ColdStartFrame({
}: {
step: 1 | 2 | 3;
title: string;
eyebrow: string;
eyebrow?: string | null;
children: ReactNode;
footer: ReactNode;
focusToken?: number;
@@ -39,7 +39,7 @@ export function R6ColdStartFrame({
<section className="onboarding-r6-shell" data-step={step}>
<header className="onboarding-r6-head">
<div>
<p>{eyebrow}</p>
{eyebrow ? <p>{eyebrow}</p> : null}
<h1
ref={titleRef}
tabIndex={focusToken === undefined ? undefined : -1}
@@ -29,6 +29,8 @@ it("shows every role and preserves a long original message in the detail pane",
messages: [
{ role: "user", content: long },
{ role: "assistant", content: "已收到" },
{ role: "tool", content: "3 tests passed" },
{ role: "unknown", content: "来源角色缺失" },
],
}}
sources={[]}
@@ -38,6 +40,10 @@ it("shows every role and preserves a long original message in the detail pane",
expect(screen.getByText("你")).toBeTruthy();
expect(screen.getByText("助手")).toBeTruthy();
expect(screen.getByText("工具")).toBeTruthy();
expect(screen.getByText("作者待核对")).toBeTruthy();
expect(screen.getByText(long)).toHaveProperty("textContent", long);
expect(screen.getByText("已收到")).toBeTruthy();
expect(screen.queryByText("原始对话")).toBeNull();
expect(screen.getByRole("article", { name: "原始对话" })).toBeTruthy();
});
+2 -2
View File
@@ -14,6 +14,7 @@ function authorLabel(role: string, t: ReturnType<typeof useGuiLanguage>["t"]) {
if (role === "user") return t("selectedMemory.author.user");
if (role === "assistant" || role === "agent")
return t("selectedMemory.author.agent");
if (role === "tool") return t("selectedMemory.author.tool");
return t("selectedMemory.author.unknown");
}
@@ -46,8 +47,7 @@ export function SelectedRawDetail({
record.source || t("selectedMemory.sourceUnknown"),
);
return (
<article className="map-dialogue-reading">
<p className="selected-memory-status">{t("memory:原始对话")}</p>
<article className="map-dialogue-reading" aria-label={t("memory:原始对话")}>
<ul className="map-transcript">
{messages.map((message) => (
<li key={`${rawRecordId(record)}:${message.role}:${message.content}`}>
+15 -22
View File
@@ -3,8 +3,9 @@
display: grid;
grid-template-columns: minmax(0, 738px) 278px;
gap: 30px;
min-height: 100%;
height: auto;
min-height: 0;
height: 100%;
align-items: stretch;
}
.first-import-r6-main {
min-width: 0;
@@ -12,20 +13,8 @@
display: flex;
flex-direction: column;
}
.first-import-r6-intro {
margin: 0;
color: #5a6169;
font-size: 13px;
line-height: 25px;
}
.first-import-r6-subintro {
margin: 1px 0 20px;
color: #63707b;
font-size: 12px;
line-height: 24px;
}
.first-import-r6-history-window {
margin: -10px 0 12px;
margin: 0 0 12px;
padding: 8px 10px;
border: 1px solid #ead7a7;
border-radius: 8px;
@@ -39,7 +28,7 @@
}
.first-import-r6-feed {
flex: 0 0 auto;
margin-top: 26px;
margin-top: 20px;
overflow: visible;
scrollbar-width: thin;
overscroll-behavior: contain;
@@ -101,6 +90,7 @@
overflow: auto;
scrollbar-width: thin;
overscroll-behavior: contain;
align-self: stretch;
}
.first-import-r6-eyebrow {
margin: 0 0 14px;
@@ -223,20 +213,22 @@
.first-import-r6-options details p {
margin: 6px 0;
}
.first-import-r6-footer-note {
flex: 1 1 240px;
min-width: 0;
.first-import-r6-feedback {
margin: 0 0 12px;
padding: 10px 12px;
border-radius: 8px;
background: rgb(255 255 255 / 50%);
}
.first-import-r6-footer-note p {
.first-import-r6-feedback p {
margin: 0;
font-size: 12px;
line-height: 20px;
color: #63707b;
}
.first-import-r6-footer-note [role="alert"] {
.first-import-r6-feedback [role="alert"] {
color: #944242;
}
.first-import-r6-footer-note .first-import-r6-partial-success {
.first-import-r6-feedback .first-import-r6-partial-success {
color: #245f50;
font-weight: 600;
}
@@ -248,6 +240,7 @@
flex-wrap: wrap;
gap: 12px;
align-items: center;
margin-left: auto;
}
.first-import-r6-footer-actions > [data-variant="primary"] {
min-width: min(160px, 100%);
+30 -84
View File
@@ -115,14 +115,16 @@
}
.onboarding-r6-stage {
flex: 1;
min-height: 852px;
padding: 32px 24px 2px;
min-height: 0;
display: flex;
padding: 24px;
}
.onboarding-r6-shell {
min-height: 0;
width: 1120px;
max-width: 100%;
height: 820px;
height: 100%;
max-height: 820px;
margin: 0 auto;
display: flex;
flex-direction: column;
@@ -137,7 +139,7 @@
inset 0 -1px 0 #adc0d22e;
}
.onboarding-r6-head {
min-height: 123.7px;
min-height: 0;
flex: 0 0 auto;
display: flex;
flex-wrap: wrap;
@@ -195,6 +197,7 @@
background: #ffffff38;
display: flex;
align-items: center;
justify-content: flex-end;
gap: 16px;
border-radius: 0 0 25px 25px;
}
@@ -224,27 +227,21 @@
}
}
.onboarding-r6-discovery {
height: 560.2px;
height: 100%;
min-height: 0;
display: grid;
grid-template-columns: minmax(0, 738px) 278px;
gap: 30px;
}
.onboarding-r6-apps {
position: relative;
min-width: 0;
}
.onboarding-r6-intro {
margin: 0;
font-size: 13px;
line-height: 24.05px;
color: #5a6169;
}
.onboarding-r6-intro p {
margin: 0;
min-height: 0;
display: grid;
grid-template-rows: auto minmax(0, 1fr);
gap: 12px;
}
.onboarding-r6-list-heading {
position: absolute;
inset: 96px 0 auto;
position: static;
height: 30px;
display: flex;
align-items: center;
@@ -277,8 +274,10 @@
cursor: default;
}
.onboarding-r6-list-area {
position: absolute;
inset: 142px 0 auto;
position: static;
min-height: 0;
display: flex;
flex-direction: column;
}
.onboarding-r6-list-status .onboarding-r6-quiet:disabled {
color: #4e6478;
@@ -288,7 +287,9 @@
padding: 0;
margin: 0;
list-style: none;
height: 330px;
min-height: 0;
height: auto;
flex: 1;
display: flex;
flex-direction: column;
gap: 12px;
@@ -451,44 +452,6 @@
content: "↓";
margin-right: 6px;
}
.onboarding-r6-import {
position: absolute;
top: 498px;
left: 0;
right: 0;
display: flex;
align-items: center;
gap: 10px;
font-size: 12px;
color: #6a7988;
}
/* 143:2085 puts 导入历史包 first and the "没有本机历史?" sentence after it; the
markup keeps the sentence first so it stays the accessible label order for
the control that follows. Reordering visually only, rather than moving the
DOM, keeps that reading order intact. */
.onboarding-r6-import > p {
order: 2;
}
.onboarding-r6-import > .history-import,
.onboarding-r6-import > .history-import-trigger {
order: 1;
}
.onboarding-r6-import > p {
margin: 0;
}
.onboarding-r6-import .history-import-trigger {
font: inherit;
border: 0;
border-radius: 0;
background: transparent;
color: #4e6478;
padding: 5px 11px;
min-height: 30px;
box-shadow: none;
}
.onboarding-r6-import .history-import-trigger > svg {
display: none;
}
.onboarding-r6-summary {
border-radius: 22px;
border: 1px solid #ffffffe0;
@@ -500,6 +463,8 @@
display: flex;
flex-direction: column;
gap: 11px;
align-self: stretch;
min-height: 0;
}
.onboarding-r6-summary p {
margin: 0;
@@ -572,8 +537,8 @@
border: 0;
}
.onboarding-r6-mode {
width: min(720px, 100%);
margin: 0 auto;
width: 100%;
margin: 0;
display: flex;
flex-direction: column;
gap: 20px;
@@ -801,17 +766,11 @@
flex-direction: column;
}
.onboarding-r6-apps {
min-height: 590px;
min-height: 0;
}
.onboarding-r6-list-heading {
top: 110px;
}
.onboarding-r6-list-area {
top: 154px;
}
.onboarding-r6-import {
top: 552px;
flex-wrap: wrap;
.onboarding-r6-list {
height: min(330px, 45vh);
flex: none;
}
.onboarding-r6-row > label {
padding: 14px 10px;
@@ -853,20 +812,7 @@
}
.onboarding-r6-discovery > .onboarding-r6-apps {
min-height: 0;
display: grid;
grid-template-columns: minmax(0, 1fr);
grid-template-rows: auto 30px minmax(0, 1fr) auto;
gap: 12px;
}
.onboarding-r6-apps > .onboarding-r6-list-heading,
.onboarding-r6-apps > .onboarding-r6-list-area,
.onboarding-r6-apps > .onboarding-r6-import {
position: static;
min-height: 0;
}
.onboarding-r6-apps > .onboarding-r6-list-area {
display: flex;
flex-direction: column;
grid-template-rows: auto minmax(0, 1fr);
}
.onboarding-r6-list-area > .onboarding-r6-list {
min-height: 0;
@@ -229,6 +229,12 @@ describe("English catalog", () => {
});
it("uses CLDR English singular and plural forms", () => {
expect(translateCount("en", "sdk.raw.total", 1, { count: 1 })).toBe(
"1 original conversation",
);
expect(translateCount("en", "sdk.raw.total", 2, { count: 2 })).toBe(
"2 original conversations",
);
expect(
translateCount("en", "calls.headline.found", 1, {
host: "Cursor",
@@ -73,6 +73,8 @@ export const currentChineseMessages = {
"sdk.raw.searchFailed": "原始对话搜索失败,请重试。",
"sdk.raw.recent": "最近原始对话",
"sdk.raw.empty": "还没有原始对话。连接来源后,对话会显示在这里。",
"sdk.raw.total": "{count} 条原始对话",
"sdk.raw.total.one": "{count} 条原始对话",
"sdk.raw.found": "找到 {count} 条",
"sdk.raw.found.one": "找到 {count} 条",
@@ -134,6 +136,7 @@ export const currentChineseMessages = {
"selectedMemory.save": "保存修改",
"selectedMemory.author.user": "你",
"selectedMemory.author.agent": "助手",
"selectedMemory.author.tool": "工具",
"selectedMemory.sourceUnavailable": "来源权限已变化或原文不可用。",
"selectedMemory.related": "对应记忆",
"selectedMemory.viewMemory": "查看记忆",
@@ -222,6 +225,8 @@ export const currentEnglishMessages: Record<
"sdk.raw.recent": "Recent original conversations",
"sdk.raw.empty":
"No original conversations yet. Connect a source to see conversations here.",
"sdk.raw.total": "{count} original conversations",
"sdk.raw.total.one": "{count} original conversation",
"sdk.raw.found": "{count} results",
"sdk.raw.found.one": "{count} result",
@@ -289,6 +294,7 @@ export const currentEnglishMessages: Record<
"selectedMemory.save": "Save changes",
"selectedMemory.author.user": "You",
"selectedMemory.author.agent": "Assistant",
"selectedMemory.author.tool": "Tool",
"selectedMemory.sourceUnavailable":
"Source permissions changed or the original text is unavailable.",
"selectedMemory.related": "Related memory",
@@ -384,6 +390,8 @@ export const currentSpanishMessages: Record<
"sdk.raw.recent": "Conversaciones originales recientes",
"sdk.raw.empty":
"Todavía no hay conversaciones originales. Conecta una fuente para verlas aquí.",
"sdk.raw.total": "{count} conversaciones originales",
"sdk.raw.total.one": "{count} conversación original",
"sdk.raw.found": "{count} resultados",
"sdk.raw.found.one": "{count} resultado",
@@ -452,6 +460,7 @@ export const currentSpanishMessages: Record<
"selectedMemory.save": "Guardar cambios",
"selectedMemory.author.user": "Tú",
"selectedMemory.author.agent": "Asistente",
"selectedMemory.author.tool": "Herramienta",
"selectedMemory.sourceUnavailable":
"Los permisos del origen cambiaron o el texto no está disponible.",
"selectedMemory.related": "Memoria correspondiente",
@@ -86,6 +86,8 @@ export const germanMessages: Record<MessageKey, string> = {
"sdk.raw.recent": "Letzte Originalunterhaltungen",
"sdk.raw.empty":
"Noch keine Originalunterhaltungen. Verbinde eine Quelle, damit sie hier erscheinen.",
"sdk.raw.total": "{count} Originalunterhaltungen",
"sdk.raw.total.one": "{count} Originalunterhaltung",
"memory:当前使用基础记忆。新对话会保存为原始记录,不会生成高级记忆;以前生成的高级记忆仍会保留,但当前不会显示或参与记忆召回。":
"Der Basisspeicher ist aktiv. Neue Unterhaltungen werden als Originale gespeichert und erzeugen keine erweiterten Erinnerungen. Vorhandene erweiterte Erinnerungen bleiben gespeichert, werden aber nicht angezeigt oder abgerufen.",
"memory:当前使用智能记忆。新对话会由模型整理为高级记忆。":
@@ -158,6 +160,7 @@ export const germanMessages: Record<MessageKey, string> = {
"selectedMemory.save": "Speichern",
"selectedMemory.author.user": "Sie",
"selectedMemory.author.agent": "Assistent",
"selectedMemory.author.tool": "Tool",
"selectedMemory.sourceUnavailable":
"Quellberechtigungen haben sich geändert oder der Originaltext ist nicht verfügbar.",
"selectedMemory.related": "Zugehörige Erinnerung",
@@ -87,6 +87,8 @@ export const frenchMessages: Record<MessageKey, string> = {
"sdk.raw.recent": "Conversations originales récentes",
"sdk.raw.empty":
"Aucune conversation originale pour le moment. Connectez une source pour les afficher ici.",
"sdk.raw.total": "{count} conversations originales",
"sdk.raw.total.one": "{count} conversation originale",
"memory:当前使用基础记忆。新对话会保存为原始记录,不会生成高级记忆;以前生成的高级记忆仍会保留,但当前不会显示或参与记忆召回。":
"La mémoire de base est active. Les nouvelles conversations sont conservées telles quelles et ne créent pas de mémoires avancées. Les mémoires avancées existantes restent enregistrées, mais ne sont ni affichées ni rappelées.",
"memory:当前使用智能记忆。新对话会由模型整理为高级记忆。":
@@ -162,6 +164,7 @@ export const frenchMessages: Record<MessageKey, string> = {
"selectedMemory.save": "Enregistrer",
"selectedMemory.author.user": "Vous",
"selectedMemory.author.agent": "Assistant",
"selectedMemory.author.tool": "Outil",
"selectedMemory.sourceUnavailable":
"Les autorisations de la source ont changé ou le texte original est indisponible.",
"selectedMemory.related": "Mémoire associée",
@@ -81,6 +81,8 @@ export const indonesianMessages: Record<MessageKey, string> = {
"sdk.raw.recent": "Percakapan asli terbaru",
"sdk.raw.empty":
"Belum ada percakapan asli. Hubungkan sumber agar percakapan tampil di sini.",
"sdk.raw.total": "{count} percakapan asli",
"sdk.raw.total.one": "{count} percakapan asli",
"memory:当前使用基础记忆。新对话会保存为原始记录,不会生成高级记忆;以前生成的高级记忆仍会保留,但当前不会显示或参与记忆召回。":
"Memori dasar aktif. Percakapan baru disimpan sebagai catatan asli dan tidak membuat memori lanjutan. Memori lanjutan yang ada tetap tersimpan, tetapi tidak ditampilkan atau digunakan untuk recall.",
"memory:当前使用智能记忆。新对话会由模型整理为高级记忆。":
@@ -151,6 +153,7 @@ export const indonesianMessages: Record<MessageKey, string> = {
"selectedMemory.save": "Simpan perubahan",
"selectedMemory.author.user": "Anda",
"selectedMemory.author.agent": "Asisten",
"selectedMemory.author.tool": "Alat",
"selectedMemory.sourceUnavailable":
"Izin sumber berubah atau teks asli tidak tersedia.",
"selectedMemory.related": "Memori terkait",
@@ -83,6 +83,8 @@ export const japaneseMessages: Record<MessageKey, string> = {
"sdk.raw.recent": "最近の元の会話",
"sdk.raw.empty":
"元の会話はまだありません。ソースを接続すると、ここに表示されます。",
"sdk.raw.total": "元の会話 {count} 件",
"sdk.raw.total.one": "元の会話 {count} 件",
"memory:当前使用基础记忆。新对话会保存为原始记录,不会生成高级记忆;以前生成的高级记忆仍会保留,但当前不会显示或参与记忆召回。":
"基本メモリを使用中です。新しい会話は原文として保存され、高度なメモリは生成されません。既存の高度なメモリは保持されますが、現在は表示も呼び出しもされません。",
"memory:当前使用智能记忆。新对话会由模型整理为高级记忆。":
@@ -152,6 +154,7 @@ export const japaneseMessages: Record<MessageKey, string> = {
"selectedMemory.save": "変更を保存",
"selectedMemory.author.user": "あなた",
"selectedMemory.author.agent": "アシスタント",
"selectedMemory.author.tool": "ツール",
"selectedMemory.sourceUnavailable":
"ソースの権限が変更されたか、原文を利用できません。",
"selectedMemory.related": "関連する記憶",
@@ -80,6 +80,8 @@ export const koreanMessages: Record<MessageKey, string> = {
"sdk.raw.recent": "최근 원본 대화",
"sdk.raw.empty":
"아직 원본 대화가 없습니다. 소스를 연결하면 여기에 표시됩니다.",
"sdk.raw.total": "원본 대화 {count}개",
"sdk.raw.total.one": "원본 대화 {count}개",
"memory:当前使用基础记忆。新对话会保存为原始记录,不会生成高级记忆;以前生成的高级记忆仍会保留,但当前不会显示或参与记忆召回。":
"기본 메모리를 사용 중입니다. 새 대화는 원본 기록으로 저장되며 고급 메모리를 만들지 않습니다. 기존 고급 메모리는 보존되지만 현재 표시되거나 회상에 사용되지 않습니다.",
"memory:当前使用智能记忆。新对话会由模型整理为高级记忆。":
@@ -149,6 +151,7 @@ export const koreanMessages: Record<MessageKey, string> = {
"selectedMemory.save": "변경 사항 저장",
"selectedMemory.author.user": "나",
"selectedMemory.author.agent": "어시스턴트",
"selectedMemory.author.tool": "도구",
"selectedMemory.sourceUnavailable":
"출처 권한이 변경되었거나 원문을 사용할 수 없습니다.",
"selectedMemory.related": "관련 기억",
@@ -86,6 +86,8 @@ export const portugueseMessages: Record<MessageKey, string> = {
"sdk.raw.recent": "Conversas originais recentes",
"sdk.raw.empty":
"Ainda não há conversas originais. Ligue uma origem para as ver aqui.",
"sdk.raw.total": "{count} conversas originais",
"sdk.raw.total.one": "{count} conversa original",
"memory:当前使用基础记忆。新对话会保存为原始记录,不会生成高级记忆;以前生成的高级记忆仍会保留,但当前不会显示或参与记忆召回。":
"A memória básica está ativa. As novas conversas são guardadas como registos originais e não criam memórias avançadas. As memórias avançadas existentes continuam guardadas, mas não são mostradas nem usadas na recuperação.",
"memory:当前使用智能记忆。新对话会由模型整理为高级记忆。":
@@ -159,6 +161,7 @@ export const portugueseMessages: Record<MessageKey, string> = {
"selectedMemory.save": "Salvar alterações",
"selectedMemory.author.user": "Você",
"selectedMemory.author.agent": "Assistente",
"selectedMemory.author.tool": "Ferramenta",
"selectedMemory.sourceUnavailable":
"As permissões da fonte mudaram ou o texto original não está disponível.",
"selectedMemory.related": "Memória relacionada",
@@ -83,6 +83,8 @@ export const russianMessages: Record<MessageKey, string> = {
"sdk.raw.recent": "Недавние исходные диалоги",
"sdk.raw.empty":
"Исходных диалогов пока нет. Подключите источник, и они появятся здесь.",
"sdk.raw.total": "Исходных диалогов: {count}",
"sdk.raw.total.one": "{count} исходный диалог",
"memory:当前使用基础记忆。新对话会保存为原始记录,不会生成高级记忆;以前生成的高级记忆仍会保留,但当前不会显示或参与记忆召回。":
"Включена базовая память. Новые диалоги сохраняются в исходном виде и не создают расширенные воспоминания. Существующие расширенные воспоминания сохраняются, но не показываются и не используются при поиске.",
"memory:当前使用智能记忆。新对话会由模型整理为高级记忆。":
@@ -155,6 +157,7 @@ export const russianMessages: Record<MessageKey, string> = {
"selectedMemory.save": "Сохранить изменения",
"selectedMemory.author.user": "Вы",
"selectedMemory.author.agent": "Ассистент",
"selectedMemory.author.tool": "Инструмент",
"selectedMemory.sourceUnavailable":
"Разрешения источника изменились, или исходный текст недоступен.",
"selectedMemory.related": "Связанная запись памяти",
@@ -74,6 +74,8 @@ export const traditionalChineseMessages: Record<MessageKey, string> = {
"sdk.raw.searchFailed": "原始對話搜尋失敗,請重試。",
"sdk.raw.recent": "最近原始對話",
"sdk.raw.empty": "還沒有原始對話。連接來源後,對話會顯示在這裡。",
"sdk.raw.total": "{count} 則原始對話",
"sdk.raw.total.one": "{count} 則原始對話",
"memory:当前使用基础记忆。新对话会保存为原始记录,不会生成高级记忆;以前生成的高级记忆仍会保留,但当前不会显示或参与记忆召回。":
"目前使用基礎記憶。新對話會保留為原始記錄,不會產生進階記憶;過去的進階記憶仍會保留,但目前不會顯示或參與記憶召回。",
"memory:当前使用智能记忆。新对话会由模型整理为高级记忆。":
@@ -139,6 +141,7 @@ export const traditionalChineseMessages: Record<MessageKey, string> = {
"selectedMemory.save": "儲存修改",
"selectedMemory.author.user": "你",
"selectedMemory.author.agent": "助理",
"selectedMemory.author.tool": "工具",
"selectedMemory.sourceUnavailable": "來源權限已變化或原文不可用。",
"selectedMemory.related": "對應記憶",
"selectedMemory.viewMemory": "檢視記憶",