From c76af6822fcffa5fb1069b779a39f60fce148bf3 Mon Sep 17 00:00:00 2001 From: yetone Date: Sun, 27 Sep 2026 21:38:25 +0800 Subject: [PATCH] =?UTF-8?q?ui:=20a=20subscription's=20windows=20side=20by?= =?UTF-8?q?=20side=20put=20every=20count=20under=20its=20name=20once=20one?= =?UTF-8?q?'s=20doesn't=20fit=20beside=20it,=20not=20one=20up=20by=20its?= =?UTF-8?q?=20name=20and=20the=20next=20a=20line=20below=20(#90);=20a=20pr?= =?UTF-8?q?ovider's=20models=20can=20be=20picked=20all=20at=20once=20(thos?= =?UTF-8?q?e=20the=20filter=20shows)=20or=20none,=20and=20their=20list=20s?= =?UTF-8?q?crolls=20on=20its=20own,=20not=20the=20editor=20with=20it=20(#1?= =?UTF-8?q?15);=20DeepSeek=20Harness's=20own=20models=20are=20listed=20und?= =?UTF-8?q?er=20its=20name,=20apart=20from=20magpie's=20DeepSeek=20provide?= =?UTF-8?q?r,=20which=20read=20as=20DeepSeek=20twice=20(#115);=20Hide=20em?= =?UTF-8?q?ails=20says=20when=20it=20is=20on=20=E2=80=94=20a=20struck=20ey?= =?UTF-8?q?e,=20=E9=82=AE=E7=AE=B1=E5=B7=B2=E6=89=93=E7=A0=81=20and=20the?= =?UTF-8?q?=20accent,=20an=20open=20eye=20while=20emails=20show?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/agent/dsh.go | 10 ++++---- internal/gui/assets/app.css | 7 ++++-- internal/gui/assets/app.js | 41 ++++++++++++++++++++++++++++++++- internal/gui/assets/i18n.js | 3 +++ internal/gui/assets/routing.css | 2 +- 5 files changed, 55 insertions(+), 8 deletions(-) diff --git a/internal/agent/dsh.go b/internal/agent/dsh.go index 385876c9..21a851bb 100644 --- a/internal/agent/dsh.go +++ b/internal/agent/dsh.go @@ -31,11 +31,13 @@ const dshMark = "# magpie" // dshKeyRef is the credential dsh signs gateway requests with. const dshKeyRef = "MAGPIE_API_KEY" -// dshModels are the models dsh reaches on its own, as it ships them. +// dshModels are the models dsh reaches on its own, as it ships them — under +// its own name, not DeepSeek: magpie's DeepSeek provider is a group of that +// name too, and two would read as one listed twice (#115). var dshModels = []Option{ - {Value: "deepseek-flash", Label: "DeepSeek-V41-Flash", Icon: "deepseek-color", Group: "DeepSeek"}, - {Value: "deepseek-v4-pro", Label: "DeepSeek-V4-Pro", Icon: "deepseek-color", Group: "DeepSeek"}, - {Value: "deepseek-v4-flash", Label: "DeepSeek-V4-Flash", Icon: "deepseek-color", Group: "DeepSeek"}, + {Value: "deepseek-flash", Label: "DeepSeek-V41-Flash", Icon: "deepseek-color", Group: "DeepSeek Harness"}, + {Value: "deepseek-v4-pro", Label: "DeepSeek-V4-Pro", Icon: "deepseek-color", Group: "DeepSeek Harness"}, + {Value: "deepseek-v4-flash", Label: "DeepSeek-V4-Flash", Icon: "deepseek-color", Group: "DeepSeek Harness"}, } func dsh(home string) *Agent { diff --git a/internal/gui/assets/app.css b/internal/gui/assets/app.css index af3d1bf7..048e1b39 100644 --- a/internal/gui/assets/app.css +++ b/internal/gui/assets/app.css @@ -413,6 +413,7 @@ body.window .field.solo { grid-column: auto; } .quota-labels > span { white-space: nowrap; } .quota-labels .quota-n { max-width: 100%; padding: 0; text-align: left; overflow: hidden; text-overflow: ellipsis; color: var(--fg-2); font-size: inherit; font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap; } .quota-labels .quota-n:hover { color: var(--fg); } +.quota-windows.stacked .quota-n { flex-basis: 100%; } .quota-track { height: 4px; border-radius: 2px; background: var(--pill); overflow: hidden; } .quota-track i { display: block; height: 100%; border-radius: inherit; background: var(--accent); } .subscription-error { font-size: 11px; color: var(--faint); } @@ -1030,8 +1031,10 @@ body.window .ag-sk-icon { width: 22px; height: 22px; border-radius: 6px; } /* model chips: click to expose, click again to hide */ .models { display: flex; flex-direction: column; gap: 6px; } -.models > input { max-width: 260px; } -.mchips { display: flex; flex-wrap: wrap; gap: 4px; max-height: 132px; overflow-y: auto; padding: 1px; } +.mbulk { display: flex; align-items: center; gap: 6px; } +.mbulk > input { max-width: 260px; flex: 1; } +/* a long list scrolls on its own, not the editor with it past its ends */ +.mchips { display: flex; flex-wrap: wrap; gap: 4px; max-height: 132px; overflow-y: auto; overscroll-behavior: contain; padding: 1px; } .mchips::-webkit-scrollbar { width: 0; } .mchip { display: inline-flex; align-items: center; gap: 5px; height: 24px; padding: 0 9px; border-radius: 999px; diff --git a/internal/gui/assets/app.js b/internal/gui/assets/app.js index 0d9b74ef..478f4496 100644 --- a/internal/gui/assets/app.js +++ b/internal/gui/assets/app.js @@ -2936,7 +2936,24 @@ function renderModels(p) { names.append(row); } }; - if (q) { q.oninput = draw; box.append(q); } + // every model at once (those the filter shows, when there is one), or none + const bulk = el("div", "mbulk"); + const allOn = el("button", "text action", t("Select all")); + allOn.title = t("Pick every model listed (those the filter shows)"); + allOn.onclick = () => { + const f = (q?.value || "").trim().toLowerCase(); + const ids = p.models.filter((m) => !f || m.id.toLowerCase().includes(f) || (m.name || "").toLowerCase().includes(f) || (m.default || "").toLowerCase().includes(f)).map((m) => m.id); + draft.chosen = [...draft.chosen, ...ids.filter((id) => !draft.chosen.includes(id))]; + draw(); + }; + const allOff = el("button", "text action", t("Select none")); + allOff.title = t("Unpick every model"); + allOff.onclick = () => { draft.chosen = []; draw(); }; + bulk.append(allOn, allOff); + if (q || p.models.length > 1) { + if (q) { q.oninput = draw; bulk.prepend(q); } + box.append(bulk); + } box.append(chips, names); const foot = el("div", "mfoot"); const add = input(draft.typed || "", t("add a model id…")); @@ -3839,6 +3856,19 @@ function panelQuotaRow(q) { return row; } +// quotaFit puts every window's count under its name once one's doesn't fit +// beside it, so windows side by side read alike rather than one count up +// by its name and the next a line below (#90) +const quotaFit = new ResizeObserver((es) => { + for (const { target: g } of es) { + const wraps = [...g.querySelectorAll(".quota-labels")].some((l) => { + const [name, n] = l.children; + return name.scrollWidth + 6 + n.scrollWidth > l.clientWidth; + }); + g.classList.toggle("stacked", wraps); + } +}); + // quotaWindows: one account's allowance as meters, or why there are none. function quotaWindows(sub) { if (sub.balance) { @@ -3868,6 +3898,7 @@ function quotaWindows(sub) { if (w.resetsAt) quota.title = t("Resets {when}", { when: new Date(w.resetsAt).toLocaleString() }); windows.append(quota); } + quotaFit.observe(windows); return windows; } @@ -4924,6 +4955,8 @@ setInterval(renderUpdateBadge, 15 * 60 * 1000); // a window left open still hear // a tooltip — is swapped for blurred stand-in letters while it's on, as the // page redraws too; the address itself is kept aside to put back. (() => { + const EYE = "M2 12s3.5-8 10-8 10 8 10 8-3.5 8-10 8-10-8-10-8zM12 9a3 3 0 1 0 0 6 3 3 0 0 0 0-6z"; + const EYE_OFF = "M9.9 4.2A10.4 10.4 0 0 1 12 4c6.5 0 10 8 10 8a17.6 17.6 0 0 1-2.2 3.2M6.6 6.6C3.9 8.4 2 12 2 12s3.5 8 10 8a9.7 9.7 0 0 0 5.4-1.6M9.9 9.9a3 3 0 0 0 4.2 4.2M2 2l20 20"; const EMAIL = /[\w.+-]+@[\w-]+(?:\.[\w-]+)+/g, IS_EMAIL = new RegExp(EMAIL.source); // stand-in letters of the address's shape, the same each time it's drawn: // blurred, they read as a name without being one @@ -4982,6 +5015,12 @@ setInterval(renderUpdateBadge, 15 * 60 * 1000); // a window left open still hear }; return (on) => { btn.setAttribute("aria-pressed", String(on)); + // what it is now, in its icon and its words: an open eye while the + // addresses show, struck through once they're hidden + btn.querySelector("path").setAttribute("d", on ? EYE_OFF : EYE); + const label = btn.querySelector("[data-t]"); + label.dataset.en = on ? "Emails hidden" : "Hide emails"; + label.textContent = t(label.dataset.en); view.classList.toggle("masked", on); if (on) { mask(); watch.observe(view, OBS); } else { watch.disconnect(); unmask(); } diff --git a/internal/gui/assets/i18n.js b/internal/gui/assets/i18n.js index 93345fc1..a5537275 100644 --- a/internal/gui/assets/i18n.js +++ b/internal/gui/assets/i18n.js @@ -406,6 +406,8 @@ const I18N = { "Names & levels": "名称与推理档位", "Rename the models agents see, or offer fewer of their reasoning levels": "重命名 Agent 看到的模型,或只提供部分推理档位", "Test models": "测试模型", + "Pick every model listed (those the filter shows)": "选中列出的所有模型(有筛选时只选筛出来的)", + "Unpick every model": "取消选中所有模型", "Send a tiny request to each model agents see, to find the ones that don't answer": "给 Agent 能看到的每个模型发一个极小的请求,找出不通的模型", "{n} of {all} models didn't answer": "{all} 个模型中有 {n} 个没有响应", "All {n} models answered": "{n} 个模型全部可用", @@ -665,6 +667,7 @@ const I18N = { // Gateway: routing, live "live, as the gateway decides": "实时 · 网关正在做的决定", "Hide emails": "邮箱打码", + "Emails hidden": "邮箱已打码", "Add a model": "添加模型", "Add another model": "再添加一个模型", "{api} only · after the others": "仅 {api} · 排在其他之后", diff --git a/internal/gui/assets/routing.css b/internal/gui/assets/routing.css index 2d21fbdc..9400ada4 100644 --- a/internal/gui/assets/routing.css +++ b/internal/gui/assets/routing.css @@ -134,7 +134,7 @@ #view-routing .row-head .rt-livedot + .note { margin-left: 8px; } .rt-mask { display: inline-flex; align-items: center; gap: 5px; } .rt-mask svg { width: 13px; height: 13px; flex: none; } -.rt-mask[aria-pressed="true"] { color: var(--fg); background: var(--pill); box-shadow: inset 0 0 0 1px var(--line); } +.rt-mask[aria-pressed="true"] { color: var(--accent); background: color-mix(in srgb, var(--accent) 13%, transparent); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 38%, transparent); } /* an email, hidden: stand-in letters of its shape, softly blurred — coming in out of focus when asked for */ .pii { filter: blur(2.2px); opacity: .5; user-select: none; }