Compare commits

..
Author SHA1 Message Date
kristinheandCursor 099f5ae7c0 test(memhub): update memory search affordance
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-27 13:13:55 +08:00
kristinheandCursor 2735d8693a fix(memhub): prioritize memory list space
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-27 12:19:49 +08:00
kristinheandCursor 47bbfb4b27 fix(memhub): simplify memory note copy
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-27 12:11:54 +08:00
kristinheandCursor 3f169c9951 style(memhub): format language selector
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-27 12:01:58 +08:00
kristinheandCursor cb97aa5822 fix(memhub): show attributed app calls on home
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-27 11:43:25 +08:00
kristinheandCursor 48a5d70026 fix(memhub): stabilize language setting layout
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-27 11:20:53 +08:00
kristinheandCursor b21c929728 fix(memhub): stabilize memory detail layout
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-27 11:12:47 +08:00
kristinheandCursor b9b3c587ff fix(memhub): hide recent update internals
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-27 11:01:14 +08:00
16 changed files with 325 additions and 132 deletions

No files matched your search

+79 -7
View File
@@ -199,7 +199,7 @@ it("localizes failure and empty states without leaking errors", async () => {
).toBeTruthy();
expect(document.body.textContent).not.toContain("synthetic_private_error");
});
it("shows latest recorded call separately from newly generated memory and opens its history", async () => {
it("shows latest recorded call separately without exposing recent-update detail entries", async () => {
history.mockResolvedValue({
available: true,
recording_error: false,
@@ -233,12 +233,11 @@ it("shows latest recorded call separately from newly generated memory and opens
await screen.findByText("Codex 查到 3 条相关内容");
expect(document.querySelector(".r6-home-call-direction")).toBeTruthy();
expect(screen.queryByText("实际治理记忆")).toBeNull();
expect(screen.getByText(/今天 .+ · 查询成功/)).toBeTruthy();
expect(screen.getByText(/Codex · 今天 .+ · 查询成功/)).toBeTruthy();
fireEvent.click(screen.getByRole("tab", { name: "最近更新" }));
expect(screen.getByText("实际治理记忆")).toBeTruthy();
const memoryActions = screen.getAllByRole("button", { name: "查看记忆" });
fireEvent.click(memoryActions[memoryActions.length - 1]);
expect(onMemories).toHaveBeenCalledWith("m1");
expect(screen.queryByRole("button", { name: "查看记忆" })).toBeNull();
expect(onMemories).not.toHaveBeenCalled();
fireEvent.click(screen.getByRole("tab", { name: "最近调用" }));
expect(history).toHaveBeenCalledWith({ limit: 20, offset: 0 });
if (CALL_HISTORY_ENTRY_VISIBLE) {
@@ -247,11 +246,56 @@ it("shows latest recorded call separately from newly generated memory and opens
} else {
// D96: the recent call still shows on the home page; only the way into
// the call-history page is hidden.
expect(screen.getByText(/今天 .+ · 查询成功/)).toBeTruthy();
expect(screen.getByText(/Codex · 今天 .+ · 查询成功/)).toBeTruthy();
expect(screen.queryByRole("button", { name: "查看记录" })).toBeNull();
expect(onCalls).not.toHaveBeenCalled();
}
});
it("skips direct diagnostics and shows the latest attributed application call", async () => {
history.mockResolvedValue({
available: true,
recording_error: false,
items: [
{
event_id: "direct-diagnostic",
host_id: null,
state: "grounded",
occurred_at: new Date().toISOString(),
memory_count: 1,
raw_count: 0,
references: [],
},
{
event_id: "workbuddy-call",
host_id: "workbuddy",
state: "grounded",
occurred_at: new Date(Date.now() - 60_000).toISOString(),
memory_count: 5,
raw_count: 0,
references: [],
},
],
});
render(
<ApplicationsHome
imports={imports}
onManage={() => {}}
onMemories={() => {}}
onSettings={() => {}}
onCalls={() => {}}
/>,
);
fireEvent.click(await screen.findByRole("tab", { name: "最近调用" }));
expect(await screen.findByText("WorkBuddy 查到 5 条相关内容")).toBeTruthy();
expect(screen.queryByText(/直接调用/)).toBeNull();
expect(screen.getByText(/WorkBuddy · 今天 .+ · 查询成功/)).toBeTruthy();
const direction = document.querySelector(".r6-home-call-direction");
expect(direction?.querySelector("img")?.getAttribute("src")).toContain(
"/r6/workbuddy.png",
);
expect(direction?.querySelector(".r6-app-initial")).toBeNull();
});
it("shows only the host for memory hits whose references have no source", async () => {
history.mockResolvedValue({
available: true,
@@ -834,10 +878,38 @@ it.each([null, "", "not-a-date"])(
/>,
);
await screen.findByText("实际治理记忆");
expect(screen.getByText(/时间未知.*版本 2/)).toBeTruthy();
expect(screen.getByText("时间未知 · App 0")).toBeTruthy();
},
);
it("shows only time and source for an SDK recent update", async () => {
summary.mockResolvedValue({
memory_count: 1,
latest: {
memory_id: "sdk-memory-1",
content: "SDK 原生记忆",
layer: "l2_fact",
source: "app-0",
gmt_created: 1_761_000_000,
},
observed_at: "2026-09-20T09:00:00Z",
});
render(
<ApplicationsHome
imports={imports}
onManage={() => {}}
onMemories={() => {}}
onSettings={() => {}}
/>,
);
await screen.findByText("SDK 原生记忆");
expect(screen.getByText(/^\d{2}:\d{2} · App 0$/)).toBeTruthy();
expect(document.body.textContent).not.toContain("SDK l2_fact");
expect(screen.queryByRole("button", { name: "查看记忆" })).toBeNull();
});
it("does not treat an available adapter as an installed application", async () => {
render(
<ApplicationsHome
+22 -16
View File
@@ -83,13 +83,6 @@ function stamped(value: string, locale: GuiLocale, t: Translate, suffix = "") {
return `${parts.today ? t("apps.today") : ""}${parts.clock}${suffix}`;
}
function memoryAction(action: string | undefined, t: Translate) {
if (action === "create") return t("apps.newMemory");
if (action === "correct" || action === "restore")
return t("apps.updatedMemory");
return t("apps.saved");
}
type ActivityChoice = "call" | "update";
function sourceMatchesHost(sourceId: string, hostId: string | null) {
@@ -101,6 +94,10 @@ function successfulCall(item: CallEvent) {
return item.state === "grounded" || item.state === "empty";
}
function attributedSuccessfulCall(item: CallEvent) {
return item.host_id !== null && successfulCall(item);
}
function callAttention(events: CallEvent[]) {
const hosts = new Set<string>();
for (const event of events) {
@@ -219,7 +216,10 @@ export function ApplicationsHome({
const result = await getCallHistory({ limit: 20, offset: 0 });
if (cancelled) return;
setCallEvents(result.items);
setRecentCall(result.items.find(successfulCall) ?? null);
// The home card promises an application identity. Direct diagnostic
// calls have no host and remain available in call history, but cannot
// truthfully supply the icon and name required by this surface.
setRecentCall(result.items.find(attributedSuccessfulCall) ?? null);
setCallState(
!result.available
? "disabled"
@@ -604,6 +604,9 @@ export function ApplicationsHome({
const recentCallHost = imports.sources.find((source) =>
sourceMatchesHost(source.source_id, recentCall?.host_id ?? null),
);
const recentCallHostName = recentCall
? (recentCallHost?.display_name ?? callHostName(recentCall.host_id, locale))
: null;
const attention = callAttention(callEvents);
const attentionHost = attention ? callHostName(attention.host, locale) : null;
const activity = summaryUnavailable
@@ -665,6 +668,11 @@ export function ApplicationsHome({
: latest.source
? [latest.source]
: [];
const latestSourceNames = latestSourceIds.map(
(id) =>
imports.sources.find((source) => source.source_id === id)?.display_name ??
id,
);
const time = latestClock?.clock ?? (latest ? t("apps.unknownTime") : null);
const checked = visible ? clockParts(visible.observed_at, locale) : null;
// A failed read leaves the last health in place on purpose: clearing it would
@@ -936,8 +944,7 @@ export function ApplicationsHome({
: activity === "call" && recentCall
? callDetailHeadline(
recentCall,
recentCallHost?.display_name ??
callHostName(recentCall.host_id, locale),
recentCallHostName ?? undefined,
locale,
)
: activity === "memory" && latest
@@ -958,19 +965,18 @@ export function ApplicationsHome({
? t("apps.callReadFailure")
: t("apps.memoryReadFailure")
: activity === "call" && recentCall
? `${stamped(recentCall.occurred_at, locale, t)} · ${callRowDetail(recentCall, locale)}${callState === "incomplete" ? t("apps.incompleteCalls") : ""}`
? `${recentCallHostName} · ${stamped(recentCall.occurred_at, locale, t)} · ${callRowDetail(recentCall, locale)}${callState === "incomplete" ? t("apps.incompleteCalls") : ""}`
: activity === "memory" && latest
? "revision" in latest
? `${time} · ${memoryAction(latest.action, t)} · ${latestSourceIds.map((id) => imports.sources.find((s) => s.source_id === id)?.display_name ?? id).join(", ")} · ${t("apps.revision", { count: new Intl.NumberFormat(locale).format(latest.revision) })}`
: `${time} · SDK ${latest.layer} · ${latestSourceIds.map((id) => imports.sources.find((s) => s.source_id === id)?.display_name ?? id).join("、")}`
? [time, latestSourceNames.join(", ")]
.filter(Boolean)
.join(" · ")
: activity === "importing"
? t("apps.savingConversations")
: t("apps.waitingMemories")}
</small>
</section>
{(activity === "unavailable" ||
(activity === "call" && CALL_HISTORY_ENTRY_VISIBLE) ||
activity === "memory") && (
(activity === "call" && CALL_HISTORY_ENTRY_VISIBLE)) && (
<button
type="button"
className="r6-home-activity-link"
@@ -58,6 +58,40 @@ it.each(["zh-CN", "en", "es"] as const)(
},
);
it.each(["system", "zh-CN", "en", "es"] as const)(
"keeps one stable settings row for the %s preference",
(preference) => {
localStorage.setItem(LANGUAGE_STORAGE_KEY, preference);
render(
<LanguageProvider>
<LanguageSelector />
</LanguageProvider>,
);
const selector = screen.getByRole("combobox") as HTMLSelectElement;
const row = selector.closest(".r6-language-setting-row");
expect(selector.value).toBe(preference);
expect(row).toBeTruthy();
expect(row?.querySelector("label")?.hidden).toBe(false);
expect(
screen
.getByRole("option", {
name: /简体中文|Simplified Chinese|Chino simplificado/,
})
.getAttribute("lang"),
).toBe("zh-CN");
expect(
screen
.getByRole("option", { name: /英语|English|Inglés/ })
.getAttribute("lang"),
).toBe("en");
expect(
screen
.getByRole("option", { name: /西班牙语|Spanish|Español/ })
.getAttribute("lang"),
).toBe("es");
},
);
it("offers an accessible selector, persists and updates its label", async () => {
localStorage.setItem(LANGUAGE_STORAGE_KEY, "zh-CN");
render(
+18 -6
View File
@@ -13,8 +13,16 @@ export function LanguageSelector({ compact = false }: { compact?: boolean }) {
compact ? "r6-language-selector--compact" : "r6-language-setting"
}
>
{!compact && <h2 id={`${id}-title`}>{t("languageTitle")}</h2>}
<div className={compact ? "r6-language-selector-row" : "r6-setting-row"}>
{!compact && (
<h2 id={`${id}-title`} className="r6-language-setting-title">
{t("languageTitle")}
</h2>
)}
<div
className={
compact ? "r6-language-selector-row" : "r6-language-setting-row"
}
>
<label id={`${id}-label`} htmlFor={`${id}-select`} hidden={compact}>
{t(compact ? "languageTitle" : "languageLabel")}
</label>
@@ -36,18 +44,22 @@ export function LanguageSelector({ compact = false }: { compact?: boolean }) {
}}
>
<option value="system">{t("languageSystem")}</option>
<option value="zh-CN" lang={compact ? "zh-CN" : undefined}>
<option value="zh-CN" lang="zh-CN">
{compact ? "简体中文" : t("languageChinese")}
</option>
<option value="en" lang={compact ? "en" : undefined}>
<option value="en" lang="en">
{compact ? "English" : t("languageEnglish")}
</option>
<option value="es" lang={compact ? "es" : undefined}>
<option value="es" lang="es">
{compact ? "Español" : t("languageSpanish")}
</option>
</select>
</div>
{!compact && <p id={`${id}-description`}>{t("languageDescription")}</p>}
{!compact && (
<p id={`${id}-description`} className="r6-language-setting-description">
{t("languageDescription")}
</p>
)}
{error && (
<p role="alert" className="r6-settings-error">
{t(error)}
@@ -82,3 +82,38 @@ it("SYS-01: reduced motion skips animation and does not require animationend", (
);
expect(screen.queryByRole("dialog")).toBeNull();
});
it("starts a new or resumed detail at the top without resetting the current detail", () => {
vi.stubGlobal("matchMedia", () => ({
matches: false,
addEventListener() {},
removeEventListener() {},
}));
const content = (scrollResetKey: string, suspended = false) => (
<MemoryEvidencePanel
open
suspended={suspended}
scrollResetKey={scrollResetKey}
onClose={() => {}}
>
<p>{scrollResetKey}</p>
</MemoryEvidencePanel>
);
const view = render(content("memory-a"));
const panel = view.container.querySelector<HTMLElement>(".map-inspector");
expect(panel).toBeTruthy();
if (!panel) return;
panel.scrollTop = 180;
view.rerender(content("memory-a"));
expect(panel.scrollTop).toBe(180);
view.rerender(content("memory-b"));
expect(panel.scrollTop).toBe(0);
panel.scrollTop = 120;
view.rerender(content("memory-b", true));
expect(panel.scrollTop).toBe(120);
view.rerender(content("memory-b"));
expect(panel.scrollTop).toBe(0);
});
@@ -15,11 +15,14 @@ export function MemoryEvidencePanel({
onClose,
children,
suspended = false,
scrollResetKey,
}: {
open: boolean;
onClose(): void;
children: ReactNode;
suspended?: boolean;
/** A different detail starts at the top instead of inheriting old scroll. */
scrollResetKey?: string | null;
}) {
const { text } = useMemoryLanguage();
const compact = useMediaPreference("(max-width: 1100px)");
@@ -37,9 +40,21 @@ export function MemoryEvidencePanel({
reduceMotion,
reduceTransparency,
});
const priorScrollOwner = useRef({ key: scrollResetKey, suspended });
const present = !compact || open || phase !== "closed";
const modal = compact && present && !suspended;
useLayoutEffect(() => {
const previous = priorScrollOwner.current;
priorScrollOwner.current = { key: scrollResetKey, suspended };
if (
!suspended &&
(previous.key !== scrollResetKey || previous.suspended) &&
panel.current
)
panel.current.scrollTop = 0;
}, [scrollResetKey, suspended]);
useLayoutEffect(() => {
const element = panel.current;
if (!element) return;
+9 -33
View File
@@ -186,18 +186,17 @@ it("OBS-19: Enter in the memory filter submits it; submitting it again re-reads"
/>,
);
await act(async () => {});
await userEvent.click(screen.getByRole("button", { name: "刷新地图记忆" }));
expect(onRetry).toHaveBeenCalledTimes(1);
expect(screen.queryByRole("button", { name: "刷新地图记忆" })).toBeNull();
// Typing alone does not search; Enter submits the words.
const input = screen.getByRole("textbox", { name: "筛选地图记忆" });
await userEvent.type(input, "第一");
expect(onQuery).not.toHaveBeenCalled();
await userEvent.type(input, "{Enter}");
expect(onQuery).toHaveBeenCalledWith("第一");
// Submitting the words already applied re-reads, like the refresh button.
// Submitting the currently applied query re-reads without a separate row.
await userEvent.clear(input);
await userEvent.type(input, "{Enter}");
expect(onRetry).toHaveBeenCalledTimes(2);
expect(onRetry).toHaveBeenCalledOnce();
});
it("searches only when 搜索 is pressed, not on every keystroke", async () => {
@@ -223,35 +222,6 @@ it("searches only when 搜索 is pressed, not on every keystroke", async () => {
expect(resetPage).toHaveBeenCalled();
});
it("clearing a submitted keyword immediately shows every memory from the selected source", async () => {
const onQuery = vi.fn();
const onSource = vi.fn();
const resetPage = vi.fn();
render(
<Host
simplified
libraryView={{
...pagedView({ items: [], total: 0, offset: 0, limit: 100 }),
query: "github",
source: "workbuddy",
onQuery,
onSource,
resetPage,
}}
/>,
);
const input = screen.getByRole<HTMLInputElement>("textbox", {
name: "筛选地图记忆",
});
expect(input.value).toBe("github");
await userEvent.clear(input);
expect(onQuery).toHaveBeenCalledExactlyOnceWith("");
expect(resetPage).toHaveBeenCalledOnce();
expect(onSource).not.toHaveBeenCalled();
});
it("OBS-15: switching memory shows a read in progress instead of the previous memory", async () => {
const user = userEvent.setup();
// The read is deliberately never answered: what matters is what is on
@@ -539,6 +509,12 @@ it("D96: lists a memory by when its conversation happened, not when it was gener
/共 519 条,第 2 \/ 6 页/,
),
).toBeTruthy();
expect(screen.getAllByText(/共 519 条/)).toHaveLength(1);
expect(
screen
.getByRole("navigation", { name: "地图记忆分页" })
.classList.contains("map-pagination"),
).toBe(true);
});
it("names the SDK's no-source bucket 未识别来源, apart from a source under review", () => {
+14 -51
View File
@@ -16,7 +16,6 @@ import {
ChevronRight,
FileText,
Network,
RefreshCw,
Search,
X,
} from "lucide-react";
@@ -679,18 +678,7 @@ export function MemoryMap(props: Props) {
}}
onChange={(e) => {
if (library) {
const next = e.target.value;
setSearchDraft(next);
// An empty search box means "no keyword filter". Keeping the
// last submitted query active after the reader removed its
// text made the source picker look empty even when that app
// had memories.
if (!next && library.query.trim()) {
setFilterNotice("");
clear();
library.onQuery("");
library.resetPage();
}
setSearchDraft(e.target.value);
return;
}
setFilterNotice("");
@@ -707,43 +695,17 @@ export function MemoryMap(props: Props) {
</button>
) : null}
</form>
{library && (
<div className="map-list-heading" aria-live="polite">
<span>
{library.stale
? library.page && library.page.total > 0
? text(
"共 {count} 条 · 正在核对来源…",
{ count: plain(library.page.total) },
library.page.total,
)
: text("正在核对来源…")
: library.failed
? text("读取失败")
: library.loading && library.page?.total === 0
? text("正在读取…")
: library.page
? `${text(
"共 {count} 条",
{ count: plain(library.page.total) },
library.page.total,
)}${refreshing ? text(" · 正在核对…") : ""}`
: text("正在读取…")}
</span>
{!library.stale && (
<button
type="button"
className="quiet map-tool"
aria-label={text("刷新地图记忆")}
title={text("刷新地图记忆")}
disabled={library.loading || hidden}
onClick={library.onRetry}
>
<RefreshCw size={16} aria-hidden="true" />
</button>
)}
</div>
)}
{library && (library.loading || library.stale || library.failed) ? (
<p className="map-list-status" aria-live="polite">
{library.failed
? text("读取失败")
: library.stale
? text("正在核对来源…")
: refreshing && library.page?.total !== 0
? text("正在核对…")
: text("正在读取…")}
</p>
) : null}
{!props.simplified && (
<div className="map-groups">
<button
@@ -884,7 +846,7 @@ export function MemoryMap(props: Props) {
!library.loading &&
!library.failed &&
!library.stale && (
<nav aria-label={text("地图记忆分页")} className="map-list-heading">
<nav aria-label={text("地图记忆分页")} className="map-pagination">
<button
type="button"
className="quiet map-tool"
@@ -1100,6 +1062,7 @@ export function MemoryMap(props: Props) {
<MemoryEvidencePanel
open={inspectorOpen && !!(activeId || visibleGroupNode)}
suspended={overlaysSuspended}
scrollResetKey={activeId ?? visibleGroupNode?.node_id ?? "empty"}
onClose={closeInspector}
>
<header className="map-inspector-heading">
@@ -26,7 +26,7 @@ it("opens a Spanish composer without saving, processing or enabling anything", a
screen.getByRole("textbox", { name: "¿Qué quieres recordar?" }),
).toBeTruthy();
expect(
(screen.getByRole("button", { name: "Guardar texto" }) as HTMLButtonElement)
(screen.getByRole("button", { name: "Guardar" }) as HTMLButtonElement)
.disabled,
).toBe(true);
await userEvent.click(
@@ -165,12 +165,12 @@ it("saves from an empty library with the linked-app scope by default", async ()
expect(document.activeElement).toBe(input);
await userEvent.type(input, note.text);
expect(
screen.getByRole("button", { name: "保存原话" }).hasAttribute("disabled"),
screen.getByRole("button", { name: "保存" }).hasAttribute("disabled"),
).toBe(false);
expect(
screen.queryByRole("checkbox", { name: /允许已关联应用读取/ }),
).toBeNull();
await userEvent.click(screen.getByRole("button", { name: "保存原话" }));
await userEvent.click(screen.getByRole("button", { name: "保存" }));
await waitFor(() =>
expect(api.saveMemoryNote).toHaveBeenCalledWith(
{
@@ -208,7 +208,7 @@ it("preserves draft and request identity after an uncertain save", async () => {
page();
await userEvent.click(screen.getByRole("button", { name: "添加记忆" }));
await userEvent.type(screen.getByRole("textbox"), note.text);
await userEvent.click(screen.getByRole("button", { name: "保存原话" }));
await userEvent.click(screen.getByRole("button", { name: "保存" }));
expect(await screen.findByRole("alert")).toBeTruthy();
expect((screen.getByRole("textbox") as HTMLTextAreaElement).value).toBe(
note.text,
@@ -219,7 +219,7 @@ it("preserves draft and request identity after an uncertain save", async () => {
expect(result?.status).toBe("unknown");
expect(result?.reason).toBe("save_result_unconfirmed");
expect(result?.status).not.toBe("failed");
await userEvent.click(screen.getByRole("button", { name: "保存原话" }));
await userEvent.click(screen.getByRole("button", { name: "保存" }));
expect(api.saveMemoryNote.mock.calls[0][0].request_id).toBe(
api.saveMemoryNote.mock.calls[1][0].request_id,
);
@@ -235,7 +235,7 @@ it("closes as saved when the POST response is lost but the operation exists", as
const callbacks = page();
await userEvent.click(screen.getByRole("button", { name: "添加记忆" }));
await userEvent.type(screen.getByRole("textbox"), note.text);
await userEvent.click(screen.getByRole("button", { name: "保存原话" }));
await userEvent.click(screen.getByRole("button", { name: "保存" }));
await waitFor(() =>
expect(api.getMemoryNoteOperation).toHaveBeenCalledOnce(),
);
@@ -292,7 +292,7 @@ it("does not ask for separate consent when an old note is waiting for permission
page();
await userEvent.click(screen.getByRole("button", { name: "添加记忆" }));
expect(await screen.findByText("原话已保存,等待整理")).toBeTruthy();
expect(await screen.findByText("正在等待整理")).toBeTruthy();
expect(screen.queryByRole("button", { name: "模型授权" })).toBeNull();
await userEvent.click(screen.getByRole("button", { name: "重新整理" }));
expect(api.processMemoryNote).toHaveBeenCalledExactlyOnceWith(
+3 -3
View File
@@ -16,8 +16,8 @@ import { isDefiniteRefusal } from "./write-outcome";
const states: Record<MemoryNoteReceipt["state"], string> = {
waiting_for_model: "原话已保存,等待可用模型",
waiting_for_permission: "原话已保存,等待整理",
pending: "原话已保存,等待整理",
waiting_for_permission: "正在等待整理",
pending: "正在等待整理",
running: "正在整理",
failed: "原话已保存,整理未完成",
ready: "整理已完成",
@@ -371,7 +371,7 @@ export function MemoryNoteComposer({
</button>
)}
<button type="submit" disabled={busy || !text.trim()}>
{copy(busy ? "保存中…" : "保存原话")}
{copy(busy ? "保存中…" : "保存")}
</button>
</div>
</form>
+2 -3
View File
@@ -25,9 +25,7 @@ it("translates memory filters and detail affordances live without refetching", a
expect(
screen.getByRole("textbox", { name: "Filtrar memorias del mapa" }),
).toBeTruthy();
expect(
screen.getByRole("button", { name: "Actualizar memorias" }),
).toBeTruthy();
expect(screen.getByRole("button", { name: "Buscar" })).toBeTruthy();
const calls = reads.sdkLibrary.mock.calls.length;
act(() => {
localStorage.setItem(LANGUAGE_STORAGE_KEY, "zh-CN");
@@ -36,6 +34,7 @@ it("translates memory filters and detail affordances live without refetching", a
);
});
expect(screen.getByRole("heading", { name: "记忆", level: 1 })).toBeTruthy();
expect(screen.getByRole("button", { name: "搜索" })).toBeTruthy();
expect(reads.sdkLibrary).toHaveBeenCalledTimes(calls);
localStorage.clear();
});
+25
View File
@@ -772,6 +772,31 @@ button.rail-button.active::before {
text-overflow: ellipsis;
white-space: nowrap;
}
.map-pagination {
display: flex;
flex: none;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-top: auto;
padding: 12px 6px 0;
border-top: 1px solid var(--bd);
color: var(--tx3);
font-size: 12px;
}
.map-pagination > span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.map-list-status {
flex: none;
margin: 0;
padding: 4px;
color: var(--tx3);
font-size: 12px;
line-height: 20px;
}
.map-filter-notice {
font-size: 12px;
line-height: 1.65;
+26 -6
View File
@@ -24,14 +24,31 @@
border-radius: 16px;
background: #fff;
}
.r6-settings .r6-language-setting h2 {
margin: 0 0 12px;
.r6-language-setting-title {
margin: 0 0 18px;
font-size: 18px;
line-height: 28px;
}
.r6-settings .r6-language-setting-row {
display: grid;
grid-template-columns: 96px minmax(0, 1fr);
align-items: center;
gap: 16px;
min-height: 44px;
}
.r6-settings .r6-language-setting-row label {
min-width: 0;
font-size: 14px;
line-height: 24px;
white-space: nowrap;
}
.r6-settings .r6-language-setting select {
box-sizing: border-box;
width: 100%;
max-width: 100%;
min-height: 36px;
padding: 6px 10px;
min-width: 0;
min-height: 44px;
padding: 8px 12px;
border: 1px solid #dce4eb;
border-radius: 8px;
color: inherit;
@@ -42,8 +59,11 @@
outline: 2px solid #225f9d;
outline-offset: 2px;
}
.r6-settings .r6-language-setting > p {
margin: 12px 0 0;
.r6-language-setting-description {
margin: 16px 0 0;
color: #5e6e7a;
font-size: 13px;
line-height: 22px;
overflow-wrap: anywhere;
}
.r6-settings .r6-settings-layout {
+33
View File
@@ -112,6 +112,8 @@
.r6-shell .r6-memory .map-search {
height: 44px;
margin: 13px 0 21px;
gap: 7px;
padding: 0 4px 0 12px;
border: 1px solid #c6d2da;
background: rgb(255 255 255 / 50%);
border-radius: 8px;
@@ -120,7 +122,33 @@
min-height: 40px;
font-size: 13px;
}
.r6-shell .r6-memory .map-search-submit {
min-height: 34px;
padding: 0 12px;
border: 1px solid rgb(64 87 103 / 18%);
border-radius: 6px;
background: #4f6373;
color: #fff;
font:
500 12px / 20px "Noto Sans SC",
"PingFang SC",
sans-serif;
box-shadow: inset 0 1px 0 rgb(255 255 255 / 18%);
transition:
background-color var(--r6-fast),
border-color var(--r6-fast);
}
.r6-shell .r6-memory .map-search-submit:hover:not(:disabled) {
background: #435966;
border-color: rgb(48 71 87 / 28%);
}
.r6-shell .r6-memory .map-search-submit:focus-visible {
outline: 2px solid #557c93;
outline-offset: 2px;
}
.r6-shell .r6-memory .map-memory-list {
flex: 1 1 0;
min-height: 0;
margin: 0;
padding: 0;
}
@@ -208,6 +236,9 @@
font-weight: 500;
}
.selected-memory-detail {
display: flex;
min-width: 0;
flex-direction: column;
color: #384a57;
font-family: "Noto Sans SC", "PingFang SC", sans-serif;
}
@@ -228,6 +259,8 @@
}
.selected-memory-body {
display: block;
flex: none;
min-width: 0;
font-size: 17px;
line-height: 29px;
letter-spacing: normal;
@@ -1722,6 +1722,7 @@ const baseEnglishMessages = {
"memory:原话已保存,等待可用模型":
"Original saved; waiting for an available model",
"memory:原话已保存,等待整理": "Original saved; waiting for processing",
"memory:正在等待整理": "Awaiting processing",
"memory:正在整理": "Processing",
"memory:原话已保存,整理未完成": "Original saved; processing incomplete",
"memory:整理已完成": "Processing complete",
@@ -288,6 +288,7 @@ export const memoryChineseMessages = {
"memory:重新整理": "重新整理",
"memory:原话已保存,等待可用模型": "原话已保存,等待可用模型",
"memory:原话已保存,等待整理": "原话已保存,等待整理",
"memory:正在等待整理": "正在等待整理",
"memory:正在整理": "正在整理",
"memory:原话已保存,整理未完成": "原话已保存,整理未完成",
"memory:整理已完成": "整理已完成",
@@ -696,6 +697,7 @@ export const memorySpanishMessages: Record<
"memory:重新整理": "Volver a procesar",
"memory:原话已保存,等待可用模型": "Texto guardado; esperando un modelo",
"memory:原话已保存,等待整理": "Texto guardado; pendiente de procesar",
"memory:正在等待整理": "Pendiente de procesar",
"memory:正在整理": "Procesando",
"memory:原话已保存,整理未完成": "Texto guardado; procesamiento incompleto",
"memory:整理已完成": "Procesamiento completado",