@morehao/dsh-github-trending 0.1.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/client.js CHANGED
@@ -50,6 +50,12 @@ var DICT = {
50
50
  "section.subtitle": "每天定时把 GitHub Trending 抓取、总结并提交到你的仓库;内容全部由本地核心产出。",
51
51
  "tab.settings": "设置",
52
52
  "tab.runs": "运行",
53
+ "st.source": "内容来源",
54
+ "st.model": "模型",
55
+ "st.repo": "仓库",
56
+ "st.auth": "认证",
57
+ "st.scope": "榜单",
58
+ "st.schedule": "调度",
53
59
  "card.source.title": "内容来源",
54
60
  "card.source.hint": "唯一来源是本地二进制",
55
61
  "card.source.desc": "插件 spawn 核心的一次性运行:核心完成抓取、总结、渲染与提交。不连接服务端,也不提供容器来源。",
@@ -69,6 +75,7 @@ var DICT = {
69
75
  "core.downloaded": "核心已下载并校验通过",
70
76
  "core.installed": "已安装",
71
77
  "core.missing": "未安装",
78
+ "core.override": "覆盖路径:{path}",
72
79
  "core.order": "解析顺序:TRENDING_DIGEST_CORE(仅开发/排障)→ Release 资产(缓存命中直接用,否则下载并校验);都不可用时报 CORE_UNAVAILABLE。",
73
80
  "coexist.title": "检测到本机另有 CLI",
74
81
  "coexist.hint": "插件仍使用自带核心,不切换、不接管它的定时。",
@@ -84,7 +91,10 @@ var DICT = {
84
91
  "model.mode": "密钥来源",
85
92
  "model.mode.direct": "DSH 凭据 / 手动密钥",
86
93
  "model.mode.proxy": "回环代理(凭据不可导出)",
87
- "model.keyRef": "凭据引用",
94
+ "model.credential": "凭据状态",
95
+ "model.credential.proxyHint": "回环代理用宿主自己的凭据转发,密钥不经过插件、也不落盘,本页无需填写。",
96
+ "model.credential.directHint": "密钥在运行时从宿主凭据 / 环境变量解析,直传给核心。",
97
+ "model.credential.missingHint": "当前是直传模式,但凭据引用为空或解析不到;点右上角「沿用 DSH 配置」重新填充。",
88
98
  "model.params": "参数",
89
99
  "model.preflight": "连通性预检",
90
100
  "model.preflighting": "预检中…",
@@ -93,25 +103,36 @@ var DICT = {
93
103
  "card.repo.hint": "内容写到哪里、怎么提交",
94
104
  "repo.path": "仓库路径",
95
105
  "repo.path.placeholder": "绝对路径,例如 /Users/you/notes",
106
+ "repo.path.empty": "尚未选择目录",
96
107
  "repo.pick": "选择目录",
97
108
  "repo.baseBranch": "目标分支",
98
- "repo.remote": "远端",
99
109
  "repo.outputDir": "输出目录",
110
+ "repo.outputDir.hint": "仓库内的相对目录",
111
+ "branch.loading": "读取中…",
112
+ "branch.fromOrigin": "仓库默认分支(origin/HEAD)",
113
+ "branch.fromHead": "仓库当前分支(未找到 origin)",
114
+ "branch.readonlyHint": "自动读取,不可修改",
100
115
  "repo.commitMode": "提交方式",
101
116
  "repo.commitMode.branch": "分支 + PR",
102
117
  "repo.commitMode.direct": "直接提交",
103
118
  "repo.autoMerge": "自动合并 PR",
104
- "repo.titleTemplate": "PR 标题模板",
105
119
  "card.auth.title": "GitHub 认证",
106
120
  "card.auth.hint": "只申请 public_repo",
107
121
  "auth.mode": "方式",
108
- "auth.device": "Device Flow",
109
122
  "auth.pat": "PAT",
110
123
  "auth.gh": "本机 gh",
124
+ "auth.gh.hint": "使用本机 gh 登录态:终端执行 `gh auth login` 完成登录,再点「测试认证」。",
125
+ "auth.pat.hint": "在 GitHub → Settings → Developer settings 生成 Personal Access Token(勾选 public_repo),填到下方引用或环境变量。",
111
126
  "auth.tokenRef": "凭据引用",
127
+ "auth.tokenRef.hint": "凭据引用名 / 环境变量名",
128
+ "auth.test": "测试认证",
129
+ "auth.testing": "验证中…",
130
+ "auth.test.ok": "认证成功",
131
+ "auth.test.fail": "认证失败",
112
132
  "auth.status": "状态",
113
133
  "auth.configured": "已配置",
114
134
  "auth.missing": "未配置",
135
+ "auth.hostProxy": "沿用 DSH 凭据",
115
136
  "card.scope.title": "榜单范围",
116
137
  "scope.period": "周期",
117
138
  "scope.daily": "每日",
@@ -125,6 +146,9 @@ var DICT = {
125
146
  "schedule.next": "下次运行",
126
147
  "schedule.source": "由插件自己的 60 s tick 驱动(宿主 schedule 是会话本地提醒,覆盖不到无人值守)",
127
148
  "settings.save": "保存设置",
149
+ "settings.saveSection": "保存本页",
150
+ "settings.saveSection.hint": "每个 tab 独立保存;黄点表示该页有未保存改动",
151
+ "settings.dirty": "有未保存改动",
128
152
  "settings.saving": "保存中…",
129
153
  "settings.saved": "已保存",
130
154
  "settings.import": "从本机 CLI 导入",
@@ -133,6 +157,7 @@ var DICT = {
133
157
  "settings.troubleshoot": "排障",
134
158
  "settings.copyConfig": "复制脱敏配置",
135
159
  "settings.copied": "已复制",
160
+ "settings.lastRun": "上次运行",
136
161
  "run.now": "本次",
137
162
  "run.history": "历史",
138
163
  "run.button": "立即运行",
@@ -158,6 +183,8 @@ var DICT = {
158
183
  "run.usage.hash": "内容哈希",
159
184
  "run.usage.branch": "分支",
160
185
  "run.usage.pr": "PR",
186
+ "run.pendingPr": "存在未合并 PR",
187
+ "run.pendingPr.hint": "同日重跑会在分支上追加提交并更新该 PR;若已合并/关闭,记录会自动清理。",
161
188
  "run.error.title": "最近错误",
162
189
  "run.history.title": "历史记录",
163
190
  "run.history.hint": "最近 30 条,超出即裁剪",
@@ -172,15 +199,14 @@ var DICT = {
172
199
  "state.skipped": "无变更",
173
200
  "state.failed": "失败",
174
201
  "state.dry": "试跑",
175
- "widget.title": "GitHub 趋势",
176
- "widget.next": "下次",
177
- "widget.last": "上次",
178
- "widget.never": "从未运行",
179
- "widget.run": "立即运行",
180
- "widget.open": "打开面板",
181
202
  "quickfill.title": "沿用 DSH 已配置的模型",
182
- "quickfill.empty": "宿主没有可用的 provider 目录",
183
- "quickfill.hint": "选择 provider 与模型后写入下面的字段;不会自动改你已有的配置。",
203
+ "quickfill.empty": "没有找到已配置的模型",
204
+ "quickfill.hint": "优先一键沿用 DSH 当前默认模型;需要时可以展开选择其他已配置的供应商与模型。",
205
+ "quickfill.default": "DSH 当前默认模型",
206
+ "quickfill.useDefault": "使用默认模型",
207
+ "quickfill.applying": "应用中…",
208
+ "quickfill.others": "选择其他已配置模型",
209
+ "quickfill.others.hide": "收起其他模型",
184
210
  "quickfill.mode.direct": "密钥可导出,直传核心",
185
211
  "quickfill.mode.proxy": "凭据不可导出,走回环代理",
186
212
  "quickfill.mode.missing": "缺少可用凭据,需要手动填写",
@@ -188,13 +214,144 @@ var DICT = {
188
214
  "quickfill.close": "关闭",
189
215
  "quickfill.loading": "读取中…",
190
216
  "error.prefix": "错误",
191
- "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 插件"
192
343
  },
193
344
  en: {
194
345
  "section.title": "GitHub Trending",
195
346
  "section.subtitle": "Fetch, summarize and commit GitHub Trending into your own repo on a daily schedule. All content comes from the local core binary.",
196
347
  "tab.settings": "Settings",
197
348
  "tab.runs": "Runs",
349
+ "st.source": "Source",
350
+ "st.model": "Model",
351
+ "st.repo": "Repository",
352
+ "st.auth": "Auth",
353
+ "st.scope": "Scope",
354
+ "st.schedule": "Schedule",
198
355
  "card.source.title": "Content source",
199
356
  "card.source.hint": "one source: the local binary",
200
357
  "card.source.desc": "The plugin spawns one core run: fetch, summarize, render and commit happen inside the core. No server, no container.",
@@ -208,6 +365,7 @@ var DICT = {
208
365
  "core.incompatible": "incompatible",
209
366
  "core.installed": "installed",
210
367
  "core.missing": "missing",
368
+ "core.override": "Override path: {path}",
211
369
  "core.cached": "cached",
212
370
  "core.notDownloaded": "not downloaded",
213
371
  "core.download": "Download core",
@@ -229,7 +387,10 @@ var DICT = {
229
387
  "model.mode": "Key source",
230
388
  "model.mode.direct": "DSH credential / manual key",
231
389
  "model.mode.proxy": "Loopback proxy (credential not exportable)",
232
- "model.keyRef": "Credential ref",
390
+ "model.credential": "Credential",
391
+ "model.credential.proxyHint": "The loopback proxy forwards with the host credential; the key never passes through the plugin. Nothing to fill in here.",
392
+ "model.credential.directHint": "Resolved at run time from host credentials / env and passed straight to the core.",
393
+ "model.credential.missingHint": "Direct mode, but the ref is empty or unresolvable; use “Reuse DSH configuration” to refill it.",
233
394
  "model.params": "Parameters",
234
395
  "model.preflight": "Test connection",
235
396
  "model.preflighting": "Testing…",
@@ -238,25 +399,36 @@ var DICT = {
238
399
  "card.repo.hint": "where content lands and how it is committed",
239
400
  "repo.path": "Repository path",
240
401
  "repo.path.placeholder": "absolute path, e.g. /Users/you/notes",
402
+ "repo.path.empty": "No folder chosen",
241
403
  "repo.pick": "Choose folder",
242
404
  "repo.baseBranch": "Base branch",
243
- "repo.remote": "Remote",
244
405
  "repo.outputDir": "Output dir",
406
+ "repo.outputDir.hint": "relative directory inside the repo",
407
+ "branch.loading": "Reading…",
408
+ "branch.fromOrigin": "repo default branch (origin/HEAD)",
409
+ "branch.fromHead": "current branch (no origin found)",
410
+ "branch.readonlyHint": "Read automatically, cannot be edited",
245
411
  "repo.commitMode": "Commit mode",
246
412
  "repo.commitMode.branch": "Branch + PR",
247
413
  "repo.commitMode.direct": "Direct commit",
248
414
  "repo.autoMerge": "Auto-merge PR",
249
- "repo.titleTemplate": "PR title template",
250
415
  "card.auth.title": "GitHub authentication",
251
416
  "card.auth.hint": "public_repo only",
252
417
  "auth.mode": "Mode",
253
- "auth.device": "Device flow",
254
418
  "auth.pat": "PAT",
255
419
  "auth.gh": "Local gh",
420
+ "auth.gh.hint": "Uses your local gh login: run `gh auth login` in a terminal, then click Test authentication.",
421
+ "auth.pat.hint": "Create a Personal Access Token under GitHub → Settings → Developer settings (scope public_repo) and put it in the ref below or an env var.",
256
422
  "auth.tokenRef": "Credential ref",
423
+ "auth.tokenRef.hint": "credential ref / env var name",
424
+ "auth.test": "Test authentication",
425
+ "auth.testing": "Verifying…",
426
+ "auth.test.ok": "Authenticated",
427
+ "auth.test.fail": "Authentication failed",
257
428
  "auth.status": "Status",
258
429
  "auth.configured": "configured",
259
430
  "auth.missing": "missing",
431
+ "auth.hostProxy": "uses DSH credential",
260
432
  "card.scope.title": "Trending scope",
261
433
  "scope.period": "Period",
262
434
  "scope.daily": "Daily",
@@ -270,6 +442,9 @@ var DICT = {
270
442
  "schedule.next": "Next run",
271
443
  "schedule.source": "Driven by the plugin’s own 60 s tick (host schedule is session-local and cannot cover unattended runs)",
272
444
  "settings.save": "Save",
445
+ "settings.saveSection": "Save this tab",
446
+ "settings.saveSection.hint": "Each tab saves independently; a yellow dot marks unsaved changes",
447
+ "settings.dirty": "Unsaved changes",
273
448
  "settings.saving": "Saving…",
274
449
  "settings.saved": "Saved",
275
450
  "settings.import": "Import from local CLI",
@@ -278,6 +453,7 @@ var DICT = {
278
453
  "settings.troubleshoot": "Troubleshooting",
279
454
  "settings.copyConfig": "Copy redacted config",
280
455
  "settings.copied": "Copied",
456
+ "settings.lastRun": "Last run",
281
457
  "run.now": "This run",
282
458
  "run.history": "History",
283
459
  "run.button": "Run now",
@@ -303,6 +479,8 @@ var DICT = {
303
479
  "run.usage.hash": "Content hash",
304
480
  "run.usage.branch": "Branch",
305
481
  "run.usage.pr": "PR",
482
+ "run.pendingPr": "Unmerged PR exists",
483
+ "run.pendingPr.hint": "Re-running the same day appends a commit and updates this PR; merged/closed records are cleaned up automatically.",
306
484
  "run.error.title": "Last error",
307
485
  "run.history.title": "History",
308
486
  "run.history.hint": "last 30 records",
@@ -317,15 +495,14 @@ var DICT = {
317
495
  "state.skipped": "no change",
318
496
  "state.failed": "failed",
319
497
  "state.dry": "dry run",
320
- "widget.title": "GitHub Trending",
321
- "widget.next": "Next",
322
- "widget.last": "Last",
323
- "widget.never": "never",
324
- "widget.run": "Run",
325
- "widget.open": "Open panel",
326
498
  "quickfill.title": "Reuse a configured DSH model",
327
- "quickfill.empty": "No provider catalog available",
328
- "quickfill.hint": "Pick a provider and model; existing fields are only overwritten after you confirm.",
499
+ "quickfill.empty": "No configured model found",
500
+ "quickfill.hint": "One click reuses the current DSH default model; expand to pick another configured provider and model if needed.",
501
+ "quickfill.default": "Current DSH default model",
502
+ "quickfill.useDefault": "Use default model",
503
+ "quickfill.applying": "Applying…",
504
+ "quickfill.others": "Choose another configured model",
505
+ "quickfill.others.hide": "Hide other models",
329
506
  "quickfill.mode.direct": "exportable key, passed to the core",
330
507
  "quickfill.mode.proxy": "credential not exportable, uses the loopback proxy",
331
508
  "quickfill.mode.missing": "no usable credential, fill it in manually",
@@ -333,7 +510,132 @@ var DICT = {
333
510
  "quickfill.close": "Close",
334
511
  "quickfill.loading": "Loading…",
335
512
  "error.prefix": "Error",
336
- "label.optional": "optional"
513
+ "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.",
514
+ "err.reason.non-fast-forward": "Push rejected (non-fast-forward): the remote branch moved ahead. Fetch and rebase onto it, then retry.",
515
+ "err.reason.auth": "Push authentication failed: refresh the GitHub token or run `gh auth login`, then retry. Local commits are kept.",
516
+ "err.reason.protected": "The target branch is protected: open a PR instead, or adjust the branch protection rules.",
517
+ "err.reason.hook": "A repository hook rejected the operation: fix it per the hook output and retry. Local commits are kept.",
518
+ "err.reason.pathspec": "Nothing to stage at that path: check that the output directory matches the repository layout.",
519
+ "err.reason.unknown": "Push failed. Local commits are kept; check the run log and retry.",
520
+ "skip.merged": "The PR for this date was merged; skipped.",
521
+ "skip.pr-closed": "The PR for this date was closed unmerged. Scheduled runs do not reopen it; run manually to reopen.",
522
+ "skip.unchanged": "The branch already holds the same artifacts; no new commit.",
523
+ "label.optional": "optional",
524
+ "nav.overview": "Overview",
525
+ "nav.digest": "Digest",
526
+ "nav.runs": "Runs",
527
+ "nav.settings": "Settings",
528
+ "shell.nav": "Panel",
529
+ "shell.overview.dek": "A daily digest on a schedule; start with the next run, readiness and the last delivery.",
530
+ "shell.digest.dek": "Look back over the last 30 days; only runs with a saved preview copy can be read.",
531
+ "shell.runs.dek": "The last 30 runs; expand one for artifacts, errors and logs.",
532
+ "shell.settings.dek": "The existing settings page; each section saves on its own.",
533
+ "shell.nextRun": "Next run",
534
+ "shell.scheduleOff": "Schedule off",
535
+ "shell.ready": "Ready {ready} / {total}",
536
+ "shell.github": "GitHub",
537
+ "shell.package.copy": "Copy full package name",
538
+ "time.in.minutes": "in {minutes} min",
539
+ "time.in.hours": "in {hours} h {minutes} min",
540
+ "time.in.days": "in {days} d",
541
+ "overview.last.none": "No runs yet",
542
+ "overview.last.files": "{count} files",
543
+ "overview.last.digest": "{date} · GitHub Trending digest",
544
+ "overview.last.read": "Read this issue",
545
+ "overview.last.noPreview": "This run has no saved preview copy",
546
+ "overview.last.prPending": "Pending merge",
547
+ "overview.last.prCreated": "PR created",
548
+ "overview.last.prUnknown": "PR state unknown",
549
+ "overview.last.direct": "Direct commit",
550
+ "overview.model": "Model",
551
+ "overview.ready.title": "Readiness",
552
+ "overview.ready.core": "Core",
553
+ "overview.ready.model": "Model",
554
+ "overview.ready.repo": "Repository",
555
+ "overview.ready.github": "GitHub",
556
+ "overview.ready.view": "View",
557
+ "overview.week.title": "Last 7 days",
558
+ "overview.week.all": "All runs",
559
+ "overview.week.none": "No record",
560
+ "overview.week.wait": "Waiting for {time}",
561
+ "overview.live.title": "Running",
562
+ "overview.live.core": "Core is working",
563
+ "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.",
564
+ "overview.live.started": "Started",
565
+ "run.dryRunShort": "Dry run",
566
+ "run.usage.commit": "Commit",
567
+ "runs.count": "Last {count} runs; older ones are trimmed",
568
+ "runs.files": "{count} files",
569
+ "runs.noChange": "Content matches the previous issue; the core produced no new file",
570
+ "runs.dryHint": "dry run writes nothing and commits nothing",
571
+ "runs.empty": "No matching records",
572
+ "runs.filter.all": "All",
573
+ "runs.filter.failed": "Failed",
574
+ "runs.filter.dry": "Dry run",
575
+ "runs.pendingPr": "Pending merge",
576
+ "runs.prUnknown": "State unknown",
577
+ "runs.prCreated": "PR created",
578
+ "runs.legacy": "Legacy",
579
+ "runs.legacyHint": "Legacy records have no preview copy and use the old hash algorithm, so they cannot be compared with v2 directly.",
580
+ "runs.detail.coreModel": "Core / model",
581
+ "runs.detail.date": "Run date",
582
+ "runs.detail.warnings": "Warnings",
583
+ "runs.detail.none": "None",
584
+ "runs.detail.files": "Files / duration",
585
+ "runs.detail.readDigest": "Read this digest",
586
+ "runs.detail.noPreview": "No preview copy",
587
+ "runs.detail.log": "View log",
588
+ "digest.allRuns": "All runs",
589
+ "digest.window": "Last 30 days · {days} days with runs · {previews} with previews",
590
+ "digest.window.empty": "No runs in the last 30 days",
591
+ "digest.prev": "Previous (older)",
592
+ "digest.next": "Next (newer)",
593
+ "digest.select": "Choose a digest date",
594
+ "digest.render": "Rendered",
595
+ "digest.raw": "Raw",
596
+ "digest.mode": "Preview mode",
597
+ "digest.copyPath": "Copy path",
598
+ "digest.copied": "Copied",
599
+ "digest.openPr": "Open PR",
600
+ "digest.truncated": "Truncated at 512 KB",
601
+ "digest.loading": "Loading preview…",
602
+ "digest.tag.preview": "Preview",
603
+ "digest.tag.failed": "Failed",
604
+ "digest.tag.skipped": "No change",
605
+ "digest.tag.dry": "Dry run",
606
+ "digest.tag.legacy": "Legacy",
607
+ "digest.tag.missing": "No preview",
608
+ "digest.empty.none.title": "No digest to look back on yet",
609
+ "digest.empty.none.reason": "Once a run succeeds and keeps a preview copy, the last 30 days will be listed here by date.",
610
+ "digest.empty.failed.title": "No digest preview for this issue",
611
+ "digest.empty.failed.reason": "This run failed, so there is no readable digest file.",
612
+ "digest.empty.skipped.title": "No new digest for this issue",
613
+ "digest.empty.skipped.reason": "Content matched the previous issue; the core produced no new file, so there is no new preview copy.",
614
+ "digest.empty.dry.title": "A dry run produces no artifact",
615
+ "digest.empty.dry.reason": "A dry run only fetches and renders; it writes no file and commits nothing, so there is nothing to preview.",
616
+ "digest.empty.legacy.title": "Legacy records have no preview copy",
617
+ "digest.empty.legacy.reason": "Preview copies are kept from successful runs after this feature shipped; older records only have run results.",
618
+ "digest.empty.missing.title": "The preview copy was not kept",
619
+ "digest.empty.missing.reason": "This run succeeded, but no preview copy was saved at the time, or it has been cleaned up.",
620
+ "digest.empty.viewRuns": "View runs",
621
+ "digest.empty.latest": "Read the latest issue · {date}",
622
+ "log.title": "Run log",
623
+ "log.loading": "Loading log…",
624
+ "log.empty": "This run has no log",
625
+ "log.truncated": "Shows at most the last 64 KB",
626
+ "log.copy": "Copy log",
627
+ "log.copied": "Copied",
628
+ "log.close": "Close",
629
+ "about.title": "About",
630
+ "about.copy": "Copy full package name",
631
+ "about.copied": "Copied",
632
+ "about.close": "Close",
633
+ "about.repo": "GitHub repository",
634
+ "about.repo.missing": "package.json has no resolvable repository URL",
635
+ "about.license": "License",
636
+ "about.core": "Core",
637
+ "about.env": "Runtime",
638
+ "about.env.value": "DSH Web · Node plugin"
337
639
  }
338
640
  };
339
641
  function activeLang(locale) {
@@ -356,7 +658,7 @@ function buildTranslate(locale) {
356
658
  // client/styles.ts
357
659
  var STYLE_ID = "dsh-github-trending-style";
358
660
  var CSS = `
359
- .dshgt-root{--gt-fg:var(--dsh-fg,#1f2328);--gt-muted:var(--dsh-muted,#6b7280);--gt-bg:var(--dsh-bg,#fff);--gt-card:var(--dsh-bg-subtle,#f8fafc);--gt-border:var(--dsh-border,#e5e7eb);--gt-accent:var(--dsh-accent,#2563eb);--gt-ok:#16a34a;--gt-warn:#d97706;--gt-err:#dc2626;color:var(--gt-fg);font-size:13px;line-height:1.5;display:flex;flex-direction:column;gap:12px;}
661
+ .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;}
360
662
  .dshgt-root *{box-sizing:border-box;}
361
663
  .dshgt-head{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;}
362
664
  .dshgt-title{font-size:15px;font-weight:600;}
@@ -364,6 +666,16 @@ var CSS = `
364
666
  .dshgt-tabs{display:flex;gap:6px;border-bottom:1px solid var(--gt-border);}
365
667
  .dshgt-tab{appearance:none;border:none;background:none;padding:6px 10px;font:inherit;color:var(--gt-muted);cursor:pointer;border-bottom:2px solid transparent;}
366
668
  .dshgt-tab[aria-selected="true"]{color:var(--gt-fg);border-bottom-color:var(--gt-accent);font-weight:600;}
669
+ .dshgt-shell{height:100%;min-height:0;overflow:hidden;}
670
+ .dshgt-content{flex:1;min-height:0;display:flex;}
671
+ .dshgt-scroll{flex:1;min-height:0;overflow-y:auto;display:flex;flex-direction:column;gap:12px;padding-right:4px;}
672
+ .dshgt-settings{flex:1;min-height:0;display:grid;grid-template-columns:150px minmax(0,1fr);gap:16px;}
673
+ .dshgt-vtabs{display:flex;flex-direction:column;gap:2px;border-right:1px solid var(--gt-border);padding-right:8px;overflow-y:auto;}
674
+ .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;}
675
+ .dshgt-vtab:hover{background:var(--dsw-alias-interactive-bg-hover,color-mix(in srgb,var(--gt-accent) 8%,transparent));}
676
+ .dshgt-vtab[aria-selected="true"]{background:color-mix(in srgb,var(--gt-accent) 12%,transparent);color:var(--gt-fg);font-weight:600;}
677
+ .dshgt-vtab-dirty{width:6px;height:6px;border-radius:50%;background:var(--gt-warn);flex:none;margin-left:auto;}
678
+ .dshgt-vpanel{min-width:0;min-height:0;overflow-y:auto;display:flex;flex-direction:column;gap:12px;padding-right:4px;}
367
679
  .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;}
368
680
  .dshgt-card-h{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}
369
681
  .dshgt-card-h h3{margin:0;font-size:13px;font-weight:600;}
@@ -378,7 +690,8 @@ var CSS = `
378
690
  .dshgt-btn{appearance:none;font:inherit;cursor:pointer;border-radius:7px;border:1px solid var(--gt-border);background:var(--gt-bg);padding:5px 10px;color:inherit;}
379
691
  .dshgt-btn:hover:not(:disabled){border-color:color-mix(in srgb,var(--gt-accent) 45%,var(--gt-border));}
380
692
  .dshgt-btn:disabled{opacity:.55;cursor:not-allowed;}
381
- .dshgt-btn-primary{background:var(--gt-accent);border-color:var(--gt-accent);color:#fff;}
693
+ .dshgt-btn-primary{background:var(--dsw-alias-button-primary-fill,var(--gt-accent));border-color:var(--dsw-alias-button-primary-fill,var(--gt-accent));color:var(--dsw-alias-label-primary-foreground,#fff);}
694
+ .dshgt-default{display:flex;align-items:center;justify-content:space-between;gap:10px;border:1px solid var(--gt-border);border-radius:9px;padding:10px 12px;background:var(--dsw-alias-bg-layer-2,var(--gt-card));}
382
695
  .dshgt-btn-danger{background:transparent;border-color:var(--gt-err);color:var(--gt-err);}
383
696
  .dshgt-btn-sm{padding:3px 8px;font-size:12px;}
384
697
  .dshgt-pill{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--gt-border);border-radius:999px;padding:2px 9px;font-size:12px;background:var(--gt-bg);}
@@ -406,12 +719,206 @@ var CSS = `
406
719
  .dshgt-switch::after{content:"";position:absolute;top:2px;left:2px;width:14px;height:14px;border-radius:50%;background:#fff;transition:transform .15s;}
407
720
  .dshgt-switch[aria-checked="true"]{background:var(--gt-accent);}
408
721
  .dshgt-switch[aria-checked="true"]::after{transform:translateX(16px);}
409
- .dshgt-widget{border:1px solid var(--gt-border);border-radius:10px;padding:10px 12px;background:var(--gt-card);display:flex;flex-direction:column;gap:6px;font-size:12px;}
410
- .dshgt-widget h4{margin:0;font-size:12px;font-weight:600;}
411
722
  .dshgt-footer{display:flex;gap:8px;align-items:center;flex-wrap:wrap;}
412
723
  .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;}
413
- .dshgt-modal-mask{position:fixed;inset:0;background:rgba(15,23,42,.38);display:flex;align-items:center;justify-content:center;z-index:2147483000;}
724
+ .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;}
414
725
  .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);}
726
+
727
+ /* ——— 01 工作台外壳 ——— */
728
+ .dshgt-shell{gap:0;height:100%;min-height:0;overflow:hidden;}
729
+ .dshgt-app{flex:1;min-height:0;display:grid;grid-template-columns:216px minmax(0,1fr);background:var(--gt-bg);}
730
+ .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;}
731
+ .dshgt-brand{display:flex;align-items:center;gap:9px;padding:2px 6px 16px;}
732
+ .dshgt-brand svg{flex:none;border-radius:8px;}
733
+ .dshgt-brand-name{font-size:13.5px;font-weight:600;line-height:1.3;}
734
+ .dshgt-brand-ver{font-size:11px;color:var(--gt-muted);margin-top:1px;min-height:15px;}
735
+ .dshgt-nav{display:flex;flex-direction:column;gap:2px;}
736
+ .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;}
737
+ .dshgt-nav-item svg{flex:none;opacity:.82;}
738
+ .dshgt-nav-item:hover{background:color-mix(in srgb,var(--gt-fg) 6%,var(--gt-bg));color:var(--gt-fg);}
739
+ .dshgt-nav-item.on{background:var(--gt-bg);border-color:var(--gt-border);color:var(--gt-fg);font-weight:600;}
740
+ .dshgt-nav-item.on svg{opacity:1;}
741
+ .dshgt-nav-tail{margin-left:auto;display:flex;align-items:center;gap:5px;font-size:11.5px;color:var(--gt-muted);font-weight:400;}
742
+ .dshgt-rail-spacer{flex:1;min-height:16px;}
743
+ .dshgt-rail-status{border-top:1px solid var(--gt-border);padding:12px 8px 10px;}
744
+ .dshgt-rail-cap{font-size:11.5px;color:var(--gt-muted);margin-bottom:3px;}
745
+ .dshgt-rail-time{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:14px;line-height:1.35;}
746
+ .dshgt-rail-count{font-size:12px;color:var(--gt-muted);margin-top:1px;min-height:18px;}
747
+ .dshgt-rail-ready{display:flex;align-items:center;gap:7px;margin-top:10px;font-size:12px;color:var(--gt-muted);}
748
+ .dshgt-rail-pkg{border-top:1px solid var(--gt-border);padding:10px 8px 8px;}
749
+ .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%;}
750
+ .dshgt-pkg-name:hover{color:var(--gt-fg);}
751
+ .dshgt-pkg-sub{display:flex;align-items:center;gap:8px;margin-top:3px;font-size:11px;color:var(--gt-muted);}
752
+ .dshgt-pkg-sub .dshgt-mono{font-size:10.5px;color:var(--gt-fg);}
753
+ .dshgt-pkg-sub a{display:inline-flex;align-items:center;gap:3px;font-size:11px;color:var(--gt-accent);text-decoration:none;}
754
+ .dshgt-pkg-sub a:hover{text-decoration:underline;}
755
+ .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;}
756
+ .dshgt-pkg-copy:hover{background:color-mix(in srgb,var(--gt-fg) 7%,var(--gt-bg));color:var(--gt-fg);}
757
+ .dshgt-pkg-copy:disabled{opacity:.4;cursor:not-allowed;}
758
+ .dshgt-main{display:flex;flex-direction:column;min-width:0;min-height:0;background:var(--gt-bg);}
759
+ .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;}
760
+ .dshgt-page-heading{min-width:0;}
761
+ .dshgt-page-head h1{margin:0;font-size:21px;line-height:1.25;font-weight:650;letter-spacing:-.012em;}
762
+ .dshgt-dek{margin:4px 0 0;font-size:13px;color:var(--gt-muted);max-width:580px;line-height:1.5;}
763
+ .dshgt-head-actions{margin-left:auto;display:flex;align-items:center;gap:8px;padding-top:2px;flex:none;}
764
+ .dshgt-main-body{flex:1;min-height:0;display:flex;min-width:0;}
765
+ .dshgt-scroll{flex:1;min-height:0;overflow-y:auto;overflow-x:hidden;scrollbar-gutter:stable;}
766
+ .dshgt-main-body > .dshgt-settings{flex:1;min-height:0;padding:16px 24px 20px;}
767
+ .dshgt-view{padding:22px 30px 38px;max-width:1180px;width:100%;display:flex;flex-direction:column;gap:18px;}
768
+ .dshgt-grow{flex:1;min-width:0;}
769
+ .dshgt-dim{font-size:12px;color:var(--gt-muted);}
770
+ .dshgt-empty{padding:48px 0;text-align:center;color:var(--gt-muted);font-size:13px;}
771
+ .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;}
772
+ .dshgt-inline-tag{margin-left:6px;vertical-align:1px;}
773
+
774
+ /* ——— 概览 ——— */
775
+ .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;}
776
+ .dshgt-delivery-top{display:flex;align-items:center;gap:10px;flex-wrap:wrap;}
777
+ .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);}
778
+ .dshgt-delivery h2{margin:4px 0 0;font-size:18px;font-weight:600;letter-spacing:-.01em;line-height:1.35;word-break:break-word;}
779
+ .dshgt-when{display:flex;gap:14px;flex-wrap:wrap;font-size:12.5px;color:var(--gt-muted);}
780
+ .dshgt-when a{color:var(--gt-accent);text-decoration:none;}
781
+ .dshgt-when a:hover{text-decoration:underline;}
782
+ .dshgt-delivery-error{margin-top:6px;}
783
+ .dshgt-artifacts{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:10px;}
784
+ .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;}
785
+ .dshgt-filechip .dshgt-mono{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
786
+ .dshgt-filechip:hover{border-color:color-mix(in srgb,var(--gt-accent) 40%,var(--gt-border));}
787
+ .dshgt-filechip-static{cursor:default;}
788
+ .dshgt-filechip-static:hover{border-color:var(--gt-border);}
789
+ .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;}
790
+ .dshgt-fact{padding:0 14px;border-left:1px solid var(--gt-border);min-width:0;}
791
+ .dshgt-fact:first-child{padding-left:0;border-left:0;}
792
+ .dshgt-fact-k{font-size:11.5px;color:var(--gt-muted);margin-bottom:2px;}
793
+ .dshgt-fact-v{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
794
+ .dshgt-block{display:flex;flex-direction:column;gap:8px;}
795
+ .dshgt-section-label{display:flex;align-items:center;gap:9px;font-size:12px;color:var(--gt-muted);letter-spacing:.02em;}
796
+ .dshgt-section-label:after{content:"";height:1px;flex:1;background:color-mix(in srgb,var(--gt-border) 70%,transparent);}
797
+ .dshgt-section-tail{margin-left:auto;font-size:11.5px;color:var(--gt-muted);}
798
+ .dshgt-ready-list{border-top:1px solid var(--gt-border);}
799
+ .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;}
800
+ .dshgt-ready-label{color:var(--gt-muted);font-size:13px;}
801
+ .dshgt-ready-value{color:var(--gt-fg);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
802
+ .dshgt-ready-extra{color:var(--gt-muted);font-size:12px;margin-left:7px;}
803
+ .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;}
804
+ .dshgt-linkbtn:hover{text-decoration:underline;}
805
+ .dshgt-week{display:flex;gap:6px;align-items:stretch;}
806
+ .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;}
807
+ .dshgt-day-clickable{cursor:pointer;}
808
+ .dshgt-day-clickable:hover{border-color:color-mix(in srgb,var(--gt-accent) 40%,var(--gt-border));}
809
+ .dshgt-day-date{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px;color:var(--gt-muted);}
810
+ .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;}
811
+ .dshgt-lamp{width:7px;height:7px;border-radius:50%;background:var(--gt-muted);flex:none;}
812
+ .dshgt-lamp.ok{background:var(--gt-ok);}
813
+ .dshgt-lamp.warn{background:var(--gt-warn);}
814
+ .dshgt-lamp.err{background:var(--gt-err);}
815
+ .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;}
816
+ .dshgt-live-top{display:flex;align-items:center;gap:9px;font-size:13px;flex-wrap:wrap;}
817
+ .dshgt-live-elapsed{margin-left:auto;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:13px;color:var(--gt-muted);}
818
+ .dshgt-live-note{display:flex;gap:10px;flex-wrap:wrap;font-size:12px;color:var(--gt-muted);line-height:1.6;}
819
+ .dshgt-live-actions{display:flex;gap:8px;}
820
+ .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;}
821
+ @keyframes dshgt-spin{to{transform:rotate(360deg);}}
822
+
823
+ /* ——— 日报 ——— */
824
+ .dshgt-digest{padding-top:20px;gap:12px;}
825
+ .dshgt-digest-subbar{display:flex;align-items:center;gap:10px;flex-wrap:wrap;}
826
+ .dshgt-datenav{display:flex;align-items:center;gap:2px;border:1px solid var(--gt-border);border-radius:8px;background:var(--gt-card);padding:2px;}
827
+ .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;}
828
+ .dshgt-icon-btn:hover:not(:disabled){background:color-mix(in srgb,var(--gt-fg) 7%,var(--gt-bg));border-color:var(--gt-border);}
829
+ .dshgt-icon-btn:disabled{opacity:.3;cursor:not-allowed;}
830
+ .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;}
831
+ .dshgt-digest-sheet{max-width:900px;width:100%;border:1px solid var(--gt-border);border-radius:12px;background:var(--gt-card);overflow:hidden;}
832
+ .dshgt-digest-body{display:flex;flex-direction:column;min-width:0;}
833
+ .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));}
834
+ .dshgt-seg{display:inline-flex;border:1px solid var(--gt-border);border-radius:8px;padding:2px;background:var(--gt-bg);gap:2px;}
835
+ .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;}
836
+ .dshgt-seg button.on{background:var(--gt-card);color:var(--gt-fg);font-weight:600;}
837
+ .dshgt-digest-doc{padding:26px 32px 22px;min-width:0;}
838
+ .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);}
839
+ .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;}
840
+ .dshgt-digest-loading{padding:40px 28px;color:var(--gt-muted);font-size:13px;}
841
+ .dshgt-digest-empty{padding:42px 36px 46px;max-width:720px;}
842
+ .dshgt-status-line{display:flex;align-items:center;gap:8px;font-size:12.5px;color:var(--gt-muted);}
843
+ .dshgt-status-line b{color:var(--gt-fg);font-weight:600;}
844
+ .dshgt-digest-empty h2{margin:14px 0 6px;font-size:20px;font-weight:650;letter-spacing:-.012em;}
845
+ .dshgt-digest-empty p{margin:0;max-width:580px;font-size:13.5px;line-height:1.7;color:var(--gt-muted);}
846
+ .dshgt-digest-error{margin-top:10px;color:var(--gt-err);}
847
+ .dshgt-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:16px;}
848
+ .dshgt-md{font-size:14px;line-height:1.7;color:var(--gt-fg);word-break:break-word;}
849
+ .dshgt-md-h{margin:20px 0 8px;font-weight:650;line-height:1.35;letter-spacing:-.01em;}
850
+ .dshgt-md-h1{font-size:24px;margin-top:0;}
851
+ .dshgt-md-h2{font-size:18px;}
852
+ .dshgt-md-h3{font-size:15.5px;}
853
+ .dshgt-md-h4,.dshgt-md-h5,.dshgt-md-h6{font-size:14px;}
854
+ .dshgt-md-p{margin:8px 0;color:color-mix(in srgb,var(--gt-fg) 84%,var(--gt-muted));}
855
+ .dshgt-md-list{margin:8px 0;padding-left:22px;}
856
+ .dshgt-md-list li{margin:3px 0;}
857
+ .dshgt-md-quote{margin:12px 0;padding:2px 0 2px 14px;border-left:3px solid var(--gt-border);color:var(--gt-muted);}
858
+ .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;}
859
+ .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;}
860
+ .dshgt-md-hr{border:0;border-top:1px solid var(--gt-border);margin:18px 0;}
861
+ .dshgt-md a{color:var(--gt-accent);text-decoration:none;}
862
+ .dshgt-md a:hover{text-decoration:underline;}
863
+
864
+ /* ——— 运行记录 ——— */
865
+ .dshgt-runs-head{display:flex;align-items:center;gap:10px;flex-wrap:wrap;}
866
+ .dshgt-runs-filter{display:flex;gap:4px;margin-left:auto;}
867
+ .dshgt-run-list{border-top:1px solid var(--gt-border);}
868
+ .dshgt-run-item{border-bottom:1px solid color-mix(in srgb,var(--gt-border) 70%,transparent);}
869
+ .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;}
870
+ .dshgt-run-row:hover{background:color-mix(in srgb,var(--gt-fg) 4%,var(--gt-bg));}
871
+ .dshgt-run-date{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;color:var(--gt-muted);white-space:nowrap;}
872
+ .dshgt-run-what{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:13px;}
873
+ .dshgt-run-muted{color:var(--gt-muted);font-size:12px;margin-left:6px;}
874
+ .dshgt-run-files,.dshgt-run-duration{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;color:var(--gt-muted);text-align:right;}
875
+ .dshgt-run-chev{color:var(--gt-muted);text-align:center;transition:transform .15s;}
876
+ .dshgt-run-item.open .dshgt-run-chev{transform:rotate(90deg);}
877
+ .dshgt-run-detail{padding:2px 6px 16px 30px;display:flex;flex-direction:column;gap:10px;}
878
+ .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);}
879
+ .dshgt-detail-k{font-size:11.5px;color:var(--gt-muted);}
880
+ .dshgt-detail-v{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;word-break:break-all;}
881
+ .dshgt-detail-v a{color:var(--gt-accent);text-decoration:none;}
882
+ .dshgt-detail-v a:hover{text-decoration:underline;}
883
+ .dshgt-run-actions{display:flex;align-items:center;gap:10px;flex-wrap:wrap;}
884
+
885
+ /* ——— 日志抽屉 / 关于 ——— */
886
+ .dshgt-log-modal{max-width:820px;width:calc(100% - 32px);}
887
+ .dshgt-log-head{display:flex;align-items:center;gap:10px;flex-wrap:wrap;}
888
+ .dshgt-log-title{margin:0;font-size:15px;}
889
+ .dshgt-log-head .dshgt-btn{margin-left:auto;}
890
+ .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;}
891
+ .dshgt-log-foot{display:flex;align-items:center;gap:10px;flex-wrap:wrap;}
892
+ .dshgt-about{max-width:520px;}
893
+ .dshgt-about-head{display:flex;align-items:center;gap:10px;}
894
+ .dshgt-about-title{margin:0;font-size:16px;}
895
+ .dshgt-about-head .dshgt-btn{margin-left:auto;}
896
+ .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;}
897
+ .dshgt-about-pkg:hover{border-color:color-mix(in srgb,var(--gt-accent) 40%,var(--gt-border));}
898
+ .dshgt-about-pkg .dshgt-mono{font-size:12px;word-break:break-all;}
899
+ .dshgt-about .dshgt-kv{grid-template-columns:110px 1fr;}
900
+ .dshgt-about .dshgt-kv a{color:var(--gt-accent);text-decoration:none;word-break:break-all;}
901
+ .dshgt-about .dshgt-kv a:hover{text-decoration:underline;}
902
+
903
+ @media (max-width:1100px){
904
+ .dshgt-app{grid-template-columns:190px minmax(0,1fr);}
905
+ .dshgt-page-head{padding:18px 22px 12px;}
906
+ .dshgt-view{padding:18px 22px 30px;}
907
+ .dshgt-facts{grid-template-columns:repeat(2,minmax(0,1fr));row-gap:10px;}
908
+ .dshgt-fact:nth-child(3){padding-left:0;border-left:0;}
909
+ .dshgt-week{flex-wrap:wrap;}
910
+ .dshgt-day{flex:1 1 88px;}
911
+ .dshgt-run-row{grid-template-columns:18px 112px minmax(0,1fr) 70px 20px;}
912
+ .dshgt-run-files{display:none;}
913
+ .dshgt-detail-grid{grid-template-columns:repeat(2,minmax(0,1fr));}
914
+ .dshgt-dateselect{min-width:180px;}
915
+ }
916
+ @media (max-width:980px){
917
+ .dshgt-app{grid-template-columns:172px minmax(0,1fr);}
918
+ .dshgt-page-head h1{font-size:19px;}
919
+ .dshgt-dek{font-size:12.5px;}
920
+ .dshgt-main-body > .dshgt-settings{padding:14px 18px 18px;}
921
+ }
415
922
  `;
416
923
  function injectStyles() {
417
924
  if (typeof document === "undefined") return () => void 0;
@@ -452,9 +959,6 @@ function Row(props) {
452
959
  props.children
453
960
  );
454
961
  }
455
- function Inline(props) {
456
- return h("div", { className: "dshgt-inline" }, props.children);
457
- }
458
962
  function Input(props) {
459
963
  const { value, onInput, placeholder, mono, width, password, ...rest } = props;
460
964
  return h("input", {
@@ -567,6 +1071,21 @@ function localTime(iso) {
567
1071
  if (Number.isNaN(date.getTime())) return "—";
568
1072
  return date.toLocaleString();
569
1073
  }
1074
+ async function copyText(value) {
1075
+ try {
1076
+ if (typeof navigator === "undefined" || navigator.clipboard === void 0) return false;
1077
+ await navigator.clipboard.writeText(value);
1078
+ return true;
1079
+ } catch {
1080
+ return false;
1081
+ }
1082
+ }
1083
+ function shortTime(iso) {
1084
+ if (iso === null || iso === void 0) return "—";
1085
+ const date = new Date(iso);
1086
+ if (Number.isNaN(date.getTime())) return "—";
1087
+ return date.toLocaleString(void 0, { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" });
1088
+ }
570
1089
 
571
1090
  // client/api.ts
572
1091
  var API = "/api/morehao/github-trending";
@@ -592,6 +1111,12 @@ async function apiCall(method, path, body, options = {}) {
592
1111
  }
593
1112
  return json;
594
1113
  }
1114
+ function getPreview(runId, signal) {
1115
+ return apiCall("GET", `/preview?id=${encodeURIComponent(runId)}`, void 0, signal === void 0 ? {} : { signal });
1116
+ }
1117
+ function getLog(runId, signal) {
1118
+ return apiCall("GET", `/log?id=${encodeURIComponent(runId)}`, void 0, signal === void 0 ? {} : { signal });
1119
+ }
595
1120
 
596
1121
  // client/store.ts
597
1122
  var POLL_MS = 5e3;
@@ -626,15 +1151,67 @@ function useStore() {
626
1151
  return { state, error, loading, refresh };
627
1152
  }
628
1153
 
1154
+ // client/credential.ts
1155
+ function credentialView(model, t) {
1156
+ if (model.mode === "proxy") {
1157
+ return { ok: true, kind: "ok", label: t("auth.hostProxy"), hint: t("model.credential.proxyHint") };
1158
+ }
1159
+ const credential = model.credential;
1160
+ if (credential.configured) {
1161
+ return {
1162
+ ok: true,
1163
+ kind: "ok",
1164
+ label: `${t("auth.configured")}${credential.source === null ? "" : ` · ${credential.source}`}`,
1165
+ hint: t("model.credential.directHint")
1166
+ };
1167
+ }
1168
+ const missing = t("auth.missing");
1169
+ return {
1170
+ ok: false,
1171
+ kind: "err",
1172
+ label: credential.ref.trim() === "" ? missing : `${missing} · ${credential.ref}`,
1173
+ hint: t("model.credential.missingHint")
1174
+ };
1175
+ }
1176
+
629
1177
  // client/settings.ts
630
1178
  var LANGUAGES = ["all", "typescript", "python", "go", "rust", "java", "c", "cpp", "javascript", "csharp"];
1179
+ var SECTIONS = ["source", "model", "repo", "auth", "scope", "schedule"];
1180
+ var EMPTY_DIRTY = { source: false, model: false, repo: false, auth: false, scope: false, schedule: false };
1181
+ function cloneSettings(settings) {
1182
+ return JSON.parse(JSON.stringify(settings));
1183
+ }
1184
+ function sectionSlice(settings, section) {
1185
+ switch (section) {
1186
+ case "source":
1187
+ return { corePath: settings.corePath };
1188
+ case "model":
1189
+ return { model: settings.model };
1190
+ case "repo":
1191
+ return { repo: settings.repo, pr: settings.pr, output: settings.output };
1192
+ case "auth":
1193
+ return { github: settings.github };
1194
+ case "scope":
1195
+ return { trending: settings.trending };
1196
+ case "schedule":
1197
+ return { schedule: settings.schedule };
1198
+ }
1199
+ }
1200
+ function sectionEquals(a, b, section) {
1201
+ return JSON.stringify(sectionSlice(a, section)) === JSON.stringify(sectionSlice(b, section));
1202
+ }
1203
+ function applySection(target, source, section) {
1204
+ const next = cloneSettings(target);
1205
+ Object.assign(next, sectionSlice(source, section));
1206
+ return next;
1207
+ }
631
1208
  function SettingsView(props) {
632
1209
  const { t, store } = props;
633
1210
  const state = store.state;
634
1211
  const [draft, setDraft] = import_react.default.useState(null);
635
- const [dirty, setDirty] = import_react.default.useState(false);
636
- const [saving, setSaving] = import_react.default.useState(false);
637
- const [saved, setSaved] = import_react.default.useState(false);
1212
+ const [dirtySections, setDirtySections] = import_react.default.useState({ ...EMPTY_DIRTY });
1213
+ const [savingSection, setSavingSection] = import_react.default.useState(null);
1214
+ const [savedSection, setSavedSection] = import_react.default.useState(null);
638
1215
  const [problems, setProblems] = import_react.default.useState([]);
639
1216
  const [message, setMessage] = import_react.default.useState(null);
640
1217
  const [preflight, setPreflight] = import_react.default.useState(null);
@@ -642,31 +1219,69 @@ function SettingsView(props) {
642
1219
  const [rechecking, setRechecking] = import_react.default.useState(false);
643
1220
  const [downloading, setDownloading] = import_react.default.useState(false);
644
1221
  const [quickOpen, setQuickOpen] = import_react.default.useState(false);
1222
+ const [section, setSection] = import_react.default.useState("source");
1223
+ const [branchInfo, setBranchInfo] = import_react.default.useState(null);
1224
+ const [branchLoading, setBranchLoading] = import_react.default.useState(false);
1225
+ const [testing, setTesting] = import_react.default.useState(false);
1226
+ const [githubTest, setGithubTest] = import_react.default.useState(null);
1227
+ const anyDirty = SECTIONS.some((id) => dirtySections[id]);
1228
+ const sectionEditable = section !== "source";
1229
+ import_react.default.useEffect(() => {
1230
+ if (state !== null && !anyDirty) setDraft(state.settings);
1231
+ }, [state, anyDirty]);
1232
+ import_react.default.useEffect(() => {
1233
+ setProblems([]);
1234
+ }, [section]);
1235
+ const repoPath = draft === null ? "" : draft.repo.path;
645
1236
  import_react.default.useEffect(() => {
646
- if (state !== null && !dirty) setDraft(state.settings);
647
- }, [state, dirty]);
1237
+ if (repoPath.trim() === "") {
1238
+ setBranchInfo(null);
1239
+ return () => void 0;
1240
+ }
1241
+ let cancelled = false;
1242
+ setBranchLoading(true);
1243
+ apiCall("GET", `/repo/branch?path=${encodeURIComponent(repoPath)}`).then((result) => {
1244
+ if (!cancelled) setBranchInfo(result.repo);
1245
+ }).catch((error) => {
1246
+ if (!cancelled) setBranchInfo({ source: "none", message: error instanceof Error ? error.message : String(error) });
1247
+ }).finally(() => {
1248
+ if (!cancelled) setBranchLoading(false);
1249
+ });
1250
+ return () => {
1251
+ cancelled = true;
1252
+ };
1253
+ }, [repoPath]);
648
1254
  const update = (mutate) => {
649
1255
  if (draft === null) return;
650
- const next = JSON.parse(JSON.stringify(draft));
1256
+ const prev = draft;
1257
+ const next = cloneSettings(draft);
651
1258
  mutate(next);
652
1259
  setDraft(next);
653
- setDirty(true);
654
- setSaved(false);
1260
+ const changed = SECTIONS.filter((id) => !sectionEquals(prev, next, id));
1261
+ if (changed.length > 0) {
1262
+ setDirtySections((current) => {
1263
+ const merged = { ...current };
1264
+ for (const id of changed) merged[id] = true;
1265
+ return merged;
1266
+ });
1267
+ setSavedSection((current) => current !== null && changed.includes(current) ? null : current);
1268
+ setProblems([]);
1269
+ }
655
1270
  };
656
- const save = async () => {
1271
+ const save = async (section2) => {
657
1272
  if (draft === null) return;
658
- setSaving(true);
1273
+ setSavingSection(section2);
659
1274
  setProblems([]);
660
1275
  try {
661
1276
  const result = await apiCall(
662
1277
  "POST",
663
1278
  "/config",
664
- { settings: draft }
1279
+ { settings: draft, section: section2 }
665
1280
  );
666
1281
  if (result.saved) {
667
- setDraft(result.settings);
668
- setDirty(false);
669
- setSaved(true);
1282
+ setDraft((current) => current === null ? current : applySection(current, result.settings, section2));
1283
+ setDirtySections((current) => ({ ...current, [section2]: false }));
1284
+ setSavedSection(section2);
670
1285
  setMessage(null);
671
1286
  await store.refresh({ quiet: true });
672
1287
  } else {
@@ -675,7 +1290,7 @@ function SettingsView(props) {
675
1290
  } catch (error) {
676
1291
  setMessage(error instanceof Error ? error.message : String(error));
677
1292
  } finally {
678
- setSaving(false);
1293
+ setSavingSection(null);
679
1294
  }
680
1295
  };
681
1296
  const runPreflight = async () => {
@@ -715,6 +1330,19 @@ function SettingsView(props) {
715
1330
  setDownloading(false);
716
1331
  }
717
1332
  };
1333
+ const testGithub = async () => {
1334
+ if (draft === null) return;
1335
+ setTesting(true);
1336
+ setGithubTest(null);
1337
+ try {
1338
+ const result = await apiCall("POST", "/github/test", { settings: draft });
1339
+ setGithubTest(result.github);
1340
+ } catch (error) {
1341
+ setGithubTest({ ok: false, message: error instanceof Error ? error.message : String(error) });
1342
+ } finally {
1343
+ setTesting(false);
1344
+ }
1345
+ };
718
1346
  const importCli = async () => {
719
1347
  try {
720
1348
  const result = await apiCall(
@@ -725,8 +1353,8 @@ function SettingsView(props) {
725
1353
  const payload = result.import;
726
1354
  if (payload.ok && payload.settings !== void 0) {
727
1355
  setDraft(payload.settings);
728
- setDirty(false);
729
- setSaved(true);
1356
+ setDirtySections({ ...EMPTY_DIRTY });
1357
+ setSavedSection(null);
730
1358
  setMessage(t("settings.import.done", { fields: payload.imported.join("、") }));
731
1359
  await store.refresh({ quiet: true });
732
1360
  } else {
@@ -750,313 +1378,333 @@ function SettingsView(props) {
750
1378
  }
751
1379
  const providers = state.core.supportedProviders.length > 0 ? state.core.supportedProviders : ["deepseek", "openai-compatible"];
752
1380
  const coreKind = state.core.state === "ready" ? "ok" : state.core.state === "incompatible" ? "warn" : "err";
753
- return h(
754
- "div",
755
- { className: "dshgt-root" },
1381
+ const branchText = branchLoading ? t("branch.loading") : branchInfo?.branch !== void 0 ? branchInfo.branch : draft.repo.baseBranch !== "" ? draft.repo.baseBranch : "—";
1382
+ const branchHint = branchInfo?.source === "origin-head" ? t("branch.fromOrigin") : branchInfo?.source === "head" ? t("branch.fromHead") : branchInfo?.message !== void 0 ? branchInfo.message : t("branch.readonlyHint");
1383
+ const isGh = draft.github.authMode === "gh";
1384
+ const sections = [
1385
+ { id: "source", label: t("st.source") },
1386
+ { id: "model", label: t("st.model") },
1387
+ { id: "repo", label: t("st.repo") },
1388
+ { id: "auth", label: t("st.auth") },
1389
+ { id: "scope", label: t("st.scope") },
1390
+ { id: "schedule", label: t("st.schedule") }
1391
+ ];
1392
+ const sourceCard = h(
1393
+ Card,
1394
+ {
1395
+ title: t("card.source.title"),
1396
+ hint: t("card.source.hint"),
1397
+ extra: h(Btn, { small: true, onClick: () => void recheck(), disabled: rechecking }, rechecking ? t("core.checking") : t("core.recheck"))
1398
+ },
1399
+ h("p", { className: "dshgt-desc" }, t("card.source.desc")),
756
1400
  h(
757
- "div",
758
- { className: "dshgt-head" },
759
- h("span", { className: "dshgt-title" }, t("section.title")),
760
- h("span", { className: "dshgt-sub" }, t("section.subtitle")),
761
- h("span", { className: "dshgt-hint", style: { marginLeft: "auto" } }, `v${state.plugin.version}`)
1401
+ Row,
1402
+ { label: t("card.source.title") },
1403
+ h("span", { className: "dshgt-pill" }, h("span", { className: "dshgt-dot ok" }), t("source.local")),
1404
+ h("span", { className: "dshgt-hint" }, t("source.local.hint"))
762
1405
  ),
763
- // ---- 来源与核心 ----
764
1406
  h(
765
- Card,
766
- {
767
- title: t("card.source.title"),
768
- hint: t("card.source.hint"),
769
- extra: h(Btn, { small: true, onClick: () => void recheck(), disabled: rechecking }, rechecking ? t("core.checking") : t("core.recheck"))
770
- },
771
- h("p", { className: "dshgt-desc" }, t("card.source.desc")),
772
- h(
773
- Row,
774
- { label: t("card.source.title") },
775
- h("span", { className: "dshgt-pill" }, h("span", { className: "dshgt-dot ok" }), t("source.local")),
776
- h("span", { className: "dshgt-hint" }, t("source.local.hint"))
777
- ),
1407
+ Row,
1408
+ { label: t("core.label") },
1409
+ h("span", { className: "dshgt-pill" }, h("span", { className: `dshgt-dot ${coreKind}` }), state.core.version ?? t(`core.${state.core.state}`)),
1410
+ state.core.ipcSchema === void 0 ? null : h("span", { className: "dshgt-hint dshgt-mono" }, `ipc ${state.core.ipcSchema} · digest ${state.core.digestSchema ?? "?"}`),
1411
+ state.core.release === void 0 ? null : h("span", { className: "dshgt-hint dshgt-mono" }, `${state.core.release.tag} · ${state.core.release.asset}`),
778
1412
  h(
779
- Row,
780
- { label: t("core.label") },
781
- h("span", { className: "dshgt-pill" }, h("span", { className: `dshgt-dot ${coreKind}` }), state.core.version ?? t(`core.${state.core.state}`)),
782
- state.core.ipcSchema === void 0 ? null : h("span", { className: "dshgt-hint dshgt-mono" }, `ipc ${state.core.ipcSchema} · digest ${state.core.digestSchema ?? "?"}`),
783
- state.core.release === void 0 ? null : h("span", { className: "dshgt-hint dshgt-mono" }, `${state.core.release.tag} · ${state.core.release.asset}`),
784
- h(
785
- "span",
786
- { className: "dshgt-hint" },
787
- state.core.download.state === "override" ? `覆盖路径:${state.core.override?.path ?? ""}` : state.core.release?.cached === true ? t("core.cached") : t("core.notDownloaded")
788
- ),
789
- state.core.download.state === "override" ? null : h(
790
- Btn,
791
- { small: true, onClick: () => void downloadCore(), disabled: downloading },
792
- downloading ? t("core.downloading") : state.core.release?.cached === true ? t("core.redownload") : t("core.download")
793
- )
1413
+ "span",
1414
+ { className: "dshgt-hint" },
1415
+ state.core.download.state === "override" ? t("core.override", { path: state.core.override?.path ?? "" }) : state.core.release?.cached === true ? t("core.cached") : t("core.notDownloaded")
794
1416
  ),
795
- state.core.release === void 0 ? null : h("p", { className: "dshgt-hint dshgt-mono", style: { fontSize: 11 } }, `${state.core.release.url} → ${state.core.release.binaryPath}`),
796
- 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"))),
797
- h("p", { className: "dshgt-hint" }, t("core.order")),
798
- 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,
799
- h(Row, { label: "TRENDING_DIGEST_CORE" }, h(Input, { mono: true, width: 420, value: draft.corePath, placeholder: "/path/to/trendradar", onInput: (value) => update((next) => {
800
- next.corePath = value;
801
- }) }))
1417
+ state.core.download.state === "override" ? null : h(
1418
+ Btn,
1419
+ { small: true, onClick: () => void downloadCore(), disabled: downloading },
1420
+ downloading ? t("core.downloading") : state.core.release?.cached === true ? t("core.redownload") : t("core.download")
1421
+ )
1422
+ ),
1423
+ state.core.release === void 0 ? null : h("p", { className: "dshgt-hint dshgt-mono", style: { fontSize: 11 } }, `${state.core.release.url} → ${state.core.release.binaryPath}`),
1424
+ 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"))),
1425
+ h("p", { className: "dshgt-hint" }, t("core.order")),
1426
+ 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
1427
+ );
1428
+ const modelCredential = credentialView({ mode: draft.model.mode, credential: state.model.credential }, t);
1429
+ const modelCard = h(
1430
+ Card,
1431
+ {
1432
+ title: t("card.model.title"),
1433
+ hint: t("card.model.hint"),
1434
+ extra: h(Btn, { small: true, variant: "primary", onClick: () => setQuickOpen(true) }, t("model.quickfill"))
1435
+ },
1436
+ h("p", { className: "dshgt-desc" }, t("model.quickfill.desc")),
1437
+ h(
1438
+ Row,
1439
+ { label: t("model.provider") },
1440
+ h(Select, {
1441
+ value: draft.model.provider,
1442
+ options: providers.map((provider) => ({ value: provider, label: provider })),
1443
+ onChange: (value) => update((next) => {
1444
+ next.model.provider = value;
1445
+ })
1446
+ }),
1447
+ h("span", { className: "dshgt-hint" }, draft.model.mode === "proxy" ? t("model.mode.proxy") : t("model.mode.direct"))
802
1448
  ),
803
- // ---- 模型 ----
1449
+ h(Row, { label: t("model.baseURL") }, h(Input, {
1450
+ mono: true,
1451
+ width: 420,
1452
+ value: draft.model.baseURL,
1453
+ placeholder: t("model.baseURL.placeholder"),
1454
+ onInput: (value) => update((next) => {
1455
+ next.model.baseURL = value;
1456
+ })
1457
+ })),
804
1458
  h(
805
- Card,
806
- {
807
- title: t("card.model.title"),
808
- hint: t("card.model.hint"),
809
- extra: h(Btn, { small: true, variant: "primary", onClick: () => setQuickOpen(true) }, t("model.quickfill"))
810
- },
811
- h("p", { className: "dshgt-desc" }, t("model.quickfill.desc")),
812
- h(
813
- Row,
814
- { label: t("model.provider") },
815
- h(Select, {
816
- value: draft.model.provider,
817
- options: providers.map((provider) => ({ value: provider, label: provider })),
818
- onChange: (value) => update((next) => {
819
- next.model.provider = value;
820
- })
821
- }),
822
- h("span", { className: "dshgt-hint" }, state.model.mode === "proxy" ? t("model.mode.proxy") : t("model.mode.direct"))
823
- ),
824
- h(Row, { label: t("model.baseURL") }, h(Input, {
1459
+ Row,
1460
+ { label: t("model.model") },
1461
+ h(Input, {
825
1462
  mono: true,
826
- width: 420,
827
- value: draft.model.baseURL,
828
- placeholder: t("model.baseURL.placeholder"),
1463
+ width: 260,
1464
+ value: draft.model.model,
1465
+ placeholder: t("model.model.placeholder"),
829
1466
  onInput: (value) => update((next) => {
830
- next.model.baseURL = value;
1467
+ next.model.model = value;
831
1468
  })
832
- })),
833
- h(
834
- Row,
835
- { label: t("model.model") },
836
- h(Input, {
837
- mono: true,
838
- width: 260,
839
- value: draft.model.model,
840
- placeholder: t("model.model.placeholder"),
841
- onInput: (value) => update((next) => {
842
- next.model.model = value;
843
- })
844
- }),
845
- h(Btn, { small: true, onClick: () => void runPreflight(), disabled: preflighting }, preflighting ? t("model.preflighting") : t("model.preflight"))
846
- ),
847
- h(
848
- Row,
849
- { label: t("model.keyRef") },
850
- h(Input, { mono: true, width: 220, value: draft.model.apiKeyRef, onInput: (value) => update((next) => {
851
- next.model.apiKeyRef = value;
852
- }) }),
853
- h(
854
- Pill,
855
- { kind: state.model.credential.configured ? "ok" : "err" },
856
- state.model.credential.configured ? `${t("auth.configured")}${state.model.credential.source === null ? "" : ` · ${state.model.credential.source}`}` : t("auth.missing")
857
- )
858
- ),
859
- h(
860
- Row,
861
- { label: t("model.params") },
862
- h(Input, { mono: true, width: 70, value: String(draft.model.temperature), onInput: (value) => update((next) => {
863
- next.model.temperature = Number(value) || 0;
864
- }) }),
865
- h(Input, { mono: true, width: 90, value: String(draft.model.maxTokens), onInput: (value) => update((next) => {
866
- next.model.maxTokens = Math.round(Number(value) || 0);
867
- }) }),
868
- h(Toggle, { checked: draft.model.allowDegraded, label: t("model.allowDegraded"), onChange: (value) => update((next) => {
869
- next.model.allowDegraded = value;
870
- }) })
871
- ),
872
- preflight === null ? null : h(
873
- Alert,
874
- { kind: preflight.ok ? "ok" : "err", title: preflight.ok ? t("state.ok") : t("error.prefix") },
875
- h("div", null, `${preflight.mode} · ${duration(preflight.durationMs)}${preflight.endpoint === void 0 ? "" : ` · ${preflight.endpoint}`}`),
876
- preflight.message === void 0 ? null : h("div", null, preflight.message),
877
- preflight.hint === void 0 ? null : h("div", { className: "dshgt-hint" }, preflight.hint)
878
- )
1469
+ }),
1470
+ h(Btn, { small: true, onClick: () => void runPreflight(), disabled: preflighting }, preflighting ? t("model.preflighting") : t("model.preflight"))
879
1471
  ),
880
- // ---- 仓库 ----
881
1472
  h(
882
- Card,
883
- { title: t("card.repo.title"), hint: t("card.repo.hint") },
884
- h(
885
- Row,
886
- { label: t("repo.path") },
887
- h(Input, {
888
- mono: true,
889
- width: 380,
890
- value: draft.repo.path,
891
- placeholder: t("repo.path.placeholder"),
892
- onInput: (value) => update((next) => {
893
- next.repo.path = value;
894
- })
895
- }),
896
- props.pickDirectory === void 0 ? null : h(Btn, {
897
- small: true,
898
- onClick: () => {
899
- void props.pickDirectory?.().then((picked) => {
900
- if (typeof picked === "string" && picked !== "") update((next) => {
901
- next.repo.path = picked;
902
- });
1473
+ Row,
1474
+ { label: t("model.credential") },
1475
+ h(Pill, { kind: modelCredential.kind }, modelCredential.label),
1476
+ h("span", { className: "dshgt-hint" }, modelCredential.hint)
1477
+ ),
1478
+ h(
1479
+ Row,
1480
+ { label: t("model.params") },
1481
+ h(Input, { mono: true, width: 70, value: String(draft.model.temperature), onInput: (value) => update((next) => {
1482
+ next.model.temperature = Number(value) || 0;
1483
+ }) }),
1484
+ h(Input, { mono: true, width: 90, value: String(draft.model.maxTokens), onInput: (value) => update((next) => {
1485
+ next.model.maxTokens = Math.round(Number(value) || 0);
1486
+ }) }),
1487
+ h(Toggle, { checked: draft.model.allowDegraded, label: t("model.allowDegraded"), onChange: (value) => update((next) => {
1488
+ next.model.allowDegraded = value;
1489
+ }) })
1490
+ ),
1491
+ preflight === null ? null : h(
1492
+ Alert,
1493
+ { kind: preflight.ok ? "ok" : "err", title: preflight.ok ? t("state.ok") : t("error.prefix") },
1494
+ h("div", null, `${preflight.mode} · ${duration(preflight.durationMs)}${preflight.endpoint === void 0 ? "" : ` · ${preflight.endpoint}`}`),
1495
+ preflight.message === void 0 ? null : h("div", null, preflight.message),
1496
+ preflight.hint === void 0 ? null : h("div", { className: "dshgt-hint" }, preflight.hint)
1497
+ )
1498
+ );
1499
+ const repoCard = h(
1500
+ Card,
1501
+ { title: t("card.repo.title"), hint: t("card.repo.hint") },
1502
+ h(
1503
+ Row,
1504
+ { label: t("repo.path") },
1505
+ h("span", { className: "dshgt-mono", style: { wordBreak: "break-all" } }, draft.repo.path === "" ? t("repo.path.empty") : draft.repo.path),
1506
+ props.pickDirectory === void 0 ? null : h(Btn, {
1507
+ small: true,
1508
+ onClick: () => {
1509
+ void props.pickDirectory?.().then((picked) => {
1510
+ if (typeof picked === "string" && picked !== "") update((next) => {
1511
+ next.repo.path = picked;
903
1512
  });
904
- }
905
- }, t("repo.pick"))
906
- ),
907
- h(
908
- Row,
909
- { label: t("repo.baseBranch") },
910
- h(Input, { mono: true, width: 120, value: draft.repo.baseBranch, onInput: (value) => update((next) => {
911
- next.repo.baseBranch = value;
912
- }) }),
913
- h("span", { className: "dshgt-hint" }, t("repo.remote")),
914
- h(Input, { mono: true, width: 100, value: draft.repo.remote, onInput: (value) => update((next) => {
915
- next.repo.remote = value;
916
- }) }),
917
- h("span", { className: "dshgt-hint" }, t("repo.outputDir")),
918
- h(Input, { mono: true, width: 140, value: draft.output.dir, onInput: (value) => update((next) => {
919
- next.output.dir = value;
920
- }) })
921
- ),
922
- h(
923
- Row,
924
- { label: t("repo.commitMode") },
925
- h(Select, {
926
- value: draft.repo.commitMode,
927
- options: [
928
- { value: "branch", label: t("repo.commitMode.branch") },
929
- { value: "direct", label: t("repo.commitMode.direct") }
930
- ],
931
- onChange: (value) => update((next) => {
932
- next.repo.commitMode = value === "direct" ? "direct" : "branch";
933
- })
934
- }),
935
- h(Toggle, { checked: draft.pr.autoMerge, label: t("repo.autoMerge"), onChange: (value) => update((next) => {
936
- next.pr.autoMerge = value;
937
- }) })
938
- ),
939
- h(Row, { label: t("repo.titleTemplate") }, h(Input, {
940
- mono: true,
941
- width: 360,
942
- value: draft.pr.titleTemplate,
943
- onInput: (value) => update((next) => {
944
- next.pr.titleTemplate = value;
945
- })
946
- }))
1513
+ });
1514
+ }
1515
+ }, t("repo.pick"))
947
1516
  ),
948
- // ---- 认证 ----
949
1517
  h(
950
- Card,
951
- { title: t("card.auth.title"), hint: t("card.auth.hint") },
952
- h(
953
- Row,
954
- { label: t("auth.mode") },
955
- h(Select, {
956
- value: draft.github.authMode,
957
- options: [
958
- { value: "device", label: t("auth.device") },
959
- { value: "pat", label: t("auth.pat") },
960
- { value: "gh", label: t("auth.gh") }
961
- ],
962
- onChange: (value) => update((next) => {
963
- next.github.authMode = value === "pat" ? "pat" : value === "gh" ? "gh" : "device";
964
- })
965
- }),
966
- h("span", { className: "dshgt-hint" }, t("auth.tokenRef")),
967
- h(Input, { mono: true, width: 180, value: draft.github.tokenRef, onInput: (value) => update((next) => {
968
- next.github.tokenRef = value;
969
- }) }),
970
- h(Pill, { kind: state.github.configured ? "ok" : "warn" }, state.github.configured ? `${t("auth.configured")} · ${state.github.source}` : t("auth.missing"))
971
- ),
972
- state.github.configured ? null : h("p", { className: "dshgt-hint" }, "未配置 token 时 push / 开 PR 会以 GITHUB_AUTH 失败;试跑(dryRun)不受影响。")
1518
+ Row,
1519
+ { label: t("repo.baseBranch") },
1520
+ h("span", { className: "dshgt-mono" }, branchText),
1521
+ h("span", { className: "dshgt-hint" }, branchHint)
973
1522
  ),
974
- // ---- 榜单 ----
975
1523
  h(
976
- Card,
977
- { title: t("card.scope.title") },
978
- h(
979
- Row,
980
- { label: t("scope.period") },
981
- h(Select, {
982
- value: draft.trending.since,
983
- options: [
984
- { value: "daily", label: t("scope.daily") },
985
- { value: "weekly", label: t("scope.weekly") }
986
- ],
987
- onChange: (value) => update((next) => {
988
- next.trending.since = value === "weekly" ? "weekly" : "daily";
989
- })
990
- }),
991
- h("span", { className: "dshgt-hint" }, t("scope.topN")),
992
- h(Input, { mono: true, width: 70, value: String(draft.trending.topN), onInput: (value) => update((next) => {
993
- next.trending.topN = Math.round(Number(value) || 0);
994
- }) })
995
- ),
996
- h(
997
- Row,
998
- { label: t("scope.languages") },
999
- h(Chips, {
1000
- values: LANGUAGES,
1001
- selected: draft.trending.languages,
1002
- onChange: (next) => update((draftNext) => {
1003
- draftNext.trending.languages = next;
1004
- })
1524
+ Row,
1525
+ { label: t("repo.outputDir") },
1526
+ h(Input, { mono: true, width: 200, value: draft.output.dir, onInput: (value) => update((next) => {
1527
+ next.output.dir = value;
1528
+ }) }),
1529
+ h("span", { className: "dshgt-hint" }, t("repo.outputDir.hint"))
1530
+ ),
1531
+ h(
1532
+ Row,
1533
+ { label: t("repo.commitMode") },
1534
+ h(Select, {
1535
+ value: draft.repo.commitMode,
1536
+ options: [
1537
+ { value: "branch", label: t("repo.commitMode.branch") },
1538
+ { value: "direct", label: t("repo.commitMode.direct") }
1539
+ ],
1540
+ onChange: (value) => update((next) => {
1541
+ next.repo.commitMode = value === "direct" ? "direct" : "branch";
1542
+ })
1543
+ }),
1544
+ h(Toggle, { checked: draft.pr.autoMerge, label: t("repo.autoMerge"), onChange: (value) => update((next) => {
1545
+ next.pr.autoMerge = value;
1546
+ }) })
1547
+ )
1548
+ );
1549
+ const authCard = h(
1550
+ Card,
1551
+ { title: t("card.auth.title"), hint: t("card.auth.hint") },
1552
+ h(
1553
+ Row,
1554
+ { label: t("auth.mode") },
1555
+ h(Select, {
1556
+ value: draft.github.authMode,
1557
+ options: [
1558
+ { value: "gh", label: t("auth.gh") },
1559
+ { value: "pat", label: t("auth.pat") }
1560
+ ],
1561
+ onChange: (value) => update((next) => {
1562
+ next.github.authMode = value === "pat" ? "pat" : "gh";
1005
1563
  })
1564
+ }),
1565
+ h(Pill, { kind: state.github.configured ? "ok" : "warn" }, state.github.configured ? `${t("auth.configured")} · ${state.github.source}` : t("auth.missing"))
1566
+ ),
1567
+ h("p", { className: "dshgt-hint" }, isGh ? t("auth.gh.hint") : t("auth.pat.hint")),
1568
+ isGh ? null : h(
1569
+ Row,
1570
+ { label: t("auth.tokenRef") },
1571
+ h(Input, { mono: true, width: 220, value: draft.github.tokenRef, placeholder: "GITHUB_TOKEN", onInput: (value) => update((next) => {
1572
+ next.github.tokenRef = value;
1573
+ }) }),
1574
+ h("span", { className: "dshgt-hint" }, t("auth.tokenRef.hint"))
1575
+ ),
1576
+ h(
1577
+ Row,
1578
+ { label: t("auth.test") },
1579
+ h(Btn, { small: true, onClick: () => void testGithub(), disabled: testing }, testing ? t("auth.testing") : t("auth.test")),
1580
+ githubTest === null ? null : h(
1581
+ Pill,
1582
+ { kind: githubTest.ok ? "ok" : "err" },
1583
+ githubTest.ok ? `${t("auth.test.ok")}${githubTest.login === void 0 ? "" : ` · @${githubTest.login}`}` : githubTest.message ?? t("auth.test.fail")
1006
1584
  )
1585
+ )
1586
+ );
1587
+ const scopeCard = h(
1588
+ Card,
1589
+ { title: t("card.scope.title") },
1590
+ h(
1591
+ Row,
1592
+ { label: t("scope.period") },
1593
+ h(Select, {
1594
+ value: draft.trending.since,
1595
+ options: [
1596
+ { value: "daily", label: t("scope.daily") },
1597
+ { value: "weekly", label: t("scope.weekly") }
1598
+ ],
1599
+ onChange: (value) => update((next) => {
1600
+ next.trending.since = value === "weekly" ? "weekly" : "daily";
1601
+ })
1602
+ }),
1603
+ h("span", { className: "dshgt-hint" }, t("scope.topN")),
1604
+ h(Input, { mono: true, width: 70, value: String(draft.trending.topN), onInput: (value) => update((next) => {
1605
+ next.trending.topN = Math.round(Number(value) || 0);
1606
+ }) })
1007
1607
  ),
1008
- // ---- 调度 ----
1009
1608
  h(
1010
- Card,
1011
- { title: t("card.schedule.title"), hint: state.schedule.nextRunAt === void 0 ? void 0 : `${t("schedule.next")} ${localTime(state.schedule.nextRunAt)}` },
1012
- h(
1013
- Row,
1014
- { label: t("schedule.enabled") },
1015
- h(Toggle, { checked: draft.schedule.enabled, onChange: (value) => update((next) => {
1016
- next.schedule.enabled = value;
1017
- }) }),
1018
- h("span", { className: "dshgt-hint" }, t("schedule.time")),
1019
- h(Input, { mono: true, width: 110, value: draft.schedule.time, onInput: (value) => update((next) => {
1020
- next.schedule.time = value;
1021
- }) }),
1022
- h("span", { className: "dshgt-hint" }, t("schedule.timezone")),
1023
- h(Input, { mono: true, width: 170, value: draft.schedule.timezone, onInput: (value) => update((next) => {
1024
- next.schedule.timezone = value;
1025
- }) })
1026
- ),
1027
- h("p", { className: "dshgt-hint" }, t("schedule.source"))
1609
+ Row,
1610
+ { label: t("scope.languages") },
1611
+ h(Chips, {
1612
+ values: LANGUAGES,
1613
+ selected: draft.trending.languages,
1614
+ onChange: (next) => update((draftNext) => {
1615
+ draftNext.trending.languages = next;
1616
+ })
1617
+ })
1618
+ )
1619
+ );
1620
+ const scheduleCard = h(
1621
+ Card,
1622
+ { title: t("card.schedule.title"), hint: state.schedule.nextRunAt === void 0 ? void 0 : `${t("schedule.next")} ${localTime(state.schedule.nextRunAt)}` },
1623
+ h(
1624
+ Row,
1625
+ { label: t("schedule.enabled") },
1626
+ h(Toggle, { checked: draft.schedule.enabled, onChange: (value) => update((next) => {
1627
+ next.schedule.enabled = value;
1628
+ }) }),
1629
+ h("span", { className: "dshgt-hint" }, t("schedule.time")),
1630
+ h(Input, { mono: true, width: 110, value: draft.schedule.time, onInput: (value) => update((next) => {
1631
+ next.schedule.time = value;
1632
+ }) }),
1633
+ h("span", { className: "dshgt-hint" }, t("schedule.timezone")),
1634
+ h(Input, { mono: true, width: 170, value: draft.schedule.timezone, onInput: (value) => update((next) => {
1635
+ next.schedule.timezone = value;
1636
+ }) })
1637
+ ),
1638
+ h("p", { className: "dshgt-hint" }, t("schedule.source"))
1639
+ );
1640
+ const currentCard = section === "source" ? sourceCard : section === "model" ? modelCard : section === "repo" ? repoCard : section === "auth" ? authCard : section === "scope" ? scopeCard : scheduleCard;
1641
+ return h(
1642
+ "div",
1643
+ { className: "dshgt-settings" },
1644
+ h(
1645
+ "nav",
1646
+ { className: "dshgt-vtabs", role: "tablist", "aria-orientation": "vertical" },
1647
+ sections.map(
1648
+ (item) => h(
1649
+ "button",
1650
+ {
1651
+ key: item.id,
1652
+ type: "button",
1653
+ className: "dshgt-vtab",
1654
+ role: "tab",
1655
+ "aria-selected": section === item.id ? "true" : "false",
1656
+ onClick: () => setSection(item.id)
1657
+ },
1658
+ item.label,
1659
+ dirtySections[item.id] ? h("span", { className: "dshgt-vtab-dirty", title: t("settings.dirty") }) : null
1660
+ )
1661
+ )
1028
1662
  ),
1029
- // ---- 保存 / 排障 ----
1030
- problems.length > 0 ? h(Alert, { kind: "err", title: t("error.prefix") }, h("ul", { style: { margin: 0, paddingLeft: 16 } }, problems.map((problem, index) => h("li", { key: index }, `${problem.field}:${problem.message}`)))) : null,
1031
- message === null ? null : h(Alert, { kind: "ok" }, message),
1032
1663
  h(
1033
1664
  "div",
1034
- { className: "dshgt-footer" },
1035
- h(Btn, { variant: "primary", onClick: () => void save(), disabled: saving || !dirty }, saving ? t("settings.saving") : dirty ? t("settings.save") : saved ? t("settings.saved") : t("settings.save")),
1036
- h(Btn, { onClick: () => void importCli() }, t("settings.import")),
1037
- h(Btn, { onClick: copyConfig }, t("settings.copyConfig")),
1038
- state.status.lastRunAt === null ? null : h("span", { className: "dshgt-hint" }, `上次运行 ${localTime(state.status.lastRunAt)}`)
1665
+ { className: "dshgt-vpanel" },
1666
+ currentCard,
1667
+ problems.length > 0 ? h(Alert, { kind: "err", title: t("error.prefix") }, h("ul", { style: { margin: 0, paddingLeft: 16 } }, problems.map((problem, index) => h("li", { key: index }, `${problem.field}:${problem.message}`)))) : null,
1668
+ message === null ? null : h(Alert, { kind: "ok" }, message),
1669
+ h(
1670
+ "div",
1671
+ { className: "dshgt-footer" },
1672
+ sectionEditable ? h(
1673
+ Btn,
1674
+ {
1675
+ variant: "primary",
1676
+ onClick: () => void save(section),
1677
+ disabled: savingSection !== null || !dirtySections[section],
1678
+ title: t("settings.saveSection.hint")
1679
+ },
1680
+ savingSection === section ? t("settings.saving") : dirtySections[section] ? t("settings.saveSection") : savedSection === section ? t("settings.saved") : t("settings.saveSection")
1681
+ ) : null,
1682
+ sectionEditable ? h("span", { className: "dshgt-hint" }, t("settings.saveSection.hint")) : null,
1683
+ h(Btn, { onClick: () => void importCli() }, t("settings.import")),
1684
+ h(Btn, { onClick: copyConfig }, t("settings.copyConfig"))
1685
+ )
1039
1686
  ),
1040
1687
  quickOpen ? h(QuickFillModal, { t, onClose: () => setQuickOpen(false), onApplied: (settings) => {
1041
1688
  setDraft(settings);
1042
- setDirty(false);
1043
- setSaved(true);
1689
+ setDirtySections({ ...EMPTY_DIRTY });
1690
+ setSavedSection(null);
1044
1691
  void store.refresh({ quiet: true });
1045
1692
  } }) : null
1046
1693
  );
1047
1694
  }
1048
1695
  function QuickFillModal(props) {
1049
1696
  const { t } = props;
1050
- const [providers, setProviders] = import_react.default.useState(null);
1697
+ const [data, setData] = import_react.default.useState(null);
1051
1698
  const [error, setError] = import_react.default.useState(null);
1052
1699
  const [selectedProvider, setSelectedProvider] = import_react.default.useState("");
1053
1700
  const [selectedModel, setSelectedModel] = import_react.default.useState("");
1054
- const [applying, setApplying] = import_react.default.useState(false);
1701
+ const [applying, setApplying] = import_react.default.useState(null);
1702
+ const [showOthers, setShowOthers] = import_react.default.useState(false);
1055
1703
  import_react.default.useEffect(() => {
1056
1704
  void (async () => {
1057
1705
  try {
1058
1706
  const result = await apiCall("GET", "/quickfill");
1059
- setProviders(result.providers);
1707
+ setData(result);
1060
1708
  const first = result.providers[0];
1061
1709
  if (first !== void 0) {
1062
1710
  setSelectedProvider(first.id);
@@ -1064,22 +1712,40 @@ function QuickFillModal(props) {
1064
1712
  }
1065
1713
  } catch (failure) {
1066
1714
  setError(failure instanceof Error ? failure.message : String(failure));
1067
- setProviders([]);
1715
+ setData({ providers: [], defaultSelection: null });
1068
1716
  }
1069
1717
  })();
1070
1718
  }, []);
1071
- const current = providers?.find((provider) => provider.id === selectedProvider);
1072
- const apply2 = async () => {
1719
+ const providers = data?.providers ?? [];
1720
+ const defaultSelection = data?.defaultSelection ?? null;
1721
+ const current = providers.find((provider) => provider.id === selectedProvider);
1722
+ const finish = (settings) => {
1723
+ props.onApplied(settings);
1724
+ props.onClose();
1725
+ };
1726
+ const applyDefault = async () => {
1727
+ setApplying("default");
1728
+ setError(null);
1729
+ try {
1730
+ const result = await apiCall("POST", "/quickfill", { useDefault: true });
1731
+ finish(result.settings);
1732
+ } catch (failure) {
1733
+ setError(failure instanceof Error ? failure.message : String(failure));
1734
+ } finally {
1735
+ setApplying(null);
1736
+ }
1737
+ };
1738
+ const applySelected = async () => {
1073
1739
  if (current === void 0) return;
1074
- setApplying(true);
1740
+ setApplying("selected");
1741
+ setError(null);
1075
1742
  try {
1076
1743
  const result = await apiCall("POST", "/quickfill", { providerId: current.id, modelId: selectedModel });
1077
- props.onApplied(result.settings);
1078
- props.onClose();
1744
+ finish(result.settings);
1079
1745
  } catch (failure) {
1080
1746
  setError(failure instanceof Error ? failure.message : String(failure));
1081
1747
  } finally {
1082
- setApplying(false);
1748
+ setApplying(null);
1083
1749
  }
1084
1750
  };
1085
1751
  return h(
@@ -1093,250 +1759,1433 @@ function QuickFillModal(props) {
1093
1759
  h("h3", { style: { margin: 0, fontSize: 14 } }, t("quickfill.title")),
1094
1760
  h("p", { className: "dshgt-hint", style: { margin: 0 } }, t("quickfill.hint")),
1095
1761
  error === null ? null : h(Alert, { kind: "err" }, error),
1096
- providers === null ? h("p", { className: "dshgt-hint" }, t("quickfill.loading")) : providers.length === 0 ? h("p", { className: "dshgt-hint" }, t("quickfill.empty")) : h(
1762
+ data === null ? h("p", { className: "dshgt-hint" }, t("quickfill.loading")) : h(
1097
1763
  "div",
1098
- { style: { display: "flex", flexDirection: "column", gap: 8 } },
1099
- h(Select, {
1100
- value: selectedProvider,
1101
- options: providers.map((provider) => ({ value: provider.id, label: `${provider.name} (${provider.id})` })),
1102
- onChange: (value) => {
1103
- setSelectedProvider(value);
1104
- const provider = providers.find((item) => item.id === value);
1105
- setSelectedModel(provider?.models[0]?.id ?? "");
1106
- }
1107
- }),
1108
- current === void 0 ? null : h(Select, {
1109
- value: selectedModel,
1110
- options: current.models.map((model) => ({ value: model.id, label: model.name })),
1111
- onChange: setSelectedModel
1112
- }),
1113
- current === void 0 ? null : h(KV, {
1114
- items: [
1115
- ["provider", current.coreProvider],
1116
- ["baseURL", current.baseURL === "" ? t("label.optional") : current.baseURL],
1117
- ["credential", current.credential.ref ?? "—"],
1118
- ["mode", t(`quickfill.mode.${current.credential.mode}`)]
1119
- ]
1120
- })
1764
+ { style: { display: "flex", flexDirection: "column", gap: 10 } },
1765
+ defaultSelection === null ? null : h(
1766
+ "div",
1767
+ { className: "dshgt-default" },
1768
+ h(
1769
+ "div",
1770
+ { style: { display: "flex", flexDirection: "column", gap: 2 } },
1771
+ h("span", { className: "dshgt-hint" }, t("quickfill.default")),
1772
+ h("span", { className: "dshgt-mono" }, `${defaultSelection.provider} / ${defaultSelection.model}`)
1773
+ ),
1774
+ h(Btn, { variant: "primary", small: true, onClick: () => void applyDefault(), disabled: applying !== null }, applying === "default" ? t("quickfill.applying") : t("quickfill.useDefault"))
1775
+ ),
1776
+ providers.length === 0 ? defaultSelection === null ? h("p", { className: "dshgt-hint" }, t("quickfill.empty")) : null : h(
1777
+ "div",
1778
+ { style: { display: "flex", flexDirection: "column", gap: 8 } },
1779
+ h(Btn, { small: true, onClick: () => setShowOthers(!showOthers) }, showOthers ? t("quickfill.others.hide") : t("quickfill.others")),
1780
+ showOthers ? h(
1781
+ "div",
1782
+ { style: { display: "flex", flexDirection: "column", gap: 8 } },
1783
+ h(Select, {
1784
+ value: selectedProvider,
1785
+ options: providers.map((provider) => ({ value: provider.id, label: `${provider.name} (${provider.id})` })),
1786
+ onChange: (value) => {
1787
+ setSelectedProvider(value);
1788
+ const provider = providers.find((item) => item.id === value);
1789
+ setSelectedModel(provider?.models[0]?.id ?? "");
1790
+ }
1791
+ }),
1792
+ current === void 0 ? null : h(Select, {
1793
+ value: selectedModel,
1794
+ options: current.models.map((model) => ({ value: model.id, label: model.name })),
1795
+ onChange: setSelectedModel
1796
+ }),
1797
+ current === void 0 ? null : h(KV, {
1798
+ items: [
1799
+ ["provider", current.coreProvider],
1800
+ ["baseURL", current.baseURL === "" ? t("label.optional") : current.baseURL],
1801
+ ["credential", current.credential.ref ?? "—"],
1802
+ ["mode", t(`quickfill.mode.${current.credential.mode}`)]
1803
+ ]
1804
+ }),
1805
+ h(Btn, { variant: "primary", small: true, disabled: applying !== null || selectedModel === "", onClick: () => void applySelected() }, applying === "selected" ? t("quickfill.applying") : t("quickfill.apply"))
1806
+ ) : null
1807
+ )
1121
1808
  ),
1122
1809
  h(
1123
1810
  "div",
1124
1811
  { className: "dshgt-footer", style: { justifyContent: "flex-end" } },
1125
- h(Btn, { onClick: props.onClose }, t("quickfill.close")),
1126
- h(Btn, { variant: "primary", disabled: applying || current === void 0 || selectedModel === "", onClick: () => void apply2() }, t("quickfill.apply"))
1812
+ h(Btn, { onClick: props.onClose }, t("quickfill.close"))
1127
1813
  )
1128
1814
  )
1129
1815
  );
1130
1816
  }
1131
1817
 
1132
- // client/runs.ts
1133
- var STEP_KEYS = ["resolve", "handshake", "spawn", "fetch", "summarize", "render", "commit", "pr", "state"];
1134
- var CORE_STEPS = /* @__PURE__ */ new Set(["fetch", "summarize", "render", "commit", "pr"]);
1135
- function RunsView(props) {
1136
- const { t, store } = props;
1137
- const [tab, setTab] = import_react.default.useState("now");
1138
- const [dryRun, setDryRun] = import_react.default.useState(false);
1818
+ // client/error-copy.ts
1819
+ function reasonHint(t, reason) {
1820
+ if (reason === void 0 || reason === "") return null;
1821
+ const key = `err.reason.${reason}`;
1822
+ const text = t(key);
1823
+ return text === key ? null : text;
1824
+ }
1825
+ function skipHint(t, reason, fallback) {
1826
+ if (reason === void 0 || reason === "") return fallback;
1827
+ const key = `skip.${reason}`;
1828
+ const text = t(key);
1829
+ return text === key ? fallback : text;
1830
+ }
1831
+
1832
+ // client/run-controller.ts
1833
+ function useRunController(store) {
1834
+ const { refresh } = store;
1139
1835
  const [busy, setBusy] = import_react.default.useState(false);
1140
- const [summary, setSummary] = import_react.default.useState(null);
1141
1836
  const [error, setError] = import_react.default.useState(null);
1142
- const state = store.state;
1143
- const run = async () => {
1144
- setBusy(true);
1837
+ const [summary, setSummary] = import_react.default.useState(null);
1838
+ const [dryRun, setDryRun] = import_react.default.useState(false);
1839
+ const [localStartedAt, setLocalStartedAt] = import_react.default.useState(null);
1840
+ const run = import_react.default.useCallback(
1841
+ async (overrideDry) => {
1842
+ const dry = overrideDry ?? dryRun;
1843
+ setBusy(true);
1844
+ setError(null);
1845
+ setSummary(null);
1846
+ setLocalStartedAt((/* @__PURE__ */ new Date()).toISOString());
1847
+ try {
1848
+ const result = await apiCall("POST", "/run", { dryRun: dry });
1849
+ setSummary(result.summary);
1850
+ await refresh({ quiet: true });
1851
+ } catch (failure) {
1852
+ setError(failure instanceof Error ? failure.message : String(failure));
1853
+ } finally {
1854
+ setBusy(false);
1855
+ setLocalStartedAt(null);
1856
+ }
1857
+ },
1858
+ [dryRun, refresh]
1859
+ );
1860
+ const cancel = import_react.default.useCallback(async () => {
1145
1861
  setError(null);
1146
- setSummary(null);
1147
- try {
1148
- const result = await apiCall("POST", "/run", { dryRun });
1149
- setSummary(result.summary);
1150
- await store.refresh({ quiet: true });
1151
- } catch (failure) {
1152
- setError(failure instanceof Error ? failure.message : String(failure));
1153
- } finally {
1154
- setBusy(false);
1155
- }
1156
- };
1157
- const cancel = async () => {
1158
1862
  try {
1159
1863
  await apiCall("POST", "/run/cancel", {});
1160
1864
  } catch (failure) {
1161
1865
  setError(failure instanceof Error ? failure.message : String(failure));
1866
+ } finally {
1867
+ await refresh({ quiet: true });
1162
1868
  }
1869
+ }, [refresh]);
1870
+ const serverRunning = store.state?.status.running ?? null;
1871
+ return {
1872
+ running: serverRunning !== null || busy,
1873
+ busy,
1874
+ error,
1875
+ summary,
1876
+ startedAt: serverRunning?.startedAt ?? localStartedAt,
1877
+ dryRun,
1878
+ setDryRun,
1879
+ run,
1880
+ cancel,
1881
+ clearError: () => setError(null)
1163
1882
  };
1164
- if (state === null) {
1165
- return h("div", { className: "dshgt-root" }, h("p", { className: "dshgt-hint" }, store.error ?? t("quickfill.loading")));
1166
- }
1167
- const running = state.status.running !== null;
1168
- const shown = summary;
1169
- const lastRecord = state.status.history[0] === void 0 ? null : state.status.history[0];
1170
- const last = shown === null ? lastRecord : { ...lastRecord, ...shown, at: (/* @__PURE__ */ new Date()).toISOString(), model: { provider: state.settings.model.provider, model: state.settings.model.model } };
1171
- const failedStep = shown !== null && !shown.ok ? stepFromError(shown.error?.area) : null;
1883
+ }
1884
+ function useElapsed(startedAt, active) {
1885
+ const [now, setNow] = import_react.default.useState(() => Date.now());
1886
+ import_react.default.useEffect(() => {
1887
+ if (!active) return () => void 0;
1888
+ setNow(Date.now());
1889
+ const timer = setInterval(() => setNow(Date.now()), 1e3);
1890
+ return () => clearInterval(timer);
1891
+ }, [active]);
1892
+ if (startedAt === null || startedAt === void 0) return 0;
1893
+ const started = new Date(startedAt).getTime();
1894
+ if (Number.isNaN(started)) return 0;
1895
+ return Math.max(0, now - started);
1896
+ }
1897
+ function formatElapsed(ms) {
1898
+ const total = Math.floor(ms / 1e3);
1899
+ const minutes = Math.floor(total / 60);
1900
+ const seconds = total % 60;
1901
+ return `${minutes}:${String(seconds).padStart(2, "0")}`;
1902
+ }
1903
+
1904
+ // client/live-run.ts
1905
+ function LiveRun(props) {
1906
+ const { t, controller } = props;
1907
+ const elapsed = useElapsed(props.startedAt, true);
1172
1908
  return h(
1173
1909
  "div",
1174
- { className: "dshgt-root" },
1910
+ { className: "dshgt-live" },
1175
1911
  h(
1176
1912
  "div",
1177
- { className: "dshgt-head" },
1178
- h("span", { className: "dshgt-title" }, t("section.title")),
1179
- h("span", { className: "dshgt-sub" }, t("card.source.hint")),
1180
- h("span", { style: { marginLeft: "auto" } }, h(Pill, { kind: running || busy ? "warn" : "ok" }, running || busy ? t("run.running") : t("run.idle")))
1913
+ { className: "dshgt-live-top" },
1914
+ h("span", { className: "dshgt-dot warn" }),
1915
+ h("b", null, t("overview.live.title")),
1916
+ h("span", { className: "dshgt-spinner", "aria-hidden": "true" }),
1917
+ h("span", { className: "dshgt-hint" }, t("overview.live.core")),
1918
+ h("span", { className: "dshgt-live-elapsed" }, formatElapsed(elapsed))
1181
1919
  ),
1182
1920
  h(
1183
1921
  "div",
1184
- { className: "dshgt-tabs", role: "tablist" },
1185
- h("button", { type: "button", className: "dshgt-tab", role: "tab", "aria-selected": tab === "now" ? "true" : "false", onClick: () => setTab("now") }, t("run.now")),
1186
- h("button", { type: "button", className: "dshgt-tab", role: "tab", "aria-selected": tab === "history" ? "true" : "false", onClick: () => setTab("history") }, t("run.history"))
1922
+ { className: "dshgt-live-note" },
1923
+ h("span", null, t("overview.live.note")),
1924
+ props.startedAt === null ? null : h("span", { className: "dshgt-hint" }, `${t("overview.live.started")} ${localTime(props.startedAt)}`)
1187
1925
  ),
1188
- tab === "now" ? h(
1926
+ h(
1189
1927
  "div",
1190
- { style: { display: "flex", flexDirection: "column", gap: 12 } },
1191
- h(
1192
- Card,
1193
- { title: t("run.now"), hint: "spawn → 单次运行 → 退出" },
1194
- h(
1195
- Inline,
1196
- null,
1197
- h(Btn, { variant: "primary", onClick: () => void run(), disabled: busy || running }, t("run.button")),
1198
- h(Toggle, { checked: dryRun, label: t("run.dryRun"), onChange: setDryRun }),
1199
- running || busy ? h(Btn, { variant: "danger", onClick: () => void cancel() }, t("run.cancel")) : null
1200
- ),
1201
- error === null ? null : h(Alert, { kind: "err" }, error),
1202
- h(
1203
- "ul",
1204
- { className: "dshgt-steps" },
1205
- STEP_KEYS.map((key) => {
1206
- const stepState = running || busy ? CORE_STEPS.has(key) ? "pending" : key === "resolve" || key === "handshake" || key === "spawn" ? "active" : "pending" : shown === null ? "pending" : failedStep === key ? "failed" : "done";
1207
- const label = { done: "✓", active: "…", failed: "✗", pending: "—" }[stepState];
1208
- return h("li", { key, className: "dshgt-step", "data-state": stepState }, h("span", null, t(`step.${key}`)), h("span", { className: "st" }, label));
1209
- })
1210
- )
1211
- ),
1212
- last === null ? h(Card, { title: t("run.last.title") }, h("p", { className: "dshgt-hint" }, t("run.last.none"))) : h(
1213
- Card,
1214
- { 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")) },
1215
- h(KV, {
1216
- items: [
1217
- [t("col.date"), localTime(last.at)],
1218
- [t("run.usage.core"), last.coreVersion || "—"],
1219
- [t("run.usage.files"), String(last.files)],
1220
- [t("run.usage.duration"), duration(last.durationMs)],
1221
- [t("run.usage.hash"), h("span", { className: "dshgt-mono" }, shortHash(last.contentHash))],
1222
- [t("run.usage.branch"), h("span", { className: "dshgt-mono" }, last.branch ?? "—")],
1223
- [t("run.usage.pr"), last.prUrl === void 0 ? "—" : h("a", { href: last.prUrl, target: "_blank", rel: "noreferrer" }, last.prUrl)],
1224
- ["model", `${state.settings.model.provider} / ${state.settings.model.model || "—"}`]
1225
- ]
1226
- }),
1227
- last.warnings.length === 0 ? null : h("p", { className: "dshgt-hint" }, last.warnings.join(";"))
1228
- ),
1229
- shown !== null && shown.error !== void 0 ? h(
1230
- Card,
1231
- { title: t("run.error.title") },
1232
- h(
1233
- Alert,
1234
- { kind: "err", title: `${t("error.prefix")} ${shown.error.code}` },
1235
- h("div", null, shown.error.message),
1236
- h("div", { className: "dshgt-hint" }, shown.error.hint),
1237
- shown.error.inferred ? h("div", { className: "dshgt-hint" }, "(错误码由插件按核心 stderr 推断)") : null
1238
- ),
1239
- shown.logFile === void 0 ? null : h("p", { className: "dshgt-hint" }, `日志:${shown.logFile}`)
1240
- ) : null
1241
- ) : h(HistoryTable, { t, records: state.status.history })
1928
+ { className: "dshgt-live-actions" },
1929
+ h(Btn, { small: true, variant: "danger", onClick: () => void controller.cancel() }, t("run.cancel"))
1930
+ )
1242
1931
  );
1243
1932
  }
1244
- function stepFromError(area) {
1245
- switch (area) {
1246
- case "core":
1247
- return "resolve";
1248
- case "model":
1249
- return "summarize";
1250
- case "repo":
1251
- case "auth":
1252
- return "commit";
1253
- default:
1254
- return "state";
1933
+
1934
+ // client/digest-core.ts
1935
+ var DIGEST_WINDOW_DAYS = 30;
1936
+ var DATE_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
1937
+ function isValidDate(value) {
1938
+ if (typeof value !== "string") return false;
1939
+ const match = DATE_RE.exec(value);
1940
+ if (match === null) return false;
1941
+ const year = Number(match[1]);
1942
+ const month = Number(match[2]);
1943
+ const day = Number(match[3]);
1944
+ const date = new Date(Date.UTC(year, month - 1, day));
1945
+ return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day;
1946
+ }
1947
+ function zonedDateString(date, timeZone) {
1948
+ try {
1949
+ return new Intl.DateTimeFormat("en-CA", {
1950
+ timeZone,
1951
+ year: "numeric",
1952
+ month: "2-digit",
1953
+ day: "2-digit"
1954
+ }).format(date);
1955
+ } catch {
1956
+ const year = date.getFullYear();
1957
+ const month = String(date.getMonth() + 1).padStart(2, "0");
1958
+ const day = String(date.getDate()).padStart(2, "0");
1959
+ return `${year}-${month}-${day}`;
1255
1960
  }
1256
1961
  }
1257
- function HistoryTable(props) {
1258
- const { t, records } = props;
1259
- return h(
1260
- Card,
1261
- { title: t("run.history.title"), hint: t("run.history.hint") },
1262
- records.length === 0 ? h("p", { className: "dshgt-hint" }, t("run.history.empty")) : h(
1263
- "table",
1264
- { className: "dshgt-table" },
1265
- 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)))),
1266
- h(
1267
- "tbody",
1268
- null,
1269
- records.map(
1270
- (record) => h(
1271
- "tr",
1272
- { key: String(record.id) },
1273
- h("td", { className: "dshgt-mono" }, localTime(record.at)),
1274
- 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"))),
1275
- h("td", { className: "dshgt-mono" }, String(record.files ?? 0)),
1276
- h("td", { className: "dshgt-mono" }, duration(Number(record.durationMs ?? 0))),
1277
- h("td", { className: "dshgt-mono" }, [record.branch, record.prUrl].filter(Boolean).join(" · ") || "—"),
1278
- h("td", { className: "dshgt-mono" }, record.errorCode ?? "—")
1279
- )
1280
- )
1281
- )
1282
- )
1283
- );
1962
+ function addDays(date, days) {
1963
+ const match = DATE_RE.exec(date);
1964
+ if (match === null) return date;
1965
+ const value = new Date(Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3])));
1966
+ value.setUTCDate(value.getUTCDate() + days);
1967
+ return value.toISOString().slice(0, 10);
1968
+ }
1969
+ function recordDate(record, timeZone) {
1970
+ if (isValidDate(record.date)) return { date: record.date, derivedFromAt: false };
1971
+ const at = new Date(record.at);
1972
+ if (Number.isNaN(at.getTime())) return void 0;
1973
+ return { date: zonedDateString(at, timeZone), derivedFromAt: true };
1974
+ }
1975
+ function digestStatus(record) {
1976
+ if (!record.ok) return "failed";
1977
+ if (record.skipped) return "skipped";
1978
+ if (record.kind === "dry-run") return "dry";
1979
+ if (record.preview !== void 0) return "preview";
1980
+ if (record.legacy === true) return "legacy";
1981
+ return "missing";
1982
+ }
1983
+ function buildDigestDates(records, options) {
1984
+ const now = options.now ?? /* @__PURE__ */ new Date();
1985
+ const today = zonedDateString(now, options.timeZone);
1986
+ const earliest = addDays(today, -(DIGEST_WINDOW_DAYS - 1));
1987
+ const groups = /* @__PURE__ */ new Map();
1988
+ for (const record of records) {
1989
+ const info = recordDate(record, options.timeZone);
1990
+ if (info === void 0) continue;
1991
+ if (info.date < earliest || info.date > today) continue;
1992
+ const group = groups.get(info.date);
1993
+ if (group === void 0) groups.set(info.date, { records: [record], derivedFromAt: info.derivedFromAt });
1994
+ else {
1995
+ group.records.push(record);
1996
+ group.derivedFromAt = group.derivedFromAt || info.derivedFromAt;
1997
+ }
1998
+ }
1999
+ const days = [];
2000
+ for (const [date, group] of groups) {
2001
+ const sorted = [...group.records].sort((a, b) => b.at.localeCompare(a.at));
2002
+ const preferred = sorted.find(
2003
+ (record2) => record2.preview !== void 0 && record2.ok && record2.kind !== "dry-run" && !record2.skipped
2004
+ );
2005
+ const record = preferred ?? sorted[0];
2006
+ if (record === void 0) continue;
2007
+ const info = recordDate(record, options.timeZone);
2008
+ const legacy = record.legacy === true || info?.derivedFromAt === true || group.derivedFromAt;
2009
+ days.push({ date, record, status: digestStatus(record), legacy });
2010
+ }
2011
+ days.sort((a, b) => b.date.localeCompare(a.date));
2012
+ return days;
2013
+ }
2014
+ function recentDates(today, count) {
2015
+ const dates = [];
2016
+ for (let offset = count - 1; offset >= 0; offset -= 1) dates.push(addDays(today, -offset));
2017
+ return dates;
2018
+ }
2019
+ function formatDayLabel(date, lang) {
2020
+ let weekday = "";
2021
+ try {
2022
+ weekday = new Intl.DateTimeFormat(lang === "en" ? "en-US" : "zh-CN", {
2023
+ weekday: "short",
2024
+ timeZone: "UTC"
2025
+ }).format(/* @__PURE__ */ new Date(`${date}T12:00:00Z`));
2026
+ } catch {
2027
+ weekday = "";
2028
+ }
2029
+ return weekday === "" ? date : `${date} ${weekday}`;
2030
+ }
2031
+ function formatBytes(bytes) {
2032
+ if (!Number.isFinite(bytes) || bytes < 0) return "—";
2033
+ if (bytes < 1024) return `${bytes} B`;
2034
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
2035
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1284
2036
  }
1285
2037
 
1286
- // client/widget.ts
1287
- function Widget(props) {
1288
- const { t, store } = props;
2038
+ // client/overview.ts
2039
+ function statusPill(t, record) {
2040
+ if (!record.ok) return h(Pill, { kind: "err" }, t("state.failed"));
2041
+ if (record.skipped) return h(Pill, { kind: "warn" }, t("state.skipped"));
2042
+ if (record.kind === "dry-run") return h(Pill, { kind: "warn" }, t("state.dry"));
2043
+ return h(Pill, { kind: "ok" }, t("state.ok"));
2044
+ }
2045
+ var STATUS_LAMP = {
2046
+ preview: "ok",
2047
+ failed: "err",
2048
+ skipped: "warn",
2049
+ dry: "warn",
2050
+ legacy: "warn",
2051
+ missing: "warn"
2052
+ };
2053
+ function dayLabel(t, day) {
2054
+ switch (day.status) {
2055
+ case "preview":
2056
+ return duration(day.record.durationMs);
2057
+ case "failed":
2058
+ return t("state.failed");
2059
+ case "skipped":
2060
+ return t("state.skipped");
2061
+ case "dry":
2062
+ return t("state.dry");
2063
+ case "legacy":
2064
+ return t("digest.tag.legacy");
2065
+ case "missing":
2066
+ return t("digest.tag.missing");
2067
+ }
2068
+ }
2069
+ function prLabel(t, state, record) {
2070
+ if (record === null || record.prUrl === void 0) {
2071
+ if (record !== null && record.ok && !record.skipped && record.kind !== "dry-run" && state.settings.repo.commitMode === "direct") {
2072
+ return { label: t("overview.last.direct"), kind: "idle" };
2073
+ }
2074
+ return null;
2075
+ }
2076
+ const pending = state.status.pendingPr;
2077
+ const matches = pending !== null && pending.url === record.prUrl;
2078
+ if (matches && state.status.pendingPrState === "unknown") {
2079
+ return { label: t("overview.last.prUnknown"), url: record.prUrl, kind: "warn" };
2080
+ }
2081
+ if (matches) return { label: t("overview.last.prPending"), url: record.prUrl, kind: "warn" };
2082
+ return { label: t("overview.last.prCreated"), url: record.prUrl, kind: "ok" };
2083
+ }
2084
+ function readyRows(t, state) {
2085
+ const core = state.core;
2086
+ const credential = credentialView(state.model, t);
2087
+ const repo = state.settings.repo;
2088
+ return [
2089
+ {
2090
+ ok: core.state === "ready",
2091
+ label: t("overview.ready.core"),
2092
+ value: core.state === "ready" ? `${core.version ?? "—"} · ${t("core.ready")}` : t(`core.${core.state}`),
2093
+ ...core.ipcSchema === void 0 ? {} : { extra: `ipc ${core.ipcSchema} · digest ${core.digestSchema ?? "—"}` }
2094
+ },
2095
+ {
2096
+ ok: credential.ok,
2097
+ label: t("overview.ready.model"),
2098
+ value: `${state.settings.model.provider} / ${state.settings.model.model || "—"}`,
2099
+ extra: credential.label
2100
+ },
2101
+ {
2102
+ ok: repo.path.trim() !== "",
2103
+ label: t("overview.ready.repo"),
2104
+ value: repo.path.trim() === "" ? t("repo.path.empty") : repo.path,
2105
+ extra: repo.commitMode === "direct" ? t("repo.commitMode.direct") : t("repo.commitMode.branch")
2106
+ },
2107
+ {
2108
+ ok: state.github.configured,
2109
+ label: t("overview.ready.github"),
2110
+ value: state.github.configured ? t("auth.configured") : t("auth.missing"),
2111
+ extra: `${state.github.authMode}${state.github.source === "" ? "" : ` · ${state.github.source}`}`
2112
+ }
2113
+ ];
2114
+ }
2115
+ function OverviewView(props) {
2116
+ const { t, lang, store, controller } = props;
1289
2117
  const state = store.state;
1290
- const [busy, setBusy] = import_react.default.useState(false);
1291
2118
  if (state === null) {
1292
- return h("div", { className: "dshgt-widget dshgt-root" }, h("h4", null, t("widget.title")), h("span", { className: "dshgt-hint" }, store.error ?? t("quickfill.loading")));
2119
+ return h("div", { className: "dshgt-empty" }, store.error ?? t("quickfill.loading"));
1293
2120
  }
1294
- const running = state.status.running !== null;
1295
- const last = state.status.lastResult;
1296
- const coreKind = state.core.state === "ready" ? "ok" : state.core.state === "incompatible" ? "warn" : "err";
1297
- const run = async (dryRun) => {
1298
- setBusy(true);
1299
- try {
1300
- await apiCall("POST", "/run", { dryRun });
1301
- await store.refresh({ quiet: true });
1302
- } catch {
1303
- } finally {
1304
- setBusy(false);
1305
- }
1306
- };
2121
+ const last = state.status.lastResult ?? state.status.history[0] ?? null;
2122
+ const pr = prLabel(t, state, last);
2123
+ const rows = readyRows(t, state);
2124
+ const readyCount = rows.filter((row) => row.ok).length;
2125
+ const timeZone = state.schedule.timezone;
2126
+ const today = zonedDateString(/* @__PURE__ */ new Date(), timeZone);
2127
+ const days = buildDigestDates(state.status.history, { timeZone });
2128
+ const byDate = new Map(days.map((day) => [day.date, day]));
2129
+ const week = recentDates(today, 7);
1307
2130
  return h(
1308
2131
  "div",
1309
- { className: "dshgt-widget dshgt-root" },
1310
- h(
2132
+ { className: "dshgt-view dshgt-overview" },
2133
+ controller.running ? h(LiveRun, { t, controller, startedAt: controller.startedAt }) : null,
2134
+ controller.error === null ? null : h("div", { className: "dshgt-alert err" }, controller.error),
2135
+ last === null ? h(
2136
+ "section",
2137
+ { className: "dshgt-delivery" },
2138
+ h("div", { className: "dshgt-delivery-top" }, h("span", { className: "dshgt-hint" }, t("overview.last.none")))
2139
+ ) : h(
2140
+ "section",
2141
+ { className: "dshgt-delivery" },
2142
+ h(
2143
+ "div",
2144
+ { className: "dshgt-delivery-top" },
2145
+ statusPill(t, last),
2146
+ h("span", { className: "dshgt-hint" }, last.date ?? localTime(last.at)),
2147
+ h(
2148
+ "span",
2149
+ { className: "dshgt-pill-mini" },
2150
+ `${t("overview.last.files", { count: last.files })} · ${duration(last.durationMs)}`
2151
+ ),
2152
+ last.legacy === true ? h("span", { className: "dshgt-tag" }, t("runs.legacy")) : null
2153
+ ),
2154
+ h("h2", null, t("overview.last.digest", { date: last.date ?? localTime(last.at) })),
2155
+ h(
2156
+ "div",
2157
+ { className: "dshgt-when" },
2158
+ last.branch === void 0 ? null : h("span", null, `${t("run.usage.branch")} `, h("span", { className: "dshgt-mono" }, last.branch)),
2159
+ last.commit === void 0 ? null : h("span", null, `${t("run.usage.commit")} `, h("span", { className: "dshgt-mono" }, last.commit)),
2160
+ 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)
2161
+ ),
2162
+ last.ok ? null : h(
2163
+ "div",
2164
+ { className: "dshgt-alert err dshgt-delivery-error" },
2165
+ h("b", null, `${t("error.prefix")} ${last.errorCode ?? "INTERNAL"}`),
2166
+ h("div", null, last.errorMessage ?? t("state.failed")),
2167
+ last.errorReason === void 0 ? null : h("div", { className: "dshgt-hint" }, reasonHint(t, last.errorReason) ?? "")
2168
+ ),
2169
+ h(
2170
+ "div",
2171
+ { className: "dshgt-artifacts" },
2172
+ last.preview === void 0 ? h("span", { className: "dshgt-hint" }, t("overview.last.noPreview")) : h(
2173
+ "button",
2174
+ {
2175
+ type: "button",
2176
+ className: "dshgt-filechip",
2177
+ onClick: () => props.onOpenDigest(last.date)
2178
+ },
2179
+ h("span", { className: "dshgt-mono" }, last.preview.file)
2180
+ ),
2181
+ last.preview === void 0 ? null : h(Btn, { small: true, onClick: () => props.onOpenDigest(last.date) }, t("overview.last.read")),
2182
+ pr === null || pr.url === void 0 ? null : h(
2183
+ "a",
2184
+ { className: "dshgt-btn dshgt-btn-sm", href: pr.url, target: "_blank", rel: "noreferrer" },
2185
+ t("digest.openPr")
2186
+ )
2187
+ ),
2188
+ h(
2189
+ "div",
2190
+ { className: "dshgt-facts" },
2191
+ 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)),
2192
+ h("div", { className: "dshgt-fact" }, h("div", { className: "dshgt-fact-k" }, t("run.usage.hash")), h("div", { className: "dshgt-fact-v" }, shortHash(last.contentHash))),
2193
+ h("div", { className: "dshgt-fact" }, h("div", { className: "dshgt-fact-k" }, t("run.usage.core")), h("div", { className: "dshgt-fact-v" }, last.coreVersion || "—")),
2194
+ h("div", { className: "dshgt-fact" }, h("div", { className: "dshgt-fact-k" }, t("overview.model")), h("div", { className: "dshgt-fact-v" }, last.model?.model ?? "—"))
2195
+ )
2196
+ ),
2197
+ h(
2198
+ "section",
2199
+ { className: "dshgt-block" },
2200
+ h(
2201
+ "div",
2202
+ { className: "dshgt-section-label" },
2203
+ t("overview.ready.title"),
2204
+ h("span", { className: "dshgt-mono dshgt-section-tail" }, `${readyCount} / ${rows.length}`)
2205
+ ),
2206
+ h(
2207
+ "div",
2208
+ { className: "dshgt-ready-list" },
2209
+ rows.map(
2210
+ (row) => h(
2211
+ "div",
2212
+ { className: "dshgt-ready-row", key: row.label },
2213
+ h("span", { className: `dshgt-lamp ${row.ok ? "ok" : "warn"}` }),
2214
+ h("span", { className: "dshgt-ready-label" }, row.label),
2215
+ h(
2216
+ "span",
2217
+ { className: "dshgt-ready-value" },
2218
+ row.value,
2219
+ row.extra === void 0 ? null : h("span", { className: "dshgt-ready-extra" }, `· ${row.extra}`)
2220
+ ),
2221
+ h("button", { type: "button", className: "dshgt-linkbtn", onClick: props.onOpenSettings }, t("overview.ready.view"))
2222
+ )
2223
+ )
2224
+ )
2225
+ ),
2226
+ h(
2227
+ "section",
2228
+ { className: "dshgt-block" },
2229
+ h(
2230
+ "div",
2231
+ { className: "dshgt-section-label" },
2232
+ t("overview.week.title"),
2233
+ h("button", { type: "button", className: "dshgt-linkbtn dshgt-section-tail", onClick: props.onOpenRuns }, t("overview.week.all"))
2234
+ ),
2235
+ h(
2236
+ "div",
2237
+ { className: "dshgt-week" },
2238
+ week.map((date) => {
2239
+ const day = byDate.get(date);
2240
+ if (day === void 0) {
2241
+ const waiting = date === today && state.schedule.enabled;
2242
+ return h(
2243
+ "div",
2244
+ { className: "dshgt-day", key: date },
2245
+ h("div", { className: "dshgt-day-date" }, date.slice(5).replace("-", "/")),
2246
+ h("div", { className: "dshgt-day-status" }, waiting ? t("overview.week.wait", { time: state.schedule.time }) : t("overview.week.none"))
2247
+ );
2248
+ }
2249
+ return h(
2250
+ "button",
2251
+ {
2252
+ type: "button",
2253
+ className: "dshgt-day dshgt-day-clickable",
2254
+ key: date,
2255
+ onClick: () => props.onOpenDigest(date),
2256
+ title: day.record.errorCode === void 0 ? formatDayLabel(date, lang) : `${formatDayLabel(date, lang)} · ${day.record.errorCode}`
2257
+ },
2258
+ h("div", { className: "dshgt-day-date" }, date.slice(5).replace("-", "/")),
2259
+ h(
2260
+ "div",
2261
+ { className: "dshgt-day-status" },
2262
+ h("span", { className: `dshgt-lamp ${STATUS_LAMP[day.status]}` }),
2263
+ dayLabel(t, day)
2264
+ )
2265
+ );
2266
+ })
2267
+ )
2268
+ )
2269
+ );
2270
+ }
2271
+
2272
+ // client/runs.ts
2273
+ var FILTERS = [
2274
+ { id: "all", key: "runs.filter.all" },
2275
+ { id: "failed", key: "runs.filter.failed" },
2276
+ { id: "dry", key: "runs.filter.dry" }
2277
+ ];
2278
+ function recordSummary(t, record) {
2279
+ if (!record.ok) {
2280
+ return [record.errorCode, record.errorMessage].filter((item) => item !== void 0 && item !== "").join(" · ") || t("state.failed");
2281
+ }
2282
+ if (record.skipped) return skipHint(t, record.skipReason, t("runs.noChange"));
2283
+ if (record.kind === "dry-run") return t("runs.dryHint");
2284
+ if (record.branch !== void 0) return record.branch;
2285
+ if (record.commit !== void 0) return record.commit;
2286
+ if (record.prUrl !== void 0) return t("runs.prCreated");
2287
+ return "—";
2288
+ }
2289
+ function prStatus(t, state, record) {
2290
+ if (record.prUrl === void 0) return null;
2291
+ const pending = state.status.pendingPr;
2292
+ if (pending !== null && pending.url === record.prUrl) {
2293
+ return { label: state.status.pendingPrState === "unknown" ? t("runs.prUnknown") : t("runs.pendingPr"), kind: "warn" };
2294
+ }
2295
+ return { label: t("runs.prCreated"), kind: "ok" };
2296
+ }
2297
+ function LogDrawer(props) {
2298
+ const { t, runId } = props;
2299
+ const [loading, setLoading] = import_react.default.useState(true);
2300
+ const [text, setText] = import_react.default.useState("");
2301
+ const [error, setError] = import_react.default.useState(null);
2302
+ const [copied, setCopied] = import_react.default.useState(false);
2303
+ import_react.default.useEffect(() => {
2304
+ const controller = new AbortController();
2305
+ setLoading(true);
2306
+ setError(null);
2307
+ setText("");
2308
+ getLog(runId, controller.signal).then((payload) => {
2309
+ if (controller.signal.aborted) return;
2310
+ setText(payload.text);
2311
+ setLoading(false);
2312
+ }).catch((failure) => {
2313
+ if (controller.signal.aborted) return;
2314
+ setError(failure instanceof Error ? failure.message : String(failure));
2315
+ setLoading(false);
2316
+ });
2317
+ return () => controller.abort();
2318
+ }, [runId]);
2319
+ import_react.default.useEffect(() => {
2320
+ const onKey = (event) => {
2321
+ if (event.key === "Escape") props.onClose();
2322
+ };
2323
+ document.addEventListener("keydown", onKey);
2324
+ return () => document.removeEventListener("keydown", onKey);
2325
+ }, [props]);
2326
+ return h(
2327
+ "div",
2328
+ { className: "dshgt-modal-mask", onClick: (event) => {
2329
+ if (event.target === event.currentTarget) props.onClose();
2330
+ } },
2331
+ h(
1311
2332
  "div",
1312
- { className: "dshgt-card-h" },
1313
- h("h4", null, t("widget.title")),
1314
- h("span", { style: { marginLeft: "auto" } }, h(Pill, { kind: running || busy ? "warn" : last === null ? "idle" : last.ok ? "ok" : "err" }, running || busy ? t("run.running") : last === null ? t("widget.never") : last.ok ? last.skipped ? t("state.skipped") : t("state.ok") : t("state.failed")))
2333
+ { className: "dshgt-modal dshgt-log-modal", role: "dialog", "aria-modal": "true" },
2334
+ h(
2335
+ "div",
2336
+ { className: "dshgt-log-head" },
2337
+ h("h3", { className: "dshgt-log-title" }, t("log.title")),
2338
+ h("span", { className: "dshgt-hint dshgt-mono" }, runId),
2339
+ h(Btn, { small: true, onClick: () => props.onClose() }, t("log.close"))
2340
+ ),
2341
+ loading ? h("p", { className: "dshgt-hint" }, t("log.loading")) : null,
2342
+ error === null ? null : h(Alert, { kind: "err" }, error),
2343
+ loading || error !== null ? null : text === "" ? h("p", { className: "dshgt-hint" }, t("log.empty")) : h("pre", { className: "dshgt-log-pre" }, text),
2344
+ h(
2345
+ "div",
2346
+ { className: "dshgt-log-foot" },
2347
+ h(
2348
+ Btn,
2349
+ {
2350
+ small: true,
2351
+ disabled: text === "",
2352
+ onClick: () => {
2353
+ void copyText(text).then((okResult) => {
2354
+ if (!okResult) return;
2355
+ setCopied(true);
2356
+ setTimeout(() => setCopied(false), 1600);
2357
+ });
2358
+ }
2359
+ },
2360
+ copied ? t("log.copied") : t("log.copy")
2361
+ ),
2362
+ h("span", { className: "dshgt-hint" }, t("log.truncated"))
2363
+ )
2364
+ )
2365
+ );
2366
+ }
2367
+ function RunsView(props) {
2368
+ const { t, store, controller } = props;
2369
+ const [filter, setFilter] = import_react.default.useState("all");
2370
+ const [openId, setOpenId] = import_react.default.useState(null);
2371
+ const [logId, setLogId] = import_react.default.useState(null);
2372
+ const state = store.state;
2373
+ if (state === null) {
2374
+ return h("div", { className: "dshgt-empty" }, store.error ?? t("quickfill.loading"));
2375
+ }
2376
+ const history = state.status.history;
2377
+ const records = history.filter((record) => {
2378
+ if (filter === "failed") return !record.ok;
2379
+ if (filter === "dry") return record.kind === "dry-run";
2380
+ return true;
2381
+ });
2382
+ return h(
2383
+ "div",
2384
+ { className: "dshgt-view dshgt-runs" },
2385
+ controller.running ? h(LiveRun, { t, controller, startedAt: controller.startedAt }) : null,
2386
+ controller.error === null ? null : h("div", { className: "dshgt-alert err" }, controller.error),
2387
+ h(
2388
+ "div",
2389
+ { className: "dshgt-runs-head" },
2390
+ h("span", { className: "dshgt-hint" }, t("runs.count", { count: history.length })),
2391
+ h(
2392
+ "div",
2393
+ { className: "dshgt-runs-filter" },
2394
+ FILTERS.map(
2395
+ (item) => h(
2396
+ "button",
2397
+ {
2398
+ key: item.id,
2399
+ type: "button",
2400
+ className: "dshgt-chip",
2401
+ "aria-pressed": filter === item.id ? "true" : "false",
2402
+ onClick: () => setFilter(item.id)
2403
+ },
2404
+ t(item.key)
2405
+ )
2406
+ )
2407
+ )
2408
+ ),
2409
+ records.length === 0 ? h("div", { className: "dshgt-empty" }, t("runs.empty")) : h(
2410
+ "div",
2411
+ { className: "dshgt-run-list" },
2412
+ records.map((record) => {
2413
+ const open = openId === record.id;
2414
+ const pr = prStatus(t, state, record);
2415
+ return h(
2416
+ "div",
2417
+ { className: `dshgt-run-item${open ? " open" : ""}`, key: record.id },
2418
+ h(
2419
+ "button",
2420
+ {
2421
+ type: "button",
2422
+ className: "dshgt-run-row",
2423
+ "aria-expanded": open ? "true" : "false",
2424
+ onClick: () => setOpenId(open ? null : record.id)
2425
+ },
2426
+ h("span", { className: `dshgt-lamp ${record.ok ? record.skipped || record.kind === "dry-run" ? "warn" : "ok" : "err"}` }),
2427
+ h("span", { className: "dshgt-run-date" }, shortTime(record.at)),
2428
+ h(
2429
+ "span",
2430
+ { className: "dshgt-run-what" },
2431
+ record.kind === "dry-run" ? t("state.dry") : record.skipped ? t("state.skipped") : record.ok ? t("state.ok") : t("state.failed"),
2432
+ h("span", { className: "dshgt-run-muted" }, recordSummary(t, record)),
2433
+ record.legacy === true ? h("span", { className: "dshgt-tag dshgt-inline-tag" }, t("runs.legacy")) : null
2434
+ ),
2435
+ h("span", { className: "dshgt-run-files" }, t("runs.files", { count: record.files })),
2436
+ h("span", { className: "dshgt-run-duration" }, duration(record.durationMs)),
2437
+ h("span", { className: "dshgt-run-chev" }, "›")
2438
+ ),
2439
+ open ? h(
2440
+ "div",
2441
+ { className: "dshgt-run-detail" },
2442
+ !record.ok ? h(
2443
+ Alert,
2444
+ { kind: "err", title: `${t("error.prefix")} ${record.errorCode ?? "INTERNAL"}` },
2445
+ h("div", null, record.errorMessage ?? t("state.failed")),
2446
+ record.errorReason === void 0 ? null : h("div", { className: "dshgt-hint" }, reasonHint(t, record.errorReason) ?? "")
2447
+ ) : null,
2448
+ h(
2449
+ "div",
2450
+ { className: "dshgt-detail-grid" },
2451
+ detailItem(t, "run.usage.commit", record.commit === void 0 ? "—" : h("span", { className: "dshgt-mono" }, record.commit)),
2452
+ detailItem(t, "run.usage.hash", h("span", { className: "dshgt-mono" }, shortHash(record.contentHash))),
2453
+ detailItem(t, "run.usage.branch", record.branch === void 0 ? "—" : h("span", { className: "dshgt-mono" }, record.branch)),
2454
+ detailItem(t, "runs.detail.coreModel", `${record.coreVersion || "—"} · ${record.model?.model ?? "—"}`),
2455
+ detailItem(t, "runs.detail.date", record.date ?? "—"),
2456
+ detailItem(
2457
+ t,
2458
+ "run.usage.pr",
2459
+ record.prUrl === void 0 ? "—" : h(
2460
+ "span",
2461
+ { className: "dshgt-inline" },
2462
+ h("a", { href: record.prUrl, target: "_blank", rel: "noreferrer", title: record.prUrl }, t("digest.openPr")),
2463
+ pr === null ? null : h("span", { className: "dshgt-tag" }, pr.label)
2464
+ )
2465
+ ),
2466
+ detailItem(t, "runs.detail.warnings", record.warnings.length === 0 ? t("runs.detail.none") : record.warnings.join(";")),
2467
+ detailItem(t, "runs.detail.files", `${record.files} · ${duration(record.durationMs)}`)
2468
+ ),
2469
+ record.legacy === true ? h("div", { className: "dshgt-hint" }, t("runs.legacyHint")) : null,
2470
+ h(
2471
+ "div",
2472
+ { className: "dshgt-run-actions" },
2473
+ 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")),
2474
+ h(Btn, { small: true, onClick: () => setLogId(record.id) }, t("runs.detail.log")),
2475
+ record.preview === void 0 ? null : h("span", { className: "dshgt-hint dshgt-mono" }, record.preview.file)
2476
+ )
2477
+ ) : null
2478
+ );
2479
+ })
1315
2480
  ),
1316
- h("div", { className: "dshgt-hint" }, `${t("widget.next")}:${state.schedule.nextRunAt === void 0 ? "—" : localTime(state.schedule.nextRunAt)}`),
1317
- h("div", { className: "dshgt-hint" }, `${t("widget.last")}:${last === null ? t("widget.never") : `${localTime(last.at)} · ${last.files} ${t("col.files")} · ${duration(last.durationMs)}`}`),
1318
- h("div", { className: "dshgt-hint dshgt-mono" }, `core ${state.core.version ?? "—"} · ipc ${state.core.ipcSchema ?? "—"}`, h("span", { className: `dshgt-dot ${coreKind}`, style: { marginLeft: 6, display: "inline-block" } })),
2481
+ logId === null ? null : h(LogDrawer, { t, runId: logId, onClose: () => setLogId(null) })
2482
+ );
2483
+ }
2484
+ function detailItem(t, label, value) {
2485
+ return h(
2486
+ "div",
2487
+ { className: "dshgt-detail-item" },
2488
+ h("div", { className: "dshgt-detail-k" }, t(label)),
2489
+ h("div", { className: "dshgt-detail-v" }, value)
2490
+ );
2491
+ }
2492
+
2493
+ // client/markdown-core.ts
2494
+ function isSafeUrl(value) {
2495
+ try {
2496
+ const url = new URL(value);
2497
+ return url.protocol === "http:" || url.protocol === "https:";
2498
+ } catch {
2499
+ return false;
2500
+ }
2501
+ }
2502
+ function splitFrontmatter(markdown) {
2503
+ const normalized = markdown.replace(/\r\n?/g, "\n");
2504
+ const lines = normalized.split("\n");
2505
+ if ((lines[0] ?? "").trim() !== "---") return { meta: {}, body: normalized };
2506
+ let end = -1;
2507
+ for (let index = 1; index < lines.length; index += 1) {
2508
+ if ((lines[index] ?? "").trim() === "---") {
2509
+ end = index;
2510
+ break;
2511
+ }
2512
+ }
2513
+ if (end < 0) return { meta: {}, body: normalized };
2514
+ const meta = {};
2515
+ for (const line of lines.slice(1, end)) {
2516
+ const colon = line.indexOf(":");
2517
+ if (colon <= 0) continue;
2518
+ const key = line.slice(0, colon).trim();
2519
+ let value = line.slice(colon + 1).trim();
2520
+ if (value.length >= 2 && (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))) {
2521
+ value = value.slice(1, -1);
2522
+ }
2523
+ if (key !== "") meta[key] = value;
2524
+ }
2525
+ return { meta, body: lines.slice(end + 1).join("\n") };
2526
+ }
2527
+ function pushText(nodes, value) {
2528
+ if (value === "") return;
2529
+ const last = nodes[nodes.length - 1];
2530
+ if (last !== void 0 && last.type === "text") {
2531
+ last.value += value;
2532
+ return;
2533
+ }
2534
+ nodes.push({ type: "text", value });
2535
+ }
2536
+ var INLINE_PATTERN = /(\*\*[^*\n]+\*\*)|(__[^_\n]+__)|(`[^`\n]+`)|(!?\[[^\]\n]*\]\([^)\s]+\))/g;
2537
+ function parseInline(text) {
2538
+ const nodes = [];
2539
+ let cursor = 0;
2540
+ const pattern = new RegExp(INLINE_PATTERN.source, "g");
2541
+ let match;
2542
+ while ((match = pattern.exec(text)) !== null) {
2543
+ if (match.index > cursor) pushText(nodes, text.slice(cursor, match.index));
2544
+ const token = match[0];
2545
+ if (token.startsWith("**") || token.startsWith("__")) {
2546
+ nodes.push({ type: "strong", children: parseInline(token.slice(2, -2)) });
2547
+ } else if (token.startsWith("`")) {
2548
+ nodes.push({ type: "code", value: token.slice(1, -1) });
2549
+ } else {
2550
+ const image = token.startsWith("!");
2551
+ const link = /^!?\[([^\]]*)\]\(([^)]*)\)$/.exec(token);
2552
+ if (link !== null) {
2553
+ const label = link[1] ?? "";
2554
+ const href = (link[2] ?? "").trim();
2555
+ if (image) pushText(nodes, label);
2556
+ else if (isSafeUrl(href)) nodes.push({ type: "link", href, children: parseInline(label) });
2557
+ else pushText(nodes, label);
2558
+ } else {
2559
+ pushText(nodes, token);
2560
+ }
2561
+ }
2562
+ cursor = pattern.lastIndex;
2563
+ }
2564
+ if (cursor < text.length) pushText(nodes, text.slice(cursor));
2565
+ return nodes;
2566
+ }
2567
+ var FENCE = /^\s*```\s*([^\s`]*)\s*$/;
2568
+ var HEADING = /^(#{1,6})\s+(.*)$/;
2569
+ var HR = /^\s*(?:-{3,}|\*{3,}|_{3,})\s*$/;
2570
+ var QUOTE = /^\s*>\s?(.*)$/;
2571
+ var BULLET = /^\s*[-*+]\s+(.*)$/;
2572
+ var ORDERED = /^\s*\d+[.)]\s+(.*)$/;
2573
+ function isBlockStart(line) {
2574
+ return FENCE.test(line) || HEADING.test(line) || HR.test(line) || QUOTE.test(line) || BULLET.test(line) || ORDERED.test(line);
2575
+ }
2576
+ function parseBlocks(body) {
2577
+ const lines = body.split("\n");
2578
+ const blocks = [];
2579
+ let index = 0;
2580
+ while (index < lines.length) {
2581
+ const line = lines[index] ?? "";
2582
+ if (line.trim() === "") {
2583
+ index += 1;
2584
+ continue;
2585
+ }
2586
+ const fence = FENCE.exec(line);
2587
+ if (fence !== null) {
2588
+ const lang = fence[1] ?? "";
2589
+ index += 1;
2590
+ const buffer2 = [];
2591
+ while (index < lines.length) {
2592
+ const current = lines[index] ?? "";
2593
+ if (/^\s*```\s*$/.test(current)) {
2594
+ index += 1;
2595
+ break;
2596
+ }
2597
+ buffer2.push(current);
2598
+ index += 1;
2599
+ }
2600
+ blocks.push({ type: "code", value: buffer2.join("\n"), ...lang === "" ? {} : { lang } });
2601
+ continue;
2602
+ }
2603
+ const heading = HEADING.exec(line);
2604
+ if (heading !== null) {
2605
+ blocks.push({ type: "heading", level: (heading[1] ?? "#").length, children: parseInline((heading[2] ?? "").trim()) });
2606
+ index += 1;
2607
+ continue;
2608
+ }
2609
+ if (HR.test(line)) {
2610
+ blocks.push({ type: "hr" });
2611
+ index += 1;
2612
+ continue;
2613
+ }
2614
+ if (QUOTE.test(line)) {
2615
+ const buffer2 = [];
2616
+ while (index < lines.length) {
2617
+ const match = QUOTE.exec(lines[index] ?? "");
2618
+ if (match === null) break;
2619
+ buffer2.push(match[1] ?? "");
2620
+ index += 1;
2621
+ }
2622
+ blocks.push({ type: "quote", children: parseInline(buffer2.join(" ").trim()) });
2623
+ continue;
2624
+ }
2625
+ if (BULLET.test(line)) {
2626
+ const items = [];
2627
+ while (index < lines.length) {
2628
+ const match = BULLET.exec(lines[index] ?? "");
2629
+ if (match === null) break;
2630
+ items.push(parseInline((match[1] ?? "").trim()));
2631
+ index += 1;
2632
+ }
2633
+ blocks.push({ type: "list", ordered: false, items });
2634
+ continue;
2635
+ }
2636
+ if (ORDERED.test(line)) {
2637
+ const items = [];
2638
+ while (index < lines.length) {
2639
+ const match = ORDERED.exec(lines[index] ?? "");
2640
+ if (match === null) break;
2641
+ items.push(parseInline((match[1] ?? "").trim()));
2642
+ index += 1;
2643
+ }
2644
+ blocks.push({ type: "list", ordered: true, items });
2645
+ continue;
2646
+ }
2647
+ const buffer = [];
2648
+ while (index < lines.length) {
2649
+ const current = lines[index] ?? "";
2650
+ if (current.trim() === "") break;
2651
+ if (buffer.length > 0 && isBlockStart(current)) break;
2652
+ buffer.push(current.trim());
2653
+ index += 1;
2654
+ }
2655
+ blocks.push({ type: "paragraph", children: parseInline(buffer.join(" ")) });
2656
+ }
2657
+ return blocks;
2658
+ }
2659
+ function parseMarkdown(markdown) {
2660
+ const { meta, body } = splitFrontmatter(markdown);
2661
+ return { meta, blocks: parseBlocks(body) };
2662
+ }
2663
+
2664
+ // client/markdown.ts
2665
+ function renderInline(nodes, prefix) {
2666
+ return nodes.map((node, index) => {
2667
+ const key = `${prefix}-${index}`;
2668
+ switch (node.type) {
2669
+ case "text":
2670
+ return node.value;
2671
+ case "strong":
2672
+ return h("strong", { key }, renderInline(node.children, key));
2673
+ case "code":
2674
+ return h("code", { key, className: "dshgt-md-code" }, node.value);
2675
+ case "link":
2676
+ return h(
2677
+ "a",
2678
+ { key, href: node.href, target: "_blank", rel: "noreferrer" },
2679
+ renderInline(node.children, key)
2680
+ );
2681
+ }
2682
+ });
2683
+ }
2684
+ function renderBlock(block, key) {
2685
+ switch (block.type) {
2686
+ case "heading": {
2687
+ const level = Math.min(Math.max(block.level, 1), 6);
2688
+ return h(`h${level}`, { key, className: `dshgt-md-h dshgt-md-h${level}` }, renderInline(block.children, key));
2689
+ }
2690
+ case "paragraph":
2691
+ return h("p", { key, className: "dshgt-md-p" }, renderInline(block.children, key));
2692
+ case "list":
2693
+ return h(
2694
+ block.ordered ? "ol" : "ul",
2695
+ { key, className: "dshgt-md-list" },
2696
+ block.items.map((item, index) => h("li", { key: `${key}-${index}` }, renderInline(item, `${key}-${index}`)))
2697
+ );
2698
+ case "quote":
2699
+ return h("blockquote", { key, className: "dshgt-md-quote" }, renderInline(block.children, key));
2700
+ case "code":
2701
+ return h(
2702
+ "pre",
2703
+ { key, className: "dshgt-md-pre", ...block.lang === void 0 ? {} : { "data-lang": block.lang } },
2704
+ h("code", null, block.value)
2705
+ );
2706
+ case "hr":
2707
+ return h("hr", { key, className: "dshgt-md-hr" });
2708
+ }
2709
+ }
2710
+ function renderMarkdown(markdown) {
2711
+ const parsed = parseMarkdown(markdown);
2712
+ return {
2713
+ meta: parsed.meta,
2714
+ nodes: parsed.blocks.map((block, index) => renderBlock(block, `b${index}`))
2715
+ };
2716
+ }
2717
+
2718
+ // client/digest.ts
2719
+ var STATUS_TAG = {
2720
+ preview: "digest.tag.preview",
2721
+ failed: "digest.tag.failed",
2722
+ skipped: "digest.tag.skipped",
2723
+ dry: "digest.tag.dry",
2724
+ legacy: "digest.tag.legacy",
2725
+ missing: "digest.tag.missing"
2726
+ };
2727
+ var STATUS_LABEL = {
2728
+ preview: "state.ok",
2729
+ failed: "state.failed",
2730
+ skipped: "state.skipped",
2731
+ dry: "state.dry",
2732
+ legacy: "digest.tag.legacy",
2733
+ missing: "digest.tag.missing"
2734
+ };
2735
+ var STATUS_LAMP2 = {
2736
+ preview: "ok",
2737
+ failed: "err",
2738
+ skipped: "warn",
2739
+ dry: "warn",
2740
+ legacy: "warn",
2741
+ missing: "warn"
2742
+ };
2743
+ var META_KEYS = ["date", "period", "languages", "count", "degraded", "coreVersion", "contentHash", "generatedAt"];
2744
+ function EmptyDigest(props) {
2745
+ const { t, day, nearest } = props;
2746
+ const status = props.error === void 0 ? day.status : "missing";
2747
+ const key = status === "legacy" ? "legacy" : status === "missing" ? "missing" : status;
2748
+ return h(
2749
+ "div",
2750
+ { className: "dshgt-digest-empty" },
2751
+ h(
2752
+ "div",
2753
+ { className: "dshgt-status-line" },
2754
+ h("span", { className: `dshgt-lamp ${STATUS_LAMP2[status]}` }),
2755
+ h("b", null, t(STATUS_LABEL[status])),
2756
+ h("span", { className: "dshgt-dim" }, formatDayLabel(day.date, props.lang))
2757
+ ),
2758
+ h("h2", null, t(`digest.empty.${key}.title`)),
2759
+ h("p", null, t(`digest.empty.${key}.reason`)),
2760
+ day.record.errorCode === void 0 && props.error === void 0 ? null : h(
2761
+ "p",
2762
+ { className: "dshgt-mono dshgt-digest-error" },
2763
+ [day.record.errorCode, day.record.errorMessage ?? props.error].filter((item) => item !== void 0 && item !== "").join(" · ")
2764
+ ),
2765
+ h(
2766
+ "div",
2767
+ { className: "dshgt-actions" },
2768
+ h(Btn, { small: true, onClick: props.onOpenRuns }, t("digest.empty.viewRuns")),
2769
+ nearest === void 0 ? null : h(Btn, { small: true, onClick: () => props.onSelectDate(nearest.date) }, t("digest.empty.latest", { date: formatDayLabel(nearest.date, props.lang) }))
2770
+ )
2771
+ );
2772
+ }
2773
+ function DigestView(props) {
2774
+ const { t, lang, store, selectedDate } = props;
2775
+ const [mode, setMode] = import_react.default.useState("render");
2776
+ const [payload, setPayload] = import_react.default.useState(null);
2777
+ const [loading, setLoading] = import_react.default.useState(false);
2778
+ const [error, setError] = import_react.default.useState(null);
2779
+ const [copied, setCopied] = import_react.default.useState(false);
2780
+ const state = store.state;
2781
+ const history = state?.status.history ?? [];
2782
+ const timeZone = state?.schedule.timezone ?? "UTC";
2783
+ const days = buildDigestDates(history, { timeZone });
2784
+ const defaultDate = days.find((item) => item.status === "preview")?.date ?? days[0]?.date;
2785
+ const effectiveDate = selectedDate !== void 0 && days.some((day2) => day2.date === selectedDate) ? selectedDate : defaultDate;
2786
+ const day = days.find((item) => item.date === effectiveDate);
2787
+ const index = day === void 0 ? -1 : days.indexOf(day);
2788
+ const previewCount = days.filter((item) => item.status === "preview").length;
2789
+ const previewRunId = day !== void 0 && day.status === "preview" && day.record.preview !== void 0 ? day.record.id : void 0;
2790
+ import_react.default.useEffect(() => {
2791
+ if (effectiveDate !== void 0 && effectiveDate !== selectedDate) props.onSelectDate(effectiveDate);
2792
+ }, [effectiveDate, selectedDate, props.onSelectDate]);
2793
+ import_react.default.useEffect(() => {
2794
+ setMode("render");
2795
+ }, [effectiveDate]);
2796
+ import_react.default.useEffect(() => {
2797
+ if (previewRunId === void 0) {
2798
+ setPayload(null);
2799
+ setError(null);
2800
+ setLoading(false);
2801
+ return () => void 0;
2802
+ }
2803
+ const controller = new AbortController();
2804
+ setLoading(true);
2805
+ setError(null);
2806
+ setPayload(null);
2807
+ getPreview(previewRunId, controller.signal).then((result) => {
2808
+ if (controller.signal.aborted) return;
2809
+ setPayload(result);
2810
+ setLoading(false);
2811
+ }).catch((failure) => {
2812
+ if (controller.signal.aborted) return;
2813
+ setError(failure instanceof Error ? failure.message : String(failure));
2814
+ setLoading(false);
2815
+ });
2816
+ return () => controller.abort();
2817
+ }, [previewRunId]);
2818
+ if (state === null) {
2819
+ return h("div", { className: "dshgt-empty" }, store.error ?? t("quickfill.loading"));
2820
+ }
2821
+ const nearest = day === void 0 ? void 0 : days.find((item) => item.status === "preview" && item.date !== day.date);
2822
+ let rendered = null;
2823
+ let parseFailed = false;
2824
+ if (payload !== null) {
2825
+ try {
2826
+ rendered = renderMarkdown(payload.preview.text);
2827
+ } catch {
2828
+ parseFailed = true;
2829
+ }
2830
+ }
2831
+ const subbar = h(
2832
+ "div",
2833
+ { className: "dshgt-digest-subbar" },
2834
+ h(
2835
+ "div",
2836
+ { className: "dshgt-datenav" },
2837
+ h(
2838
+ "button",
2839
+ {
2840
+ type: "button",
2841
+ className: "dshgt-icon-btn",
2842
+ "aria-label": t("digest.prev"),
2843
+ title: t("digest.prev"),
2844
+ disabled: index < 0 || index >= days.length - 1,
2845
+ onClick: () => {
2846
+ const target = days[index + 1];
2847
+ if (target !== void 0) props.onSelectDate(target.date);
2848
+ }
2849
+ },
2850
+ "‹"
2851
+ ),
2852
+ h(
2853
+ "select",
2854
+ {
2855
+ className: "dshgt-dateselect",
2856
+ "aria-label": t("digest.select"),
2857
+ value: effectiveDate ?? "",
2858
+ onChange: (event) => props.onSelectDate(event.target.value),
2859
+ disabled: days.length === 0
2860
+ },
2861
+ days.map((item) => h("option", { key: item.date, value: item.date }, `${formatDayLabel(item.date, lang)} · ${t(STATUS_TAG[item.status])}`))
2862
+ ),
2863
+ h(
2864
+ "button",
2865
+ {
2866
+ type: "button",
2867
+ className: "dshgt-icon-btn",
2868
+ "aria-label": t("digest.next"),
2869
+ title: t("digest.next"),
2870
+ disabled: index <= 0,
2871
+ onClick: () => {
2872
+ const target = days[index - 1];
2873
+ if (target !== void 0) props.onSelectDate(target.date);
2874
+ }
2875
+ },
2876
+ "›"
2877
+ )
2878
+ ),
2879
+ h(
2880
+ "span",
2881
+ { className: "dshgt-dim" },
2882
+ days.length === 0 ? t("digest.window.empty") : t("digest.window", { days: days.length, previews: previewCount })
2883
+ ),
2884
+ h("span", { className: "dshgt-grow" }),
2885
+ h(Btn, { small: true, onClick: props.onOpenRuns }, t("digest.allRuns"))
2886
+ );
2887
+ if (day === void 0) {
2888
+ return h(
2889
+ "div",
2890
+ { className: "dshgt-view dshgt-digest" },
2891
+ subbar,
2892
+ h(
2893
+ "div",
2894
+ { className: "dshgt-digest-empty" },
2895
+ h("h2", null, t("digest.empty.none.title")),
2896
+ h("p", null, t("digest.empty.none.reason")),
2897
+ h("div", { className: "dshgt-actions" }, h(Btn, { small: true, onClick: props.onOpenRuns }, t("digest.empty.viewRuns")))
2898
+ )
2899
+ );
2900
+ }
2901
+ const showEmpty = day.status !== "preview" || error !== null;
2902
+ const record = day.record;
2903
+ return h(
2904
+ "div",
2905
+ { className: "dshgt-view dshgt-digest" },
2906
+ subbar,
2907
+ h(
2908
+ "div",
2909
+ { className: "dshgt-digest-sheet" },
2910
+ showEmpty ? h(EmptyDigest, {
2911
+ t,
2912
+ lang,
2913
+ day,
2914
+ ...nearest === void 0 ? {} : { nearest },
2915
+ ...error === null ? {} : { error },
2916
+ onOpenRuns: props.onOpenRuns,
2917
+ onSelectDate: props.onSelectDate
2918
+ }) : h(
2919
+ "div",
2920
+ { className: "dshgt-digest-body" },
2921
+ h(
2922
+ "div",
2923
+ { className: "dshgt-digest-bar" },
2924
+ h(
2925
+ "span",
2926
+ { className: "dshgt-filechip dshgt-filechip-static" },
2927
+ h("span", { className: "dshgt-mono" }, record.preview?.file ?? "—")
2928
+ ),
2929
+ h("span", { className: "dshgt-dim" }, record.preview === void 0 ? "—" : formatBytes(record.preview.bytes)),
2930
+ payload?.preview.truncated === true ? h("span", { className: "dshgt-tag" }, t("digest.truncated")) : null,
2931
+ h("span", { className: "dshgt-grow" }),
2932
+ h(
2933
+ "div",
2934
+ { className: "dshgt-seg", role: "group", "aria-label": t("digest.mode") },
2935
+ h(
2936
+ "button",
2937
+ {
2938
+ type: "button",
2939
+ "aria-pressed": mode === "render" ? "true" : "false",
2940
+ className: mode === "render" ? "on" : "",
2941
+ onClick: () => setMode("render")
2942
+ },
2943
+ t("digest.render")
2944
+ ),
2945
+ h(
2946
+ "button",
2947
+ {
2948
+ type: "button",
2949
+ "aria-pressed": mode === "raw" ? "true" : "false",
2950
+ className: mode === "raw" ? "on" : "",
2951
+ onClick: () => setMode("raw")
2952
+ },
2953
+ t("digest.raw")
2954
+ )
2955
+ ),
2956
+ h(
2957
+ Btn,
2958
+ {
2959
+ small: true,
2960
+ onClick: () => {
2961
+ const file = record.preview?.file;
2962
+ if (file === void 0) return;
2963
+ void copyText(file).then((okResult) => {
2964
+ if (!okResult) return;
2965
+ setCopied(true);
2966
+ setTimeout(() => setCopied(false), 1600);
2967
+ });
2968
+ }
2969
+ },
2970
+ copied ? t("digest.copied") : t("digest.copyPath")
2971
+ ),
2972
+ record.prUrl === void 0 ? null : h(
2973
+ "a",
2974
+ { className: "dshgt-btn dshgt-btn-sm", href: record.prUrl, target: "_blank", rel: "noreferrer" },
2975
+ t("digest.openPr")
2976
+ )
2977
+ ),
2978
+ 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(
2979
+ "div",
2980
+ { className: "dshgt-digest-doc" },
2981
+ rendered === null ? null : h(
2982
+ import_react.default.Fragment,
2983
+ null,
2984
+ metaLine(rendered),
2985
+ h("div", { className: "dshgt-md" }, rendered.nodes)
2986
+ )
2987
+ )
2988
+ )
2989
+ )
2990
+ );
2991
+ }
2992
+ function metaLine(rendered) {
2993
+ const entries = META_KEYS.filter((key) => rendered.meta[key] !== void 0 && rendered.meta[key] !== "");
2994
+ if (entries.length === 0) return null;
2995
+ return h(
2996
+ "div",
2997
+ { className: "dshgt-doc-meta" },
2998
+ entries.map((key) => {
2999
+ const value = rendered.meta[key] ?? "";
3000
+ if (key === "contentHash") {
3001
+ const short = value.replace(/^sha256:/, "").slice(0, 10);
3002
+ return h("span", { key, title: value }, `${key}: ${short}`);
3003
+ }
3004
+ return h("span", { key }, `${key}: ${value}`);
3005
+ })
3006
+ );
3007
+ }
3008
+
3009
+ // client/about.ts
3010
+ function AboutDialog(props) {
3011
+ const { t, state } = props;
3012
+ const [copied, setCopied] = import_react.default.useState(false);
3013
+ const plugin = state.plugin;
3014
+ const fullName = `${plugin.name}@v${plugin.version}`;
3015
+ const repository = plugin.repository;
3016
+ const ownerRepo = repository === void 0 ? void 0 : repository.replace(/^https?:\/\/github\.com\//, "");
3017
+ const repoVersion = ownerRepo === void 0 ? void 0 : `${ownerRepo}@v${plugin.version}`;
3018
+ import_react.default.useEffect(() => {
3019
+ const onKey = (event) => {
3020
+ if (event.key === "Escape") props.onClose();
3021
+ };
3022
+ document.addEventListener("keydown", onKey);
3023
+ return () => document.removeEventListener("keydown", onKey);
3024
+ }, [props]);
3025
+ const copy = () => {
3026
+ void copyText(fullName).then((ok) => {
3027
+ if (!ok) return;
3028
+ setCopied(true);
3029
+ setTimeout(() => setCopied(false), 1600);
3030
+ });
3031
+ };
3032
+ return h(
3033
+ "div",
3034
+ { className: "dshgt-modal-mask", onClick: (event) => {
3035
+ if (event.target === event.currentTarget) props.onClose();
3036
+ } },
1319
3037
  h(
1320
3038
  "div",
1321
- { className: "dshgt-inline" },
1322
- h(Btn, { small: true, variant: "primary", disabled: busy || running, onClick: () => void run(false) }, t("widget.run")),
1323
- h(Btn, { small: true, disabled: busy || running, onClick: () => void run(true) }, t("run.dryRun")),
1324
- props.onOpen === void 0 ? null : h(Btn, { small: true, onClick: props.onOpen }, t("widget.open"))
3039
+ { className: "dshgt-modal dshgt-about", role: "dialog", "aria-modal": "true", "aria-label": t("about.title") },
3040
+ h(
3041
+ "div",
3042
+ { className: "dshgt-about-head" },
3043
+ h("h3", { className: "dshgt-about-title" }, t("about.title")),
3044
+ h(Btn, { small: true, onClick: () => props.onClose() }, t("about.close"))
3045
+ ),
3046
+ h(
3047
+ "button",
3048
+ { type: "button", className: "dshgt-about-pkg", onClick: copy, title: t("about.copy") },
3049
+ h("span", { className: "dshgt-mono" }, fullName),
3050
+ h("span", { className: "dshgt-hint" }, copied ? t("about.copied") : t("about.copy"))
3051
+ ),
3052
+ h(KV, {
3053
+ items: [
3054
+ [
3055
+ t("about.repo"),
3056
+ repository === void 0 || repoVersion === void 0 ? h("span", { className: "dshgt-hint" }, t("about.repo.missing")) : h("a", { href: repository, target: "_blank", rel: "noreferrer" }, repoVersion)
3057
+ ],
3058
+ [t("about.license"), "MIT"],
3059
+ [t("about.core"), state.core.version === void 0 ? "—" : `trendradar ${state.core.version}`],
3060
+ [t("about.env"), t("about.env.value")]
3061
+ ]
3062
+ })
1325
3063
  )
1326
3064
  );
1327
3065
  }
1328
3066
 
3067
+ // client/panel-icon.ts
3068
+ var TREND_LINE = "3 16.5 9 10.5 13 14.5 21 6.5";
3069
+ var TREND_ARROW = "15.5 6.5 21 6.5 21 12";
3070
+ function SidebarPanelIcon(props) {
3071
+ const size = typeof props.size === "number" ? props.size : 16;
3072
+ return h(
3073
+ "svg",
3074
+ {
3075
+ width: size,
3076
+ height: size,
3077
+ viewBox: "0 0 24 24",
3078
+ fill: "none",
3079
+ stroke: "currentColor",
3080
+ strokeWidth: props.active === true ? 2 : 1.7,
3081
+ strokeLinecap: "round",
3082
+ strokeLinejoin: "round",
3083
+ "aria-hidden": "true",
3084
+ focusable: "false"
3085
+ },
3086
+ h("polyline", { points: TREND_LINE }),
3087
+ h("polyline", { points: TREND_ARROW })
3088
+ );
3089
+ }
3090
+
1329
3091
  // client/index.ts
1330
3092
  var PANEL_ID = "dsh-github-trending";
1331
3093
  var inject = ["slots", "locale"];
3094
+ var NAV = [
3095
+ { id: "overview", label: "nav.overview", icon: "grid" },
3096
+ { id: "digest", label: "nav.digest", icon: "doc" },
3097
+ { id: "runs", label: "nav.runs", icon: "list" },
3098
+ { id: "settings", label: "nav.settings", icon: "sliders" }
3099
+ ];
3100
+ function navIcon(name, size = 16) {
3101
+ const common = {
3102
+ width: size,
3103
+ height: size,
3104
+ viewBox: "0 0 24 24",
3105
+ fill: "none",
3106
+ stroke: "currentColor",
3107
+ strokeWidth: 1.7,
3108
+ strokeLinecap: "round",
3109
+ strokeLinejoin: "round",
3110
+ "aria-hidden": "true",
3111
+ focusable: "false"
3112
+ };
3113
+ if (name === "grid") {
3114
+ return h(
3115
+ "svg",
3116
+ common,
3117
+ h("rect", { x: 3.5, y: 3.5, width: 7, height: 7, rx: 1.5 }),
3118
+ h("rect", { x: 13.5, y: 3.5, width: 7, height: 7, rx: 1.5 }),
3119
+ h("rect", { x: 3.5, y: 13.5, width: 7, height: 7, rx: 1.5 }),
3120
+ h("rect", { x: 13.5, y: 13.5, width: 7, height: 7, rx: 1.5 })
3121
+ );
3122
+ }
3123
+ if (name === "doc") {
3124
+ return h(
3125
+ "svg",
3126
+ common,
3127
+ h("path", { d: "M5 4.5h11a2 2 0 0 1 2 2V20H7a2 2 0 0 1-2-2z" }),
3128
+ h("path", { d: "M8 8h7M8 12h7M8 16h4" })
3129
+ );
3130
+ }
3131
+ if (name === "list") {
3132
+ return h(
3133
+ "svg",
3134
+ common,
3135
+ h("path", { d: "M8 6h12M8 12h12M8 18h12" }),
3136
+ h("path", { d: "M4 6h.01M4 12h.01M4 18h.01" })
3137
+ );
3138
+ }
3139
+ return h(
3140
+ "svg",
3141
+ common,
3142
+ h("path", { d: "M4 7h10M18 7h2M4 17h4M12 17h8" }),
3143
+ h("circle", { cx: 16, cy: 7, r: 2.2 }),
3144
+ h("circle", { cx: 10, cy: 17, r: 2.2 })
3145
+ );
3146
+ }
3147
+ function brandLogo() {
3148
+ return h(
3149
+ "svg",
3150
+ { width: 28, height: 28, viewBox: "0 0 64 64", "aria-hidden": "true" },
3151
+ h("rect", { x: 3, y: 3, width: 58, height: 58, rx: 15, fill: "#0d1117", stroke: "rgba(255,255,255,.14)", strokeWidth: 1 }),
3152
+ h("path", {
3153
+ d: "M18 46V22l14 12 14-12v24",
3154
+ fill: "none",
3155
+ stroke: "#58a6ff",
3156
+ strokeWidth: 4,
3157
+ strokeLinecap: "round",
3158
+ strokeLinejoin: "round"
3159
+ }),
3160
+ h("circle", { cx: 32, cy: 45, r: 3, fill: "#3fb950" })
3161
+ );
3162
+ }
3163
+ function relativeFuture(iso, t) {
3164
+ if (iso === void 0) return void 0;
3165
+ const target = new Date(iso).getTime();
3166
+ if (Number.isNaN(target)) return void 0;
3167
+ const diff = target - Date.now();
3168
+ if (diff <= 0) return void 0;
3169
+ const minutes = Math.max(1, Math.round(diff / 6e4));
3170
+ if (minutes < 60) return t("time.in.minutes", { minutes });
3171
+ const hours = Math.floor(minutes / 60);
3172
+ if (hours < 24) return t("time.in.hours", { hours, minutes: minutes % 60 });
3173
+ return t("time.in.days", { days: Math.floor(hours / 24) });
3174
+ }
3175
+ function nextRunText(iso) {
3176
+ if (iso === void 0) return "—";
3177
+ const date = new Date(iso);
3178
+ if (Number.isNaN(date.getTime())) return "—";
3179
+ return date.toLocaleString(void 0, { month: "numeric", day: "numeric", hour: "2-digit", minute: "2-digit" });
3180
+ }
1332
3181
  function apply(ctx) {
1333
3182
  const slots = ctx.get?.("slots");
1334
3183
  if (slots === void 0 || typeof slots.register !== "function") return;
1335
3184
  const locale = ctx.get?.("locale");
1336
3185
  const t = buildTranslate(locale);
3186
+ const lang = activeLang(locale);
1337
3187
  const disposeStyles = injectStyles();
1338
3188
  const disposers = [];
1339
- const layout = ctx.get?.("layout");
1340
3189
  const pickDirectory = async () => {
1341
3190
  const workspace = ctx.get?.("uiWorkspace");
1342
3191
  return workspace?.pickDirectory?.();
@@ -1344,44 +3193,200 @@ function apply(ctx) {
1344
3193
  function store() {
1345
3194
  return useStore();
1346
3195
  }
1347
- function SettingsSurface(props) {
3196
+ function PanelView() {
1348
3197
  const value = store();
1349
- return h(SettingsView, { t, store: value, pickDirectory, ...props?.close === void 0 ? {} : { onClose: props.close } });
1350
- }
1351
- function RunsSurface() {
1352
- const value = store();
1353
- return h(RunsView, { t, store: value, page: true });
1354
- }
1355
- function SidebarWidget() {
1356
- const value = store();
1357
- return h(Widget, {
3198
+ const controller = useRunController(value);
3199
+ const [view, setView] = import_react.default.useState("overview");
3200
+ const [digestDate, setDigestDate] = import_react.default.useState(void 0);
3201
+ const [aboutOpen, setAboutOpen] = import_react.default.useState(false);
3202
+ const openDigest = import_react.default.useCallback((date) => {
3203
+ if (date !== void 0) setDigestDate(date);
3204
+ setView("digest");
3205
+ }, []);
3206
+ const openRuns = import_react.default.useCallback(() => setView("runs"), []);
3207
+ const openSettings = import_react.default.useCallback(() => setView("settings"), []);
3208
+ const state = value.state;
3209
+ const plugin = state?.plugin;
3210
+ const history = state?.status.history ?? [];
3211
+ const schedule = state?.schedule;
3212
+ const ready = state === null ? 0 : [
3213
+ state.core.state === "ready",
3214
+ credentialView(state.model, t).ok,
3215
+ state.settings.repo.path.trim() !== "",
3216
+ state.github.configured
3217
+ ].filter(Boolean).length;
3218
+ const fullName = plugin === void 0 ? void 0 : `${plugin.name}@v${plugin.version}`;
3219
+ const digestTail = digestDate ?? state?.status.lastResult?.date ?? state?.status.history[0]?.date;
3220
+ const showActions = view === "overview" || view === "runs";
3221
+ const viewNode = view === "overview" ? h(OverviewView, {
1358
3222
  t,
3223
+ lang,
1359
3224
  store: value,
1360
- onOpen: () => {
1361
- try {
1362
- layout?.selectPanel?.(PANEL_ID);
1363
- } catch {
1364
- }
1365
- }
1366
- });
3225
+ controller,
3226
+ onOpenDigest: openDigest,
3227
+ onOpenRuns: openRuns,
3228
+ onOpenSettings: openSettings
3229
+ }) : view === "digest" ? h(DigestView, {
3230
+ t,
3231
+ lang,
3232
+ store: value,
3233
+ ...digestDate === void 0 ? {} : { selectedDate: digestDate },
3234
+ onSelectDate: setDigestDate,
3235
+ onOpenRuns: openRuns
3236
+ }) : view === "runs" ? h(RunsView, { t, store: value, controller, onOpenDigest: openDigest }) : h(SettingsView, { t, store: value, pickDirectory });
3237
+ return h(
3238
+ "div",
3239
+ { className: "dshgt-root dshgt-shell" },
3240
+ h(
3241
+ "div",
3242
+ { className: "dshgt-app" },
3243
+ h(
3244
+ "aside",
3245
+ { className: "dshgt-rail" },
3246
+ h(
3247
+ "div",
3248
+ { className: "dshgt-brand" },
3249
+ brandLogo(),
3250
+ h(
3251
+ "div",
3252
+ null,
3253
+ h("div", { className: "dshgt-brand-name" }, t("section.title")),
3254
+ h("div", { className: "dshgt-brand-ver" }, plugin === void 0 ? "" : `v${plugin.version}`)
3255
+ )
3256
+ ),
3257
+ h(
3258
+ "nav",
3259
+ { className: "dshgt-nav", "aria-label": t("shell.nav") },
3260
+ NAV.map(
3261
+ (item) => h(
3262
+ "button",
3263
+ {
3264
+ key: item.id,
3265
+ type: "button",
3266
+ className: `dshgt-nav-item${view === item.id ? " on" : ""}`,
3267
+ "aria-current": view === item.id ? "page" : void 0,
3268
+ onClick: () => setView(item.id)
3269
+ },
3270
+ navIcon(item.icon),
3271
+ h("span", null, t(item.label)),
3272
+ 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
3273
+ )
3274
+ )
3275
+ ),
3276
+ h("div", { className: "dshgt-rail-spacer" }),
3277
+ h(
3278
+ "div",
3279
+ { className: "dshgt-rail-status" },
3280
+ h("div", { className: "dshgt-rail-cap" }, t("shell.nextRun")),
3281
+ h(
3282
+ "div",
3283
+ { className: "dshgt-rail-time" },
3284
+ schedule === void 0 || !schedule.enabled ? t("shell.scheduleOff") : schedule.nextRunAt === void 0 ? `${schedule.time} · ${schedule.timezone}` : nextRunText(schedule.nextRunAt)
3285
+ ),
3286
+ h(
3287
+ "div",
3288
+ { className: "dshgt-rail-count" },
3289
+ schedule !== void 0 && schedule.enabled && schedule.nextRunAt !== void 0 ? relativeFuture(schedule.nextRunAt, t) ?? "" : schedule === void 0 ? "" : `${schedule.time} · ${schedule.timezone}`
3290
+ ),
3291
+ h(
3292
+ "div",
3293
+ { className: "dshgt-rail-ready" },
3294
+ h("span", { className: `dshgt-lamp ${ready === 4 ? "ok" : ready > 0 ? "warn" : "err"}` }),
3295
+ t("shell.ready", { ready, total: 4 })
3296
+ )
3297
+ ),
3298
+ h(
3299
+ "div",
3300
+ { className: "dshgt-rail-pkg" },
3301
+ h(
3302
+ "button",
3303
+ {
3304
+ type: "button",
3305
+ className: "dshgt-pkg-name",
3306
+ onClick: () => setAboutOpen(true),
3307
+ title: plugin === void 0 ? "" : fullName
3308
+ },
3309
+ plugin?.name ?? t("section.title")
3310
+ ),
3311
+ h(
3312
+ "div",
3313
+ { className: "dshgt-pkg-sub" },
3314
+ h("span", { className: "dshgt-mono" }, plugin === void 0 ? "" : `@v${plugin.version}`),
3315
+ plugin?.repository === void 0 ? null : h(
3316
+ "a",
3317
+ { href: plugin.repository, target: "_blank", rel: "noreferrer" },
3318
+ `${t("shell.github")} ↗`
3319
+ ),
3320
+ h(
3321
+ "button",
3322
+ {
3323
+ type: "button",
3324
+ className: "dshgt-pkg-copy",
3325
+ title: t("shell.package.copy"),
3326
+ "aria-label": t("shell.package.copy"),
3327
+ disabled: fullName === void 0,
3328
+ onClick: () => {
3329
+ if (fullName !== void 0) void copyText(fullName);
3330
+ }
3331
+ },
3332
+ navIcon("doc", 13)
3333
+ )
3334
+ )
3335
+ )
3336
+ ),
3337
+ h(
3338
+ "main",
3339
+ { className: "dshgt-main" },
3340
+ h(
3341
+ "header",
3342
+ { className: "dshgt-page-head" },
3343
+ h(
3344
+ "div",
3345
+ { className: "dshgt-page-heading" },
3346
+ h("h1", null, t(`nav.${view}`)),
3347
+ h("p", { className: "dshgt-dek" }, t(`shell.${view}.dek`))
3348
+ ),
3349
+ h(
3350
+ "div",
3351
+ { className: "dshgt-head-actions", style: showActions ? void 0 : { visibility: "hidden" } },
3352
+ showActions ? [
3353
+ h(
3354
+ Btn,
3355
+ {
3356
+ key: "dry",
3357
+ onClick: () => void controller.run(true),
3358
+ disabled: controller.running || controller.busy
3359
+ },
3360
+ t("run.dryRunShort")
3361
+ ),
3362
+ h(
3363
+ Btn,
3364
+ {
3365
+ key: "run",
3366
+ variant: "primary",
3367
+ onClick: () => void controller.run(),
3368
+ disabled: controller.running || controller.busy
3369
+ },
3370
+ controller.running ? t("run.running") : t("run.button")
3371
+ ),
3372
+ controller.running ? h(Btn, { key: "cancel", variant: "danger", onClick: () => void controller.cancel() }, t("run.cancel")) : null
3373
+ ] : null
3374
+ )
3375
+ ),
3376
+ h("div", { className: "dshgt-main-body" }, view === "settings" ? viewNode : h("div", { className: "dshgt-scroll" }, viewNode))
3377
+ )
3378
+ ),
3379
+ aboutOpen && state !== null ? h(AboutDialog, { t, state, onClose: () => setAboutOpen(false) }) : null
3380
+ );
1367
3381
  }
1368
- disposers.push(
1369
- slots.inject(
1370
- "settings.section",
1371
- () => slots.register(
1372
- { name: "settings.section", id: PANEL_ID, order: 40, label: () => t("section.title") },
1373
- SettingsSurface
1374
- )
1375
- )
1376
- );
1377
3382
  disposers.push(
1378
3383
  slots.inject("main", () => {
1379
- const disposeMain = slots.register({ name: "main", key: PANEL_ID }, RunsSurface);
3384
+ const disposeMain = slots.register({ name: "main", key: PANEL_ID }, PanelView);
1380
3385
  const disposeEntry = slots.inject(
1381
3386
  "sidebar.panellist",
1382
3387
  () => slots.register(
1383
3388
  { name: "sidebar.panellist", id: PANEL_ID, order: 40, label: () => t("section.title") },
1384
- SidebarWidget
3389
+ SidebarPanelIcon
1385
3390
  )
1386
3391
  );
1387
3392
  return [disposeMain, disposeEntry];