diff --git a/internal/gui/assets/app.js b/internal/gui/assets/app.js index d62f15c2..5c8e9a9b 100644 --- a/internal/gui/assets/app.js +++ b/internal/gui/assets/app.js @@ -1678,9 +1678,11 @@ async function loadCLIs(again = 0) { } // updateCLI updates one agent's CLI; "" when it went well, else why not. -// Quiet (Update all) leaves the saying to the caller. +// Quiet (Update all) leaves the saying to the caller. Each version it went +// from and to is said once (37FlowAI on X: "Pi 从 v1.0x 更新到 v1.10"). async function updateCLI(a, btn, quiet = false) { if (cliBusy.has(a.id)) return t("{agent} is already being updated", { agent: a.name }); + const from = cliInfo[a.id]?.version; cliBusy.add(a.id); if (btn) paintCLIButton(btn, cliInfo[a.id] || {}, true); // in place: what was clicked stays else paintCLI(a.id); @@ -1688,7 +1690,9 @@ async function updateCLI(a, btn, quiet = false) { try { const c = await api("agents/cli/" + encodeURIComponent(a.id), {}); cliInfo[a.id] = c; - if (!quiet) status(t("{agent} updated to {v}", { agent: a.name, v: c.version }), "ok"); + if (cliAll && quiet) cliAll.changed.push(versionChange(a.name, from, c.version)); + if (!quiet) status(from && from !== c.version ? t("{agent} updated: {from} → {v}", { agent: a.name, from, v: c.version }) + : t("{agent} updated to {v}", { agent: a.name, v: c.version }), "ok"); return ""; } catch (e) { if (!quiet) status(e.message, "err", 12000); @@ -1702,6 +1706,10 @@ async function updateCLI(a, btn, quiet = false) { } } +// "Pi 1.0.9 → 1.1.0", or the name and the version now when the one before +// isn't known or is the same +const versionChange = (name, from, to) => from && to && from !== to ? `${name} ${from} → ${to}` : [name, to].filter(Boolean).join(" "); + // ---------- Update all (#727) ---------- // With two or more agents' CLIs behind, a line above the list updates them // all, one after another (two global npm installs at once can trip over @@ -1710,7 +1718,7 @@ async function updateCLI(a, btn, quiet = false) { // window coming back), saying how the run went or what is left, so the // list under it never jumps for a click — Update all's or a row's pill's. -let cliAll = null; // { n, done, now, failed: [{ name, msg }] } once Update all is clicked +let cliAll = null; // { n, done, now, failed: [{ name, msg }], changed: ["Pi 1.0.9 → 1.1.0"] } once Update all is clicked const updatable = () => (state?.agents || []).filter((a) => cliInfo[a.id]?.update && !cliBusy.has(a.id)); @@ -1739,10 +1747,9 @@ function paintUpdateAll(fresh = false) { b.append(svg(CLI_SPIN, 11, 1.8), el("span", "", t("Updating…"))); parts.push(b); } else if (cliAll) { - const ok = cliAll.n - cliAll.failed.length; - say.textContent = !cliAll.failed.length ? t("{n} agents updated", { n: ok }) - : t("{ok} of {n} agents updated · {failed} didn't: {names}", { ok, n: cliAll.n, failed: cliAll.failed.length, names: cliAll.failed.map((f) => f.name).join(", ") }); - say.title = cliAll.failed.map((f) => f.msg).join("\n"); + say.textContent = updatedSay(); + // one line, cut short when narrow: every change and error in full here + say.title = [...cliAll.changed, ...cliAll.failed.map((f) => f.msg)].join("\n"); } else { // what is behind now, the ones being updated by their own pill too const behind = (state?.agents || []).filter((a) => cliInfo[a.id]?.update || cliBusy.has(a.id)); @@ -1765,7 +1772,7 @@ async function updateAllCLIs() { if (cliAll && cliAll.done < cliAll.n) return; const ups = updatable(); if (!ups.length) return; - cliAll = { n: ups.length, done: 0, now: ups[0].name, failed: [] }; + cliAll = { n: ups.length, done: 0, now: ups[0].name, failed: [], changed: [] }; for (const a of ups) { cliAll.now = a.name; paintUpdateAll(); @@ -1774,10 +1781,19 @@ async function updateAllCLIs() { cliAll.done++; } paintUpdateAll(); - if (!cliAll.failed.length) status(t("{n} agents updated", { n: cliAll.n }), "ok"); + if (!cliAll.failed.length) status(updatedSay(), "ok"); else status(cliAll.failed.map((f) => f.msg).join(" · "), "err", 12000); } +// how an Update all went, with each agent's versions from and to +function updatedSay() { + const ok = cliAll.n - cliAll.failed.length, list = cliAll.changed.join(", "); + if (!cliAll.failed.length) return t("{n} agents updated: {list}", { n: ok, list }); + const names = cliAll.failed.map((f) => f.name).join(", "); + return ok ? t("{ok} of {n} agents updated: {list} · {failed} didn't: {names}", { ok, n: cliAll.n, list, failed: cliAll.failed.length, names }) + : t("{ok} of {n} agents updated · {failed} didn't: {names}", { ok, n: cliAll.n, failed: cliAll.failed.length, names }); +} + // ---------- installing the agents not here (#727) ---------- // Under the list, the agents magpie knows that aren't on this machine, each // with its vendor's install commands to copy into a terminal: the vendor's diff --git a/internal/gui/assets/i18n.js b/internal/gui/assets/i18n.js index 95f1decf..eaeaeaa4 100644 --- a/internal/gui/assets/i18n.js +++ b/internal/gui/assets/i18n.js @@ -481,6 +481,9 @@ const I18N = { "{agent} has an update": "{agent} 有新版本", "Every agent is up to date": "所有 Agent 都已是最新", "{ok} of {n} agents updated · {failed} didn't: {names}": "{n} 个 Agent 中已更新 {ok} 个 · {failed} 个未成功:{names}", + "{agent} updated: {from} → {v}": "{agent} 已更新:{from} → {v}", + "{n} agents updated: {list}": "已更新 {n} 个 Agent:{list}", + "{ok} of {n} agents updated: {list} · {failed} didn't: {names}": "{n} 个 Agent 中已更新 {ok} 个:{list} · {failed} 个未成功:{names}", "Install another agent ({n})": "安装其他 Agent({n})", "CLI not found": "未找到 CLI", "{agent}'s settings are still here ({path}), but its command-line program isn't found: it may have been uninstalled. Install another agent, below the list, has its install command.": "{agent} 的配置还在({path}),但找不到它的命令行程序,可能已被卸载。列表下方的「安装其他 Agent」里有它的安装命令。", @@ -4279,6 +4282,9 @@ const I18N = { "{agent} has an update": "{agent} 有新版本", "Every agent is up to date": "所有 Agent 都已是最新", "{ok} of {n} agents updated · {failed} didn't: {names}": "{n} 個 Agent 中已更新 {ok} 個 · {failed} 個未成功:{names}", + "{agent} updated: {from} → {v}": "{agent} 已更新:{from} → {v}", + "{n} agents updated: {list}": "已更新 {n} 個 Agent:{list}", + "{ok} of {n} agents updated: {list} · {failed} didn't: {names}": "{n} 個 Agent 中已更新 {ok} 個:{list} · {failed} 個未成功:{names}", "Install another agent ({n})": "安裝其他 Agent({n})", "CLI not found": "未找到 CLI", "{agent}'s settings are still here ({path}), but its command-line program isn't found: it may have been uninstalled. Install another agent, below the list, has its install command.": "{agent} 的設定還在({path}),但找不到它的命令列程式,可能已被解除安裝。列表下方的「安裝其他 Agent」裡有它的安裝命令。", @@ -8042,6 +8048,9 @@ const I18N = { "{agent} has an update": "{agent} にアップデートがあります", "Every agent is up to date": "すべてのエージェントが最新です", "{ok} of {n} agents updated · {failed} didn't: {names}": "{n} 個中 {ok} 個のエージェントをアップデートしました · {failed} 個は失敗:{names}", + "{agent} updated: {from} → {v}": "{agent} をアップデートしました:{from} → {v}", + "{n} agents updated: {list}": "{n} 個のエージェントをアップデートしました:{list}", + "{ok} of {n} agents updated: {list} · {failed} didn't: {names}": "{n} 個中 {ok} 個のエージェントをアップデートしました:{list} · {failed} 個は失敗:{names}", "Install another agent ({n})": "他のエージェントをインストール({n})", "CLI not found": "CLI が見つかりません", "{agent}'s settings are still here ({path}), but its command-line program isn't found: it may have been uninstalled. Install another agent, below the list, has its install command.": "{agent} の設定は残っています({path})が、コマンドラインプログラムが見つかりません。アンインストールされた可能性があります。リストの下の「他のエージェントをインストール」にインストールコマンドがあります。", @@ -11823,6 +11832,9 @@ const I18N = { "{agent} has an update": "Für {agent} gibt es ein Update", "Every agent is up to date": "Alle Agenten sind aktuell", "{ok} of {n} agents updated · {failed} didn't: {names}": "{ok} von {n} Agenten aktualisiert · {failed} fehlgeschlagen: {names}", + "{agent} updated: {from} → {v}": "{agent} aktualisiert: {from} → {v}", + "{n} agents updated: {list}": "{n} Agenten aktualisiert: {list}", + "{ok} of {n} agents updated: {list} · {failed} didn't: {names}": "{ok} von {n} Agenten aktualisiert: {list} · {failed} fehlgeschlagen: {names}", "Install another agent ({n})": "Weiteren Agenten installieren ({n})", "CLI not found": "CLI nicht gefunden", "{agent}'s settings are still here ({path}), but its command-line program isn't found: it may have been uninstalled. Install another agent, below the list, has its install command.": "Die Einstellungen von {agent} sind noch da ({path}), aber sein Kommandozeilenprogramm wird nicht gefunden: Es wurde vielleicht deinstalliert. Unter „Weiteren Agenten installieren“ unterhalb der Liste steht sein Installationsbefehl.", diff --git a/internal/gui/tests/agent-update-all.test.cjs b/internal/gui/tests/agent-update-all.test.cjs index 5cf473db..95fb3a1d 100644 --- a/internal/gui/tests/agent-update-all.test.cjs +++ b/internal/gui/tests/agent-update-all.test.cjs @@ -129,8 +129,9 @@ for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium", await page.waitForFunction(() => /didn't/.test(document.querySelector("#agentsUpdates .ag-updates-say")?.textContent || ""), null, { timeout: 4000 }); assert.deepEqual(log.posts, ["codex", "gemini", "crush"]); assert.equal(log.most, 1, "two updates ran at once"); - assert.equal(await bar.locator(".ag-updates-say").textContent(), "2 of 3 agents updated · 1 didn't: Gemini CLI"); - assert.match(await bar.locator(".ag-updates-say").getAttribute("title"), /EACCES/); + // each one updated with the versions it went from and to (37FlowAI on X) + assert.equal(await bar.locator(".ag-updates-say").textContent(), "2 of 3 agents updated: Codex 0.155.1 → 0.159.0, Crush 0.3.0 → 0.4.0 · 1 didn't: Gemini CLI"); + assert.match(await bar.locator(".ag-updates-say").getAttribute("title"), /^Codex 0\.155\.1 → 0\.159\.0\nCrush 0\.3\.0 → 0\.4\.0\n.*EACCES/); assert.match(await page.locator("#status").textContent(), /EACCES/); // updated ones lose their pill; the failed one keeps it to try again assert.equal(await page.locator(`${row("codex")} .ag-ver`).textContent(), "0.159.0"); @@ -142,6 +143,30 @@ for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium", assert.equal(await view.evaluate((v) => v.scrollTop), top, "the run moved the page"); }); + await t.test("all updated: the line lists each one's versions, in one line", async () => { + for (const [lang, want] of [ + ["en", "2 agents updated: Codex 0.155.1 → 0.159.0, Crush 0.3.0 → 0.4.0"], + ["zh", "已更新 2 个 Agent:Codex 0.155.1 → 0.159.0, Crush 0.3.0 → 0.4.0"], + ["ja", "2 個のエージェントをアップデートしました:Codex 0.155.1 → 0.159.0, Crush 0.3.0 → 0.4.0"], + ["de", "2 Agenten aktualisiert: Codex 0.155.1 → 0.159.0, Crush 0.3.0 → 0.4.0"], + ]) { + const page = await open(lang, { clis: { codex: CLIS.codex, crush: CLIS.crush } }); + const bar = page.locator("#agentsUpdates"); + await bar.waitFor(); + const h = await bar.evaluate((b) => b.getBoundingClientRect().height); + await bar.locator("button").click(); + await page.waitForFunction(() => !document.querySelector("#agentsUpdates button"), null, { timeout: 4000 }); + assert.equal(await bar.locator(".ag-updates-say").textContent(), want); + assert.equal(await page.locator("#status").textContent(), want); + assert.equal(await bar.locator(".ag-updates-say").getAttribute("title"), "Codex 0.155.1 → 0.159.0\nCrush 0.3.0 → 0.4.0"); + // narrow: still one line, cut short, the list under it not pushed down + await page.setViewportSize({ width: 420, height: 520 }); + await page.waitForTimeout(200); + assert.equal(await bar.evaluate((b) => b.getBoundingClientRect().height), h, "the line grew"); + assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), true, "the page scrolls sideways"); + } + }); + await t.test("a row's own pill leaves the line where it is", async () => { const page = await open("en", { clis: { codex: CLIS.codex, crush: CLIS.crush } }); const bar = page.locator("#agentsUpdates"); diff --git a/internal/gui/tests/cli-update.test.cjs b/internal/gui/tests/cli-update.test.cjs index 779ddaed..097a9bac 100644 --- a/internal/gui/tests/cli-update.test.cjs +++ b/internal/gui/tests/cli-update.test.cjs @@ -139,7 +139,8 @@ for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium", assert.deepEqual(posts, ["codex"]); assert.equal(await page.locator(`${row("codex")} .ag-ver`).textContent(), "0.159.0"); assert.equal(await page.locator(`${row("codex")} .ag-up`).count(), 0); - assert.match(await page.locator("#status").textContent(), /Codex updated to 0\.159\.0/); + // the version it went from too (37FlowAI on X) + assert.equal(await page.locator("#status").textContent(), "Codex updated: 0.155.1 → 0.159.0"); assert.equal(await view.evaluate((v) => v.scrollTop), top, "the update moved the page"); const now = await page.locator(`${row("codex")} .ag-ver`).evaluate((b) => b.getBoundingClientRect().top); assert(Math.abs(now - was) <= 3, `the row moved ${now - was}px`);