@iasiv5/dsh-quota-watch 0.0.11 → 0.0.13

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 CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  ## 功能
8
8
 
9
- - 在侧栏底部区块的最顶部(框架渲染的插件入口与设置行之上)显示一张常显摘要卡(无标题、无刷新按钮):GLM 与 GitHub Copilot 各占一行,统一显示**已用百分比**(Copilot 为 `100 − percent_remaining` 的单值反转),已用超过 80% 进度条转橙、超过 95% 转红;点击行首的 GLM / Copilot 名称立即触发一次宿主探测(等同原刷新按钮),点击行其余区域展开详情。
9
+ - 在侧栏底部区块的最顶部(框架渲染的插件入口与设置行之上)显示一张常显摘要卡(无标题、无刷新按钮):GLM 与 GitHub Copilot 各占一行,统一显示**已用百分比**(Copilot 为 `100 − percent_remaining` 的单值反转),已用超过 80% 进度条转橙、超过 95% 转红;点击行首的 GLM / Copilot 名称立即触发一次宿主探测(等同原刷新按钮),点击行其余区域展开详情。侧栏收起时改为 36×36px 无框按钮与 16px 配额仪表盘图标,和设置按钮对齐;颜色使用 DSH 主题语义变量,自动适配浅色/深色皮肤。点击图标打开 GLM / Copilot 概览,可继续查看额度详情;不显示常驻提醒点。
10
10
  - GLM 摘要行固定显示 5 小时窗口已用百分比,行尾等宽小字为当日 token 用量;点击行在侧栏右侧弹出详情面板(半透明毛玻璃底,深浅主题均可读,不改变卡片高度):今日 Tokens / 调用次数双大数字、窗口区为五列对齐网格(名称 | `·` | `已用`/♾️ | 百分比 | 同字号弱色「(重置: …)」,♾️ 与 `已用` 同列左对齐,弱色括注整列左对齐)、虚线分隔的分模型用量行(名称左对齐、token 数右对齐)。窗口按 MCP(月)→ 5 小时 → 每月 → 每周固定排序;接口未下发周窗口时显示「每周额度 · ♾️(无限)」占位,真实周窗口数据一旦下发即自动替换占位。同一时间至多展开一个详情;点击面板外或按 Esc 关闭。
11
11
  - GitHub Copilot 摘要行仅在 GitHub 返回有限、有效的 `remaining` 且带 `percent_remaining` 时显示;行尾等宽小字显示本期 credits 消耗的紧凑值。点击在侧栏右侧弹出精简详情(标签左对齐、数值右对齐的三行):可用额度、本期已用(credits 消耗 + 弱化的已用百分比)、重置时间。`credits_used` 绝不用于推算余额,缺失有效百分比时该行不渲染。
12
12
  - 卡片顶部不显示全局更新时间;仅在等待首个快照或刷新失败时显示一行状态文字(读取中 / 刷新失败 / 查询失败)。
@@ -48,4 +48,4 @@ dsh plugin --profile web add @iasiv5/dsh-quota-watch
48
48
 
49
49
  ## English summary
50
50
 
51
- DSH Quota Watch is a personal DSH Web sidebar card for GLM Coding Plan windows and GitHub Copilot's provider-reported quota snapshot. It resolves credentials on every host-side probe, sends only normalized values to the browser, keeps no usage history, and never infers a Copilot balance from credits used. The Copilot endpoint is undocumented and may change; invalid or unlimited quota snapshots are shown as unavailable rather than guessed.
51
+ DSH Quota Watch is a personal DSH Web sidebar card for GLM Coding Plan windows and GitHub Copilot's provider-reported quota snapshot. It resolves credentials on every host-side probe, sends only normalized values to the browser, keeps no usage history, and never infers a Copilot balance from credits used. When the sidebar is collapsed, a 36px control with a 16px quota-gauge icon replaces the text card; clicking it opens provider summaries and details. The icon uses DSH semantic theme tokens and has no permanent border or alert badge. The Copilot endpoint is undocumented and may change; invalid or unlimited quota snapshots are shown as unavailable rather than guessed.
package/lib/client.js CHANGED
@@ -89,6 +89,9 @@ var FETCH_TIMEOUT_MS = 15e3;
89
89
  var COPY = {
90
90
  zh: {
91
91
  title: "\u5957\u9910\u76D1\u63A7",
92
+ openDetails: "\u67E5\u770B\u8BE6\u60C5",
93
+ backToOverview: "\u8FD4\u56DE\u989D\u5EA6\u6982\u89C8",
94
+ providerNames: { glm: "GLM", copilot: "Copilot" },
92
95
  refresh: "\u5237\u65B0",
93
96
  loading: "\u8BFB\u53D6\u4E2D",
94
97
  failed: "\u67E5\u8BE2\u5931\u8D25",
@@ -103,7 +106,7 @@ var COPY = {
103
106
  todayCalls: "\u8C03\u7528\u6B21\u6570",
104
107
  weekUnlimited: "\u6BCF\u5468\u989D\u5EA6",
105
108
  unlimited: "\u267E\uFE0F",
106
- unlimitedNote: "\uFF08\u65E0\u9650\uFF09",
109
+ unlimitedNote: "\uFF08 \u65E0\u9650 \uFF09",
107
110
  windows: {
108
111
  "5h": "5 \u5C0F\u65F6\u989D\u5EA6",
109
112
  week: "\u6BCF\u5468\u989D\u5EA6",
@@ -113,6 +116,9 @@ var COPY = {
113
116
  },
114
117
  en: {
115
118
  title: "Quota Watch",
119
+ openDetails: "View details",
120
+ backToOverview: "Back to quota overview",
121
+ providerNames: { glm: "GLM", copilot: "Copilot" },
116
122
  refresh: "Refresh",
117
123
  loading: "Loading",
118
124
  failed: "Query failed",
@@ -127,7 +133,7 @@ var COPY = {
127
133
  todayCalls: "Calls",
128
134
  weekUnlimited: "Weekly quota",
129
135
  unlimited: "\u267E\uFE0F",
130
- unlimitedNote: " (unlimited)",
136
+ unlimitedNote: " ( unlimited )",
131
137
  windows: {
132
138
  "5h": "5-hour quota",
133
139
  week: "Weekly quota",
@@ -159,10 +165,26 @@ var STYLE_TEXT = `
159
165
  .dqw-chev { font-size: 9px; line-height: 14px; opacity: .6; flex: none; width: 10px; text-align: center; }
160
166
  .dqw-stale-mark { flex: none; font-size: 9px; line-height: 14px; color: var(--dsw-alias-label-warning, #b46900); cursor: help; }
161
167
  .dqw-errtext { font-size: 10px; line-height: 14px; opacity: .7; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
168
+ .dqw-rail-trigger { display: none; flex: none; align-items: center; justify-content: center; width: 36px; height: 36px; margin: 0; padding: 0; border: 0; border-radius: 50%; background: transparent; color: var(--dsw-alias-label-primary, inherit); cursor: pointer; transition: background-color .12s, color .12s; }
169
+ .dqw-rail-trigger svg { display: block; width: 16px; height: 16px; }
170
+ .dqw-rail-trigger:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); color: var(--dsw-alias-label-primary, inherit); }
171
+ .dqw-rail-trigger:active:not(:disabled), .dqw-rail-trigger[aria-expanded="true"] { background: var(--dsw-alias-interactive-bg-active, rgba(128,128,128,.18)); color: var(--dsw-alias-label-primary, inherit); }
172
+ .dqw-rail-trigger:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary, #5b8def); outline-offset: 2px; }
173
+ [data-dsh-frame][data-sidebar-collapsed] [data-dsh-quota-watch-card] { display: flex; align-items: center; justify-content: center; width: 36px; margin: 0 0 12px; }
174
+ [data-dsh-frame][data-sidebar-collapsed] [data-dsh-quota-watch-card] > .dqw-card { display: none; }
175
+ [data-dsh-frame][data-sidebar-collapsed] [data-dsh-quota-watch-card] > .dqw-rail-trigger { display: inline-flex; }
162
176
  .dqw-pop { position: fixed; z-index: 2147483000; min-width: 166px; max-width: 320px; padding: 10px 12px; border: 1px solid var(--dsw-alias-border-secondary, rgba(128,128,128,.35)); border-radius: 10px; background: var(--dsw-alias-bg-elevated, var(--dsw-alias-bg-base, #1f1f1f)); background: color-mix(in srgb, var(--dsw-alias-bg-base, #1f1f1f) 86%, transparent); -webkit-backdrop-filter: blur(14px) saturate(1.3); backdrop-filter: blur(14px) saturate(1.3); box-shadow: 0 8px 24px rgba(0,0,0,.25); color: var(--dsw-alias-label-primary, inherit); display: flex; flex-direction: column; gap: 8px; font: inherit; }
163
177
  @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) { .dqw-pop { background: var(--dsw-alias-bg-elevated, var(--dsw-alias-bg-base, #1f1f1f)); } }
164
178
  .dqw-pop[hidden] { display: none; }
165
179
  .dqw-pop *, .dqw-pop *::before, .dqw-pop *::after { box-sizing: border-box; }
180
+ .dqw-pop-title { margin: 0; font-size: 11px; font-weight: 600; line-height: 16px; }
181
+ .dqw-pop-back { align-self: flex-start; margin: 0; padding: 3px 5px; border: 0; border-radius: 6px; background: transparent; color: var(--dsw-alias-label-secondary, inherit); font: inherit; font-size: 10.5px; line-height: 14px; cursor: pointer; }
182
+ .dqw-pop-back:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); color: var(--dsw-alias-label-primary, inherit); }
183
+ .dqw-overview-list { display: flex; flex-direction: column; gap: 2px; }
184
+ .dqw-overview-row { display: flex; align-items: center; width: 100%; gap: 6px; margin: 0; padding: 5px 4px; border: 0; border-radius: 8px; background: transparent; color: inherit; font: inherit; text-align: left; cursor: pointer; }
185
+ .dqw-overview-row:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); }
186
+ .dqw-overview-row:focus-visible { outline: 1px solid var(--dsw-alias-brand-primary, #5b8def); outline-offset: -1px; }
187
+ .dqw-overview-error { display: flex; align-items: center; gap: 6px; padding: 5px 4px; }
166
188
  .dqw-bignums { display: flex; gap: 6px; }
167
189
  .dqw-big { flex: 1; border: 1px solid var(--dsw-alias-border-secondary, rgba(128,128,128,.25)); border-radius: 8px; padding: 5px 8px; }
168
190
  .dqw-big .dqw-big-value { font-size: 15px; font-weight: 650; line-height: 19px; font-variant-numeric: tabular-nums; }
@@ -218,6 +240,32 @@ function text(doc, tag, className, value) {
218
240
  node.textContent = String(value ?? "");
219
241
  return node;
220
242
  }
243
+ function quotaIcon(doc) {
244
+ const namespace = "http://www.w3.org/2000/svg";
245
+ const svg = doc.createElementNS(namespace, "svg");
246
+ svg.setAttribute("viewBox", "0 0 24 24");
247
+ svg.setAttribute("width", "16");
248
+ svg.setAttribute("height", "16");
249
+ svg.setAttribute("fill", "none");
250
+ svg.setAttribute("stroke", "currentColor");
251
+ svg.setAttribute("stroke-width", "1.7");
252
+ svg.setAttribute("stroke-linecap", "round");
253
+ svg.setAttribute("stroke-linejoin", "round");
254
+ svg.setAttribute("aria-hidden", "true");
255
+ svg.setAttribute("focusable", "false");
256
+ const arc = doc.createElementNS(namespace, "path");
257
+ arc.setAttribute("d", "M4 17a8 8 0 0 1 16 0");
258
+ const needle = doc.createElementNS(namespace, "path");
259
+ needle.setAttribute("d", "m12 17 3.2-5.2");
260
+ const hub = doc.createElementNS(namespace, "circle");
261
+ hub.setAttribute("cx", "12");
262
+ hub.setAttribute("cy", "17");
263
+ hub.setAttribute("r", "1");
264
+ hub.setAttribute("fill", "currentColor");
265
+ hub.setAttribute("stroke", "none");
266
+ svg.append(arc, needle, hub);
267
+ return svg;
268
+ }
221
269
  function kvRow(doc, label, value) {
222
270
  const row = doc.createElement("div");
223
271
  row.className = "dqw-kv";
@@ -289,15 +337,26 @@ function mountQuotaCard({
289
337
  const card = doc.createElement("section");
290
338
  card.className = "dqw-card";
291
339
  card.setAttribute("aria-label", copy.title);
340
+ const railTrigger = doc.createElement("button");
341
+ railTrigger.type = "button";
342
+ railTrigger.className = "dqw-rail-trigger";
343
+ railTrigger.title = copy.title;
344
+ railTrigger.setAttribute("aria-label", copy.title);
345
+ railTrigger.setAttribute("aria-haspopup", "dialog");
346
+ railTrigger.setAttribute("aria-controls", "dqw-quota-watch-pop");
347
+ railTrigger.setAttribute("aria-expanded", "false");
348
+ railTrigger.append(quotaIcon(doc));
292
349
  const body = doc.createElement("div");
293
350
  body.className = "dqw-body";
294
351
  const lastUpdated = text(doc, "p", "dqw-meta", copy.loading);
295
352
  lastUpdated.dataset.role = "updated";
296
353
  body.append(lastUpdated);
297
354
  card.append(body);
298
- container.append(style, card);
355
+ container.append(style, card, railTrigger);
299
356
  const pop = doc.createElement("div");
300
357
  pop.className = "dqw-pop";
358
+ pop.id = "dqw-quota-watch-pop";
359
+ pop.setAttribute("role", "dialog");
301
360
  pop.dataset.dshQuotaWatchPop = "";
302
361
  pop.hidden = true;
303
362
  doc.body.append(pop);
@@ -307,6 +366,7 @@ function mountQuotaCard({
307
366
  let disposed = false;
308
367
  let queuedPlace = false;
309
368
  let openKey;
369
+ let popOrigin = "row";
310
370
  const fillClass = (percent) => percent > 95 ? "dqw-bar-fill danger" : percent > 80 ? "dqw-bar-fill warn" : "dqw-bar-fill";
311
371
  const renderProvider = (key, model) => {
312
372
  if (model === null) return null;
@@ -353,8 +413,73 @@ function mountQuotaCard({
353
413
  row.append(text(doc, "span", "dqw-chev", openKey === key ? "\u25BE" : "\u25B8"));
354
414
  return [row];
355
415
  };
356
- function renderDetail(detail, key, provider) {
416
+ const renderOverviewProvider = (key, model) => {
417
+ if (model === null) return null;
418
+ const label = text(doc, "span", "dqw-label", model.label);
419
+ if (model.kind === "error") {
420
+ const row2 = doc.createElement("div");
421
+ row2.className = "dqw-overview-error";
422
+ row2.append(label, text(doc, "span", "dqw-errtext", model.error ? `${copy.failed}: ${model.error}` : copy.failed));
423
+ return row2;
424
+ }
425
+ const row = doc.createElement("button");
426
+ row.type = "button";
427
+ row.className = "dqw-overview-row";
428
+ row.dataset.dshQuotaWatchPopProvider = key;
429
+ row.setAttribute("aria-expanded", "false");
430
+ row.setAttribute("aria-label", `${model.label} \xB7 ${typeof model.percent === "number" ? formatPercent(model.percent, locale) : "\u2014"} \xB7 ${copy.openDetails}`);
431
+ row.append(label);
432
+ if (typeof model.percent === "number" && Number.isFinite(model.percent)) {
433
+ const bar = doc.createElement("span");
434
+ bar.className = "dqw-bar";
435
+ bar.setAttribute("role", "progressbar");
436
+ bar.setAttribute("aria-valuemin", "0");
437
+ bar.setAttribute("aria-valuemax", "100");
438
+ bar.setAttribute("aria-valuenow", String(Math.max(0, Math.min(100, model.percent))));
439
+ const fill = text(doc, "span", fillClass(model.percent));
440
+ fill.style.width = `${Math.max(0, Math.min(100, model.percent))}%`;
441
+ bar.append(fill);
442
+ row.append(bar);
443
+ }
444
+ if (model.stale) {
445
+ const mark = text(doc, "span", "dqw-stale-mark", "\u26A0");
446
+ mark.dataset.dshQuotaWatchStale = "";
447
+ mark.title = model.error ?? copy.stale;
448
+ row.append(mark);
449
+ }
450
+ row.append(text(doc, "span", "dqw-pct", typeof model.percent === "number" ? formatPercent(model.percent, locale) : "\u2014"));
451
+ row.append(text(doc, "span", "dqw-chev", "\u25B8"));
452
+ return row;
453
+ };
454
+ const renderOverview = () => {
455
+ pop.replaceChildren(text(doc, "p", "dqw-pop-title", copy.title));
456
+ const providers = snapshot?.providers ?? [];
457
+ const glm = providers.find((provider) => provider?.key === "glm");
458
+ const copilot = providers.find((provider) => provider?.key === "copilot");
459
+ const rows = [
460
+ renderOverviewProvider("glm", glmRowModel(glm)),
461
+ renderOverviewProvider("copilot", copilotRowModel(copilot))
462
+ ].filter(Boolean);
463
+ if (rows.length === 0) {
464
+ pop.append(text(doc, "p", "dqw-muted", lastUpdated.textContent || copy.loading));
465
+ return;
466
+ }
467
+ const list = doc.createElement("div");
468
+ list.className = "dqw-overview-list";
469
+ list.append(...rows);
470
+ pop.append(list);
471
+ };
472
+ function renderDetail(detail, key, provider, fromRail = false) {
357
473
  detail.replaceChildren();
474
+ if (fromRail) {
475
+ const back = doc.createElement("button");
476
+ back.type = "button";
477
+ back.className = "dqw-pop-back";
478
+ back.dataset.action = "back-to-overview";
479
+ back.setAttribute("aria-label", copy.backToOverview);
480
+ back.textContent = copy.backToOverview;
481
+ detail.append(back, text(doc, "p", "dqw-pop-title", copy.providerNames[key]));
482
+ }
358
483
  if (provider?.status === "stale") {
359
484
  detail.append(text(doc, "p", "dqw-error", `${copy.stale}${provider.error ? `: ${provider.error}` : ""}`));
360
485
  }
@@ -410,7 +535,7 @@ function mountQuotaCard({
410
535
  wins.append(text(doc, "span", "dqw-win-mid", "\xB7"));
411
536
  wins.append(text(doc, "span", "dqw-win-used", copy.windowUsed));
412
537
  wins.append(text(doc, "span", "dqw-win-val", percent !== void 0 ? formatPercent(percent, locale) : "\u2014"));
413
- wins.append(text(doc, "span", "dqw-kv-sub", reset ? `\uFF08${copy.reset}: ${reset}\uFF09` : ""));
538
+ wins.append(text(doc, "span", "dqw-kv-sub", reset ? `\uFF08 ${copy.reset}: ${reset} \uFF09` : ""));
414
539
  }
415
540
  detail.append(wins);
416
541
  if (Array.isArray(usage?.models) && usage.models.length > 0) {
@@ -451,9 +576,9 @@ function mountQuotaCard({
451
576
  }
452
577
  const positionPop = () => {
453
578
  if (pop.hidden || openKey === void 0) return;
454
- const row = body.querySelector(`[data-dsh-quota-watch-row="${openKey}"]`);
455
- if (!row) return;
456
- const rect = row.getBoundingClientRect();
579
+ const anchor = popOrigin === "rail" ? railTrigger : body.querySelector(`[data-dsh-quota-watch-row="${openKey}"]`);
580
+ if (!anchor) return;
581
+ const rect = anchor.getBoundingClientRect();
457
582
  const viewWidth = win.innerWidth ?? 1024;
458
583
  const viewHeight = win.innerHeight ?? 768;
459
584
  const width = pop.offsetWidth || 240;
@@ -467,24 +592,35 @@ function mountQuotaCard({
467
592
  pop.style.top = `${Math.round(top)}px`;
468
593
  };
469
594
  const syncPop = () => {
595
+ const fromRail = popOrigin === "rail" && openKey !== void 0 && !disposed;
596
+ railTrigger.setAttribute("aria-expanded", String(fromRail));
470
597
  if (openKey === void 0 || disposed) {
471
- if (!pop.hidden) {
472
- pop.hidden = true;
473
- pop.replaceChildren();
474
- delete pop.dataset.dshQuotaWatchDetail;
475
- }
598
+ pop.hidden = true;
599
+ pop.replaceChildren();
600
+ delete pop.dataset.dshQuotaWatchDetail;
601
+ delete pop.dataset.dshQuotaWatchOverview;
602
+ return;
603
+ }
604
+ if (openKey === "overview") {
605
+ renderOverview();
606
+ delete pop.dataset.dshQuotaWatchDetail;
607
+ pop.dataset.dshQuotaWatchOverview = "";
608
+ pop.setAttribute("aria-label", copy.title);
609
+ pop.hidden = false;
610
+ positionPop();
476
611
  return;
477
612
  }
478
613
  const provider = (snapshot?.providers ?? []).find((item) => item?.key === openKey);
479
614
  const row = body.querySelector(`[data-dsh-quota-watch-row="${openKey}"]`);
480
- if (!provider || !row || !row.hasAttribute("data-expandable")) {
615
+ if (!provider || !fromRail && (!row || !row.hasAttribute("data-expandable"))) {
481
616
  openKey = void 0;
482
617
  syncPop();
483
618
  return;
484
619
  }
485
- renderDetail(pop, openKey, provider);
620
+ renderDetail(pop, openKey, provider, fromRail);
486
621
  pop.dataset.dshQuotaWatchDetail = openKey;
487
- pop.setAttribute("aria-label", openKey === "glm" ? "GLM" : "Copilot");
622
+ delete pop.dataset.dshQuotaWatchOverview;
623
+ pop.setAttribute("aria-label", fromRail ? `${copy.providerNames[openKey]} \xB7 ${copy.title}` : copy.providerNames[openKey]);
488
624
  pop.hidden = false;
489
625
  positionPop();
490
626
  };
@@ -542,8 +678,27 @@ function mountQuotaCard({
542
678
  if (!disposed) place();
543
679
  });
544
680
  };
545
- const observer = new win.MutationObserver(schedulePlace);
546
- observer.observe(doc.body, { childList: true, subtree: true });
681
+ let sidebarCollapsed = Boolean(doc.querySelector("[data-dsh-frame][data-sidebar-collapsed]"));
682
+ const syncSidebarMode = () => {
683
+ const nextCollapsed = Boolean(doc.querySelector("[data-dsh-frame][data-sidebar-collapsed]"));
684
+ if (nextCollapsed === sidebarCollapsed) return;
685
+ sidebarCollapsed = nextCollapsed;
686
+ if (openKey !== void 0) {
687
+ openKey = void 0;
688
+ popOrigin = "row";
689
+ syncPop();
690
+ }
691
+ };
692
+ const observer = new win.MutationObserver(() => {
693
+ syncSidebarMode();
694
+ schedulePlace();
695
+ });
696
+ observer.observe(doc.body, {
697
+ childList: true,
698
+ subtree: true,
699
+ attributes: true,
700
+ attributeFilter: ["data-sidebar-collapsed"]
701
+ });
547
702
  place();
548
703
  const startPolling = () => {
549
704
  if (timer !== void 0 || doc.visibilityState === "hidden" || disposed) return;
@@ -561,9 +716,34 @@ function mountQuotaCard({
561
716
  else startPolling();
562
717
  };
563
718
  const toggleRow = (key) => {
719
+ popOrigin = "row";
564
720
  openKey = openKey === key ? void 0 : key;
565
721
  renderAll();
566
722
  };
723
+ const onRailClick = () => {
724
+ if (popOrigin === "rail" && openKey !== void 0) {
725
+ openKey = void 0;
726
+ popOrigin = "row";
727
+ } else {
728
+ openKey = "overview";
729
+ popOrigin = "rail";
730
+ }
731
+ syncPop();
732
+ };
733
+ const onPopClick = (event) => {
734
+ if (event.target?.closest?.('[data-action="back-to-overview"]')) {
735
+ openKey = "overview";
736
+ popOrigin = "rail";
737
+ syncPop();
738
+ return;
739
+ }
740
+ const provider = event.target?.closest?.("[data-dsh-quota-watch-pop-provider]");
741
+ if (provider) {
742
+ openKey = provider.dataset.dshQuotaWatchPopProvider;
743
+ popOrigin = "rail";
744
+ syncPop();
745
+ }
746
+ };
567
747
  const onBodyClick = (event) => {
568
748
  if (event.target?.closest?.('[data-action="refresh"]')) {
569
749
  void poll(true);
@@ -585,6 +765,7 @@ function mountQuotaCard({
585
765
  const target = event.target;
586
766
  if (pop.contains(target) || target?.closest?.("[data-dsh-quota-watch-row]") || target?.closest?.(CARD_SELECTOR)) return;
587
767
  openKey = void 0;
768
+ popOrigin = "row";
588
769
  renderAll();
589
770
  };
590
771
  const onDocScroll = () => {
@@ -596,11 +777,14 @@ function mountQuotaCard({
596
777
  const onDocKeydown = (event) => {
597
778
  if (event.key === "Escape" && openKey !== void 0) {
598
779
  openKey = void 0;
780
+ popOrigin = "row";
599
781
  renderAll();
600
782
  }
601
783
  };
602
784
  body.addEventListener("click", onBodyClick);
603
785
  body.addEventListener("keydown", onBodyKeydown);
786
+ railTrigger.addEventListener("click", onRailClick);
787
+ pop.addEventListener("click", onPopClick);
604
788
  doc.addEventListener("visibilitychange", onVisibilityChange);
605
789
  doc.addEventListener("pointerdown", onDocPointerDown, true);
606
790
  doc.addEventListener("scroll", onDocScroll, true);
@@ -618,6 +802,8 @@ function mountQuotaCard({
618
802
  win.removeEventListener("resize", onWinResize);
619
803
  body.removeEventListener("click", onBodyClick);
620
804
  body.removeEventListener("keydown", onBodyKeydown);
805
+ railTrigger.removeEventListener("click", onRailClick);
806
+ pop.removeEventListener("click", onPopClick);
621
807
  container.remove();
622
808
  pop.remove();
623
809
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iasiv5/dsh-quota-watch",
3
- "version": "0.0.11",
3
+ "version": "0.0.13",
4
4
  "description": "DSH Web sidebar monitor for GLM Coding Plan quotas and GitHub Copilot credits",
5
5
  "type": "module",
6
6
  "main": "./src/host.mjs",
package/src/client.mjs CHANGED
@@ -10,6 +10,9 @@ const FETCH_TIMEOUT_MS = 15_000
10
10
  const COPY = {
11
11
  zh: {
12
12
  title: '套餐监控',
13
+ openDetails: '查看详情',
14
+ backToOverview: '返回额度概览',
15
+ providerNames: { glm: 'GLM', copilot: 'Copilot' },
13
16
  refresh: '刷新',
14
17
  loading: '读取中',
15
18
  failed: '查询失败',
@@ -24,7 +27,7 @@ const COPY = {
24
27
  todayCalls: '调用次数',
25
28
  weekUnlimited: '每周额度',
26
29
  unlimited: '♾️',
27
- unlimitedNote: '(无限)',
30
+ unlimitedNote: '( 无限 )',
28
31
  windows: {
29
32
  '5h': '5 小时额度',
30
33
  week: '每周额度',
@@ -34,6 +37,9 @@ const COPY = {
34
37
  },
35
38
  en: {
36
39
  title: 'Quota Watch',
40
+ openDetails: 'View details',
41
+ backToOverview: 'Back to quota overview',
42
+ providerNames: { glm: 'GLM', copilot: 'Copilot' },
37
43
  refresh: 'Refresh',
38
44
  loading: 'Loading',
39
45
  failed: 'Query failed',
@@ -48,7 +54,7 @@ const COPY = {
48
54
  todayCalls: 'Calls',
49
55
  weekUnlimited: 'Weekly quota',
50
56
  unlimited: '♾️',
51
- unlimitedNote: ' (unlimited)',
57
+ unlimitedNote: ' ( unlimited )',
52
58
  windows: {
53
59
  '5h': '5-hour quota',
54
60
  week: 'Weekly quota',
@@ -81,10 +87,26 @@ const STYLE_TEXT = `
81
87
  .dqw-chev { font-size: 9px; line-height: 14px; opacity: .6; flex: none; width: 10px; text-align: center; }
82
88
  .dqw-stale-mark { flex: none; font-size: 9px; line-height: 14px; color: var(--dsw-alias-label-warning, #b46900); cursor: help; }
83
89
  .dqw-errtext { font-size: 10px; line-height: 14px; opacity: .7; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
90
+ .dqw-rail-trigger { display: none; flex: none; align-items: center; justify-content: center; width: 36px; height: 36px; margin: 0; padding: 0; border: 0; border-radius: 50%; background: transparent; color: var(--dsw-alias-label-primary, inherit); cursor: pointer; transition: background-color .12s, color .12s; }
91
+ .dqw-rail-trigger svg { display: block; width: 16px; height: 16px; }
92
+ .dqw-rail-trigger:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); color: var(--dsw-alias-label-primary, inherit); }
93
+ .dqw-rail-trigger:active:not(:disabled), .dqw-rail-trigger[aria-expanded="true"] { background: var(--dsw-alias-interactive-bg-active, rgba(128,128,128,.18)); color: var(--dsw-alias-label-primary, inherit); }
94
+ .dqw-rail-trigger:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary, #5b8def); outline-offset: 2px; }
95
+ [data-dsh-frame][data-sidebar-collapsed] [data-dsh-quota-watch-card] { display: flex; align-items: center; justify-content: center; width: 36px; margin: 0 0 12px; }
96
+ [data-dsh-frame][data-sidebar-collapsed] [data-dsh-quota-watch-card] > .dqw-card { display: none; }
97
+ [data-dsh-frame][data-sidebar-collapsed] [data-dsh-quota-watch-card] > .dqw-rail-trigger { display: inline-flex; }
84
98
  .dqw-pop { position: fixed; z-index: 2147483000; min-width: 166px; max-width: 320px; padding: 10px 12px; border: 1px solid var(--dsw-alias-border-secondary, rgba(128,128,128,.35)); border-radius: 10px; background: var(--dsw-alias-bg-elevated, var(--dsw-alias-bg-base, #1f1f1f)); background: color-mix(in srgb, var(--dsw-alias-bg-base, #1f1f1f) 86%, transparent); -webkit-backdrop-filter: blur(14px) saturate(1.3); backdrop-filter: blur(14px) saturate(1.3); box-shadow: 0 8px 24px rgba(0,0,0,.25); color: var(--dsw-alias-label-primary, inherit); display: flex; flex-direction: column; gap: 8px; font: inherit; }
85
99
  @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) { .dqw-pop { background: var(--dsw-alias-bg-elevated, var(--dsw-alias-bg-base, #1f1f1f)); } }
86
100
  .dqw-pop[hidden] { display: none; }
87
101
  .dqw-pop *, .dqw-pop *::before, .dqw-pop *::after { box-sizing: border-box; }
102
+ .dqw-pop-title { margin: 0; font-size: 11px; font-weight: 600; line-height: 16px; }
103
+ .dqw-pop-back { align-self: flex-start; margin: 0; padding: 3px 5px; border: 0; border-radius: 6px; background: transparent; color: var(--dsw-alias-label-secondary, inherit); font: inherit; font-size: 10.5px; line-height: 14px; cursor: pointer; }
104
+ .dqw-pop-back:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); color: var(--dsw-alias-label-primary, inherit); }
105
+ .dqw-overview-list { display: flex; flex-direction: column; gap: 2px; }
106
+ .dqw-overview-row { display: flex; align-items: center; width: 100%; gap: 6px; margin: 0; padding: 5px 4px; border: 0; border-radius: 8px; background: transparent; color: inherit; font: inherit; text-align: left; cursor: pointer; }
107
+ .dqw-overview-row:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); }
108
+ .dqw-overview-row:focus-visible { outline: 1px solid var(--dsw-alias-brand-primary, #5b8def); outline-offset: -1px; }
109
+ .dqw-overview-error { display: flex; align-items: center; gap: 6px; padding: 5px 4px; }
88
110
  .dqw-bignums { display: flex; gap: 6px; }
89
111
  .dqw-big { flex: 1; border: 1px solid var(--dsw-alias-border-secondary, rgba(128,128,128,.25)); border-radius: 8px; padding: 5px 8px; }
90
112
  .dqw-big .dqw-big-value { font-size: 15px; font-weight: 650; line-height: 19px; font-variant-numeric: tabular-nums; }
@@ -147,6 +169,33 @@ function text(doc, tag, className, value) {
147
169
  return node
148
170
  }
149
171
 
172
+ function quotaIcon(doc) {
173
+ const namespace = 'http://www.w3.org/2000/svg'
174
+ const svg = doc.createElementNS(namespace, 'svg')
175
+ svg.setAttribute('viewBox', '0 0 24 24')
176
+ svg.setAttribute('width', '16')
177
+ svg.setAttribute('height', '16')
178
+ svg.setAttribute('fill', 'none')
179
+ svg.setAttribute('stroke', 'currentColor')
180
+ svg.setAttribute('stroke-width', '1.7')
181
+ svg.setAttribute('stroke-linecap', 'round')
182
+ svg.setAttribute('stroke-linejoin', 'round')
183
+ svg.setAttribute('aria-hidden', 'true')
184
+ svg.setAttribute('focusable', 'false')
185
+ const arc = doc.createElementNS(namespace, 'path')
186
+ arc.setAttribute('d', 'M4 17a8 8 0 0 1 16 0')
187
+ const needle = doc.createElementNS(namespace, 'path')
188
+ needle.setAttribute('d', 'm12 17 3.2-5.2')
189
+ const hub = doc.createElementNS(namespace, 'circle')
190
+ hub.setAttribute('cx', '12')
191
+ hub.setAttribute('cy', '17')
192
+ hub.setAttribute('r', '1')
193
+ hub.setAttribute('fill', 'currentColor')
194
+ hub.setAttribute('stroke', 'none')
195
+ svg.append(arc, needle, hub)
196
+ return svg
197
+ }
198
+
150
199
  /** Aligned label/value line used by the simplified Copilot detail. */
151
200
  function kvRow(doc, label, value) {
152
201
  const row = doc.createElement('div')
@@ -228,6 +277,15 @@ export function mountQuotaCard({
228
277
  const card = doc.createElement('section')
229
278
  card.className = 'dqw-card'
230
279
  card.setAttribute('aria-label', copy.title)
280
+ const railTrigger = doc.createElement('button')
281
+ railTrigger.type = 'button'
282
+ railTrigger.className = 'dqw-rail-trigger'
283
+ railTrigger.title = copy.title
284
+ railTrigger.setAttribute('aria-label', copy.title)
285
+ railTrigger.setAttribute('aria-haspopup', 'dialog')
286
+ railTrigger.setAttribute('aria-controls', 'dqw-quota-watch-pop')
287
+ railTrigger.setAttribute('aria-expanded', 'false')
288
+ railTrigger.append(quotaIcon(doc))
231
289
  const body = doc.createElement('div')
232
290
  body.className = 'dqw-body'
233
291
  // Status line doubles as the transport-error surface; hidden once data renders.
@@ -235,10 +293,12 @@ export function mountQuotaCard({
235
293
  lastUpdated.dataset.role = 'updated'
236
294
  body.append(lastUpdated)
237
295
  card.append(body)
238
- container.append(style, card)
239
- // Details open in a fixed-position panel to the right of the rows.
296
+ container.append(style, card, railTrigger)
297
+ // Details and collapsed-rail summaries open in a viewport-level panel.
240
298
  const pop = doc.createElement('div')
241
299
  pop.className = 'dqw-pop'
300
+ pop.id = 'dqw-quota-watch-pop'
301
+ pop.setAttribute('role', 'dialog')
242
302
  pop.dataset.dshQuotaWatchPop = ''
243
303
  pop.hidden = true
244
304
  doc.body.append(pop)
@@ -248,6 +308,7 @@ export function mountQuotaCard({
248
308
  let disposed = false
249
309
  let queuedPlace = false
250
310
  let openKey
311
+ let popOrigin = 'row'
251
312
 
252
313
  const fillClass = (percent) => (percent > 95 ? 'dqw-bar-fill danger' : percent > 80 ? 'dqw-bar-fill warn' : 'dqw-bar-fill')
253
314
 
@@ -299,8 +360,75 @@ export function mountQuotaCard({
299
360
  return [row]
300
361
  }
301
362
 
302
- function renderDetail(detail, key, provider) {
363
+ const renderOverviewProvider = (key, model) => {
364
+ if (model === null) return null
365
+ const label = text(doc, 'span', 'dqw-label', model.label)
366
+ if (model.kind === 'error') {
367
+ const row = doc.createElement('div')
368
+ row.className = 'dqw-overview-error'
369
+ row.append(label, text(doc, 'span', 'dqw-errtext', model.error ? `${copy.failed}: ${model.error}` : copy.failed))
370
+ return row
371
+ }
372
+ const row = doc.createElement('button')
373
+ row.type = 'button'
374
+ row.className = 'dqw-overview-row'
375
+ row.dataset.dshQuotaWatchPopProvider = key
376
+ row.setAttribute('aria-expanded', 'false')
377
+ row.setAttribute('aria-label', `${model.label} · ${typeof model.percent === 'number' ? formatPercent(model.percent, locale) : '—'} · ${copy.openDetails}`)
378
+ row.append(label)
379
+ if (typeof model.percent === 'number' && Number.isFinite(model.percent)) {
380
+ const bar = doc.createElement('span')
381
+ bar.className = 'dqw-bar'
382
+ bar.setAttribute('role', 'progressbar')
383
+ bar.setAttribute('aria-valuemin', '0')
384
+ bar.setAttribute('aria-valuemax', '100')
385
+ bar.setAttribute('aria-valuenow', String(Math.max(0, Math.min(100, model.percent))))
386
+ const fill = text(doc, 'span', fillClass(model.percent))
387
+ fill.style.width = `${Math.max(0, Math.min(100, model.percent))}%`
388
+ bar.append(fill)
389
+ row.append(bar)
390
+ }
391
+ if (model.stale) {
392
+ const mark = text(doc, 'span', 'dqw-stale-mark', '⚠')
393
+ mark.dataset.dshQuotaWatchStale = ''
394
+ mark.title = model.error ?? copy.stale
395
+ row.append(mark)
396
+ }
397
+ row.append(text(doc, 'span', 'dqw-pct', typeof model.percent === 'number' ? formatPercent(model.percent, locale) : '—'))
398
+ row.append(text(doc, 'span', 'dqw-chev', '▸'))
399
+ return row
400
+ }
401
+
402
+ const renderOverview = () => {
403
+ pop.replaceChildren(text(doc, 'p', 'dqw-pop-title', copy.title))
404
+ const providers = snapshot?.providers ?? []
405
+ const glm = providers.find((provider) => provider?.key === 'glm')
406
+ const copilot = providers.find((provider) => provider?.key === 'copilot')
407
+ const rows = [
408
+ renderOverviewProvider('glm', glmRowModel(glm)),
409
+ renderOverviewProvider('copilot', copilotRowModel(copilot)),
410
+ ].filter(Boolean)
411
+ if (rows.length === 0) {
412
+ pop.append(text(doc, 'p', 'dqw-muted', lastUpdated.textContent || copy.loading))
413
+ return
414
+ }
415
+ const list = doc.createElement('div')
416
+ list.className = 'dqw-overview-list'
417
+ list.append(...rows)
418
+ pop.append(list)
419
+ }
420
+
421
+ function renderDetail(detail, key, provider, fromRail = false) {
303
422
  detail.replaceChildren()
423
+ if (fromRail) {
424
+ const back = doc.createElement('button')
425
+ back.type = 'button'
426
+ back.className = 'dqw-pop-back'
427
+ back.dataset.action = 'back-to-overview'
428
+ back.setAttribute('aria-label', copy.backToOverview)
429
+ back.textContent = copy.backToOverview
430
+ detail.append(back, text(doc, 'p', 'dqw-pop-title', copy.providerNames[key]))
431
+ }
304
432
  if (provider?.status === 'stale') {
305
433
  detail.append(text(doc, 'p', 'dqw-error', `${copy.stale}${provider.error ? `: ${provider.error}` : ''}`))
306
434
  }
@@ -360,7 +488,7 @@ export function mountQuotaCard({
360
488
  wins.append(text(doc, 'span', 'dqw-win-mid', '·'))
361
489
  wins.append(text(doc, 'span', 'dqw-win-used', copy.windowUsed))
362
490
  wins.append(text(doc, 'span', 'dqw-win-val', percent !== undefined ? formatPercent(percent, locale) : '—'))
363
- wins.append(text(doc, 'span', 'dqw-kv-sub', reset ? `(${copy.reset}: ${reset})` : ''))
491
+ wins.append(text(doc, 'span', 'dqw-kv-sub', reset ? `( ${copy.reset}: ${reset} )` : ''))
364
492
  }
365
493
  detail.append(wins)
366
494
  if (Array.isArray(usage?.models) && usage.models.length > 0) {
@@ -401,12 +529,14 @@ export function mountQuotaCard({
401
529
  }
402
530
  }
403
531
 
404
- /** Keep the detail panel anchored to the expanded row's right edge. */
532
+ /** Anchor the popover to the rail icon or the expanded provider row. */
405
533
  const positionPop = () => {
406
534
  if (pop.hidden || openKey === undefined) return
407
- const row = body.querySelector(`[data-dsh-quota-watch-row="${openKey}"]`)
408
- if (!row) return
409
- const rect = row.getBoundingClientRect()
535
+ const anchor = popOrigin === 'rail'
536
+ ? railTrigger
537
+ : body.querySelector(`[data-dsh-quota-watch-row="${openKey}"]`)
538
+ if (!anchor) return
539
+ const rect = anchor.getBoundingClientRect()
410
540
  const viewWidth = win.innerWidth ?? 1024
411
541
  const viewHeight = win.innerHeight ?? 768
412
542
  const width = pop.offsetWidth || 240
@@ -421,24 +551,35 @@ export function mountQuotaCard({
421
551
  }
422
552
 
423
553
  const syncPop = () => {
554
+ const fromRail = popOrigin === 'rail' && openKey !== undefined && !disposed
555
+ railTrigger.setAttribute('aria-expanded', String(fromRail))
424
556
  if (openKey === undefined || disposed) {
425
- if (!pop.hidden) {
426
- pop.hidden = true
427
- pop.replaceChildren()
428
- delete pop.dataset.dshQuotaWatchDetail
429
- }
557
+ pop.hidden = true
558
+ pop.replaceChildren()
559
+ delete pop.dataset.dshQuotaWatchDetail
560
+ delete pop.dataset.dshQuotaWatchOverview
561
+ return
562
+ }
563
+ if (openKey === 'overview') {
564
+ renderOverview()
565
+ delete pop.dataset.dshQuotaWatchDetail
566
+ pop.dataset.dshQuotaWatchOverview = ''
567
+ pop.setAttribute('aria-label', copy.title)
568
+ pop.hidden = false
569
+ positionPop()
430
570
  return
431
571
  }
432
572
  const provider = (snapshot?.providers ?? []).find((item) => item?.key === openKey)
433
573
  const row = body.querySelector(`[data-dsh-quota-watch-row="${openKey}"]`)
434
- if (!provider || !row || !row.hasAttribute('data-expandable')) {
574
+ if (!provider || (!fromRail && (!row || !row.hasAttribute('data-expandable')))) {
435
575
  openKey = undefined
436
576
  syncPop()
437
577
  return
438
578
  }
439
- renderDetail(pop, openKey, provider)
579
+ renderDetail(pop, openKey, provider, fromRail)
440
580
  pop.dataset.dshQuotaWatchDetail = openKey
441
- pop.setAttribute('aria-label', openKey === 'glm' ? 'GLM' : 'Copilot')
581
+ delete pop.dataset.dshQuotaWatchOverview
582
+ pop.setAttribute('aria-label', fromRail ? `${copy.providerNames[openKey]} · ${copy.title}` : copy.providerNames[openKey])
442
583
  pop.hidden = false
443
584
  positionPop()
444
585
  }
@@ -506,8 +647,27 @@ export function mountQuotaCard({
506
647
  })
507
648
  }
508
649
 
509
- const observer = new win.MutationObserver(schedulePlace)
510
- observer.observe(doc.body, { childList: true, subtree: true })
650
+ let sidebarCollapsed = Boolean(doc.querySelector('[data-dsh-frame][data-sidebar-collapsed]'))
651
+ const syncSidebarMode = () => {
652
+ const nextCollapsed = Boolean(doc.querySelector('[data-dsh-frame][data-sidebar-collapsed]'))
653
+ if (nextCollapsed === sidebarCollapsed) return
654
+ sidebarCollapsed = nextCollapsed
655
+ if (openKey !== undefined) {
656
+ openKey = undefined
657
+ popOrigin = 'row'
658
+ syncPop()
659
+ }
660
+ }
661
+ const observer = new win.MutationObserver(() => {
662
+ syncSidebarMode()
663
+ schedulePlace()
664
+ })
665
+ observer.observe(doc.body, {
666
+ childList: true,
667
+ subtree: true,
668
+ attributes: true,
669
+ attributeFilter: ['data-sidebar-collapsed'],
670
+ })
511
671
  place()
512
672
 
513
673
  const startPolling = () => {
@@ -524,9 +684,34 @@ export function mountQuotaCard({
524
684
  else startPolling()
525
685
  }
526
686
  const toggleRow = (key) => {
687
+ popOrigin = 'row'
527
688
  openKey = openKey === key ? undefined : key
528
689
  renderAll()
529
690
  }
691
+ const onRailClick = () => {
692
+ if (popOrigin === 'rail' && openKey !== undefined) {
693
+ openKey = undefined
694
+ popOrigin = 'row'
695
+ } else {
696
+ openKey = 'overview'
697
+ popOrigin = 'rail'
698
+ }
699
+ syncPop()
700
+ }
701
+ const onPopClick = (event) => {
702
+ if (event.target?.closest?.('[data-action="back-to-overview"]')) {
703
+ openKey = 'overview'
704
+ popOrigin = 'rail'
705
+ syncPop()
706
+ return
707
+ }
708
+ const provider = event.target?.closest?.('[data-dsh-quota-watch-pop-provider]')
709
+ if (provider) {
710
+ openKey = provider.dataset.dshQuotaWatchPopProvider
711
+ popOrigin = 'rail'
712
+ syncPop()
713
+ }
714
+ }
530
715
  const onBodyClick = (event) => {
531
716
  if (event.target?.closest?.('[data-action="refresh"]')) {
532
717
  void poll(true)
@@ -548,6 +733,7 @@ export function mountQuotaCard({
548
733
  const target = event.target
549
734
  if (pop.contains(target) || target?.closest?.('[data-dsh-quota-watch-row]') || target?.closest?.(CARD_SELECTOR)) return
550
735
  openKey = undefined
736
+ popOrigin = 'row'
551
737
  renderAll()
552
738
  }
553
739
  const onDocScroll = () => { positionPop() }
@@ -555,11 +741,14 @@ export function mountQuotaCard({
555
741
  const onDocKeydown = (event) => {
556
742
  if (event.key === 'Escape' && openKey !== undefined) {
557
743
  openKey = undefined
744
+ popOrigin = 'row'
558
745
  renderAll()
559
746
  }
560
747
  }
561
748
  body.addEventListener('click', onBodyClick)
562
749
  body.addEventListener('keydown', onBodyKeydown)
750
+ railTrigger.addEventListener('click', onRailClick)
751
+ pop.addEventListener('click', onPopClick)
563
752
  doc.addEventListener('visibilitychange', onVisibilityChange)
564
753
  doc.addEventListener('pointerdown', onDocPointerDown, true)
565
754
  doc.addEventListener('scroll', onDocScroll, true)
@@ -578,6 +767,8 @@ export function mountQuotaCard({
578
767
  win.removeEventListener('resize', onWinResize)
579
768
  body.removeEventListener('click', onBodyClick)
580
769
  body.removeEventListener('keydown', onBodyKeydown)
770
+ railTrigger.removeEventListener('click', onRailClick)
771
+ pop.removeEventListener('click', onPopClick)
581
772
  container.remove()
582
773
  pop.remove()
583
774
  }