Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
099f5ae7c0 | ||
|
|
2735d8693a | ||
|
|
47bbfb4b27 | ||
|
|
3f169c9951 | ||
|
|
cb97aa5822 | ||
|
|
48a5d70026 | ||
|
|
b21c929728 | ||
|
|
b9b3c587ff |
No files matched your search
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 () => {
|
||||
@@ -510,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", () => {
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
ChevronRight,
|
||||
FileText,
|
||||
Network,
|
||||
RefreshCw,
|
||||
Search,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
@@ -696,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
|
||||
@@ -873,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"
|
||||
@@ -1089,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(
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in new issue
Block a user