@morehao/dsh-github-trending 0.2.0 → 0.3.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
@@ -76,7 +76,6 @@ var DICT = {
76
76
  "core.installed": "已安装",
77
77
  "core.missing": "未安装",
78
78
  "core.override": "覆盖路径:{path}",
79
- "core.path.advancedHint": "仅开发/排障使用;留空走自动 Release 缓存/下载。",
80
79
  "core.order": "解析顺序:TRENDING_DIGEST_CORE(仅开发/排障)→ Release 资产(缓存命中直接用,否则下载并校验);都不可用时报 CORE_UNAVAILABLE。",
81
80
  "coexist.title": "检测到本机另有 CLI",
82
81
  "coexist.hint": "插件仍使用自带核心,不切换、不接管它的定时。",
@@ -92,10 +91,10 @@ var DICT = {
92
91
  "model.mode": "密钥来源",
93
92
  "model.mode.direct": "DSH 凭据 / 手动密钥",
94
93
  "model.mode.proxy": "回环代理(凭据不可导出)",
95
- "model.keyRef": "凭据引用",
96
94
  "model.credential": "凭据状态",
97
- "model.credential.hint": "沿用 DSH 配置时自动带入,无需填写。",
98
- "model.keyRef.advancedHint": "手动指定时才会用到;一般不填。",
95
+ "model.credential.proxyHint": "回环代理用宿主自己的凭据转发,密钥不经过插件、也不落盘,本页无需填写。",
96
+ "model.credential.directHint": "密钥在运行时从宿主凭据 / 环境变量解析,直传给核心。",
97
+ "model.credential.missingHint": "当前是直传模式,但凭据引用为空或解析不到;点右上角「沿用 DSH 配置」重新填充。",
99
98
  "model.params": "参数",
100
99
  "model.preflight": "连通性预检",
101
100
  "model.preflighting": "预检中…",
@@ -133,6 +132,7 @@ var DICT = {
133
132
  "auth.status": "状态",
134
133
  "auth.configured": "已配置",
135
134
  "auth.missing": "未配置",
135
+ "auth.hostProxy": "沿用 DSH 凭据",
136
136
  "card.scope.title": "榜单范围",
137
137
  "scope.period": "周期",
138
138
  "scope.daily": "每日",
@@ -157,8 +157,6 @@ var DICT = {
157
157
  "settings.troubleshoot": "排障",
158
158
  "settings.copyConfig": "复制脱敏配置",
159
159
  "settings.copied": "已复制",
160
- "settings.advanced": "高级设置",
161
- "settings.advanced.hide": "收起高级",
162
160
  "settings.lastRun": "上次运行",
163
161
  "run.now": "本次",
164
162
  "run.history": "历史",
@@ -186,7 +184,7 @@ var DICT = {
186
184
  "run.usage.branch": "分支",
187
185
  "run.usage.pr": "PR",
188
186
  "run.pendingPr": "存在未合并 PR",
189
- "run.pendingPr.hint": "同日重跑会更新该 PR(核心以 --force-with-lease 推送);若已合并/关闭,记录会自动清理。",
187
+ "run.pendingPr.hint": "同日重跑会在分支上追加提交并更新该 PR;若已合并/关闭,记录会自动清理。",
190
188
  "run.error.title": "最近错误",
191
189
  "run.history.title": "历史记录",
192
190
  "run.history.hint": "最近 30 条,超出即裁剪",
@@ -216,7 +214,150 @@ var DICT = {
216
214
  "quickfill.close": "关闭",
217
215
  "quickfill.loading": "读取中…",
218
216
  "error.prefix": "错误",
219
- "label.optional": "可选"
217
+ "err.reason.stale-lease": "推送被拒(stale info):本地对远端分支的记录过期(分支被删或被改),并非有人抢先推送。在仓库里执行 `git fetch --prune origin` 后重试。",
218
+ "err.reason.non-fast-forward": "推送被拒(non-fast-forward):远端分支已前进。先 `git fetch` 并 rebase 到远端分支后重试。",
219
+ "err.reason.auth": "推送认证失败:更新 GitHub token 或重新 `gh auth login` 后重试;本地提交已保留。",
220
+ "err.reason.protected": "目标分支受保护:改为通过 PR 提交,或调整分支保护规则。",
221
+ "err.reason.hook": "仓库 hook 拒绝了操作:按 hook 输出修正后重试;本地提交已保留。",
222
+ "err.reason.pathspec": "暂存路径不存在:检查设置里的输出目录是否与仓库实际目录一致。",
223
+ "err.reason.unknown": "推送失败:本地提交已保留,查看运行日志后重试。",
224
+ "skip.merged": "该日期的 PR 已合并,本次跳过。",
225
+ "skip.pr-closed": "该日期的 PR 已被关闭(未合并);定时任务不重开,如需重开请手动运行。",
226
+ "skip.unchanged": "内容与分支上已有产物一致,未新增提交。",
227
+ "label.optional": "可选",
228
+ "nav.overview": "概览",
229
+ "nav.digest": "日报",
230
+ "nav.runs": "运行记录",
231
+ "nav.settings": "设置",
232
+ "shell.nav": "面板",
233
+ "shell.overview.dek": "每天定时生成日报;先看下次运行、就绪检查与上次交付。",
234
+ "shell.digest.dek": "按日期回看最近 30 天;只有保留过预览副本的期数能读。",
235
+ "shell.runs.dek": "最近 30 次运行;展开看交付物、错误与日志。",
236
+ "shell.settings.dek": "沿用现有设置页;每个分区独立保存。",
237
+ "shell.nextRun": "下次运行",
238
+ "shell.scheduleOff": "定时未开启",
239
+ "shell.ready": "就绪 {ready} / {total}",
240
+ "shell.github": "GitHub",
241
+ "shell.package.copy": "复制完整包名",
242
+ "time.in.minutes": "{minutes} 分钟后",
243
+ "time.in.hours": "{hours} 小时 {minutes} 分后",
244
+ "time.in.days": "{days} 天后",
245
+ "overview.last.none": "还没有运行记录",
246
+ "overview.last.files": "{count} 个文件",
247
+ "overview.last.digest": "{date} · GitHub 趋势日报",
248
+ "overview.last.read": "阅读本期",
249
+ "overview.last.noPreview": "这次运行没有保留预览副本",
250
+ "overview.last.prPending": "待合并",
251
+ "overview.last.prCreated": "PR 已创建",
252
+ "overview.last.prUnknown": "PR 状态未知",
253
+ "overview.last.direct": "直接提交",
254
+ "overview.model": "模型",
255
+ "overview.ready.title": "就绪检查",
256
+ "overview.ready.core": "核心",
257
+ "overview.ready.model": "模型",
258
+ "overview.ready.repo": "仓库",
259
+ "overview.ready.github": "GitHub",
260
+ "overview.ready.view": "查看",
261
+ "overview.week.title": "最近 7 天",
262
+ "overview.week.all": "全部运行记录",
263
+ "overview.week.none": "无记录",
264
+ "overview.week.wait": "等待 {time}",
265
+ "overview.live.title": "正在运行",
266
+ "overview.live.core": "核心执行中",
267
+ "overview.live.note": "核心一次完成抓取、总结、渲染与提交,结束前没有分段进度;这里只显示已用时间和粗阶段。",
268
+ "overview.live.started": "开始于",
269
+ "run.dryRunShort": "试跑",
270
+ "run.usage.commit": "提交",
271
+ "runs.count": "最近 {count} 次,超出即裁剪",
272
+ "runs.files": "{count} 个文件",
273
+ "runs.noChange": "内容与上一期一致,核心没有产生新文件",
274
+ "runs.dryHint": "试跑不写文件、不提交",
275
+ "runs.empty": "没有符合条件的记录",
276
+ "runs.filter.all": "全部",
277
+ "runs.filter.failed": "失败",
278
+ "runs.filter.dry": "试跑",
279
+ "runs.pendingPr": "待合并",
280
+ "runs.prUnknown": "状态未知",
281
+ "runs.prCreated": "PR 已创建",
282
+ "runs.legacy": "旧记录",
283
+ "runs.legacyHint": "旧记录没有预览副本,内容哈希使用旧算法,不能与 v2 直接比较。",
284
+ "runs.detail.coreModel": "核心 / 模型",
285
+ "runs.detail.date": "运行日期",
286
+ "runs.detail.warnings": "警告",
287
+ "runs.detail.none": "无",
288
+ "runs.detail.files": "文件 / 耗时",
289
+ "runs.detail.readDigest": "阅读本期日报",
290
+ "runs.detail.noPreview": "没有预览副本",
291
+ "runs.detail.log": "查看日志",
292
+ "digest.allRuns": "全部运行记录",
293
+ "digest.window": "最近 30 天 · {days} 天有记录 · {previews} 期有预览",
294
+ "digest.window.empty": "最近 30 天没有运行记录",
295
+ "digest.prev": "上一期(更早)",
296
+ "digest.next": "下一期(更新)",
297
+ "digest.select": "选择日报日期",
298
+ "digest.render": "渲染",
299
+ "digest.raw": "原文",
300
+ "digest.mode": "预览方式",
301
+ "digest.copyPath": "复制路径",
302
+ "digest.copied": "已复制",
303
+ "digest.openPr": "打开 PR",
304
+ "digest.truncated": "已截断到 512 KB",
305
+ "digest.loading": "读取预览…",
306
+ "digest.tag.preview": "有预览",
307
+ "digest.tag.failed": "运行失败",
308
+ "digest.tag.skipped": "无变更",
309
+ "digest.tag.dry": "试跑",
310
+ "digest.tag.legacy": "旧记录",
311
+ "digest.tag.missing": "无预览",
312
+ "digest.empty.none.title": "还没有可回看的日报",
313
+ "digest.empty.none.reason": "运行成功并保留预览副本后,这里会按日期列出最近 30 天。",
314
+ "digest.empty.failed.title": "这一期没有可预览的日报",
315
+ "digest.empty.failed.reason": "这次运行失败了,没有产出可读的日报文件。",
316
+ "digest.empty.skipped.title": "这一期没有新的日报",
317
+ "digest.empty.skipped.reason": "内容与上一期一致,核心没有产生新文件,也就没有新的预览副本。",
318
+ "digest.empty.dry.title": "试跑没有产物",
319
+ "digest.empty.dry.reason": "试跑只抓取与渲染,不写文件、不提交,所以没有可预览的产物。",
320
+ "digest.empty.legacy.title": "旧记录没有预览副本",
321
+ "digest.empty.legacy.reason": "预览副本从接入后的成功运行开始保留;更早的记录只能看运行结果。",
322
+ "digest.empty.missing.title": "预览副本没有保留",
323
+ "digest.empty.missing.reason": "这次运行成功,但当时没有保留预览副本,或副本已被清理。",
324
+ "digest.empty.viewRuns": "查看运行记录",
325
+ "digest.empty.latest": "阅读最近一期 · {date}",
326
+ "log.title": "运行日志",
327
+ "log.loading": "读取日志…",
328
+ "log.empty": "这次运行没有日志",
329
+ "log.truncated": "最多显示最后 64 KB",
330
+ "log.copy": "复制日志",
331
+ "log.copied": "已复制",
332
+ "log.close": "关闭",
333
+ "about.title": "关于",
334
+ "about.copy": "复制完整包名",
335
+ "about.copied": "已复制",
336
+ "about.close": "关闭",
337
+ "about.repo": "GitHub 仓库",
338
+ "about.repo.missing": "package.json 没有可解析的仓库地址",
339
+ "about.license": "许可",
340
+ "about.core": "核心",
341
+ "about.env": "运行环境",
342
+ "about.env.value": "DSH Web · Node 插件",
343
+ "about.update.title": "插件更新",
344
+ "about.update.check": "检查更新",
345
+ "about.update.checking": "正在检查最新版…",
346
+ "about.update.current": "已是最新(v{version})",
347
+ "about.update.available": "有新版本 v{latest}(当前 v{current})",
348
+ "about.update.apply": "更新到 v{version}",
349
+ "about.update.confirm": "确认更新到 v{version}?会重装本插件并重载面板,之后需要重启 DSH 生效。",
350
+ "about.update.confirmYes": "确认更新",
351
+ "about.update.cancel": "取消",
352
+ "about.update.applying": "正在更新到 v{version}…",
353
+ "about.update.done": "已更新到 v{version},重启 DSH 后生效。",
354
+ "about.update.applied": "已更新到 v{version}。",
355
+ "about.update.failed": "更新失败:{message}",
356
+ "about.update.checkFailed": "检查更新失败:拿不到 registry 上的最新版。可以稍后重试,或手动执行下面的命令。",
357
+ "about.update.manual": "当前是本地 {kind} 安装,不能一键更新(会把开发链接换成 npm 版)。手动更新:",
358
+ "about.update.copy": "复制命令",
359
+ "about.update.copied": "已复制",
360
+ "about.update.hint": "只检查 npm 上的 latest 稳定版;更新后需要重启 DSH。"
220
361
  },
221
362
  en: {
222
363
  "section.title": "GitHub Trending",
@@ -243,7 +384,6 @@ var DICT = {
243
384
  "core.installed": "installed",
244
385
  "core.missing": "missing",
245
386
  "core.override": "Override path: {path}",
246
- "core.path.advancedHint": "Dev/troubleshooting only; leave empty to use the Release cache/download.",
247
387
  "core.cached": "cached",
248
388
  "core.notDownloaded": "not downloaded",
249
389
  "core.download": "Download core",
@@ -265,10 +405,10 @@ var DICT = {
265
405
  "model.mode": "Key source",
266
406
  "model.mode.direct": "DSH credential / manual key",
267
407
  "model.mode.proxy": "Loopback proxy (credential not exportable)",
268
- "model.keyRef": "Credential ref",
269
408
  "model.credential": "Credential",
270
- "model.credential.hint": "Filled automatically when reusing DSH config; no need to enter it.",
271
- "model.keyRef.advancedHint": "Only used when setting a ref manually; usually left empty.",
409
+ "model.credential.proxyHint": "The loopback proxy forwards with the host credential; the key never passes through the plugin. Nothing to fill in here.",
410
+ "model.credential.directHint": "Resolved at run time from host credentials / env and passed straight to the core.",
411
+ "model.credential.missingHint": "Direct mode, but the ref is empty or unresolvable; use “Reuse DSH configuration” to refill it.",
272
412
  "model.params": "Parameters",
273
413
  "model.preflight": "Test connection",
274
414
  "model.preflighting": "Testing…",
@@ -306,6 +446,7 @@ var DICT = {
306
446
  "auth.status": "Status",
307
447
  "auth.configured": "configured",
308
448
  "auth.missing": "missing",
449
+ "auth.hostProxy": "uses DSH credential",
309
450
  "card.scope.title": "Trending scope",
310
451
  "scope.period": "Period",
311
452
  "scope.daily": "Daily",
@@ -330,8 +471,6 @@ var DICT = {
330
471
  "settings.troubleshoot": "Troubleshooting",
331
472
  "settings.copyConfig": "Copy redacted config",
332
473
  "settings.copied": "Copied",
333
- "settings.advanced": "Advanced settings",
334
- "settings.advanced.hide": "Hide advanced",
335
474
  "settings.lastRun": "Last run",
336
475
  "run.now": "This run",
337
476
  "run.history": "History",
@@ -359,7 +498,7 @@ var DICT = {
359
498
  "run.usage.branch": "Branch",
360
499
  "run.usage.pr": "PR",
361
500
  "run.pendingPr": "Unmerged PR exists",
362
- "run.pendingPr.hint": "Re-running the same day updates this PR (core pushes with --force-with-lease); merged/closed records are cleaned up automatically.",
501
+ "run.pendingPr.hint": "Re-running the same day appends a commit and updates this PR; merged/closed records are cleaned up automatically.",
363
502
  "run.error.title": "Last error",
364
503
  "run.history.title": "History",
365
504
  "run.history.hint": "last 30 records",
@@ -389,7 +528,150 @@ var DICT = {
389
528
  "quickfill.close": "Close",
390
529
  "quickfill.loading": "Loading…",
391
530
  "error.prefix": "Error",
392
- "label.optional": "optional"
531
+ "err.reason.stale-lease": "Push rejected (stale info): the local record of the remote branch is out of date (deleted or moved), not a race. Run `git fetch --prune origin` in the repo and retry.",
532
+ "err.reason.non-fast-forward": "Push rejected (non-fast-forward): the remote branch moved ahead. Fetch and rebase onto it, then retry.",
533
+ "err.reason.auth": "Push authentication failed: refresh the GitHub token or run `gh auth login`, then retry. Local commits are kept.",
534
+ "err.reason.protected": "The target branch is protected: open a PR instead, or adjust the branch protection rules.",
535
+ "err.reason.hook": "A repository hook rejected the operation: fix it per the hook output and retry. Local commits are kept.",
536
+ "err.reason.pathspec": "Nothing to stage at that path: check that the output directory matches the repository layout.",
537
+ "err.reason.unknown": "Push failed. Local commits are kept; check the run log and retry.",
538
+ "skip.merged": "The PR for this date was merged; skipped.",
539
+ "skip.pr-closed": "The PR for this date was closed unmerged. Scheduled runs do not reopen it; run manually to reopen.",
540
+ "skip.unchanged": "The branch already holds the same artifacts; no new commit.",
541
+ "label.optional": "optional",
542
+ "nav.overview": "Overview",
543
+ "nav.digest": "Digest",
544
+ "nav.runs": "Runs",
545
+ "nav.settings": "Settings",
546
+ "shell.nav": "Panel",
547
+ "shell.overview.dek": "A daily digest on a schedule; start with the next run, readiness and the last delivery.",
548
+ "shell.digest.dek": "Look back over the last 30 days; only runs with a saved preview copy can be read.",
549
+ "shell.runs.dek": "The last 30 runs; expand one for artifacts, errors and logs.",
550
+ "shell.settings.dek": "The existing settings page; each section saves on its own.",
551
+ "shell.nextRun": "Next run",
552
+ "shell.scheduleOff": "Schedule off",
553
+ "shell.ready": "Ready {ready} / {total}",
554
+ "shell.github": "GitHub",
555
+ "shell.package.copy": "Copy full package name",
556
+ "time.in.minutes": "in {minutes} min",
557
+ "time.in.hours": "in {hours} h {minutes} min",
558
+ "time.in.days": "in {days} d",
559
+ "overview.last.none": "No runs yet",
560
+ "overview.last.files": "{count} files",
561
+ "overview.last.digest": "{date} · GitHub Trending digest",
562
+ "overview.last.read": "Read this issue",
563
+ "overview.last.noPreview": "This run has no saved preview copy",
564
+ "overview.last.prPending": "Pending merge",
565
+ "overview.last.prCreated": "PR created",
566
+ "overview.last.prUnknown": "PR state unknown",
567
+ "overview.last.direct": "Direct commit",
568
+ "overview.model": "Model",
569
+ "overview.ready.title": "Readiness",
570
+ "overview.ready.core": "Core",
571
+ "overview.ready.model": "Model",
572
+ "overview.ready.repo": "Repository",
573
+ "overview.ready.github": "GitHub",
574
+ "overview.ready.view": "View",
575
+ "overview.week.title": "Last 7 days",
576
+ "overview.week.all": "All runs",
577
+ "overview.week.none": "No record",
578
+ "overview.week.wait": "Waiting for {time}",
579
+ "overview.live.title": "Running",
580
+ "overview.live.core": "Core is working",
581
+ "overview.live.note": "The core fetches, summarizes, renders and commits in one run; there is no per-step progress before it finishes. Only elapsed time and a coarse phase are shown here.",
582
+ "overview.live.started": "Started",
583
+ "run.dryRunShort": "Dry run",
584
+ "run.usage.commit": "Commit",
585
+ "runs.count": "Last {count} runs; older ones are trimmed",
586
+ "runs.files": "{count} files",
587
+ "runs.noChange": "Content matches the previous issue; the core produced no new file",
588
+ "runs.dryHint": "dry run writes nothing and commits nothing",
589
+ "runs.empty": "No matching records",
590
+ "runs.filter.all": "All",
591
+ "runs.filter.failed": "Failed",
592
+ "runs.filter.dry": "Dry run",
593
+ "runs.pendingPr": "Pending merge",
594
+ "runs.prUnknown": "State unknown",
595
+ "runs.prCreated": "PR created",
596
+ "runs.legacy": "Legacy",
597
+ "runs.legacyHint": "Legacy records have no preview copy and use the old hash algorithm, so they cannot be compared with v2 directly.",
598
+ "runs.detail.coreModel": "Core / model",
599
+ "runs.detail.date": "Run date",
600
+ "runs.detail.warnings": "Warnings",
601
+ "runs.detail.none": "None",
602
+ "runs.detail.files": "Files / duration",
603
+ "runs.detail.readDigest": "Read this digest",
604
+ "runs.detail.noPreview": "No preview copy",
605
+ "runs.detail.log": "View log",
606
+ "digest.allRuns": "All runs",
607
+ "digest.window": "Last 30 days · {days} days with runs · {previews} with previews",
608
+ "digest.window.empty": "No runs in the last 30 days",
609
+ "digest.prev": "Previous (older)",
610
+ "digest.next": "Next (newer)",
611
+ "digest.select": "Choose a digest date",
612
+ "digest.render": "Rendered",
613
+ "digest.raw": "Raw",
614
+ "digest.mode": "Preview mode",
615
+ "digest.copyPath": "Copy path",
616
+ "digest.copied": "Copied",
617
+ "digest.openPr": "Open PR",
618
+ "digest.truncated": "Truncated at 512 KB",
619
+ "digest.loading": "Loading preview…",
620
+ "digest.tag.preview": "Preview",
621
+ "digest.tag.failed": "Failed",
622
+ "digest.tag.skipped": "No change",
623
+ "digest.tag.dry": "Dry run",
624
+ "digest.tag.legacy": "Legacy",
625
+ "digest.tag.missing": "No preview",
626
+ "digest.empty.none.title": "No digest to look back on yet",
627
+ "digest.empty.none.reason": "Once a run succeeds and keeps a preview copy, the last 30 days will be listed here by date.",
628
+ "digest.empty.failed.title": "No digest preview for this issue",
629
+ "digest.empty.failed.reason": "This run failed, so there is no readable digest file.",
630
+ "digest.empty.skipped.title": "No new digest for this issue",
631
+ "digest.empty.skipped.reason": "Content matched the previous issue; the core produced no new file, so there is no new preview copy.",
632
+ "digest.empty.dry.title": "A dry run produces no artifact",
633
+ "digest.empty.dry.reason": "A dry run only fetches and renders; it writes no file and commits nothing, so there is nothing to preview.",
634
+ "digest.empty.legacy.title": "Legacy records have no preview copy",
635
+ "digest.empty.legacy.reason": "Preview copies are kept from successful runs after this feature shipped; older records only have run results.",
636
+ "digest.empty.missing.title": "The preview copy was not kept",
637
+ "digest.empty.missing.reason": "This run succeeded, but no preview copy was saved at the time, or it has been cleaned up.",
638
+ "digest.empty.viewRuns": "View runs",
639
+ "digest.empty.latest": "Read the latest issue · {date}",
640
+ "log.title": "Run log",
641
+ "log.loading": "Loading log…",
642
+ "log.empty": "This run has no log",
643
+ "log.truncated": "Shows at most the last 64 KB",
644
+ "log.copy": "Copy log",
645
+ "log.copied": "Copied",
646
+ "log.close": "Close",
647
+ "about.title": "About",
648
+ "about.copy": "Copy full package name",
649
+ "about.copied": "Copied",
650
+ "about.close": "Close",
651
+ "about.repo": "GitHub repository",
652
+ "about.repo.missing": "package.json has no resolvable repository URL",
653
+ "about.license": "License",
654
+ "about.core": "Core",
655
+ "about.env": "Runtime",
656
+ "about.env.value": "DSH Web · Node plugin",
657
+ "about.update.title": "Plugin update",
658
+ "about.update.check": "Check for updates",
659
+ "about.update.checking": "Checking the latest version…",
660
+ "about.update.current": "Up to date (v{version})",
661
+ "about.update.available": "v{latest} is available (current v{current})",
662
+ "about.update.apply": "Update to v{version}",
663
+ "about.update.confirm": "Update to v{version}? The plugin will be reinstalled and the panel reloaded; restart DSH afterwards.",
664
+ "about.update.confirmYes": "Update now",
665
+ "about.update.cancel": "Cancel",
666
+ "about.update.applying": "Updating to v{version}…",
667
+ "about.update.done": "Updated to v{version}; restart DSH to apply.",
668
+ "about.update.applied": "Updated to v{version}.",
669
+ "about.update.failed": "Update failed: {message}",
670
+ "about.update.checkFailed": "Update check failed: cannot reach the registry for the latest version. Retry later or run the command below.",
671
+ "about.update.manual": "Installed from a local {kind} spec; one-click update is disabled (it would replace the dev link). Update manually:",
672
+ "about.update.copy": "Copy command",
673
+ "about.update.copied": "Copied",
674
+ "about.update.hint": "Checks the npm latest stable tag only; applying requires a DSH restart."
393
675
  }
394
676
  };
395
677
  function activeLang(locale) {
@@ -412,14 +694,14 @@ function buildTranslate(locale) {
412
694
  // client/styles.ts
413
695
  var STYLE_ID = "dsh-github-trending-style";
414
696
  var CSS = `
415
- .dshgt-root{--gt-fg:var(--dsw-alias-label-primary,#1f2328);--gt-muted:var(--dsw-alias-label-secondary,#6b7280);--gt-bg:var(--dsw-alias-bg-base,#fff);--gt-card:var(--dsw-alias-bg-layer-1,#f8fafc);--gt-border:var(--dsw-alias-border-l2,#e5e7eb);--gt-accent:var(--dsw-alias-state-business-primary,#2563eb);--gt-ok:var(--dsw-alias-state-success-primary,#16a34a);--gt-warn:var(--dsw-alias-state-warn-primary,#d97706);--gt-err:var(--dsw-alias-state-error-primary,#dc2626);color:var(--gt-fg);font-size:13px;line-height:1.5;display:flex;flex-direction:column;gap:12px;}
697
+ .dshgt-root{--gt-fg:var(--dsw-alias-label-primary,#1f2328);--gt-muted:var(--dsw-alias-label-secondary,#6b7280);--gt-bg:var(--dsw-alias-bg-base,#fff);--gt-card:var(--dsw-alias-bg-layer-1,#f8fafc);--gt-border:var(--dsw-alias-border-l2,#e5e7eb);--gt-accent:var(--dsw-alias-state-business-primary,#2563eb);--gt-ok:var(--dsw-alias-state-success-primary,#16a34a);--gt-warn:var(--dsw-alias-state-warn-primary,#d97706);--gt-err:var(--dsw-alias-state-error-primary,#dc2626);color:var(--gt-fg);font-size:14px;line-height:1.55;display:flex;flex-direction:column;gap:12px;}
416
698
  .dshgt-root *{box-sizing:border-box;}
417
699
  .dshgt-head{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;}
418
- .dshgt-title{font-size:15px;font-weight:600;}
700
+ .dshgt-title{font-size:15px;font-weight:600;white-space:nowrap;}
419
701
  .dshgt-sub{color:var(--gt-muted);font-size:12px;}
420
702
  .dshgt-tabs{display:flex;gap:6px;border-bottom:1px solid var(--gt-border);}
421
703
  .dshgt-tab{appearance:none;border:none;background:none;padding:6px 10px;font:inherit;color:var(--gt-muted);cursor:pointer;border-bottom:2px solid transparent;}
422
- .dshgt-tab[aria-selected="true"]{color:var(--gt-fg);border-bottom-color:var(--gt-accent);font-weight:600;}
704
+ .dshgt-tab[aria-selected="true"]{color:var(--gt-fg);border-bottom-color:var(--gt-accent);font-weight:600;white-space:nowrap;}
423
705
  .dshgt-shell{height:100%;min-height:0;overflow:hidden;}
424
706
  .dshgt-content{flex:1;min-height:0;display:flex;}
425
707
  .dshgt-scroll{flex:1;min-height:0;overflow-y:auto;display:flex;flex-direction:column;gap:12px;padding-right:4px;}
@@ -427,12 +709,12 @@ var CSS = `
427
709
  .dshgt-vtabs{display:flex;flex-direction:column;gap:2px;border-right:1px solid var(--gt-border);padding-right:8px;overflow-y:auto;}
428
710
  .dshgt-vtab{appearance:none;border:none;background:none;text-align:left;padding:7px 10px;border-radius:7px;color:var(--gt-muted);cursor:pointer;font:inherit;font-size:12px;display:flex;align-items:center;gap:6px;}
429
711
  .dshgt-vtab:hover{background:var(--dsw-alias-interactive-bg-hover,color-mix(in srgb,var(--gt-accent) 8%,transparent));}
430
- .dshgt-vtab[aria-selected="true"]{background:color-mix(in srgb,var(--gt-accent) 12%,transparent);color:var(--gt-fg);font-weight:600;}
712
+ .dshgt-vtab[aria-selected="true"]{background:color-mix(in srgb,var(--gt-accent) 12%,transparent);color:var(--gt-fg);font-weight:600;white-space:nowrap;}
431
713
  .dshgt-vtab-dirty{width:6px;height:6px;border-radius:50%;background:var(--gt-warn);flex:none;margin-left:auto;}
432
714
  .dshgt-vpanel{min-width:0;min-height:0;overflow-y:auto;display:flex;flex-direction:column;gap:12px;padding-right:4px;}
433
715
  .dshgt-card{border:1px solid var(--gt-border);border-radius:10px;background:var(--gt-card);padding:12px 14px;display:flex;flex-direction:column;gap:8px;}
434
716
  .dshgt-card-h{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}
435
- .dshgt-card-h h3{margin:0;font-size:13px;font-weight:600;}
717
+ .dshgt-card-h h3{margin:0;font-size:13px;font-weight:600;white-space:nowrap;}
436
718
  .dshgt-hint{color:var(--gt-muted);font-size:12px;}
437
719
  .dshgt-desc{margin:0;color:var(--gt-muted);font-size:12px;}
438
720
  .dshgt-row{display:flex;gap:10px;align-items:center;min-height:28px;flex-wrap:wrap;}
@@ -477,6 +759,219 @@ var CSS = `
477
759
  .dshgt-monobox{margin:0;padding:8px;border-radius:7px;background:color-mix(in srgb,var(--gt-fg) 6%,var(--gt-bg));max-height:220px;overflow:auto;white-space:pre-wrap;word-break:break-word;}
478
760
  .dshgt-modal-mask{position:fixed;inset:0;background:var(--dsw-alias-bg-mask-1,rgba(15,23,42,.38));display:flex;align-items:center;justify-content:center;z-index:2147483000;}
479
761
  .dshgt-modal{background:var(--gt-bg);border-radius:12px;border:1px solid var(--gt-border);max-width:640px;width:calc(100% - 32px);max-height:80vh;overflow:auto;padding:16px;display:flex;flex-direction:column;gap:10px;box-shadow:0 20px 50px rgba(15,23,42,.25);}
762
+
763
+ /* ——— 01 工作台外壳 ——— */
764
+ .dshgt-shell{gap:0;height:100%;min-height:0;overflow:hidden;}
765
+ .dshgt-app{flex:1;min-height:0;display:grid;grid-template-columns:216px minmax(0,1fr);background:var(--gt-bg);}
766
+ .dshgt-rail{background:color-mix(in srgb,var(--gt-fg) 3%,var(--gt-bg));border-right:1px solid var(--gt-border);display:flex;flex-direction:column;padding:14px 10px 12px;min-height:0;overflow:hidden;}
767
+ .dshgt-brand{display:flex;align-items:center;gap:9px;padding:2px 6px 16px;}
768
+ .dshgt-brand svg{flex:none;border-radius:8px;}
769
+ .dshgt-brand-name{font-size:13.5px;font-weight:600;line-height:1.3;}
770
+ .dshgt-brand-ver{font-size:11px;color:var(--gt-muted);margin-top:1px;min-height:15px;}
771
+ .dshgt-nav{display:flex;flex-direction:column;gap:2px;}
772
+ .dshgt-nav-item{display:flex;align-items:center;gap:9px;height:36px;padding:0 9px;border:1px solid transparent;border-radius:8px;background:none;text-align:left;font-family:inherit;font-size:13.5px;color:var(--gt-muted);width:100%;cursor:pointer;}
773
+ .dshgt-nav-item svg{flex:none;opacity:.82;}
774
+ .dshgt-nav-item:hover{background:color-mix(in srgb,var(--gt-fg) 6%,var(--gt-bg));color:var(--gt-fg);}
775
+ .dshgt-nav-item.on{background:var(--gt-bg);border-color:var(--gt-border);color:var(--gt-fg);font-weight:600;white-space:nowrap;}
776
+ .dshgt-nav-item.on svg{opacity:1;}
777
+ .dshgt-nav-tail{margin-left:auto;display:flex;align-items:center;gap:5px;font-size:11.5px;color:var(--gt-muted);font-weight:400;}
778
+ .dshgt-rail-spacer{flex:1;min-height:16px;}
779
+ .dshgt-rail-status{border-top:1px solid var(--gt-border);padding:12px 8px 10px;}
780
+ .dshgt-rail-cap{font-size:11.5px;color:var(--gt-muted);margin-bottom:3px;}
781
+ .dshgt-rail-time{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:14px;line-height:1.35;}
782
+ .dshgt-rail-count{font-size:12px;color:var(--gt-muted);margin-top:1px;min-height:18px;}
783
+ .dshgt-rail-ready{display:flex;align-items:center;gap:7px;margin-top:10px;font-size:12px;color:var(--gt-muted);}
784
+ .dshgt-rail-pkg{border-top:1px solid var(--gt-border);padding:10px 8px 8px;}
785
+ .dshgt-pkg-name{border:0;background:none;padding:0;text-align:left;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:10px;line-height:1.45;color:var(--gt-muted);word-break:break-all;cursor:pointer;max-width:100%;}
786
+ .dshgt-pkg-name:hover{color:var(--gt-fg);}
787
+ .dshgt-pkg-sub{display:flex;align-items:center;gap:8px;margin-top:3px;font-size:11px;color:var(--gt-muted);}
788
+ .dshgt-pkg-sub .dshgt-mono{font-size:10.5px;color:var(--gt-fg);}
789
+ .dshgt-pkg-sub a{display:inline-flex;align-items:center;gap:3px;font-size:11px;color:var(--gt-accent);text-decoration:none;}
790
+ .dshgt-pkg-sub a:hover{text-decoration:underline;}
791
+ .dshgt-pkg-copy{width:22px;height:22px;margin-left:auto;display:grid;place-items:center;border:0;background:none;border-radius:4px;color:var(--gt-muted);cursor:pointer;}
792
+ .dshgt-pkg-copy:hover{background:color-mix(in srgb,var(--gt-fg) 7%,var(--gt-bg));color:var(--gt-fg);}
793
+ .dshgt-pkg-copy:disabled{opacity:.4;cursor:not-allowed;}
794
+ .dshgt-main{display:flex;flex-direction:column;min-width:0;min-height:0;background:var(--gt-bg);}
795
+ .dshgt-page-head{display:flex;align-items:flex-start;gap:16px;padding:20px 30px 14px;border-bottom:1px solid color-mix(in srgb,var(--gt-border) 70%,transparent);flex:none;}
796
+ .dshgt-page-heading{min-width:0;}
797
+ .dshgt-page-head h1{margin:0;font-size:21px;line-height:1.25;font-weight:650;letter-spacing:-.012em;}
798
+ .dshgt-dek{margin:4px 0 0;font-size:13px;color:var(--gt-muted);max-width:580px;line-height:1.5;}
799
+ .dshgt-head-actions{margin-left:auto;display:flex;align-items:center;gap:8px;padding-top:2px;flex:none;}
800
+ .dshgt-main-body{flex:1;min-height:0;display:flex;min-width:0;}
801
+ .dshgt-scroll{flex:1;min-height:0;overflow-y:auto;overflow-x:hidden;scrollbar-gutter:stable;}
802
+ .dshgt-main-body > .dshgt-settings{flex:1;min-height:0;padding:16px 24px 20px;}
803
+ .dshgt-view{padding:22px 30px 38px;max-width:1180px;width:100%;display:flex;flex-direction:column;gap:18px;}
804
+ .dshgt-grow{flex:1;min-width:0;}
805
+ .dshgt-dim{font-size:12px;color:var(--gt-muted);}
806
+ .dshgt-empty{padding:48px 0;text-align:center;color:var(--gt-muted);font-size:13px;}
807
+ .dshgt-tag{display:inline-flex;align-items:center;height:20px;padding:0 7px;border:1px solid var(--gt-border);border-radius:999px;font-size:11px;color:var(--gt-muted);background:var(--gt-bg);white-space:nowrap;}
808
+ .dshgt-inline-tag{margin-left:6px;vertical-align:1px;}
809
+
810
+ /* ——— 概览 ——— */
811
+ .dshgt-delivery{border:1px solid var(--gt-border);border-radius:12px;padding:18px 20px 16px;background:var(--gt-card);display:flex;flex-direction:column;gap:6px;}
812
+ .dshgt-delivery-top{display:flex;align-items:center;gap:10px;flex-wrap:wrap;}
813
+ .dshgt-pill-mini{margin-left:auto;font-size:12px;color:var(--gt-muted);border:1px solid var(--gt-border);border-radius:999px;padding:2px 9px;background:var(--gt-bg);}
814
+ .dshgt-delivery h2{margin:4px 0 0;font-size:18px;font-weight:600;letter-spacing:-.01em;line-height:1.35;word-break:break-word;}
815
+ .dshgt-when{display:flex;gap:14px;flex-wrap:wrap;font-size:12.5px;color:var(--gt-muted);}
816
+ .dshgt-when a{color:var(--gt-accent);text-decoration:none;}
817
+ .dshgt-when a:hover{text-decoration:underline;}
818
+ .dshgt-delivery-error{margin-top:6px;}
819
+ .dshgt-artifacts{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:10px;}
820
+ .dshgt-filechip{display:inline-flex;align-items:center;gap:7px;min-height:32px;padding:0 10px;border:1px solid var(--gt-border);border-radius:8px;background:var(--gt-bg);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;color:var(--gt-fg);cursor:pointer;text-align:left;max-width:100%;overflow:hidden;}
821
+ .dshgt-filechip .dshgt-mono{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
822
+ .dshgt-filechip:hover{border-color:color-mix(in srgb,var(--gt-accent) 40%,var(--gt-border));}
823
+ .dshgt-filechip-static{cursor:default;}
824
+ .dshgt-filechip-static:hover{border-color:var(--gt-border);}
825
+ .dshgt-facts{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:0;border-top:1px solid var(--gt-border);margin-top:12px;padding-top:10px;}
826
+ .dshgt-fact{padding:0 14px;border-left:1px solid var(--gt-border);min-width:0;}
827
+ .dshgt-fact:first-child{padding-left:0;border-left:0;}
828
+ .dshgt-fact-k{font-size:11.5px;color:var(--gt-muted);margin-bottom:2px;}
829
+ .dshgt-fact-v{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
830
+ .dshgt-block{display:flex;flex-direction:column;gap:8px;}
831
+ .dshgt-section-label{display:flex;align-items:center;gap:9px;font-size:12px;color:var(--gt-muted);letter-spacing:.02em;}
832
+ .dshgt-section-label:after{content:"";height:1px;flex:1;background:color-mix(in srgb,var(--gt-border) 70%,transparent);}
833
+ .dshgt-section-tail{margin-left:auto;font-size:11.5px;color:var(--gt-muted);}
834
+ .dshgt-ready-list{border-top:1px solid var(--gt-border);}
835
+ .dshgt-ready-row{display:grid;grid-template-columns:16px 108px minmax(0,1fr) auto;align-items:center;gap:12px;min-height:46px;border-bottom:1px solid color-mix(in srgb,var(--gt-border) 70%,transparent);font-size:13.5px;}
836
+ .dshgt-ready-label{color:var(--gt-muted);font-size:13px;}
837
+ .dshgt-ready-value{color:var(--gt-fg);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
838
+ .dshgt-ready-extra{color:var(--gt-muted);font-size:12px;margin-left:7px;}
839
+ .dshgt-linkbtn{border:0;background:none;color:var(--gt-accent);font-family:inherit;font-size:12.5px;padding:6px 2px;border-radius:4px;cursor:pointer;}
840
+ .dshgt-linkbtn:hover{text-decoration:underline;}
841
+ .dshgt-week{display:flex;gap:6px;align-items:stretch;}
842
+ .dshgt-day{flex:1;min-width:0;border:1px solid var(--gt-border);border-radius:8px;padding:8px;background:var(--gt-card);display:flex;flex-direction:column;gap:5px;text-align:left;font-family:inherit;color:inherit;}
843
+ .dshgt-day-clickable{cursor:pointer;}
844
+ .dshgt-day-clickable:hover{border-color:color-mix(in srgb,var(--gt-accent) 40%,var(--gt-border));}
845
+ .dshgt-day-date{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px;color:var(--gt-muted);}
846
+ .dshgt-day-status{display:flex;align-items:center;gap:6px;font-size:11.5px;color:var(--gt-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
847
+ .dshgt-lamp{width:7px;height:7px;border-radius:50%;background:var(--gt-muted);flex:none;}
848
+ .dshgt-lamp.ok{background:var(--gt-ok);}
849
+ .dshgt-lamp.warn{background:var(--gt-warn);}
850
+ .dshgt-lamp.err{background:var(--gt-err);}
851
+ .dshgt-live{border:1px solid var(--gt-border);border-radius:12px;padding:16px 18px;background:var(--gt-card);display:flex;flex-direction:column;gap:8px;}
852
+ .dshgt-live-top{display:flex;align-items:center;gap:9px;font-size:13px;flex-wrap:wrap;}
853
+ .dshgt-live-elapsed{margin-left:auto;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:13px;color:var(--gt-muted);}
854
+ .dshgt-live-note{display:flex;gap:10px;flex-wrap:wrap;font-size:12px;color:var(--gt-muted);line-height:1.6;}
855
+ .dshgt-live-actions{display:flex;gap:8px;}
856
+ .dshgt-spinner{width:13px;height:13px;border-radius:50%;border:1.6px solid var(--gt-border);border-top-color:var(--gt-accent);animation:dshgt-spin .8s linear infinite;display:inline-block;flex:none;}
857
+ @keyframes dshgt-spin{to{transform:rotate(360deg);}}
858
+
859
+ /* ——— 日报 ——— */
860
+ .dshgt-digest{padding-top:20px;gap:12px;}
861
+ .dshgt-digest-subbar{display:flex;align-items:center;gap:10px;flex-wrap:wrap;}
862
+ .dshgt-datenav{display:flex;align-items:center;gap:2px;border:1px solid var(--gt-border);border-radius:8px;background:var(--gt-card);padding:2px;}
863
+ .dshgt-icon-btn{width:28px;height:28px;display:grid;place-items:center;border:1px solid transparent;background:var(--gt-bg);border-radius:6px;color:var(--gt-fg);font-family:inherit;font-size:18px;line-height:1;cursor:pointer;flex:none;}
864
+ .dshgt-icon-btn:hover:not(:disabled){background:color-mix(in srgb,var(--gt-fg) 7%,var(--gt-bg));border-color:var(--gt-border);}
865
+ .dshgt-icon-btn:disabled{opacity:.3;cursor:not-allowed;}
866
+ .dshgt-dateselect{appearance:none;height:28px;border:0;background:transparent;font-family:inherit;font-size:13px;color:var(--gt-fg);padding:0 8px;min-width:210px;max-width:320px;cursor:pointer;}
867
+ .dshgt-digest-sheet{max-width:900px;width:100%;border:1px solid var(--gt-border);border-radius:12px;background:var(--gt-card);overflow:hidden;}
868
+ .dshgt-digest-body{display:flex;flex-direction:column;min-width:0;}
869
+ .dshgt-digest-bar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:9px 12px;border-bottom:1px solid color-mix(in srgb,var(--gt-border) 70%,transparent);background:color-mix(in srgb,var(--gt-fg) 2.5%,var(--gt-card));}
870
+ .dshgt-seg{display:inline-flex;border:1px solid var(--gt-border);border-radius:8px;padding:2px;background:var(--gt-bg);gap:2px;}
871
+ .dshgt-seg button{border:0;background:none;height:26px;padding:0 11px;border-radius:6px;font-family:inherit;font-size:12.5px;color:var(--gt-muted);cursor:pointer;}
872
+ .dshgt-seg button.on{background:var(--gt-card);color:var(--gt-fg);font-weight:600;white-space:nowrap;}
873
+ .dshgt-digest-doc{padding:26px 32px 22px;min-width:0;}
874
+ .dshgt-doc-meta{display:flex;gap:12px;flex-wrap:wrap;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px;color:var(--gt-muted);padding-bottom:12px;border-bottom:1px solid color-mix(in srgb,var(--gt-border) 70%,transparent);}
875
+ .dshgt-digest-raw{margin:0;padding:22px 28px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;line-height:1.75;white-space:pre-wrap;word-break:break-word;color:var(--gt-muted);background:color-mix(in srgb,var(--gt-fg) 2.5%,var(--gt-card));max-height:640px;overflow:auto;}
876
+ .dshgt-digest-loading{padding:40px 28px;color:var(--gt-muted);font-size:13px;}
877
+ .dshgt-digest-empty{padding:42px 36px 46px;max-width:720px;}
878
+ .dshgt-status-line{display:flex;align-items:center;gap:8px;font-size:12.5px;color:var(--gt-muted);}
879
+ .dshgt-status-line b{color:var(--gt-fg);font-weight:600;white-space:nowrap;}
880
+ .dshgt-digest-empty h2{margin:14px 0 6px;font-size:20px;font-weight:650;letter-spacing:-.012em;}
881
+ .dshgt-digest-empty p{margin:0;max-width:580px;font-size:13.5px;line-height:1.7;color:var(--gt-muted);}
882
+ .dshgt-digest-error{margin-top:10px;color:var(--gt-err);}
883
+ .dshgt-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:16px;}
884
+ .dshgt-md{font-size:14px;line-height:1.7;color:var(--gt-fg);word-break:break-word;}
885
+ .dshgt-md-h{margin:20px 0 8px;font-weight:650;line-height:1.35;letter-spacing:-.01em;}
886
+ .dshgt-md-h1{font-size:24px;margin-top:0;}
887
+ .dshgt-md-h2{font-size:18px;}
888
+ .dshgt-md-h3{font-size:15.5px;}
889
+ .dshgt-md-h4,.dshgt-md-h5,.dshgt-md-h6{font-size:14px;}
890
+ .dshgt-md-p{margin:8px 0;color:color-mix(in srgb,var(--gt-fg) 84%,var(--gt-muted));}
891
+ .dshgt-md-list{margin:8px 0;padding-left:22px;}
892
+ .dshgt-md-list li{margin:3px 0;}
893
+ .dshgt-md-quote{margin:12px 0;padding:2px 0 2px 14px;border-left:3px solid var(--gt-border);color:var(--gt-muted);}
894
+ .dshgt-md-tablewrap{margin:12px 0;border:1px solid var(--gt-border);border-radius:8px;overflow-x:auto;}
895
+ .dshgt-md-table{width:100%;min-width:560px;border-collapse:collapse;font-size:13px;line-height:1.6;}
896
+ .dshgt-md-table th,.dshgt-md-table td{padding:7px 10px;text-align:left;vertical-align:top;border-bottom:1px solid color-mix(in srgb,var(--gt-border) 65%,transparent);word-break:normal;overflow-wrap:break-word;}
897
+ .dshgt-md-table th{background:color-mix(in srgb,var(--gt-fg) 4%,var(--gt-card));font-weight:600;white-space:nowrap;}
898
+ .dshgt-md-table tbody tr:last-child td{border-bottom:0;}
899
+ .dshgt-md-table tbody tr:nth-child(even){background:color-mix(in srgb,var(--gt-fg) 2%,var(--gt-card));}
900
+ .dshgt-md-pre{margin:12px 0;padding:12px 14px;border-radius:8px;background:color-mix(in srgb,var(--gt-fg) 5%,var(--gt-card));overflow:auto;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;line-height:1.6;}
901
+ .dshgt-md-code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;background:color-mix(in srgb,var(--gt-fg) 7%,var(--gt-card));padding:1px 4px;border-radius:4px;}
902
+ .dshgt-md-hr{border:0;border-top:1px solid var(--gt-border);margin:18px 0;}
903
+ .dshgt-md a{color:var(--gt-accent);text-decoration:none;}
904
+ .dshgt-md a:hover{text-decoration:underline;}
905
+
906
+ /* ——— 运行记录 ——— */
907
+ .dshgt-runs-head{display:flex;align-items:center;gap:10px;flex-wrap:wrap;}
908
+ .dshgt-runs-filter{display:flex;gap:4px;margin-left:auto;}
909
+ .dshgt-run-list{border-top:1px solid var(--gt-border);}
910
+ .dshgt-run-item{border-bottom:1px solid color-mix(in srgb,var(--gt-border) 70%,transparent);}
911
+ .dshgt-run-row{display:grid;grid-template-columns:18px 132px minmax(0,1fr) 80px 82px 20px;align-items:center;gap:12px;min-height:52px;width:100%;padding:0 6px;border:0;background:none;text-align:left;font-family:inherit;color:var(--gt-fg);cursor:pointer;border-radius:6px;}
912
+ .dshgt-run-row:hover{background:color-mix(in srgb,var(--gt-fg) 4%,var(--gt-bg));}
913
+ .dshgt-run-date{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;color:var(--gt-muted);white-space:nowrap;}
914
+ .dshgt-run-what{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:13px;}
915
+ .dshgt-run-muted{color:var(--gt-muted);font-size:12px;margin-left:6px;}
916
+ .dshgt-run-files,.dshgt-run-duration{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;color:var(--gt-muted);text-align:right;}
917
+ .dshgt-run-chev{color:var(--gt-muted);text-align:center;transition:transform .15s;}
918
+ .dshgt-run-item.open .dshgt-run-chev{transform:rotate(90deg);}
919
+ .dshgt-run-detail{padding:2px 6px 16px 30px;display:flex;flex-direction:column;gap:10px;}
920
+ .dshgt-detail-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px 22px;padding:12px 0;border-top:1px solid color-mix(in srgb,var(--gt-border) 70%,transparent);}
921
+ .dshgt-detail-k{font-size:11.5px;color:var(--gt-muted);}
922
+ .dshgt-detail-v{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;word-break:break-all;}
923
+ .dshgt-detail-v a{color:var(--gt-accent);text-decoration:none;}
924
+ .dshgt-detail-v a:hover{text-decoration:underline;}
925
+ .dshgt-run-actions{display:flex;align-items:center;gap:10px;flex-wrap:wrap;}
926
+
927
+ /* ——— 日志抽屉 / 关于 ——— */
928
+ .dshgt-log-modal{max-width:820px;width:calc(100% - 32px);}
929
+ .dshgt-log-head{display:flex;align-items:center;gap:10px;flex-wrap:wrap;}
930
+ .dshgt-log-title{margin:0;font-size:15px;}
931
+ .dshgt-log-head .dshgt-btn{margin-left:auto;}
932
+ .dshgt-log-pre{margin:0;max-height:52vh;overflow:auto;padding:12px;border-radius:8px;background:color-mix(in srgb,var(--gt-fg) 5%,var(--gt-card));font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;line-height:1.6;white-space:pre-wrap;word-break:break-word;}
933
+ .dshgt-log-foot{display:flex;align-items:center;gap:10px;flex-wrap:wrap;}
934
+ .dshgt-about{max-width:520px;}
935
+ .dshgt-about-head{display:flex;align-items:center;gap:10px;}
936
+ .dshgt-about-title{margin:0;font-size:16px;}
937
+ .dshgt-about-head .dshgt-btn{margin-left:auto;}
938
+ .dshgt-about-pkg{display:flex;align-items:center;justify-content:space-between;gap:10px;width:100%;text-align:left;border:1px solid var(--gt-border);background:var(--gt-card);border-radius:8px;padding:9px 10px;cursor:pointer;font-family:inherit;color:inherit;}
939
+ .dshgt-about-pkg:hover{border-color:color-mix(in srgb,var(--gt-accent) 40%,var(--gt-border));}
940
+ .dshgt-about-pkg .dshgt-mono{font-size:12px;word-break:break-all;}
941
+ .dshgt-about .dshgt-kv{grid-template-columns:110px 1fr;}
942
+ .dshgt-about .dshgt-kv a{color:var(--gt-accent);text-decoration:none;word-break:break-all;}
943
+ .dshgt-about .dshgt-kv a:hover{text-decoration:underline;}
944
+ .dshgt-update{margin-top:14px;padding-top:12px;border-top:1px solid color-mix(in srgb,var(--gt-border) 70%,transparent);}
945
+ .dshgt-update-head{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap;}
946
+ .dshgt-update-head h4{margin:0;font-size:13px;font-weight:600;}
947
+ .dshgt-update-head .dshgt-hint{font-size:11.5px;}
948
+ .dshgt-update-line{margin:6px 0 0;font-size:12.5px;color:var(--gt-muted);line-height:1.55;}
949
+ .dshgt-update-err{color:var(--gt-err);}
950
+ .dshgt-update-warn{margin:6px 0 0;font-size:12px;color:var(--gt-warn);line-height:1.55;}
951
+ .dshgt-update-actions{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-top:9px;}
952
+ .dshgt-update-manual{margin-top:9px;padding:9px 10px;border:1px solid color-mix(in srgb,var(--gt-warn) 35%,var(--gt-border));border-radius:8px;background:color-mix(in srgb,var(--gt-warn) 6%,var(--gt-card));}
953
+ .dshgt-update-manual .dshgt-update-warn{margin:0 0 6px;}
954
+ .dshgt-update-cmd{display:block;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11.5px;line-height:1.5;color:var(--gt-fg);word-break:break-all;user-select:all;}
955
+
956
+ @media (max-width:1100px){
957
+ .dshgt-app{grid-template-columns:190px minmax(0,1fr);}
958
+ .dshgt-page-head{padding:18px 22px 12px;}
959
+ .dshgt-view{padding:18px 22px 30px;}
960
+ .dshgt-facts{grid-template-columns:repeat(2,minmax(0,1fr));row-gap:10px;}
961
+ .dshgt-fact:nth-child(3){padding-left:0;border-left:0;}
962
+ .dshgt-week{flex-wrap:wrap;}
963
+ .dshgt-day{flex:1 1 88px;}
964
+ .dshgt-run-row{grid-template-columns:18px 112px minmax(0,1fr) 70px 20px;}
965
+ .dshgt-run-files{display:none;}
966
+ .dshgt-detail-grid{grid-template-columns:repeat(2,minmax(0,1fr));}
967
+ .dshgt-dateselect{min-width:180px;}
968
+ }
969
+ @media (max-width:980px){
970
+ .dshgt-app{grid-template-columns:172px minmax(0,1fr);}
971
+ .dshgt-page-head h1{font-size:19px;}
972
+ .dshgt-dek{font-size:12.5px;}
973
+ .dshgt-main-body > .dshgt-settings{padding:14px 18px 18px;}
974
+ }
480
975
  `;
481
976
  function injectStyles() {
482
977
  if (typeof document === "undefined") return () => void 0;
@@ -517,9 +1012,6 @@ function Row(props) {
517
1012
  props.children
518
1013
  );
519
1014
  }
520
- function Inline(props) {
521
- return h("div", { className: "dshgt-inline" }, props.children);
522
- }
523
1015
  function Input(props) {
524
1016
  const { value, onInput, placeholder, mono, width, password, ...rest } = props;
525
1017
  return h("input", {
@@ -632,6 +1124,21 @@ function localTime(iso) {
632
1124
  if (Number.isNaN(date.getTime())) return "—";
633
1125
  return date.toLocaleString();
634
1126
  }
1127
+ async function copyText(value) {
1128
+ try {
1129
+ if (typeof navigator === "undefined" || navigator.clipboard === void 0) return false;
1130
+ await navigator.clipboard.writeText(value);
1131
+ return true;
1132
+ } catch {
1133
+ return false;
1134
+ }
1135
+ }
1136
+ function shortTime(iso) {
1137
+ if (iso === null || iso === void 0) return "—";
1138
+ const date = new Date(iso);
1139
+ if (Number.isNaN(date.getTime())) return "—";
1140
+ return date.toLocaleString(void 0, { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" });
1141
+ }
635
1142
 
636
1143
  // client/api.ts
637
1144
  var API = "/api/morehao/github-trending";
@@ -657,6 +1164,23 @@ async function apiCall(method, path, body, options = {}) {
657
1164
  }
658
1165
  return json;
659
1166
  }
1167
+ function getPreview(runId, signal) {
1168
+ return apiCall("GET", `/preview?id=${encodeURIComponent(runId)}`, void 0, signal === void 0 ? {} : { signal });
1169
+ }
1170
+ function getLog(runId, signal) {
1171
+ return apiCall("GET", `/log?id=${encodeURIComponent(runId)}`, void 0, signal === void 0 ? {} : { signal });
1172
+ }
1173
+ async function getPluginUpdate(options = {}) {
1174
+ const query = options.fresh === true ? "?fresh=1" : "";
1175
+ const payload = await apiCall("GET", `/update${query}`, void 0, {
1176
+ ...options.signal === void 0 ? {} : { signal: options.signal }
1177
+ });
1178
+ return payload.update;
1179
+ }
1180
+ async function applyPluginUpdate() {
1181
+ const payload = await apiCall("POST", "/update", {});
1182
+ return payload.result;
1183
+ }
660
1184
 
661
1185
  // client/store.ts
662
1186
  var POLL_MS = 5e3;
@@ -691,6 +1215,29 @@ function useStore() {
691
1215
  return { state, error, loading, refresh };
692
1216
  }
693
1217
 
1218
+ // client/credential.ts
1219
+ function credentialView(model, t) {
1220
+ if (model.mode === "proxy") {
1221
+ return { ok: true, kind: "ok", label: t("auth.hostProxy"), hint: t("model.credential.proxyHint") };
1222
+ }
1223
+ const credential = model.credential;
1224
+ if (credential.configured) {
1225
+ return {
1226
+ ok: true,
1227
+ kind: "ok",
1228
+ label: `${t("auth.configured")}${credential.source === null ? "" : ` · ${credential.source}`}`,
1229
+ hint: t("model.credential.directHint")
1230
+ };
1231
+ }
1232
+ const missing = t("auth.missing");
1233
+ return {
1234
+ ok: false,
1235
+ kind: "err",
1236
+ label: credential.ref.trim() === "" ? missing : `${missing} · ${credential.ref}`,
1237
+ hint: t("model.credential.missingHint")
1238
+ };
1239
+ }
1240
+
694
1241
  // client/settings.ts
695
1242
  var LANGUAGES = ["all", "typescript", "python", "go", "rust", "java", "c", "cpp", "javascript", "csharp"];
696
1243
  var SECTIONS = ["source", "model", "repo", "auth", "scope", "schedule"];
@@ -736,13 +1283,13 @@ function SettingsView(props) {
736
1283
  const [rechecking, setRechecking] = import_react.default.useState(false);
737
1284
  const [downloading, setDownloading] = import_react.default.useState(false);
738
1285
  const [quickOpen, setQuickOpen] = import_react.default.useState(false);
739
- const [advanced, setAdvanced] = import_react.default.useState(false);
740
1286
  const [section, setSection] = import_react.default.useState("source");
741
1287
  const [branchInfo, setBranchInfo] = import_react.default.useState(null);
742
1288
  const [branchLoading, setBranchLoading] = import_react.default.useState(false);
743
1289
  const [testing, setTesting] = import_react.default.useState(false);
744
1290
  const [githubTest, setGithubTest] = import_react.default.useState(null);
745
1291
  const anyDirty = SECTIONS.some((id) => dirtySections[id]);
1292
+ const sectionEditable = section !== "source";
746
1293
  import_react.default.useEffect(() => {
747
1294
  if (state !== null && !anyDirty) setDraft(state.settings);
748
1295
  }, [state, anyDirty]);
@@ -940,11 +1487,9 @@ function SettingsView(props) {
940
1487
  state.core.release === void 0 ? null : h("p", { className: "dshgt-hint dshgt-mono", style: { fontSize: 11 } }, `${state.core.release.url} → ${state.core.release.binaryPath}`),
941
1488
  state.core.state === "ready" ? null : h(Alert, { kind: "err", title: t("core.label") }, h("div", null, state.core.message ?? ""), h("div", { className: "dshgt-hint" }, t("core.order"))),
942
1489
  h("p", { className: "dshgt-hint" }, t("core.order")),
943
- state.coexistence.found ? h(Alert, { kind: "warn", title: t("coexist.title") }, h("div", { className: "dshgt-mono", style: { fontSize: 12 } }, `${state.coexistence.path ?? ""} ${state.coexistence.version ?? ""}`), h("div", null, state.coexistence.message ?? ""), h("div", { className: "dshgt-hint" }, t("coexist.hint"))) : null,
944
- advanced ? h(Row, { label: "TRENDING_DIGEST_CORE" }, h(Input, { mono: true, width: 420, value: draft.corePath, placeholder: "/path/to/trendradar", onInput: (value) => update((next) => {
945
- next.corePath = value;
946
- }) }), h("span", { className: "dshgt-hint" }, t("core.path.advancedHint"))) : null
1490
+ state.coexistence.found ? h(Alert, { kind: "warn", title: t("coexist.title") }, h("div", { className: "dshgt-mono", style: { fontSize: 12 } }, `${state.coexistence.path ?? ""} ${state.coexistence.version ?? ""}`), h("div", null, state.coexistence.message ?? ""), h("div", { className: "dshgt-hint" }, t("coexist.hint"))) : null
947
1491
  );
1492
+ const modelCredential = credentialView({ mode: draft.model.mode, credential: state.model.credential }, t);
948
1493
  const modelCard = h(
949
1494
  Card,
950
1495
  {
@@ -963,7 +1508,7 @@ function SettingsView(props) {
963
1508
  next.model.provider = value;
964
1509
  })
965
1510
  }),
966
- h("span", { className: "dshgt-hint" }, state.model.mode === "proxy" ? t("model.mode.proxy") : t("model.mode.direct"))
1511
+ h("span", { className: "dshgt-hint" }, draft.model.mode === "proxy" ? t("model.mode.proxy") : t("model.mode.direct"))
967
1512
  ),
968
1513
  h(Row, { label: t("model.baseURL") }, h(Input, {
969
1514
  mono: true,
@@ -988,22 +1533,11 @@ function SettingsView(props) {
988
1533
  }),
989
1534
  h(Btn, { small: true, onClick: () => void runPreflight(), disabled: preflighting }, preflighting ? t("model.preflighting") : t("model.preflight"))
990
1535
  ),
991
- advanced ? h(
992
- Row,
993
- { label: t("model.keyRef") },
994
- h(Input, { mono: true, width: 220, value: draft.model.apiKeyRef, onInput: (value) => update((next) => {
995
- next.model.apiKeyRef = value;
996
- }) }),
997
- h("span", { className: "dshgt-hint" }, t("model.keyRef.advancedHint"))
998
- ) : h(
1536
+ h(
999
1537
  Row,
1000
1538
  { label: t("model.credential") },
1001
- h(
1002
- Pill,
1003
- { kind: state.model.credential.configured ? "ok" : "err" },
1004
- state.model.credential.configured ? `${t("auth.configured")}${state.model.credential.source === null ? "" : ` · ${state.model.credential.source}`}` : t("auth.missing")
1005
- ),
1006
- h("span", { className: "dshgt-hint" }, t("model.credential.hint"))
1539
+ h(Pill, { kind: modelCredential.kind }, modelCredential.label),
1540
+ h("span", { className: "dshgt-hint" }, modelCredential.hint)
1007
1541
  ),
1008
1542
  h(
1009
1543
  Row,
@@ -1199,7 +1733,7 @@ function SettingsView(props) {
1199
1733
  h(
1200
1734
  "div",
1201
1735
  { className: "dshgt-footer" },
1202
- h(
1736
+ sectionEditable ? h(
1203
1737
  Btn,
1204
1738
  {
1205
1739
  variant: "primary",
@@ -1208,11 +1742,10 @@ function SettingsView(props) {
1208
1742
  title: t("settings.saveSection.hint")
1209
1743
  },
1210
1744
  savingSection === section ? t("settings.saving") : dirtySections[section] ? t("settings.saveSection") : savedSection === section ? t("settings.saved") : t("settings.saveSection")
1211
- ),
1212
- h("span", { className: "dshgt-hint" }, t("settings.saveSection.hint")),
1745
+ ) : null,
1746
+ sectionEditable ? h("span", { className: "dshgt-hint" }, t("settings.saveSection.hint")) : null,
1213
1747
  h(Btn, { onClick: () => void importCli() }, t("settings.import")),
1214
- h(Btn, { onClick: copyConfig }, t("settings.copyConfig")),
1215
- h(Btn, { onClick: () => setAdvanced(!advanced) }, advanced ? t("settings.advanced.hide") : t("settings.advanced"))
1748
+ h(Btn, { onClick: copyConfig }, t("settings.copyConfig"))
1216
1749
  )
1217
1750
  ),
1218
1751
  quickOpen ? h(QuickFillModal, { t, onClose: () => setQuickOpen(false), onApplied: (settings) => {
@@ -1346,166 +1879,1543 @@ function QuickFillModal(props) {
1346
1879
  );
1347
1880
  }
1348
1881
 
1349
- // client/runs.ts
1350
- var STEP_KEYS = ["resolve", "handshake", "spawn", "fetch", "summarize", "render", "commit", "pr", "state"];
1351
- var CORE_STEPS = /* @__PURE__ */ new Set(["fetch", "summarize", "render", "commit", "pr"]);
1352
- function RunsView(props) {
1353
- const { t, store } = props;
1354
- const [tab, setTab] = import_react.default.useState("now");
1355
- const [dryRun, setDryRun] = import_react.default.useState(false);
1882
+ // client/error-copy.ts
1883
+ function reasonHint(t, reason) {
1884
+ if (reason === void 0 || reason === "") return null;
1885
+ const key = `err.reason.${reason}`;
1886
+ const text = t(key);
1887
+ return text === key ? null : text;
1888
+ }
1889
+ function skipHint(t, reason, fallback) {
1890
+ if (reason === void 0 || reason === "") return fallback;
1891
+ const key = `skip.${reason}`;
1892
+ const text = t(key);
1893
+ return text === key ? fallback : text;
1894
+ }
1895
+
1896
+ // client/run-controller.ts
1897
+ function useRunController(store) {
1898
+ const { refresh } = store;
1356
1899
  const [busy, setBusy] = import_react.default.useState(false);
1357
- const [summary, setSummary] = import_react.default.useState(null);
1358
1900
  const [error, setError] = import_react.default.useState(null);
1359
- const state = store.state;
1360
- const run = async () => {
1361
- setBusy(true);
1901
+ const [summary, setSummary] = import_react.default.useState(null);
1902
+ const [dryRun, setDryRun] = import_react.default.useState(false);
1903
+ const [localStartedAt, setLocalStartedAt] = import_react.default.useState(null);
1904
+ const run = import_react.default.useCallback(
1905
+ async (overrideDry) => {
1906
+ const dry = overrideDry ?? dryRun;
1907
+ setBusy(true);
1908
+ setError(null);
1909
+ setSummary(null);
1910
+ setLocalStartedAt((/* @__PURE__ */ new Date()).toISOString());
1911
+ try {
1912
+ const result = await apiCall("POST", "/run", { dryRun: dry });
1913
+ setSummary(result.summary);
1914
+ await refresh({ quiet: true });
1915
+ } catch (failure) {
1916
+ setError(failure instanceof Error ? failure.message : String(failure));
1917
+ } finally {
1918
+ setBusy(false);
1919
+ setLocalStartedAt(null);
1920
+ }
1921
+ },
1922
+ [dryRun, refresh]
1923
+ );
1924
+ const cancel = import_react.default.useCallback(async () => {
1362
1925
  setError(null);
1363
- setSummary(null);
1364
- try {
1365
- const result = await apiCall("POST", "/run", { dryRun });
1366
- setSummary(result.summary);
1367
- await store.refresh({ quiet: true });
1368
- } catch (failure) {
1369
- setError(failure instanceof Error ? failure.message : String(failure));
1370
- } finally {
1371
- setBusy(false);
1372
- }
1373
- };
1374
- const cancel = async () => {
1375
1926
  try {
1376
1927
  await apiCall("POST", "/run/cancel", {});
1377
1928
  } catch (failure) {
1378
1929
  setError(failure instanceof Error ? failure.message : String(failure));
1930
+ } finally {
1931
+ await refresh({ quiet: true });
1379
1932
  }
1933
+ }, [refresh]);
1934
+ const serverRunning = store.state?.status.running ?? null;
1935
+ return {
1936
+ running: serverRunning !== null || busy,
1937
+ busy,
1938
+ error,
1939
+ summary,
1940
+ startedAt: serverRunning?.startedAt ?? localStartedAt,
1941
+ dryRun,
1942
+ setDryRun,
1943
+ run,
1944
+ cancel,
1945
+ clearError: () => setError(null)
1380
1946
  };
1381
- if (state === null) {
1382
- return h("div", { className: "dshgt-root" }, h("p", { className: "dshgt-hint" }, store.error ?? t("quickfill.loading")));
1383
- }
1384
- const running = state.status.running !== null;
1385
- const pendingPr = state.status.pendingPr;
1386
- const shown = summary;
1387
- const lastRecord = state.status.history[0] === void 0 ? null : state.status.history[0];
1388
- const last = shown === null ? lastRecord : { ...lastRecord, ...shown, at: (/* @__PURE__ */ new Date()).toISOString(), model: { provider: state.settings.model.provider, model: state.settings.model.model } };
1389
- const failedStep = shown !== null && !shown.ok ? stepFromError(shown.error?.area) : null;
1947
+ }
1948
+ function useElapsed(startedAt, active) {
1949
+ const [now, setNow] = import_react.default.useState(() => Date.now());
1950
+ import_react.default.useEffect(() => {
1951
+ if (!active) return () => void 0;
1952
+ setNow(Date.now());
1953
+ const timer = setInterval(() => setNow(Date.now()), 1e3);
1954
+ return () => clearInterval(timer);
1955
+ }, [active]);
1956
+ if (startedAt === null || startedAt === void 0) return 0;
1957
+ const started = new Date(startedAt).getTime();
1958
+ if (Number.isNaN(started)) return 0;
1959
+ return Math.max(0, now - started);
1960
+ }
1961
+ function formatElapsed(ms) {
1962
+ const total = Math.floor(ms / 1e3);
1963
+ const minutes = Math.floor(total / 60);
1964
+ const seconds = total % 60;
1965
+ return `${minutes}:${String(seconds).padStart(2, "0")}`;
1966
+ }
1967
+
1968
+ // client/live-run.ts
1969
+ function LiveRun(props) {
1970
+ const { t, controller } = props;
1971
+ const elapsed = useElapsed(props.startedAt, true);
1390
1972
  return h(
1391
1973
  "div",
1392
- { className: "dshgt-root" },
1974
+ { className: "dshgt-live" },
1393
1975
  h(
1394
1976
  "div",
1395
- { className: "dshgt-head" },
1396
- h("span", { className: "dshgt-title" }, t("section.title")),
1397
- h("span", { className: "dshgt-sub" }, t("card.source.hint")),
1398
- h("span", { style: { marginLeft: "auto" } }, h(Pill, { kind: running || busy ? "warn" : "ok" }, running || busy ? t("run.running") : t("run.idle")))
1977
+ { className: "dshgt-live-top" },
1978
+ h("span", { className: "dshgt-dot warn" }),
1979
+ h("b", null, t("overview.live.title")),
1980
+ h("span", { className: "dshgt-spinner", "aria-hidden": "true" }),
1981
+ h("span", { className: "dshgt-hint" }, t("overview.live.core")),
1982
+ h("span", { className: "dshgt-live-elapsed" }, formatElapsed(elapsed))
1399
1983
  ),
1400
1984
  h(
1401
1985
  "div",
1402
- { className: "dshgt-tabs", role: "tablist" },
1403
- h("button", { type: "button", className: "dshgt-tab", role: "tab", "aria-selected": tab === "now" ? "true" : "false", onClick: () => setTab("now") }, t("run.now")),
1404
- h("button", { type: "button", className: "dshgt-tab", role: "tab", "aria-selected": tab === "history" ? "true" : "false", onClick: () => setTab("history") }, t("run.history"))
1986
+ { className: "dshgt-live-note" },
1987
+ h("span", null, t("overview.live.note")),
1988
+ props.startedAt === null ? null : h("span", { className: "dshgt-hint" }, `${t("overview.live.started")} ${localTime(props.startedAt)}`)
1405
1989
  ),
1406
- tab === "now" ? h(
1990
+ h(
1407
1991
  "div",
1408
- { style: { display: "flex", flexDirection: "column", gap: 12 } },
1992
+ { className: "dshgt-live-actions" },
1993
+ h(Btn, { small: true, variant: "danger", onClick: () => void controller.cancel() }, t("run.cancel"))
1994
+ )
1995
+ );
1996
+ }
1997
+
1998
+ // client/digest-core.ts
1999
+ var DIGEST_WINDOW_DAYS = 30;
2000
+ var DATE_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
2001
+ function isValidDate(value) {
2002
+ if (typeof value !== "string") return false;
2003
+ const match = DATE_RE.exec(value);
2004
+ if (match === null) return false;
2005
+ const year = Number(match[1]);
2006
+ const month = Number(match[2]);
2007
+ const day = Number(match[3]);
2008
+ const date = new Date(Date.UTC(year, month - 1, day));
2009
+ return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day;
2010
+ }
2011
+ function zonedDateString(date, timeZone) {
2012
+ try {
2013
+ return new Intl.DateTimeFormat("en-CA", {
2014
+ timeZone,
2015
+ year: "numeric",
2016
+ month: "2-digit",
2017
+ day: "2-digit"
2018
+ }).format(date);
2019
+ } catch {
2020
+ const year = date.getFullYear();
2021
+ const month = String(date.getMonth() + 1).padStart(2, "0");
2022
+ const day = String(date.getDate()).padStart(2, "0");
2023
+ return `${year}-${month}-${day}`;
2024
+ }
2025
+ }
2026
+ function addDays(date, days) {
2027
+ const match = DATE_RE.exec(date);
2028
+ if (match === null) return date;
2029
+ const value = new Date(Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3])));
2030
+ value.setUTCDate(value.getUTCDate() + days);
2031
+ return value.toISOString().slice(0, 10);
2032
+ }
2033
+ function recordDate(record, timeZone) {
2034
+ if (isValidDate(record.date)) return { date: record.date, derivedFromAt: false };
2035
+ const at = new Date(record.at);
2036
+ if (Number.isNaN(at.getTime())) return void 0;
2037
+ return { date: zonedDateString(at, timeZone), derivedFromAt: true };
2038
+ }
2039
+ function digestStatus(record) {
2040
+ if (!record.ok) return "failed";
2041
+ if (record.skipped) return "skipped";
2042
+ if (record.kind === "dry-run") return "dry";
2043
+ if (record.preview !== void 0) return "preview";
2044
+ if (record.legacy === true) return "legacy";
2045
+ return "missing";
2046
+ }
2047
+ function buildDigestDates(records, options) {
2048
+ const now = options.now ?? /* @__PURE__ */ new Date();
2049
+ const today = zonedDateString(now, options.timeZone);
2050
+ const earliest = addDays(today, -(DIGEST_WINDOW_DAYS - 1));
2051
+ const groups = /* @__PURE__ */ new Map();
2052
+ for (const record of records) {
2053
+ const info = recordDate(record, options.timeZone);
2054
+ if (info === void 0) continue;
2055
+ if (info.date < earliest || info.date > today) continue;
2056
+ const group = groups.get(info.date);
2057
+ if (group === void 0) groups.set(info.date, { records: [record], derivedFromAt: info.derivedFromAt });
2058
+ else {
2059
+ group.records.push(record);
2060
+ group.derivedFromAt = group.derivedFromAt || info.derivedFromAt;
2061
+ }
2062
+ }
2063
+ const days = [];
2064
+ for (const [date, group] of groups) {
2065
+ const sorted = [...group.records].sort((a, b) => b.at.localeCompare(a.at));
2066
+ const preferred = sorted.find(
2067
+ (record2) => record2.preview !== void 0 && record2.ok && record2.kind !== "dry-run" && !record2.skipped
2068
+ );
2069
+ const record = preferred ?? sorted[0];
2070
+ if (record === void 0) continue;
2071
+ const info = recordDate(record, options.timeZone);
2072
+ const legacy = record.legacy === true || info?.derivedFromAt === true || group.derivedFromAt;
2073
+ days.push({ date, record, status: digestStatus(record), legacy });
2074
+ }
2075
+ days.sort((a, b) => b.date.localeCompare(a.date));
2076
+ return days;
2077
+ }
2078
+ function recentDates(today, count) {
2079
+ const dates = [];
2080
+ for (let offset = count - 1; offset >= 0; offset -= 1) dates.push(addDays(today, -offset));
2081
+ return dates;
2082
+ }
2083
+ function formatDayLabel(date, lang) {
2084
+ let weekday = "";
2085
+ try {
2086
+ weekday = new Intl.DateTimeFormat(lang === "en" ? "en-US" : "zh-CN", {
2087
+ weekday: "short",
2088
+ timeZone: "UTC"
2089
+ }).format(/* @__PURE__ */ new Date(`${date}T12:00:00Z`));
2090
+ } catch {
2091
+ weekday = "";
2092
+ }
2093
+ return weekday === "" ? date : `${date} ${weekday}`;
2094
+ }
2095
+ function formatBytes(bytes) {
2096
+ if (!Number.isFinite(bytes) || bytes < 0) return "—";
2097
+ if (bytes < 1024) return `${bytes} B`;
2098
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
2099
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
2100
+ }
2101
+
2102
+ // client/overview.ts
2103
+ function statusPill(t, record) {
2104
+ if (!record.ok) return h(Pill, { kind: "err" }, t("state.failed"));
2105
+ if (record.skipped) return h(Pill, { kind: "warn" }, t("state.skipped"));
2106
+ if (record.kind === "dry-run") return h(Pill, { kind: "warn" }, t("state.dry"));
2107
+ return h(Pill, { kind: "ok" }, t("state.ok"));
2108
+ }
2109
+ var STATUS_LAMP = {
2110
+ preview: "ok",
2111
+ failed: "err",
2112
+ skipped: "warn",
2113
+ dry: "warn",
2114
+ legacy: "warn",
2115
+ missing: "warn"
2116
+ };
2117
+ function dayLabel(t, day) {
2118
+ switch (day.status) {
2119
+ case "preview":
2120
+ return duration(day.record.durationMs);
2121
+ case "failed":
2122
+ return t("state.failed");
2123
+ case "skipped":
2124
+ return t("state.skipped");
2125
+ case "dry":
2126
+ return t("state.dry");
2127
+ case "legacy":
2128
+ return t("digest.tag.legacy");
2129
+ case "missing":
2130
+ return t("digest.tag.missing");
2131
+ }
2132
+ }
2133
+ function prLabel(t, state, record) {
2134
+ if (record === null || record.prUrl === void 0) {
2135
+ if (record !== null && record.ok && !record.skipped && record.kind !== "dry-run" && state.settings.repo.commitMode === "direct") {
2136
+ return { label: t("overview.last.direct"), kind: "idle" };
2137
+ }
2138
+ return null;
2139
+ }
2140
+ const pending = state.status.pendingPr;
2141
+ const matches = pending !== null && pending.url === record.prUrl;
2142
+ if (matches && state.status.pendingPrState === "unknown") {
2143
+ return { label: t("overview.last.prUnknown"), url: record.prUrl, kind: "warn" };
2144
+ }
2145
+ if (matches) return { label: t("overview.last.prPending"), url: record.prUrl, kind: "warn" };
2146
+ return { label: t("overview.last.prCreated"), url: record.prUrl, kind: "ok" };
2147
+ }
2148
+ function readyRows(t, state) {
2149
+ const core = state.core;
2150
+ const credential = credentialView(state.model, t);
2151
+ const repo = state.settings.repo;
2152
+ return [
2153
+ {
2154
+ ok: core.state === "ready",
2155
+ label: t("overview.ready.core"),
2156
+ value: core.state === "ready" ? `${core.version ?? "—"} · ${t("core.ready")}` : t(`core.${core.state}`),
2157
+ ...core.ipcSchema === void 0 ? {} : { extra: `ipc ${core.ipcSchema} · digest ${core.digestSchema ?? "—"}` }
2158
+ },
2159
+ {
2160
+ ok: credential.ok,
2161
+ label: t("overview.ready.model"),
2162
+ value: `${state.settings.model.provider} / ${state.settings.model.model || "—"}`,
2163
+ extra: credential.label
2164
+ },
2165
+ {
2166
+ ok: repo.path.trim() !== "",
2167
+ label: t("overview.ready.repo"),
2168
+ value: repo.path.trim() === "" ? t("repo.path.empty") : repo.path,
2169
+ extra: repo.commitMode === "direct" ? t("repo.commitMode.direct") : t("repo.commitMode.branch")
2170
+ },
2171
+ {
2172
+ ok: state.github.configured,
2173
+ label: t("overview.ready.github"),
2174
+ value: state.github.configured ? t("auth.configured") : t("auth.missing"),
2175
+ extra: `${state.github.authMode}${state.github.source === "" ? "" : ` · ${state.github.source}`}`
2176
+ }
2177
+ ];
2178
+ }
2179
+ function OverviewView(props) {
2180
+ const { t, lang, store, controller } = props;
2181
+ const state = store.state;
2182
+ if (state === null) {
2183
+ return h("div", { className: "dshgt-empty" }, store.error ?? t("quickfill.loading"));
2184
+ }
2185
+ const last = state.status.lastResult ?? state.status.history[0] ?? null;
2186
+ const pr = prLabel(t, state, last);
2187
+ const rows = readyRows(t, state);
2188
+ const readyCount = rows.filter((row) => row.ok).length;
2189
+ const timeZone = state.schedule.timezone;
2190
+ const today = zonedDateString(/* @__PURE__ */ new Date(), timeZone);
2191
+ const days = buildDigestDates(state.status.history, { timeZone });
2192
+ const byDate = new Map(days.map((day) => [day.date, day]));
2193
+ const week = recentDates(today, 7);
2194
+ return h(
2195
+ "div",
2196
+ { className: "dshgt-view dshgt-overview" },
2197
+ controller.running ? h(LiveRun, { t, controller, startedAt: controller.startedAt }) : null,
2198
+ controller.error === null ? null : h("div", { className: "dshgt-alert err" }, controller.error),
2199
+ last === null ? h(
2200
+ "section",
2201
+ { className: "dshgt-delivery" },
2202
+ h("div", { className: "dshgt-delivery-top" }, h("span", { className: "dshgt-hint" }, t("overview.last.none")))
2203
+ ) : h(
2204
+ "section",
2205
+ { className: "dshgt-delivery" },
1409
2206
  h(
1410
- Card,
1411
- { title: t("run.now"), hint: "spawn → 单次运行 → 退出" },
2207
+ "div",
2208
+ { className: "dshgt-delivery-top" },
2209
+ statusPill(t, last),
2210
+ h("span", { className: "dshgt-hint" }, last.date ?? localTime(last.at)),
1412
2211
  h(
1413
- Inline,
1414
- null,
1415
- h(Btn, { variant: "primary", onClick: () => void run(), disabled: busy || running }, t("run.button")),
1416
- h(Toggle, { checked: dryRun, label: t("run.dryRun"), onChange: setDryRun }),
1417
- running || busy ? h(Btn, { variant: "danger", onClick: () => void cancel() }, t("run.cancel")) : null
2212
+ "span",
2213
+ { className: "dshgt-pill-mini" },
2214
+ `${t("overview.last.files", { count: last.files })} · ${duration(last.durationMs)}`
1418
2215
  ),
1419
- pendingPr === null ? null : h(
1420
- Alert,
1421
- { kind: "warn", title: t("run.pendingPr") },
1422
- h("div", null, h("a", { href: pendingPr.url, target: "_blank", rel: "noreferrer" }, pendingPr.url)),
1423
- h("div", { className: "dshgt-hint" }, t("run.pendingPr.hint"))
2216
+ last.legacy === true ? h("span", { className: "dshgt-tag" }, t("runs.legacy")) : null
2217
+ ),
2218
+ h("h2", null, t("overview.last.digest", { date: last.date ?? localTime(last.at) })),
2219
+ h(
2220
+ "div",
2221
+ { className: "dshgt-when" },
2222
+ last.branch === void 0 ? null : h("span", null, `${t("run.usage.branch")} `, h("span", { className: "dshgt-mono" }, last.branch)),
2223
+ last.commit === void 0 ? null : h("span", null, `${t("run.usage.commit")} `, h("span", { className: "dshgt-mono" }, last.commit)),
2224
+ pr === null ? null : pr.url === void 0 ? h("span", { className: "dshgt-hint" }, pr.label) : h("a", { href: pr.url, target: "_blank", rel: "noreferrer" }, pr.label)
2225
+ ),
2226
+ last.ok ? null : h(
2227
+ "div",
2228
+ { className: "dshgt-alert err dshgt-delivery-error" },
2229
+ h("b", null, `${t("error.prefix")} ${last.errorCode ?? "INTERNAL"}`),
2230
+ h("div", null, last.errorMessage ?? t("state.failed")),
2231
+ last.errorReason === void 0 ? null : h("div", { className: "dshgt-hint" }, reasonHint(t, last.errorReason) ?? "")
2232
+ ),
2233
+ h(
2234
+ "div",
2235
+ { className: "dshgt-artifacts" },
2236
+ last.preview === void 0 ? h("span", { className: "dshgt-hint" }, t("overview.last.noPreview")) : h(
2237
+ "button",
2238
+ {
2239
+ type: "button",
2240
+ className: "dshgt-filechip",
2241
+ onClick: () => props.onOpenDigest(last.date)
2242
+ },
2243
+ h("span", { className: "dshgt-mono" }, last.preview.file)
1424
2244
  ),
1425
- error === null ? null : h(Alert, { kind: "err" }, error),
1426
- h(
1427
- "ul",
1428
- { className: "dshgt-steps" },
1429
- STEP_KEYS.map((key) => {
1430
- const stepState = running || busy ? CORE_STEPS.has(key) ? "pending" : key === "resolve" || key === "handshake" || key === "spawn" ? "active" : "pending" : shown === null ? "pending" : failedStep === key ? "failed" : "done";
1431
- const label = { done: "✓", active: "…", failed: "✗", pending: "—" }[stepState];
1432
- return h("li", { key, className: "dshgt-step", "data-state": stepState }, h("span", null, t(`step.${key}`)), h("span", { className: "st" }, label));
1433
- })
2245
+ last.preview === void 0 ? null : h(Btn, { small: true, onClick: () => props.onOpenDigest(last.date) }, t("overview.last.read")),
2246
+ pr === null || pr.url === void 0 ? null : h(
2247
+ "a",
2248
+ { className: "dshgt-btn dshgt-btn-sm", href: pr.url, target: "_blank", rel: "noreferrer" },
2249
+ t("digest.openPr")
1434
2250
  )
1435
2251
  ),
1436
- last === null ? h(Card, { title: t("run.last.title") }, h("p", { className: "dshgt-hint" }, t("run.last.none"))) : h(
1437
- Card,
1438
- { title: t("run.last.title"), extra: h(Pill, { kind: last.ok ? last.skipped ? "warn" : "ok" : "err" }, last.skipped ? t("state.skipped") : last.ok ? t("state.ok") : t("state.failed")) },
1439
- h(KV, {
1440
- items: [
1441
- [t("col.date"), localTime(last.at)],
1442
- [t("run.usage.core"), last.coreVersion || "—"],
1443
- [t("run.usage.files"), String(last.files)],
1444
- [t("run.usage.duration"), duration(last.durationMs)],
1445
- [t("run.usage.hash"), h("span", { className: "dshgt-mono" }, shortHash(last.contentHash))],
1446
- [t("run.usage.branch"), h("span", { className: "dshgt-mono" }, last.branch ?? "—")],
1447
- [t("run.usage.pr"), last.prUrl === void 0 ? "—" : h("a", { href: last.prUrl, target: "_blank", rel: "noreferrer" }, last.prUrl)],
1448
- ["model", `${state.settings.model.provider} / ${state.settings.model.model || "—"}`]
1449
- ]
1450
- }),
1451
- last.warnings.length === 0 ? null : h("p", { className: "dshgt-hint" }, last.warnings.join(";"))
2252
+ h(
2253
+ "div",
2254
+ { className: "dshgt-facts" },
2255
+ h("div", { className: "dshgt-fact" }, h("div", { className: "dshgt-fact-k" }, t("run.usage.commit")), h("div", { className: "dshgt-fact-v" }, last.commit === void 0 ? "—" : last.commit)),
2256
+ h("div", { className: "dshgt-fact" }, h("div", { className: "dshgt-fact-k" }, t("run.usage.hash")), h("div", { className: "dshgt-fact-v" }, shortHash(last.contentHash))),
2257
+ h("div", { className: "dshgt-fact" }, h("div", { className: "dshgt-fact-k" }, t("run.usage.core")), h("div", { className: "dshgt-fact-v" }, last.coreVersion || "—")),
2258
+ h("div", { className: "dshgt-fact" }, h("div", { className: "dshgt-fact-k" }, t("overview.model")), h("div", { className: "dshgt-fact-v" }, last.model?.model ?? "—"))
2259
+ )
2260
+ ),
2261
+ h(
2262
+ "section",
2263
+ { className: "dshgt-block" },
2264
+ h(
2265
+ "div",
2266
+ { className: "dshgt-section-label" },
2267
+ t("overview.ready.title"),
2268
+ h("span", { className: "dshgt-mono dshgt-section-tail" }, `${readyCount} / ${rows.length}`)
2269
+ ),
2270
+ h(
2271
+ "div",
2272
+ { className: "dshgt-ready-list" },
2273
+ rows.map(
2274
+ (row) => h(
2275
+ "div",
2276
+ { className: "dshgt-ready-row", key: row.label },
2277
+ h("span", { className: `dshgt-lamp ${row.ok ? "ok" : "warn"}` }),
2278
+ h("span", { className: "dshgt-ready-label" }, row.label),
2279
+ h(
2280
+ "span",
2281
+ { className: "dshgt-ready-value" },
2282
+ row.value,
2283
+ row.extra === void 0 ? null : h("span", { className: "dshgt-ready-extra" }, `· ${row.extra}`)
2284
+ ),
2285
+ h("button", { type: "button", className: "dshgt-linkbtn", onClick: props.onOpenSettings }, t("overview.ready.view"))
2286
+ )
2287
+ )
2288
+ )
2289
+ ),
2290
+ h(
2291
+ "section",
2292
+ { className: "dshgt-block" },
2293
+ h(
2294
+ "div",
2295
+ { className: "dshgt-section-label" },
2296
+ t("overview.week.title"),
2297
+ h("button", { type: "button", className: "dshgt-linkbtn dshgt-section-tail", onClick: props.onOpenRuns }, t("overview.week.all"))
1452
2298
  ),
1453
- shown !== null && shown.error !== void 0 ? h(
1454
- Card,
1455
- { title: t("run.error.title") },
2299
+ h(
2300
+ "div",
2301
+ { className: "dshgt-week" },
2302
+ week.map((date) => {
2303
+ const day = byDate.get(date);
2304
+ if (day === void 0) {
2305
+ const waiting = date === today && state.schedule.enabled;
2306
+ return h(
2307
+ "div",
2308
+ { className: "dshgt-day", key: date },
2309
+ h("div", { className: "dshgt-day-date" }, date.slice(5).replace("-", "/")),
2310
+ h("div", { className: "dshgt-day-status" }, waiting ? t("overview.week.wait", { time: state.schedule.time }) : t("overview.week.none"))
2311
+ );
2312
+ }
2313
+ return h(
2314
+ "button",
2315
+ {
2316
+ type: "button",
2317
+ className: "dshgt-day dshgt-day-clickable",
2318
+ key: date,
2319
+ onClick: () => props.onOpenDigest(date),
2320
+ title: day.record.errorCode === void 0 ? formatDayLabel(date, lang) : `${formatDayLabel(date, lang)} · ${day.record.errorCode}`
2321
+ },
2322
+ h("div", { className: "dshgt-day-date" }, date.slice(5).replace("-", "/")),
2323
+ h(
2324
+ "div",
2325
+ { className: "dshgt-day-status" },
2326
+ h("span", { className: `dshgt-lamp ${STATUS_LAMP[day.status]}` }),
2327
+ dayLabel(t, day)
2328
+ )
2329
+ );
2330
+ })
2331
+ )
2332
+ )
2333
+ );
2334
+ }
2335
+
2336
+ // client/runs.ts
2337
+ var FILTERS = [
2338
+ { id: "all", key: "runs.filter.all" },
2339
+ { id: "failed", key: "runs.filter.failed" },
2340
+ { id: "dry", key: "runs.filter.dry" }
2341
+ ];
2342
+ function recordSummary(t, record) {
2343
+ if (!record.ok) {
2344
+ return [record.errorCode, record.errorMessage].filter((item) => item !== void 0 && item !== "").join(" · ") || t("state.failed");
2345
+ }
2346
+ if (record.skipped) return skipHint(t, record.skipReason, t("runs.noChange"));
2347
+ if (record.kind === "dry-run") return t("runs.dryHint");
2348
+ if (record.branch !== void 0) return record.branch;
2349
+ if (record.commit !== void 0) return record.commit;
2350
+ if (record.prUrl !== void 0) return t("runs.prCreated");
2351
+ return "—";
2352
+ }
2353
+ function prStatus(t, state, record) {
2354
+ if (record.prUrl === void 0) return null;
2355
+ const pending = state.status.pendingPr;
2356
+ if (pending !== null && pending.url === record.prUrl) {
2357
+ return { label: state.status.pendingPrState === "unknown" ? t("runs.prUnknown") : t("runs.pendingPr"), kind: "warn" };
2358
+ }
2359
+ return { label: t("runs.prCreated"), kind: "ok" };
2360
+ }
2361
+ function LogDrawer(props) {
2362
+ const { t, runId } = props;
2363
+ const [loading, setLoading] = import_react.default.useState(true);
2364
+ const [text, setText] = import_react.default.useState("");
2365
+ const [error, setError] = import_react.default.useState(null);
2366
+ const [copied, setCopied] = import_react.default.useState(false);
2367
+ import_react.default.useEffect(() => {
2368
+ const controller = new AbortController();
2369
+ setLoading(true);
2370
+ setError(null);
2371
+ setText("");
2372
+ getLog(runId, controller.signal).then((payload) => {
2373
+ if (controller.signal.aborted) return;
2374
+ setText(payload.text);
2375
+ setLoading(false);
2376
+ }).catch((failure) => {
2377
+ if (controller.signal.aborted) return;
2378
+ setError(failure instanceof Error ? failure.message : String(failure));
2379
+ setLoading(false);
2380
+ });
2381
+ return () => controller.abort();
2382
+ }, [runId]);
2383
+ import_react.default.useEffect(() => {
2384
+ const onKey = (event) => {
2385
+ if (event.key === "Escape") props.onClose();
2386
+ };
2387
+ document.addEventListener("keydown", onKey);
2388
+ return () => document.removeEventListener("keydown", onKey);
2389
+ }, [props]);
2390
+ return h(
2391
+ "div",
2392
+ { className: "dshgt-modal-mask", onClick: (event) => {
2393
+ if (event.target === event.currentTarget) props.onClose();
2394
+ } },
2395
+ h(
2396
+ "div",
2397
+ { className: "dshgt-modal dshgt-log-modal", role: "dialog", "aria-modal": "true" },
2398
+ h(
2399
+ "div",
2400
+ { className: "dshgt-log-head" },
2401
+ h("h3", { className: "dshgt-log-title" }, t("log.title")),
2402
+ h("span", { className: "dshgt-hint dshgt-mono" }, runId),
2403
+ h(Btn, { small: true, onClick: () => props.onClose() }, t("log.close"))
2404
+ ),
2405
+ loading ? h("p", { className: "dshgt-hint" }, t("log.loading")) : null,
2406
+ error === null ? null : h(Alert, { kind: "err" }, error),
2407
+ loading || error !== null ? null : text === "" ? h("p", { className: "dshgt-hint" }, t("log.empty")) : h("pre", { className: "dshgt-log-pre" }, text),
2408
+ h(
2409
+ "div",
2410
+ { className: "dshgt-log-foot" },
1456
2411
  h(
1457
- Alert,
1458
- { kind: "err", title: `${t("error.prefix")} ${shown.error.code}` },
1459
- h("div", null, shown.error.message),
1460
- h("div", { className: "dshgt-hint" }, shown.error.hint),
1461
- shown.error.inferred ? h("div", { className: "dshgt-hint" }, "(错误码由插件按核心 stderr 推断)") : null
2412
+ Btn,
2413
+ {
2414
+ small: true,
2415
+ disabled: text === "",
2416
+ onClick: () => {
2417
+ void copyText(text).then((okResult) => {
2418
+ if (!okResult) return;
2419
+ setCopied(true);
2420
+ setTimeout(() => setCopied(false), 1600);
2421
+ });
2422
+ }
2423
+ },
2424
+ copied ? t("log.copied") : t("log.copy")
1462
2425
  ),
1463
- shown.logFile === void 0 ? null : h("p", { className: "dshgt-hint" }, `日志:${shown.logFile}`)
1464
- ) : null
1465
- ) : h(HistoryTable, { t, records: state.status.history })
2426
+ h("span", { className: "dshgt-hint" }, t("log.truncated"))
2427
+ )
2428
+ )
1466
2429
  );
1467
2430
  }
1468
- function stepFromError(area) {
1469
- switch (area) {
1470
- case "core":
1471
- return "resolve";
1472
- case "model":
1473
- return "summarize";
1474
- case "repo":
1475
- case "auth":
1476
- return "commit";
1477
- default:
1478
- return "state";
2431
+ function RunsView(props) {
2432
+ const { t, store, controller } = props;
2433
+ const [filter, setFilter] = import_react.default.useState("all");
2434
+ const [openId, setOpenId] = import_react.default.useState(null);
2435
+ const [logId, setLogId] = import_react.default.useState(null);
2436
+ const state = store.state;
2437
+ if (state === null) {
2438
+ return h("div", { className: "dshgt-empty" }, store.error ?? t("quickfill.loading"));
1479
2439
  }
2440
+ const history = state.status.history;
2441
+ const records = history.filter((record) => {
2442
+ if (filter === "failed") return !record.ok;
2443
+ if (filter === "dry") return record.kind === "dry-run";
2444
+ return true;
2445
+ });
2446
+ return h(
2447
+ "div",
2448
+ { className: "dshgt-view dshgt-runs" },
2449
+ controller.running ? h(LiveRun, { t, controller, startedAt: controller.startedAt }) : null,
2450
+ controller.error === null ? null : h("div", { className: "dshgt-alert err" }, controller.error),
2451
+ h(
2452
+ "div",
2453
+ { className: "dshgt-runs-head" },
2454
+ h("span", { className: "dshgt-hint" }, t("runs.count", { count: history.length })),
2455
+ h(
2456
+ "div",
2457
+ { className: "dshgt-runs-filter" },
2458
+ FILTERS.map(
2459
+ (item) => h(
2460
+ "button",
2461
+ {
2462
+ key: item.id,
2463
+ type: "button",
2464
+ className: "dshgt-chip",
2465
+ "aria-pressed": filter === item.id ? "true" : "false",
2466
+ onClick: () => setFilter(item.id)
2467
+ },
2468
+ t(item.key)
2469
+ )
2470
+ )
2471
+ )
2472
+ ),
2473
+ records.length === 0 ? h("div", { className: "dshgt-empty" }, t("runs.empty")) : h(
2474
+ "div",
2475
+ { className: "dshgt-run-list" },
2476
+ records.map((record) => {
2477
+ const open = openId === record.id;
2478
+ const pr = prStatus(t, state, record);
2479
+ return h(
2480
+ "div",
2481
+ { className: `dshgt-run-item${open ? " open" : ""}`, key: record.id },
2482
+ h(
2483
+ "button",
2484
+ {
2485
+ type: "button",
2486
+ className: "dshgt-run-row",
2487
+ "aria-expanded": open ? "true" : "false",
2488
+ onClick: () => setOpenId(open ? null : record.id)
2489
+ },
2490
+ h("span", { className: `dshgt-lamp ${record.ok ? record.skipped || record.kind === "dry-run" ? "warn" : "ok" : "err"}` }),
2491
+ h("span", { className: "dshgt-run-date" }, shortTime(record.at)),
2492
+ h(
2493
+ "span",
2494
+ { className: "dshgt-run-what" },
2495
+ record.kind === "dry-run" ? t("state.dry") : record.skipped ? t("state.skipped") : record.ok ? t("state.ok") : t("state.failed"),
2496
+ h("span", { className: "dshgt-run-muted" }, recordSummary(t, record)),
2497
+ record.legacy === true ? h("span", { className: "dshgt-tag dshgt-inline-tag" }, t("runs.legacy")) : null
2498
+ ),
2499
+ h("span", { className: "dshgt-run-files" }, t("runs.files", { count: record.files })),
2500
+ h("span", { className: "dshgt-run-duration" }, duration(record.durationMs)),
2501
+ h("span", { className: "dshgt-run-chev" }, "›")
2502
+ ),
2503
+ open ? h(
2504
+ "div",
2505
+ { className: "dshgt-run-detail" },
2506
+ !record.ok ? h(
2507
+ Alert,
2508
+ { kind: "err", title: `${t("error.prefix")} ${record.errorCode ?? "INTERNAL"}` },
2509
+ h("div", null, record.errorMessage ?? t("state.failed")),
2510
+ record.errorReason === void 0 ? null : h("div", { className: "dshgt-hint" }, reasonHint(t, record.errorReason) ?? "")
2511
+ ) : null,
2512
+ h(
2513
+ "div",
2514
+ { className: "dshgt-detail-grid" },
2515
+ detailItem(t, "run.usage.commit", record.commit === void 0 ? "—" : h("span", { className: "dshgt-mono" }, record.commit)),
2516
+ detailItem(t, "run.usage.hash", h("span", { className: "dshgt-mono" }, shortHash(record.contentHash))),
2517
+ detailItem(t, "run.usage.branch", record.branch === void 0 ? "—" : h("span", { className: "dshgt-mono" }, record.branch)),
2518
+ detailItem(t, "runs.detail.coreModel", `${record.coreVersion || "—"} · ${record.model?.model ?? "—"}`),
2519
+ detailItem(t, "runs.detail.date", record.date ?? "—"),
2520
+ detailItem(
2521
+ t,
2522
+ "run.usage.pr",
2523
+ record.prUrl === void 0 ? "—" : h(
2524
+ "span",
2525
+ { className: "dshgt-inline" },
2526
+ h("a", { href: record.prUrl, target: "_blank", rel: "noreferrer", title: record.prUrl }, t("digest.openPr")),
2527
+ pr === null ? null : h("span", { className: "dshgt-tag" }, pr.label)
2528
+ )
2529
+ ),
2530
+ detailItem(t, "runs.detail.warnings", record.warnings.length === 0 ? t("runs.detail.none") : record.warnings.join(";")),
2531
+ detailItem(t, "runs.detail.files", `${record.files} · ${duration(record.durationMs)}`)
2532
+ ),
2533
+ record.legacy === true ? h("div", { className: "dshgt-hint" }, t("runs.legacyHint")) : null,
2534
+ h(
2535
+ "div",
2536
+ { className: "dshgt-run-actions" },
2537
+ record.preview === void 0 ? h("span", { className: "dshgt-hint" }, t("runs.detail.noPreview")) : h(Btn, { small: true, onClick: () => props.onOpenDigest(record.date) }, t("runs.detail.readDigest")),
2538
+ h(Btn, { small: true, onClick: () => setLogId(record.id) }, t("runs.detail.log")),
2539
+ record.preview === void 0 ? null : h("span", { className: "dshgt-hint dshgt-mono" }, record.preview.file)
2540
+ )
2541
+ ) : null
2542
+ );
2543
+ })
2544
+ ),
2545
+ logId === null ? null : h(LogDrawer, { t, runId: logId, onClose: () => setLogId(null) })
2546
+ );
1480
2547
  }
1481
- function HistoryTable(props) {
1482
- const { t, records } = props;
2548
+ function detailItem(t, label, value) {
1483
2549
  return h(
1484
- Card,
1485
- { title: t("run.history.title"), hint: t("run.history.hint") },
1486
- records.length === 0 ? h("p", { className: "dshgt-hint" }, t("run.history.empty")) : h(
1487
- "table",
1488
- { className: "dshgt-table" },
1489
- h("thead", null, h("tr", null, [t("col.date"), t("col.result"), t("col.files"), t("col.duration"), t("col.commit"), t("col.error")].map((label) => h("th", { key: label }, label)))),
2550
+ "div",
2551
+ { className: "dshgt-detail-item" },
2552
+ h("div", { className: "dshgt-detail-k" }, t(label)),
2553
+ h("div", { className: "dshgt-detail-v" }, value)
2554
+ );
2555
+ }
2556
+
2557
+ // client/markdown-core.ts
2558
+ function isSafeUrl(value) {
2559
+ try {
2560
+ const url = new URL(value);
2561
+ return url.protocol === "http:" || url.protocol === "https:";
2562
+ } catch {
2563
+ return false;
2564
+ }
2565
+ }
2566
+ function splitFrontmatter(markdown) {
2567
+ const normalized = markdown.replace(/\r\n?/g, "\n");
2568
+ const lines = normalized.split("\n");
2569
+ if ((lines[0] ?? "").trim() !== "---") return { meta: {}, body: normalized };
2570
+ let end = -1;
2571
+ for (let index = 1; index < lines.length; index += 1) {
2572
+ if ((lines[index] ?? "").trim() === "---") {
2573
+ end = index;
2574
+ break;
2575
+ }
2576
+ }
2577
+ if (end < 0) return { meta: {}, body: normalized };
2578
+ const meta = {};
2579
+ for (const line of lines.slice(1, end)) {
2580
+ const colon = line.indexOf(":");
2581
+ if (colon <= 0) continue;
2582
+ const key = line.slice(0, colon).trim();
2583
+ let value = line.slice(colon + 1).trim();
2584
+ if (value.length >= 2 && (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))) {
2585
+ value = value.slice(1, -1);
2586
+ }
2587
+ if (key !== "") meta[key] = value;
2588
+ }
2589
+ return { meta, body: lines.slice(end + 1).join("\n") };
2590
+ }
2591
+ function pushText(nodes, value) {
2592
+ if (value === "") return;
2593
+ const last = nodes[nodes.length - 1];
2594
+ if (last !== void 0 && last.type === "text") {
2595
+ last.value += value;
2596
+ return;
2597
+ }
2598
+ nodes.push({ type: "text", value });
2599
+ }
2600
+ var INLINE_PATTERN = /(\*\*[^*\n]+\*\*)|(__[^_\n]+__)|(`[^`\n]+`)|(!?\[[^\]\n]*\]\([^)\s]+\))/g;
2601
+ function parseInline(text) {
2602
+ const nodes = [];
2603
+ let cursor = 0;
2604
+ const pattern = new RegExp(INLINE_PATTERN.source, "g");
2605
+ let match;
2606
+ while ((match = pattern.exec(text)) !== null) {
2607
+ if (match.index > cursor) pushText(nodes, text.slice(cursor, match.index));
2608
+ const token = match[0];
2609
+ if (token.startsWith("**") || token.startsWith("__")) {
2610
+ nodes.push({ type: "strong", children: parseInline(token.slice(2, -2)) });
2611
+ } else if (token.startsWith("`")) {
2612
+ nodes.push({ type: "code", value: token.slice(1, -1) });
2613
+ } else {
2614
+ const image = token.startsWith("!");
2615
+ const link = /^!?\[([^\]]*)\]\(([^)]*)\)$/.exec(token);
2616
+ if (link !== null) {
2617
+ const label = link[1] ?? "";
2618
+ const href = (link[2] ?? "").trim();
2619
+ if (image) pushText(nodes, label);
2620
+ else if (isSafeUrl(href)) nodes.push({ type: "link", href, children: parseInline(label) });
2621
+ else pushText(nodes, label);
2622
+ } else {
2623
+ pushText(nodes, token);
2624
+ }
2625
+ }
2626
+ cursor = pattern.lastIndex;
2627
+ }
2628
+ if (cursor < text.length) pushText(nodes, text.slice(cursor));
2629
+ return nodes;
2630
+ }
2631
+ var FENCE = /^\s*```\s*([^\s`]*)\s*$/;
2632
+ var HEADING = /^(#{1,6})\s+(.*)$/;
2633
+ var HR = /^\s*(?:-{3,}|\*{3,}|_{3,})\s*$/;
2634
+ var QUOTE = /^\s*>\s?(.*)$/;
2635
+ var BULLET = /^\s*[-*+]\s+(.*)$/;
2636
+ var ORDERED = /^\s*\d+[.)]\s+(.*)$/;
2637
+ var TABLE_DELIM_CELL = /^:?-+:?$/;
2638
+ function isBlockStart(line) {
2639
+ return FENCE.test(line) || HEADING.test(line) || HR.test(line) || QUOTE.test(line) || BULLET.test(line) || ORDERED.test(line);
2640
+ }
2641
+ function splitTableRow(line) {
2642
+ const cells = [];
2643
+ let current = "";
2644
+ let inCode = false;
2645
+ const text = line.trim();
2646
+ for (let cursor = 0; cursor < text.length; cursor += 1) {
2647
+ const char = text[cursor] ?? "";
2648
+ if (char === "\\" && text[cursor + 1] === "|") {
2649
+ current += "|";
2650
+ cursor += 1;
2651
+ continue;
2652
+ }
2653
+ if (char === "`") {
2654
+ inCode = !inCode;
2655
+ current += char;
2656
+ continue;
2657
+ }
2658
+ if (char === "|" && !inCode) {
2659
+ cells.push(current);
2660
+ current = "";
2661
+ continue;
2662
+ }
2663
+ current += char;
2664
+ }
2665
+ cells.push(current);
2666
+ if (cells.length > 1 && (cells[0] ?? "").trim() === "") cells.shift();
2667
+ if (cells.length > 1 && (cells[cells.length - 1] ?? "").trim() === "") cells.pop();
2668
+ return cells.map((cell) => cell.trim());
2669
+ }
2670
+ function parseAlignRow(line) {
2671
+ const cells = splitTableRow(line);
2672
+ if (cells.length === 0) return null;
2673
+ const align = [];
2674
+ for (const cell of cells) {
2675
+ const value = cell.trim();
2676
+ if (!TABLE_DELIM_CELL.test(value)) return null;
2677
+ const left = value.startsWith(":");
2678
+ const right = value.endsWith(":");
2679
+ align.push(left && right ? "center" : right ? "right" : left ? "left" : void 0);
2680
+ }
2681
+ return align;
2682
+ }
2683
+ function tableAt(lines, index) {
2684
+ const headerLine = lines[index] ?? "";
2685
+ if (!headerLine.includes("|")) return null;
2686
+ const align = parseAlignRow(lines[index + 1] ?? "");
2687
+ if (align === null) return null;
2688
+ const headerCells = splitTableRow(headerLine);
2689
+ if (headerCells.length === 0 || headerCells.length !== align.length) return null;
2690
+ const rows = [];
2691
+ let cursor = index + 2;
2692
+ while (cursor < lines.length) {
2693
+ const line = lines[cursor] ?? "";
2694
+ if (line.trim() === "" || !line.includes("|")) break;
2695
+ if (isBlockStart(line)) break;
2696
+ const cells = splitTableRow(line).slice(0, headerCells.length);
2697
+ while (cells.length < headerCells.length) cells.push("");
2698
+ rows.push(cells.map((cell) => parseInline(cell)));
2699
+ cursor += 1;
2700
+ }
2701
+ return { align, header: headerCells.map((cell) => parseInline(cell)), rows, next: cursor };
2702
+ }
2703
+ function parseBlocks(body) {
2704
+ const lines = body.split("\n");
2705
+ const blocks = [];
2706
+ let index = 0;
2707
+ while (index < lines.length) {
2708
+ const line = lines[index] ?? "";
2709
+ if (line.trim() === "") {
2710
+ index += 1;
2711
+ continue;
2712
+ }
2713
+ const fence = FENCE.exec(line);
2714
+ if (fence !== null) {
2715
+ const lang = fence[1] ?? "";
2716
+ index += 1;
2717
+ const buffer2 = [];
2718
+ while (index < lines.length) {
2719
+ const current = lines[index] ?? "";
2720
+ if (/^\s*```\s*$/.test(current)) {
2721
+ index += 1;
2722
+ break;
2723
+ }
2724
+ buffer2.push(current);
2725
+ index += 1;
2726
+ }
2727
+ blocks.push({ type: "code", value: buffer2.join("\n"), ...lang === "" ? {} : { lang } });
2728
+ continue;
2729
+ }
2730
+ const heading = HEADING.exec(line);
2731
+ if (heading !== null) {
2732
+ blocks.push({ type: "heading", level: (heading[1] ?? "#").length, children: parseInline((heading[2] ?? "").trim()) });
2733
+ index += 1;
2734
+ continue;
2735
+ }
2736
+ if (HR.test(line)) {
2737
+ blocks.push({ type: "hr" });
2738
+ index += 1;
2739
+ continue;
2740
+ }
2741
+ if (QUOTE.test(line)) {
2742
+ const buffer2 = [];
2743
+ while (index < lines.length) {
2744
+ const match = QUOTE.exec(lines[index] ?? "");
2745
+ if (match === null) break;
2746
+ buffer2.push(match[1] ?? "");
2747
+ index += 1;
2748
+ }
2749
+ blocks.push({ type: "quote", children: parseInline(buffer2.join(" ").trim()) });
2750
+ continue;
2751
+ }
2752
+ if (BULLET.test(line)) {
2753
+ const items = [];
2754
+ while (index < lines.length) {
2755
+ const match = BULLET.exec(lines[index] ?? "");
2756
+ if (match === null) break;
2757
+ items.push(parseInline((match[1] ?? "").trim()));
2758
+ index += 1;
2759
+ }
2760
+ blocks.push({ type: "list", ordered: false, items });
2761
+ continue;
2762
+ }
2763
+ if (ORDERED.test(line)) {
2764
+ const items = [];
2765
+ while (index < lines.length) {
2766
+ const match = ORDERED.exec(lines[index] ?? "");
2767
+ if (match === null) break;
2768
+ items.push(parseInline((match[1] ?? "").trim()));
2769
+ index += 1;
2770
+ }
2771
+ blocks.push({ type: "list", ordered: true, items });
2772
+ continue;
2773
+ }
2774
+ const table = tableAt(lines, index);
2775
+ if (table !== null) {
2776
+ blocks.push({ type: "table", align: table.align, header: table.header, rows: table.rows });
2777
+ index = table.next;
2778
+ continue;
2779
+ }
2780
+ const buffer = [];
2781
+ while (index < lines.length) {
2782
+ const current = lines[index] ?? "";
2783
+ if (current.trim() === "") break;
2784
+ if (buffer.length > 0 && (isBlockStart(current) || tableAt(lines, index) !== null)) break;
2785
+ buffer.push(current.trim());
2786
+ index += 1;
2787
+ }
2788
+ blocks.push({ type: "paragraph", children: parseInline(buffer.join(" ")) });
2789
+ }
2790
+ return blocks;
2791
+ }
2792
+ function parseMarkdown(markdown) {
2793
+ const { meta, body } = splitFrontmatter(markdown);
2794
+ return { meta, blocks: parseBlocks(body) };
2795
+ }
2796
+
2797
+ // client/markdown.ts
2798
+ function renderInline(nodes, prefix) {
2799
+ return nodes.map((node, index) => {
2800
+ const key = `${prefix}-${index}`;
2801
+ switch (node.type) {
2802
+ case "text":
2803
+ return node.value;
2804
+ case "strong":
2805
+ return h("strong", { key }, renderInline(node.children, key));
2806
+ case "code":
2807
+ return h("code", { key, className: "dshgt-md-code" }, node.value);
2808
+ case "link":
2809
+ return h(
2810
+ "a",
2811
+ { key, href: node.href, target: "_blank", rel: "noreferrer" },
2812
+ renderInline(node.children, key)
2813
+ );
2814
+ }
2815
+ });
2816
+ }
2817
+ function alignStyle(align) {
2818
+ return align === void 0 ? void 0 : { textAlign: align };
2819
+ }
2820
+ function renderBlock(block, key) {
2821
+ switch (block.type) {
2822
+ case "heading": {
2823
+ const level = Math.min(Math.max(block.level, 1), 6);
2824
+ return h(`h${level}`, { key, className: `dshgt-md-h dshgt-md-h${level}` }, renderInline(block.children, key));
2825
+ }
2826
+ case "paragraph":
2827
+ return h("p", { key, className: "dshgt-md-p" }, renderInline(block.children, key));
2828
+ case "list":
2829
+ return h(
2830
+ block.ordered ? "ol" : "ul",
2831
+ { key, className: "dshgt-md-list" },
2832
+ block.items.map((item, index) => h("li", { key: `${key}-${index}` }, renderInline(item, `${key}-${index}`)))
2833
+ );
2834
+ case "quote":
2835
+ return h("blockquote", { key, className: "dshgt-md-quote" }, renderInline(block.children, key));
2836
+ case "code":
2837
+ return h(
2838
+ "pre",
2839
+ { key, className: "dshgt-md-pre", ...block.lang === void 0 ? {} : { "data-lang": block.lang } },
2840
+ h("code", null, block.value)
2841
+ );
2842
+ case "hr":
2843
+ return h("hr", { key, className: "dshgt-md-hr" });
2844
+ case "table":
2845
+ return h(
2846
+ "div",
2847
+ { key, className: "dshgt-md-tablewrap" },
2848
+ h(
2849
+ "table",
2850
+ { className: "dshgt-md-table" },
2851
+ h(
2852
+ "thead",
2853
+ null,
2854
+ h(
2855
+ "tr",
2856
+ null,
2857
+ block.header.map(
2858
+ (cell, index) => h("th", { key: `h${index}`, style: alignStyle(block.align[index]) }, renderInline(cell, `${key}-h${index}`))
2859
+ )
2860
+ )
2861
+ ),
2862
+ h(
2863
+ "tbody",
2864
+ null,
2865
+ block.rows.map(
2866
+ (row, rowIndex) => h(
2867
+ "tr",
2868
+ { key: `r${rowIndex}` },
2869
+ row.map(
2870
+ (cell, cellIndex) => h(
2871
+ "td",
2872
+ { key: `c${cellIndex}`, style: alignStyle(block.align[cellIndex]) },
2873
+ renderInline(cell, `${key}-r${rowIndex}c${cellIndex}`)
2874
+ )
2875
+ )
2876
+ )
2877
+ )
2878
+ )
2879
+ )
2880
+ );
2881
+ }
2882
+ }
2883
+ function renderMarkdown(markdown) {
2884
+ const parsed = parseMarkdown(markdown);
2885
+ return {
2886
+ meta: parsed.meta,
2887
+ nodes: parsed.blocks.map((block, index) => renderBlock(block, `b${index}`))
2888
+ };
2889
+ }
2890
+
2891
+ // client/digest.ts
2892
+ var STATUS_TAG = {
2893
+ preview: "digest.tag.preview",
2894
+ failed: "digest.tag.failed",
2895
+ skipped: "digest.tag.skipped",
2896
+ dry: "digest.tag.dry",
2897
+ legacy: "digest.tag.legacy",
2898
+ missing: "digest.tag.missing"
2899
+ };
2900
+ var STATUS_LABEL = {
2901
+ preview: "state.ok",
2902
+ failed: "state.failed",
2903
+ skipped: "state.skipped",
2904
+ dry: "state.dry",
2905
+ legacy: "digest.tag.legacy",
2906
+ missing: "digest.tag.missing"
2907
+ };
2908
+ var STATUS_LAMP2 = {
2909
+ preview: "ok",
2910
+ failed: "err",
2911
+ skipped: "warn",
2912
+ dry: "warn",
2913
+ legacy: "warn",
2914
+ missing: "warn"
2915
+ };
2916
+ var META_KEYS = ["date", "period", "languages", "count", "degraded", "coreVersion", "contentHash", "generatedAt"];
2917
+ function EmptyDigest(props) {
2918
+ const { t, day, nearest } = props;
2919
+ const status = props.error === void 0 ? day.status : "missing";
2920
+ const key = status === "legacy" ? "legacy" : status === "missing" ? "missing" : status;
2921
+ return h(
2922
+ "div",
2923
+ { className: "dshgt-digest-empty" },
2924
+ h(
2925
+ "div",
2926
+ { className: "dshgt-status-line" },
2927
+ h("span", { className: `dshgt-lamp ${STATUS_LAMP2[status]}` }),
2928
+ h("b", null, t(STATUS_LABEL[status])),
2929
+ h("span", { className: "dshgt-dim" }, formatDayLabel(day.date, props.lang))
2930
+ ),
2931
+ h("h2", null, t(`digest.empty.${key}.title`)),
2932
+ h("p", null, t(`digest.empty.${key}.reason`)),
2933
+ day.record.errorCode === void 0 && props.error === void 0 ? null : h(
2934
+ "p",
2935
+ { className: "dshgt-mono dshgt-digest-error" },
2936
+ [day.record.errorCode, day.record.errorMessage ?? props.error].filter((item) => item !== void 0 && item !== "").join(" · ")
2937
+ ),
2938
+ h(
2939
+ "div",
2940
+ { className: "dshgt-actions" },
2941
+ h(Btn, { small: true, onClick: props.onOpenRuns }, t("digest.empty.viewRuns")),
2942
+ nearest === void 0 ? null : h(Btn, { small: true, onClick: () => props.onSelectDate(nearest.date) }, t("digest.empty.latest", { date: formatDayLabel(nearest.date, props.lang) }))
2943
+ )
2944
+ );
2945
+ }
2946
+ function DigestView(props) {
2947
+ const { t, lang, store, selectedDate } = props;
2948
+ const [mode, setMode] = import_react.default.useState("render");
2949
+ const [payload, setPayload] = import_react.default.useState(null);
2950
+ const [loading, setLoading] = import_react.default.useState(false);
2951
+ const [error, setError] = import_react.default.useState(null);
2952
+ const [copied, setCopied] = import_react.default.useState(false);
2953
+ const state = store.state;
2954
+ const history = state?.status.history ?? [];
2955
+ const timeZone = state?.schedule.timezone ?? "UTC";
2956
+ const days = buildDigestDates(history, { timeZone });
2957
+ const defaultDate = days.find((item) => item.status === "preview")?.date ?? days[0]?.date;
2958
+ const effectiveDate = selectedDate !== void 0 && days.some((day2) => day2.date === selectedDate) ? selectedDate : defaultDate;
2959
+ const day = days.find((item) => item.date === effectiveDate);
2960
+ const index = day === void 0 ? -1 : days.indexOf(day);
2961
+ const previewCount = days.filter((item) => item.status === "preview").length;
2962
+ const previewRunId = day !== void 0 && day.status === "preview" && day.record.preview !== void 0 ? day.record.id : void 0;
2963
+ import_react.default.useEffect(() => {
2964
+ if (effectiveDate !== void 0 && effectiveDate !== selectedDate) props.onSelectDate(effectiveDate);
2965
+ }, [effectiveDate, selectedDate, props.onSelectDate]);
2966
+ import_react.default.useEffect(() => {
2967
+ setMode("render");
2968
+ }, [effectiveDate]);
2969
+ import_react.default.useEffect(() => {
2970
+ if (previewRunId === void 0) {
2971
+ setPayload(null);
2972
+ setError(null);
2973
+ setLoading(false);
2974
+ return () => void 0;
2975
+ }
2976
+ const controller = new AbortController();
2977
+ setLoading(true);
2978
+ setError(null);
2979
+ setPayload(null);
2980
+ getPreview(previewRunId, controller.signal).then((result) => {
2981
+ if (controller.signal.aborted) return;
2982
+ setPayload(result);
2983
+ setLoading(false);
2984
+ }).catch((failure) => {
2985
+ if (controller.signal.aborted) return;
2986
+ setError(failure instanceof Error ? failure.message : String(failure));
2987
+ setLoading(false);
2988
+ });
2989
+ return () => controller.abort();
2990
+ }, [previewRunId]);
2991
+ if (state === null) {
2992
+ return h("div", { className: "dshgt-empty" }, store.error ?? t("quickfill.loading"));
2993
+ }
2994
+ const nearest = day === void 0 ? void 0 : days.find((item) => item.status === "preview" && item.date !== day.date);
2995
+ let rendered = null;
2996
+ let parseFailed = false;
2997
+ if (payload !== null) {
2998
+ try {
2999
+ rendered = renderMarkdown(payload.preview.text);
3000
+ } catch {
3001
+ parseFailed = true;
3002
+ }
3003
+ }
3004
+ const subbar = h(
3005
+ "div",
3006
+ { className: "dshgt-digest-subbar" },
3007
+ h(
3008
+ "div",
3009
+ { className: "dshgt-datenav" },
1490
3010
  h(
1491
- "tbody",
1492
- null,
1493
- records.map(
1494
- (record) => h(
1495
- "tr",
1496
- { key: String(record.id) },
1497
- h("td", { className: "dshgt-mono" }, localTime(record.at)),
1498
- h("td", null, h(Pill, { kind: record.ok ? record.skipped ? "warn" : "ok" : "err" }, record.kind === "dry-run" ? t("state.dry") : record.skipped ? t("state.skipped") : record.ok ? t("state.ok") : t("state.failed"))),
1499
- h("td", { className: "dshgt-mono" }, String(record.files ?? 0)),
1500
- h("td", { className: "dshgt-mono" }, duration(Number(record.durationMs ?? 0))),
1501
- h("td", { className: "dshgt-mono" }, [record.branch, record.prUrl].filter(Boolean).join(" · ") || "—"),
1502
- h("td", { className: "dshgt-mono" }, record.errorCode ?? "—")
3011
+ "button",
3012
+ {
3013
+ type: "button",
3014
+ className: "dshgt-icon-btn",
3015
+ "aria-label": t("digest.prev"),
3016
+ title: t("digest.prev"),
3017
+ disabled: index < 0 || index >= days.length - 1,
3018
+ onClick: () => {
3019
+ const target = days[index + 1];
3020
+ if (target !== void 0) props.onSelectDate(target.date);
3021
+ }
3022
+ },
3023
+ "‹"
3024
+ ),
3025
+ h(
3026
+ "select",
3027
+ {
3028
+ className: "dshgt-dateselect",
3029
+ "aria-label": t("digest.select"),
3030
+ value: effectiveDate ?? "",
3031
+ onChange: (event) => props.onSelectDate(event.target.value),
3032
+ disabled: days.length === 0
3033
+ },
3034
+ days.map((item) => h("option", { key: item.date, value: item.date }, `${formatDayLabel(item.date, lang)} · ${t(STATUS_TAG[item.status])}`))
3035
+ ),
3036
+ h(
3037
+ "button",
3038
+ {
3039
+ type: "button",
3040
+ className: "dshgt-icon-btn",
3041
+ "aria-label": t("digest.next"),
3042
+ title: t("digest.next"),
3043
+ disabled: index <= 0,
3044
+ onClick: () => {
3045
+ const target = days[index - 1];
3046
+ if (target !== void 0) props.onSelectDate(target.date);
3047
+ }
3048
+ },
3049
+ "›"
3050
+ )
3051
+ ),
3052
+ h(
3053
+ "span",
3054
+ { className: "dshgt-dim" },
3055
+ days.length === 0 ? t("digest.window.empty") : t("digest.window", { days: days.length, previews: previewCount })
3056
+ ),
3057
+ h("span", { className: "dshgt-grow" }),
3058
+ h(Btn, { small: true, onClick: props.onOpenRuns }, t("digest.allRuns"))
3059
+ );
3060
+ if (day === void 0) {
3061
+ return h(
3062
+ "div",
3063
+ { className: "dshgt-view dshgt-digest" },
3064
+ subbar,
3065
+ h(
3066
+ "div",
3067
+ { className: "dshgt-digest-empty" },
3068
+ h("h2", null, t("digest.empty.none.title")),
3069
+ h("p", null, t("digest.empty.none.reason")),
3070
+ h("div", { className: "dshgt-actions" }, h(Btn, { small: true, onClick: props.onOpenRuns }, t("digest.empty.viewRuns")))
3071
+ )
3072
+ );
3073
+ }
3074
+ const showEmpty = day.status !== "preview" || error !== null;
3075
+ const record = day.record;
3076
+ return h(
3077
+ "div",
3078
+ { className: "dshgt-view dshgt-digest" },
3079
+ subbar,
3080
+ h(
3081
+ "div",
3082
+ { className: "dshgt-digest-sheet" },
3083
+ showEmpty ? h(EmptyDigest, {
3084
+ t,
3085
+ lang,
3086
+ day,
3087
+ ...nearest === void 0 ? {} : { nearest },
3088
+ ...error === null ? {} : { error },
3089
+ onOpenRuns: props.onOpenRuns,
3090
+ onSelectDate: props.onSelectDate
3091
+ }) : h(
3092
+ "div",
3093
+ { className: "dshgt-digest-body" },
3094
+ h(
3095
+ "div",
3096
+ { className: "dshgt-digest-bar" },
3097
+ h(
3098
+ "span",
3099
+ { className: "dshgt-filechip dshgt-filechip-static" },
3100
+ h("span", { className: "dshgt-mono" }, record.preview?.file ?? "—")
3101
+ ),
3102
+ h("span", { className: "dshgt-dim" }, record.preview === void 0 ? "—" : formatBytes(record.preview.bytes)),
3103
+ payload?.preview.truncated === true ? h("span", { className: "dshgt-tag" }, t("digest.truncated")) : null,
3104
+ h("span", { className: "dshgt-grow" }),
3105
+ h(
3106
+ "div",
3107
+ { className: "dshgt-seg", role: "group", "aria-label": t("digest.mode") },
3108
+ h(
3109
+ "button",
3110
+ {
3111
+ type: "button",
3112
+ "aria-pressed": mode === "render" ? "true" : "false",
3113
+ className: mode === "render" ? "on" : "",
3114
+ onClick: () => setMode("render")
3115
+ },
3116
+ t("digest.render")
3117
+ ),
3118
+ h(
3119
+ "button",
3120
+ {
3121
+ type: "button",
3122
+ "aria-pressed": mode === "raw" ? "true" : "false",
3123
+ className: mode === "raw" ? "on" : "",
3124
+ onClick: () => setMode("raw")
3125
+ },
3126
+ t("digest.raw")
3127
+ )
3128
+ ),
3129
+ h(
3130
+ Btn,
3131
+ {
3132
+ small: true,
3133
+ onClick: () => {
3134
+ const file = record.preview?.file;
3135
+ if (file === void 0) return;
3136
+ void copyText(file).then((okResult) => {
3137
+ if (!okResult) return;
3138
+ setCopied(true);
3139
+ setTimeout(() => setCopied(false), 1600);
3140
+ });
3141
+ }
3142
+ },
3143
+ copied ? t("digest.copied") : t("digest.copyPath")
3144
+ ),
3145
+ record.prUrl === void 0 ? null : h(
3146
+ "a",
3147
+ { className: "dshgt-btn dshgt-btn-sm", href: record.prUrl, target: "_blank", rel: "noreferrer" },
3148
+ t("digest.openPr")
3149
+ )
3150
+ ),
3151
+ loading || payload === null ? h("div", { className: "dshgt-digest-loading" }, t("digest.loading")) : mode === "raw" || parseFailed ? h("pre", { className: "dshgt-digest-raw" }, payload.preview.text) : h(
3152
+ "div",
3153
+ { className: "dshgt-digest-doc" },
3154
+ rendered === null ? null : h(
3155
+ import_react.default.Fragment,
3156
+ null,
3157
+ metaLine(rendered),
3158
+ h("div", { className: "dshgt-md" }, rendered.nodes)
1503
3159
  )
1504
3160
  )
1505
3161
  )
1506
3162
  )
1507
3163
  );
1508
3164
  }
3165
+ function metaLine(rendered) {
3166
+ const entries = META_KEYS.filter((key) => rendered.meta[key] !== void 0 && rendered.meta[key] !== "");
3167
+ if (entries.length === 0) return null;
3168
+ return h(
3169
+ "div",
3170
+ { className: "dshgt-doc-meta" },
3171
+ entries.map((key) => {
3172
+ const value = rendered.meta[key] ?? "";
3173
+ if (key === "contentHash") {
3174
+ const short = value.replace(/^sha256:/, "").slice(0, 10);
3175
+ return h("span", { key, title: value }, `${key}: ${short}`);
3176
+ }
3177
+ return h("span", { key }, `${key}: ${value}`);
3178
+ })
3179
+ );
3180
+ }
3181
+
3182
+ // node_modules/@morehao/dsh-plugin-kit/dist/update-client.js
3183
+ var IDLE = { status: "idle", view: null, error: null };
3184
+ function errorOf(error) {
3185
+ if (error !== null && typeof error === "object") {
3186
+ const value = error;
3187
+ const message = typeof value.message === "string" && value.message !== "" ? value.message : "操作失败";
3188
+ return typeof value.code === "string" ? { code: value.code, message } : { message };
3189
+ }
3190
+ return { message: String(error ?? "操作失败") };
3191
+ }
3192
+ function createUpdateController(options) {
3193
+ const now = options.now ?? Date.now;
3194
+ let state = { ...IDLE };
3195
+ const listeners = /* @__PURE__ */ new Set();
3196
+ let inFlight = null;
3197
+ function emit(next) {
3198
+ state = next;
3199
+ options.onChange?.(state);
3200
+ for (const listener of listeners) listener(state);
3201
+ }
3202
+ async function check(checkOptions = {}) {
3203
+ if (inFlight !== null) return;
3204
+ inFlight = "check";
3205
+ emit({ ...state, status: "checking", error: null });
3206
+ try {
3207
+ const view = await options.request.check(checkOptions);
3208
+ const checkedAt = new Date(now()).toISOString();
3209
+ if (view.checkFailed) {
3210
+ emit({
3211
+ status: "failed",
3212
+ view,
3213
+ error: { code: "CHECK_FAILED", message: "检查更新失败:拿不到 registry 上的最新版本" },
3214
+ checkedAt
3215
+ });
3216
+ } else {
3217
+ emit({ status: view.updateAvailable ? "available" : "current", view, error: null, checkedAt });
3218
+ }
3219
+ } catch (error) {
3220
+ emit({ ...state, status: "failed", error: errorOf(error) });
3221
+ } finally {
3222
+ inFlight = null;
3223
+ }
3224
+ }
3225
+ async function apply2() {
3226
+ if (inFlight !== null) return;
3227
+ const view = state.view;
3228
+ if (view === null || !view.updateAvailable) {
3229
+ emit({ ...state, status: "failed", error: { code: "NO_UPDATE", message: "没有可更新的版本,请先检查更新" } });
3230
+ return;
3231
+ }
3232
+ if (!view.canApply) {
3233
+ emit({
3234
+ ...state,
3235
+ status: "failed",
3236
+ error: { code: "NO_APPLY", message: `当前环境不支持一键更新,请执行:${view.manualCommand}` }
3237
+ });
3238
+ return;
3239
+ }
3240
+ inFlight = "apply";
3241
+ emit({ ...state, status: "applying", error: null });
3242
+ try {
3243
+ const result = await options.request.apply();
3244
+ const restartRequired = result.restartRequired ?? result.application !== "applied";
3245
+ emit({ ...state, status: restartRequired ? "restart-required" : "applied", error: null });
3246
+ } catch (error) {
3247
+ emit({ ...state, status: "failed", error: errorOf(error) });
3248
+ } finally {
3249
+ inFlight = null;
3250
+ }
3251
+ }
3252
+ return {
3253
+ getState: () => state,
3254
+ subscribe: (listener) => {
3255
+ listeners.add(listener);
3256
+ return () => {
3257
+ listeners.delete(listener);
3258
+ };
3259
+ },
3260
+ check,
3261
+ apply: apply2,
3262
+ reset: () => emit({ ...IDLE })
3263
+ };
3264
+ }
3265
+
3266
+ // client/about.ts
3267
+ function AboutDialog(props) {
3268
+ const { t, state } = props;
3269
+ const [copied, setCopied] = import_react.default.useState(false);
3270
+ const [copiedCommand, setCopiedCommand] = import_react.default.useState(false);
3271
+ const [confirming, setConfirming] = import_react.default.useState(false);
3272
+ const controllerRef = import_react.default.useRef(null);
3273
+ if (controllerRef.current === null) {
3274
+ controllerRef.current = createUpdateController({
3275
+ request: {
3276
+ check: (options) => getPluginUpdate(options.fresh === true ? { fresh: true } : {}),
3277
+ apply: () => applyPluginUpdate()
3278
+ }
3279
+ });
3280
+ }
3281
+ const controller = controllerRef.current;
3282
+ const [updateState, setUpdateState] = import_react.default.useState(() => controller.getState());
3283
+ import_react.default.useEffect(() => {
3284
+ const unsubscribe = controller.subscribe(setUpdateState);
3285
+ if (controller.getState().status === "idle") void controller.check();
3286
+ return unsubscribe;
3287
+ }, [controller]);
3288
+ import_react.default.useEffect(() => {
3289
+ if (updateState.status === "applying" || updateState.status === "restart-required" || updateState.status === "applied") {
3290
+ setConfirming(false);
3291
+ }
3292
+ }, [updateState.status]);
3293
+ const plugin = state.plugin;
3294
+ const fullName = `${plugin.name}@v${plugin.version}`;
3295
+ const repository = plugin.repository;
3296
+ const ownerRepo = repository === void 0 ? void 0 : repository.replace(/^https?:\/\/github\.com\//, "");
3297
+ const repoVersion = ownerRepo === void 0 ? void 0 : `${ownerRepo}@v${plugin.version}`;
3298
+ import_react.default.useEffect(() => {
3299
+ const onKey = (event) => {
3300
+ if (event.key === "Escape") props.onClose();
3301
+ };
3302
+ document.addEventListener("keydown", onKey);
3303
+ return () => document.removeEventListener("keydown", onKey);
3304
+ }, [props]);
3305
+ const copy = () => {
3306
+ void copyText(fullName).then((ok) => {
3307
+ if (!ok) return;
3308
+ setCopied(true);
3309
+ setTimeout(() => setCopied(false), 1600);
3310
+ });
3311
+ };
3312
+ const copyCommand = () => {
3313
+ const command = updateState.view?.manualCommand;
3314
+ if (command === void 0) return;
3315
+ void copyText(command).then((ok) => {
3316
+ if (!ok) return;
3317
+ setCopiedCommand(true);
3318
+ setTimeout(() => setCopiedCommand(false), 1600);
3319
+ });
3320
+ };
3321
+ const view = updateState.view;
3322
+ const status = updateState.status;
3323
+ const target = view !== null && view.updateAvailable ? view.latestVersion ?? "" : "";
3324
+ const busy = status === "checking" || status === "applying";
3325
+ const settled = status === "restart-required" || status === "applied";
3326
+ const actionable = view !== null && view.updateAvailable && view.canApply && !settled && !busy;
3327
+ const manual = view !== null && (!view.canApply || view.checkFailed) ? view.manualCommand : null;
3328
+ const statusLine = () => {
3329
+ if (status === "checking" || view === null) return t("about.update.checking");
3330
+ if (status === "applying") return t("about.update.applying", { version: target });
3331
+ if (status === "restart-required") return t("about.update.done", { version: view.latestVersion ?? target });
3332
+ if (status === "applied") return t("about.update.applied", { version: view.latestVersion ?? target });
3333
+ if (status === "failed") {
3334
+ if (updateState.error?.code === "CHECK_FAILED") return t("about.update.checkFailed");
3335
+ return t("about.update.failed", { message: updateState.error?.message ?? "" });
3336
+ }
3337
+ if (view.checkFailed) return t("about.update.checkFailed");
3338
+ if (view.updateAvailable) return t("about.update.available", { latest: view.latestVersion ?? "", current: view.currentVersion });
3339
+ return t("about.update.current", { version: view.currentVersion });
3340
+ };
3341
+ const updateActions = [
3342
+ h(Btn, { key: "check", small: true, disabled: busy, onClick: () => void controller.check({ fresh: true }) }, t("about.update.check")),
3343
+ manual === null ? null : h(
3344
+ Btn,
3345
+ { key: "copy-command", small: true, onClick: copyCommand },
3346
+ copiedCommand ? t("about.update.copied") : t("about.update.copy")
3347
+ ),
3348
+ actionable ? confirming ? [
3349
+ h(
3350
+ Btn,
3351
+ {
3352
+ key: "confirm",
3353
+ small: true,
3354
+ variant: "danger",
3355
+ onClick: () => {
3356
+ setConfirming(false);
3357
+ void controller.apply();
3358
+ }
3359
+ },
3360
+ t("about.update.confirmYes")
3361
+ ),
3362
+ h(Btn, { key: "cancel", small: true, onClick: () => setConfirming(false) }, t("about.update.cancel"))
3363
+ ] : h(Btn, { key: "apply", small: true, variant: "primary", onClick: () => setConfirming(true) }, t("about.update.apply", { version: target })) : null
3364
+ ].filter((item) => item !== null);
3365
+ const dialog = h(
3366
+ "div",
3367
+ { className: "dshgt-modal-mask", onClick: (event) => {
3368
+ if (event.target === event.currentTarget) props.onClose();
3369
+ } },
3370
+ h(
3371
+ "div",
3372
+ { className: "dshgt-modal dshgt-about", role: "dialog", "aria-modal": "true", "aria-label": t("about.title") },
3373
+ h(
3374
+ "div",
3375
+ { className: "dshgt-about-head" },
3376
+ h("h3", { className: "dshgt-about-title" }, t("about.title")),
3377
+ h(Btn, { small: true, onClick: () => props.onClose() }, t("about.close"))
3378
+ ),
3379
+ h(
3380
+ "button",
3381
+ { type: "button", className: "dshgt-about-pkg", onClick: copy, title: t("about.copy") },
3382
+ h("span", { className: "dshgt-mono" }, fullName),
3383
+ h("span", { className: "dshgt-hint" }, copied ? t("about.copied") : t("about.copy"))
3384
+ ),
3385
+ h(KV, {
3386
+ items: [
3387
+ [
3388
+ t("about.repo"),
3389
+ repository === void 0 || repoVersion === void 0 ? h("span", { className: "dshgt-hint" }, t("about.repo.missing")) : h("a", { href: repository, target: "_blank", rel: "noreferrer" }, repoVersion)
3390
+ ],
3391
+ [t("about.license"), "MIT"],
3392
+ [t("about.core"), state.core.version === void 0 ? "—" : `trendradar ${state.core.version}`],
3393
+ [t("about.env"), t("about.env.value")]
3394
+ ]
3395
+ }),
3396
+ h(
3397
+ "div",
3398
+ { className: "dshgt-update" },
3399
+ h(
3400
+ "div",
3401
+ { className: "dshgt-update-head" },
3402
+ h("h4", null, t("about.update.title")),
3403
+ h("span", { className: "dshgt-hint" }, t("about.update.hint"))
3404
+ ),
3405
+ h("p", { className: `dshgt-update-line${status === "failed" ? " dshgt-update-err" : ""}` }, statusLine()),
3406
+ confirming ? h("p", { className: "dshgt-update-warn" }, t("about.update.confirm", { version: target })) : null,
3407
+ h("div", { className: "dshgt-update-actions" }, updateActions),
3408
+ manual === null ? null : h(
3409
+ "div",
3410
+ { className: "dshgt-update-manual" },
3411
+ h("p", { className: "dshgt-update-warn" }, t("about.update.manual", { kind: view?.installKind ?? "unknown" })),
3412
+ h("code", { className: "dshgt-update-cmd" }, manual)
3413
+ )
3414
+ )
3415
+ )
3416
+ );
3417
+ return h("div", { className: "dshgt-root" }, dialog);
3418
+ }
1509
3419
 
1510
3420
  // client/panel-icon.ts
1511
3421
  var TREND_LINE = "3 16.5 9 10.5 13 14.5 21 6.5";
@@ -1534,11 +3444,99 @@ function SidebarPanelIcon(props) {
1534
3444
  // client/index.ts
1535
3445
  var PANEL_ID = "dsh-github-trending";
1536
3446
  var inject = ["slots", "locale"];
3447
+ var NAV = [
3448
+ { id: "overview", label: "nav.overview", icon: "grid" },
3449
+ { id: "digest", label: "nav.digest", icon: "doc" },
3450
+ { id: "runs", label: "nav.runs", icon: "list" },
3451
+ { id: "settings", label: "nav.settings", icon: "sliders" }
3452
+ ];
3453
+ function navIcon(name, size = 16) {
3454
+ const common = {
3455
+ width: size,
3456
+ height: size,
3457
+ viewBox: "0 0 24 24",
3458
+ fill: "none",
3459
+ stroke: "currentColor",
3460
+ strokeWidth: 1.7,
3461
+ strokeLinecap: "round",
3462
+ strokeLinejoin: "round",
3463
+ "aria-hidden": "true",
3464
+ focusable: "false"
3465
+ };
3466
+ if (name === "grid") {
3467
+ return h(
3468
+ "svg",
3469
+ common,
3470
+ h("rect", { x: 3.5, y: 3.5, width: 7, height: 7, rx: 1.5 }),
3471
+ h("rect", { x: 13.5, y: 3.5, width: 7, height: 7, rx: 1.5 }),
3472
+ h("rect", { x: 3.5, y: 13.5, width: 7, height: 7, rx: 1.5 }),
3473
+ h("rect", { x: 13.5, y: 13.5, width: 7, height: 7, rx: 1.5 })
3474
+ );
3475
+ }
3476
+ if (name === "doc") {
3477
+ return h(
3478
+ "svg",
3479
+ common,
3480
+ h("path", { d: "M5 4.5h11a2 2 0 0 1 2 2V20H7a2 2 0 0 1-2-2z" }),
3481
+ h("path", { d: "M8 8h7M8 12h7M8 16h4" })
3482
+ );
3483
+ }
3484
+ if (name === "list") {
3485
+ return h(
3486
+ "svg",
3487
+ common,
3488
+ h("path", { d: "M8 6h12M8 12h12M8 18h12" }),
3489
+ h("path", { d: "M4 6h.01M4 12h.01M4 18h.01" })
3490
+ );
3491
+ }
3492
+ return h(
3493
+ "svg",
3494
+ common,
3495
+ h("path", { d: "M4 7h10M18 7h2M4 17h4M12 17h8" }),
3496
+ h("circle", { cx: 16, cy: 7, r: 2.2 }),
3497
+ h("circle", { cx: 10, cy: 17, r: 2.2 })
3498
+ );
3499
+ }
3500
+ function brandLogo() {
3501
+ return h(
3502
+ "svg",
3503
+ { width: 28, height: 28, viewBox: "0 0 64 64", "aria-hidden": "true" },
3504
+ h("rect", { x: 3, y: 3, width: 58, height: 58, rx: 15, fill: "#0d1117", stroke: "rgba(255,255,255,.14)", strokeWidth: 1 }),
3505
+ h("path", {
3506
+ d: "M18 46V22l14 12 14-12v24",
3507
+ fill: "none",
3508
+ stroke: "#58a6ff",
3509
+ strokeWidth: 4,
3510
+ strokeLinecap: "round",
3511
+ strokeLinejoin: "round"
3512
+ }),
3513
+ h("circle", { cx: 32, cy: 45, r: 3, fill: "#3fb950" })
3514
+ );
3515
+ }
3516
+ function relativeFuture(iso, t) {
3517
+ if (iso === void 0) return void 0;
3518
+ const target = new Date(iso).getTime();
3519
+ if (Number.isNaN(target)) return void 0;
3520
+ const diff = target - Date.now();
3521
+ if (diff <= 0) return void 0;
3522
+ const minutes = Math.max(1, Math.round(diff / 6e4));
3523
+ if (minutes < 60) return t("time.in.minutes", { minutes });
3524
+ const hours = Math.floor(minutes / 60);
3525
+ if (hours < 24) return t("time.in.hours", { hours, minutes: minutes % 60 });
3526
+ return t("time.in.days", { days: Math.floor(hours / 24) });
3527
+ }
3528
+ function nextRunText(iso) {
3529
+ if (iso === void 0) return "—";
3530
+ const date = new Date(iso);
3531
+ if (Number.isNaN(date.getTime())) return "—";
3532
+ return date.toLocaleString(void 0, { month: "numeric", day: "numeric", hour: "2-digit", minute: "2-digit" });
3533
+ }
1537
3534
  function apply(ctx) {
1538
3535
  const slots = ctx.get?.("slots");
1539
3536
  if (slots === void 0 || typeof slots.register !== "function") return;
1540
3537
  const locale = ctx.get?.("locale");
1541
3538
  const t = buildTranslate(locale);
3539
+ const lang = activeLang(locale);
1542
3540
  const disposeStyles = injectStyles();
1543
3541
  const disposers = [];
1544
3542
  const pickDirectory = async () => {
@@ -1550,32 +3548,188 @@ function apply(ctx) {
1550
3548
  }
1551
3549
  function PanelView() {
1552
3550
  const value = store();
1553
- const [tab, setTab] = import_react.default.useState("runs");
1554
- const tabButton = (id, label) => h(
1555
- "button",
1556
- {
1557
- type: "button",
1558
- className: "dshgt-tab",
1559
- role: "tab",
1560
- "aria-selected": tab === id ? "true" : "false",
1561
- onClick: () => setTab(id)
1562
- },
1563
- label
1564
- );
3551
+ const controller = useRunController(value);
3552
+ const [view, setView] = import_react.default.useState("overview");
3553
+ const [digestDate, setDigestDate] = import_react.default.useState(void 0);
3554
+ const [aboutOpen, setAboutOpen] = import_react.default.useState(false);
3555
+ const openDigest = import_react.default.useCallback((date) => {
3556
+ if (date !== void 0) setDigestDate(date);
3557
+ setView("digest");
3558
+ }, []);
3559
+ const openRuns = import_react.default.useCallback(() => setView("runs"), []);
3560
+ const openSettings = import_react.default.useCallback(() => setView("settings"), []);
3561
+ const state = value.state;
3562
+ const plugin = state?.plugin;
3563
+ const history = state?.status.history ?? [];
3564
+ const schedule = state?.schedule;
3565
+ const ready = state === null ? 0 : [
3566
+ state.core.state === "ready",
3567
+ credentialView(state.model, t).ok,
3568
+ state.settings.repo.path.trim() !== "",
3569
+ state.github.configured
3570
+ ].filter(Boolean).length;
3571
+ const fullName = plugin === void 0 ? void 0 : `${plugin.name}@v${plugin.version}`;
3572
+ const digestTail = digestDate ?? state?.status.lastResult?.date ?? state?.status.history[0]?.date;
3573
+ const showActions = view === "overview" || view === "runs";
3574
+ const viewNode = view === "overview" ? h(OverviewView, {
3575
+ t,
3576
+ lang,
3577
+ store: value,
3578
+ controller,
3579
+ onOpenDigest: openDigest,
3580
+ onOpenRuns: openRuns,
3581
+ onOpenSettings: openSettings
3582
+ }) : view === "digest" ? h(DigestView, {
3583
+ t,
3584
+ lang,
3585
+ store: value,
3586
+ ...digestDate === void 0 ? {} : { selectedDate: digestDate },
3587
+ onSelectDate: setDigestDate,
3588
+ onOpenRuns: openRuns
3589
+ }) : view === "runs" ? h(RunsView, { t, store: value, controller, onOpenDigest: openDigest }) : h(SettingsView, { t, store: value, pickDirectory });
1565
3590
  return h(
1566
3591
  "div",
1567
3592
  { className: "dshgt-root dshgt-shell" },
1568
3593
  h(
1569
3594
  "div",
1570
- { className: "dshgt-tabs", role: "tablist" },
1571
- tabButton("runs", t("tab.runs")),
1572
- tabButton("settings", t("tab.settings"))
3595
+ { className: "dshgt-app" },
3596
+ h(
3597
+ "aside",
3598
+ { className: "dshgt-rail" },
3599
+ h(
3600
+ "div",
3601
+ { className: "dshgt-brand" },
3602
+ brandLogo(),
3603
+ h(
3604
+ "div",
3605
+ null,
3606
+ h("div", { className: "dshgt-brand-name" }, t("section.title")),
3607
+ h("div", { className: "dshgt-brand-ver" }, plugin === void 0 ? "" : `v${plugin.version}`)
3608
+ )
3609
+ ),
3610
+ h(
3611
+ "nav",
3612
+ { className: "dshgt-nav", "aria-label": t("shell.nav") },
3613
+ NAV.map(
3614
+ (item) => h(
3615
+ "button",
3616
+ {
3617
+ key: item.id,
3618
+ type: "button",
3619
+ className: `dshgt-nav-item${view === item.id ? " on" : ""}`,
3620
+ "aria-current": view === item.id ? "page" : void 0,
3621
+ onClick: () => setView(item.id)
3622
+ },
3623
+ navIcon(item.icon),
3624
+ h("span", null, t(item.label)),
3625
+ item.id === "digest" && digestTail !== void 0 ? h("span", { className: "dshgt-nav-tail" }, h("span", { className: "dshgt-mono" }, digestTail.slice(5))) : item.id === "runs" && history.length > 0 ? h("span", { className: "dshgt-nav-tail" }, h("span", { className: "dshgt-mono" }, String(history.length))) : null
3626
+ )
3627
+ )
3628
+ ),
3629
+ h("div", { className: "dshgt-rail-spacer" }),
3630
+ h(
3631
+ "div",
3632
+ { className: "dshgt-rail-status" },
3633
+ h("div", { className: "dshgt-rail-cap" }, t("shell.nextRun")),
3634
+ h(
3635
+ "div",
3636
+ { className: "dshgt-rail-time" },
3637
+ schedule === void 0 || !schedule.enabled ? t("shell.scheduleOff") : schedule.nextRunAt === void 0 ? `${schedule.time} · ${schedule.timezone}` : nextRunText(schedule.nextRunAt)
3638
+ ),
3639
+ h(
3640
+ "div",
3641
+ { className: "dshgt-rail-count" },
3642
+ schedule !== void 0 && schedule.enabled && schedule.nextRunAt !== void 0 ? relativeFuture(schedule.nextRunAt, t) ?? "" : schedule === void 0 ? "" : `${schedule.time} · ${schedule.timezone}`
3643
+ ),
3644
+ h(
3645
+ "div",
3646
+ { className: "dshgt-rail-ready" },
3647
+ h("span", { className: `dshgt-lamp ${ready === 4 ? "ok" : ready > 0 ? "warn" : "err"}` }),
3648
+ t("shell.ready", { ready, total: 4 })
3649
+ )
3650
+ ),
3651
+ h(
3652
+ "div",
3653
+ { className: "dshgt-rail-pkg" },
3654
+ h(
3655
+ "button",
3656
+ {
3657
+ type: "button",
3658
+ className: "dshgt-pkg-name",
3659
+ onClick: () => setAboutOpen(true),
3660
+ title: plugin === void 0 ? "" : fullName
3661
+ },
3662
+ plugin?.name ?? t("section.title")
3663
+ ),
3664
+ h(
3665
+ "div",
3666
+ { className: "dshgt-pkg-sub" },
3667
+ h("span", { className: "dshgt-mono" }, plugin === void 0 ? "" : `@v${plugin.version}`),
3668
+ plugin?.repository === void 0 ? null : h(
3669
+ "a",
3670
+ { href: plugin.repository, target: "_blank", rel: "noreferrer" },
3671
+ `${t("shell.github")} ↗`
3672
+ ),
3673
+ h(
3674
+ "button",
3675
+ {
3676
+ type: "button",
3677
+ className: "dshgt-pkg-copy",
3678
+ title: t("shell.package.copy"),
3679
+ "aria-label": t("shell.package.copy"),
3680
+ disabled: fullName === void 0,
3681
+ onClick: () => {
3682
+ if (fullName !== void 0) void copyText(fullName);
3683
+ }
3684
+ },
3685
+ navIcon("doc", 13)
3686
+ )
3687
+ )
3688
+ )
3689
+ ),
3690
+ h(
3691
+ "main",
3692
+ { className: "dshgt-main" },
3693
+ h(
3694
+ "header",
3695
+ { className: "dshgt-page-head" },
3696
+ h(
3697
+ "div",
3698
+ { className: "dshgt-page-heading" },
3699
+ h("h1", null, t(`nav.${view}`)),
3700
+ h("p", { className: "dshgt-dek" }, t(`shell.${view}.dek`))
3701
+ ),
3702
+ h(
3703
+ "div",
3704
+ { className: "dshgt-head-actions", style: showActions ? void 0 : { visibility: "hidden" } },
3705
+ showActions ? [
3706
+ h(
3707
+ Btn,
3708
+ {
3709
+ key: "dry",
3710
+ onClick: () => void controller.run(true),
3711
+ disabled: controller.running || controller.busy
3712
+ },
3713
+ t("run.dryRunShort")
3714
+ ),
3715
+ h(
3716
+ Btn,
3717
+ {
3718
+ key: "run",
3719
+ variant: "primary",
3720
+ onClick: () => void controller.run(),
3721
+ disabled: controller.running || controller.busy
3722
+ },
3723
+ controller.running ? t("run.running") : t("run.button")
3724
+ ),
3725
+ controller.running ? h(Btn, { key: "cancel", variant: "danger", onClick: () => void controller.cancel() }, t("run.cancel")) : null
3726
+ ] : null
3727
+ )
3728
+ ),
3729
+ h("div", { className: "dshgt-main-body" }, view === "settings" ? viewNode : h("div", { className: "dshgt-scroll" }, viewNode))
3730
+ )
1573
3731
  ),
1574
- h(
1575
- "div",
1576
- { className: "dshgt-content" },
1577
- tab === "settings" ? h(SettingsView, { t, store: value, pickDirectory }) : h("div", { className: "dshgt-scroll" }, h(RunsView, { t, store: value, page: true }))
1578
- )
3732
+ aboutOpen && state !== null ? h(AboutDialog, { t, state, onClose: () => setAboutOpen(false) }) : null
1579
3733
  );
1580
3734
  }
1581
3735
  disposers.push(