@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.
- package/dist/backend/codex-metadata.d.ts +8 -0
- package/dist/backend/codex-metadata.js +74 -17
- package/dist/backend/codex-metadata.js.map +1 -1
- package/dist/backend/credential-profile.d.ts +2 -0
- package/dist/backend/credential-profile.js +2 -0
- package/dist/backend/credential-profile.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/config.js.map +1 -1
- package/dist/fleet-manager.d.ts +1 -0
- package/dist/fleet-manager.js +24 -14
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/credential-profiles/SKILL.md +4 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +3 -2
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +1 -1
- package/dist/tmux-exec-helper.d.ts +94 -0
- package/dist/tmux-exec-helper.js +407 -0
- package/dist/tmux-exec-helper.js.map +1 -0
- package/dist/tmux-manager.js +26 -11
- package/dist/tmux-manager.js.map +1 -1
- package/dist/topic-commands.d.ts +10 -0
- package/dist/topic-commands.js +27 -15
- package/dist/topic-commands.js.map +1 -1
- package/dist/ui/panel-chat.js +18 -11
- package/dist/ui/shared/app.css +2 -11
- package/dist/ui/shared/card-strings.js +3 -3
- package/dist/ui/shared/identity-card.js +21 -21
- package/dist/ui/shared/ui-icons.js +0 -1
- package/dist/ui/shared/ui-menu.js +12 -5
- package/dist/usage/providers.d.ts +10 -1
- package/dist/usage/providers.js +63 -3
- package/dist/usage/providers.js.map +1 -1
- package/dist/usage/usage-api.d.ts +3 -1
- package/dist/usage/usage-api.js +5 -4
- package/dist/usage/usage-api.js.map +1 -1
- package/dist/view-api.js +4 -2
- package/dist/view-api.js.map +1 -1
- package/dist/web-api.d.ts +0 -3
- package/dist/web-api.js +62 -13
- package/dist/web-api.js.map +1 -1
- package/package.json +1 -1
package/dist/ui/panel-chat.js
CHANGED
|
@@ -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
|
|
34
|
-
//
|
|
35
|
-
//
|
|
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}
|
|
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
|
|
53
|
-
function
|
|
53
|
+
/** The header's card menu: ☑ Show card, ☑ Show picture and bio. */
|
|
54
|
+
function CardMenu({ card }) {
|
|
54
55
|
const prefs = useStore(chatCardPrefs);
|
|
55
|
-
|
|
56
|
-
|
|
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}<${
|
|
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}>
|
package/dist/ui/shared/app.css
CHANGED
|
@@ -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
|
|
1620
|
-
|
|
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
|
-
/**
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
|
175
|
-
*
|
|
176
|
-
*
|
|
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,
|
|
184
|
+
export function IdentityCard({ name, card, compact = false, sizeControl = !compact, rowHidden = false }) {
|
|
180
185
|
useStore(cardSizeStore);
|
|
181
|
-
const
|
|
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[
|
|
188
|
-
? html`<img class=${cls} src=${card[slot].src} alt=${alt} title=${whose(card[slot])} decoding="async" onError=${() =>
|
|
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
|
|
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}
|
|
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
|
-
/**
|
|
8
|
-
|
|
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
|
|
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
|
-
/**
|
|
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
|
package/dist/usage/providers.js
CHANGED
|
@@ -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
|
*
|