@songsid/agend 2.2.0-beta.15 → 2.2.0-beta.17
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/README.md +1 -0
- package/README.zh-TW.md +1 -0
- package/dist/backend/antigravity.d.ts +25 -0
- package/dist/backend/antigravity.js +94 -9
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/cli.js +19 -0
- package/dist/cli.js.map +1 -1
- package/dist/compact-watch.d.ts +78 -0
- package/dist/compact-watch.js +247 -0
- package/dist/compact-watch.js.map +1 -0
- package/dist/daemon.js +46 -6
- package/dist/daemon.js.map +1 -1
- package/dist/fleet-manager.d.ts +3 -0
- package/dist/fleet-manager.js +76 -5
- package/dist/fleet-manager.js.map +1 -1
- package/dist/locale.js +4 -0
- package/dist/locale.js.map +1 -1
- package/dist/service-installer.d.ts +24 -0
- package/dist/service-installer.js +49 -1
- package/dist/service-installer.js.map +1 -1
- package/dist/topic-commands.d.ts +23 -2
- package/dist/topic-commands.js +48 -19
- package/dist/topic-commands.js.map +1 -1
- package/dist/transcript-jsonl.d.ts +7 -2
- package/dist/transcript-jsonl.js +13 -7
- package/dist/transcript-jsonl.js.map +1 -1
- package/dist/ui/chat-store.js +32 -2
- package/dist/ui/chat-strings.js +8 -2
- package/dist/ui/chat-thread.js +75 -12
- package/dist/ui/panel-chat.js +4 -1
- package/dist/ui/shared/app.css +1 -0
- package/dist/usage/providers.d.ts +28 -8
- package/dist/usage/providers.js +96 -41
- package/dist/usage/providers.js.map +1 -1
- package/dist/web-api.js +1 -1
- package/dist/web-api.js.map +1 -1
- package/dist/web-commands.d.ts +14 -1
- package/dist/web-commands.js +6 -3
- package/dist/web-commands.js.map +1 -1
- package/dist/web-upload.d.ts +7 -3
- package/dist/web-upload.js +20 -8
- package/dist/web-upload.js.map +1 -1
- package/package.json +1 -1
package/dist/ui/chat-store.js
CHANGED
|
@@ -31,6 +31,13 @@ function writePollMine(mine) {
|
|
|
31
31
|
try { globalThis.localStorage?.setItem(POLL_MINE_KEY, JSON.stringify(mine)); } catch { /* storage unavailable */ }
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/** Board 003648ac: an answer the server marks `sent` (handed over, and its text says only that) leaves after this. */
|
|
35
|
+
const SENT_CLEAR_MS = 8000;
|
|
36
|
+
const COMPACT_DONE_CLEAR_MS = 6000;
|
|
37
|
+
const COMPACT_UNKNOWN_CLEAR_MS = 12000;
|
|
38
|
+
/** The fleet stops following a /compact after 10 minutes (compact-watch.ts); the page waits a minute more. */
|
|
39
|
+
const COMPACT_PAGE_LIMIT_MS = 11 * 60_000;
|
|
40
|
+
|
|
34
41
|
/** Build a store. `deps` (fetch, toast, announce, t, now) are injected so tests can drive every path. */
|
|
35
42
|
export function createChatStore(deps) {
|
|
36
43
|
const s = {
|
|
@@ -77,6 +84,7 @@ export function createChatStore(deps) {
|
|
|
77
84
|
const waits = i.state === "awaiting_input" ? (typeof i.interaction_summary === "string" ? i.interaction_summary : "") : null;
|
|
78
85
|
if (i.name === s.current && waits != null && s.awaiting[i.name] == null) deps.announce(tf("chat.awaiting", i.name));
|
|
79
86
|
s.awaiting[i.name] = waits;
|
|
87
|
+
if (i.compact) noteCompact(i.name, i.compact);
|
|
80
88
|
}
|
|
81
89
|
changed(null);
|
|
82
90
|
}
|
|
@@ -337,10 +345,32 @@ export function createChatStore(deps) {
|
|
|
337
345
|
if (!yes || !alive()) { delete s.commands[target]; changed(target); return; }
|
|
338
346
|
r = await post({ confirm: r.confirm.token });
|
|
339
347
|
}
|
|
340
|
-
|
|
341
|
-
|
|
348
|
+
const follow = r && r.follow && r.follow.kind === "compact" ? r.follow : null;
|
|
349
|
+
showCommand(target, r && r.choices ? { command, busy: false, choices: r.choices }
|
|
350
|
+
: r && r.error ? { command, busy: false, error: r.error }
|
|
351
|
+
: { command, busy: false, text: (r && r.text) || "", ...(r && r.sent === true ? { sent: true } : {}), ...(follow ? { compact: follow.id ? "running" : "unknown", watch: follow.id } : {}) });
|
|
342
352
|
changed(target);
|
|
343
353
|
}
|
|
354
|
+
// Board 003648ac: an answer the server marks `sent` leaves by itself; a sent /compact says "compacting…" until the
|
|
355
|
+
// status frame's `compact` (same watch id) says it ended, then leaves when it is done. Everything else — a refusal
|
|
356
|
+
// (even one worded as text), a list to choose from, an answer to read, a compaction timed out or lost — stays.
|
|
357
|
+
function showCommand(target, card) {
|
|
358
|
+
s.commands[target] = card;
|
|
359
|
+
const later = (ms, fn) => deps.setTimeout(() => { if (s.commands[target] === card) { fn(); changed(target); } }, ms);
|
|
360
|
+
const leave = () => { delete s.commands[target]; };
|
|
361
|
+
if (card.busy || card.error || card.choices || card.compact === "timeout" || card.compact === "lost") return;
|
|
362
|
+
if (card.compact === "running") later(COMPACT_PAGE_LIMIT_MS, () => showCommand(target, { ...card, compact: "timeout" }));
|
|
363
|
+
else if (card.compact === "done") later(COMPACT_DONE_CLEAR_MS, leave);
|
|
364
|
+
else if (card.compact === "unknown") later(COMPACT_UNKNOWN_CLEAR_MS, leave);
|
|
365
|
+
else if (card.sent === true) later(SENT_CLEAR_MS, leave);
|
|
366
|
+
}
|
|
367
|
+
/** The status frame's word on a followed /compact: done, timed out, or lost. */
|
|
368
|
+
function noteCompact(name, view) {
|
|
369
|
+
const c = s.commands[name];
|
|
370
|
+
if (!c || c.compact !== "running" || !view || view.id !== c.watch || !["done", "timeout", "lost"].includes(view.state)) return;
|
|
371
|
+
showCommand(name, { ...c, compact: view.state });
|
|
372
|
+
changed(name);
|
|
373
|
+
}
|
|
344
374
|
function dismissCommand(target) { delete s.commands[target]; changed(target); }
|
|
345
375
|
/** Whether a command can start for `target` now (none is out): checked before a typed command line is consumed. */
|
|
346
376
|
const commandFree = (target) => !(s.commands[target] && s.commands[target].busy);
|
package/dist/ui/chat-strings.js
CHANGED
|
@@ -4,6 +4,9 @@ import { register } from "/assets/app-i18n.js";
|
|
|
4
4
|
register("chat", {
|
|
5
5
|
en: {
|
|
6
6
|
cmdConfirm: "Clear", cmdConfirmBackend: "Switch and restart", cmdRunning: "Running /{0}…", cmdNotCommand: "Not a command here — Enter sends it as a message.", cmdDismiss: "Dismiss", cmdCommands: "Commands",
|
|
7
|
+
compactRunning: "Compacting…", compactDone: "✅ Conversation compacted", compactUnknown: "This backend does not report when compaction ends.",
|
|
8
|
+
compactTimeout: "No sign that compaction finished within {0} minutes — check the agent's own chat.",
|
|
9
|
+
compactLost: "Can't follow this compaction: the command did not reach the agent, or the agent stopped or changed backend.",
|
|
7
10
|
cmdChoose: "Choose", cmdCurrent: "current", chipCompact: "Compact", chipClear: "Clear…", chipContext: "Context {0}% used",
|
|
8
11
|
chipModel: "Model: {0} — choose another", chipEffort: "Effort: {0} — choose another",
|
|
9
12
|
cmd_ctx: "Context use, model and effort", cmd_compact: "Compact the conversation", cmd_clear: "Clear the conversation (asks first)",
|
|
@@ -19,7 +22,7 @@ register("chat", {
|
|
|
19
22
|
a11yWorking: "{0} started working", a11yIdle: "{0} finished", a11yReplied: "{0} replied", conversationWith: "Conversation with {0}",
|
|
20
23
|
stopping: "Stopping {0}…", awaiting: "{0} is waiting for your input", approxNote: "Read from the terminal, so approximate", elapsedTitle: "Time since this page saw it start (approximate)",
|
|
21
24
|
dropHere: "Drop files to attach them", asText: "As text", pasteAttached: "A long paste ({0} characters) was attached as a text file", pasteKeptAsText: "No room for another attachment: the long paste stays as text",
|
|
22
|
-
send: "Send", composerPlaceholder: "Message {0}…", composerHint: "Enter to send · Shift+Enter for a new line", composerMore: "/ for commands · Esc stops the reply", attachTitle: "Attach files (
|
|
25
|
+
send: "Send", composerPlaceholder: "Message {0}…", composerHint: "Enter to send · Shift+Enter for a new line", composerMore: "/ for commands · Esc stops the reply", attachTitle: "Attach files (any type — or paste / drop them)",
|
|
23
26
|
pvPreview: "Preview", pvStop: "Stop", pvDownload: "Download", pvMenu: "Preview options", pvHtml: "HTML",
|
|
24
27
|
pvAllowButton: "Allow previews", pvAllow: "Allow HTML previews — they may be able to send data out", pvDisallow: "Stop allowing HTML previews",
|
|
25
28
|
pvConfirm: "Allow HTML previews?\n\nThis applies on every device where you are signed in. A preview runs the agent's HTML in an isolated frame. It cannot use your login, but it may be able to send data out, and it can slow or freeze this tab. You still click Preview each time.",
|
|
@@ -89,6 +92,9 @@ register("chat", {
|
|
|
89
92
|
},
|
|
90
93
|
"zh-TW": {
|
|
91
94
|
cmdConfirm: "清空", cmdConfirmBackend: "切換並重新啟動", cmdRunning: "執行 /{0} 中…", cmdNotCommand: "這裡沒有這個指令——按 Enter 會當成一般訊息送出。", cmdDismiss: "關閉", cmdCommands: "指令",
|
|
95
|
+
compactRunning: "壓縮中…", compactDone: "✅ 對話已壓縮", compactUnknown: "這個後端不會回報壓縮何時完成。",
|
|
96
|
+
compactTimeout: "{0} 分鐘內沒有看到壓縮完成的紀錄,請到代理自己的對話確認。",
|
|
97
|
+
compactLost: "無法再追蹤這次壓縮:指令沒有送到代理,或代理已停止、換了後端。",
|
|
92
98
|
cmdChoose: "選擇", cmdCurrent: "目前", chipCompact: "壓縮", chipClear: "清空…", chipContext: "已用 {0}% 的 context",
|
|
93
99
|
chipModel: "模型:{0}——選擇其他", chipEffort: "推理強度:{0}——選擇其他",
|
|
94
100
|
cmd_ctx: "context 用量、模型與推理強度", cmd_compact: "壓縮對話", cmd_clear: "清空對話(會先確認)",
|
|
@@ -104,7 +110,7 @@ register("chat", {
|
|
|
104
110
|
a11yWorking: "{0} 開始工作", a11yIdle: "{0} 已完成", a11yReplied: "{0} 已回覆", conversationWith: "與 {0} 的對話",
|
|
105
111
|
stopping: "正在中止 {0}…", awaiting: "{0} 正在等你回應", approxNote: "從終端畫面判讀,僅供參考", elapsedTitle: "從這個頁面看到它開始工作起算(約略)",
|
|
106
112
|
dropHere: "放開即可附加檔案", asText: "改為文字", pasteAttached: "貼上的長文字({0} 個字元)已改為文字檔附加", pasteKeptAsText: "附件已滿,貼上的長文字保留為一般文字",
|
|
107
|
-
send: "送出", composerPlaceholder: "傳訊息給 {0}…", composerHint: "Enter 送出 · Shift+Enter 換行", composerMore: "/ 叫出指令 · Esc 中止回覆", attachTitle: "
|
|
113
|
+
send: "送出", composerPlaceholder: "傳訊息給 {0}…", composerHint: "Enter 送出 · Shift+Enter 換行", composerMore: "/ 叫出指令 · Esc 中止回覆", attachTitle: "附加檔案(任何類型,也可以貼上或拖曳)",
|
|
108
114
|
pvPreview: "預覽", pvStop: "停止", pvDownload: "下載", pvMenu: "預覽選項", pvHtml: "HTML",
|
|
109
115
|
pvAllowButton: "允許預覽", pvAllow: "允許預覽 HTML(預覽可能把資料傳出去)", pvDisallow: "不再允許預覽 HTML",
|
|
110
116
|
pvConfirm: "要允許預覽 HTML 嗎?\n\n這會套用到你登入的每一台裝置。預覽會在隔離的框架裡執行 agent 的 HTML。它無法使用你的登入,但可能把資料傳出去,也可能讓這個分頁變慢或卡住。每次仍需要你按「預覽」。",
|
package/dist/ui/chat-thread.js
CHANGED
|
@@ -31,6 +31,12 @@ const COPY = '<svg class="icon" width="14" height="14" viewBox="0 0 24 24" fill=
|
|
|
31
31
|
/** Escapes & " ' < >: safe for text and inside a quoted attribute. */
|
|
32
32
|
export function escAttr(s) { return String(s == null ? "" : s).replace(/&/g, "&").replace(/"/g, """).replace(/'/g, "'").replace(/</g, "<").replace(/>/g, ">"); }
|
|
33
33
|
export const msgKey = (x) => `${x.boot}-${x.id}`;
|
|
34
|
+
/**
|
|
35
|
+
* Board 00ef2430: a thread opens with only its newest messages drawn — enough for a screen — so switching chats paints
|
|
36
|
+
* at once; the older ones are drawn after that, a chunk at a time between frames, the reader's place kept.
|
|
37
|
+
*/
|
|
38
|
+
export const THREAD_TAIL = 24;
|
|
39
|
+
export const THREAD_CHUNK = 40;
|
|
34
40
|
|
|
35
41
|
/**
|
|
36
42
|
* Bind a renderer to `list` (the thread element) inside `scroller`. opts: { t, tf, isUser(x), onJump({show, unseen}),
|
|
@@ -47,6 +53,10 @@ export function createThread(list, scroller, opts) {
|
|
|
47
53
|
// moves down or stays), and those must not count as the reader leaving the bottom. Reaching the bottom pins again.
|
|
48
54
|
let pinned = true;
|
|
49
55
|
let lastTop = 0;
|
|
56
|
+
// The oldest message drawn (by key): everything after it is drawn, so new messages never push the window. Null: none
|
|
57
|
+
// drawn yet — the next render draws the newest THREAD_TAIL and the rest follows (grow).
|
|
58
|
+
let firstKey = null;
|
|
59
|
+
let growTimer = null, growFrame = null, disposed = false;
|
|
50
60
|
const cardRefresh = new WeakMap(); // card node → re-read availability (weak: a card that left takes its source with it)
|
|
51
61
|
const tr = opts.t, trf = opts.tf;
|
|
52
62
|
const stopIn = (node) => (P() ? P().stopIn(node) : []);
|
|
@@ -428,6 +438,44 @@ export function createThread(list, scroller, opts) {
|
|
|
428
438
|
const ro = typeof ResizeObserver === "function" ? new ResizeObserver(resized) : null;
|
|
429
439
|
if (ro) { ro.observe(list); ro.observe(scroller); }
|
|
430
440
|
|
|
441
|
+
/** Where drawing starts in `msgs`: at the oldest drawn message, or (none yet) the newest THREAD_TAIL. */
|
|
442
|
+
function startIndex() {
|
|
443
|
+
const i = firstKey === null ? -1 : msgs.findIndex((x) => msgKey(x) === firstKey);
|
|
444
|
+
// Whatever is drawn stays drawn: the list may have reordered (merged across fleet restarts) or lost its oldest to
|
|
445
|
+
// the cap — so the window starts at the earlier of the oldest drawn key and the first message still drawn.
|
|
446
|
+
const still = nodes.size ? msgs.findIndex((x) => nodes.has(msgKey(x))) : -1;
|
|
447
|
+
const at = [i, still].filter((n) => n >= 0);
|
|
448
|
+
return at.length ? Math.min(...at) : Math.max(0, msgs.length - THREAD_TAIL);
|
|
449
|
+
}
|
|
450
|
+
/** Draw the next chunk of older messages, keeping the reader where they were; then the next, until all are drawn. */
|
|
451
|
+
function grow() {
|
|
452
|
+
growTimer = growFrame = null;
|
|
453
|
+
if (disposed) return; // the chat left: nothing more is drawn
|
|
454
|
+
const at = startIndex();
|
|
455
|
+
if (at <= 0) return;
|
|
456
|
+
firstKey = msgKey(msgs[Math.max(0, at - THREAD_CHUNK)]);
|
|
457
|
+
const fromBottom = scroller.scrollHeight - scroller.scrollTop;
|
|
458
|
+
draw({ older: true }); // drawn above the reader: never "new below" (render alone counts those)
|
|
459
|
+
if (pinned) toBottom();
|
|
460
|
+
else { scroller.scrollTop = scroller.scrollHeight - fromBottom; lastTop = scroller.scrollTop; }
|
|
461
|
+
scheduleGrow();
|
|
462
|
+
}
|
|
463
|
+
/** After the next frame has painted (the click's own frame stays light), draw a chunk more. */
|
|
464
|
+
function scheduleGrow() {
|
|
465
|
+
if (growTimer !== null || growFrame !== null || startIndex() <= 0) return;
|
|
466
|
+
const later = () => { growFrame = null; growTimer = setTimeout(grow, 0); };
|
|
467
|
+
if (typeof requestAnimationFrame === "function") growFrame = requestAnimationFrame(later); else later();
|
|
468
|
+
}
|
|
469
|
+
/** Everything drawn now (a remembered scroll position, a message to show that is not drawn yet). */
|
|
470
|
+
function growAll() {
|
|
471
|
+
if (startIndex() <= 0) return;
|
|
472
|
+
if (growFrame !== null && typeof cancelAnimationFrame === "function") cancelAnimationFrame(growFrame);
|
|
473
|
+
if (growTimer !== null) clearTimeout(growTimer);
|
|
474
|
+
growTimer = growFrame = null;
|
|
475
|
+
firstKey = msgs.length ? msgKey(msgs[0]) : null;
|
|
476
|
+
draw({ older: true });
|
|
477
|
+
}
|
|
478
|
+
|
|
431
479
|
/** Draw `next` (this instance's messages). `restore`: the scroll position to return to (null = the bottom). */
|
|
432
480
|
function render(next, renderOpts = {}) {
|
|
433
481
|
const stick = pinned || nearBottom();
|
|
@@ -435,24 +483,43 @@ export function createThread(list, scroller, opts) {
|
|
|
435
483
|
if (!msgs.length) {
|
|
436
484
|
for (const v of nodes.values()) stopIn(v.node);
|
|
437
485
|
nodes.clear();
|
|
486
|
+
firstKey = null;
|
|
438
487
|
armDay();
|
|
439
488
|
list.textContent = "";
|
|
440
489
|
opts.onEmpty(true);
|
|
441
490
|
return;
|
|
442
491
|
}
|
|
443
492
|
opts.onEmpty(false);
|
|
444
|
-
|
|
493
|
+
// A position to return to that is not the bottom needs the messages above it: all of them, at once (as before).
|
|
494
|
+
if ("restore" in renderOpts && renderOpts.restore != null) firstKey = msgKey(msgs[0]);
|
|
495
|
+
const added = draw({});
|
|
496
|
+
if (firstKey === null || startIndex() > 0) { firstKey = msgKey(msgs[startIndex()]); scheduleGrow(); }
|
|
497
|
+
if ("restore" in renderOpts) {
|
|
498
|
+
unseen = 0;
|
|
499
|
+
if (renderOpts.restore == null) toBottom();
|
|
500
|
+
else { scroller.scrollTop = renderOpts.restore; pinned = nearBottom(); lastTop = scroller.scrollTop; }
|
|
501
|
+
} else if (stick) toBottom();
|
|
502
|
+
else unseen += added;
|
|
503
|
+
reportJump();
|
|
504
|
+
}
|
|
505
|
+
/** Put the messages from startIndex() on in the list (nodes reused when unchanged). Returns how many are new. */
|
|
506
|
+
function draw(drawOpts) {
|
|
507
|
+
const shown = msgs.slice(startIndex());
|
|
508
|
+
const keep = new Set(shown.map(msgKey));
|
|
445
509
|
for (const [k, v] of nodes) if (!keep.has(k)) { stopIn(v.node); v.node.remove(); nodes.delete(k); }
|
|
446
510
|
let prev = null, added = 0;
|
|
447
|
-
for (const x of
|
|
448
|
-
const key = msgKey(x)
|
|
511
|
+
for (const x of shown) {
|
|
512
|
+
const key = msgKey(x);
|
|
449
513
|
let have = nodes.get(key);
|
|
514
|
+
// Filling in older messages: a node drawn from this very message object is current — not drawn again.
|
|
515
|
+
if (drawOpts.older && have && have.x === x) { prev = have.node; continue; }
|
|
516
|
+
const html = msgHtml(x);
|
|
450
517
|
if (!have || have.html !== html) {
|
|
451
518
|
const stopped = have ? stopIn(have.node) : [];
|
|
452
519
|
const node = msgNode(html, x, stopped);
|
|
453
520
|
if (have) have.node.replaceWith(node); else added++;
|
|
454
|
-
have = { html, node }; nodes.set(key, have);
|
|
455
|
-
} else timeLabel(have.node);
|
|
521
|
+
have = { html, node, x }; nodes.set(key, have);
|
|
522
|
+
} else { have.x = x; timeLabel(have.node); }
|
|
456
523
|
const want = prev ? prev.nextSibling : list.firstChild;
|
|
457
524
|
// A node already in the list is being moved: its preview stops first. A new node (still in its template) has none.
|
|
458
525
|
if (have.node !== want) { if (have.node.parentNode === list) stopIn(have.node); list.insertBefore(have.node, want); }
|
|
@@ -460,13 +527,7 @@ export function createThread(list, scroller, opts) {
|
|
|
460
527
|
}
|
|
461
528
|
armDay();
|
|
462
529
|
armPolls();
|
|
463
|
-
|
|
464
|
-
unseen = 0;
|
|
465
|
-
if (renderOpts.restore == null) toBottom();
|
|
466
|
-
else { scroller.scrollTop = renderOpts.restore; pinned = nearBottom(); lastTop = scroller.scrollTop; }
|
|
467
|
-
} else if (stick) toBottom();
|
|
468
|
-
else unseen += added;
|
|
469
|
-
reportJump();
|
|
530
|
+
return added;
|
|
470
531
|
}
|
|
471
532
|
|
|
472
533
|
function onScroll() {
|
|
@@ -481,6 +542,7 @@ export function createThread(list, scroller, opts) {
|
|
|
481
542
|
function jumpLatest() { toBottom(); unseen = 0; reportJump(); }
|
|
482
543
|
/** Show one message (the panel's "from …" link, #1481): scrolled into view and briefly marked. */
|
|
483
544
|
function reveal(key) {
|
|
545
|
+
if (!nodes.has(key) && msgs.some((x) => msgKey(x) === key)) growAll(); // an older message not drawn yet
|
|
484
546
|
const have = nodes.get(key); if (!have) return false;
|
|
485
547
|
if (typeof have.node.scrollIntoView === "function") have.node.scrollIntoView({ block: "center" });
|
|
486
548
|
have.node.classList.add("flash");
|
|
@@ -535,6 +597,7 @@ export function createThread(list, scroller, opts) {
|
|
|
535
597
|
|
|
536
598
|
/** The thread goes (the chat left, or another instance opened): every preview in it stops first. */
|
|
537
599
|
function dispose() {
|
|
600
|
+
disposed = true;
|
|
538
601
|
if (ro) ro.disconnect();
|
|
539
602
|
if (P()) P().stopAll("leave");
|
|
540
603
|
nodes.clear();
|
package/dist/ui/panel-chat.js
CHANGED
|
@@ -322,6 +322,9 @@ function CommandCard({ name, lease }) {
|
|
|
322
322
|
aria-label=${t("chat.cmdDismiss")} onClick=${() => store.dismissCommand(name)}><${Icon} name="close" size=${14} /></button>`}</div>
|
|
323
323
|
${c.error ? html`<p class="cmd-text">${c.error}</p>` : null}
|
|
324
324
|
${c.text ? html`<pre class="cmd-text">${c.text}</pre>` : null}
|
|
325
|
+
${c.compact ? html`<p class="cmd-text cmd-phase">${c.compact === "running" ? t("chat.compactRunning")
|
|
326
|
+
: c.compact === "done" ? t("chat.compactDone") : c.compact === "timeout" ? t("chat.compactTimeout", "10")
|
|
327
|
+
: c.compact === "lost" ? t("chat.compactLost") : t("chat.compactUnknown")}</p>` : null}
|
|
325
328
|
${c.choices && c.choices.prompt ? html`<pre class="cmd-text">${c.choices.prompt}</pre>` : null}
|
|
326
329
|
${c.choices ? html`<div class="cmd-choices" role="group" aria-label=${t("chat.cmdChoose")}>${c.choices.options.map((o) => html`<button key=${o.id} type="button"
|
|
327
330
|
class=${`btn btn-sm${o.id === c.choices.current ? " current" : ""}`} aria-current=${o.id === c.choices.current ? "true" : undefined} onClick=${() => run(o.id)}>${o.label}</button>`)}</div>` : null}
|
|
@@ -434,7 +437,7 @@ function Composer({ name, lease, busy }) {
|
|
|
434
437
|
${files.length ? html`<div class="pending-files">${files.map((f, i) => html`<${FileChip} key=${`${f.name}-${i}`} f=${f} i=${i} name=${name} />`)}</div>` : null}
|
|
435
438
|
<div class="composer">
|
|
436
439
|
<button id="attachBtn" type="button" class="icon-btn" title=${t("chat.attachTitle")} aria-label=${t("chat.attachTitle")} onClick=${() => fileIn.current && fileIn.current.click()}><${Icon} name="attach" /></button>
|
|
437
|
-
<input ref=${fileIn} type="file" multiple hidden
|
|
440
|
+
<input ref=${fileIn} type="file" multiple hidden
|
|
438
441
|
onChange=${(e) => { store.addFiles(name, e.target.files); e.target.value = ""; }} />
|
|
439
442
|
<textarea id="msgIn" ref=${input} rows="1" value=${value} placeholder=${t("chat.composerPlaceholder", name)} aria-label=${t("chat.composerPlaceholder", name)}
|
|
440
443
|
autocomplete="off" aria-controls=${palette ? "cmdPalette" : undefined} aria-expanded=${palette ? "true" : "false"}
|
package/dist/ui/shared/app.css
CHANGED
|
@@ -1075,6 +1075,7 @@ strong.org-name { font-size: var(--fs-sm); color: var(--text); }
|
|
|
1075
1075
|
.cmd-head { display: flex; align-items: center; justify-content: space-between; gap: var(--s-2); }
|
|
1076
1076
|
.cmd-name { font-weight: var(--fw-semibold); }
|
|
1077
1077
|
.cmd-text { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; font-family: var(--font-ui); color: var(--text-2); }
|
|
1078
|
+
.cmd-phase { color: var(--text); font-weight: var(--fw-semibold); }
|
|
1078
1079
|
.cmd-choices { display: flex; flex-wrap: wrap; gap: var(--s-1); max-height: 180px; overflow-y: auto; }
|
|
1079
1080
|
.cmd-choices .btn.current { border-color: var(--accent); color: var(--accent); }
|
|
1080
1081
|
.quick-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2); margin-bottom: var(--s-2); font-size: var(--fs-xs); color: var(--text-2); }
|
|
@@ -143,22 +143,42 @@ export interface ClaudeUsageScope {
|
|
|
143
143
|
* always; beside profiles (`besideProfiles`), its statusline fallback leaves the profiles' instances out.
|
|
144
144
|
*/
|
|
145
145
|
export declare function fetchClaudeUsage(storeHome?: string, scope?: ClaudeUsageScope): Promise<Omit<ProviderUsage, "id" | "name">>;
|
|
146
|
+
/**
|
|
147
|
+
* One quota bucket of `retrieveUserQuotaSummary` (agy's proto: QuotaSummaryBucket). `group` is not on the wire: it is
|
|
148
|
+
* the display name of the QuotaSummaryGroup the bucket came in, copied on by agyQuotaBuckets.
|
|
149
|
+
*/
|
|
146
150
|
interface AgyBucket {
|
|
147
151
|
bucketId?: string;
|
|
148
152
|
displayName?: string;
|
|
153
|
+
window?: string;
|
|
154
|
+
disabled?: boolean;
|
|
149
155
|
remainingFraction?: number;
|
|
150
156
|
resetTime?: string;
|
|
157
|
+
group?: string;
|
|
151
158
|
}
|
|
159
|
+
interface AgyQuotaSummary {
|
|
160
|
+
groups?: {
|
|
161
|
+
displayName?: string;
|
|
162
|
+
buckets?: AgyBucket[];
|
|
163
|
+
}[];
|
|
164
|
+
buckets?: AgyBucket[];
|
|
165
|
+
}
|
|
166
|
+
/** The summary's buckets, each tagged with its group's name. The top-level list (deprecated in the proto) only counts
|
|
167
|
+
* when no group carries any bucket. */
|
|
168
|
+
export declare function agyQuotaBuckets(body: AgyQuotaSummary): AgyBucket[];
|
|
152
169
|
/**
|
|
153
|
-
* Pool the summary's buckets into
|
|
170
|
+
* Pool the summary's buckets into one metric per pool and limit.
|
|
171
|
+
*
|
|
172
|
+
* Three payload shapes exist:
|
|
173
|
+
* - pooled ids (`gemini-5h`, `gemini-weekly`, `3p-5h`, `3p-weekly`), which map 1:1;
|
|
174
|
+
* - PER-MODEL buckets (`gemini-3.6-flash-high`, `claude-sonnet-4-6`, …), aggregated into a Gemini pool and an
|
|
175
|
+
* everything-else pool;
|
|
176
|
+
* - (#1898, agy 2026-10) named groups ("Gemini Models", "Claude and GPT Models") whose buckets are the group's
|
|
177
|
+
* five-hour and weekly limits. The group decides the pool; the bucket's window, id or name decides the limit.
|
|
154
178
|
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
* `claude-sonnet-4-6`, `gpt-oss-120b-medium`, …) — those aggregate into a
|
|
159
|
-
* Gemini pool and an everything-else pool, keeping each pool's WORST remaining
|
|
160
|
-
* fraction, because eleven per-model meters is noise and the binding constraint
|
|
161
|
-
* is the one that runs out first.
|
|
179
|
+
* Every metric keeps the WORST remaining fraction of its buckets, because the binding constraint is the one that runs
|
|
180
|
+
* out first. Within one pool a bucket with a known limit is authoritative over per-model buckets; one pool never hides
|
|
181
|
+
* another. Disabled buckets (agy dims them, no number) are left out.
|
|
162
182
|
*/
|
|
163
183
|
export declare function agyPoolMetrics(buckets: AgyBucket[]): UsageMetric[];
|
|
164
184
|
interface AgyStoredToken {
|
package/dist/usage/providers.js
CHANGED
|
@@ -549,42 +549,96 @@ function agyOAuthClient(env = process.env) {
|
|
|
549
549
|
/** Refreshed bearer, cached until shortly before expiry. The CLI's token file is
|
|
550
550
|
* never written back — it belongs to agy; refresh tokens are reusable. */
|
|
551
551
|
let agyTokenCache = null;
|
|
552
|
+
/** The summary's buckets, each tagged with its group's name. The top-level list (deprecated in the proto) only counts
|
|
553
|
+
* when no group carries any bucket. */
|
|
554
|
+
export function agyQuotaBuckets(body) {
|
|
555
|
+
const grouped = (body.groups ?? []).flatMap(g => (g.buckets ?? []).map(b => ({ ...b, group: g.displayName?.trim() || undefined })));
|
|
556
|
+
return grouped.length ? grouped : (body.buckets ?? []);
|
|
557
|
+
}
|
|
558
|
+
const AGY_POOLED = {
|
|
559
|
+
"gemini-5h": { pool: "Gemini", window: "session" },
|
|
560
|
+
"gemini-weekly": { pool: "Gemini", window: "weekly" },
|
|
561
|
+
"3p-5h": { pool: "Claude & others", window: "session" },
|
|
562
|
+
"3p-weekly": { pool: "Claude & others", window: "weekly" },
|
|
563
|
+
};
|
|
564
|
+
/** The pool a bucket belongs to: its group when the summary names one (#1898), else a guess from the bucket id. */
|
|
565
|
+
function agyPool(b, id) {
|
|
566
|
+
const pooled = AGY_POOLED[id];
|
|
567
|
+
if (pooled)
|
|
568
|
+
return pooled.pool;
|
|
569
|
+
const group = b.group;
|
|
570
|
+
if (group) {
|
|
571
|
+
if (/gemini/i.test(group))
|
|
572
|
+
return "Gemini";
|
|
573
|
+
if (/claude|gpt|third.?party|\b3p\b/i.test(group))
|
|
574
|
+
return "Claude & others";
|
|
575
|
+
return group;
|
|
576
|
+
}
|
|
577
|
+
return id.startsWith("gemini") ? "Gemini" : "Claude & others";
|
|
578
|
+
}
|
|
579
|
+
/** The limit a bucket counts against ("Five Hour Limit", window "5h" …), or null for a per-model bucket. */
|
|
580
|
+
function agyWindow(b, id) {
|
|
581
|
+
const pooled = AGY_POOLED[id];
|
|
582
|
+
if (pooled)
|
|
583
|
+
return pooled.window;
|
|
584
|
+
// One source at a time, the most specific first: the bucket's own window, then its id, then — only inside a named
|
|
585
|
+
// group, where a per-model name ("Gemini 2.5 High") cannot occur — its display name. A word that merely appears in an
|
|
586
|
+
// opaque id never overrides a window the bucket states.
|
|
587
|
+
for (const text of [b.window, id, b.group ? b.displayName : undefined]) {
|
|
588
|
+
const window = text ? agyWindowOf(text) : null;
|
|
589
|
+
if (window)
|
|
590
|
+
return window;
|
|
591
|
+
}
|
|
592
|
+
return null;
|
|
593
|
+
}
|
|
594
|
+
/** "5h", "5 h", "5-hour", "5_hours", "Five Hour Limit", "session" → session; "weekly", "7d", "7 days" → weekly. */
|
|
595
|
+
function agyWindowOf(text) {
|
|
596
|
+
if (/(^|[^0-9.])5[\s_-]*h(ours?|rs?)?\b|five[\s_-]*h(ours?|rs?)\b|\bsession\b/i.test(text))
|
|
597
|
+
return "session";
|
|
598
|
+
if (/week|(^|[^0-9.])7[\s_-]*d(ays?)?\b|seven[\s_-]*days?\b/i.test(text))
|
|
599
|
+
return "weekly";
|
|
600
|
+
return null;
|
|
601
|
+
}
|
|
602
|
+
function agyLabel(pool, window) {
|
|
603
|
+
if (pool === "Claude & others") {
|
|
604
|
+
return window === "session" ? { text: "Claude & others (session)", i18n: i18n("usage.metric.agy_claude_others_session") }
|
|
605
|
+
: window === "weekly" ? { text: "Claude & others (weekly)", i18n: i18n("usage.metric.agy_claude_others_weekly") }
|
|
606
|
+
: { text: pool, i18n: i18n("usage.metric.agy_claude_others") };
|
|
607
|
+
}
|
|
608
|
+
return window === "session" ? { text: `${pool} (session)`, i18n: i18n("usage.metric.named_session", pool) }
|
|
609
|
+
: window === "weekly" ? { text: `${pool} (weekly)`, i18n: i18n("usage.metric.named_weekly", pool) }
|
|
610
|
+
: { text: pool, i18n: i18n("usage.metric.named", pool) };
|
|
611
|
+
}
|
|
552
612
|
/**
|
|
553
|
-
* Pool the summary's buckets into
|
|
613
|
+
* Pool the summary's buckets into one metric per pool and limit.
|
|
554
614
|
*
|
|
555
|
-
*
|
|
556
|
-
* `gemini-weekly`, `3p-5h`, `3p-weekly`), which map 1:1
|
|
557
|
-
*
|
|
558
|
-
*
|
|
559
|
-
* Gemini
|
|
560
|
-
*
|
|
561
|
-
*
|
|
615
|
+
* Three payload shapes exist:
|
|
616
|
+
* - pooled ids (`gemini-5h`, `gemini-weekly`, `3p-5h`, `3p-weekly`), which map 1:1;
|
|
617
|
+
* - PER-MODEL buckets (`gemini-3.6-flash-high`, `claude-sonnet-4-6`, …), aggregated into a Gemini pool and an
|
|
618
|
+
* everything-else pool;
|
|
619
|
+
* - (#1898, agy 2026-10) named groups ("Gemini Models", "Claude and GPT Models") whose buckets are the group's
|
|
620
|
+
* five-hour and weekly limits. The group decides the pool; the bucket's window, id or name decides the limit.
|
|
621
|
+
*
|
|
622
|
+
* Every metric keeps the WORST remaining fraction of its buckets, because the binding constraint is the one that runs
|
|
623
|
+
* out first. Within one pool a bucket with a known limit is authoritative over per-model buckets; one pool never hides
|
|
624
|
+
* another. Disabled buckets (agy dims them, no number) are left out.
|
|
562
625
|
*/
|
|
563
626
|
export function agyPoolMetrics(buckets) {
|
|
564
|
-
const
|
|
565
|
-
"gemini-5h": { text: "Gemini (session)", i18n: i18n("usage.metric.named_session", "Gemini") },
|
|
566
|
-
"gemini-weekly": { text: "Gemini (weekly)", i18n: i18n("usage.metric.named_weekly", "Gemini") },
|
|
567
|
-
"3p-5h": { text: "Claude & others (session)", i18n: i18n("usage.metric.agy_claude_others_session") },
|
|
568
|
-
"3p-weekly": { text: "Claude & others (weekly)", i18n: i18n("usage.metric.agy_claude_others_weekly") },
|
|
569
|
-
};
|
|
570
|
-
const direct = [];
|
|
571
|
-
const pools = new Map();
|
|
627
|
+
const entries = new Map();
|
|
572
628
|
for (const b of buckets) {
|
|
573
|
-
|
|
629
|
+
const id = b.bucketId?.trim() || (b.group ? b.displayName?.trim() : undefined);
|
|
630
|
+
if (!id || b.disabled === true || typeof b.remainingFraction !== "number" || !Number.isFinite(b.remainingFraction))
|
|
574
631
|
continue;
|
|
632
|
+
const pool = agyPool(b, id);
|
|
633
|
+
const window = agyWindow(b, id);
|
|
634
|
+
const key = `${pool}\u0000${window ?? ""}`;
|
|
575
635
|
const reset = b.resetTime ?? null;
|
|
576
|
-
const
|
|
577
|
-
if (pooledLabel) {
|
|
578
|
-
direct.push({ label: pooledLabel.text, labelI18n: pooledLabel.i18n, type: "percent", used: (1 - b.remainingFraction) * 100, resetsAt: reset });
|
|
579
|
-
continue;
|
|
580
|
-
}
|
|
581
|
-
const pool = b.bucketId.startsWith("gemini") ? "Gemini" : "Claude & others";
|
|
582
|
-
const existing = pools.get(pool);
|
|
636
|
+
const existing = entries.get(key);
|
|
583
637
|
if (!existing) {
|
|
584
|
-
|
|
638
|
+
entries.set(key, { pool, window, fraction: b.remainingFraction, resetsAt: reset, models: 1 });
|
|
585
639
|
continue;
|
|
586
640
|
}
|
|
587
|
-
// Count every
|
|
641
|
+
// Count every bucket in the entry; track the worst separately. Replacing the
|
|
588
642
|
// whole entry on a new worst would restart the count at that bucket.
|
|
589
643
|
existing.models += 1;
|
|
590
644
|
if (b.remainingFraction < existing.fraction) {
|
|
@@ -592,22 +646,24 @@ export function agyPoolMetrics(buckets) {
|
|
|
592
646
|
existing.resetsAt = reset;
|
|
593
647
|
}
|
|
594
648
|
}
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
used: (1 -
|
|
604
|
-
resetsAt: p.resetsAt,
|
|
649
|
+
const limited = new Set([...entries.values()].filter(e => e.window).map(e => e.pool));
|
|
650
|
+
const order = (w) => (w === "session" ? 0 : w === "weekly" ? 1 : 2);
|
|
651
|
+
const pools = [...new Set([...entries.values()].map(e => e.pool))];
|
|
652
|
+
return [...entries.values()]
|
|
653
|
+
.filter(e => e.window || !limited.has(e.pool))
|
|
654
|
+
.sort((a, b) => pools.indexOf(a.pool) - pools.indexOf(b.pool) || order(a.window) - order(b.window))
|
|
655
|
+
.map(e => {
|
|
656
|
+
const label = agyLabel(e.pool, e.window);
|
|
657
|
+
const metric = { label: label.text, labelI18n: label.i18n, type: "percent", used: (1 - e.fraction) * 100, resetsAt: e.resetsAt };
|
|
605
658
|
// Which number this is: the highest usage across the pool's models. Without
|
|
606
659
|
// it a flat 0% right after a weekly rollover is indistinguishable from a
|
|
607
660
|
// broken reading — which is exactly how it was reported.
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
661
|
+
if (!e.window || e.models > 1) {
|
|
662
|
+
metric.note = `busiest of ${e.models} model${e.models === 1 ? "" : "s"}`;
|
|
663
|
+
metric.noteI18n = i18n(e.models === 1 ? "usage.note.busiest_model" : "usage.note.busiest_models", e.models);
|
|
664
|
+
}
|
|
665
|
+
return metric;
|
|
666
|
+
});
|
|
611
667
|
}
|
|
612
668
|
/**
|
|
613
669
|
* agy's token file, in either shape.
|
|
@@ -735,8 +791,7 @@ async function fetchAntigravityUsage() {
|
|
|
735
791
|
catch {
|
|
736
792
|
continue;
|
|
737
793
|
}
|
|
738
|
-
const
|
|
739
|
-
const metrics = agyPoolMetrics(buckets);
|
|
794
|
+
const metrics = agyPoolMetrics(agyQuotaBuckets(body ?? {}));
|
|
740
795
|
return metrics.length
|
|
741
796
|
? { status: "ok", plan: null, metrics }
|
|
742
797
|
: { status: "ok", plan: null, hint: "No quota information reported for this account.", hintI18n: i18n("usage.hint.no_quota"), metrics: [] };
|