@songsid/agend 2.3.0-beta.13 → 2.3.0-beta.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/backend/codex-metadata.d.ts +8 -0
  2. package/dist/backend/codex-metadata.js +74 -17
  3. package/dist/backend/codex-metadata.js.map +1 -1
  4. package/dist/backend/credential-profile.d.ts +2 -0
  5. package/dist/backend/credential-profile.js +2 -0
  6. package/dist/backend/credential-profile.js.map +1 -1
  7. package/dist/config.d.ts +1 -1
  8. package/dist/config.js +1 -1
  9. package/dist/config.js.map +1 -1
  10. package/dist/fleet-manager.d.ts +1 -0
  11. package/dist/fleet-manager.js +24 -14
  12. package/dist/fleet-manager.js.map +1 -1
  13. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +4 -1
  14. package/dist/general-knowledge/skills/web-appearance/SKILL.md +3 -2
  15. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +1 -1
  16. package/dist/tmux-exec-helper.d.ts +94 -0
  17. package/dist/tmux-exec-helper.js +407 -0
  18. package/dist/tmux-exec-helper.js.map +1 -0
  19. package/dist/tmux-manager.js +26 -11
  20. package/dist/tmux-manager.js.map +1 -1
  21. package/dist/topic-commands.d.ts +10 -0
  22. package/dist/topic-commands.js +27 -15
  23. package/dist/topic-commands.js.map +1 -1
  24. package/dist/ui/panel-chat.js +18 -11
  25. package/dist/ui/shared/app.css +2 -11
  26. package/dist/ui/shared/card-strings.js +3 -3
  27. package/dist/ui/shared/identity-card.js +21 -21
  28. package/dist/ui/shared/ui-icons.js +0 -1
  29. package/dist/ui/shared/ui-menu.js +12 -5
  30. package/dist/usage/providers.d.ts +10 -1
  31. package/dist/usage/providers.js +63 -3
  32. package/dist/usage/providers.js.map +1 -1
  33. package/dist/usage/usage-api.d.ts +3 -1
  34. package/dist/usage/usage-api.js +5 -4
  35. package/dist/usage/usage-api.js.map +1 -1
  36. package/dist/view-api.js +4 -2
  37. package/dist/view-api.js.map +1 -1
  38. package/dist/web-api.d.ts +0 -3
  39. package/dist/web-api.js +62 -13
  40. package/dist/web-api.js.map +1 -1
  41. package/package.json +1 -1
@@ -26,13 +26,15 @@ import { confirmDialog } from "/assets/ui-confirm.js";
26
26
  import { savePref } from "/assets/app-prefs.js";
27
27
  import { applyTheme, clearAmbient, resolvedMode, themeFor, themeImageUrl, themeMenuItems, themesStore } from "/assets/app-themes.js";
28
28
  import { colorMenuItem } from "/assets/identity-color.js";
29
- import { IdentityCard, useCard } from "/assets/identity-card.js";
29
+ import { brokenPictures, cardFoldable, IdentityCard, useCard } from "/assets/identity-card.js";
30
30
  import { AgentWidget, instanceBinds, widgetFor } from "/assets/agent-widget.js";
31
31
 
32
32
  // #1629 r4 §1: the identity card under the chat's header — hidden by default; shown or not is this device's choice.
33
- // #2217 (the user, 2026-10-19): no row of its own. Shown, its banner sits right under the header with Hide card on it, and its
34
- // picture and bio can be folded away (the banner stays) — this device's choice too, kept the same way. Hidden, a card
35
- // button in the header brings it back (a guest's header too: it has no ⋯ menu).
33
+ // #2217 (the user, 2026-10-19): no row of its own; its picture and bio can be folded away (the banner stays) — this
34
+ // device's choice too, kept the same way. The user (2026-10-20): no control on the card — one card button in the header
35
+ // (a guest's and the public link's header too: they have no ⋯ menu), pressed-looking while the card is shown, opens a
36
+ // small menu of two checkboxes: Show card, and Show picture and bio (disabled while the card is hidden, or when it cannot
37
+ // fold: no banner, or nothing on its row).
36
38
  const CHAT_CARD_KEY = "agend_chat_card";
37
39
  const CHAT_CARD_ROW_KEY = "agend_chat_card_row";
38
40
  const readKey = (k, v) => { try { return localStorage.getItem(k) === v; } catch { return false; } };
@@ -45,15 +47,20 @@ function ChatCard({ name, card }) {
45
47
  const prefs = useStore(chatCardPrefs);
46
48
  if (!card || !prefs.open) return null;
47
49
  return html`<div data-trust="card" class="chat-card open">
48
- <${IdentityCard} name=${name} card=${card} compact=${true} onHide=${() => setChatCardOpen(false)}
49
- rowHidden=${prefs.rowHidden} onToggleRow=${() => setChatCardRowHidden(!prefs.rowHidden)} />
50
+ <${IdentityCard} name=${name} card=${card} compact=${true} rowHidden=${prefs.rowHidden} />
50
51
  </div>`;
51
52
  }
52
- /** The header's way back to a hidden card. */
53
- function ShowCardButton({ card }) {
53
+ /** The header's card menu: ☑ Show card, ☑ Show picture and bio. */
54
+ function CardMenu({ card }) {
54
55
  const prefs = useStore(chatCardPrefs);
55
- if (!card || prefs.open) return null;
56
- return html`<button type="button" class="icon-btn chat-card-show" aria-label=${t("card.show")} title=${t("card.show")} onClick=${() => setChatCardOpen(true)}><${Icon} name="idcard" /></button>`;
56
+ useStore(brokenPictures); // a banner that failed to load: the card cannot fold, the item greys out
57
+ if (!card) return null;
58
+ const items = [
59
+ { key: "card", checkbox: true, checked: prefs.open, label: t("card.show"), onSelect: () => setChatCardOpen(!prefs.open) },
60
+ { key: "row", checkbox: true, checked: !prefs.rowHidden && cardFoldable(card), disabled: !prefs.open || !cardFoldable(card), label: t("card.showRow"),
61
+ onSelect: () => setChatCardRowHidden(!prefs.rowHidden) },
62
+ ];
63
+ return html`<${Menu} items=${items} label=${t("card.menu")} icon="idcard" cls="chat-card-menu" active=${prefs.open} />`;
57
64
  }
58
65
  import "./chat-strings.js";
59
66
  import { confirmedWrite, selfConfirmed } from "./settings-confirm.js";
@@ -315,7 +322,7 @@ function ChatView({ name, inst, lease, navKey, exec, awaiting }) {
315
322
  ${inst.model ? html`<button type="button" class="hd-chip" title=${t("chat.chipModel", modelText(inst))} aria-label=${t("chat.chipModel", modelText(inst))} onClick=${() => pick("model")}>${modelText(inst)}</button>` : null}
316
323
  ${inst.effort ? html`<button type="button" class="hd-chip" title=${t("chat.chipEffort", inst.effort)} aria-label=${t("chat.chipEffort", inst.effort)} onClick=${() => pick("effort")}>${inst.effort}</button>` : null}`;
317
324
  return html`<div class=${`panel p-chat${wrap ? " wrap-code" : ""}`} ref=${view}>
318
- <${PanelHeader} title=${headerName(namesOf(inst || { name }, names.primary))} sub=${sub} nav=${guest ? null : html`<${InstanceSwitch} name=${name} current="chat" />`} notice=${html`<${ChatEffects} name=${name} lease=${lease} element=${effectSurface} store=${store} open=${dialog === "effects"} onPlayback=${publishCanvasPlayback} onClose=${() => setDialog(null)} active=${fxHere} />`}>${fxHere ? html`<${ChatEffectPosters} name=${name} lease=${lease} element=${effectSurface} />` : null}<${ShowCardButton} card=${card} /><${HeaderTools} navKey=${navKey} />${guest ? null : html`<${Menu} items=${items} label=${t("app.more")} />`}</${PanelHeader}>
325
+ <${PanelHeader} title=${headerName(namesOf(inst || { name }, names.primary))} sub=${sub} nav=${guest ? null : html`<${InstanceSwitch} name=${name} current="chat" />`} notice=${html`<${ChatEffects} name=${name} lease=${lease} element=${effectSurface} store=${store} open=${dialog === "effects"} onPlayback=${publishCanvasPlayback} onClose=${() => setDialog(null)} active=${fxHere} />`}>${fxHere ? html`<${ChatEffectPosters} name=${name} lease=${lease} element=${effectSurface} />` : null}<${CardMenu} card=${card} /><${HeaderTools} navKey=${navKey} />${guest ? null : html`<${Menu} items=${items} label=${t("app.more")} />`}</${PanelHeader}>
319
326
  <${ChatCard} key=${name} name=${name} card=${card} />
320
327
  <${AgentWidget} slot="chat_header" widget=${widgetFor(name, "chat_header")} binds=${instanceBinds(inst, exec, awaiting)} />
321
328
  <div class="chat-split" ref=${split}>
@@ -1616,16 +1616,8 @@ strong.org-name { font-size: var(--fs-sm); color: var(--text); }
1616
1616
  .side-widget { padding: 0 var(--s-3) var(--s-2); }
1617
1617
  .aw-sidebar { max-height: 240px; overflow: auto; }
1618
1618
 
1619
- /* The card's own controls (the chat's Hide card, Hide picture and bio): small and round. On the banner they sit on its
1620
- corners over a dark disc, so they read on any picture; on the row, Hide picture and bio takes the row's bottom-right. */
1621
- .id-ctl { flex-shrink: 0; display: inline-grid; place-items: center; width: 28px; height: 28px; padding: 0; border: 1px solid var(--border); border-radius: var(--r-full); background: var(--surface); color: var(--text-2); cursor: pointer; }
1622
- .id-ctl:hover { color: var(--text); border-color: var(--border-strong); }
1623
- .id-row .id-ctl { margin-left: auto; border-color: var(--border-strong); background: var(--surface-2); color: var(--text); }
1624
- .id-row .id-fold { align-self: flex-end; }
1625
- .id-banner-box .id-ctl { position: absolute; z-index: 1; right: var(--s-2); border-color: rgba(255, 255, 255, .35); background: rgba(0, 0, 0, .5); color: #fff; }
1626
- .id-banner-box .id-ctl:hover { background: rgba(0, 0, 0, .7); color: #fff; }
1627
- .id-banner-box .id-hide { top: var(--s-2); }
1628
- .id-banner-box .id-unfold { bottom: var(--s-2); }
1619
+ /* The chat header's card menu button (the user, 2026-10-20: no control on the card): pressed-looking while the card shows. */
1620
+ .chat-card-menu > .icon-btn.active { background: var(--accent-dim); color: var(--accent); }
1629
1621
  /* A phone: the portrait stays on the text's row, smaller (the user's ask: never a portrait alone on a line with the text
1630
1622
  under it); the S / M / L control takes the line below. The chat header's card is trimmed further: a smaller portrait
1631
1623
  and banner, and its tagline and bio cut to a few lines (Details and View show them whole). */
@@ -1638,7 +1630,6 @@ strong.org-name { font-size: var(--fs-sm); color: var(--text); }
1638
1630
  .id-card.card-c.bs-l { --id-bmin: 96px; --id-bmax: 130px; }
1639
1631
  .id-row { flex-wrap: wrap; gap: var(--s-2) var(--s-3); padding: var(--s-2) var(--s-3); }
1640
1632
  .card-size { flex-basis: 100%; justify-content: flex-end; }
1641
- .id-ctl { width: 32px; height: 32px; }
1642
1633
  .id-card.card-c .id-tagline, .id-card.card-c .id-bio { display: -webkit-box; -webkit-box-orient: vertical; overflow: hidden; }
1643
1634
  .id-card.card-c .id-tagline { -webkit-line-clamp: 2; line-clamp: 2; }
1644
1635
  .id-card.card-c .id-bio { -webkit-line-clamp: 3; line-clamp: 3; }
@@ -11,10 +11,10 @@ register("card", {
11
11
  sizeSName: "Small", sizeMName: "Medium", sizeLName: "Large",
12
12
  byHuman: "set by a person",
13
13
  byAgent: "set by the agent",
14
+ menu: "Identity card",
14
15
  show: "Show card",
15
16
  hide: "Hide card",
16
17
  showRow: "Show picture and bio",
17
- hideRow: "Hide picture and bio",
18
18
  bannerSafe: "Every screen",
19
19
  bannerPhone: "Phone · M card",
20
20
  },
@@ -26,10 +26,10 @@ register("card", {
26
26
  sizeSName: "小", sizeMName: "中", sizeLName: "大",
27
27
  byHuman: "由人設定",
28
28
  byAgent: "由 agent 設定",
29
+ menu: "身分卡",
29
30
  show: "顯示卡片",
30
31
  hide: "隱藏卡片",
31
- showRow: "顯示小圖與介紹",
32
- hideRow: "隱藏小圖與介紹",
32
+ showRow: "顯示小圖和介紹",
33
33
  bannerSafe: "所有螢幕",
34
34
  bannerPhone: "手機 · 中卡片",
35
35
  },
@@ -11,7 +11,6 @@ import { html, useEffect, useState } from "./app-html.js";
11
11
  import { t } from "./app-i18n.js";
12
12
  import { createStore, useStore } from "./app-store.js";
13
13
  import { onPrefsChanged, prefInForce, savePref } from "./app-prefs.js";
14
- import { Icon } from "./ui-icons.js";
15
14
  import "./card-strings.js";
16
15
 
17
16
  export const CARD_SIZES = ["s", "m", "l"];
@@ -161,43 +160,45 @@ function SizeControl({ name, size }) {
161
160
  </div>`;
162
161
  }
163
162
 
164
- /** A small round control on the card (the chat's Hide card and Hide picture and bio). */
165
- function CardControl({ cls, icon, label, onClick, expanded }) {
166
- return html`<button type="button" class=${`id-ctl ${cls}`} aria-label=${label} title=${label}
167
- aria-expanded=${expanded === undefined ? undefined : expanded ? "true" : "false"} onClick=${onClick}><${Icon} name=${icon} size=${16} /></button>`;
168
- }
163
+ /**
164
+ * #2217 hotfix (Prism): pictures that failed to load on this page, by src — shared, so the chat's card menu judges a card
165
+ * as drawn (a broken banner is not drawn, and then the card cannot fold), not by its metadata alone.
166
+ */
167
+ export const brokenPictures = createStore({});
168
+ const markBroken = (src) => { if (src && !brokenPictures.get()[src]) brokenPictures.set((s) => ({ ...s, [src]: true })); };
169
+ const drawn = (card, slot) => !!(card && card[slot] && !brokenPictures.get()[card[slot].src]);
170
+ /**
171
+ * #2217: whether a card can fold its row away and keep the banner — a banner drawn, and something on its row (a picture
172
+ * drawn, a tagline or a bio). The chat's card menu enables Show picture and bio only then.
173
+ */
174
+ export const cardFoldable = (card) => !!(card && drawn(card, "banner") && (drawn(card, "portrait") || card.tagline || card.bio));
169
175
 
170
176
  /**
171
177
  * The card. `compact`: the chat header's own small size (no size control; the banner still follows the size choice,
172
178
  * #2038). Nothing when the card is empty. The
173
179
  * tagline and bio say whose they are (title), since a person's field wins over the agent's.
174
- * #2217: the chat's controls, on the card itself (the user, 2026-10-19): `onHide` — Hide card, on the banner's top-right corner
175
- * (on the row when there is no banner); `onToggleRow` — hide the picture, tagline and bio and keep the banner, from the
176
- * row's bottom-right corner, and back from the banner's bottom-right while they are hidden (`rowHidden`). Only a card
177
- * with a banner and something on its row can fold its row (else it would be the card hidden).
180
+ * #2217: `rowHidden` — the chat's picture and bio folded away, the banner kept. The user (2026-10-20): no control on the
181
+ * card at all — the chat header's two buttons show / hide the card and fold / unfold its row. Only a card with a banner
182
+ * and something on its row folds (cardFoldable; else it would be the card hidden).
178
183
  */
179
- export function IdentityCard({ name, card, compact = false, sizeControl = !compact, onHide = null, rowHidden = false, onToggleRow = null }) {
184
+ export function IdentityCard({ name, card, compact = false, sizeControl = !compact, rowHidden = false }) {
180
185
  useStore(cardSizeStore);
181
- const [broken, setBroken] = useState({});
186
+ const broken = useStore(brokenPictures);
182
187
  if (!card) return null;
183
188
  const size = compact ? "c" : cardSizeFor(name);
184
189
  // #2038: the chat header's card is its own small size, but its banner follows the person's size choice too.
185
190
  const bannerSize = compact ? ` bs-${cardSizeFor(name)}` : "";
186
191
  const whose = (f) => t(f.by === "human" ? "card.byHuman" : "card.byAgent");
187
- const pic = (slot, cls, alt) => card[slot] && broken[slot] !== card[slot].src
188
- ? html`<img class=${cls} src=${card[slot].src} alt=${alt} title=${whose(card[slot])} decoding="async" onError=${() => setBroken({ ...broken, [slot]: card[slot].src })} />` : null;
192
+ const pic = (slot, cls, alt) => card[slot] && !broken[card[slot].src]
193
+ ? html`<img class=${cls} src=${card[slot].src} alt=${alt} title=${whose(card[slot])} decoding="async" onError=${() => markBroken(card[slot].src)} />` : null;
189
194
  // #2086: the banner at its focus; on a screen wider than BANNER_MAX_ASPECT it is centred over a blurred copy of itself.
190
195
  const focus = card.banner_focus ? card.banner_focus.value : 50;
191
196
  const atFocus = (el) => { if (el && el.style) el.style.setProperty("--id-focus", `${focus}%`); };
192
197
  const banner = pic("banner", "id-banner", "");
193
198
  const portrait = pic("portrait", "id-portrait", t("card.portraitAlt", name));
194
- const foldable = !!(onToggleRow && banner && (portrait || card.tagline || card.bio));
195
- const rowShown = !(foldable && rowHidden);
196
- const hide = onHide ? html`<${CardControl} cls="id-hide" icon="eyeOff" label=${t("card.hide")} onClick=${onHide} />` : null;
197
- const fold = foldable ? html`<${CardControl} cls=${rowShown ? "id-fold" : "id-fold id-unfold"} icon=${rowShown ? "up" : "user"}
198
- label=${t(rowShown ? "card.hideRow" : "card.showRow")} expanded=${rowShown} onClick=${onToggleRow} />` : null;
199
+ const rowShown = !(rowHidden && banner && cardFoldable(card));
199
200
  return html`<section class=${`id-card card-${size}${bannerSize}${rowShown ? "" : " row-hidden"}`} aria-label=${t("card.label", name)}>
200
- ${banner ? html`<div class="id-banner-box" ref=${atFocus}><img class="id-banner-fill" src=${card.banner.src} alt="" aria-hidden="true" decoding="async" />${banner}${hide}${rowShown ? null : fold}</div>` : null}
201
+ ${banner ? html`<div class="id-banner-box" ref=${atFocus}><img class="id-banner-fill" src=${card.banner.src} alt="" aria-hidden="true" decoding="async" />${banner}</div>` : null}
201
202
  ${rowShown ? html`<div class="id-row">
202
203
  ${portrait}
203
204
  <div class="id-text">
@@ -205,7 +206,6 @@ export function IdentityCard({ name, card, compact = false, sizeControl = !compa
205
206
  ${card.bio ? html`<p class="id-bio" title=${whose(card.bio)}>${card.bio.text}</p>` : null}
206
207
  </div>
207
208
  ${sizeControl ? html`<${SizeControl} name=${name} size=${size} />` : null}
208
- ${banner ? null : hide}${fold}
209
209
  </div>` : null}
210
210
  </section>`;
211
211
  }
@@ -40,7 +40,6 @@ const P = {
40
40
  sort: () => html`<path d="M7 4v16M3 16l4 4 4-4M17 20V4M13 8l4-4 4 4"/>`, // #2179: the sidebar's sort
41
41
  star: () => html`<path d="M12 2.5l2.94 5.96 6.56.95-4.75 4.63 1.12 6.54L12 17.5l-5.87 3.08 1.12-6.54L2.5 9.41l6.56-.95L12 2.5z"/>`,
42
42
  eye: () => html`<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/>`,
43
- eyeOff: () => html`<path d="M3 3l18 18M10.6 5.1A10.9 10.9 0 0 1 12 5c7 0 11 7 11 7a18.6 18.6 0 0 1-3.2 4.2M6.6 6.6C3.5 8.6 1 12 1 12s4 7 11 7a10.6 10.6 0 0 0 5.4-1.6M9.9 9.9a3 3 0 0 0 4.2 4.2"/>`,
44
43
  idcard: () => html`<rect x="2" y="5" width="20" height="14" rx="2"/><circle cx="8" cy="11" r="2"/><path d="M5 16a3 3 0 0 1 6 0M14 10h5M14 14h3"/>`,
45
44
  check: () => html`<path d="M20 6L9 17l-5-5"/>`,
46
45
  inbox: () => html`<path d="M22 12h-6l-2 3h-4l-2-3H2"/><path d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/>`,
@@ -4,21 +4,25 @@ import { html, useEffect, useRef, useState } from "./app-html.js";
4
4
  import { Icon } from "./ui-icons.js";
5
5
  import { t } from "./app-i18n.js";
6
6
 
7
- /** items: [{ key, label, icon?, onSelect, danger?, disabled? }] (falsy entries are skipped). */
8
- export function Menu({ items, label, icon = "more", cls = "" }) {
7
+ /**
8
+ * items: [{ key, label, icon?, onSelect, danger?, disabled? }] (falsy entries are skipped). `checked` makes a radio item;
9
+ * `checkbox: true` with `checked` a checkbox item (#2217 hotfix: the chat's card menu), which toggles and keeps the menu
10
+ * open. `active`: the button shows a pressed look (what it controls is on), without changing its role.
11
+ */
12
+ export function Menu({ items, label, icon = "more", cls = "", active = false }) {
9
13
  const [open, setOpen] = useState(false);
10
14
  const btn = useRef(null), list = useRef(null);
11
15
  const shown = (items || []).filter(Boolean);
12
16
  useEffect(() => {
13
17
  if (!open) return undefined;
14
- const first = list.current && list.current.querySelector("[role=menuitem]:not([disabled])");
18
+ const first = list.current && list.current.querySelector("[role=menuitem]:not([disabled]), [role=menuitemradio]:not([disabled]), [role=menuitemcheckbox]:not([disabled])");
15
19
  if (first) first.focus();
16
20
  const outside = (e) => { if (list.current && !list.current.contains(e.target) && btn.current && !btn.current.contains(e.target)) setOpen(false); };
17
21
  document.addEventListener("pointerdown", outside, true);
18
22
  return () => document.removeEventListener("pointerdown", outside, true);
19
23
  }, [open]);
20
24
  function onKey(e) {
21
- const all = [...list.current.querySelectorAll("[role=menuitem]:not([disabled]), [role=menuitemradio]:not([disabled])")];
25
+ const all = [...list.current.querySelectorAll("[role=menuitem]:not([disabled]), [role=menuitemradio]:not([disabled]), [role=menuitemcheckbox]:not([disabled])")];
22
26
  const i = all.indexOf(document.activeElement);
23
27
  if (e.key === "Escape") { e.preventDefault(); e.stopPropagation(); setOpen(false); btn.current && btn.current.focus(); }
24
28
  else if (e.key === "ArrowDown") { e.preventDefault(); (all[(i + 1) % all.length] || all[0])?.focus(); }
@@ -29,11 +33,14 @@ export function Menu({ items, label, icon = "more", cls = "" }) {
29
33
  }
30
34
  function choose(item) { setOpen(false); btn.current && btn.current.focus(); item.onSelect(); }
31
35
  return html`<div class=${`menu ${cls}`}>
32
- <button ref=${btn} type="button" class="icon-btn" aria-haspopup="menu" aria-expanded=${open ? "true" : "false"}
36
+ <button ref=${btn} type="button" class=${`icon-btn${active ? " active" : ""}`} aria-haspopup="menu" aria-expanded=${open ? "true" : "false"}
33
37
  aria-label=${label || t("app.more")} title=${label || t("app.more")} onClick=${() => setOpen(!open)}><${Icon} name=${icon} /></button>
34
38
  ${open ? html`<div data-trust="menu" ref=${list} class="menu-list" role="menu" aria-label=${label || t("app.more")} onKeyDown=${onKey}>
35
39
  ${shown.map(item => item.heading
36
40
  ? html`<div key=${item.key} class="menu-heading" role="presentation">${item.label}</div>`
41
+ : item.checkbox
42
+ ? html`<button key=${item.key} type="button" role="menuitemcheckbox" aria-checked=${item.checked ? "true" : "false"} class="menu-item menu-radio"
43
+ disabled=${!!item.disabled} onClick=${() => item.onSelect()}><span class="menu-tick" aria-hidden="true">${item.checked ? html`<${Icon} name="check" size=${16} />` : null}</span><span>${item.label}</span></button>`
37
44
  : item.checked !== undefined
38
45
  // #2196: one of a group of choices (Page colours): a radio item, the chosen one ticked.
39
46
  ? html`<button key=${item.key} type="button" role="menuitemradio" aria-checked=${item.checked ? "true" : "false"} class="menu-item menu-radio"
@@ -131,10 +131,17 @@ export declare function shellRcSetsClaudeToken(read: (path: string) => string, h
131
131
  * `storeHome` (a credential profile's config dir, #1699): that profile's login only.
132
132
  */
133
133
  export declare function getClaudeOAuthToken(storeHome?: string): Promise<string | null>;
134
- /** Which subscription a Claude row is (#1699): a profile's (with its name), or the shared one beside profiles. */
134
+ /**
135
+ * Which subscription a Claude row is (#1699): a profile's (with its name), or the shared one beside profiles.
136
+ * #2236: `hostOnly` — a profile on the host that no instance uses: shown only when it has a login (signed out, it is
137
+ * not set up, not something to act on). No row is ever dropped as "the same account as another": which account a
138
+ * login is cannot be proven from config files (a stale `.claude.json`, an env token), and a hidden quota in use is
139
+ * worse than two rows of one subscription (Prism, #2237 review).
140
+ */
135
141
  export interface ClaudeUsageScope {
136
142
  profile?: string;
137
143
  besideProfiles?: boolean;
144
+ hostOnly?: boolean;
138
145
  }
139
146
  /**
140
147
  * Exported for tests — the statusline/API preference order is the subject.
@@ -265,6 +272,8 @@ export declare function fetchMuseUsage(): Promise<Omit<ProviderUsage, "id" | "na
265
272
  * auth files point at these same logical sources.
266
273
  */
267
274
  export declare function providersForConfig(config: UsageFleetConfig, base?: UsageProvider[]): UsageProvider[];
275
+ /** #2236: a Claude login's row — kept on screen whether or not a running instance uses it. */
276
+ export declare function isHostLoginUsageProvider(id: string): boolean;
268
277
  /**
269
278
  * ClassicBot channels are bindings too (#1220): a channel on a credential
270
279
  * profile needs that subscription's row, and one on the shared login keeps the
@@ -20,12 +20,12 @@
20
20
  */
21
21
  import { readFile, writeFile, access, constants } from "node:fs/promises";
22
22
  import { readFileSync, readdirSync } from "node:fs";
23
- import { join, resolve, sep } from "node:path";
23
+ import { dirname, join, resolve, sep } from "node:path";
24
24
  import { execFile } from "node:child_process";
25
25
  import { getAgendHome } from "../paths.js";
26
26
  import { loadFleetConfig } from "../config.js";
27
27
  import { classicProfile, profileName, readClassicBindings } from "../classic-bindings.js";
28
- import { CHAT_PROFILE_LOGIN_BACKENDS, credentialHomeSpec, credentialProfileLoginCommand, credentialProfileStoreHome, instanceCredentialProfile, } from "../backend/credential-profile.js";
28
+ import { CHAT_PROFILE_LOGIN_BACKENDS, credentialHomeSpec, credentialProfileHome, credentialProfileLoginCommand, credentialProfileStoreHome, instanceCredentialProfile, isCredentialProfileName, } from "../backend/credential-profile.js";
29
29
  import { claudeKeychainService } from "../backend/claude-code.js";
30
30
  import { readKiroAuthTokens } from "../backend/kiro-auth-store.js";
31
31
  import { homedir, userInfo } from "node:os";
@@ -364,6 +364,9 @@ export async function fetchClaudeUsage(storeHome, scope = {}) {
364
364
  // limits down. This is the case the whole statusline source exists for.
365
365
  if (statusline)
366
366
  return statuslineUsage(statusline, null);
367
+ // #2236: a profile on the host that no instance uses, signed out: not set up here — no row.
368
+ if (profileHome !== null && scope.hostOnly)
369
+ return { status: "no-credentials", hint: "Not logged in.", metrics: [] };
367
370
  if (profileHome !== null) {
368
371
  // On screen (`ok`, as kiro's signed-out row), with what signs this profile in — never a fallback: from chat
369
372
  // (`/login claude-code <profile>`, #1699 part 2), or the exact line on the host — the same words as the auth alert.
@@ -510,9 +513,14 @@ export async function fetchClaudeUsage(storeHome, scope = {}) {
510
513
  // the app), so embedding it is the accepted norm, not a leaked key. It is
511
514
  // required for the refresh-token grant.
512
515
  const AGY_TOKEN_FILE = ["antigravity-cli", "antigravity-oauth-token"];
516
+ /**
517
+ * The production base first: checked live 2026-10-20 (a user's account, read-only), the daily base's quota summary had
518
+ * only the "Claude and GPT models" group while the production base's had "Gemini Models" too — the Gemini line then
519
+ * fell back to the busiest single model. The daily base stays as the fallback.
520
+ */
513
521
  const AGY_CLOUDCODE_URLS = [
514
- "https://daily-cloudcode-pa.googleapis.com",
515
522
  "https://cloudcode-pa.googleapis.com",
523
+ "https://daily-cloudcode-pa.googleapis.com",
516
524
  ];
517
525
  /**
518
526
  * The Google installed-app OAuth client pair, READ FROM THE INSTALLED agy
@@ -592,6 +600,11 @@ function agyPool(b, id) {
592
600
  const pooled = AGY_POOLED[id];
593
601
  if (pooled)
594
602
  return pooled.pool;
603
+ // #2240: Gemini 4 Argon has a quota of its own on paid Pro plans ("Argon quota is separate", as agy shows it): its own pool,
604
+ // from a summary group or from its models (gemini-4-argon-*), never folded into Gemini's worst-of by the rule below.
605
+ const argon = agyArgonPool(b.group) ?? agyArgonPool(id);
606
+ if (argon)
607
+ return argon;
595
608
  const group = b.group;
596
609
  if (group) {
597
610
  if (/gemini/i.test(group))
@@ -615,6 +628,13 @@ function agyPool(b, id) {
615
628
  return "Claude & others";
616
629
  return id;
617
630
  }
631
+ /** "Gemini 4 Argon", "GEMINI 4 ARGON", "gemini-4-argon-high" → "Gemini 4 Argon"; any other Argon → "Argon"; else null. */
632
+ function agyArgonPool(text) {
633
+ if (!text || !/argon/i.test(text))
634
+ return null;
635
+ const version = /gemini[\s_-]*(\d+(?:\.\d+)?)[\s_-]*argon/i.exec(text);
636
+ return version ? `Gemini ${version[1]} Argon` : "Argon";
637
+ }
618
638
  /** The limit a bucket counts against ("Five Hour Limit", window "5h" …), or null for a per-model bucket. */
619
639
  function agyWindow(b, id) {
620
640
  const pooled = AGY_POOLED[id];
@@ -1523,6 +1543,8 @@ export function providersForConfig(config, base = DEFAULT_PROVIDERS) {
1523
1543
  const backendName = usageBackendForProviderId(provider.id);
1524
1544
  if (!backendName || !credentialHomeSpec(backendName))
1525
1545
  return [provider];
1546
+ if (provider.id === "claude")
1547
+ return claudeHostRows(config, provider, backendName);
1526
1548
  const profiles = effectiveCredentialProfiles(config, backendName);
1527
1549
  if (profiles.length === 0)
1528
1550
  return [provider];
@@ -1553,6 +1575,44 @@ export function providersForConfig(config, base = DEFAULT_PROVIDERS) {
1553
1575
  return rows;
1554
1576
  });
1555
1577
  }
1578
+ /**
1579
+ * #2236: one row per Claude login ON THE HOST, used by an instance or not — the default (shared) login and every
1580
+ * credential profile there. A subscription at its limit is exactly the one a person stops using; it must not vanish
1581
+ * because nothing runs on it (a user's first account, full, had no row once every instance moved to new profiles).
1582
+ * The shared row is "Claude (default)" beside profiles (shown whenever that login exists); a profile no instance uses
1583
+ * shows only when signed in. isHostLoginUsageProvider keeps these rows through the snapshot's running-instances filter.
1584
+ */
1585
+ function claudeHostRows(config, provider, backendName) {
1586
+ const inUse = effectiveCredentialProfiles(config, backendName);
1587
+ const profiles = [...new Set([...inUse, ...claudeProfilesOnHost()])].sort();
1588
+ if (profiles.length === 0)
1589
+ return [provider];
1590
+ const homes = profiles.map(profile => credentialProfileStoreHome(getAgendHome(), backendName, profile));
1591
+ return [
1592
+ { id: provider.id, name: `${provider.name} (default)`, fetch: () => provider.fetch(undefined, { besideProfiles: true }) },
1593
+ ...profiles.map((profile, i) => ({
1594
+ id: `${provider.id}:${profile}`,
1595
+ name: `${provider.name} (${profile})`,
1596
+ profile,
1597
+ fetch: () => provider.fetch(homes[i], inUse.includes(profile) ? { profile } : { profile, hostOnly: true }),
1598
+ })),
1599
+ ];
1600
+ }
1601
+ /** #2236: the Claude credential profiles on this host (their directories), whether or not an instance uses them. */
1602
+ function claudeProfilesOnHost() {
1603
+ try {
1604
+ return readdirSync(dirname(credentialProfileHome(getAgendHome(), "claude-code", "x")), { withFileTypes: true })
1605
+ .filter(e => e.isDirectory() && isCredentialProfileName(e.name))
1606
+ .map(e => e.name);
1607
+ }
1608
+ catch {
1609
+ return [];
1610
+ }
1611
+ }
1612
+ /** #2236: a Claude login's row — kept on screen whether or not a running instance uses it. */
1613
+ export function isHostLoginUsageProvider(id) {
1614
+ return id === "claude" || id.startsWith("claude:");
1615
+ }
1556
1616
  /**
1557
1617
  * Whether at least one effective fleet binding uses the legacy shared login.
1558
1618
  *