settings: the model Codex writes its memories with is picked in Settings › General too, under "Codex auto-review model" (#985, Hu9956: it was only the notebook square on Codex's card, unlabeled beside the subagent squares, and wasn't found; on a Free account two rounds of memory consolidation took the 30-day window from 100 to 34) — a "Codex memories model" row with a line that says what happens: unset, Codex's own gpt-5.6-luna and gpt-5.6-terra whatever model the conversation is on; set, this model. Its button opens the same app model picker as the square, on Codex's own memories field, and a pick goes through the same setPick and /api/set, so the row and the square are one setting: renderAgents draws the row again after every pick, from either place. No Codex with the field to pick (or gateway mode): no row. The square stays; its tooltip and aria-label now read "Codex memories model: …" in place of the bare "memories: …". zh/ja/de strings added. codex-memories-settings (Chromium and WebKit, 440px and 1100px, en/zh/ja/de: the row beside auto-review, no native select, fits the row, a pick posts {codex, memories} and lights the square, Default on the square puts the row back to Codex's own, no click scrolls Settings; no Codex, no row) fails on origin/main 97c2fac1 (no row) and passes 18/18; codex-memories (its square label updated), codex-auto-review, codex-titles, i18n-keys, gui-ja, gui-de and click-scroll pass. No Go change.

This commit is contained in:
yetone committed 2026-10-06 14:13:19 +08:00
1 parent 97c2fac127
commit 14b6b2b2ad
5 files changed
+223 -3

No files matched your search

+28 -1
View File
@@ -240,6 +240,7 @@ function renderAgentsLoading() {
let agentArranging = false, agentRenderPending = false;
function renderAgents() {
renderCodexMemories(); // Settings' row is the same field
if (agentArranging) { agentRenderPending = true; return; }
if (unhideCleared()) return; // it renders again with the hiding dropped
const page = $("#view-agents");
@@ -2696,7 +2697,7 @@ function extraField(a, f) {
b.append(svg(EXTRA_GLYPH[f.label] || EXTRA_GLYPH.tiers, 13, 1.5));
const opt = optionFor(f, f.value);
b.title = f.label === SUB_EFFORT ? subEffortTitle(a, f, opt) : f.menu ? menuTitle(f)
: t("{label}: {value}", { label: t(f.label), value: t(opt?.label || f.value || (f.label === MEMORIES ? MEMORIES_DEFAULT : "same as model")) }) + (opt?.note && !FOLLOWS_MODEL.includes(f.label) ? "\n" + t(opt.note) : "");
: t("{label}: {value}", { label: f.label === MEMORIES ? t("Codex memories model") : t(f.label), value: t(opt?.label || f.value || (f.label === MEMORIES ? MEMORIES_DEFAULT : "same as model")) }) + (opt?.note && !FOLLOWS_MODEL.includes(f.label) ? "\n" + t(opt.note) : "");
b.setAttribute("aria-label", b.title);
b.dataset.key = f.key;
b.onclick = (ev) => openPicker(a, f, b, ev);
@@ -17498,6 +17499,7 @@ function renderTrayUsage(s, keep) {
$("#fullContextSegs").replaceChildren(compactRow);
renderCodexTitles(s);
renderCodexAutoReview(s);
renderCodexMemories();
const rate = s.fx?.rate;
const currencySub = $("#currencySub");
currencySub.textContent = t("What a cost — the Usage page's, the tray panel's, the TUI's and the CLI's — is shown as; a vendor's own balance, already in its own currency, is never converted");
@@ -17767,6 +17769,31 @@ function renderCodexAutoReview(s) {
$("#codexAutoReviewPick").replaceChildren(b);
}
// renderCodexMemories: the model Codex writes its memories with (#985,
// Hu9956: the notebook square on Codex's card was all there was, beside the
// subagent squares, and wasn't found). It is the card's own field, picked
// in the same picker and written the same way, so the row and the square
// show one setting; renderAgents draws the row again after every pick.
// No Codex with a memories field to pick: no row.
function renderCodexMemories() {
const row = $("#codexMemoriesRow");
if (!row) return;
const a = state.agents.find((x) => x.id === "codex");
const f = a?.fields?.find((x) => x.label === MEMORIES);
row.hidden = gatewayMode || !f || (!f.options.length && !f.value);
if (row.hidden) return;
const opt = optionFor(f, f.value);
$("#codexMemoriesSub").textContent = f.value ? t("Codex sums threads up and folds them into its memories with this model, whatever model the conversation is on")
: t("Codex sums threads up and folds them into its memories with its own models, gpt-5.6-luna and gpt-5.6-terra, whatever model the conversation is on");
const b = el("button", "rt-cond on");
b.type = "button";
b.setAttribute("aria-label", t("Codex memories model"));
if (f.value) b.append(icon(opt?.icon || "generic"), el("span", "", opt?.label || f.value));
else b.append(icon("openai"), el("span", "", t("Codex’s own pick")));
b.onclick = (ev) => openPicker(a, f, b, ev);
$("#codexMemoriesPick").replaceChildren(b);
}
// renderImages: the model that describes images to a model that can't see
// them — the one magpie picks, one named, or none, and then such an image
// is turned away.
+9
View File
@@ -531,6 +531,9 @@ const I18N = {
"Codex’s own pick": "由 Codex 决定",
"as Codex picks it": "按 Codex 默认选择",
"Codex’s auto-review decides approvals on this model, whatever model the conversation is on": "Codex 的自动审批(auto-review)用这个模型决定是否批准,不论对话用的是哪个模型",
"Codex memories model": "Codex 记忆整理模型",
"Codex sums threads up and folds them into its memories with this model, whatever model the conversation is on": "Codex 用这个模型总结对话、整理成记忆,不论对话用的是哪个模型",
"Codex sums threads up and folds them into its memories with its own models, gpt-5.6-luna and gpt-5.6-terra, whatever model the conversation is on": "Codex 用自带的 gpt-5.6-luna 和 gpt-5.6-terra 总结对话、整理成记忆,不论对话用的是哪个模型",
"Codex’s auto-review decides approvals on codex-auto-review when it has it, else on the conversation’s model at low effort": "Codex 的自动审批(auto-review)有 codex-auto-review 时用它,否则用当前对话的模型(low 推理强度)",
"no title, nothing sent": "不生成标题,不发出请求",
"Codex asks its own model for each thread’s title through its ChatGPT sign-in, even while the conversation is on magpie’s models": "Codex 经 ChatGPT 登录用自带模型为每个会话生成标题,即使对话用的是 magpie 的模型",
@@ -3978,6 +3981,9 @@ const I18N = {
"Codex’s own pick": "Codex に任せる",
"as Codex picks it": "Codex の既定どおり",
"Codex’s auto-review decides approvals on this model, whatever model the conversation is on": "Codex の自動レビュー(auto-review)は、会話のモデルにかかわらずこのモデルで承認を判断します",
"Codex memories model": "Codex メモリ整理のモデル",
"Codex sums threads up and folds them into its memories with this model, whatever model the conversation is on": "Codex は会話のモデルにかかわらず、このモデルでスレッドを要約し、メモリに整理します",
"Codex sums threads up and folds them into its memories with its own models, gpt-5.6-luna and gpt-5.6-terra, whatever model the conversation is on": "Codex は会話のモデルにかかわらず、自身の gpt-5.6-luna と gpt-5.6-terra でスレッドを要約し、メモリに整理します",
"Codex’s auto-review decides approvals on codex-auto-review when it has it, else on the conversation’s model at low effort": "Codex の自動レビュー(auto-review)は codex-auto-review があればそれで、なければ会話のモデル(low の推論)で承認を判断します",
"no title, nothing sent": "タイトルなし、送信なし",
"Codex asks its own model for each thread’s title through its ChatGPT sign-in, even while the conversation is on magpie’s models": "会話が magpie のモデルで行われていても、Codex は ChatGPT のサインインを通じて自身のモデルに各スレッドのタイトルを生成させます",
@@ -7428,6 +7434,9 @@ const I18N = {
"Codex’s own pick": "Wie Codex es wählt",
"as Codex picks it": "Codex’ Standard",
"Codex’s auto-review decides approvals on this model, whatever model the conversation is on": "Codex’ Auto-Review entscheidet Freigaben mit diesem Modell, egal auf welchem Modell die Unterhaltung läuft",
"Codex memories model": "Codex-Modell für Erinnerungen",
"Codex sums threads up and folds them into its memories with this model, whatever model the conversation is on": "Codex fasst Threads mit diesem Modell zusammen und ordnet sie in seine Erinnerungen ein, egal auf welchem Modell die Unterhaltung läuft",
"Codex sums threads up and folds them into its memories with its own models, gpt-5.6-luna and gpt-5.6-terra, whatever model the conversation is on": "Codex fasst Threads mit seinen eigenen Modellen gpt-5.6-luna und gpt-5.6-terra zusammen und ordnet sie in seine Erinnerungen ein, egal auf welchem Modell die Unterhaltung läuft",
"Codex’s auto-review decides approvals on codex-auto-review when it has it, else on the conversation’s model at low effort": "Codex’ Auto-Review entscheidet Freigaben mit codex-auto-review, wenn vorhanden, sonst mit dem Modell der Unterhaltung bei niedrigem Aufwand",
"no title, nothing sent": "kein Titel, nichts gesendet",
"Codex asks its own model for each thread’s title through its ChatGPT sign-in, even while the conversation is on magpie’s models": "Codex fragt über seine ChatGPT-Anmeldung sein eigenes Modell nach dem Titel jedes Threads, auch wenn die Unterhaltung auf magpie-Modellen läuft",
+4
View File
@@ -277,6 +277,10 @@
<div class="who"><div class="name" data-t>Codex auto-review model</div><div class="sub" id="codexAutoReviewSub"></div></div>
<div class="val" id="codexAutoReviewPick"></div>
</div>
<div class="row pref" id="codexMemoriesRow" hidden>
<div class="who"><div class="name" data-t>Codex memories model</div><div class="sub" id="codexMemoriesSub"></div></div>
<div class="val" id="codexMemoriesPick"></div>
</div>
<div class="row pref">
<div class="who"><div class="name" data-t>Open at login</div><div class="sub" data-t>Start magpie in the tray when you log in, so agents find the gateway from the start</div></div>
<div id="loginSegs"></div>
@@ -0,0 +1,180 @@
// Run with Node's test runner and Playwright on the module path; see README.md.
// Settings' "Codex memories model" (#985, Hu9956: the model Codex writes its
// memories with was only the notebook square on Codex's card, beside the
// subagent squares, and wasn't found). The row sits under "Codex auto-review
// model" with a title and a line that says what happens; unset it is
// Codex's own pick. It opens the app's model picker (no native select), and
// a pick posts Codex's memories field as the card's square does: the square
// lights with it, and Default picked on the square puts the row back. No
// Codex with the field: no row. The square's label names it. No click
// scrolls the page. Chromium and WebKit, en/zh/ja/de, 440px and wide; no
// backend, the API is faked here.
const assert = require("node:assert/strict");
const fs = require("node:fs/promises");
const path = require("node:path");
const { test } = require("node:test");
const { chromium, webkit } = require("playwright");
const assets = path.resolve(__dirname, "../assets");
const models = [{ value: "gpt-5.5", label: "GPT-5.5", ref: "openai/gpt-5.5" }, { value: "gpt-5.4-mini", label: "GPT-5.4 mini", ref: "openai/gpt-5.4-mini" }];
const agents = (codex) => codex ? [{
id: "codex", name: "Codex", path: "/test/config.toml", icon: "codex-color", wired: true,
fields: [
{ key: "model", label: "model", value: "gpt-5.5", options: models },
{ key: "subagent", label: "subagents", value: "", options: models },
{ key: "memories", label: "memories", value: "", options: models },
],
}] : [{ id: "claude", name: "Claude Code", path: "/test/settings.json", icon: "claude-color", fields: [{ key: "model", label: "model", value: "", options: [] }] }];
const settings = (lang) => ({
theme: "light", lang, tray: "panel", quotaLeft: false, currency: "usd",
dock: false, dockWindow: false, proxy: "", redact: false, redactPersonal: false, redactWords: [],
codexWarmup: "", claudeWarmup: "", codexWarmAt: "", claudeWarmAt: "", workbuddyCheckin: false, noStats: false,
trayUsage: "", trayUsageEvery: 3, vision: "", imageGen: "",
version: "0.1.400", dir: "~/.config/magpie", gateway: "http://127.0.0.1:3425",
proxyNow: "none", proxySource: "none", login: false,
visionModels: [], imageGenModels: [], workbuddyCheckins: [], lanURLs: [], titleModels: [],
fx: { rate: 7.2, at: new Date().toISOString(), stale: false },
});
function server(lang, sets, codex) {
let cur = { agents: agents(codex), profiles: [] };
return async (route) => {
const req = route.request(), url = new URL(req.url());
const json = (data) => route.fulfill({ json: data });
if (url.pathname === "/boot.js") return route.fulfill({ contentType: "text/javascript", body: `window.bootPrefs = {lang:"${lang}",theme:"light",web:false};` });
if (url.pathname === "/wails/runtime.js") return route.fulfill({ contentType: "text/javascript", body: "export const Window = {};" });
if (url.pathname === "/api/state") return json({ ...cur, settings: { lang, theme: "light" } });
if (url.pathname === "/api/set") {
const body = req.postDataJSON();
sets.push(body);
cur = JSON.parse(JSON.stringify(cur));
cur.agents.find((a) => a.id === body.agent).fields.find((f) => f.key === body.field).value = body.value;
return json({ ...cur, settings: { lang, theme: "light" } });
}
if (url.pathname === "/api/settings") return json(settings(lang));
if (url.pathname === "/api/usage/quotas") return json([]);
if (url.pathname === "/api/groups") return json({ groups: [], models: [] });
if (url.pathname === "/api/agents/cli") return json({ agents: {}, pending: false });
if (url.pathname === "/api/providers") return json({ providers: [], gateway: { running: true } });
if (url.pathname === "/api/plugins") return json({ plugins: [] });
if (url.pathname.startsWith("/api/")) return json({});
const file = path.join(assets, url.pathname === "/" ? "index.html" : url.pathname);
const contentType = { ".html": "text/html", ".js": "text/javascript", ".css": "text/css", ".svg": "image/svg+xml", ".png": "image/png" }[path.extname(file)];
try {
await route.fulfill({ body: await fs.readFile(file), contentType });
} catch {
await route.fulfill({ status: 404, body: "" });
}
};
}
// the words as i18n.js has them
const W = {
en: { name: "Codex memories model", own: "Codex’s own pick", subOwn: /gpt-5\.6-luna and gpt-5\.6-terra/, subSet: /with this model/, square: "Codex memories model: GPT-5.4 mini", def: "Default" },
zh: { name: "Codex 记忆整理模型", own: "由 Codex 决定", subOwn: /gpt-5\.6-luna 和 gpt-5\.6-terra/, subSet: /用这个模型/, square: "Codex 记忆整理模型:GPT-5.4 mini", def: "默认" },
ja: { name: "Codex メモリ整理のモデル", own: null, subOwn: /gpt-5\.6-luna と gpt-5\.6-terra/, subSet: /このモデルで/, square: null, def: null },
de: { name: "Codex-Modell für Erinnerungen", own: null, subOwn: /gpt-5\.6-luna und gpt-5\.6-terra/, subSet: /mit diesem Modell/, square: null, def: null },
};
const view = (page) => page.locator("#view-settings").evaluate((v) => v.scrollTop);
for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium", "webkit"])) {
for (const width of [440, 1100]) {
for (const lang of ["en", "zh", "ja", "de"]) {
test(`${engine} ${width}px ${lang}: Settings picks the model Codex writes its memories with, the card's square in step`, async (t) => {
const w = W[lang];
const browser = await (engine === "webkit" ? webkit.launch() : chromium.launch({ channel: "chromium" }));
const context = await browser.newContext({ viewport: { width, height: 520 }, reducedMotion: "reduce" });
const page = await context.newPage();
page.setDefaultTimeout(5000);
const errors = [], sets = [];
page.on("pageerror", (e) => errors.push(e.message));
await page.route("**/*", server(lang, sets, true));
t.after(async () => {
if (process.env.ARTIFACT_DIR) {
await fs.mkdir(process.env.ARTIFACT_DIR, { recursive: true });
await page.screenshot({ path: path.join(process.env.ARTIFACT_DIR, `${engine}-${width}-${lang}-codex-memories-settings.png`) });
}
await browser.close();
});
await page.goto("http://magpie.test/?view=settings");
const row = page.locator("#codexMemoriesRow");
const pick = page.locator("#codexMemoriesPick button");
await pick.waitFor();
// beside the other two of Codex's background models
assert.equal(await page.evaluate(() => document.querySelector("#codexAutoReviewRow").nextElementSibling?.id), "codexMemoriesRow");
assert.equal(await row.locator("select").count(), 0, "no native select");
assert.equal((await row.locator(".name").textContent()).trim(), w.name);
if (w.own) assert.equal((await pick.textContent()).trim(), w.own, "Codex's own pick by default");
assert.match(await row.locator(".sub").textContent(), w.subOwn);
// the title, its line and the button fit the row
const fit = await row.evaluate((r) => {
const box = r.getBoundingClientRect();
return [...r.querySelectorAll(".name, .sub, button")].every((e) => {
const b = e.getBoundingClientRect();
return b.width > 0 && b.left >= box.left - 1 && b.right <= box.right + 1;
}) && document.documentElement.scrollWidth <= innerWidth;
});
assert(fit, "the row overflows");
// scrolled by a wheel till the row is mid-view, so a click that
// moved the page would show
const box = await page.locator("#view-settings").boundingBox();
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
const top = () => pick.evaluate((e) => e.getBoundingClientRect().top);
for (let i = 0; i < 60 && (await top()) > 200; i++) { await page.mouse.wheel(0, 100); await page.waitForTimeout(30); }
const before = await view(page);
assert(before > 0, "the settings list must scroll to the row");
// the app's model picker, Default first
await pick.click();
await page.locator("#pop:not([hidden]) #list li").first().waitFor();
assert.equal(await page.locator("#pop").evaluate((e) => e.classList.contains("model-picker")), true);
assert.equal(await page.locator("select").count(), 0, "a native select");
if (w.def) assert((await page.locator("#pop #list li").first().innerText()).includes(w.def));
await page.locator("#pop #list li", { hasText: "GPT-5.4 mini" }).first().click();
await page.locator("#codexMemoriesPick button", { hasText: "GPT-5.4 mini" }).waitFor();
assert.deepEqual(sets, [{ agent: "codex", field: "memories", value: "gpt-5.4-mini" }]);
assert.match(await row.locator(".sub").textContent(), w.subSet);
await page.waitForTimeout(300);
assert.equal(await view(page), before, "picking a model scrolled the page");
// the card's square is the same setting, and says what it is
await page.evaluate(() => show("agents"));
const codex = '.row.agent[data-id="codex"]';
await page.locator(`${codex} .ag-link`).click();
const square = page.locator(`${codex} .extras-cell .field.extra.set[data-key="memories"]`);
await square.waitFor();
if (w.square) assert.equal(await square.getAttribute("aria-label"), w.square);
assert.equal(await square.getAttribute("title"), await square.getAttribute("aria-label"));
assert((await square.getAttribute("title")).startsWith(w.name), "the square's tooltip names it");
await square.click();
await page.locator("#pop:not([hidden]) #list li").first().waitFor();
await page.locator("#pop #list li").first().click();
await page.waitForFunction(() => !document.querySelector('.row.agent[data-id="codex"] .field.extra.set[data-key="memories"]'));
assert.deepEqual(sets.at(-1), { agent: "codex", field: "memories", value: "" });
// and Settings shows Codex's own again
await page.evaluate(() => show("settings"));
if (w.own) await page.locator("#codexMemoriesPick button", { hasText: w.own }).waitFor();
assert.match(await row.locator(".sub").textContent(), w.subOwn);
assert.deepEqual(errors, []);
});
}
}
test(`${engine}: no Codex, no memories row`, async (t) => {
const browser = await (engine === "webkit" ? webkit.launch() : chromium.launch({ channel: "chromium" }));
const page = await browser.newPage({ viewport: { width: 900, height: 520 } });
page.setDefaultTimeout(5000);
const errors = [];
page.on("pageerror", (e) => errors.push(e.message));
await page.route("**/*", server("en", [], false));
t.after(() => browser.close());
await page.goto("http://magpie.test/?view=settings");
await page.locator("#codexAutoReviewPick button").waitFor();
await page.waitForTimeout(300);
assert.equal(await page.locator("#codexMemoriesRow").isHidden(), true);
assert.deepEqual(errors, []);
});
}
+2 -2
View File
@@ -61,8 +61,8 @@ function server(lang, sets) {
// the words as i18n.js has them
const W = {
en: { unset: "memories: Codex's own: gpt-5.6-terra, threads summed up with gpt-5.6-luna", def: "Default", set: "memories: GPT-5.4 mini" },
zh: { unset: "记忆整理模型:Codex 自带的:整理用 gpt-5.6-terra,总结对话用 gpt-5.6-luna", def: "默认", set: "记忆整理模型:GPT-5.4 mini" },
en: { unset: "Codex memories model: Codex's own: gpt-5.6-terra, threads summed up with gpt-5.6-luna", def: "Default", set: "Codex memories model: GPT-5.4 mini" },
zh: { unset: "Codex 记忆整理模型:Codex 自带的:整理用 gpt-5.6-terra,总结对话用 gpt-5.6-luna", def: "默认", set: "Codex 记忆整理模型:GPT-5.4 mini" },
};
const codex = '.row.agent[data-id="codex"]';