@morehao/dsh-github-trending 0.4.5 → 0.5.1

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/client.js CHANGED
@@ -55,6 +55,8 @@ var DICT = {
55
55
  "st.repo": "仓库",
56
56
  "st.auth": "认证",
57
57
  "st.scope": "榜单",
58
+ "st.summarize": "摘要与用量",
59
+ "st.releases": "发版订阅",
58
60
  "st.schedule": "调度",
59
61
  "card.source.title": "内容来源",
60
62
  "card.source.hint": "唯一来源是本地二进制",
@@ -231,6 +233,7 @@ var DICT = {
231
233
  "nav.overview": "概览",
232
234
  "nav.digest": "榜单",
233
235
  "nav.runs": "运行记录",
236
+ "nav.subscriptions": "订阅",
234
237
  "nav.settings": "设置",
235
238
  "period.daily": "日榜",
236
239
  "period.weekly": "周榜",
@@ -240,6 +243,7 @@ var DICT = {
240
243
  "shell.digest.dek": "按周期回看日榜 / 周榜 / 月榜;只有保留过预览副本的期数能读。",
241
244
  "shell.runs.dek": "最近 30 次运行;展开看交付物、错误与日志。",
242
245
  "shell.settings.dek": "沿用现有设置页;每个分区独立保存。",
246
+ "shell.subscriptions.dek": "仓库发版订阅列表,随数据仓版本化;经核心 subscribe 命令增删。",
243
247
  "shell.nextRun": "下次运行",
244
248
  "shell.scheduleOff": "定时未开启",
245
249
  "shell.ready": "就绪 {ready} / {total}",
@@ -361,6 +365,80 @@ var DICT = {
361
365
  "log.copy": "复制日志",
362
366
  "log.copied": "已复制",
363
367
  "log.close": "关闭",
368
+ "run.usage.tokens": "本次任务 tokens",
369
+ "run.usage.tokensTotal": "累计全部用量",
370
+ "run.usage.tokensNote": "只展示总用量,不拆 prompt/completion 等维度;本次任务总量 = 榜单摘要 + 发版订阅阶段(含订阅)。旧核心无数据时显示「—」。",
371
+ "run.usage.breakdown": "榜单摘要 {summary} · 发版订阅 {releases} · 合计 {total}",
372
+ "run.usage.breakdownSimple": "榜单摘要 {summary} · 合计 {total}",
373
+ "run.usage.releasesOnly": "发版订阅 {releases} · 合计 {total}",
374
+ "releases.card.title": "发版订阅(releases 阶段)",
375
+ "releases.card.on": "已开启",
376
+ "releases.card.off": "未开启",
377
+ "releases.stat.repos": "仓库",
378
+ "releases.stat.new": "新发版",
379
+ "releases.stat.summarized": "已摘要",
380
+ "releases.stat.degraded": "降级",
381
+ "releases.stat.failed": "失败",
382
+ "releases.stat.skipped": "跳过",
383
+ "releases.stat.tokens": "本阶段 tokens",
384
+ "releases.card.hint": "订阅仓库的新 Release 写成中文摘要,与榜单同分支同 PR。",
385
+ "releases.badge.summarized": "已摘要",
386
+ "releases.badge.degraded": "降级为原文",
387
+ "releases.badge.off": "未启用",
388
+ "releases.empty": "本次未启用发版订阅",
389
+ "runs.detail.tokens": "本次任务 tokens",
390
+ "runs.detail.releases": "发版订阅",
391
+ "runs.detail.tokensNone": "—",
392
+ "runs.footer.total": "历史累计 tokens:{total}",
393
+ "card.summarize.title": "摘要与用量",
394
+ "card.summarize.hint": "对应核心 summarize.*(core 0.4.1+)",
395
+ "summarize.batchSize": "批量大小",
396
+ "summarize.batchSize.help": "每次模型调用处理的仓库数;批次越大速度越快,单次提示词也越长。",
397
+ "summarize.maxRetries": "失败重试",
398
+ "summarize.maxRetries.help": "单批摘要失败后的额外重试次数;0 表示不重试,仍失败按降级策略处理。",
399
+ "summarize.enrich": "外部增强",
400
+ "summarize.enrich.help": "摘要前补充外部上下文:repo 抓取 GitHub topics,readme 再摘录 README;需要只读 token。",
401
+ "summarize.enrich.none": "不增强",
402
+ "summarize.enrich.repo": "仓库(含 GitHub topics)",
403
+ "summarize.enrich.readme": "仓库(含 README 摘录)",
404
+ "summarize.readmeChars": "README 摘录长度",
405
+ "summarize.readmeChars.help": "enrich 为 readme 时,每个仓库 README 摘录的最大字符数(超出截断)。",
406
+ "summarize.enrichNote": "外部增强需要只读 token:enrich=repo/readme 建议配置 GITHUB_READ_TOKEN(独立只读);readme 模式在网络慢时可能因预算耗尽跳过部分仓库(自动降级,不影响运行)。",
407
+ "card.releases.title": "发版订阅",
408
+ "card.releases.hint": "对应核心 releases.*(core 0.5.0+)",
409
+ "releases.includePrerelease": "含预发布",
410
+ "releases.includePrerelease.help": "GitHub 上标记为 pre-release 的版本也计入新发版。",
411
+ "releases.includeDrafts": "含草稿",
412
+ "releases.includeDrafts.help": "草稿(draft)Release 也计入;草稿只有写权限的 token 才看得到。",
413
+ "releases.notesChars": "notes 截断",
414
+ "releases.notesChars.help": "单个 Release notes 的截断长度,避免提示词过长(500–20000)。",
415
+ "releases.manage": "管理订阅列表 →",
416
+ "subs.title": "仓库发版订阅",
417
+ "subs.desc": "订阅后,每次运行会把每个仓库「新增的 Release(无 Release 时回退 Tag)」写成中文 changelog,渲染到 <output.dir>/releases/,与榜单同分支同 PR;没有订阅时不产出、也不改动榜单产物。",
418
+ "subs.add": "添加仓库",
419
+ "subs.add.hint": "添加订阅(owner/repo,一个或多个,逗号/空格分隔)",
420
+ "subs.add.placeholder": "denoland/deno",
421
+ "subs.add.confirm": "添加",
422
+ "subs.adding": "添加中…",
423
+ "subs.cancel": "取消",
424
+ "subs.add.note": "写入前校验 owner/repo 形态、去重;只写 .git/pending,随下次运行落盘并提交。",
425
+ "subs.add.queued": "已加入待提交队列,下次运行随产物提交。",
426
+ "subs.remove.queued": "已标记移除,下次运行随产物提交。",
427
+ "subs.remove": "移除",
428
+ "subs.removing": "移除中…",
429
+ "subs.empty": "还没有订阅任何仓库",
430
+ "subs.loading": "加载订阅列表…",
431
+ "subs.input.invalid": "请输入 owner/repo 形态,例如 vercel/next.js",
432
+ "subs.col.repo": "仓库(owner/repo)",
433
+ "subs.param.hint": "阶段行为参数(每仓上限、回退 Tag、周期等)在设置页「发版订阅」分区调整。",
434
+ "subs.param.link": "前往设置 → 发版订阅",
435
+ "overview.tokens.title": "Token 用量",
436
+ "overview.tokens.thisRun": "本次任务",
437
+ "overview.tokens.total": "累计全部",
438
+ "overview.releases.title": "发版订阅",
439
+ "overview.releases.new": "新发版",
440
+ "overview.releases.repos": "仓库",
441
+ "overview.tokens.note": "概览一屏带出「这次任务共花了多少 token / 历史上累计多少 / 订阅仓库有几个新版本」;只给总用量,不拆维度;本次任务总量已含订阅阶段消耗。旧核心无数据时对应小块整体隐藏,不留 0。",
364
442
  "about.title": "关于",
365
443
  "about.copy": "复制完整包名",
366
444
  "about.copied": "已复制",
@@ -383,6 +461,8 @@ var DICT = {
383
461
  "st.repo": "Repository",
384
462
  "st.auth": "Auth",
385
463
  "st.scope": "Scope",
464
+ "st.summarize": "Summary & usage",
465
+ "st.releases": "Releases",
386
466
  "st.schedule": "Schedule",
387
467
  "card.source.title": "Content source",
388
468
  "card.source.hint": "one source: the local binary",
@@ -559,6 +639,7 @@ var DICT = {
559
639
  "nav.overview": "Overview",
560
640
  "nav.digest": "Board",
561
641
  "nav.runs": "Runs",
642
+ "nav.subscriptions": "Subscriptions",
562
643
  "nav.settings": "Settings",
563
644
  "period.daily": "Daily",
564
645
  "period.weekly": "Weekly",
@@ -568,6 +649,7 @@ var DICT = {
568
649
  "shell.digest.dek": "Look back per period — daily / weekly / monthly; only issues with a saved preview copy can be read.",
569
650
  "shell.runs.dek": "The last 30 runs; expand one for artifacts, errors and logs.",
570
651
  "shell.settings.dek": "The existing settings page; each section saves on its own.",
652
+ "shell.subscriptions.dek": "Repo release subscription list, versioned in the data repo; add or remove via the core subscribe command.",
571
653
  "shell.nextRun": "Next run",
572
654
  "shell.scheduleOff": "Schedule off",
573
655
  "shell.ready": "Ready {ready} / {total}",
@@ -689,6 +771,80 @@ var DICT = {
689
771
  "log.copy": "Copy log",
690
772
  "log.copied": "Copied",
691
773
  "log.close": "Close",
774
+ "run.usage.tokens": "This run tokens",
775
+ "run.usage.tokensTotal": "All-time total",
776
+ "run.usage.tokensNote": 'Only total usage is shown, not split into prompt/completion; this run total = board summary + releases stage (subscriptions included). Old core with no data shows "—".',
777
+ "run.usage.breakdown": "Board {summary} · Releases {releases} · Total {total}",
778
+ "run.usage.breakdownSimple": "Board {summary} · Total {total}",
779
+ "run.usage.releasesOnly": "Releases {releases} · Total {total}",
780
+ "releases.card.title": "Releases stage",
781
+ "releases.card.on": "On",
782
+ "releases.card.off": "Off",
783
+ "releases.stat.repos": "Repos",
784
+ "releases.stat.new": "New",
785
+ "releases.stat.summarized": "Summarized",
786
+ "releases.stat.degraded": "Degraded",
787
+ "releases.stat.failed": "Failed",
788
+ "releases.stat.skipped": "Skipped",
789
+ "releases.stat.tokens": "Stage tokens",
790
+ "releases.card.hint": "New releases of subscribed repos become Chinese summaries, committed to the same branch/PR as the board.",
791
+ "releases.badge.summarized": "Summarized",
792
+ "releases.badge.degraded": "Degraded to original",
793
+ "releases.badge.off": "Disabled",
794
+ "releases.empty": "Releases were not enabled for this run",
795
+ "runs.detail.tokens": "This run tokens",
796
+ "runs.detail.releases": "Releases",
797
+ "runs.detail.tokensNone": "—",
798
+ "runs.footer.total": "All-time tokens: {total}",
799
+ "card.summarize.title": "Summary & usage",
800
+ "card.summarize.hint": "Maps to core summarize.* (core 0.4.1+)",
801
+ "summarize.batchSize": "Batch size",
802
+ "summarize.batchSize.help": "Repos per model call; larger batches run faster but make each prompt longer.",
803
+ "summarize.maxRetries": "Retries",
804
+ "summarize.maxRetries.help": "Extra retries per failed batch; 0 disables retry, and a batch that still fails follows the degrade policy.",
805
+ "summarize.enrich": "Enrich",
806
+ "summarize.enrich.help": "Adds external context before summarizing: repo fetches GitHub topics, readme also excerpts the README. Requires a read-only token.",
807
+ "summarize.enrich.none": "None",
808
+ "summarize.enrich.repo": "Repo (+GitHub topics)",
809
+ "summarize.enrich.readme": "Repo (+README excerpt)",
810
+ "summarize.readmeChars": "README excerpt chars",
811
+ "summarize.readmeChars.help": "Max README characters excerpted per repo when enrich=readme (longer text is truncated).",
812
+ "summarize.enrichNote": "Enrich needs a read-only token: enrich=repo/readme is best paired with GITHUB_READ_TOKEN (separate, read-only). In readme mode slow networks may skip some repos when the budget runs out (auto-degrades; the run is unaffected).",
813
+ "card.releases.title": "Releases",
814
+ "card.releases.hint": "Maps to core releases.* (core 0.5.0+)",
815
+ "releases.includePrerelease": "Include pre-releases",
816
+ "releases.includePrerelease.help": "Releases marked as pre-release on GitHub also count as new releases.",
817
+ "releases.includeDrafts": "Include drafts",
818
+ "releases.includeDrafts.help": "Draft releases also count; drafts are only visible to tokens with write access.",
819
+ "releases.notesChars": "Notes truncate",
820
+ "releases.notesChars.help": "Truncate each release notes to this length to keep prompts bounded (500–20000).",
821
+ "releases.manage": "Manage subscriptions →",
822
+ "subs.title": "Repo release subscriptions",
823
+ "subs.desc": "Once subscribed, each run writes every repo's new releases (falling back to tags) as a Chinese changelog rendered under <output.dir>/releases/, in the same branch/PR as the board. With no subscriptions the stage produces nothing and leaves board artifacts untouched.",
824
+ "subs.add": "Add repo",
825
+ "subs.add.hint": "Add subscriptions (owner/repo, one or more, comma/space separated)",
826
+ "subs.add.placeholder": "denoland/deno",
827
+ "subs.add.confirm": "Add",
828
+ "subs.adding": "Adding…",
829
+ "subs.cancel": "Cancel",
830
+ "subs.add.note": "Validates owner/repo form and dedupes before writing; only writes .git/pending, landed with artifacts on the next run.",
831
+ "subs.add.queued": "Queued; committed with artifacts on the next run.",
832
+ "subs.remove.queued": "Marked for removal; committed with artifacts on the next run.",
833
+ "subs.remove": "Remove",
834
+ "subs.removing": "Removing…",
835
+ "subs.empty": "No repos subscribed yet",
836
+ "subs.loading": "Loading subscriptions…",
837
+ "subs.input.invalid": "Enter owner/repo form, e.g. vercel/next.js",
838
+ "subs.col.repo": "Repo (owner/repo)",
839
+ "subs.param.hint": "Stage behaviour parameters (per-repo limit, tag fallback, period, etc.) are adjusted in the settings page Releases section.",
840
+ "subs.param.link": "Go to Settings → Releases",
841
+ "overview.tokens.title": "Token usage",
842
+ "overview.tokens.thisRun": "This run",
843
+ "overview.tokens.total": "All-time",
844
+ "overview.releases.title": "Releases",
845
+ "overview.releases.new": "New",
846
+ "overview.releases.repos": "Repos",
847
+ "overview.tokens.note": 'The overview surfaces "how many tokens this run spent / all-time accumulated / how many new versions across subscribed repos"; total only, not split by dimension; this run total already includes the subscriptions stage. With an old core that has no data, the corresponding tile is hidden entirely, never shown as 0.',
692
848
  "about.title": "About",
693
849
  "about.copy": "Copy full package name",
694
850
  "about.copied": "Copied",
@@ -746,7 +902,13 @@ var CSS = `
746
902
  .dshgt-hint{color:var(--gt-muted);font-size:12px;}
747
903
  .dshgt-desc{margin:0;color:var(--gt-muted);font-size:12px;}
748
904
  .dshgt-row{display:flex;gap:10px;align-items:center;min-height:28px;flex-wrap:wrap;}
749
- .dshgt-row>label.dshgt-label{width:96px;color:var(--gt-muted);flex:none;font-size:12px;}
905
+ .dshgt-row>label.dshgt-label{width:96px;color:var(--gt-muted);flex:none;font-size:12px;display:flex;align-items:flex-start;gap:5px;}
906
+ .dshgt-label-text{flex:1;min-width:0;}
907
+ /* 字段说明:圆问号 + 纯 CSS 悬浮气泡(::after 读 data-tip),键盘聚焦同样展开。 */
908
+ .dshgt-help{position:relative;flex:none;margin-top:2px;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;border:1px solid var(--gt-border);border-radius:50%;background:var(--gt-bg);color:var(--gt-muted);font-size:10px;line-height:1;font-weight:600;cursor:help;user-select:none;outline:none;}
909
+ .dshgt-help:hover,.dshgt-help:focus-visible{color:var(--gt-fg);border-color:color-mix(in srgb,var(--gt-accent) 55%,var(--gt-border));}
910
+ .dshgt-help::after{content:attr(data-tip);position:absolute;left:0;top:calc(100% + 7px);z-index:30;width:max-content;max-width:280px;padding:7px 9px;border-radius:8px;background:var(--dsw-alias-bg-layer-2,var(--gt-card));color:var(--gt-fg);border:1px solid var(--gt-border);font-size:12px;font-weight:400;line-height:1.55;letter-spacing:0;text-align:left;white-space:normal;overflow-wrap:break-word;box-shadow:0 10px 26px rgba(0,0,0,.28);opacity:0;visibility:hidden;pointer-events:none;transition:opacity .12s ease,visibility .12s;}
911
+ .dshgt-help:hover::after,.dshgt-help:focus-visible::after{opacity:1;visibility:visible;}
750
912
  .dshgt-inline{display:flex;gap:8px;align-items:center;flex-wrap:wrap;}
751
913
  .dshgt-input,.dshgt-select,.dshgt-textarea{font:inherit;color:inherit;background:var(--gt-bg);border:1px solid var(--gt-border);border-radius:7px;padding:5px 8px;min-width:0;}
752
914
  .dshgt-input:focus,.dshgt-select:focus,.dshgt-textarea:focus{outline:2px solid color-mix(in srgb,var(--gt-accent) 35%,transparent);outline-offset:1px;}
@@ -1088,11 +1250,29 @@ function Card(props) {
1088
1250
  children
1089
1251
  );
1090
1252
  }
1253
+ function Help(props) {
1254
+ return h(
1255
+ "span",
1256
+ {
1257
+ className: "dshgt-help",
1258
+ tabIndex: 0,
1259
+ role: "note",
1260
+ "aria-label": props.text,
1261
+ "data-tip": props.text
1262
+ },
1263
+ "?"
1264
+ );
1265
+ }
1091
1266
  function Row(props) {
1092
1267
  return h(
1093
1268
  "div",
1094
1269
  { className: "dshgt-row" },
1095
- props.label === void 0 ? null : h("label", { className: "dshgt-label" }, props.label),
1270
+ props.label === void 0 ? null : h(
1271
+ "label",
1272
+ { className: "dshgt-label" },
1273
+ h("span", { className: "dshgt-label-text" }, props.label),
1274
+ props.help === void 0 ? null : h(Help, { text: props.help })
1275
+ ),
1096
1276
  props.children
1097
1277
  );
1098
1278
  }
@@ -1202,6 +1382,12 @@ function duration(ms) {
1202
1382
  function shortHash(hash) {
1203
1383
  return typeof hash === "string" && hash !== "" ? hash.slice(0, 10) : "—";
1204
1384
  }
1385
+ function formatTokens(value) {
1386
+ if (value === void 0 || !Number.isFinite(value)) return "—";
1387
+ if (value < 1e4) return value.toLocaleString("en-US");
1388
+ if (value < 1e6) return `${(value / 1e3).toFixed(1)}k`.replace(".0k", "k");
1389
+ return `${(value / 1e6).toFixed(1)}M`.replace(".0M", "M");
1390
+ }
1205
1391
  function localTime(iso) {
1206
1392
  if (iso === null || iso === void 0) return "—";
1207
1393
  const date = new Date(iso);
@@ -1279,6 +1465,14 @@ async function applyPluginUpdate() {
1279
1465
  const payload = await apiCall("POST", "/update", {});
1280
1466
  return payload.result;
1281
1467
  }
1468
+ async function getSubscriptions(signal) {
1469
+ const payload = await apiCall("GET", "/subscriptions", void 0, signal === void 0 ? {} : { signal });
1470
+ return Array.isArray(payload.repos) ? payload.repos : [];
1471
+ }
1472
+ async function postSubscriptions(action, repos) {
1473
+ const payload = await apiCall("POST", "/subscriptions", { action, repos });
1474
+ return Array.isArray(payload.repos) ? payload.repos : [];
1475
+ }
1282
1476
 
1283
1477
  // client/store.ts
1284
1478
  var POLL_MS = 5e3;
@@ -1338,8 +1532,8 @@ function credentialView(model, t) {
1338
1532
 
1339
1533
  // client/settings.ts
1340
1534
  var LANGUAGES = ["all", "typescript", "python", "go", "rust", "java", "c", "cpp", "javascript", "csharp"];
1341
- var SECTIONS = ["source", "model", "repo", "auth", "scope", "schedule"];
1342
- var EMPTY_DIRTY = { source: false, model: false, repo: false, auth: false, scope: false, schedule: false };
1535
+ var SECTIONS = ["source", "model", "repo", "auth", "scope", "summarize", "releases", "schedule"];
1536
+ var EMPTY_DIRTY = { source: false, model: false, repo: false, auth: false, scope: false, summarize: false, releases: false, schedule: false };
1343
1537
  function cloneSettings(settings) {
1344
1538
  return JSON.parse(JSON.stringify(settings));
1345
1539
  }
@@ -1355,10 +1549,15 @@ function sectionSlice(settings, section) {
1355
1549
  return { github: settings.github };
1356
1550
  case "scope":
1357
1551
  return { trending: settings.trending };
1552
+ case "summarize":
1553
+ return { summarize: settings.summarize };
1554
+ case "releases":
1555
+ return { releases: settings.releases };
1358
1556
  case "schedule":
1359
1557
  return { schedule: settings.schedule };
1360
1558
  }
1361
1559
  }
1560
+ var NOTICE_TTL_MS = 5e3;
1362
1561
  function sectionEquals(a, b, section) {
1363
1562
  return JSON.stringify(sectionSlice(a, section)) === JSON.stringify(sectionSlice(b, section));
1364
1563
  }
@@ -1375,13 +1574,13 @@ function SettingsView(props) {
1375
1574
  const [savingSection, setSavingSection] = import_react.default.useState(null);
1376
1575
  const [savedSection, setSavedSection] = import_react.default.useState(null);
1377
1576
  const [problems, setProblems] = import_react.default.useState([]);
1378
- const [message, setMessage] = import_react.default.useState(null);
1577
+ const [notice, setNotice] = import_react.default.useState(null);
1379
1578
  const [preflight, setPreflight] = import_react.default.useState(null);
1380
1579
  const [preflighting, setPreflighting] = import_react.default.useState(false);
1381
1580
  const [rechecking, setRechecking] = import_react.default.useState(false);
1382
1581
  const [downloading, setDownloading] = import_react.default.useState(false);
1383
1582
  const [quickOpen, setQuickOpen] = import_react.default.useState(false);
1384
- const [section, setSection] = import_react.default.useState("source");
1583
+ const [section, setSection] = import_react.default.useState(props.initialSection ?? "source");
1385
1584
  const [branchInfo, setBranchInfo] = import_react.default.useState(null);
1386
1585
  const [branchLoading, setBranchLoading] = import_react.default.useState(false);
1387
1586
  const [testing, setTesting] = import_react.default.useState(false);
@@ -1394,6 +1593,14 @@ function SettingsView(props) {
1394
1593
  import_react.default.useEffect(() => {
1395
1594
  setProblems([]);
1396
1595
  }, [section]);
1596
+ import_react.default.useEffect(() => {
1597
+ if (notice === null || notice.kind !== "ok") return void 0;
1598
+ const timer = setTimeout(() => {
1599
+ setNotice((current) => current === notice ? null : current);
1600
+ }, NOTICE_TTL_MS);
1601
+ return () => clearTimeout(timer);
1602
+ }, [notice]);
1603
+ const notify = (kind, text) => setNotice({ kind, text });
1397
1604
  const repoPath = draft === null ? "" : draft.repo.path;
1398
1605
  import_react.default.useEffect(() => {
1399
1606
  if (repoPath.trim() === "") {
@@ -1444,13 +1651,13 @@ function SettingsView(props) {
1444
1651
  setDraft((current) => current === null ? current : applySection(current, result.settings, section2));
1445
1652
  setDirtySections((current) => ({ ...current, [section2]: false }));
1446
1653
  setSavedSection(section2);
1447
- setMessage(null);
1654
+ setNotice(null);
1448
1655
  await store.refresh({ quiet: true });
1449
1656
  } else {
1450
1657
  setProblems(result.problems ?? []);
1451
1658
  }
1452
1659
  } catch (error) {
1453
- setMessage(error instanceof Error ? error.message : String(error));
1660
+ notify("err", error instanceof Error ? error.message : String(error));
1454
1661
  } finally {
1455
1662
  setSavingSection(null);
1456
1663
  }
@@ -1481,13 +1688,13 @@ function SettingsView(props) {
1481
1688
  const downloadCore = async () => {
1482
1689
  setDownloading(true);
1483
1690
  setProblems([]);
1484
- setMessage(null);
1691
+ setNotice(null);
1485
1692
  try {
1486
1693
  await apiCall("POST", "/core/download", {});
1487
- setMessage(t("core.downloaded"));
1694
+ notify("ok", t("core.downloaded"));
1488
1695
  await store.refresh({ quiet: true });
1489
1696
  } catch (error) {
1490
- setMessage(error instanceof Error ? error.message : String(error));
1697
+ notify("err", error instanceof Error ? error.message : String(error));
1491
1698
  } finally {
1492
1699
  setDownloading(false);
1493
1700
  }
@@ -1517,13 +1724,13 @@ function SettingsView(props) {
1517
1724
  setDraft(payload.settings);
1518
1725
  setDirtySections({ ...EMPTY_DIRTY });
1519
1726
  setSavedSection(null);
1520
- setMessage(t("settings.import.done", { fields: payload.imported.join("、") }));
1727
+ notify("ok", t("settings.import.done", { fields: payload.imported.join("、") }));
1521
1728
  await store.refresh({ quiet: true });
1522
1729
  } else {
1523
- setMessage(payload.problems[0] ?? t("settings.import.empty"));
1730
+ notify("err", payload.problems[0] ?? t("settings.import.empty"));
1524
1731
  }
1525
1732
  } catch (error) {
1526
- setMessage(error instanceof Error ? error.message : String(error));
1733
+ notify("err", error instanceof Error ? error.message : String(error));
1527
1734
  }
1528
1735
  };
1529
1736
  const copyConfig = () => {
@@ -1533,7 +1740,7 @@ function SettingsView(props) {
1533
1740
  null,
1534
1741
  2
1535
1742
  );
1536
- void navigator.clipboard?.writeText(redacted).then(() => setMessage(t("settings.copied")));
1743
+ void navigator.clipboard?.writeText(redacted).then(() => notify("ok", t("settings.copied")));
1537
1744
  };
1538
1745
  if (state === null || draft === null) {
1539
1746
  return h("div", { className: "dshgt-root" }, h("p", { className: "dshgt-hint" }, store.error ?? t("quickfill.loading")));
@@ -1549,6 +1756,8 @@ function SettingsView(props) {
1549
1756
  { id: "repo", label: t("st.repo") },
1550
1757
  { id: "auth", label: t("st.auth") },
1551
1758
  { id: "scope", label: t("st.scope") },
1759
+ { id: "summarize", label: t("st.summarize") },
1760
+ { id: "releases", label: t("st.releases") },
1552
1761
  { id: "schedule", label: t("st.schedule") }
1553
1762
  ];
1554
1763
  const sourceCard = h(
@@ -1809,7 +2018,77 @@ function SettingsView(props) {
1809
2018
  ),
1810
2019
  h("p", { className: "dshgt-hint" }, t("schedule.source"))
1811
2020
  );
1812
- const currentCard = section === "source" ? sourceCard : section === "model" ? modelCard : section === "repo" ? repoCard : section === "auth" ? authCard : section === "scope" ? scopeCard : scheduleCard;
2021
+ const summarizeCard = h(
2022
+ Card,
2023
+ { title: t("card.summarize.title"), hint: t("card.summarize.hint") },
2024
+ h(
2025
+ Row,
2026
+ { label: t("summarize.batchSize"), help: t("summarize.batchSize.help") },
2027
+ h(Input, { mono: true, width: 90, value: String(draft.summarize.batchSize), onInput: (value) => update((next) => {
2028
+ next.summarize.batchSize = Math.round(Number(value) || 0);
2029
+ }) })
2030
+ ),
2031
+ h(
2032
+ Row,
2033
+ { label: t("summarize.maxRetries"), help: t("summarize.maxRetries.help") },
2034
+ h(Input, { mono: true, width: 90, value: String(draft.summarize.maxRetries), onInput: (value) => update((next) => {
2035
+ next.summarize.maxRetries = Math.round(Number(value) || 0);
2036
+ }) })
2037
+ ),
2038
+ h(
2039
+ Row,
2040
+ { label: t("summarize.enrich"), help: t("summarize.enrich.help") },
2041
+ h(Select, {
2042
+ value: draft.summarize.enrich,
2043
+ options: [
2044
+ { value: "none", label: t("summarize.enrich.none") },
2045
+ { value: "repo", label: t("summarize.enrich.repo") },
2046
+ { value: "readme", label: t("summarize.enrich.readme") }
2047
+ ],
2048
+ onChange: (value) => update((next) => {
2049
+ next.summarize.enrich = value === "repo" ? "repo" : value === "readme" ? "readme" : "none";
2050
+ })
2051
+ })
2052
+ ),
2053
+ h(
2054
+ Row,
2055
+ { label: t("summarize.readmeChars"), help: t("summarize.readmeChars.help") },
2056
+ h(Input, { mono: true, width: 90, value: String(draft.summarize.readmeChars), onInput: (value) => update((next) => {
2057
+ next.summarize.readmeChars = Math.round(Number(value) || 0);
2058
+ }) })
2059
+ ),
2060
+ draft.summarize.enrich === "none" ? null : h(Alert, { kind: "warn" }, h("div", null, t("summarize.enrichNote")))
2061
+ );
2062
+ const releasesCard = h(
2063
+ Card,
2064
+ {
2065
+ title: t("card.releases.title"),
2066
+ hint: t("card.releases.hint"),
2067
+ extra: props.onOpenSubscriptions === void 0 ? void 0 : h(Btn, { small: true, onClick: () => props.onOpenSubscriptions?.() }, t("releases.manage"))
2068
+ },
2069
+ h(
2070
+ Row,
2071
+ { label: t("releases.includePrerelease"), help: t("releases.includePrerelease.help") },
2072
+ h(Toggle, { checked: draft.releases.includePrerelease, onChange: (value) => update((next) => {
2073
+ next.releases.includePrerelease = value;
2074
+ }) })
2075
+ ),
2076
+ h(
2077
+ Row,
2078
+ { label: t("releases.includeDrafts"), help: t("releases.includeDrafts.help") },
2079
+ h(Toggle, { checked: draft.releases.includeDrafts, onChange: (value) => update((next) => {
2080
+ next.releases.includeDrafts = value;
2081
+ }) })
2082
+ ),
2083
+ h(
2084
+ Row,
2085
+ { label: t("releases.notesChars"), help: t("releases.notesChars.help") },
2086
+ h(Input, { mono: true, width: 90, value: String(draft.releases.maxNotesChars), onInput: (value) => update((next) => {
2087
+ next.releases.maxNotesChars = Math.round(Number(value) || 0);
2088
+ }) })
2089
+ )
2090
+ );
2091
+ const currentCard = section === "source" ? sourceCard : section === "model" ? modelCard : section === "repo" ? repoCard : section === "auth" ? authCard : section === "scope" ? scopeCard : section === "summarize" ? summarizeCard : section === "releases" ? releasesCard : scheduleCard;
1813
2092
  return h(
1814
2093
  "div",
1815
2094
  { className: "dshgt-settings" },
@@ -1837,7 +2116,7 @@ function SettingsView(props) {
1837
2116
  { className: "dshgt-vpanel" },
1838
2117
  currentCard,
1839
2118
  problems.length > 0 ? h(Alert, { kind: "err", title: t("error.prefix") }, h("ul", { style: { margin: 0, paddingLeft: 16 } }, problems.map((problem, index) => h("li", { key: index }, `${problem.field}:${problem.message}`)))) : null,
1840
- message === null ? null : h(Alert, { kind: "ok" }, message),
2119
+ notice === null ? null : h(Alert, { kind: notice.kind }, notice.text),
1841
2120
  h(
1842
2121
  "div",
1843
2122
  { className: "dshgt-footer" },
@@ -2442,6 +2721,37 @@ function OverviewView(props) {
2442
2721
  h("div", { className: "dshgt-fact" }, h("div", { className: "dshgt-fact-k" }, t("overview.model")), h("div", { className: "dshgt-fact-v" }, last.model?.model ?? "—"))
2443
2722
  )
2444
2723
  ),
2724
+ h(
2725
+ "section",
2726
+ { className: "dshgt-block dshgt-usage-glance" },
2727
+ h(
2728
+ "div",
2729
+ { className: "dshgt-glance-grid" },
2730
+ h(
2731
+ "div",
2732
+ { className: "dshgt-glance" },
2733
+ h("div", { className: "dshgt-glance-k" }, t("overview.tokens.title")),
2734
+ h(
2735
+ "div",
2736
+ { className: "dshgt-glance-vs" },
2737
+ h("div", { className: "dshgt-glance-cell" }, h("span", { className: "dshgt-hint" }, t("overview.tokens.thisRun")), h("span", { className: "dshgt-glance-num dshgt-mono" }, last?.tokens === void 0 ? "—" : formatTokens(last.tokens)), h("span", { className: "dshgt-hint" }, "tokens")),
2738
+ h("div", { className: "dshgt-glance-cell" }, h("span", { className: "dshgt-hint" }, t("overview.tokens.total")), h("span", { className: "dshgt-glance-num dshgt-mono" }, state.status.totalTokens === void 0 ? "—" : formatTokens(state.status.totalTokens)), h("span", { className: "dshgt-hint" }, "tokens"))
2739
+ )
2740
+ ),
2741
+ h(
2742
+ "div",
2743
+ { className: "dshgt-glance" },
2744
+ h("div", { className: "dshgt-glance-k" }, t("overview.releases.title")),
2745
+ h(
2746
+ "div",
2747
+ { className: "dshgt-glance-vs" },
2748
+ h("div", { className: "dshgt-glance-cell" }, h("span", { className: "dshgt-hint" }, t("overview.releases.new")), h("span", { className: "dshgt-glance-num dshgt-mono" }, last?.releases === void 0 ? "—" : String(last.releases.newReleases))),
2749
+ h("div", { className: "dshgt-glance-cell" }, h("span", { className: "dshgt-hint" }, t("overview.releases.repos")), h("span", { className: "dshgt-glance-num dshgt-mono" }, last?.releases === void 0 ? "—" : String(last.releases.repos)))
2750
+ )
2751
+ )
2752
+ ),
2753
+ h("p", { className: "dshgt-hint dshgt-glance-note" }, t("overview.tokens.note"))
2754
+ ),
2445
2755
  h(
2446
2756
  "section",
2447
2757
  { className: "dshgt-block" },
@@ -2703,6 +3013,7 @@ function RunsView(props) {
2703
3013
  record.legacy === true ? h("span", { className: "dshgt-tag dshgt-inline-tag" }, t("runs.legacy")) : null
2704
3014
  ),
2705
3015
  h("span", { className: "dshgt-run-files" }, t("runs.files", { count: record.files })),
3016
+ record.tokens === void 0 ? null : h("span", { className: "dshgt-run-tokens dshgt-mono", title: t("run.usage.tokens") }, `${formatTokens(record.tokens)} tok`),
2706
3017
  h("span", { className: "dshgt-run-duration" }, duration(record.durationMs)),
2707
3018
  h("span", { className: "dshgt-run-chev" }, "›")
2708
3019
  ),
@@ -2735,8 +3046,10 @@ function RunsView(props) {
2735
3046
  )
2736
3047
  ),
2737
3048
  detailItem(t, "runs.detail.warnings", record.warnings.length === 0 ? t("runs.detail.none") : record.warnings.join(";")),
2738
- detailItem(t, "runs.detail.files", `${record.files} · ${duration(record.durationMs)}`)
3049
+ detailItem(t, "runs.detail.files", `${record.files} · ${duration(record.durationMs)}`),
3050
+ detailItem(t, "runs.detail.tokens", record.tokens === void 0 ? t("runs.detail.tokensNone") : h("span", { className: "dshgt-mono" }, `${formatTokens(record.tokens)} tokens`))
2739
3051
  ),
3052
+ record.releases === void 0 ? null : ReleasesDetail({ t, record }),
2740
3053
  record.legacy === true ? h("div", { className: "dshgt-hint" }, t("runs.legacyHint")) : null,
2741
3054
  h(
2742
3055
  "div",
@@ -2759,9 +3072,34 @@ function RunsView(props) {
2759
3072
  );
2760
3073
  })
2761
3074
  ),
3075
+ state.status.totalTokens === void 0 ? null : h("div", { className: "dshgt-runs-foot dshgt-hint" }, t("runs.footer.total", { total: formatTokens(state.status.totalTokens) })),
2762
3076
  logId === null ? null : h(LogDrawer, { t, runId: logId, onClose: () => setLogId(null) })
2763
3077
  );
2764
3078
  }
3079
+ function ReleasesDetail(props) {
3080
+ const { t, record } = props;
3081
+ const r = record.releases;
3082
+ if (r === void 0 || !r.enabled) {
3083
+ return h("div", { className: "dshgt-releases-detail" }, h("span", { className: "dshgt-hint" }, t("releases.empty")));
3084
+ }
3085
+ const stats = [
3086
+ [t("releases.stat.repos"), r.repos],
3087
+ [t("releases.stat.new"), r.newReleases],
3088
+ [t("releases.stat.summarized"), r.summarized],
3089
+ [t("releases.stat.degraded"), r.degraded],
3090
+ [t("releases.stat.failed"), r.failed],
3091
+ [t("releases.stat.skipped"), r.skipped]
3092
+ ];
3093
+ return h(
3094
+ "div",
3095
+ { className: "dshgt-releases-detail" },
3096
+ h(
3097
+ "div",
3098
+ { className: "dshgt-releases-stats" },
3099
+ stats.filter(([, value]) => value !== void 0).map(([label, value]) => h("span", { className: "dshgt-releases-stat", key: label }, h("span", { className: "dshgt-hint" }, label), h("span", { className: "dshgt-mono" }, String(value))))
3100
+ )
3101
+ );
3102
+ }
2765
3103
  function detailItem(t, label, value) {
2766
3104
  return h(
2767
3105
  "div",
@@ -3442,6 +3780,131 @@ function metaLine(rendered) {
3442
3780
  );
3443
3781
  }
3444
3782
 
3783
+ // client/subscriptions.ts
3784
+ var REPO_PATTERN = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
3785
+ function SubscriptionsView(props) {
3786
+ const { t } = props;
3787
+ const [repos, setRepos] = import_react.default.useState(null);
3788
+ const [error, setError] = import_react.default.useState(null);
3789
+ const [busy, setBusy] = import_react.default.useState(null);
3790
+ const [adding, setAdding] = import_react.default.useState(false);
3791
+ const [input, setInput] = import_react.default.useState("");
3792
+ const [note, setNote] = import_react.default.useState(null);
3793
+ const load = import_react.default.useCallback(() => {
3794
+ setError(null);
3795
+ getSubscriptions().then((list2) => setRepos(list2)).catch((failure) => {
3796
+ setError(failure instanceof Error ? failure.message : String(failure));
3797
+ setRepos([]);
3798
+ });
3799
+ }, []);
3800
+ import_react.default.useEffect(() => {
3801
+ load();
3802
+ }, [load]);
3803
+ const doAdd = () => {
3804
+ const candidates = input.split(/[\s,]+/).map((item) => item.trim()).filter((item) => item !== "");
3805
+ const valid = candidates.filter((item) => REPO_PATTERN.test(item));
3806
+ if (valid.length === 0) {
3807
+ setError(t("subs.input.invalid"));
3808
+ return;
3809
+ }
3810
+ setBusy("add");
3811
+ setError(null);
3812
+ setNote(null);
3813
+ postSubscriptions("add", valid).then((list2) => {
3814
+ setRepos(list2);
3815
+ setInput("");
3816
+ setAdding(false);
3817
+ setNote(t("subs.add.queued"));
3818
+ }).catch((failure) => setError(failure instanceof Error ? failure.message : String(failure))).finally(() => setBusy(null));
3819
+ };
3820
+ const doRemove = (repo) => {
3821
+ setBusy(`remove:${repo}`);
3822
+ setError(null);
3823
+ setNote(null);
3824
+ postSubscriptions("remove", [repo]).then((list2) => {
3825
+ setRepos(list2);
3826
+ setNote(t("subs.remove.queued"));
3827
+ }).catch((failure) => setError(failure instanceof Error ? failure.message : String(failure))).finally(() => setBusy(null));
3828
+ };
3829
+ const list = repos ?? [];
3830
+ return h(
3831
+ "div",
3832
+ { className: "dshgt-view dshgt-subs" },
3833
+ h(
3834
+ "section",
3835
+ { className: "dshgt-block dshgt-subs-card" },
3836
+ h(
3837
+ "div",
3838
+ { className: "dshgt-subs-head" },
3839
+ h("h2", null, t("subs.title")),
3840
+ h(
3841
+ "div",
3842
+ { className: "dshgt-subs-head-actions" },
3843
+ h(Btn, { small: true, variant: "primary", onClick: () => setAdding(true), disabled: busy !== null || adding }, t("subs.add"))
3844
+ )
3845
+ ),
3846
+ h("p", { className: "dshgt-desc" }, t("subs.desc")),
3847
+ error === null ? null : h(Alert, { kind: "err" }, error),
3848
+ note === null ? null : h(Alert, { kind: "ok" }, note),
3849
+ repos === null ? h("p", { className: "dshgt-hint" }, t("subs.loading")) : list.length === 0 ? h("div", { className: "dshgt-empty" }, t("subs.empty")) : h(
3850
+ "div",
3851
+ { className: "dshgt-subs-list" },
3852
+ h(
3853
+ "div",
3854
+ { className: "dshgt-subs-row dshgt-subs-row-head" },
3855
+ h("span", null, t("subs.col.repo")),
3856
+ h("span", null, "")
3857
+ ),
3858
+ list.map(
3859
+ (repo) => h(
3860
+ "div",
3861
+ { className: "dshgt-subs-row", key: repo },
3862
+ h("span", { className: "dshgt-mono dshgt-subs-repo" }, repo),
3863
+ h(
3864
+ "span",
3865
+ { className: "dshgt-subs-actions" },
3866
+ h(
3867
+ Btn,
3868
+ { small: true, variant: "danger", onClick: () => doRemove(repo), disabled: busy !== null },
3869
+ busy === `remove:${repo}` ? t("subs.removing") : t("subs.remove")
3870
+ )
3871
+ )
3872
+ )
3873
+ )
3874
+ ),
3875
+ adding ? h(
3876
+ "div",
3877
+ { className: "dshgt-subs-add" },
3878
+ h("div", { className: "dshgt-hint" }, t("subs.add.hint")),
3879
+ h(
3880
+ "div",
3881
+ { className: "dshgt-subs-add-row" },
3882
+ h(Input, {
3883
+ mono: true,
3884
+ width: 320,
3885
+ value: input,
3886
+ placeholder: t("subs.add.placeholder"),
3887
+ onInput: (value) => setInput(value)
3888
+ }),
3889
+ h(Btn, { small: true, variant: "primary", onClick: doAdd, disabled: busy !== null }, busy === "add" ? t("subs.adding") : t("subs.add.confirm")),
3890
+ h(Btn, { small: true, onClick: () => {
3891
+ setAdding(false);
3892
+ setInput("");
3893
+ setError(null);
3894
+ } }, t("subs.cancel"))
3895
+ ),
3896
+ h("p", { className: "dshgt-hint" }, t("subs.add.note"))
3897
+ ) : null
3898
+ ),
3899
+ h(
3900
+ "section",
3901
+ { className: "dshgt-block" },
3902
+ h("p", { className: "dshgt-hint" }, t("subs.param.hint")),
3903
+ h(Btn, { small: true, onClick: props.onOpenSettings }, t("subs.param.link"))
3904
+ )
3905
+ );
3906
+ }
3907
+
3445
3908
  // node_modules/@morehao/dsh-plugin-kit/dist/update-client.js
3446
3909
  var IDLE = { status: "idle", view: null, error: null };
3447
3910
  function errorOf(error) {
@@ -3832,6 +4295,7 @@ var NAV = [
3832
4295
  { id: "overview", label: "nav.overview", icon: "grid" },
3833
4296
  { id: "digest", label: "nav.digest", icon: "doc" },
3834
4297
  { id: "runs", label: "nav.runs", icon: "list" },
4298
+ { id: "subscriptions", label: "nav.subscriptions", icon: "rss" },
3835
4299
  { id: "settings", label: "nav.settings", icon: "sliders" }
3836
4300
  ];
3837
4301
  function navIcon(name, size = 16) {
@@ -3873,6 +4337,15 @@ function navIcon(name, size = 16) {
3873
4337
  h("path", { d: "M4 6h.01M4 12h.01M4 18h.01" })
3874
4338
  );
3875
4339
  }
4340
+ if (name === "rss") {
4341
+ return h(
4342
+ "svg",
4343
+ common,
4344
+ h("path", { d: "M5 5a14 14 0 0 1 14 14" }),
4345
+ h("path", { d: "M5 11a8 8 0 0 1 8 8" }),
4346
+ h("circle", { cx: 6, cy: 18, r: 1.6 })
4347
+ );
4348
+ }
3876
4349
  return h(
3877
4350
  "svg",
3878
4351
  common,
@@ -3932,6 +4405,7 @@ function apply(ctx) {
3932
4405
  }, []);
3933
4406
  const openRuns = import_react.default.useCallback(() => setView("runs"), []);
3934
4407
  const openSettings = import_react.default.useCallback(() => setView("settings"), []);
4408
+ const openSubscriptions = import_react.default.useCallback(() => setView("subscriptions"), []);
3935
4409
  const openAbout = import_react.default.useCallback(() => setAboutOpen(true), []);
3936
4410
  const state = value.state;
3937
4411
  const plugin = state?.plugin;
@@ -3962,7 +4436,7 @@ function apply(ctx) {
3962
4436
  ...digestTarget2 === void 0 ? {} : { selected: digestTarget2 },
3963
4437
  onSelect: setDigestTarget,
3964
4438
  onOpenRuns: openRuns
3965
- }) : view === "runs" ? h(RunsView, { t, store: value, controller, onOpenDigest: openDigest }) : h(SettingsView, { t, store: value, pickDirectory });
4439
+ }) : view === "runs" ? h(RunsView, { t, store: value, controller, onOpenDigest: openDigest }) : view === "subscriptions" ? h(SubscriptionsView, { t, onOpenSettings: openSettings }) : h(SettingsView, { t, store: value, pickDirectory, onOpenSubscriptions: openSubscriptions });
3966
4440
  return h(
3967
4441
  "div",
3968
4442
  { className: "dshgt-root dshgt-shell" },