dsh-background-by-model 0.4.1 → 0.4.2

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/lib/client.js CHANGED
@@ -25,9 +25,11 @@ window.__ModuleLoader__.load({
25
25
  pageInterface: "界面",
26
26
  pageModelBg: "模型背景",
27
27
  pageProfile: "配置",
28
+ pageHostCheck: "宿主自检",
28
29
  descInterface: "为主界面的各个区域单独调节透明度与模糊,营造空间层次感。这里的设置对所有模型生效",
29
30
  descModelBg: "每条规则 = 一个匹配串 + 一套背景外观。切换模型时,从上往下找第一条匹配串出现在模型名里的规则;都没中就使用第 1 条",
30
31
  descProfile: "把当前全部规则连同图片导出备份,或从文件一键恢复",
32
+ descHostCheck: "逐条检查本插件依赖的宿主接口在这个 dsh 版本上是否还在:会话与模型服务、它着色的界面元素、以及被重新发出的设计令牌。红色那一行就是坏掉的地方",
31
33
  uiTitle: "主界面",
32
34
  uiOpacity: "透明度",
33
35
  uiBlur: "模糊度",
@@ -121,7 +123,32 @@ window.__ModuleLoader__.load({
121
123
  toastExportDone: "配置文件已开始下载",
122
124
  importDone: "已导入配置",
123
125
  importFail: "导入失败,文件格式不正确",
124
- footerTag: "按模型换背景插件"
126
+ footerTag: "按模型换背景插件",
127
+ hostAllOk: "这个 dsh 版本上一切正常",
128
+ hostPartial: "有东西坏掉了",
129
+ hostBroken: "插件依赖的接口全都不见了",
130
+ hostUnknown: "还读不到结果",
131
+ hostSummary: "{pass} 项正常 · {fail} 项失效 · {skip} 项当前无法观察",
132
+ hostRunning: "检查中…",
133
+ hostRerun: "重新检查",
134
+ hostCopy: "复制诊断报告",
135
+ hostCopied: "诊断报告已复制",
136
+ hostCopyFail: "复制失败,浏览器拒绝了剪贴板访问",
137
+ hostPass: "正常",
138
+ hostFail: "失效",
139
+ hostNa: "未观察",
140
+ hostInfo: "仅记录",
141
+ hostPlugin: "插件",
142
+ hostPluginFloor: "要求 dsh",
143
+ hostDiskScan: "安装文件",
144
+ hostUnreadable: "未读到",
145
+ hostModel: "当前模型",
146
+ hostModelNone: "未检测到",
147
+ hostExpectedIn: "应该还在",
148
+ hostUsedBy: "本插件读取处",
149
+ hostBelowFloor: "当前 dsh 版本低于本插件声明的下限,宿主可能已经拒绝加载它。升级 dsh,或用 dsh plugin allow-version 明确放行。",
150
+ hostDiskFailures: "有 {n} 条契约在安装目录里已经找不到对应的宿主代码 —— 说明它们被改名或删掉了,而不是这次没观察到。",
151
+ hostHint: "灰色的「未观察」不是故障:设置弹窗打开时看不到对话页,没开过文件时右侧栏也不在 DOM 里。想让它变绿,把对应界面打开再看一次。"
125
152
  };
126
153
  const en = {
127
154
  nav: "Model background",
@@ -130,9 +157,11 @@ window.__ModuleLoader__.load({
130
157
  pageInterface: "Interface",
131
158
  pageModelBg: "Model background",
132
159
  pageProfile: "Profile",
160
+ pageHostCheck: "Host check",
133
161
  descInterface: "Tune opacity and blur per surface to build depth. These settings are global and apply to every model",
134
162
  descModelBg: "Each rule is a match string plus one look. On a model switch the list is scanned top-down for the first match string contained in the model name; if nothing hits, rule 1 is used",
135
163
  descProfile: "Back up every rule together with its image, or restore one from a file",
164
+ descHostCheck: "Checks every host interface this plugin depends on against the dsh build you are running: the session and model services, the elements it paints, and the design tokens it re-emits. The red line is what broke",
136
165
  uiTitle: "Interface",
137
166
  uiOpacity: "Opacity",
138
167
  uiBlur: "Blur",
@@ -226,7 +255,32 @@ window.__ModuleLoader__.load({
226
255
  toastExportDone: "Export started",
227
256
  importDone: "Profile imported",
228
257
  importFail: "Import failed — invalid file",
229
- footerTag: "Per-model wallpaper plugin"
258
+ footerTag: "Per-model wallpaper plugin",
259
+ hostAllOk: "Everything holds on this dsh build",
260
+ hostPartial: "Something broke",
261
+ hostBroken: "None of the interfaces this plugin needs are there",
262
+ hostUnknown: "No result yet",
263
+ hostSummary: "{pass} ok · {fail} broken · {skip} not observable right now",
264
+ hostRunning: "Checking…",
265
+ hostRerun: "Check again",
266
+ hostCopy: "Copy diagnostics",
267
+ hostCopied: "Diagnostics copied",
268
+ hostCopyFail: "Copy failed — the browser denied clipboard access",
269
+ hostPass: "ok",
270
+ hostFail: "broken",
271
+ hostNa: "n/a",
272
+ hostInfo: "note",
273
+ hostPlugin: "Plugin",
274
+ hostPluginFloor: "needs dsh",
275
+ hostDiskScan: "Installed files",
276
+ hostUnreadable: "unreadable",
277
+ hostModel: "Current model",
278
+ hostModelNone: "not detected",
279
+ hostExpectedIn: "Should still be in",
280
+ hostUsedBy: "Read by",
281
+ hostBelowFloor: "This dsh is older than the floor this plugin declares, so the host may already refuse to load it. Update dsh, or grant it explicitly with dsh plugin allow-version.",
282
+ hostDiskFailures: "{n} contract(s) can no longer be found in the installed host code — they were renamed or removed, this is not just \"not observable right now\".",
283
+ hostHint: "A grey \"n/a\" is not a failure: the conversation view is off screen while the settings dialog is open, and the right panel is not in the DOM until a file has been opened. Open that surface and check again to turn it green."
230
284
  };
231
285
  //#endregion
232
286
  //#region src/schema.ts
@@ -687,6 +741,21 @@ window.__ModuleLoader__.load({
687
741
  const parts = [d.provider, d.model].filter((s) => typeof s === "string" && s !== "");
688
742
  return parts.length > 0 ? parts.join(" ") : null;
689
743
  }
744
+ /**
745
+ * The installed-host half of the self-check: the host version and whether the
746
+ * packages on disk still contain every literal the plugin depends on.
747
+ *
748
+ * Returns null when the node half does not answer (an older build of this plugin
749
+ * still mounted, a host without the RPC channel); the panel then shows the live
750
+ * probe alone rather than claiming the disk scan passed.
751
+ */
752
+ async function readHostScan(lang) {
753
+ const data = await rpcCall("hostCheck", { lang });
754
+ if (data === null || typeof data !== "object") return null;
755
+ const d = data;
756
+ if (!Array.isArray(d.results)) return null;
757
+ return data;
758
+ }
690
759
  //#endregion
691
760
  //#region src/client/utils/color.ts
692
761
  function hsvToHsl(h, s, v) {
@@ -2098,6 +2167,1193 @@ window.__ModuleLoader__.load({
2098
2167
  release();
2099
2168
  };
2100
2169
  }
2170
+ //#endregion
2171
+ //#region src/host-contracts.ts
2172
+ const SIDEBAR_RIGHT = "dsh-client-ui-sidebar-right";
2173
+ /**
2174
+ * The contract table.
2175
+ *
2176
+ * Ordered by group and then by how visible a break is: the model hop first (the
2177
+ * plugin's whole purpose), then the surfaces it paints, then the tokens it
2178
+ * re-emits, then the regions it decorates.
2179
+ */
2180
+ const HOST_CONTRACTS = [
2181
+ {
2182
+ id: "sessions.list",
2183
+ group: "model",
2184
+ kind: "host-service",
2185
+ criticality: "core",
2186
+ since: "0.1.7",
2187
+ label: {
2188
+ en: "Session list service",
2189
+ zh: "会话列表服务"
2190
+ },
2191
+ symptom: {
2192
+ en: "no session id can be read, so the background is stuck on rule 1 and never follows the model",
2193
+ zh: "读不到会话 id,背景会一直停在规则 1,不再跟着模型切换"
2194
+ },
2195
+ target: "sessions.list",
2196
+ checks: [
2197
+ {
2198
+ kind: "service",
2199
+ id: "sessions"
2200
+ },
2201
+ {
2202
+ kind: "path",
2203
+ service: "sessions",
2204
+ path: "list",
2205
+ expect: "observable"
2206
+ },
2207
+ {
2208
+ kind: "path",
2209
+ service: "sessions",
2210
+ path: "list.getSnapshot",
2211
+ expect: "function"
2212
+ }
2213
+ ],
2214
+ sources: [{
2215
+ path: "dsh-api-session-controller/lib/client.js",
2216
+ literal: "retainedBy"
2217
+ }],
2218
+ usedBy: "src/client/modelbg.ts"
2219
+ },
2220
+ {
2221
+ id: "sessions.list.current",
2222
+ group: "model",
2223
+ kind: "host-service",
2224
+ criticality: "label",
2225
+ since: "<=0.1.6",
2226
+ label: {
2227
+ en: "Session list: legacy current id",
2228
+ zh: "会话列表:旧版 current 字段"
2229
+ },
2230
+ symptom: {
2231
+ en: "harmless on its own — the 0.1.7 hops below take over",
2232
+ zh: "本身无害 —— 0.1.7 上由下面的兜底路径接管"
2233
+ },
2234
+ target: "list.current",
2235
+ checks: [],
2236
+ sources: [{
2237
+ path: "dsh-api-session-controller/lib/client.js",
2238
+ literal: "retainedBy"
2239
+ }],
2240
+ usedBy: "src/client/modelbg.ts"
2241
+ },
2242
+ {
2243
+ id: "sessions.retainedBy.mainView",
2244
+ group: "model",
2245
+ kind: "host-service",
2246
+ criticality: "label",
2247
+ since: "0.1.7",
2248
+ label: {
2249
+ en: "Main-view retention on the session row",
2250
+ zh: "会话行的主视图占用标记"
2251
+ },
2252
+ symptom: {
2253
+ en: "while uiSession is not mounted yet, no session id can be derived",
2254
+ zh: "uiSession 尚未挂载时推导不出会话 id"
2255
+ },
2256
+ target: "byId[].retainedBy.mainView",
2257
+ checks: [],
2258
+ sources: [{
2259
+ path: "dsh-api-session-controller/lib/client.js",
2260
+ literal: "retainedBy"
2261
+ }],
2262
+ usedBy: "src/client/modelbg.ts"
2263
+ },
2264
+ {
2265
+ id: "uiSession.current",
2266
+ group: "model",
2267
+ kind: "host-service",
2268
+ criticality: "core",
2269
+ since: "0.1.7",
2270
+ label: {
2271
+ en: "uiSession service (main view session)",
2272
+ zh: "uiSession 服务(主视图会话)"
2273
+ },
2274
+ symptom: {
2275
+ en: "the session id can only be guessed from the list retention",
2276
+ zh: "会话 id 只能退回用列表占用标记去猜"
2277
+ },
2278
+ target: "uiSession.current",
2279
+ checks: [{
2280
+ kind: "path",
2281
+ service: "uiSession",
2282
+ path: "current",
2283
+ expect: "observable"
2284
+ }],
2285
+ sources: [{
2286
+ path: "dsh-client-ui-session/lib/client.js",
2287
+ literal: "uiSession"
2288
+ }],
2289
+ usedBy: "src/client/modelbg.ts"
2290
+ },
2291
+ {
2292
+ id: "sessions.binding.projections",
2293
+ group: "model",
2294
+ kind: "host-service",
2295
+ criticality: "core",
2296
+ since: "<=0.1.6",
2297
+ label: {
2298
+ en: "Session binding + projections",
2299
+ zh: "会话 binding 与 projections"
2300
+ },
2301
+ symptom: {
2302
+ en: "this session's own model selection is unreadable — only the host default is left",
2303
+ zh: "读不到本会话自己的模型选择,只剩宿主默认模型可用"
2304
+ },
2305
+ target: "binding(id).session.projections.faceOf(\"modelSelection\")",
2306
+ checks: [{
2307
+ kind: "path",
2308
+ service: "sessions",
2309
+ path: "binding",
2310
+ expect: "function"
2311
+ }],
2312
+ sources: [{
2313
+ path: "dsh-api-session-controller/lib/client.js",
2314
+ literal: "faceOf"
2315
+ }],
2316
+ usedBy: "src/client/modelbg.ts"
2317
+ },
2318
+ {
2319
+ id: "modelDirectories.directoryFor",
2320
+ group: "model",
2321
+ kind: "host-service",
2322
+ criticality: "label",
2323
+ since: "<=0.1.6",
2324
+ label: {
2325
+ en: "Model directory (display names)",
2326
+ zh: "模型目录(显示名)"
2327
+ },
2328
+ symptom: {
2329
+ en: "the model is shown as provider/model id instead of its display name",
2330
+ zh: "模型只会显示 provider/模型 id,读不到显示名"
2331
+ },
2332
+ target: "modelDirectories.directoryFor(id)",
2333
+ checks: [{
2334
+ kind: "path",
2335
+ service: "modelDirectories",
2336
+ path: "directoryFor",
2337
+ expect: "function"
2338
+ }],
2339
+ sources: [{
2340
+ path: "dsh-client-ui-model-selection/lib/client.js",
2341
+ literal: "modelSelection"
2342
+ }],
2343
+ usedBy: "src/client/modelbg.ts"
2344
+ },
2345
+ {
2346
+ id: "shell.overlay",
2347
+ group: "surface",
2348
+ kind: "host-attr",
2349
+ criticality: "looks",
2350
+ since: "<=0.1.6",
2351
+ label: {
2352
+ en: "AppFrame overlay + its three columns",
2353
+ zh: "主框架浮层与三列"
2354
+ },
2355
+ symptom: {
2356
+ en: "per-part blur and the main-background opacity cannot find the columns at all",
2357
+ zh: "分区域模糊与主背景透明度完全找不到那几列"
2358
+ },
2359
+ target: "[data-shell-overlay]",
2360
+ checks: [{
2361
+ kind: "attr",
2362
+ attr: "data-shell-overlay"
2363
+ }],
2364
+ sources: [{
2365
+ path: "dsh-client-ui-layout/lib/client.js",
2366
+ literal: "data-shell-overlay"
2367
+ }],
2368
+ usedBy: "src/client/wallpaper.ts"
2369
+ },
2370
+ {
2371
+ id: "settings.dialog",
2372
+ group: "surface",
2373
+ kind: "dom-selector",
2374
+ criticality: "looks",
2375
+ since: "<=0.1.6",
2376
+ label: {
2377
+ en: "Settings dialog (aria-modal)",
2378
+ zh: "设置弹窗(aria-modal)"
2379
+ },
2380
+ symptom: {
2381
+ en: "the settings panel opacity and blur sliders stop having any effect",
2382
+ zh: "设置界面透明度与模糊失效"
2383
+ },
2384
+ target: "[role=\"dialog\"][aria-modal=\"true\"][aria-labelledby]",
2385
+ checks: [{
2386
+ kind: "selector",
2387
+ selector: "div[role=\"dialog\"][aria-modal=\"true\"][aria-labelledby]",
2388
+ optionalWhen: "settingsClosed"
2389
+ }],
2390
+ sources: [{
2391
+ path: "dsh-client-ui-primitives/lib/index.js",
2392
+ literal: "aria-modal"
2393
+ }, {
2394
+ path: "src/client/components/ui.css.ts",
2395
+ literal: ".dab-card"
2396
+ }],
2397
+ usedBy: "src/client/wallpaper.ts"
2398
+ },
2399
+ {
2400
+ id: "settings.card",
2401
+ group: "surface",
2402
+ kind: "host-style",
2403
+ criticality: "looks",
2404
+ since: "<=0.1.6",
2405
+ label: {
2406
+ en: "Option card inside the dialog (.dab-card)",
2407
+ zh: "弹窗内的选项卡(.dab-card)"
2408
+ },
2409
+ symptom: {
2410
+ en: "the dialog option-panel blur has nothing to attach to",
2411
+ zh: "弹窗内选项卡的模糊没有附着对象"
2412
+ },
2413
+ target: ".dab-card",
2414
+ checks: [{
2415
+ kind: "rule",
2416
+ match: ".dab-card",
2417
+ hint: "settings dialog card class"
2418
+ }],
2419
+ sources: [{
2420
+ path: "src/client/components/ui.css.ts",
2421
+ literal: ".dab-card"
2422
+ }],
2423
+ usedBy: "src/client/wallpaper.ts"
2424
+ },
2425
+ {
2426
+ id: "composer.card",
2427
+ group: "surface",
2428
+ kind: "host-attr",
2429
+ criticality: "looks",
2430
+ since: "<=0.1.6",
2431
+ label: {
2432
+ en: "Composer card",
2433
+ zh: "输入框卡片"
2434
+ },
2435
+ symptom: {
2436
+ en: "the composer loses its backdrop — the input blur slider does nothing",
2437
+ zh: "输入框失去毛玻璃背板,输入区模糊滑块失效"
2438
+ },
2439
+ target: "[data-composer-card]",
2440
+ checks: [{
2441
+ kind: "attr",
2442
+ attr: "data-composer-card",
2443
+ optionalWhen: "chatNotMounted"
2444
+ }],
2445
+ sources: [{
2446
+ path: "dsh-client-ui-conversation/lib/client.js",
2447
+ literal: "data-composer-card"
2448
+ }],
2449
+ usedBy: "src/client/wallpaper.ts"
2450
+ },
2451
+ {
2452
+ id: "cordis.panel",
2453
+ group: "surface",
2454
+ kind: "host-attr",
2455
+ criticality: "label",
2456
+ since: "<=0.1.6",
2457
+ label: {
2458
+ en: "Cordis panel",
2459
+ zh: "Cordis 面板"
2460
+ },
2461
+ symptom: {
2462
+ en: "the Cordis panel keeps the host surface instead of the input slider's alpha",
2463
+ zh: "Cordis 面板拿不到输入区滑块的透明度,仍是宿主表面"
2464
+ },
2465
+ target: "[data-cordis-panel]",
2466
+ checks: [{
2467
+ kind: "attr",
2468
+ attr: "data-cordis-panel",
2469
+ optionalWhen: "cordisClosed"
2470
+ }],
2471
+ sources: [{
2472
+ path: "dsh-client-ui-cordis/lib/client.js",
2473
+ literal: "data-cordis-panel"
2474
+ }],
2475
+ usedBy: "src/client/wallpaper.ts"
2476
+ },
2477
+ {
2478
+ id: "rightbar.panel",
2479
+ group: "surface",
2480
+ kind: "host-attr",
2481
+ criticality: "looks",
2482
+ since: "0.1.7",
2483
+ label: {
2484
+ en: "File-preview panel host",
2485
+ zh: "文件预览面板宿主"
2486
+ },
2487
+ symptom: {
2488
+ en: "the right panel loses its own opacity/blur card and falls back to the main background",
2489
+ zh: "右侧栏失去自己的透明度/模糊控制,退回跟随主背景"
2490
+ },
2491
+ target: "[data-sidebar-right-panel]",
2492
+ checks: [{
2493
+ kind: "selector",
2494
+ selector: "div[data-sidebar-right-panel]",
2495
+ optionalWhen: "noRightPanel"
2496
+ }],
2497
+ sources: [{
2498
+ path: `${SIDEBAR_RIGHT}/lib/client.js`,
2499
+ literal: "data-sidebar-right-panel"
2500
+ }],
2501
+ usedBy: "src/client/wallpaper.ts"
2502
+ },
2503
+ {
2504
+ id: "rightbar.openFlag",
2505
+ group: "surface",
2506
+ kind: "host-attr",
2507
+ criticality: "looks",
2508
+ since: "0.1.7",
2509
+ label: {
2510
+ en: "Right panel open/closed flag",
2511
+ zh: "右侧栏开合标记"
2512
+ },
2513
+ symptom: {
2514
+ en: "the panel's backdrop cannot tell open from closed — a frosted plate covers the empty half",
2515
+ zh: "分不清右侧栏开着还是关着,空白的右半屏会出现磨砂板"
2516
+ },
2517
+ target: "[data-sidebar-right-open]",
2518
+ checks: [{
2519
+ kind: "rule",
2520
+ match: "[data-sidebar-right-open]",
2521
+ hint: "right panel open flag"
2522
+ }],
2523
+ sources: [{
2524
+ path: `${SIDEBAR_RIGHT}/lib/client.js`,
2525
+ literal: "data-sidebar-right-open"
2526
+ }],
2527
+ usedBy: "src/client/wallpaper.ts"
2528
+ },
2529
+ {
2530
+ id: "dockkit.content",
2531
+ group: "surface",
2532
+ kind: "host-attr",
2533
+ criticality: "core",
2534
+ since: "0.1.7",
2535
+ label: {
2536
+ en: "Dock tab host (the panel's painted surface)",
2537
+ zh: "停靠页宿主(面板真正的表面)"
2538
+ },
2539
+ symptom: {
2540
+ en: "nothing carries the panel surface any more — the right-panel card is painted on a container that is always full width",
2541
+ zh: "面板表面不再落在任何元素上,右面板卡片会被画在一个恒定满宽的容器上"
2542
+ },
2543
+ target: "[data-dockkit-content]",
2544
+ checks: [{
2545
+ kind: "rule",
2546
+ match: "[data-dockkit-content]",
2547
+ hint: "dock tab host"
2548
+ }],
2549
+ sources: [{
2550
+ path: ".",
2551
+ literal: "dockkit-content",
2552
+ root: "frontend"
2553
+ }],
2554
+ usedBy: "src/client/wallpaper.ts"
2555
+ },
2556
+ {
2557
+ id: "dockkit.empty",
2558
+ group: "surface",
2559
+ kind: "host-attr",
2560
+ criticality: "looks",
2561
+ since: "0.1.7",
2562
+ label: {
2563
+ en: "Dock empty seat",
2564
+ zh: "停靠空位"
2565
+ },
2566
+ symptom: {
2567
+ en: "an empty preview pane paints the host surface over the wallpaper",
2568
+ zh: "空的预览格会用宿主表面盖住壁纸"
2569
+ },
2570
+ target: "[data-dockkit-empty]",
2571
+ checks: [{
2572
+ kind: "rule",
2573
+ match: "[data-dockkit-empty]",
2574
+ hint: "dock empty seat"
2575
+ }],
2576
+ sources: [{
2577
+ path: `${SIDEBAR_RIGHT}/lib/client.js`,
2578
+ literal: "data-dockkit-empty"
2579
+ }],
2580
+ usedBy: "src/client/wallpaper.ts"
2581
+ },
2582
+ {
2583
+ id: "dockkit.float",
2584
+ group: "surface",
2585
+ kind: "host-attr",
2586
+ criticality: "label",
2587
+ since: "0.1.7",
2588
+ label: {
2589
+ en: "Floating dock pane flag",
2590
+ zh: "浮动停靠面板标记"
2591
+ },
2592
+ symptom: {
2593
+ en: "floating panes would be painted like docked ones, frosting a panel that should stay the host's",
2594
+ zh: "浮动面板会被当作停靠面板上色,本该保持宿主样式的面板被磨砂"
2595
+ },
2596
+ target: "[data-dockkit-float]",
2597
+ checks: [{
2598
+ kind: "rule",
2599
+ match: "[data-dockkit-float]",
2600
+ hint: "floating pane flag"
2601
+ }],
2602
+ sources: [{
2603
+ path: `${SIDEBAR_RIGHT}/lib/client.js`,
2604
+ literal: "data-dockkit-float"
2605
+ }],
2606
+ usedBy: "src/client/wallpaper.ts"
2607
+ },
2608
+ {
2609
+ id: "chat.flow",
2610
+ group: "region",
2611
+ kind: "dom-selector",
2612
+ criticality: "looks",
2613
+ since: "<=0.1.6",
2614
+ label: {
2615
+ en: "Chat message column",
2616
+ zh: "对话消息列"
2617
+ },
2618
+ symptom: {
2619
+ en: "the conversation card (and its opacity/blur) cannot find the message column",
2620
+ zh: "对话卡片(及其透明度/模糊)找不到消息列"
2621
+ },
2622
+ target: "[data-chat-flow]",
2623
+ checks: [{
2624
+ kind: "selector",
2625
+ selector: "[data-chat-flow]",
2626
+ optionalWhen: "chatNotMounted"
2627
+ }],
2628
+ sources: [{
2629
+ path: "dsh-client-ui-chat/lib/client.js",
2630
+ literal: "data-chat-flow"
2631
+ }],
2632
+ usedBy: "src/client/wallpaper.ts"
2633
+ },
2634
+ {
2635
+ id: "conversation.scroll",
2636
+ group: "region",
2637
+ kind: "dom-selector",
2638
+ criticality: "looks",
2639
+ since: "<=0.1.6",
2640
+ label: {
2641
+ en: "Conversation scrollport",
2642
+ zh: "对话滚动区"
2643
+ },
2644
+ symptom: {
2645
+ en: "the marker-less fallback would wrap the whole scrollport in the conversation card",
2646
+ zh: "标记缺失时的兜底会把整个滚动区套上对话卡片"
2647
+ },
2648
+ target: "[data-conversation-scroll]",
2649
+ checks: [{
2650
+ kind: "selector",
2651
+ selector: "[data-conversation-scroll]",
2652
+ optionalWhen: "chatNotMounted"
2653
+ }],
2654
+ sources: [{
2655
+ path: "dsh-client-ui-conversation/lib/client.js",
2656
+ literal: "data-conversation-scroll"
2657
+ }],
2658
+ usedBy: "src/client/wallpaper.ts"
2659
+ },
2660
+ {
2661
+ id: "trajectory.root",
2662
+ group: "region",
2663
+ kind: "dom-selector",
2664
+ criticality: "looks",
2665
+ since: "<=0.1.6",
2666
+ label: {
2667
+ en: "Trajectory view root",
2668
+ zh: "轨迹页根节点"
2669
+ },
2670
+ symptom: {
2671
+ en: "the trajectory opacity/blur sliders stop reaching the trajectory page",
2672
+ zh: "轨迹页的透明度与模糊滑块失效"
2673
+ },
2674
+ target: "[data-conversation-composer-overlay]",
2675
+ checks: [{
2676
+ kind: "selector",
2677
+ selector: "[data-conversation-composer-overlay]",
2678
+ optionalWhen: "trajectoryNotMounted"
2679
+ }],
2680
+ sources: [{
2681
+ path: "dsh-client-ui-trajectory/lib/client.js",
2682
+ literal: "data-conversation-composer-overlay"
2683
+ }],
2684
+ usedBy: "src/client/wallpaper.ts"
2685
+ },
2686
+ {
2687
+ id: "markdown.wideTable",
2688
+ group: "region",
2689
+ kind: "host-style",
2690
+ criticality: "label",
2691
+ since: "<=0.1.6",
2692
+ label: {
2693
+ en: "Wide markdown table clamp",
2694
+ zh: "宽表格收拢样式"
2695
+ },
2696
+ symptom: {
2697
+ en: "a wide table bleeds outside the conversation card instead of scrolling inside it",
2698
+ zh: "宽表格会溢出到对话卡片外,而不是在卡片内横向滚动"
2699
+ },
2700
+ target: ".md-table-wide",
2701
+ checks: [{
2702
+ kind: "rule",
2703
+ match: ".md-table-wide",
2704
+ hint: "markdown table class"
2705
+ }],
2706
+ sources: [{
2707
+ path: "dsh-client-ui-chat/lib/client.js",
2708
+ literal: "md-table-wide"
2709
+ }],
2710
+ usedBy: "src/client/wallpaper.ts"
2711
+ },
2712
+ {
2713
+ id: "token.bgBase",
2714
+ group: "token",
2715
+ kind: "host-token",
2716
+ criticality: "looks",
2717
+ since: "<=0.1.6",
2718
+ label: {
2719
+ en: "Main background surface",
2720
+ zh: "主背景表面"
2721
+ },
2722
+ symptom: {
2723
+ en: "the main-background slider and the right-panel card lose their surface",
2724
+ zh: "主背景透明度与右侧栏卡片失去表面色"
2725
+ },
2726
+ target: "--dsw-alias-bg-base",
2727
+ checks: [{
2728
+ kind: "computed",
2729
+ token: "--dsw-alias-bg-base"
2730
+ }],
2731
+ sources: [{
2732
+ path: "dsh-client-ui-theme/lib/client.js",
2733
+ literal: "--dsw-alias-bg-base"
2734
+ }],
2735
+ usedBy: "src/client/wallpaper.ts"
2736
+ },
2737
+ {
2738
+ id: "token.layers",
2739
+ group: "token",
2740
+ kind: "host-token",
2741
+ criticality: "looks",
2742
+ since: "<=0.1.6",
2743
+ label: {
2744
+ en: "Layered surfaces (cards)",
2745
+ zh: "层次表面(卡片)"
2746
+ },
2747
+ symptom: {
2748
+ en: "the card slider, the conversation card and the trajectory slider go blank",
2749
+ zh: "卡片滑块、对话卡片与轨迹滑块失去颜色"
2750
+ },
2751
+ target: "--dsw-alias-bg-layer-1/2/3",
2752
+ checks: [
2753
+ {
2754
+ kind: "computed",
2755
+ token: "--dsw-alias-bg-layer-1"
2756
+ },
2757
+ {
2758
+ kind: "computed",
2759
+ token: "--dsw-alias-bg-layer-2"
2760
+ },
2761
+ {
2762
+ kind: "computed",
2763
+ token: "--dsw-alias-bg-layer-3"
2764
+ }
2765
+ ],
2766
+ sources: [{
2767
+ path: "dsh-client-ui-theme/lib/client.js",
2768
+ literal: "--dsw-alias-bg-layer-1"
2769
+ }],
2770
+ usedBy: "src/client/wallpaper.ts"
2771
+ },
2772
+ {
2773
+ id: "token.menuSurface",
2774
+ group: "token",
2775
+ kind: "host-token",
2776
+ criticality: "core",
2777
+ since: "0.1.7",
2778
+ label: {
2779
+ en: "Menu material fill (0.1.7 split)",
2780
+ zh: "菜单材质填充(0.1.7 拆分)"
2781
+ },
2782
+ symptom: {
2783
+ en: "the menu panel and its sticky group headings take two different fills — the solid band across the model picker",
2784
+ zh: "菜单面板与吸顶分组标题用了两种填充 —— 模型选择器上那道白道"
2785
+ },
2786
+ target: "--dsw-menu-surface-fill",
2787
+ checks: [{
2788
+ kind: "rule",
2789
+ match: "--dsw-menu-surface-fill",
2790
+ hint: "MenuSurface material fill"
2791
+ }, {
2792
+ kind: "computed",
2793
+ token: "--dsw-menu-surface-fill",
2794
+ on: ".dab-root"
2795
+ }],
2796
+ sources: [{
2797
+ path: "dsh-client-ui-theme/lib/client.js",
2798
+ literal: "--dsw-menu-surface-fill"
2799
+ }, {
2800
+ path: "dsh-client-ui-primitives/lib/MenuSurface.module.css",
2801
+ literal: "var(--dsw-menu-surface-fill)"
2802
+ }],
2803
+ usedBy: "src/client/utils/color.ts, src/client/wallpaper.ts"
2804
+ },
2805
+ {
2806
+ id: "token.menuAlias",
2807
+ group: "token",
2808
+ kind: "host-token",
2809
+ criticality: "looks",
2810
+ since: "<=0.1.6",
2811
+ label: {
2812
+ en: "Menu overlay fill",
2813
+ zh: "菜单浮层填充"
2814
+ },
2815
+ symptom: {
2816
+ en: "dropdowns, the slash menu and popovers keep the host fill while the panel follows the plugin",
2817
+ zh: "下拉、斜杠菜单与浮层保持宿主填充,而面板跟随插件,两边不一致"
2818
+ },
2819
+ target: "--dsw-specific-menu",
2820
+ checks: [{
2821
+ kind: "rule",
2822
+ match: "--dsw-specific-menu",
2823
+ hint: "menu overlay fill"
2824
+ }, {
2825
+ kind: "computed",
2826
+ token: "--dsw-specific-menu",
2827
+ on: ".dab-root"
2828
+ }],
2829
+ sources: [{
2830
+ path: "dsh-client-ui-theme/lib/client.js",
2831
+ literal: "--dsw-specific-menu"
2832
+ }, {
2833
+ path: "dsh-client-ui-chat/lib/client.js",
2834
+ literal: "--dsw-specific-menu"
2835
+ }],
2836
+ usedBy: "src/client/utils/color.ts, src/client/wallpaper.ts"
2837
+ },
2838
+ {
2839
+ id: "token.sidebarFill",
2840
+ group: "token",
2841
+ kind: "host-token",
2842
+ criticality: "looks",
2843
+ since: "<=0.1.6",
2844
+ label: {
2845
+ en: "Sidebar fill",
2846
+ zh: "左侧栏填充"
2847
+ },
2848
+ symptom: {
2849
+ en: "the sidebar opacity slider stops tinting the sidebar",
2850
+ zh: "左侧栏透明度滑块失效"
2851
+ },
2852
+ target: "--dsw-specific-sidebar-fill",
2853
+ checks: [{
2854
+ kind: "computed",
2855
+ token: "--dsw-specific-sidebar-fill"
2856
+ }],
2857
+ sources: [{
2858
+ path: "dsh-client-ui-theme/lib/client.js",
2859
+ literal: "--dsw-specific-sidebar-fill"
2860
+ }],
2861
+ usedBy: "src/client/utils/color.ts, src/client/wallpaper.ts"
2862
+ },
2863
+ {
2864
+ id: "token.inputFill",
2865
+ group: "token",
2866
+ kind: "host-token",
2867
+ criticality: "looks",
2868
+ since: "<=0.1.6",
2869
+ label: {
2870
+ en: "Input/control surface",
2871
+ zh: "输入与控件表面"
2872
+ },
2873
+ symptom: {
2874
+ en: "the input slider stops tinting the composer and the controls",
2875
+ zh: "输入区透明度滑块失效"
2876
+ },
2877
+ target: "--dsw-specific-input-major",
2878
+ checks: [{
2879
+ kind: "computed",
2880
+ token: "--dsw-specific-input-major"
2881
+ }],
2882
+ sources: [{
2883
+ path: "dsh-client-ui-theme/lib/client.js",
2884
+ literal: "--dsw-specific-input-major"
2885
+ }],
2886
+ usedBy: "src/client/utils/color.ts, src/client/wallpaper.ts"
2887
+ },
2888
+ {
2889
+ id: "token.schemeFlag",
2890
+ group: "token",
2891
+ kind: "host-attr",
2892
+ criticality: "core",
2893
+ since: "<=0.1.6",
2894
+ label: {
2895
+ en: "Dark-scheme flag on <body>",
2896
+ zh: "<body> 上的深色标记"
2897
+ },
2898
+ symptom: {
2899
+ en: "a dark rule can no longer force the dark palette — light surfaces flash through",
2900
+ zh: "深色规则无法强制深色调色板,会闪出浅色表面"
2901
+ },
2902
+ target: "body[data-ds-dark-theme]",
2903
+ checks: [{
2904
+ kind: "rule",
2905
+ match: "data-ds-dark-theme",
2906
+ hint: "dark scheme attribute"
2907
+ }],
2908
+ sources: [{
2909
+ path: "dsh-client-ui-theme/lib/client.js",
2910
+ literal: "data-ds-dark-theme"
2911
+ }, {
2912
+ path: ".",
2913
+ literal: "data-ds-dark-theme",
2914
+ root: "frontend"
2915
+ }],
2916
+ usedBy: "src/client/wallpaper.ts"
2917
+ },
2918
+ {
2919
+ id: "token.labelCaption",
2920
+ group: "token",
2921
+ kind: "host-token",
2922
+ criticality: "label",
2923
+ since: "<=0.1.6",
2924
+ label: {
2925
+ en: "Caption label token",
2926
+ zh: "弱化文字令牌"
2927
+ },
2928
+ symptom: {
2929
+ en: "placeholder text falls back to a hard-coded grey instead of the theme's caption colour",
2930
+ zh: "占位文字退回固定灰色,不再跟随主题的弱化色"
2931
+ },
2932
+ target: "--dsw-alias-label-caption",
2933
+ checks: [{
2934
+ kind: "computed",
2935
+ token: "--dsw-alias-label-caption"
2936
+ }],
2937
+ sources: [{
2938
+ path: "dsh-client-ui-theme/lib/client.js",
2939
+ literal: "--dsw-alias-label-caption"
2940
+ }],
2941
+ usedBy: "src/client/wallpaper.ts"
2942
+ }
2943
+ ];
2944
+ /** Localized label lookup for a contract. */
2945
+ function labelOf(c, lang) {
2946
+ return lang === "zh" ? c.label.zh : c.label.en;
2947
+ }
2948
+ /** Localized symptom lookup for a contract. */
2949
+ function symptomOf(c, lang) {
2950
+ return lang === "zh" ? c.symptom.zh : c.symptom.en;
2951
+ }
2952
+ /** Groups the panel renders, in order. */
2953
+ const CONTRACT_GROUPS = [
2954
+ {
2955
+ id: "model",
2956
+ label: {
2957
+ en: "Model → rule resolution",
2958
+ zh: "模型 → 规则匹配"
2959
+ }
2960
+ },
2961
+ {
2962
+ id: "surface",
2963
+ label: {
2964
+ en: "Surfaces the plugin paints",
2965
+ zh: "插件着色的界面区域"
2966
+ }
2967
+ },
2968
+ {
2969
+ id: "token",
2970
+ label: {
2971
+ en: "Design tokens re-emitted",
2972
+ zh: "被重新发出的设计令牌"
2973
+ }
2974
+ },
2975
+ {
2976
+ id: "region",
2977
+ label: {
2978
+ en: "Conversation views",
2979
+ zh: "对话视图"
2980
+ }
2981
+ }
2982
+ ];
2983
+ /** Overall verdict of a report, for the panel's headline. */
2984
+ function verdictOf(report) {
2985
+ if (report.results.length === 0) return "unknown";
2986
+ if (report.summary.fail > 0) return report.summary.pass > 0 ? "partial" : "broken";
2987
+ return report.summary.pass > 0 ? "ok" : "unknown";
2988
+ }
2989
+ /** Markdown rendering of a report — the thing a user pastes into an issue. */
2990
+ function reportToMarkdown(report, lang) {
2991
+ const zh = lang === "zh";
2992
+ const lines = [];
2993
+ lines.push(zh ? "## dsh-background-by-model 宿主自检" : "## dsh-background-by-model host check");
2994
+ lines.push("");
2995
+ lines.push(`- ${zh ? "阶段" : "phase"}: ${report.phase}`);
2996
+ if (report.plugin.version !== "") lines.push(`- ${zh ? "插件" : "plugin"}: ${report.plugin.version} (dsh >=${report.plugin.floor})`);
2997
+ lines.push(`- dsh: ${report.host.version !== "" ? report.host.version : zh ? "未读到" : "unreadable"}${report.host.compatible === false ? zh ? "(低于插件下限)" : " (below the plugin floor)" : ""}`);
2998
+ if (report.host.node !== "") lines.push(`- node: ${report.host.node}`);
2999
+ if (report.host.root !== "") lines.push(`- host root: ${report.host.root}`);
3000
+ if (report.host.note !== "") lines.push(`- note: ${report.host.note}`);
3001
+ if (report.host.model !== void 0) {
3002
+ const m = report.host.model;
3003
+ lines.push(`- ${zh ? "模型" : "model"}: \`${m.text !== "" ? m.text : "(none)"}\` · ${m.source}${m.note !== "" ? ` · ${m.note}` : ""} · ${zh ? "规则" : "rule"} ${m.rule !== "" ? m.rule : "-"}${m.matched ? "" : zh ? "(兜底)" : " (fallback)"}`);
3004
+ }
3005
+ lines.push("");
3006
+ lines.push(`### ${zh ? "结论" : "Summary"}: ${report.summary.pass} pass / ${report.summary.fail} fail / ${report.summary.skip} n/a`);
3007
+ lines.push("");
3008
+ for (const r of report.results) {
3009
+ const mark = r.status === "pass" ? "PASS" : r.status === "fail" ? "**FAIL**" : r.status === "skip" ? "n/a" : "info";
3010
+ lines.push(`- ${mark} \`${r.id}\` — ${r.label} · \`${r.target}\``);
3011
+ if (r.status === "fail") {
3012
+ lines.push(` - ${zh ? "现象" : "symptom"}: ${r.symptom}`);
3013
+ lines.push(` - ${zh ? "原因" : "reason"}: ${r.reason ?? r.detail}`);
3014
+ lines.push(` - ${zh ? "期望位置" : "expected in"}: ${r.sources.map((s) => s.path).join(", ")}`);
3015
+ } else if (r.detail !== "") lines.push(` - ${r.detail}`);
3016
+ }
3017
+ return lines.join("\n");
3018
+ }
3019
+ //#endregion
3020
+ //#region src/client/judge.ts
3021
+ /** The real browser environment. */
3022
+ function browserEnv(ctx) {
3023
+ return {
3024
+ querySelector: (selector) => document.querySelector(selector),
3025
+ computedValue: (token, on) => {
3026
+ if (typeof document === "undefined") return "";
3027
+ const host = on ?? document.documentElement;
3028
+ return getComputedStyle(host).getPropertyValue(token).trim();
3029
+ },
3030
+ styleSheets: () => typeof document === "undefined" ? [] : Array.from(document.styleSheets),
3031
+ ctx
3032
+ };
3033
+ }
3034
+ const ANCHORS = {
3035
+ settingsClosed: "div[role=\"dialog\"][aria-modal=\"true\"][aria-labelledby]",
3036
+ chatNotMounted: "[data-chat-flow],[data-conversation-scroll]",
3037
+ trajectoryNotMounted: "[data-conversation-composer-overlay]",
3038
+ noRightPanel: "div[data-sidebar-right-panel],[data-dockkit-strip],[data-dockkit-pane]",
3039
+ cordisClosed: "[data-cordis-panel]"
3040
+ };
3041
+ function mounted(env, anchorId) {
3042
+ const sel = ANCHORS[anchorId];
3043
+ if (sel === void 0) return true;
3044
+ try {
3045
+ return env.querySelector(sel) !== null;
3046
+ } catch {
3047
+ return false;
3048
+ }
3049
+ }
3050
+ /** Walk a dotted path off a root object, tolerating a missing first hop. */
3051
+ function walkPath(root, path) {
3052
+ let cur = root;
3053
+ for (const key of path.split(".")) {
3054
+ if (cur === null || cur === void 0) return void 0;
3055
+ cur = cur[key];
3056
+ }
3057
+ return cur;
3058
+ }
3059
+ function looksObservable(v) {
3060
+ return typeof v === "object" && v !== null && typeof v.subscribe === "function" && typeof v.getSnapshot === "function";
3061
+ }
3062
+ /**
3063
+ * A stylesheet mention of a literal.
3064
+ *
3065
+ * The host's rules are the only place a renamed token still answers honestly:
3066
+ * a token can be *declared* by this plugin while the host's rule that consumed
3067
+ * it is gone. Cross-origin sheets (none on a local dsh, but the probe must not
3068
+ * throw if one ever appears) are skipped.
3069
+ */
3070
+ function ruleMentioned(env, literal) {
3071
+ for (const sheet of env.styleSheets()) {
3072
+ if (sheet.ownerNode?.dataset?.plugin !== void 0) continue;
3073
+ let rules = null;
3074
+ try {
3075
+ rules = sheet.cssRules;
3076
+ } catch {
3077
+ continue;
3078
+ }
3079
+ if (rules === null) continue;
3080
+ for (const rule of Array.from(rules)) {
3081
+ const text = rule.cssText;
3082
+ if (typeof text === "string" && text.includes(literal)) return true;
3083
+ const inner = rule.cssRules;
3084
+ if (inner === void 0 || inner === null) continue;
3085
+ for (const child of Array.from(inner)) {
3086
+ const childText = child.cssText;
3087
+ if (typeof childText === "string" && childText.includes(literal)) return true;
3088
+ }
3089
+ }
3090
+ }
3091
+ return false;
3092
+ }
3093
+ /** Whether any reachable host stylesheet could be read (false ⇒ the check is blind). */
3094
+ function stylesReadable(env) {
3095
+ for (const sheet of env.styleSheets()) try {
3096
+ if (sheet.cssRules !== null) return true;
3097
+ } catch {}
3098
+ return false;
3099
+ }
3100
+ function evaluate(env, check) {
3101
+ switch (check.kind) {
3102
+ case "service": {
3103
+ let svc;
3104
+ try {
3105
+ svc = env.ctx.get(check.id);
3106
+ } catch {
3107
+ svc = void 0;
3108
+ }
3109
+ return svc === void 0 || svc === null ? {
3110
+ status: "fail",
3111
+ name: `ctx.get('${check.id}')`,
3112
+ detail: "service not published",
3113
+ reason: `the host does not publish the "${check.id}" service`
3114
+ } : {
3115
+ status: "pass",
3116
+ name: `ctx.get('${check.id}')`,
3117
+ detail: "service present"
3118
+ };
3119
+ }
3120
+ case "path": {
3121
+ let svc;
3122
+ try {
3123
+ svc = env.ctx.get(check.service);
3124
+ } catch {
3125
+ svc = void 0;
3126
+ }
3127
+ if (svc === void 0 || svc === null) return {
3128
+ status: "skip",
3129
+ name: `${check.service}.${check.path}`,
3130
+ detail: "service not published yet"
3131
+ };
3132
+ const value = walkPath(svc, check.path);
3133
+ const name = `${check.service}.${check.path}`;
3134
+ if (check.expect === "function") return typeof value === "function" ? {
3135
+ status: "pass",
3136
+ name,
3137
+ detail: "is a function"
3138
+ } : {
3139
+ status: "fail",
3140
+ name,
3141
+ detail: `expected a function, got ${typeof value}`,
3142
+ reason: `"${name}" is not a function on this host`
3143
+ };
3144
+ if (check.expect === "observable") return looksObservable(value) ? {
3145
+ status: "pass",
3146
+ name,
3147
+ detail: "is an observable face"
3148
+ } : {
3149
+ status: "fail",
3150
+ name,
3151
+ detail: "not an observable face",
3152
+ reason: `"${name}" no longer exposes subscribe()/getSnapshot()`
3153
+ };
3154
+ return value === void 0 ? {
3155
+ status: "fail",
3156
+ name,
3157
+ detail: "undefined",
3158
+ reason: `"${name}" is gone`
3159
+ } : {
3160
+ status: "pass",
3161
+ name,
3162
+ detail: typeof value
3163
+ };
3164
+ }
3165
+ case "selector": {
3166
+ const present = env.querySelector(check.selector) !== null;
3167
+ const name = `querySelector(${JSON.stringify(check.selector)})`;
3168
+ if (present) return {
3169
+ status: "pass",
3170
+ name,
3171
+ detail: "element found"
3172
+ };
3173
+ if (check.optionalWhen !== void 0 && !mounted(env, check.optionalWhen)) return {
3174
+ status: "skip",
3175
+ name,
3176
+ detail: "not on screen right now"
3177
+ };
3178
+ return {
3179
+ status: "fail",
3180
+ name,
3181
+ detail: "no element matches",
3182
+ reason: `no element matches ${check.selector} while its surface is on screen`
3183
+ };
3184
+ }
3185
+ case "attr": {
3186
+ const present = env.querySelector(`[${check.attr}]`) !== null;
3187
+ const name = `querySelector([${check.attr}])`;
3188
+ if (present) return {
3189
+ status: "pass",
3190
+ name,
3191
+ detail: "attribute found"
3192
+ };
3193
+ if (check.optionalWhen !== void 0 && !mounted(env, check.optionalWhen)) return {
3194
+ status: "skip",
3195
+ name,
3196
+ detail: "not on screen right now"
3197
+ };
3198
+ return {
3199
+ status: "fail",
3200
+ name,
3201
+ detail: "no element carries it",
3202
+ reason: `nothing in the DOM carries ${check.attr} while its surface is on screen`
3203
+ };
3204
+ }
3205
+ case "computed": {
3206
+ const host = check.on !== void 0 ? env.querySelector(check.on) : null;
3207
+ const name = `computed(${check.token}${check.on !== void 0 ? ` on ${check.on}` : ""})`;
3208
+ if (check.on !== void 0 && host === null) return {
3209
+ status: "skip",
3210
+ name,
3211
+ detail: `${check.on} not mounted`
3212
+ };
3213
+ const value = env.computedValue(check.token, host);
3214
+ return value !== "" ? {
3215
+ status: "pass",
3216
+ name,
3217
+ detail: value
3218
+ } : {
3219
+ status: "fail",
3220
+ name,
3221
+ detail: "resolves to nothing",
3222
+ reason: `${check.token} resolves to an empty value`
3223
+ };
3224
+ }
3225
+ case "rule": {
3226
+ const name = `stylesheet includes ${JSON.stringify(check.match)}`;
3227
+ if (!stylesReadable(env)) return {
3228
+ status: "skip",
3229
+ name,
3230
+ detail: "no stylesheet is readable"
3231
+ };
3232
+ return ruleMentioned(env, check.match) ? {
3233
+ status: "pass",
3234
+ name,
3235
+ detail: "declared by a host rule"
3236
+ } : {
3237
+ status: "fail",
3238
+ name,
3239
+ detail: "no rule mentions it",
3240
+ reason: `no stylesheet declares ${check.match}`
3241
+ };
3242
+ }
3243
+ default: return {
3244
+ status: "fail",
3245
+ name: "unknown check",
3246
+ detail: String(check)
3247
+ };
3248
+ }
3249
+ }
3250
+ /** A contract with no checks is documentation only (a legacy alias we still accept). */
3251
+ function noChecks(c, lang) {
3252
+ return {
3253
+ id: c.id,
3254
+ label: labelOf(c, lang),
3255
+ symptom: symptomOf(c, lang),
3256
+ status: "info",
3257
+ detail: lang === "zh" ? "无需运行时检查 —— 仅记录兼容用法" : "no runtime check — accepted for compatibility",
3258
+ target: c.target,
3259
+ sources: c.sources,
3260
+ usedBy: c.usedBy,
3261
+ checks: []
3262
+ };
3263
+ }
3264
+ /**
3265
+ * Run every contract against a probe environment.
3266
+ *
3267
+ * Only the FIRST failing check of a contract is reported: the later ones usually
3268
+ * depend on it (no service ⇒ no methods on it), so listing them all would turn
3269
+ * one rename into five red lines.
3270
+ */
3271
+ function probeContracts(env, lang) {
3272
+ return HOST_CONTRACTS.map((c) => {
3273
+ const result = {
3274
+ id: c.id,
3275
+ label: labelOf(c, lang),
3276
+ symptom: symptomOf(c, lang),
3277
+ status: "pass",
3278
+ detail: "",
3279
+ target: c.target,
3280
+ sources: c.sources,
3281
+ usedBy: c.usedBy,
3282
+ checks: []
3283
+ };
3284
+ if (c.checks.length === 0) return noChecks(c, lang);
3285
+ const outcomes = [];
3286
+ let firstFail;
3287
+ for (const check of c.checks) {
3288
+ let outcome;
3289
+ try {
3290
+ outcome = evaluate(env, check);
3291
+ } catch (e) {
3292
+ outcome = {
3293
+ status: "fail",
3294
+ name: check.kind,
3295
+ detail: String(e),
3296
+ reason: `${check.kind} check threw`
3297
+ };
3298
+ }
3299
+ outcomes.push(outcome);
3300
+ if (outcome.status === "fail" && firstFail === void 0) firstFail = outcome;
3301
+ }
3302
+ result.checks = outcomes.map((o) => `${o.status} · ${o.name} — ${o.detail}`);
3303
+ if (firstFail !== void 0) {
3304
+ result.status = "fail";
3305
+ result.detail = `${firstFail.name} — ${firstFail.detail}`;
3306
+ result.reason = firstFail.reason;
3307
+ return result;
3308
+ }
3309
+ const ran = outcomes.filter((o) => o.status !== "skip");
3310
+ if (ran.length === 0) {
3311
+ result.status = "skip";
3312
+ result.detail = outcomes.map((o) => `${o.name} — ${o.detail}`).join("; ");
3313
+ return result;
3314
+ }
3315
+ result.status = ran.some((o) => o.status === "info") && ran.every((o) => o.status === "info") ? "info" : "pass";
3316
+ const last = ran[ran.length - 1];
3317
+ result.detail = `${last.name} — ${last.detail}`;
3318
+ return result;
3319
+ });
3320
+ }
3321
+ /** Assemble the panel's report around a set of probe results. */
3322
+ function buildClientReport(input, host) {
3323
+ const results = probeContracts(browserEnv(input.ctx), input.lang);
3324
+ const pass = results.filter((r) => r.status === "pass").length;
3325
+ const fail = results.filter((r) => r.status === "fail").length;
3326
+ const skip = results.filter((r) => r.status === "skip" || r.status === "info").length;
3327
+ return {
3328
+ phase: "client",
3329
+ at: (/* @__PURE__ */ new Date()).toISOString(),
3330
+ plugin: {
3331
+ version: input.pluginVersion,
3332
+ floor: input.floor
3333
+ },
3334
+ host: {
3335
+ version: host.version,
3336
+ compatible: host.compatible,
3337
+ node: "",
3338
+ root: "",
3339
+ note: host.note,
3340
+ model: input.model
3341
+ },
3342
+ results,
3343
+ summary: {
3344
+ pass,
3345
+ fail,
3346
+ skip,
3347
+ total: results.length
3348
+ }
3349
+ };
3350
+ }
3351
+ //#endregion
3352
+ //#region src/client/build-info.ts
3353
+ /** This plugin's version at build time ('0.0.0' when built unbundled). */
3354
+ const PLUGIN_VERSION = "0.4.2";
3355
+ /** The dsh floor this build declares, straight from `engines.dsh`. */
3356
+ const DSH_FLOOR = "0.1.7-rc.2";
2101
3357
  const UI_CSS = `
2102
3358
  /* The section is rendered INLINE inside the host settings dialog's content
2103
3359
  * column: the host provides the modal chrome (backdrop, centering, closing).
@@ -2281,6 +3537,48 @@ window.__ModuleLoader__.load({
2281
3537
  .dab-rule-section-title{font-size:12px;font-weight:600;margin-bottom:9px;color:var(--dsw-alias-label-secondary,var(--dsw-alias-label-tertiary))}
2282
3538
  @container (max-width:760px){.dab-rule-cols{grid-template-columns:1fr}}
2283
3539
 
3540
+ /* ── host self-check ─────────────────────────────────────────────────────── */
3541
+ .dab-verdict{display:flex;flex-direction:column;gap:14px;position:relative;overflow:hidden}
3542
+ .dab-verdict::after{content:'';position:absolute;inset:0;pointer-events:none;opacity:.5;background:linear-gradient(120deg,transparent 55%,color-mix(in srgb,var(--dab-verdict-tone,transparent) 26%,transparent))}
3543
+ .dab-verdict-ok{--dab-verdict-tone:var(--dsw-alias-state-success-primary,var(--dsw-alias-brand-primary));border-color:color-mix(in srgb,var(--dsw-alias-state-success-primary,var(--dsw-alias-brand-primary)) 38%,var(--dsw-alias-border-l2))}
3544
+ .dab-verdict-partial,.dab-verdict-broken{--dab-verdict-tone:var(--dsw-alias-state-error-primary);border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary) 40%,var(--dsw-alias-border-l2))}
3545
+ .dab-verdict-unknown{--dab-verdict-tone:var(--dsw-alias-label-tertiary)}
3546
+ .dab-verdict-head{display:flex;align-items:center;gap:13px;flex-wrap:wrap}
3547
+ .dab-verdict-ico{width:34px;height:34px;flex:none;border-radius:11px;display:grid;place-items:center;color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-bg-layer-2)}
3548
+ .dab-verdict-ico-ok{color:var(--dsw-alias-state-success-primary,var(--dsw-alias-brand-primary));background:color-mix(in srgb,var(--dsw-alias-state-success-primary,var(--dsw-alias-brand-primary)) 14%,transparent)}
3549
+ .dab-verdict-ico-partial,.dab-verdict-ico-broken{color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 14%,transparent)}
3550
+ .dab-verdict-text{flex:1;min-width:170px}
3551
+ .dab-verdict-title{font-size:14.5px;font-weight:650}
3552
+ .dab-verdict-sub{margin-top:4px;font-size:12px;color:var(--dsw-alias-label-tertiary)}
3553
+ .dab-verdict-actions{display:flex;gap:8px;flex-wrap:wrap;margin-left:auto}
3554
+ .dab-facts{display:flex;flex-wrap:wrap;gap:8px;position:relative;z-index:1}
3555
+ .dab-fact{display:inline-flex;align-items:center;gap:7px;height:27px;padding:0 11px;border-radius:99px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);max-width:100%}
3556
+ .dab-fact.is-bad{border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary) 45%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 10%,transparent)}
3557
+ .dab-fact-k{font-size:10.5px;font-weight:650;letter-spacing:.06em;text-transform:uppercase;color:var(--dsw-alias-label-tertiary);white-space:nowrap}
3558
+ .dab-fact-v{font-family:var(--dab-mono);font-size:11.5px;color:var(--dsw-alias-label-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:32ch}
3559
+ .dab-check-group-title{display:flex;align-items:center;gap:9px;font-size:12.5px;font-weight:650;margin-bottom:11px}
3560
+ .dab-check-group-count{margin-left:auto;font-family:var(--dab-mono);font-size:11px;font-weight:500;color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-bg-layer-2);border-radius:99px;padding:3px 9px}
3561
+ .dab-checks{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:2px}
3562
+ .dab-check{display:flex;gap:10px;padding:9px 10px;border-radius:11px;transition:background .2s ease}
3563
+ .dab-check:hover{background:var(--dsw-alias-bg-layer-2)}
3564
+ .dab-check-ico{width:19px;height:19px;flex:none;margin-top:1px;border-radius:50%;display:grid;place-items:center;color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-bg-layer-2)}
3565
+ .dab-check-ico-pass{color:var(--dsw-alias-state-success-primary,var(--dsw-alias-brand-primary));background:color-mix(in srgb,var(--dsw-alias-state-success-primary,var(--dsw-alias-brand-primary)) 15%,transparent)}
3566
+ .dab-check-ico-fail{color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 15%,transparent)}
3567
+ .dab-check-dot{width:5px;height:5px;border-radius:50%;background:currentColor;opacity:.65}
3568
+ .dab-check-body{min-width:0;flex:1;display:flex;flex-direction:column;gap:3px}
3569
+ .dab-check-head{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap}
3570
+ .dab-check-label{font-size:12.5px;font-weight:600}
3571
+ .dab-check-status{font-size:10px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;padding:1px 7px;border-radius:99px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-tertiary)}
3572
+ .dab-check-status-pass{color:var(--dsw-alias-state-success-primary,var(--dsw-alias-brand-primary));background:color-mix(in srgb,var(--dsw-alias-state-success-primary,var(--dsw-alias-brand-primary)) 14%,transparent)}
3573
+ .dab-check-status-fail{color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 14%,transparent)}
3574
+ .dab-check-target{font-family:var(--dab-mono);font-size:10.5px;color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-bg-layer-2);border-radius:6px;padding:1px 6px;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
3575
+ .dab-check-detail{font-family:var(--dab-mono);font-size:11px;color:var(--dsw-alias-label-tertiary);line-height:1.5;overflow-wrap:anywhere}
3576
+ .dab-check-reason{display:flex;flex-direction:column;gap:5px;font-size:12px;line-height:1.55;color:var(--dsw-alias-label-secondary,var(--dsw-alias-label-tertiary))}
3577
+ .dab-check-symptom{color:var(--dsw-alias-state-error-primary)}
3578
+ .dab-check-where{display:flex;gap:7px;align-items:baseline;flex-wrap:wrap;font-size:11px;color:var(--dsw-alias-label-tertiary)}
3579
+ .dab-check-where code{font-family:var(--dab-mono);font-size:10.5px;overflow-wrap:anywhere}
3580
+ .dab-check-note{margin:12px 0 0;font-size:11.5px;line-height:1.55;color:var(--dsw-alias-label-tertiary);position:relative;z-index:1}
3581
+
2284
3582
  /* ── keyframes ───────────────────────────────────────────────────────────── */
2285
3583
  @keyframes dab-fade-in{from{opacity:0}to{opacity:1}}
2286
3584
  @keyframes dab-page-in{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}}
@@ -2302,6 +3600,8 @@ window.__ModuleLoader__.load({
2302
3600
  .dab-nav-item{flex:none;padding:0 10px}
2303
3601
  .dab-nav-item.is-active{background:var(--dsw-alias-bg-layer-2)}
2304
3602
  .dab-types{grid-template-columns:1fr}
3603
+ .dab-verdict-actions{margin-left:0;width:100%}
3604
+ .dab-verdict-actions .dab-btn{flex:1}
2305
3605
  }
2306
3606
  @container (min-width:621px){
2307
3607
  .dab-shell{grid-template-columns:158px minmax(0,1fr);gap:26px}
@@ -2434,6 +3734,11 @@ window.__ModuleLoader__.load({
2434
3734
  className,
2435
3735
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M7.2 1.8l1.2 3.1 3.1 1.2-3.1 1.2-1.2 3.1-1.2-3.1L2.9 6.1 6 4.9l1.2-3.1z" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M12 10.5l.5 1.2 1.2.5-1.2.5-.5 1.2-.5-1.2-1.2-.5 1.2-.5.5-1.2z" })]
2436
3736
  });
3737
+ const RefreshIcon = ({ size, className }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Glyph, {
3738
+ size,
3739
+ className,
3740
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M13.2 8A5.2 5.2 0 1 1 11.5 4.2" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M13.4 1.8v2.9h-2.9" })]
3741
+ });
2437
3742
  const DownloadIcon = ({ size, className }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Glyph, {
2438
3743
  size,
2439
3744
  className,
@@ -2494,6 +3799,17 @@ window.__ModuleLoader__.load({
2494
3799
  className,
2495
3800
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M8 2.6l5.4 9.8H2.6L8 2.6z" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M8 6.8v2.4M8 11.4v.01" })]
2496
3801
  });
3802
+ const CopyIcon = ({ size, className }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Glyph, {
3803
+ size,
3804
+ className,
3805
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
3806
+ x: "5.4",
3807
+ y: "5.4",
3808
+ width: "7.6",
3809
+ height: "7.6",
3810
+ rx: "1.6"
3811
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M10.6 5.4V4.2a1.6 1.6 0 0 0-1.6-1.6H4.2a1.6 1.6 0 0 0-1.6 1.6v4.8a1.6 1.6 0 0 0 1.6 1.6h1.2" })]
3812
+ });
2497
3813
  const CanvasIcon = ({ size, className }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Glyph, {
2498
3814
  size,
2499
3815
  className,
@@ -4311,6 +5627,290 @@ window.__ModuleLoader__.load({
4311
5627
  ] });
4312
5628
  }
4313
5629
  //#endregion
5630
+ //#region src/client/components/pages/HostPage.tsx
5631
+ /**
5632
+ * The "Host check" page.
5633
+ *
5634
+ * Two answers side by side, because a plugin that styles someone else's interface
5635
+ * fails in two different ways:
5636
+ *
5637
+ * this build, live the probe in ../../judge.ts runs against the DOM, the
5638
+ * host stylesheets and the Cordis services — what the
5639
+ * plugin can actually see right now.
5640
+ * the installed files the node half's scan (src/host-scan.ts) reports whether
5641
+ * the packages on disk still contain every literal the
5642
+ * plugin depends on, and names the host's own version.
5643
+ *
5644
+ * The page exists so a failure stops being "背景不动了": it names the hop, the
5645
+ * literal, the file that should define it and the file of this plugin that reads
5646
+ * it — and copies all of that as one markdown block for an issue.
5647
+ */
5648
+ /** Contract id → its group, so the panel's order is the table's order. */
5649
+ const GROUP_OF = new Map(HOST_CONTRACTS.map((c) => [c.id, c.group]));
5650
+ function HostPage({ p, notify }) {
5651
+ const { t, lang, readModelFacts } = p;
5652
+ const [client, setClient] = (0, react.useState)(null);
5653
+ const [scanned, setScanned] = (0, react.useState)(null);
5654
+ const [busy, setBusy] = (0, react.useState)(false);
5655
+ const run = (0, react.useCallback)(async () => {
5656
+ setBusy(true);
5657
+ try {
5658
+ setClient(buildClientReport({
5659
+ ctx: p.ctx,
5660
+ lang,
5661
+ pluginVersion: PLUGIN_VERSION,
5662
+ floor: DSH_FLOOR,
5663
+ model: readModelFacts?.()
5664
+ }, {
5665
+ version: "",
5666
+ compatible: null,
5667
+ note: ""
5668
+ }));
5669
+ setScanned(await readHostScan(lang));
5670
+ } finally {
5671
+ setBusy(false);
5672
+ }
5673
+ }, [
5674
+ p,
5675
+ lang,
5676
+ readModelFacts
5677
+ ]);
5678
+ (0, react.useEffect)(() => {
5679
+ run();
5680
+ }, [run]);
5681
+ const hostVersion = scanned?.host.version ?? "";
5682
+ const compatible = scanned?.host.compatible ?? null;
5683
+ const verdict = client === null ? "unknown" : verdictOf(client);
5684
+ const diskVerdict = scanned === null ? "unknown" : verdictOf(scanned);
5685
+ const rows = client?.results ?? [];
5686
+ const copyReport = async () => {
5687
+ const parts = [];
5688
+ if (client !== null) parts.push(reportToMarkdown(client, lang));
5689
+ if (scanned !== null) parts.push(reportToMarkdown(scanned, lang));
5690
+ try {
5691
+ await navigator.clipboard.writeText(parts.join("\n\n"));
5692
+ notify(t("hostCopied"));
5693
+ } catch {
5694
+ notify(t("hostCopyFail"), false);
5695
+ }
5696
+ };
5697
+ const statusWord = (s) => s === "pass" ? t("hostPass") : s === "fail" ? t("hostFail") : s === "skip" ? t("hostNa") : t("hostInfo");
5698
+ const row = (r) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
5699
+ className: `dab-check dab-check-${r.status}`,
5700
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5701
+ className: `dab-check-ico dab-check-ico-${r.status}`,
5702
+ "aria-hidden": "true",
5703
+ children: r.status === "pass" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckIcon, { size: 12 }) : r.status === "fail" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AlertIcon, { size: 12 }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dab-check-dot" })
5704
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5705
+ className: "dab-check-body",
5706
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5707
+ className: "dab-check-head",
5708
+ children: [
5709
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5710
+ className: "dab-check-label",
5711
+ children: r.label
5712
+ }),
5713
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5714
+ className: `dab-check-status dab-check-status-${r.status}`,
5715
+ children: statusWord(r.status)
5716
+ }),
5717
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
5718
+ className: "dab-check-target",
5719
+ children: r.target
5720
+ })
5721
+ ]
5722
+ }), r.status === "fail" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5723
+ className: "dab-check-reason",
5724
+ children: [
5725
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: r.reason ?? r.detail }),
5726
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5727
+ className: "dab-check-symptom",
5728
+ children: r.symptom
5729
+ }),
5730
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5731
+ className: "dab-check-where",
5732
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("hostExpectedIn") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: r.sources.map((s) => s.path).join(" · ") })]
5733
+ }),
5734
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5735
+ className: "dab-check-where",
5736
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("hostUsedBy") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: r.usedBy })]
5737
+ })
5738
+ ]
5739
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5740
+ className: "dab-check-detail",
5741
+ title: r.checks.join("\n"),
5742
+ children: r.detail
5743
+ })]
5744
+ })]
5745
+ }, r.id);
5746
+ let rise = 1;
5747
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
5748
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
5749
+ className: "dab-head dab-rise",
5750
+ style: { "--d": 0 },
5751
+ children: [
5752
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5753
+ className: "dab-overline",
5754
+ children: "Host check"
5755
+ }),
5756
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
5757
+ className: "dab-h1",
5758
+ children: t("pageHostCheck")
5759
+ }),
5760
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5761
+ className: "dab-desc",
5762
+ children: t("descHostCheck")
5763
+ })
5764
+ ]
5765
+ }),
5766
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
5767
+ className: `dab-card dab-verdict dab-verdict-${verdict} dab-rise`,
5768
+ style: { "--d": rise++ },
5769
+ children: [
5770
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5771
+ className: "dab-verdict-head",
5772
+ children: [
5773
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5774
+ className: `dab-verdict-ico dab-verdict-ico-${verdict}`,
5775
+ "aria-hidden": "true",
5776
+ children: verdict === "ok" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckIcon, { size: 16 }) : verdict === "unknown" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LayersIcon, { size: 16 }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AlertIcon, { size: 16 })
5777
+ }),
5778
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5779
+ className: "dab-verdict-text",
5780
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5781
+ className: "dab-verdict-title",
5782
+ children: verdict === "ok" ? t("hostAllOk") : verdict === "unknown" ? t("hostUnknown") : verdict === "broken" ? t("hostBroken") : t("hostPartial")
5783
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5784
+ className: "dab-verdict-sub",
5785
+ children: t("hostSummary").replace("{pass}", String(client?.summary.pass ?? 0)).replace("{fail}", String(client?.summary.fail ?? 0)).replace("{skip}", String(client?.summary.skip ?? 0))
5786
+ })]
5787
+ }),
5788
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5789
+ className: "dab-verdict-actions",
5790
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
5791
+ type: "button",
5792
+ className: "dab-btn",
5793
+ disabled: busy,
5794
+ onClick: () => void run(),
5795
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RefreshIcon, { size: 13 }), busy ? t("hostRunning") : t("hostRerun")]
5796
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
5797
+ type: "button",
5798
+ className: "dab-btn dab-btn-primary",
5799
+ onClick: () => void copyReport(),
5800
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CopyIcon, { size: 13 }), t("hostCopy")]
5801
+ })]
5802
+ })
5803
+ ]
5804
+ }),
5805
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5806
+ className: "dab-facts",
5807
+ children: [
5808
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Fact, {
5809
+ label: "dsh",
5810
+ value: hostVersion !== "" ? hostVersion : t("hostUnreadable"),
5811
+ bad: compatible === false
5812
+ }),
5813
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Fact, {
5814
+ label: t("hostPluginFloor"),
5815
+ value: `>=${DSH_FLOOR}`
5816
+ }),
5817
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Fact, {
5818
+ label: t("hostPlugin"),
5819
+ value: PLUGIN_VERSION
5820
+ }),
5821
+ scanned !== null && scanned.host.node !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Fact, {
5822
+ label: "node",
5823
+ value: scanned.host.node
5824
+ }) : null,
5825
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Fact, {
5826
+ label: t("hostDiskScan"),
5827
+ value: diskVerdict === "ok" ? `${scanned?.summary.pass ?? 0}/${scanned?.summary.total ?? 0}` : diskVerdict === "unknown" ? t("hostNa") : `${scanned?.summary.fail ?? 0} ${t("hostFail")}`,
5828
+ bad: diskVerdict === "broken" || diskVerdict === "partial"
5829
+ }),
5830
+ client?.host.model !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Fact, {
5831
+ label: t("hostModel"),
5832
+ value: client.host.model.text !== "" ? client.host.model.text : t("hostModelNone")
5833
+ }) : null
5834
+ ]
5835
+ }),
5836
+ compatible === false ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5837
+ className: "dab-check-note",
5838
+ children: t("hostBelowFloor")
5839
+ }) : null,
5840
+ scanned !== null && scanned.host.note !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5841
+ className: "dab-check-note",
5842
+ children: scanned.host.note
5843
+ }) : null,
5844
+ diskFailures(scanned) > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5845
+ className: "dab-check-note",
5846
+ children: t("hostDiskFailures").replace("{n}", String(diskFailures(scanned)))
5847
+ }) : null
5848
+ ]
5849
+ }),
5850
+ client === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
5851
+ className: "dab-card dab-rise",
5852
+ style: { "--d": rise },
5853
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5854
+ className: "dab-check-detail",
5855
+ children: t("hostRunning")
5856
+ })
5857
+ }) : null,
5858
+ CONTRACT_GROUPS.map((group) => {
5859
+ const groupRows = rows.filter((r) => GROUP_OF.get(r.id) === group.id);
5860
+ if (groupRows.length === 0) return null;
5861
+ const ok = groupRows.filter((r) => r.status === "pass").length;
5862
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
5863
+ className: "dab-card dab-rise",
5864
+ style: { "--d": rise++ },
5865
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5866
+ className: "dab-check-group-title",
5867
+ children: [lang === "zh" ? group.label.zh : group.label.en, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
5868
+ className: "dab-check-group-count",
5869
+ children: [
5870
+ ok,
5871
+ "/",
5872
+ groupRows.length
5873
+ ]
5874
+ })]
5875
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
5876
+ className: "dab-checks",
5877
+ children: groupRows.map(row)
5878
+ })]
5879
+ }, group.id);
5880
+ }),
5881
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5882
+ className: "dab-hint dab-rise",
5883
+ style: { "--d": rise++ },
5884
+ children: t("hostHint")
5885
+ }),
5886
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("footer", {
5887
+ className: "dab-footer dab-rise",
5888
+ style: { "--d": rise },
5889
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5890
+ className: "dab-footer-mono",
5891
+ children: "dsh-background-by-model"
5892
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("footerTag") })]
5893
+ })
5894
+ ] });
5895
+ }
5896
+ /** How many contracts the on-disk scan could not find (0 when it did not answer). */
5897
+ function diskFailures(report) {
5898
+ return report === null ? 0 : report.summary.fail;
5899
+ }
5900
+ function Fact({ label, value, bad = false }) {
5901
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5902
+ className: `dab-fact${bad ? " is-bad" : ""}`,
5903
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5904
+ className: "dab-fact-k",
5905
+ children: label
5906
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
5907
+ className: "dab-fact-v",
5908
+ title: value,
5909
+ children: value
5910
+ })]
5911
+ });
5912
+ }
5913
+ //#endregion
4314
5914
  //#region src/client/components/ThemeSection.tsx
4315
5915
  /**
4316
5916
  * dsh-background-by-model — settings section shell.
@@ -4358,6 +5958,14 @@ window.__ModuleLoader__.load({
4358
5958
  p: props,
4359
5959
  notify
4360
5960
  })
5961
+ },
5962
+ {
5963
+ label: t("pageHostCheck"),
5964
+ Icon: GearIcon,
5965
+ node: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HostPage, {
5966
+ p: props,
5967
+ notify
5968
+ })
4361
5969
  }
4362
5970
  ];
4363
5971
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ErrorBoundary, {
@@ -4620,11 +6228,33 @@ window.__ModuleLoader__.load({
4620
6228
  if (id === activeRuleId) applyActive();
4621
6229
  else sync();
4622
6230
  };
6231
+ const activeLang = () => {
6232
+ try {
6233
+ const snapshot = ctx.locale.getSnapshot();
6234
+ return typeof snapshot?.active === "string" && snapshot.active.toLowerCase().startsWith("zh") ? "zh" : "en";
6235
+ } catch {
6236
+ return "en";
6237
+ }
6238
+ };
6239
+ /** The model-resolution facts the host self-check reports, read on demand. */
6240
+ const readModelFacts = () => {
6241
+ const rule = activeRuleId === null ? null : ruleById(activeRuleId);
6242
+ return {
6243
+ text: modelText,
6244
+ source: modelSource,
6245
+ note: modelNote,
6246
+ rule: rule === null ? "" : rule.match !== "" ? rule.match : rule.id,
6247
+ matched: activeMatched
6248
+ };
6249
+ };
4623
6250
  const sectionInject = (actions) => {
4624
6251
  bound = actions;
4625
6252
  sync();
4626
6253
  return {
4627
6254
  t: ctx.locale.bind(NS),
6255
+ ctx,
6256
+ lang: activeLang(),
6257
+ readModelFacts,
4628
6258
  imageOf: (slot) => displayImageOf(slot),
4629
6259
  addRule: () => {
4630
6260
  const rule = newRule(nextRuleId(), nextSlot());
@@ -4876,6 +6506,26 @@ window.__ModuleLoader__.load({
4876
6506
  const onPageHide = () => flushSave();
4877
6507
  window.addEventListener("pagehide", onPageHide);
4878
6508
  ctx.effect(() => () => window.removeEventListener("pagehide", onPageHide), "dsh-background-by-model: pagehide flush");
6509
+ const hostCheckTimer = window.setTimeout(() => {
6510
+ try {
6511
+ const report = buildClientReport({
6512
+ ctx,
6513
+ lang: activeLang(),
6514
+ pluginVersion: PLUGIN_VERSION,
6515
+ floor: DSH_FLOOR,
6516
+ model: readModelFacts()
6517
+ }, {
6518
+ version: "",
6519
+ compatible: null,
6520
+ note: ""
6521
+ });
6522
+ const failed = report.results.filter((r) => r.status === "fail");
6523
+ if (failed.length === 0) return;
6524
+ console.warn(`dsh-background-by-model: ${failed.length} of ${report.results.length} host contract(s) are broken on this dsh build. Open Settings → ${ctx.locale.bind(NS)("pageHostCheck")} for the full report.`);
6525
+ for (const r of failed) console.warn(`dsh-background-by-model: [${r.id}] ${r.reason ?? r.detail} — read by ${r.usedBy}; expected in ${r.sources.map((s) => s.path).join(", ")}`);
6526
+ } catch {}
6527
+ }, 2500);
6528
+ ctx.effect(() => () => window.clearTimeout(hostCheckTimer), "dsh-background-by-model: host check");
4879
6529
  }
4880
6530
  /** Color of the currently active rule, or null when it uses the system theme. */
4881
6531
  function activeRuleColor() {