dsh-connect-agnes-token-plan 0.10.2 → 0.11.0
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/CHANGELOG.md +18 -0
- package/README.md +11 -3
- package/client.js +642 -63
- package/lib/index.js +6 -7
- package/package.json +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
本文件只记**公开行为变化**(新增能力、破坏性改动、重要修复)。实现细节、重构与测试加固请直接看 `git log`。
|
|
4
4
|
|
|
5
|
+
## [0.11.0] — 2026-10-16
|
|
6
|
+
|
|
7
|
+
本版两条主线:新能力**对话框用量胶囊**(输入框右侧的第二条读数,按 provider 门控),与**面板操作体感**批次(ADR-014)落地。
|
|
8
|
+
|
|
9
|
+
### 对话框用量胶囊:输入框右侧的第二条读数(两个上游、一个座席,provider 门控)
|
|
10
|
+
|
|
11
|
+
- **新:注册 `conversation.input.right` composer 座席并挂载 `ComposerUsagePill`**——读数按当前会话模型的 provider 门控:`agnes-token-plan` 模型画控制台 5 小时 / 每周请求窗口的使用百分比,`agnescode` 模型画桌面端积分余额(画存量、不造百分比:网关只发积分不给上限,把余额换算成百分比需要发明一个它占谁的份额);**其它 provider 的会话一律不挂载**(无读取、无定时器、无读数)——别的插件的会话绝不该带这些数字,`agnescode` 的额度宇宙也是桌面网关自己的,不能给它画控制台的窗口。
|
|
12
|
+
- **门控坐在组件边界而非 hook 之后**:外层组件只决定挂哪个内层组件,轮询 hook 全在内层里——先调 hook 再早退会违反 Rules of Hooks,React 会把组件丢弃到会话结束(姊妹插件有专门钉住这条的回归 scenario)。
|
|
13
|
+
- **读数与面板同源、不造第二把尺**:百分比走 `windowPercent`(平台自己的 `usagePct` 优先、`used/limit` 只作兜底、**从不钳制**——超额窗口报 >100 是事实不是渲染 bug),严重度点走 `usageTier`(90 / 70,与面板进度条同源);AgnesCode 侧 `0` 是「耗尽」读数、字段缺席是「读不到」(破折号),不是零。
|
|
14
|
+
- **软失败(§5 不变量②)**:读取失败保留最后读数并标记过期,不清空、不抛错;注册失败面板照常工作、该模块缺席。
|
|
15
|
+
- 影响面:`src/client/{composer-usage-pill,const,apply,cards,agnescode-tab,i18n,styles,index}.ts` + `client.js`(随 src 同 commit 重建);`test/composer-slot-gate.mjs`(新增:钉住座席 / 渲染器 / projection / 物化契约与两个 provider id 的双侧拼写)、`test/dom-render.test.mjs`(胶囊真 DOM scenario)、`test/render.test.mjs` / `test/contract.test.mjs`(§11c);文档与 CI 门禁同步。
|
|
16
|
+
|
|
17
|
+
### 面板操作体感:快照取数并行 + 保存回显不再拖全量快照(ADR-014)
|
|
18
|
+
|
|
19
|
+
- **可感变化(账号失效 / 控制台降级场景)**:① 快照五源取数全部并行(原 `overview` 先行再批量;失效账号下两条 15 s 超时链叠加,单次快照最坏 30 s 起,现压到 ~15 s;死令牌锁号不重新引入——`getToken` 单飞 + `rejected` 集 + 逐请求 401 处置,测试 B 组按新形状重钉:每源至多一次出示 / 每 poll 一次登录);② 接入 API tab 的 provider 开关 / 模型清单 / 出图·视频工具卡的「保存后全量快照 reload」改为把 POST 响应体就地并入快照 `llm` 块——关模型、切开关的反馈不再等下一轮慢轮询;③ AgnesCode 三动作(开关 / 检测本机登录态 / 保存模型清单)直接应用 POST 的完整响应,harvest 成功即时翻出已关联卡片(此前要再排一次最长 30 s 的 GET);④ `/agnescode` 路由的 balance / catalog 两个 30 s 上游读改并行(最坏 60 s → 30 s)。工具卡「实际生效模型」徽标与额度数字仍随 30 s 轮询更新(Host 计算值,本地不造数)。
|
|
20
|
+
- **归位说明**:此批次(①–④)在 0.10.2 发布(2026-10-10)之后的补记提交里被记在 `[0.10.2]` 节下,但已发布的 0.10.2 tarball 里**没有其中任何一条**(0.10.2 的 `overview` 仍先行单独取数、客户端半边尚无 `LlmEchoPatch`);`[0.10.2]` 下的「面板操作体感」节移入本节订正。
|
|
21
|
+
- 影响面:`src/host/{snapshot-aggregate,routes/agnescode}.ts`、`src/client/{panel-page,provider-controls,model-picker,wire,agnescode-tab}.ts`、`test/routes.test.mjs`、`docs/`(ADR-014、ARCHITECTURE §3、PROVIDER-HOT-RELOAD、AGNES-API、PITFALLS)、`lib/` + `client.js`(随 src 同 commit 重建)。
|
|
22
|
+
|
|
5
23
|
## [0.10.2] — 2026-10-10
|
|
6
24
|
|
|
7
25
|
### 修复:视频状态轮询断点续查、额度渲染与模型花名册的几处真实缺陷 + 凭据隔离加固
|
package/README.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# dsh-connect-agnes-token-plan
|
|
2
2
|
|
|
3
|
-
把 Agnes 接进 DSH
|
|
3
|
+
把 Agnes 接进 DSH:**Plugins 页**的插件卡(三个 tab),外加**会话输入框右侧的一枚用量胶囊**。**DSH** 即 **DeepSeek Harness**;**Agnes**(爱思)是全模态大模型平台,本插件对接其**控制台 API** 与**推理 API**。
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
> **第一次读这个仓库?先看这三句。**
|
|
7
7
|
>
|
|
8
|
-
> 1. **这是给 DeepSeek Harness
|
|
8
|
+
> 1. **这是给 DeepSeek Harness 用的插件**,不是独立应用;面板入口在 DSH 的 **Plugins 页**那张卡上(三个 tab,见下),另有一枚会话内的用量胶囊(见「对话框用量胶囊」)。
|
|
9
9
|
> 2. **三种「红」是正常的,不是回归**:① `test/wiring.test.mjs` 等 peer 套件报 `cannot resolve the peer dependency` —— 那是 DSH runtime 自带的包不在本仓库,属环境问题,先查 `test/peer-roots.mjs`;② 没装 `dsh` CLI 时 e2e 会 SKIP,属预期;③ 若在 **23:00–00:00** 之间单独跑 `render.test.mjs` 报红,那是历史上的午夜窗口缺陷(已在 0.10.0 修复,见 [docs/PITFALLS.md](docs/PITFALLS.md) §48),当前安装版本已含,无需升级。
|
|
10
10
|
> 3. **见到「奇怪」的写法先别定罪**——本仓多数刻意的取舍都在 [docs/PITFALLS.md](docs/PITFALLS.md) / [docs/ADR.md](docs/ADR.md) 有明文出处。**动手前先 grep 一遍 docs/**。
|
|
11
11
|
>
|
|
@@ -58,6 +58,14 @@
|
|
|
58
58
|
|
|
59
59
|
注意:检测不到登录态时,面板会逐条列出探测过哪些文件、各自为什么没成(「没装 App」「解不开密」「会话里没有令牌」是三种不同的处理);
|
|
60
60
|
|
|
61
|
+
## 对话框用量胶囊(自动出现)
|
|
62
|
+
|
|
63
|
+
输入框右侧会出现一枚用量胶囊,**画哪条上游的额度由当前会话选中的模型决定**,前缀写明是谁的:选中 `agnes-token-plan` 的模型显示控制台的 **5 小时 / 本周**已用百分比(前缀 `Token Plan`);选中 `agnescode`(桌面端)的模型显示那套**积分余额**(前缀 `AgnesCode`)。圆点分级(绿 / 黄 / 红 / 灰),余下的细节——重置时刻、账号昵称与时效 / 永久拆分、以及「为什么没有数字」——都在悬停提示里。
|
|
64
|
+
|
|
65
|
+
- 读数与面板**同源**:控制台百分比优先用平台自己报的 `usage_pct`、缺失才回落 `已用 / 上限`,**不做钳制**(超额窗口就显示 >100%,那是事实);桌面端画的是**存量**,所以**不造百分比**——网关只公布积分,没有「上限」可以当分母;
|
|
66
|
+
- 两条上游**互不借用**:`agnescode` 的会话只读它自己的路由、绝不画控制台窗口,反之亦然;切到**别的 provider**(其它插件)的模型时胶囊直接消失,也不再发任何请求;
|
|
67
|
+
- 读不到时**保留上一次数字并标「上次数据」**,不会清零或伪装成 0%;余额里的 `0` 是「烧完了」的读数(红色告警),`—` 才是「这次没读到」——这个区别是刻意的。
|
|
68
|
+
|
|
61
69
|
## 边界
|
|
62
70
|
|
|
63
71
|
- 面板**不代你操作账务**——不改套餐、不代扣额度、不碰 Key 明文;数据来自 Agnes 控制台自己的 API,与网页控制台口径一致。
|
|
@@ -104,7 +112,7 @@ npm run doctor:json # 机器读:JSON(可进你的巡检 / 工单脚本
|
|
|
104
112
|
- [docs/API.md](docs/API.md) — 路由与控制台端点
|
|
105
113
|
- [docs/AGNES-API.md](docs/AGNES-API.md) — Agnes 接口全集(控制台额度侧 + 推理侧)
|
|
106
114
|
- [docs/TESTING.md](docs/TESTING.md) — 测试体系
|
|
107
|
-
- [docs/PITFALLS.md](docs/PITFALLS.md) — 真实踩坑(
|
|
115
|
+
- [docs/PITFALLS.md](docs/PITFALLS.md) — 真实踩坑(56 条)
|
|
108
116
|
- [CHANGELOG.md](CHANGELOG.md) — 版本变化
|
|
109
117
|
|
|
110
118
|
AI 协作会话请先读 [AGENTS.md](AGENTS.md)。人类贡献者先读 [SETUP.md](docs/SETUP.md) 与 [ARCHITECTURE.md](docs/ARCHITECTURE.md) §2 理解结构与改动代价,改代码前再读 [AGENTS.md](AGENTS.md) 的红线与 [TESTING.md](docs/TESTING.md) 的 `npm test` 门禁。
|
package/client.js
CHANGED
|
@@ -13,10 +13,12 @@ var dsh_connect_agnes_token_plan_client = (function() {
|
|
|
13
13
|
|
|
14
14
|
//#endregion
|
|
15
15
|
//#region src/client/const.ts
|
|
16
|
-
var PANEL_ID, NS, SNAPSHOT_PATH, ACCOUNT_PATH, API_KEY_PATH, PROVIDER_PATH, MODELS_PATH, DRAW_PATH, VIDEO_PATH, AGNESCODE_PATH, AGNES_SIGNUP_URL, AGNESCODE_SITE_URL;
|
|
16
|
+
var PANEL_ID, NS, PROVIDER_ID, AGNESCODE_PROVIDER_ID, SNAPSHOT_PATH, ACCOUNT_PATH, API_KEY_PATH, PROVIDER_PATH, MODELS_PATH, DRAW_PATH, VIDEO_PATH, AGNESCODE_PATH, AGNES_SIGNUP_URL, AGNESCODE_SITE_URL;
|
|
17
17
|
var init_const = __esmMin((() => {
|
|
18
18
|
PANEL_ID = "dsh-connect-agnes-token-plan";
|
|
19
19
|
NS = PANEL_ID;
|
|
20
|
+
PROVIDER_ID = "agnes-token-plan";
|
|
21
|
+
AGNESCODE_PROVIDER_ID = "agnescode";
|
|
20
22
|
SNAPSHOT_PATH = `/api/${PANEL_ID}/snapshot`;
|
|
21
23
|
ACCOUNT_PATH = `/api/${PANEL_ID}/account`;
|
|
22
24
|
API_KEY_PATH = `/api/${PANEL_ID}/api-key`;
|
|
@@ -108,6 +110,17 @@ var dsh_connect_agnes_token_plan_client = (function() {
|
|
|
108
110
|
"quota.total.video": "视频",
|
|
109
111
|
"quota.total.activeDays": "活跃天数",
|
|
110
112
|
"quota.usageMissing": "{label}:暂未读到。",
|
|
113
|
+
"composer.planPrefix": "Token Plan",
|
|
114
|
+
"composer.window5h": "5 小时",
|
|
115
|
+
"composer.windowWeekly": "本周",
|
|
116
|
+
"composer.pillTitle": "Token Plan 额度",
|
|
117
|
+
"composer.notConnected": "控制台未连接",
|
|
118
|
+
"composer.noWindows": "控制台未返回额度窗口",
|
|
119
|
+
"composer.balancePrefix": "AgnesCode",
|
|
120
|
+
"composer.balanceTitle": "AgnesCode 积分余额",
|
|
121
|
+
"composer.balanceUnit": "积分",
|
|
122
|
+
"composer.balanceUnread": "本次没能读到余额",
|
|
123
|
+
"composer.stale": "上次数据",
|
|
111
124
|
"usage.none": "该区间内没有用量记录。",
|
|
112
125
|
"usage.period": "近 {days} 天",
|
|
113
126
|
"usage.periodUnknown": "用量区间",
|
|
@@ -330,6 +343,17 @@ var dsh_connect_agnes_token_plan_client = (function() {
|
|
|
330
343
|
"quota.total.video": "Video",
|
|
331
344
|
"quota.total.activeDays": "Active days",
|
|
332
345
|
"quota.usageMissing": "{label}: not read yet.",
|
|
346
|
+
"composer.planPrefix": "Token Plan",
|
|
347
|
+
"composer.window5h": "5h",
|
|
348
|
+
"composer.windowWeekly": "this week",
|
|
349
|
+
"composer.pillTitle": "Token Plan quota",
|
|
350
|
+
"composer.notConnected": "console not connected",
|
|
351
|
+
"composer.noWindows": "the console returned no quota windows",
|
|
352
|
+
"composer.balancePrefix": "AgnesCode",
|
|
353
|
+
"composer.balanceTitle": "AgnesCode credit balance",
|
|
354
|
+
"composer.balanceUnit": "credits",
|
|
355
|
+
"composer.balanceUnread": "the balance could not be read this time",
|
|
356
|
+
"composer.stale": "last reading",
|
|
333
357
|
"usage.none": "No usage recorded in this range.",
|
|
334
358
|
"usage.period": "Last {days} days",
|
|
335
359
|
"usage.periodUnknown": "Usage range",
|
|
@@ -1344,7 +1368,42 @@ var dsh_connect_agnes_token_plan_client = (function() {
|
|
|
1344
1368
|
fontSize: 11,
|
|
1345
1369
|
lineHeight: "16px",
|
|
1346
1370
|
marginBottom: 2
|
|
1347
|
-
}
|
|
1371
|
+
},
|
|
1372
|
+
pillTrigger: {
|
|
1373
|
+
appearance: "none",
|
|
1374
|
+
display: "inline-flex",
|
|
1375
|
+
alignItems: "center",
|
|
1376
|
+
gap: 5,
|
|
1377
|
+
height: 28,
|
|
1378
|
+
padding: "0 6px",
|
|
1379
|
+
flexShrink: 0,
|
|
1380
|
+
background: "none",
|
|
1381
|
+
border: "none",
|
|
1382
|
+
color: "inherit",
|
|
1383
|
+
fontFamily: "inherit",
|
|
1384
|
+
fontSize: 12,
|
|
1385
|
+
lineHeight: "20px",
|
|
1386
|
+
whiteSpace: "nowrap",
|
|
1387
|
+
userSelect: "none",
|
|
1388
|
+
cursor: "default"
|
|
1389
|
+
},
|
|
1390
|
+
pillDot: {
|
|
1391
|
+
width: 7,
|
|
1392
|
+
height: 7,
|
|
1393
|
+
borderRadius: 999,
|
|
1394
|
+
flexShrink: 0
|
|
1395
|
+
},
|
|
1396
|
+
pillPlan: {
|
|
1397
|
+
color: "var(--dsw-alias-label-secondary)",
|
|
1398
|
+
flexShrink: 0
|
|
1399
|
+
},
|
|
1400
|
+
pillReading: { fontVariantNumeric: "tabular-nums" },
|
|
1401
|
+
pillStale: { color: "var(--dsw-alias-label-secondary)" },
|
|
1402
|
+
pillErrorText: { color: "var(--dsw-alias-state-error-primary)" },
|
|
1403
|
+
pillTierOk: { background: "var(--dsw-alias-state-success-primary, #22c55e)" },
|
|
1404
|
+
pillTierWarn: { background: "var(--dsw-alias-state-warn-primary, #f59e0b)" },
|
|
1405
|
+
pillTierError: { background: "var(--dsw-alias-state-error-primary, #ef4444)" },
|
|
1406
|
+
pillTierUnknown: { background: "var(--dsw-alias-label-secondary)" }
|
|
1348
1407
|
};
|
|
1349
1408
|
}));
|
|
1350
1409
|
|
|
@@ -1861,14 +1920,16 @@ var dsh_connect_agnes_token_plan_client = (function() {
|
|
|
1861
1920
|
* The curated model allow-list: which of this key's models get pushed to
|
|
1862
1921
|
* DSH's model list.
|
|
1863
1922
|
*
|
|
1864
|
-
* Hook-based like `ApiKeyForm`, so the render suite exercises the secret-
|
|
1923
|
+
* Hook-based like `ApiKeyForm`, so the Node render suite exercises the secret-
|
|
1865
1924
|
* free half it draws - `ModelRoster` and the counts - instead of this
|
|
1866
|
-
* state machine. The
|
|
1867
|
-
*
|
|
1868
|
-
*
|
|
1869
|
-
*
|
|
1925
|
+
* state machine. The hook path itself (search filter, tick→dirty footer, save
|
|
1926
|
+
* POST) is mounted for real by `test/dom-render.test.mjs` (jsdom + real
|
|
1927
|
+
* react-dom over the shipped artifact). The draft/dirty/saved machinery now
|
|
1928
|
+
* lives in `roster-draft.ts`, shared with `AgnescodeModelPicker`; what stays
|
|
1929
|
+
* here is only what this roster does differently — it POSTs to `MODELS_PATH`
|
|
1930
|
+
* itself and words its errors with the `llm.roster*` keys.
|
|
1870
1931
|
*/
|
|
1871
|
-
function ModelPicker({ llm, onDone, tt }) {
|
|
1932
|
+
function ModelPicker({ llm, onDone, onEcho, tt }) {
|
|
1872
1933
|
const models = Array.isArray(llm?.models) ? llm.models : [];
|
|
1873
1934
|
const hostIds = Array.isArray(llm?.enabledModelIds) ? llm.enabledModelIds : [];
|
|
1874
1935
|
const { ids, busy, setBusy, query, setQuery, setSavedKey, notice, setNotice, dirty, justSaved, visible, tickedCount, bulk, toggle, discard } = useRosterDraft(models, hostIds);
|
|
@@ -1878,9 +1939,15 @@ var dsh_connect_agnes_token_plan_client = (function() {
|
|
|
1878
1939
|
setNotice(null);
|
|
1879
1940
|
const posted = JSON.stringify(ids);
|
|
1880
1941
|
try {
|
|
1881
|
-
await postJsonOrThrow(MODELS_PATH, { enabledModelIds: ids });
|
|
1942
|
+
const body = await postJsonOrThrow(MODELS_PATH, { enabledModelIds: ids });
|
|
1882
1943
|
setSavedKey(posted);
|
|
1883
|
-
|
|
1944
|
+
if (onEcho) onEcho({
|
|
1945
|
+
enabledModelIds: Array.isArray(body.enabledModelIds) ? body.enabledModelIds : ids,
|
|
1946
|
+
registerProvider: body.registerProvider === true,
|
|
1947
|
+
providerRegistered: body.providerRegistered === true,
|
|
1948
|
+
providerError: typeof body.providerError === "string" ? body.providerError : ""
|
|
1949
|
+
});
|
|
1950
|
+
else onDone?.();
|
|
1884
1951
|
} catch (error) {
|
|
1885
1952
|
setNotice(format(tt("llm.rosterError"), { error: error instanceof Error ? error.message : String(error) }));
|
|
1886
1953
|
} finally {
|
|
@@ -1890,6 +1957,7 @@ var dsh_connect_agnes_token_plan_client = (function() {
|
|
|
1890
1957
|
busy,
|
|
1891
1958
|
ids,
|
|
1892
1959
|
onDone,
|
|
1960
|
+
onEcho,
|
|
1893
1961
|
tt,
|
|
1894
1962
|
setBusy,
|
|
1895
1963
|
setNotice,
|
|
@@ -2213,10 +2281,12 @@ var dsh_connect_agnes_token_plan_client = (function() {
|
|
|
2213
2281
|
* restart. Hook-based like `ApiKeyForm`, so the render suite (which
|
|
2214
2282
|
* cannot mount hooks) exercises the secret-free status lines instead;
|
|
2215
2283
|
* the route itself is covered by `routes.test.mjs`. The state shown is
|
|
2216
|
-
* the SNAPSHOT's effective value, never local optimism
|
|
2217
|
-
*
|
|
2284
|
+
* the SNAPSHOT's effective value, never local optimism: the flip's OWN
|
|
2285
|
+
* POST answer (the Host's post-write state) is merged into that snapshot
|
|
2286
|
+
* by `onEcho`, and the plain `onDone` full reload is only the fallback
|
|
2287
|
+
* for mounts without an echo hook.
|
|
2218
2288
|
*/
|
|
2219
|
-
function ProviderSwitch({ llm, onDone, tt }) {
|
|
2289
|
+
function ProviderSwitch({ llm, onDone, onEcho, tt }) {
|
|
2220
2290
|
const [busy, setBusy] = useState(false);
|
|
2221
2291
|
const [switchError, setSwitchError] = useState(null);
|
|
2222
2292
|
const enabled = llm?.registerProvider === true;
|
|
@@ -2224,8 +2294,14 @@ var dsh_connect_agnes_token_plan_client = (function() {
|
|
|
2224
2294
|
setBusy(true);
|
|
2225
2295
|
setSwitchError(null);
|
|
2226
2296
|
try {
|
|
2227
|
-
await postJsonOrThrow(PROVIDER_PATH, { enabled: !enabled });
|
|
2228
|
-
|
|
2297
|
+
const body = await postJsonOrThrow(PROVIDER_PATH, { enabled: !enabled });
|
|
2298
|
+
if (onEcho) onEcho({
|
|
2299
|
+
registerProvider: body.registerProvider === true,
|
|
2300
|
+
registerSource: typeof body.registerSource === "string" ? body.registerSource : "",
|
|
2301
|
+
providerRegistered: body.providerRegistered === true,
|
|
2302
|
+
providerError: typeof body.providerError === "string" ? body.providerError : ""
|
|
2303
|
+
});
|
|
2304
|
+
else onDone?.();
|
|
2229
2305
|
} catch (error) {
|
|
2230
2306
|
setSwitchError(format(tt("llm.switchError"), { error: error instanceof Error ? error.message : String(error) }));
|
|
2231
2307
|
} finally {
|
|
@@ -2234,6 +2310,7 @@ var dsh_connect_agnes_token_plan_client = (function() {
|
|
|
2234
2310
|
}, [
|
|
2235
2311
|
enabled,
|
|
2236
2312
|
onDone,
|
|
2313
|
+
onEcho,
|
|
2237
2314
|
tt
|
|
2238
2315
|
]);
|
|
2239
2316
|
return h("div", null, h(Switch, {
|
|
@@ -2267,7 +2344,7 @@ var dsh_connect_agnes_token_plan_client = (function() {
|
|
|
2267
2344
|
* `useCallback` through, so `render.test.mjs` pins the picker's row markup —
|
|
2268
2345
|
* the `modelRowHead` wrap below is that contract.
|
|
2269
2346
|
*/
|
|
2270
|
-
function ToolSwitch({ path, prefix, modelKey, enabled, hasKey, candidates, preferred, effective, extraNote, onDone, tt }) {
|
|
2347
|
+
function ToolSwitch({ path, prefix, modelKey, enabledKey, sourceKey, preferredKey, enabled, hasKey, candidates, preferred, effective, extraNote, onDone, onEcho, tt }) {
|
|
2271
2348
|
const [busy, setBusy] = useState(false);
|
|
2272
2349
|
const [switchError, setSwitchError] = useState(null);
|
|
2273
2350
|
const k = (suffix) => tt(`${prefix}.${suffix}`);
|
|
@@ -2275,8 +2352,15 @@ var dsh_connect_agnes_token_plan_client = (function() {
|
|
|
2275
2352
|
setBusy(true);
|
|
2276
2353
|
setSwitchError(null);
|
|
2277
2354
|
try {
|
|
2278
|
-
await postJsonOrThrow(path, { enabled: !enabled });
|
|
2279
|
-
|
|
2355
|
+
const body = await postJsonOrThrow(path, { enabled: !enabled });
|
|
2356
|
+
if (onEcho) {
|
|
2357
|
+
const patch = {};
|
|
2358
|
+
if (enabledKey === "drawEnabled") patch.drawEnabled = body[enabledKey] === true;
|
|
2359
|
+
else patch.videoEnabled = body[enabledKey] === true;
|
|
2360
|
+
if (sourceKey === "drawSource") patch.drawSource = typeof body[sourceKey] === "string" ? body[sourceKey] : "";
|
|
2361
|
+
else patch.videoSource = typeof body[sourceKey] === "string" ? body[sourceKey] : "";
|
|
2362
|
+
onEcho(patch);
|
|
2363
|
+
} else onDone?.();
|
|
2280
2364
|
} catch (error) {
|
|
2281
2365
|
setSwitchError(format(k("switchError"), { error: error instanceof Error ? error.message : String(error) }));
|
|
2282
2366
|
} finally {
|
|
@@ -2285,16 +2369,25 @@ var dsh_connect_agnes_token_plan_client = (function() {
|
|
|
2285
2369
|
}, [
|
|
2286
2370
|
path,
|
|
2287
2371
|
prefix,
|
|
2372
|
+
enabledKey,
|
|
2373
|
+
sourceKey,
|
|
2288
2374
|
enabled,
|
|
2289
2375
|
onDone,
|
|
2376
|
+
onEcho,
|
|
2290
2377
|
tt
|
|
2291
2378
|
]);
|
|
2292
2379
|
const saveModel = useCallback(async (id) => {
|
|
2293
2380
|
setBusy(true);
|
|
2294
2381
|
setSwitchError(null);
|
|
2295
2382
|
try {
|
|
2296
|
-
await postJsonOrThrow(path, { [modelKey]: id });
|
|
2297
|
-
|
|
2383
|
+
const body = await postJsonOrThrow(path, { [modelKey]: id });
|
|
2384
|
+
if (onEcho) {
|
|
2385
|
+
const patch = {};
|
|
2386
|
+
const picked = typeof body[modelKey] === "string" ? body[modelKey] : "";
|
|
2387
|
+
if (preferredKey === "drawPreferredModel") patch.drawPreferredModel = picked === "" ? void 0 : picked;
|
|
2388
|
+
else patch.videoPreferredModel = picked === "" ? void 0 : picked;
|
|
2389
|
+
onEcho(patch);
|
|
2390
|
+
} else onDone?.();
|
|
2298
2391
|
} catch (error) {
|
|
2299
2392
|
setSwitchError(format(k("switchError"), { error: error instanceof Error ? error.message : String(error) }));
|
|
2300
2393
|
} finally {
|
|
@@ -2304,7 +2397,9 @@ var dsh_connect_agnes_token_plan_client = (function() {
|
|
|
2304
2397
|
path,
|
|
2305
2398
|
modelKey,
|
|
2306
2399
|
prefix,
|
|
2400
|
+
preferredKey,
|
|
2307
2401
|
onDone,
|
|
2402
|
+
onEcho,
|
|
2308
2403
|
tt
|
|
2309
2404
|
]);
|
|
2310
2405
|
const statusText = enabled ? hasKey ? k("onList") : k("needsKey") : k("off");
|
|
@@ -2384,17 +2479,21 @@ var dsh_connect_agnes_token_plan_client = (function() {
|
|
|
2384
2479
|
* shows the effective value immediately. The route is covered by
|
|
2385
2480
|
* `routes.test.mjs`.
|
|
2386
2481
|
*/
|
|
2387
|
-
function DrawSwitch({ llm, onDone, tt }) {
|
|
2482
|
+
function DrawSwitch({ llm, onDone, onEcho, tt }) {
|
|
2388
2483
|
return h(ToolSwitch, {
|
|
2389
2484
|
path: DRAW_PATH,
|
|
2390
2485
|
prefix: "draw",
|
|
2391
2486
|
modelKey: "drawModelId",
|
|
2487
|
+
enabledKey: "drawEnabled",
|
|
2488
|
+
sourceKey: "drawSource",
|
|
2489
|
+
preferredKey: "drawPreferredModel",
|
|
2392
2490
|
enabled: llm?.drawEnabled === true,
|
|
2393
2491
|
hasKey: llm?.hasApiKey === true,
|
|
2394
2492
|
candidates: rosterIds(llm?.drawCandidateIds),
|
|
2395
2493
|
preferred: llm?.drawPreferredModel != null ? String(llm.drawPreferredModel) : null,
|
|
2396
2494
|
effective: String(llm?.drawModel ?? ""),
|
|
2397
2495
|
onDone,
|
|
2496
|
+
onEcho,
|
|
2398
2497
|
tt
|
|
2399
2498
|
});
|
|
2400
2499
|
}
|
|
@@ -2411,7 +2510,7 @@ var dsh_connect_agnes_token_plan_client = (function() {
|
|
|
2411
2510
|
* nearest whole second. The card's note line says so, rather than leaving a
|
|
2412
2511
|
* reader wondering why a seconds-based model sits next to a frame-based one.
|
|
2413
2512
|
*/
|
|
2414
|
-
function VideoSwitch({ llm, onDone, tt }) {
|
|
2513
|
+
function VideoSwitch({ llm, onDone, onEcho, tt }) {
|
|
2415
2514
|
const excluded = rosterIds(llm?.video25ModelIds);
|
|
2416
2515
|
const extraNote = excluded.length > 0 ? h("div", { style: {
|
|
2417
2516
|
...S.muted,
|
|
@@ -2425,6 +2524,9 @@ var dsh_connect_agnes_token_plan_client = (function() {
|
|
|
2425
2524
|
path: VIDEO_PATH,
|
|
2426
2525
|
prefix: "video",
|
|
2427
2526
|
modelKey: "videoModelId",
|
|
2527
|
+
enabledKey: "videoEnabled",
|
|
2528
|
+
sourceKey: "videoSource",
|
|
2529
|
+
preferredKey: "videoPreferredModel",
|
|
2428
2530
|
enabled: llm?.videoEnabled === true,
|
|
2429
2531
|
hasKey: llm?.hasApiKey === true,
|
|
2430
2532
|
candidates: rosterIds(llm?.videoCandidateIds),
|
|
@@ -2432,6 +2534,7 @@ var dsh_connect_agnes_token_plan_client = (function() {
|
|
|
2432
2534
|
effective: String(llm?.videoModel ?? ""),
|
|
2433
2535
|
extraNote,
|
|
2434
2536
|
onDone,
|
|
2537
|
+
onEcho,
|
|
2435
2538
|
tt
|
|
2436
2539
|
});
|
|
2437
2540
|
}
|
|
@@ -2443,14 +2546,16 @@ var dsh_connect_agnes_token_plan_client = (function() {
|
|
|
2443
2546
|
* constituents (`ProviderSwitch`, `ProviderRegStatus`, and the `ModelPicker`
|
|
2444
2547
|
* it composes) rather than in the key editor's file.
|
|
2445
2548
|
*/
|
|
2446
|
-
function ProviderForm({ llm, onDone, tt }) {
|
|
2549
|
+
function ProviderForm({ llm, onDone, onEcho, tt }) {
|
|
2447
2550
|
return h("div", null, h(ProviderSwitch, {
|
|
2448
2551
|
llm,
|
|
2449
2552
|
onDone,
|
|
2553
|
+
onEcho,
|
|
2450
2554
|
tt
|
|
2451
2555
|
}), h(ModelPicker, {
|
|
2452
2556
|
llm,
|
|
2453
2557
|
onDone,
|
|
2558
|
+
onEcho,
|
|
2454
2559
|
tt
|
|
2455
2560
|
}), h(ProviderRegStatus, {
|
|
2456
2561
|
llm,
|
|
@@ -2607,20 +2712,63 @@ var dsh_connect_agnes_token_plan_client = (function() {
|
|
|
2607
2712
|
|
|
2608
2713
|
//#endregion
|
|
2609
2714
|
//#region src/client/cards.ts
|
|
2610
|
-
/**
|
|
2715
|
+
/**
|
|
2716
|
+
* One percentage's severity: 90 danger / 70 warn, else ok.
|
|
2717
|
+
*
|
|
2718
|
+
* Its own function because a SECOND surface reads the same thresholds — the
|
|
2719
|
+
* composer usage pill paints a status dot with them (`composer-usage-pill.ts`).
|
|
2720
|
+
* Two copies of "90 / 70" would be two answers to "when is a window in
|
|
2721
|
+
* trouble", and the panel's tonal vocabulary is not something to re-invent per
|
|
2722
|
+
* surface.
|
|
2723
|
+
*/
|
|
2724
|
+
function usageTier(pct) {
|
|
2725
|
+
if (pct >= 90) return "error";
|
|
2726
|
+
if (pct >= 70) return "warn";
|
|
2727
|
+
return "ok";
|
|
2728
|
+
}
|
|
2729
|
+
/**
|
|
2730
|
+
* The bar fill for a usage percentage, by {@link usageTier}.
|
|
2731
|
+
*
|
|
2732
|
+
* The fill is the whole answer: the bar is the only surface that colours by
|
|
2733
|
+
* tier, and the figure keeps the panel's one text colour. (This used to also
|
|
2734
|
+
* return a `color` "for the figure", which no caller ever read — a second
|
|
2735
|
+
* answer to a question nobody asked.)
|
|
2736
|
+
*/
|
|
2611
2737
|
function usageTone(pct) {
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2738
|
+
const tier = usageTier(pct);
|
|
2739
|
+
if (tier === "error") return S.barFillError;
|
|
2740
|
+
if (tier === "warn") return S.barFillWarn;
|
|
2741
|
+
return S.barFill;
|
|
2742
|
+
}
|
|
2743
|
+
/**
|
|
2744
|
+
* One window's consumption percentage — the panel's single reading of it.
|
|
2745
|
+
*
|
|
2746
|
+
* The percentage is the platform's own `usagePct`, quoted verbatim — never a
|
|
2747
|
+
* re-derivation. The Host parses it from the subscription response and ships it
|
|
2748
|
+
* down for exactly this purpose (docs/API.md §快照, AGNES-API.md: the bar is a
|
|
2749
|
+
* transcription, not a computation). Two reasons it is NOT `used/limit` first:
|
|
2750
|
+
*
|
|
2751
|
+
* - the platform's denominator need not be `limit` (it may be a separate
|
|
2752
|
+
* quota ceiling for the same window), so the arithmetic silently shows a
|
|
2753
|
+
* different number than the one the console shows;
|
|
2754
|
+
* - clamping to 100 would rewrite the platform's own value. A window in
|
|
2755
|
+
* overage reports >100, and that is a fact, not a rendering bug.
|
|
2756
|
+
*
|
|
2757
|
+
* `used / limit` remains only as the fallback for a Host that predates the
|
|
2758
|
+
* field or omits it for this window — a shape mismatch must still render.
|
|
2759
|
+
* `null` means "no reading", which is a different claim from `0`.
|
|
2760
|
+
*
|
|
2761
|
+
* Exported because the composer pill quotes the same two windows and must not
|
|
2762
|
+
* grow a second opinion about what a window's percentage is.
|
|
2763
|
+
*/
|
|
2764
|
+
function windowPercent(window) {
|
|
2765
|
+
if (window === null || typeof window !== "object") return null;
|
|
2766
|
+
const source = window;
|
|
2767
|
+
const platformPct = typeof source.usagePct === "number" && Number.isFinite(source.usagePct) ? source.usagePct : null;
|
|
2768
|
+
if (platformPct !== null) return platformPct;
|
|
2769
|
+
const limit = Number(source.limit) || 0;
|
|
2770
|
+
const used = typeof source.used === "number" && Number.isFinite(source.used) ? source.used : null;
|
|
2771
|
+
return used !== null && limit > 0 ? used / limit * 100 : null;
|
|
2624
2772
|
}
|
|
2625
2773
|
/**
|
|
2626
2774
|
* The window's own name, from the key the Host emits.
|
|
@@ -2643,18 +2791,26 @@ var dsh_connect_agnes_token_plan_client = (function() {
|
|
|
2643
2791
|
return format(tt("quota.perHours"), { hours: value });
|
|
2644
2792
|
}
|
|
2645
2793
|
/**
|
|
2646
|
-
* The unit a dimension is counted in — `""`
|
|
2794
|
+
* The unit a dimension is counted in — `""` only where naming one would be an
|
|
2795
|
+
* invention.
|
|
2647
2796
|
*
|
|
2648
2797
|
* Video is deliberately unitless. The platform's limit field is
|
|
2649
2798
|
* `video_daily_limit` while its usage field is `total_video_seconds`, and it
|
|
2650
2799
|
* never says which one the limit is expressed in, so printing "500 秒" would
|
|
2651
2800
|
* assert something nobody verified.
|
|
2801
|
+
*
|
|
2802
|
+
* An UNKNOWN unit falls back to the raw name, the same rule `windowLabel`
|
|
2803
|
+
* follows for an unknown key: a Host that grew a dimension this bundle has
|
|
2804
|
+
* never heard of must show up as itself. Returning `""` there would drop the
|
|
2805
|
+
* noun and print a naked "1500" — the exact "reads as available capacity"
|
|
2806
|
+
* shape this card exists to avoid, and silent where an unknown KEY is loud.
|
|
2652
2807
|
*/
|
|
2653
2808
|
function unitOf(unit, tt) {
|
|
2654
2809
|
const name = String(unit ?? "");
|
|
2655
2810
|
if (name === "requests") return tt("quota.unit.requests");
|
|
2656
2811
|
if (name === "images") return tt("quota.unit.images");
|
|
2657
|
-
return "";
|
|
2812
|
+
if (name === "video") return "";
|
|
2813
|
+
return name;
|
|
2658
2814
|
}
|
|
2659
2815
|
/** `monthly` / `yearly` as the panel phrases them. */
|
|
2660
2816
|
function cycleLabel(cycle, tt) {
|
|
@@ -2687,9 +2843,7 @@ var dsh_connect_agnes_token_plan_client = (function() {
|
|
|
2687
2843
|
const limit = Number(source.limit) || 0;
|
|
2688
2844
|
const unit = unitOf(source.unit, tt);
|
|
2689
2845
|
const used = typeof source.used === "number" && Number.isFinite(source.used) ? source.used : null;
|
|
2690
|
-
const
|
|
2691
|
-
const pct = platformPct !== null ? platformPct : used !== null && limit > 0 ? used / limit * 100 : null;
|
|
2692
|
-
const tone = usageTone(pct ?? 0);
|
|
2846
|
+
const pct = windowPercent(source);
|
|
2693
2847
|
const resetAt = typeof source.resetAt === "number" && Number.isFinite(source.resetAt) ? source.resetAt : null;
|
|
2694
2848
|
const resetInSeconds = typeof source.resetInSeconds === "number" && Number.isFinite(source.resetInSeconds) ? source.resetInSeconds : null;
|
|
2695
2849
|
const resetLine = resetAt !== null ? format(tt("quota.resetAt"), { time: clockSameDay(resetAt) }) : resetInSeconds !== null ? format(tt("quota.resetCountdown"), { minutes: Math.max(1, Math.round(resetInSeconds / 60)) }) : null;
|
|
@@ -2699,9 +2853,10 @@ var dsh_connect_agnes_token_plan_client = (function() {
|
|
|
2699
2853
|
"aria-label": `${label} ${tt("quota.used")} ${pct.toFixed(1)}%`,
|
|
2700
2854
|
"aria-valuenow": pct.toFixed(1),
|
|
2701
2855
|
"aria-valuemin": 0,
|
|
2702
|
-
"aria-valuemax": 100
|
|
2856
|
+
"aria-valuemax": Math.max(100, Math.ceil(pct)),
|
|
2857
|
+
"aria-valuetext": `${pct.toFixed(1)}%`
|
|
2703
2858
|
}, h("div", { style: {
|
|
2704
|
-
...
|
|
2859
|
+
...usageTone(pct),
|
|
2705
2860
|
width: `${pct}%`
|
|
2706
2861
|
} })), (pct === null || used === null) && resetLine === null ? null : h("div", { style: S.quotaFoot }, pct === null || used === null ? null : h("span", { style: S.quotaUsed }, `${tt("quota.used")} ${count(used)} / ${count(limit)}`), resetLine === null ? null : h("span", { style: {
|
|
2707
2862
|
...S.muted,
|
|
@@ -3084,11 +3239,7 @@ var dsh_connect_agnes_token_plan_client = (function() {
|
|
|
3084
3239
|
action: "switch",
|
|
3085
3240
|
enabled
|
|
3086
3241
|
});
|
|
3087
|
-
if (alive.current) setState(
|
|
3088
|
-
...current,
|
|
3089
|
-
enabled: body.enabled === true,
|
|
3090
|
-
providerRegistered: body.providerRegistered === true
|
|
3091
|
-
} : current);
|
|
3242
|
+
if (alive.current) setState(body);
|
|
3092
3243
|
} catch (why) {
|
|
3093
3244
|
if (alive.current) setNote(format(tt("agnescode.switchError"), { error: why instanceof Error ? why.message : String(why) }));
|
|
3094
3245
|
}
|
|
@@ -3099,32 +3250,23 @@ var dsh_connect_agnes_token_plan_client = (function() {
|
|
|
3099
3250
|
try {
|
|
3100
3251
|
const body = await postJson(AGNESCODE_PATH, { action: "harvest" });
|
|
3101
3252
|
if (alive.current) {
|
|
3102
|
-
|
|
3103
|
-
const attempts = Array.isArray(harvestBlock?.attempts) ? harvestBlock.attempts : [];
|
|
3104
|
-
setState((current) => current ? {
|
|
3105
|
-
...current,
|
|
3106
|
-
harvest: {
|
|
3107
|
-
ok: body?.ok === true,
|
|
3108
|
-
attempts
|
|
3109
|
-
}
|
|
3110
|
-
} : current);
|
|
3253
|
+
if (body !== null) setState(body);
|
|
3111
3254
|
if (body?.ok === true) setNote(tt("agnescode.harvestOk"));
|
|
3112
3255
|
else setNote(tt("agnescode.harvestFail"));
|
|
3113
|
-
load();
|
|
3114
3256
|
}
|
|
3115
3257
|
} catch (why) {
|
|
3116
3258
|
if (alive.current) setNote(format(tt("agnescode.error"), { error: why instanceof Error ? why.message : String(why) }));
|
|
3117
3259
|
} finally {
|
|
3118
3260
|
if (alive.current) setHarvestBusy(false);
|
|
3119
3261
|
}
|
|
3120
|
-
}, [
|
|
3262
|
+
}, [tt]);
|
|
3121
3263
|
const saveModels = useCallback(async (ids) => {
|
|
3122
|
-
await postJsonOrThrow(AGNESCODE_PATH, {
|
|
3264
|
+
const body = await postJsonOrThrow(AGNESCODE_PATH, {
|
|
3123
3265
|
action: "saveModels",
|
|
3124
3266
|
enabledModelIds: ids
|
|
3125
3267
|
});
|
|
3126
|
-
if (alive.current)
|
|
3127
|
-
}, [
|
|
3268
|
+
if (alive.current) setState(body);
|
|
3269
|
+
}, []);
|
|
3128
3270
|
const enabled = state?.enabled === true;
|
|
3129
3271
|
const view = agnescodeView(state, error);
|
|
3130
3272
|
const loggedIn = view.linked;
|
|
@@ -3560,6 +3702,21 @@ var dsh_connect_agnes_token_plan_client = (function() {
|
|
|
3560
3702
|
if (inFlight.current === controller) inFlight.current = null;
|
|
3561
3703
|
}
|
|
3562
3704
|
}, []);
|
|
3705
|
+
const dataRef = useRef(null);
|
|
3706
|
+
dataRef.current = data;
|
|
3707
|
+
const echoLlm = useCallback((patch) => {
|
|
3708
|
+
if (dataRef.current === null) {
|
|
3709
|
+
load();
|
|
3710
|
+
return;
|
|
3711
|
+
}
|
|
3712
|
+
setData((current) => current === null ? current : {
|
|
3713
|
+
...current,
|
|
3714
|
+
llm: {
|
|
3715
|
+
...current.llm ?? {},
|
|
3716
|
+
...patch
|
|
3717
|
+
}
|
|
3718
|
+
});
|
|
3719
|
+
}, [load]);
|
|
3563
3720
|
const toggleSection = useCallback((key) => {
|
|
3564
3721
|
setOpenSections((current) => ({
|
|
3565
3722
|
...current,
|
|
@@ -3749,6 +3906,7 @@ var dsh_connect_agnes_token_plan_client = (function() {
|
|
|
3749
3906
|
}, h(ProviderForm, {
|
|
3750
3907
|
llm: data?.llm ?? null,
|
|
3751
3908
|
onDone: () => void load(),
|
|
3909
|
+
onEcho: echoLlm,
|
|
3752
3910
|
tt
|
|
3753
3911
|
})), h(SectionCard, {
|
|
3754
3912
|
title: tt("draw.title"),
|
|
@@ -3758,6 +3916,7 @@ var dsh_connect_agnes_token_plan_client = (function() {
|
|
|
3758
3916
|
}, h(DrawSwitch, {
|
|
3759
3917
|
llm: data?.llm ?? null,
|
|
3760
3918
|
onDone: () => void load(),
|
|
3919
|
+
onEcho: echoLlm,
|
|
3761
3920
|
tt
|
|
3762
3921
|
})), h(SectionCard, {
|
|
3763
3922
|
title: tt("video.title"),
|
|
@@ -3767,6 +3926,7 @@ var dsh_connect_agnes_token_plan_client = (function() {
|
|
|
3767
3926
|
}, h(VideoSwitch, {
|
|
3768
3927
|
llm: data?.llm ?? null,
|
|
3769
3928
|
onDone: () => void load(),
|
|
3929
|
+
onEcho: echoLlm,
|
|
3770
3930
|
tt
|
|
3771
3931
|
})), h(SectionCard, {
|
|
3772
3932
|
title: tt("llm.title"),
|
|
@@ -3816,6 +3976,404 @@ var dsh_connect_agnes_token_plan_client = (function() {
|
|
|
3816
3976
|
init_agnescode_tab();
|
|
3817
3977
|
}));
|
|
3818
3978
|
|
|
3979
|
+
//#endregion
|
|
3980
|
+
//#region src/client/composer-usage-pill.ts
|
|
3981
|
+
/**
|
|
3982
|
+
* The provider the session is currently pointed at, `next` winning over
|
|
3983
|
+
* `lastUsed` — with one repair: an EMPTY `next` (the transient frame right after
|
|
3984
|
+
* a picker switch, before it carries the new selection) must not shadow a
|
|
3985
|
+
* still-usable `lastUsed`. A bare `next ?? lastUsed` reads a provider off `{}`,
|
|
3986
|
+
* finds none, and hides the pill for OWNED models on exactly the frames a switch
|
|
3987
|
+
* produces.
|
|
3988
|
+
*/
|
|
3989
|
+
function selectedProviderOf(projection) {
|
|
3990
|
+
const usable = (candidate) => candidate !== null && candidate !== void 0 && (typeof candidate.provider === "string" || typeof candidate.model === "string");
|
|
3991
|
+
const p = projection ?? {};
|
|
3992
|
+
const provider = (usable(p.next) ? p.next : usable(p.lastUsed) ? p.lastUsed : void 0)?.provider;
|
|
3993
|
+
return typeof provider === "string" && provider !== "" ? provider : void 0;
|
|
3994
|
+
}
|
|
3995
|
+
/**
|
|
3996
|
+
* One window by key, skipping rows that are not objects at all.
|
|
3997
|
+
*
|
|
3998
|
+
* A shape-drifted row must not throw inside the composer: the panel already
|
|
3999
|
+
* reports shape warnings, and a pill that crashes would take the toolbar down
|
|
4000
|
+
* with it.
|
|
4001
|
+
*/
|
|
4002
|
+
function windowByKey(windows, key) {
|
|
4003
|
+
if (!Array.isArray(windows)) return void 0;
|
|
4004
|
+
for (const row of windows) {
|
|
4005
|
+
if (row === null || typeof row !== "object") continue;
|
|
4006
|
+
if (row.key === key) return row;
|
|
4007
|
+
}
|
|
4008
|
+
}
|
|
4009
|
+
/**
|
|
4010
|
+
* The worse of several readings, with "unknown" only when NONE is readable.
|
|
4011
|
+
*
|
|
4012
|
+
* `null` (no reading) is skipped rather than treated as zero: a window the Host
|
|
4013
|
+
* could not measure must not drag the dot down to a healthy-looking grade.
|
|
4014
|
+
*/
|
|
4015
|
+
function worstTier(percentages) {
|
|
4016
|
+
const rank = {
|
|
4017
|
+
error: 3,
|
|
4018
|
+
warn: 2,
|
|
4019
|
+
ok: 1,
|
|
4020
|
+
unknown: 0
|
|
4021
|
+
};
|
|
4022
|
+
let worst = "unknown";
|
|
4023
|
+
for (const pct of percentages) {
|
|
4024
|
+
if (pct === null) continue;
|
|
4025
|
+
const tier = usageTier(pct);
|
|
4026
|
+
if (rank[tier] > rank[worst]) worst = tier;
|
|
4027
|
+
}
|
|
4028
|
+
return worst;
|
|
4029
|
+
}
|
|
4030
|
+
/**
|
|
4031
|
+
* A percentage as the pill writes it — the platform's own figure at one decimal,
|
|
4032
|
+
* the same precision the window card leads with. `—` is reserved for "no reading",
|
|
4033
|
+
* which is a different claim from `0.0%`.
|
|
4034
|
+
*/
|
|
4035
|
+
function percentText(pct) {
|
|
4036
|
+
return pct === null ? "—" : `${pct.toFixed(1)}%`;
|
|
4037
|
+
}
|
|
4038
|
+
/**
|
|
4039
|
+
* The pill's own route reader.
|
|
4040
|
+
*
|
|
4041
|
+
* It exists because the pill and the panel are mounted independently: the card
|
|
4042
|
+
* is a Plugins-page surface that may never be opened in a given session, while
|
|
4043
|
+
* the pill sits in the composer of every conversation whose model is one of this
|
|
4044
|
+
* plugin's. Sharing one poller would mean either polling for a card nobody
|
|
4045
|
+
* opened or rendering a card from a read the pill happened to pay for.
|
|
4046
|
+
*
|
|
4047
|
+
* ONE core, TWO routes. The generation guard, the abort, the stated cadence and
|
|
4048
|
+
* the hidden-tab pause are identical for both upstreams; only the route, the
|
|
4049
|
+
* body reading and the default rate differ. A second copy of this body is
|
|
4050
|
+
* exactly what `dup-gate` exists to refuse, and two hand-kept copies would drift
|
|
4051
|
+
* the first time one of them is repaired.
|
|
4052
|
+
*
|
|
4053
|
+
* What it does NOT carry is the panel's policy: the failure taxonomy (guidance
|
|
4054
|
+
* lines, the sign-in form gate), the refresh spinner, and the `llm` echo merge
|
|
4055
|
+
* all belong to a screen with forms on it. Here a failure is one bit — "this
|
|
4056
|
+
* reading is stale" — and the numbers stay.
|
|
4057
|
+
*
|
|
4058
|
+
* @param route - module-scope stable: a fresh literal per render would rebuild
|
|
4059
|
+
* `load` and restart the poll interval with it.
|
|
4060
|
+
* @param initialCadenceMs - the rate to follow before any answer arrives;
|
|
4061
|
+
* `null` means "no timer until something states one".
|
|
4062
|
+
*/
|
|
4063
|
+
function useRemoteRead(route, initialCadenceMs) {
|
|
4064
|
+
const [reading, setReading] = useState({
|
|
4065
|
+
data: null,
|
|
4066
|
+
updatedAt: 0,
|
|
4067
|
+
cadenceMs: initialCadenceMs,
|
|
4068
|
+
loadedOnce: false,
|
|
4069
|
+
failed: false
|
|
4070
|
+
});
|
|
4071
|
+
const cadenceRef = useRef(initialCadenceMs);
|
|
4072
|
+
cadenceRef.current = reading.cadenceMs;
|
|
4073
|
+
const generation = useRef(0);
|
|
4074
|
+
const inFlight = useRef(null);
|
|
4075
|
+
const load = useCallback(async () => {
|
|
4076
|
+
generation.current += 1;
|
|
4077
|
+
const mine = generation.current;
|
|
4078
|
+
const isCurrent = () => generation.current === mine;
|
|
4079
|
+
inFlight.current?.abort?.();
|
|
4080
|
+
const controller = typeof AbortController === "function" ? new AbortController() : null;
|
|
4081
|
+
inFlight.current = controller;
|
|
4082
|
+
const fail = () => {
|
|
4083
|
+
if (isCurrent()) setReading((current) => ({
|
|
4084
|
+
...current,
|
|
4085
|
+
failed: true,
|
|
4086
|
+
loadedOnce: true
|
|
4087
|
+
}));
|
|
4088
|
+
};
|
|
4089
|
+
try {
|
|
4090
|
+
const response = await fetch(route.path, {
|
|
4091
|
+
headers: { accept: "application/json" },
|
|
4092
|
+
cache: "no-store",
|
|
4093
|
+
...controller ? { signal: controller.signal } : {}
|
|
4094
|
+
});
|
|
4095
|
+
if (!isCurrent()) return;
|
|
4096
|
+
if (!response.ok) {
|
|
4097
|
+
fail();
|
|
4098
|
+
return;
|
|
4099
|
+
}
|
|
4100
|
+
const body = await response.json().catch(() => null);
|
|
4101
|
+
if (!isCurrent()) return;
|
|
4102
|
+
const read = route.parse(body, response.status);
|
|
4103
|
+
if (read.data === null) {
|
|
4104
|
+
fail();
|
|
4105
|
+
return;
|
|
4106
|
+
}
|
|
4107
|
+
setReading({
|
|
4108
|
+
data: read.data,
|
|
4109
|
+
updatedAt: Date.now(),
|
|
4110
|
+
cadenceMs: read.cadenceMs !== void 0 ? read.cadenceMs : cadenceRef.current,
|
|
4111
|
+
loadedOnce: true,
|
|
4112
|
+
failed: false
|
|
4113
|
+
});
|
|
4114
|
+
} catch {
|
|
4115
|
+
fail();
|
|
4116
|
+
} finally {
|
|
4117
|
+
if (inFlight.current === controller) inFlight.current = null;
|
|
4118
|
+
}
|
|
4119
|
+
}, [route]);
|
|
4120
|
+
useEffect(() => {
|
|
4121
|
+
load();
|
|
4122
|
+
}, [load]);
|
|
4123
|
+
useEffect(() => {
|
|
4124
|
+
const cadenceMs = reading.cadenceMs;
|
|
4125
|
+
if (cadenceMs === null) return void 0;
|
|
4126
|
+
let timer = null;
|
|
4127
|
+
const start = () => {
|
|
4128
|
+
if (timer === null) timer = setInterval(() => {
|
|
4129
|
+
load();
|
|
4130
|
+
}, cadenceMs);
|
|
4131
|
+
};
|
|
4132
|
+
const stop = () => {
|
|
4133
|
+
if (timer !== null) {
|
|
4134
|
+
clearInterval(timer);
|
|
4135
|
+
timer = null;
|
|
4136
|
+
}
|
|
4137
|
+
};
|
|
4138
|
+
start();
|
|
4139
|
+
const onVisibility = () => {
|
|
4140
|
+
if (typeof document === "undefined") return;
|
|
4141
|
+
if (document.visibilityState === "hidden") stop();
|
|
4142
|
+
else {
|
|
4143
|
+
load();
|
|
4144
|
+
start();
|
|
4145
|
+
}
|
|
4146
|
+
};
|
|
4147
|
+
if (typeof document !== "undefined" && typeof document.addEventListener === "function") document.addEventListener("visibilitychange", onVisibility);
|
|
4148
|
+
return () => {
|
|
4149
|
+
stop();
|
|
4150
|
+
if (typeof document !== "undefined" && typeof document.removeEventListener === "function") document.removeEventListener("visibilitychange", onVisibility);
|
|
4151
|
+
};
|
|
4152
|
+
}, [load, reading.cadenceMs]);
|
|
4153
|
+
useEffect(() => () => {
|
|
4154
|
+
generation.current += 1;
|
|
4155
|
+
inFlight.current?.abort?.();
|
|
4156
|
+
}, []);
|
|
4157
|
+
return reading;
|
|
4158
|
+
}
|
|
4159
|
+
/**
|
|
4160
|
+
* "These are the last numbers we managed to get."
|
|
4161
|
+
*
|
|
4162
|
+
* Two causes: the last read failed, or the reading has aged past two cadences —
|
|
4163
|
+
* expressed as a multiple of the rate being followed, so this file holds no
|
|
4164
|
+
* second spelling of any poll interval. Without a cadence there is no expected
|
|
4165
|
+
* freshness to judge against, so only a failure can mark it.
|
|
4166
|
+
*/
|
|
4167
|
+
function isStale(reading, now) {
|
|
4168
|
+
const aged = reading.cadenceMs !== null && reading.updatedAt > 0 && now - reading.updatedAt > reading.cadenceMs * 2;
|
|
4169
|
+
return reading.loadedOnce && (reading.failed || aged);
|
|
4170
|
+
}
|
|
4171
|
+
/** The console reader (the pill's `/snapshot` loop). */
|
|
4172
|
+
function useConsoleQuota() {
|
|
4173
|
+
return useRemoteRead(SNAPSHOT_ROUTE, null);
|
|
4174
|
+
}
|
|
4175
|
+
/** The desktop-app reader (the pill's `/agnescode` loop). */
|
|
4176
|
+
function useAgnescodeState() {
|
|
4177
|
+
return useRemoteRead(AGNESCODE_ROUTE, AGNESCODE_POLL_MS);
|
|
4178
|
+
}
|
|
4179
|
+
/** The shared shell: the tier dot, the upstream prefix, the reading. */
|
|
4180
|
+
function PillBody(props) {
|
|
4181
|
+
const { tt, planLabel, tier, stale, title, children } = props;
|
|
4182
|
+
return h("button", {
|
|
4183
|
+
type: "button",
|
|
4184
|
+
className: PILL_CLASS,
|
|
4185
|
+
title,
|
|
4186
|
+
"aria-label": title,
|
|
4187
|
+
style: S.pillTrigger
|
|
4188
|
+
}, h("span", {
|
|
4189
|
+
className: `${PILL_CLASS}-dot`,
|
|
4190
|
+
"aria-hidden": "true",
|
|
4191
|
+
style: {
|
|
4192
|
+
...S.pillDot,
|
|
4193
|
+
...TIER_INK[tier]
|
|
4194
|
+
}
|
|
4195
|
+
}), h("span", {
|
|
4196
|
+
className: `${PILL_CLASS}-plan`,
|
|
4197
|
+
style: S.pillPlan
|
|
4198
|
+
}, `${planLabel} · `), h("span", {
|
|
4199
|
+
className: `${PILL_CLASS}-text`,
|
|
4200
|
+
style: tier === "error" ? S.pillErrorText : void 0
|
|
4201
|
+
}, children, stale ? h("span", { style: S.pillStale }, ` · ${tt("composer.stale")}`) : null));
|
|
4202
|
+
}
|
|
4203
|
+
/** The first of several epoch-second instants that is a usable number, else 0. */
|
|
4204
|
+
function firstInstant(...values) {
|
|
4205
|
+
for (const value of values) if (typeof value === "number" && Number.isFinite(value) && value > 0) return value;
|
|
4206
|
+
return 0;
|
|
4207
|
+
}
|
|
4208
|
+
/**
|
|
4209
|
+
* The console reading: the two request windows and the tooltip that carries what
|
|
4210
|
+
* the body has no room for (which plan, which quota, when the window resets, and
|
|
4211
|
+
* whether this reading is the last known one).
|
|
4212
|
+
*/
|
|
4213
|
+
function ConsoleQuotaPill({ tt }) {
|
|
4214
|
+
const reading = useConsoleQuota();
|
|
4215
|
+
const quota = reading.data?.quota ?? null;
|
|
4216
|
+
const windows = Array.isArray(quota?.windows) ? quota.windows : [];
|
|
4217
|
+
const fiveHour = windowByKey(windows, WINDOW_5H);
|
|
4218
|
+
const weekly = windowByKey(windows, WINDOW_WEEKLY);
|
|
4219
|
+
const p5 = windowPercent(fiveHour);
|
|
4220
|
+
const pWeekly = windowPercent(weekly);
|
|
4221
|
+
const tier = worstTier([p5, pWeekly]);
|
|
4222
|
+
const stale = isStale(reading, Date.now());
|
|
4223
|
+
const disconnected = quota !== null && quota.consoleConnected === false;
|
|
4224
|
+
const planName = String(quota?.plan?.displayName || quota?.plan?.name || "");
|
|
4225
|
+
const resetAt = firstInstant(fiveHour?.resetAt, weekly?.resetAt);
|
|
4226
|
+
const title = [
|
|
4227
|
+
tt("composer.pillTitle"),
|
|
4228
|
+
planName === "" ? null : planName,
|
|
4229
|
+
!reading.loadedOnce ? tt("panel.loading") : disconnected ? tt("composer.notConnected") : null,
|
|
4230
|
+
reading.loadedOnce && !disconnected && windows.length === 0 ? tt("composer.noWindows") : null,
|
|
4231
|
+
resetAt > 0 ? format(tt("quota.resetAt"), { time: clockSameDay(resetAt) }) : null,
|
|
4232
|
+
stale ? tt("composer.stale") : null
|
|
4233
|
+
].filter(Boolean).join(" · ");
|
|
4234
|
+
const windowText = (label, pct) => `${label} ${percentText(pct)}`;
|
|
4235
|
+
return h(PillBody, {
|
|
4236
|
+
tt,
|
|
4237
|
+
planLabel: tt("composer.planPrefix"),
|
|
4238
|
+
tier,
|
|
4239
|
+
stale,
|
|
4240
|
+
title,
|
|
4241
|
+
children: [
|
|
4242
|
+
h("span", {
|
|
4243
|
+
key: WINDOW_5H,
|
|
4244
|
+
style: S.pillReading
|
|
4245
|
+
}, windowText(tt("composer.window5h"), p5)),
|
|
4246
|
+
" · ",
|
|
4247
|
+
h("span", {
|
|
4248
|
+
key: WINDOW_WEEKLY,
|
|
4249
|
+
style: S.pillReading
|
|
4250
|
+
}, windowText(tt("composer.windowWeekly"), pWeekly))
|
|
4251
|
+
]
|
|
4252
|
+
});
|
|
4253
|
+
}
|
|
4254
|
+
/**
|
|
4255
|
+
* One credit balance as a number, or `null` when the state declared none.
|
|
4256
|
+
*
|
|
4257
|
+
* The AgnesCode gateway publishes STOCKS: `totalBalance` plus its time-limited
|
|
4258
|
+
* and permanent parts. There is no cap in that universe, so the pill draws the
|
|
4259
|
+
* balance — never a percentage, which would require inventing the limit it is a
|
|
4260
|
+
* share of.
|
|
4261
|
+
*
|
|
4262
|
+
* `0` is a READING (a drained balance is the alarm, and it must read `0` in the
|
|
4263
|
+
* error tier), while a missing or non-finite field is ABSENCE — the grey dash.
|
|
4264
|
+
* The panel's own rule for this tab ("a rendered zero would present an unread
|
|
4265
|
+
* figure as a measurement") is the same one, applied to a field-level gap the
|
|
4266
|
+
* panel never has to face because it renders the block only when it is there.
|
|
4267
|
+
*/
|
|
4268
|
+
function balanceOf(state) {
|
|
4269
|
+
const total = state?.balance?.totalBalance;
|
|
4270
|
+
return typeof total === "number" && Number.isFinite(total) ? total : null;
|
|
4271
|
+
}
|
|
4272
|
+
/** One part of the balance (time-limited / permanent), or `null` when unreadable. */
|
|
4273
|
+
function partOf(state, key) {
|
|
4274
|
+
const value = state?.balance?.[key];
|
|
4275
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
4276
|
+
}
|
|
4277
|
+
/**
|
|
4278
|
+
* The desktop-app reading: the gateway's own credit balance.
|
|
4279
|
+
*
|
|
4280
|
+
* It is a STOCK, not a meter — see {@link balanceOf} — so the body carries the
|
|
4281
|
+
* figure and the tooltip carries the three facts that have no room there: which
|
|
4282
|
+
* account (`nickname`), how the total splits (time-limited / permanent), and —
|
|
4283
|
+
* when there is no figure at all — WHICH fact is missing. "The desktop App is
|
|
4284
|
+
* not signed in", "the provider switch is off" and "the gateway did not answer
|
|
4285
|
+
* this time" are three different problems, and a bare dash beside them left the
|
|
4286
|
+
* reader guessing (the same repair the sibling's capsule made for its gateway).
|
|
4287
|
+
*/
|
|
4288
|
+
function AgnescodeBalancePill({ tt }) {
|
|
4289
|
+
const reading = useAgnescodeState();
|
|
4290
|
+
const state = reading.data;
|
|
4291
|
+
const balance = balanceOf(state);
|
|
4292
|
+
const tier = balance === null ? "unknown" : balance <= 0 ? "error" : "ok";
|
|
4293
|
+
const stale = isStale(reading, Date.now());
|
|
4294
|
+
const pending = !reading.loadedOnce && balance === null;
|
|
4295
|
+
const why = !reading.loadedOnce || balance !== null ? null : state === null ? tt("composer.balanceUnread") : state.enabled !== true ? tt("agnescode.unregistered") : state.loggedIn !== true ? tt("agnescode.notLogged") : tt("composer.balanceUnread");
|
|
4296
|
+
const timeLimited = partOf(state, "timeSensitiveBalance");
|
|
4297
|
+
const permanent = partOf(state, "permanentBalance");
|
|
4298
|
+
const split = timeLimited === null || permanent === null ? null : `${tt("agnescode.balanceTimeLabel")} ${count(timeLimited)} · ${tt("agnescode.balancePermanentLabel")} ${count(permanent)}`;
|
|
4299
|
+
const title = [
|
|
4300
|
+
tt("composer.balanceTitle"),
|
|
4301
|
+
state?.loggedIn === true && String(state.nickname ?? "") !== "" ? format(tt("agnescode.loggedIn"), { nick: String(state.nickname) }) : null,
|
|
4302
|
+
split,
|
|
4303
|
+
why,
|
|
4304
|
+
stale ? tt("composer.stale") : null
|
|
4305
|
+
].filter(Boolean).join(" · ");
|
|
4306
|
+
return h(PillBody, {
|
|
4307
|
+
tt,
|
|
4308
|
+
planLabel: tt("composer.balancePrefix"),
|
|
4309
|
+
tier,
|
|
4310
|
+
stale,
|
|
4311
|
+
title,
|
|
4312
|
+
children: h("span", { style: S.pillReading }, pending ? "…" : balance === null ? "—" : `${count(balance)} ${tt("composer.balanceUnit")}`)
|
|
4313
|
+
});
|
|
4314
|
+
}
|
|
4315
|
+
/**
|
|
4316
|
+
* The composer pill.
|
|
4317
|
+
*
|
|
4318
|
+
* Renders nothing unless the session's model belongs to this plugin — so no other
|
|
4319
|
+
* provider's conversation carries a foreign readout, and no fetch loop runs while
|
|
4320
|
+
* it is absent (the inner reader is not mounted). WHICH upstream it belongs to is
|
|
4321
|
+
* the same decision as whether one mounts: the console provider reads the
|
|
4322
|
+
* snapshot, the desktop-app provider reads its own route, and neither is mounted
|
|
4323
|
+
* for the other's models.
|
|
4324
|
+
*/
|
|
4325
|
+
function ComposerUsagePill(props) {
|
|
4326
|
+
const { tt, useProjection } = props;
|
|
4327
|
+
const provider = selectedProviderOf((useProjection ?? (() => void 0))("modelSelection"));
|
|
4328
|
+
if (provider === "agnes-token-plan") return h(ConsoleQuotaPill, { tt });
|
|
4329
|
+
if (provider === "agnescode") return h(AgnescodeBalancePill, { tt });
|
|
4330
|
+
return null;
|
|
4331
|
+
}
|
|
4332
|
+
var PILL_CLASS, WINDOW_5H, WINDOW_WEEKLY, TIER_INK, SNAPSHOT_ROUTE, AGNESCODE_ROUTE;
|
|
4333
|
+
var init_composer_usage_pill = __esmMin((() => {
|
|
4334
|
+
init_runtime();
|
|
4335
|
+
init_styles();
|
|
4336
|
+
init_format();
|
|
4337
|
+
init_const();
|
|
4338
|
+
init_snapshot();
|
|
4339
|
+
init_cards();
|
|
4340
|
+
init_agnescode_tab();
|
|
4341
|
+
PILL_CLASS = "dsh-agnes-composer-pill";
|
|
4342
|
+
WINDOW_5H = "requests5h";
|
|
4343
|
+
WINDOW_WEEKLY = "requestsWeekly";
|
|
4344
|
+
TIER_INK = {
|
|
4345
|
+
error: S.pillTierError,
|
|
4346
|
+
warn: S.pillTierWarn,
|
|
4347
|
+
ok: S.pillTierOk,
|
|
4348
|
+
unknown: S.pillTierUnknown
|
|
4349
|
+
};
|
|
4350
|
+
SNAPSHOT_ROUTE = {
|
|
4351
|
+
path: SNAPSHOT_PATH,
|
|
4352
|
+
parse: (body, status) => {
|
|
4353
|
+
if (status < 200 || status >= 300) return { data: null };
|
|
4354
|
+
const read = interpretSnapshot(body);
|
|
4355
|
+
if (read.data === null) return { data: null };
|
|
4356
|
+
const stated = read.data.pollSeconds;
|
|
4357
|
+
return {
|
|
4358
|
+
data: read.data,
|
|
4359
|
+
cadenceMs: typeof stated === "number" && Number.isFinite(stated) && stated > 0 ? Math.floor(stated) * 1e3 : null
|
|
4360
|
+
};
|
|
4361
|
+
}
|
|
4362
|
+
};
|
|
4363
|
+
AGNESCODE_ROUTE = {
|
|
4364
|
+
path: AGNESCODE_PATH,
|
|
4365
|
+
parse: (body, status) => {
|
|
4366
|
+
if (status < 200 || status >= 300) return { data: null };
|
|
4367
|
+
const state = body;
|
|
4368
|
+
if (state === null || typeof state !== "object" || state.ok !== true) return { data: null };
|
|
4369
|
+
return {
|
|
4370
|
+
data: state,
|
|
4371
|
+
cadenceMs: AGNESCODE_POLL_MS
|
|
4372
|
+
};
|
|
4373
|
+
}
|
|
4374
|
+
};
|
|
4375
|
+
}));
|
|
4376
|
+
|
|
3819
4377
|
//#endregion
|
|
3820
4378
|
//#region src/client/apply.ts
|
|
3821
4379
|
/**
|
|
@@ -3861,6 +4419,17 @@ var dsh_connect_agnes_token_plan_client = (function() {
|
|
|
3861
4419
|
} catch (error) {
|
|
3862
4420
|
console.warn(`[${NS}] config card registration failed:`, error);
|
|
3863
4421
|
}
|
|
4422
|
+
try {
|
|
4423
|
+
disposers.push(ctx.slots.inject("conversation.input.right", () => ctx.slots.register({
|
|
4424
|
+
name: "conversation.input.right",
|
|
4425
|
+
id: `${NS}-composer-pill`,
|
|
4426
|
+
locale: NS,
|
|
4427
|
+
order: 4,
|
|
4428
|
+
inject: () => ({ tt })
|
|
4429
|
+
}, ComposerUsagePill)));
|
|
4430
|
+
} catch (error) {
|
|
4431
|
+
console.warn(`[${NS}] composer pill registration failed:`, error);
|
|
4432
|
+
}
|
|
3864
4433
|
ctx.effect(() => () => {
|
|
3865
4434
|
for (const dispose of disposers.splice(0)) try {
|
|
3866
4435
|
dispose();
|
|
@@ -3872,6 +4441,7 @@ var dsh_connect_agnes_token_plan_client = (function() {
|
|
|
3872
4441
|
init_const();
|
|
3873
4442
|
init_i18n();
|
|
3874
4443
|
init_panel_page();
|
|
4444
|
+
init_composer_usage_pill();
|
|
3875
4445
|
inject = ["slots", "locale"];
|
|
3876
4446
|
}));
|
|
3877
4447
|
|
|
@@ -3892,6 +4462,7 @@ var dsh_connect_agnes_token_plan_client = (function() {
|
|
|
3892
4462
|
init_api_key_form();
|
|
3893
4463
|
init_model_picker();
|
|
3894
4464
|
init_panel_page();
|
|
4465
|
+
init_composer_usage_pill();
|
|
3895
4466
|
init_agnescode_tab();
|
|
3896
4467
|
function clientFactory(loaderRequire) {
|
|
3897
4468
|
provideClientReact(loaderRequire("react"));
|
|
@@ -3935,7 +4506,14 @@ var dsh_connect_agnes_token_plan_client = (function() {
|
|
|
3935
4506
|
allowListFor,
|
|
3936
4507
|
toggleModelIn,
|
|
3937
4508
|
setAllModelsIn,
|
|
3938
|
-
bulkModelsIn
|
|
4509
|
+
bulkModelsIn,
|
|
4510
|
+
selectedProviderOf,
|
|
4511
|
+
windowByKey,
|
|
4512
|
+
worstTier,
|
|
4513
|
+
percentText,
|
|
4514
|
+
windowPercent,
|
|
4515
|
+
usageTier,
|
|
4516
|
+
balanceOf
|
|
3939
4517
|
}),
|
|
3940
4518
|
components: Object.freeze({
|
|
3941
4519
|
PlanCard,
|
|
@@ -3956,6 +4534,7 @@ var dsh_connect_agnes_token_plan_client = (function() {
|
|
|
3956
4534
|
ModelRoster,
|
|
3957
4535
|
ModelPicker,
|
|
3958
4536
|
PanelPage,
|
|
4537
|
+
ComposerUsagePill,
|
|
3959
4538
|
AgnescodeTab,
|
|
3960
4539
|
AgnescodeRoster,
|
|
3961
4540
|
AgnescodeModelPicker
|
package/lib/index.js
CHANGED
|
@@ -4638,8 +4638,8 @@ async function buildSnapshotBody({ settings, cache, inflight, tokenStore, apiKey
|
|
|
4638
4638
|
const providerState = publisher.state;
|
|
4639
4639
|
const resolveApiKey = async () => (await apiKeyStore.resolve()).value;
|
|
4640
4640
|
const { startDate, endDate } = usageWindow(settings.usageDays);
|
|
4641
|
-
const overview
|
|
4642
|
-
|
|
4641
|
+
const [overview, series, subscription, plans, catalog] = await Promise.all([
|
|
4642
|
+
soft(() => fetchConsole(settings, USAGE_OVERVIEW_PATH, void 0, settings.cacheSeconds * 1e3, cache, inflight, tokenStore)),
|
|
4643
4643
|
soft(() => fetchConsole(settings, USAGE_SERIES_PATH, {
|
|
4644
4644
|
range: "custom",
|
|
4645
4645
|
start_date: startDate,
|
|
@@ -5801,16 +5801,15 @@ function registerAgnescodeRoute(ctx, wiring) {
|
|
|
5801
5801
|
bffBase = state?.bffBase ?? "";
|
|
5802
5802
|
expiresAtMs = state?.expiresAtMs ?? null;
|
|
5803
5803
|
credential = (await agnescodeStore.resolve().catch(() => ({ credential: null }))).credential ?? null;
|
|
5804
|
-
if (loggedIn && credential?.accessToken) balance = await fetchAgnescodeBalance(credential).catch(() => null);
|
|
5805
5804
|
}
|
|
5806
5805
|
} catch (why) {
|
|
5807
5806
|
error = redactSecrets(why instanceof Error ? why.message : String(why));
|
|
5808
5807
|
}
|
|
5809
5808
|
let models = null;
|
|
5810
|
-
|
|
5811
|
-
|
|
5812
|
-
|
|
5813
|
-
models =
|
|
5809
|
+
if (credential?.accessToken) {
|
|
5810
|
+
const [b, m] = await Promise.all([loggedIn ? fetchAgnescodeBalance(credential).catch(() => null) : Promise.resolve(null), fetchAgnescodeCatalog(credential).catch(() => null)]);
|
|
5811
|
+
balance = b;
|
|
5812
|
+
models = m;
|
|
5814
5813
|
}
|
|
5815
5814
|
const roster = models !== null && Array.isArray(models) && models.length > 0 ? models : AGNESCODE_FALLBACK_MODELS;
|
|
5816
5815
|
const publisherState = agnescodePublisher?.state ?? null;
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "dsh-connect-agnes-token-plan",
|
|
3
3
|
"displayName": "DSH Connect Agnes Token Plan",
|
|
4
4
|
"icon": "./icon.svg",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.11.0",
|
|
6
6
|
"description": "Agnes connect bundle for the Harness Web UI: Token Plan quota panel (account-wide quota windows, cumulative usage, console sign-in) plus optional provider registration that brings Agnes models into DSH conversation, with host-side rate-limit self-healing.",
|
|
7
7
|
"keywords": [
|
|
8
8
|
"dsh-plugin",
|
|
@@ -95,6 +95,9 @@
|
|
|
95
95
|
"@commitlint/config-conventional": "^21.2.3",
|
|
96
96
|
"@types/node": "^26.6.3",
|
|
97
97
|
"jscpd": "^5.4.0",
|
|
98
|
+
"jsdom": "^30.1.3",
|
|
99
|
+
"react": "^18.3.1",
|
|
100
|
+
"react-dom": "^18.3.1",
|
|
98
101
|
"tsdown": "^0.23.0",
|
|
99
102
|
"typescript": "^5.9.3"
|
|
100
103
|
}
|