@theseus-whale/dsh-continuity 0.0.0-stage → 4.12.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.
Files changed (258) hide show
  1. package/INSTALL.md +25 -0
  2. package/PANEL.md +92 -0
  3. package/README.md +286 -2
  4. package/RELEASE-NOTES.md +732 -0
  5. package/bin/bc-acceptance-full.mjs +91 -0
  6. package/bin/bc-acceptance-tight.mjs +66 -0
  7. package/bin/bc-acceptance.mjs +22 -0
  8. package/bin/bc-add-keywords.mjs +21 -0
  9. package/bin/bc-demo.mjs +47 -0
  10. package/bin/bc-expand-hint.mjs +179 -0
  11. package/bin/bc-ingest-index.mjs +35 -0
  12. package/bin/bc-ingest.mjs +31 -0
  13. package/bin/bc-label-demo.mjs +18 -0
  14. package/bin/bc-relabel.mjs +150 -0
  15. package/bin/bc-sessionize.mjs +59 -0
  16. package/bin/coref_pipeline.py +47 -0
  17. package/bin/coref_service.py +57 -0
  18. package/bin/debug_ckip.py +16 -0
  19. package/bin/debug_ner_zh.py +18 -0
  20. package/bin/doctor.mjs +275 -0
  21. package/bin/en_ner_error_audit.py +33 -0
  22. package/bin/eval-label-rules.mjs +342 -0
  23. package/bin/export-turns.py +68 -0
  24. package/bin/extract-user-paragraph.mjs +14 -0
  25. package/bin/extraction-composite-smoke.mjs +28 -0
  26. package/bin/extraction-latency.mjs +58 -0
  27. package/bin/extraction-user-smoke.mjs +28 -0
  28. package/bin/extraction_service.py +164 -0
  29. package/bin/facet-active-experiment.mjs +110 -0
  30. package/bin/facet-covid.mjs +8 -0
  31. package/bin/facet-curve.mjs +27 -0
  32. package/bin/facet-embedding-train.mjs +20 -0
  33. package/bin/facet-embedding-validate.mjs +83 -0
  34. package/bin/facet-eval-roleplay.mjs +42 -0
  35. package/bin/facet-eval.mjs +38 -0
  36. package/bin/facet-fold.mjs +31 -0
  37. package/bin/facet-load.mjs +16 -0
  38. package/bin/facet-mlp-scan.mjs +45 -0
  39. package/bin/facet-mvp.mjs +34 -0
  40. package/bin/facet-online-experiments.mjs +78 -0
  41. package/bin/facet-online-race.mjs +99 -0
  42. package/bin/facet-online-validate.mjs +51 -0
  43. package/bin/facet-panel.mjs +22 -0
  44. package/bin/facet-passage.mjs +41 -0
  45. package/bin/facet-random-validate.mjs +48 -0
  46. package/bin/facet-roleplay.mjs +33 -0
  47. package/bin/facet-scan.mjs +56 -0
  48. package/bin/facet-segment.mjs +34 -0
  49. package/bin/facet-train.mjs +19 -0
  50. package/bin/facet-validate.mjs +103 -0
  51. package/bin/facet-zeroshot.mjs +14 -0
  52. package/bin/install-templates.mjs +72 -0
  53. package/bin/memory-bench.mjs +53 -0
  54. package/bin/memory-build-summary.mjs +43 -0
  55. package/bin/memory-build-vector.mjs +31 -0
  56. package/bin/memory-build.mjs +41 -0
  57. package/bin/memory-calibration.mjs +70 -0
  58. package/bin/memory-closed-loop.mjs +119 -0
  59. package/bin/memory-dry-run.mjs +70 -0
  60. package/bin/memory-eval.mjs +104 -0
  61. package/bin/memory-event-alias-test.mjs +89 -0
  62. package/bin/memory-extraction-bench.mjs +58 -0
  63. package/bin/memory-load-gate-replay.mjs +137 -0
  64. package/bin/memory-loop-test.mjs +90 -0
  65. package/bin/memory-pre-turn-harness.mjs +63 -0
  66. package/bin/memory-query.mjs +39 -0
  67. package/bin/memory-replay-hints.mjs +84 -0
  68. package/bin/memory-scale.mjs +58 -0
  69. package/bin/memory-scenario-test.mjs +146 -0
  70. package/bin/memory-user-loop.mjs +95 -0
  71. package/bin/ner_bench_tiny_zh.py +59 -0
  72. package/bin/ner_bench_zh.py +60 -0
  73. package/bin/ner_conll_en.py +139 -0
  74. package/bin/ner_direct_en.py +123 -0
  75. package/bin/ner_direct_zh.py +100 -0
  76. package/bin/ner_pipeline.py +208 -0
  77. package/bin/noise-selfcheck.mjs +50 -0
  78. package/bin/pack-integrity.mjs +47 -0
  79. package/bin/panel-check.mjs +57 -0
  80. package/bin/plugin-integrity.mjs +74 -0
  81. package/bin/preview-live.mjs +13 -0
  82. package/bin/public-install-check.mjs +222 -0
  83. package/bin/relabel-apply.mjs +272 -0
  84. package/bin/relabel-dry-run.mjs +381 -0
  85. package/bin/relik_pipeline.py +39 -0
  86. package/bin/relik_service.py +52 -0
  87. package/bin/srl_pipeline.py +55 -0
  88. package/bin/srl_service.py +68 -0
  89. package/bin/summary-index.mjs +151 -0
  90. package/bin/summary-query.mjs +79 -0
  91. package/bin/vec-check.mjs +27 -0
  92. package/bin/zh_ner_error_audit.py +36 -0
  93. package/bin/zh_ner_slot_eval.py +38 -0
  94. package/client.js +760 -0
  95. package/cordis.patch.yml +378 -0
  96. package/docs/API-NOTES.md +1743 -0
  97. package/docs/ARCHITECTURE.md +355 -0
  98. package/docs/CACHE.md +253 -0
  99. package/docs/CONDUCT.md +30 -0
  100. package/docs/CREATOR-HANDOFF.md +102 -0
  101. package/docs/EXPERIMENT.md +136 -0
  102. package/docs/FACET-MVP-REPORT.md +473 -0
  103. package/docs/GUIDE-0.4.66.md +195 -0
  104. package/docs/GUIDE.md +127 -0
  105. package/docs/MECHANISM.md +243 -0
  106. package/docs/NEW-INSTANCE-HANDOFF.md +219 -0
  107. package/docs/README.md +37 -0
  108. package/docs/SESSION-LAYOUT.md +151 -0
  109. package/docs/history-map.md +249 -0
  110. package/docs/relabel-migration-wiring.md +366 -0
  111. package/guides/00-READ-FIRST.md +206 -0
  112. package/guides/01-INSTALL.md +207 -0
  113. package/guides/02-TABLES.md +225 -0
  114. package/guides/03-AUDIT.md +201 -0
  115. package/guides/04-REBUILD.md +306 -0
  116. package/guides/05-FIRST-RUN.md +191 -0
  117. package/guides/06-EMBEDDING.md +146 -0
  118. package/guides/07-PTC-AND-ASSETS.md +221 -0
  119. package/icon.svg +7 -0
  120. package/locale/en.json +6 -0
  121. package/locale/zh.json +6 -0
  122. package/models/README.md +13 -0
  123. package/models/Xenova/bge-small-zh-v1.5/config.json +31 -0
  124. package/models/Xenova/bge-small-zh-v1.5/onnx/model_quantized.onnx +0 -0
  125. package/models/Xenova/bge-small-zh-v1.5/tokenizer.json +21278 -0
  126. package/models/Xenova/bge-small-zh-v1.5/tokenizer_config.json +15 -0
  127. package/package.json +139 -4
  128. package/src/asset-tool.mjs +237 -0
  129. package/src/assets.mjs +244 -0
  130. package/src/backfill.mjs +74 -0
  131. package/src/backup.mjs +80 -0
  132. package/src/deadend.mjs +95 -0
  133. package/src/deleted-store.mjs +35 -0
  134. package/src/engine.mjs +3831 -0
  135. package/src/facets/classifier.mjs +146 -0
  136. package/src/facets/embedding-service.mjs +53 -0
  137. package/src/facets/embedding-worker.mjs +27 -0
  138. package/src/facets/embedding.mjs +163 -0
  139. package/src/facets/extract.mjs +80 -0
  140. package/src/facets/fold-plan.mjs +84 -0
  141. package/src/facets/hybrid.mjs +29 -0
  142. package/src/facets/index.mjs +113 -0
  143. package/src/facets/linear-probe.mjs +43 -0
  144. package/src/facets/load.mjs +37 -0
  145. package/src/facets/mlp-probe.mjs +54 -0
  146. package/src/facets/mlp-service.mjs +53 -0
  147. package/src/facets/mlp-worker.mjs +26 -0
  148. package/src/facets/online-knn.mjs +58 -0
  149. package/src/facets/online-models.mjs +227 -0
  150. package/src/facets/panel-state.mjs +23 -0
  151. package/src/facets/panel.mjs +73 -0
  152. package/src/facets/random.mjs +26 -0
  153. package/src/facets/scanner.mjs +35 -0
  154. package/src/facets/schema.mjs +75 -0
  155. package/src/facets/seed-examples.mjs +58 -0
  156. package/src/facets/sensitivity.mjs +98 -0
  157. package/src/facets/service.mjs +53 -0
  158. package/src/facets/store.mjs +39 -0
  159. package/src/facets/worker.mjs +23 -0
  160. package/src/folds-store.mjs +109 -0
  161. package/src/groups.mjs +167 -0
  162. package/src/handoff-tool.mjs +51 -0
  163. package/src/handoff.mjs +127 -0
  164. package/src/index.mjs +649 -0
  165. package/src/inject-policy.mjs +17 -0
  166. package/src/learning.mjs +88 -0
  167. package/src/memory/alias-proposal.mjs +40 -0
  168. package/src/memory/bc-audit.mjs +245 -0
  169. package/src/memory/bc-index-updater.mjs +404 -0
  170. package/src/memory/bc-index.mjs +48 -0
  171. package/src/memory/bc-label.mjs +220 -0
  172. package/src/memory/bc-panel-state.mjs +107 -0
  173. package/src/memory/bc-pre-step.mjs +463 -0
  174. package/src/memory/bc-recall.mjs +470 -0
  175. package/src/memory/bc-shift.mjs +123 -0
  176. package/src/memory/bc-store.mjs +41 -0
  177. package/src/memory/bm25.mjs +112 -0
  178. package/src/memory/calibration.mjs +54 -0
  179. package/src/memory/chatter.mjs +29 -0
  180. package/src/memory/cmap-file.mjs +107 -0
  181. package/src/memory/concepts-file.mjs +41 -0
  182. package/src/memory/coref-slots.mjs +40 -0
  183. package/src/memory/event-alias-ledger.mjs +87 -0
  184. package/src/memory/event-log.mjs +95 -0
  185. package/src/memory/event-model.mjs +132 -0
  186. package/src/memory/event-segmenter.mjs +36 -0
  187. package/src/memory/event-summary-tree.mjs +122 -0
  188. package/src/memory/extraction-adapter.mjs +76 -0
  189. package/src/memory/extraction-composite.mjs +75 -0
  190. package/src/memory/extraction-core.mjs +76 -0
  191. package/src/memory/extraction-pre-step.mjs +161 -0
  192. package/src/memory/extraction-provider.mjs +300 -0
  193. package/src/memory/extraction-score.mjs +36 -0
  194. package/src/memory/facet-stabilizer.mjs +59 -0
  195. package/src/memory/history-map.mjs +139 -0
  196. package/src/memory/language-router.mjs +30 -0
  197. package/src/memory/load-gate.mjs +161 -0
  198. package/src/memory/mechanical-note.mjs +104 -0
  199. package/src/memory/onboarding.mjs +163 -0
  200. package/src/memory/persona.mjs +78 -0
  201. package/src/memory/planner.mjs +249 -0
  202. package/src/memory/pre-turn.mjs +80 -0
  203. package/src/memory/query-aliases.mjs +39 -0
  204. package/src/memory/recall-cue.mjs +43 -0
  205. package/src/memory/recall.mjs +65 -0
  206. package/src/memory/referent-candidates.mjs +55 -0
  207. package/src/memory/referent-ledger.mjs +86 -0
  208. package/src/memory/relabel-migrate.mjs +376 -0
  209. package/src/memory/relik-adapter.mjs +25 -0
  210. package/src/memory/special-keywords.mjs +60 -0
  211. package/src/memory/structured-adapters.mjs +75 -0
  212. package/src/memory/summary-index.mjs +335 -0
  213. package/src/memory/summary-tree.mjs +110 -0
  214. package/src/memory/term-extractor.mjs +44 -0
  215. package/src/memory/tokenize.mjs +9 -0
  216. package/src/memory/vector-index.mjs +54 -0
  217. package/src/memory/window-summary.mjs +133 -0
  218. package/src/mood.mjs +133 -0
  219. package/src/peer-tool.mjs +63 -0
  220. package/src/peer.mjs +144 -0
  221. package/src/policy.mjs +170 -0
  222. package/src/preflight.mjs +113 -0
  223. package/src/provenance.mjs +94 -0
  224. package/src/range.mjs +75 -0
  225. package/src/recall-exact-tool.mjs +267 -0
  226. package/src/recall-tool.mjs +521 -0
  227. package/src/recheck.mjs +99 -0
  228. package/src/save-start.mjs +29 -0
  229. package/src/session-guard.mjs +140 -0
  230. package/src/session-log-tool.mjs +322 -0
  231. package/src/session-log.mjs +606 -0
  232. package/src/skills.mjs +111 -0
  233. package/src/split-range.mjs +115 -0
  234. package/src/state.mjs +198 -0
  235. package/src/t0.mjs +149 -0
  236. package/src/tag-confirm.mjs +78 -0
  237. package/src/tag-detect.mjs +42 -0
  238. package/src/tag-maintenance.mjs +285 -0
  239. package/src/tag-reject.mjs +73 -0
  240. package/src/tags-store.mjs +94 -0
  241. package/src/tags.mjs +692 -0
  242. package/src/threads-backend.mjs +26 -0
  243. package/src/threads-core.mjs +1887 -0
  244. package/src/threads-store.mjs +207 -0
  245. package/src/threads-tool.mjs +522 -0
  246. package/src/threads.mjs +929 -0
  247. package/src/tool-preview.mjs +427 -0
  248. package/src/topic.mjs +63 -0
  249. package/src/training.mjs +75 -0
  250. package/src/turn-tag.mjs +62 -0
  251. package/src/user-turn.mjs +39 -0
  252. package/templates/cmap.md +11 -0
  253. package/templates/concepts.md +12 -0
  254. package/templates/habits.md +40 -0
  255. package/templates/keywords.md +6 -0
  256. package/templates/rebuild.md +134 -0
  257. package/templates/rules-safe.md +184 -0
  258. package/templates/rules.md +177 -0
package/client.js ADDED
@@ -0,0 +1,760 @@
1
+ /**
2
+ * dsh-continuity — Web Client half.
3
+ *
4
+ * Registers the left-sidebar entry (`sidebar.panellist`) and the main panel it
5
+ * opens (`main`, keyed by the same id). Visible text is registered through the
6
+ * Client locale service; colors and surfaces come from `--dsw-alias-*` tokens.
7
+ * The data comes from the Host route registered by `src/index.mjs`.
8
+ */
9
+ window.__ModuleLoader__.load({
10
+ id: 'dsh-continuity',
11
+ factory(require) {
12
+ // 宿主没给 react(或加载器变了)时安静退出:宁可没有 UI,也不能让 renderer 起不来。
13
+ let React = null;
14
+ try { React = require('react'); } catch (error) {
15
+ try { console.warn('[dsh-continuity] 没有 react,客户端禁用:' + String(error?.message ?? error)); } catch { /* ignore */ }
16
+ return { inject: [], apply() { /* disabled */ } };
17
+ }
18
+ if (React === null || typeof React.createElement !== 'function') {
19
+ return { inject: [], apply() { /* disabled */ } };
20
+ }
21
+ const h = React.createElement;
22
+ const NS = 'dsh-continuity';
23
+ const STATE_URL = '/dsh-continuity/state.json';
24
+ const TAGS_URL = '/dsh-continuity/tags.json';
25
+ const BC_URL = '/dsh-continuity/bc.json';
26
+ const FACETS_URL = '/dsh-continuity/facets.json';
27
+ const TAGS_TAB_ID = 'continuity-tags';
28
+ const TAGS_TAB_KIND = 'continuityTags';
29
+
30
+ const EN = {
31
+ 'panel.eyebrow': 'DSH CONTINUITY',
32
+ 'panel.title': 'Continuity',
33
+ 'tags.title': 'Tags',
34
+ 'panel.subtitle': 'Band-controlled compaction with labeled memory. Keep a verbatim recent window, remove a small oldest slice per pass, and tag every summary as a gist with a pointer to its original.',
35
+ 'status.engine': 'Engine mounted',
36
+ 'status.inert': 'Engine not mounted',
37
+ 'status.host': 'Host {version}',
38
+ 'status.updated': 'Updated {time}',
39
+ 'band.title': 'Band controller',
40
+ 'band.hint': 'Measure → plan → the oldest balanced range. One small pass per step, never into the protected tail.',
41
+ 'band.setpoint': 'setpoint',
42
+ 'band.high': 'high',
43
+ 'band.low': 'low',
44
+ 'band.emergency': 'emergency',
45
+ 'band.protect': 'protect',
46
+ 'band.max': 'max / pass',
47
+ 'band.min': 'min action',
48
+ 'projection.title': 'Config-only projection',
49
+ 'projection.hint': 'Computed from the live model route. The shipped backend triggers at min(0.92·W, W − O − headroom) and keeps retainRatio × (W − O) verbatim.',
50
+ 'projection.context': 'context window',
51
+ 'projection.reserved': 'reserved output',
52
+ 'projection.message': 'message budget',
53
+ 'projection.threshold': 'base threshold',
54
+ 'projection.retain': 'base retain',
55
+ 'projection.pass': 'base pass',
56
+ 'projection.setpoint': 'band setpoint',
57
+ 'projection.protect': 'band protect',
58
+ 'projection.source': 'source',
59
+ 'projection.invalid': 'retain ≥ threshold — this config would be rejected',
60
+ 'provenance.title': 'Provenance is part of the policy',
61
+ 'provenance.hint': 'A label on the memory itself, not a banner per event.',
62
+ 'provenance.convention': 'Standing convention (one system section)',
63
+ 'provenance.sample': 'Checkpoint header',
64
+ 'passes.title': 'Recent passes',
65
+ 'passes.empty': 'No pass recorded yet. Select the Continuity preset for a new session; passes appear here as they happen.',
66
+ 'passes.range': 'range',
67
+ 'passes.count': 'messages',
68
+ 'passes.tokens': 'tokens',
69
+ 'passes.reason': 'reason',
70
+ 'facts.title': 'Measured at runtime',
71
+ 'facts.hint': 'Recorded from the live 0.2.1-rc.3 Host, not from npm types.',
72
+ 'try.title': 'Try it',
73
+ 'try.one': 'Start a new session and pick the preset "Band (config-only)" to test thresholdRatio 0.92 / retainRatio 0.5 on the shipped backend.',
74
+ 'try.two': 'Pick "Continuity" to test the band engine, gist headers, and the standing convention.',
75
+ 'try.three': 'Ask about a detail that only exists in a compacted range. Expected: the model cites the [gist] header and says it only remembers the gist.',
76
+ 'try.four': 'Existing sessions keep the preset revision they started with; rollback is one preset selection or disabling the bundle.',
77
+ 'error.title': 'The state route is not answering',
78
+ 'error.body': 'The sidebar entry is registered, but the Host route returned: {message}',
79
+ 'error.reload': 'Reload page',
80
+ 'footer': 'The seam is policy, not personhood.',
81
+ };
82
+
83
+ const ZH = {
84
+ 'panel.eyebrow': 'DSH 连续性',
85
+ 'panel.title': '连续性',
86
+ 'tags.title': '标签',
87
+ 'panel.subtitle': '带标签记忆的带宽压缩:保留最近的逐字窗口,每次只移除最旧的一小片,并把每份摘要标成 [gist],附回原文的指针。',
88
+ 'status.engine': '引擎已挂载',
89
+ 'status.inert': '引擎未挂载',
90
+ 'status.host': '宿主 {version}',
91
+ 'status.updated': '更新于 {time}',
92
+ 'band.title': '带宽控制器',
93
+ 'band.hint': '测量 → 规划 → 最旧且边界平衡的区间。每步只做一小刀,绝不折进受保护的尾部。',
94
+ 'band.setpoint': '设定点',
95
+ 'band.high': '高水位',
96
+ 'band.low': '低水位',
97
+ 'band.emergency': '紧急',
98
+ 'band.protect': '保护',
99
+ 'band.max': '单次上限',
100
+ 'band.min': '最小动作',
101
+ 'projection.title': '仅配置实验投影',
102
+ 'projection.hint': '按当前模型路由计算。自带后端在 min(0.92·W, W − O − headroom) 触发,并保留 retainRatio × (W − O) 的逐字尾部。',
103
+ 'projection.context': '上下文窗口',
104
+ 'projection.reserved': '输出预留',
105
+ 'projection.message': '消息预算',
106
+ 'projection.threshold': '基线阈值',
107
+ 'projection.retain': '基线保留',
108
+ 'projection.pass': '单次压缩量',
109
+ 'projection.setpoint': '带宽设定点',
110
+ 'projection.protect': '带宽保护',
111
+ 'projection.source': '来源',
112
+ 'projection.invalid': '保留 ≥ 阈值 —— 该配置会被拒绝',
113
+ 'provenance.title': '出处也是策略的一部分',
114
+ 'provenance.hint': '标签贴在记忆本身上,而不是每条事件前的横幅。',
115
+ 'provenance.convention': '常驻约定(单条系统段)',
116
+ 'provenance.sample': '检查点标题',
117
+ 'passes.title': '最近压缩',
118
+ 'passes.empty': '还没有记录。用「Continuity」预设新建会话,压缩发生后会出现在这里。',
119
+ 'passes.range': '区间',
120
+ 'passes.count': '消息',
121
+ 'passes.tokens': 'tokens',
122
+ 'passes.reason': '原因',
123
+ 'facts.title': '运行时实测',
124
+ 'facts.hint': '来自运行中的 0.2.1-rc.3 宿主,而不是 npm 类型声明。',
125
+ 'try.title': '怎么试',
126
+ 'try.one': '新建会话并选择「Band(仅配置)」预设,在自带后端上测试 thresholdRatio 0.92 / retainRatio 0.5。',
127
+ 'try.two': '选择「Continuity」预设,测试带宽引擎、gist 标题与常驻约定。',
128
+ 'try.three': '追问一个只存在于被压缩区间里的细节。预期:模型引用 [gist] 标题并说明只记得概要。',
129
+ 'try.four': '已有会话保留它们启动时的预设版本;回滚只需改选预设或停用插件。',
130
+ 'error.title': '状态路由没有响应',
131
+ 'error.body': '侧栏入口已注册,但宿主路由返回:{message}',
132
+ 'error.reload': '刷新页面',
133
+ 'footer': '接缝是策略,不是人格。',
134
+ };
135
+
136
+ const S = {
137
+ page: {
138
+ height: '100%',
139
+ minHeight: '100%',
140
+ overflowY: 'auto',
141
+ boxSizing: 'border-box',
142
+ padding: '28px 32px 56px',
143
+ color: 'var(--dsw-alias-label-primary)',
144
+ background: 'var(--dsw-alias-bg-base)',
145
+ },
146
+ inner: { maxWidth: 900, margin: '0 auto', display: 'flex', flexDirection: 'column', gap: 16 },
147
+ eyebrow: {
148
+ fontSize: 11,
149
+ letterSpacing: '0.12em',
150
+ fontWeight: 650,
151
+ color: 'var(--dsw-alias-label-secondary)',
152
+ },
153
+ h1: { margin: '6px 0 0', fontSize: 24, lineHeight: '32px', fontWeight: 650, letterSpacing: '-0.015em' },
154
+ subtitle: { margin: '6px 0 0', fontSize: 13, lineHeight: '20px', color: 'var(--dsw-alias-label-secondary)', maxWidth: 720 },
155
+ badges: { display: 'flex', flexWrap: 'wrap', gap: 8, marginTop: 12 },
156
+ badge: {
157
+ display: 'inline-flex',
158
+ alignItems: 'center',
159
+ gap: 6,
160
+ height: 22,
161
+ padding: '0 9px',
162
+ borderRadius: 999,
163
+ border: '1px solid var(--dsw-alias-border-l1)',
164
+ background: 'var(--dsw-alias-bg-layer-1)',
165
+ color: 'var(--dsw-alias-label-secondary)',
166
+ fontSize: 11,
167
+ lineHeight: '20px',
168
+ },
169
+ dot: (color) => ({ width: 6, height: 6, borderRadius: 3, background: color }),
170
+ card: {
171
+ border: '1px solid var(--dsw-alias-border-l1)',
172
+ background: 'var(--dsw-alias-bg-layer-1)',
173
+ borderRadius: 12,
174
+ padding: '16px 18px',
175
+ },
176
+ cardHead: { display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 12, marginBottom: 4 },
177
+ h2: { margin: 0, fontSize: 14, lineHeight: '20px', fontWeight: 650 },
178
+ hint: { margin: 0, fontSize: 12, lineHeight: '18px', color: 'var(--dsw-alias-label-secondary)' },
179
+ grid: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))', gap: '10px 18px', marginTop: 12 },
180
+ kv: { display: 'flex', flexDirection: 'column', gap: 2, minWidth: 0 },
181
+ k: { fontSize: 11, color: 'var(--dsw-alias-label-secondary)' },
182
+ v: { fontSize: 13, lineHeight: '18px', overflowWrap: 'anywhere' },
183
+ mono: {
184
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace',
185
+ fontSize: 12,
186
+ lineHeight: '18px',
187
+ overflowWrap: 'anywhere',
188
+ },
189
+ pre: {
190
+ margin: '10px 0 0',
191
+ padding: '10px 12px',
192
+ borderRadius: 8,
193
+ border: '1px solid var(--dsw-alias-border-l1)',
194
+ background: 'var(--dsw-alias-bg-layer-2)',
195
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace',
196
+ fontSize: 12,
197
+ lineHeight: '18px',
198
+ whiteSpace: 'pre-wrap',
199
+ overflowWrap: 'anywhere',
200
+ },
201
+ row: {
202
+ display: 'grid',
203
+ gridTemplateColumns: 'minmax(120px, 180px) minmax(0, 1fr)',
204
+ gap: 12,
205
+ padding: '7px 0',
206
+ borderTop: '1px solid var(--dsw-alias-border-l1)',
207
+ fontSize: 12,
208
+ lineHeight: '18px',
209
+ },
210
+ rowLabel: { color: 'var(--dsw-alias-label-secondary)' },
211
+ list: { margin: '10px 0 0', padding: 0, listStyle: 'none', display: 'flex', flexDirection: 'column' },
212
+ steps: { margin: '10px 0 0', paddingLeft: 18, display: 'flex', flexDirection: 'column', gap: 6, fontSize: 12, lineHeight: '18px' },
213
+ button: {
214
+ height: 28,
215
+ padding: '0 12px',
216
+ borderRadius: 7,
217
+ border: '1px solid var(--dsw-alias-border-l2)',
218
+ background: 'var(--dsw-alias-bg-layer-2)',
219
+ color: 'var(--dsw-alias-label-primary)',
220
+ fontSize: 12,
221
+ cursor: 'pointer',
222
+ },
223
+ };
224
+
225
+ function fmt(template, values) {
226
+ return String(template).replace(/\{(\w+)\}/g, (match, key) => (key in values ? String(values[key]) : match));
227
+ }
228
+
229
+ function pct(value) {
230
+ return `${Math.round(Number(value) * 100)}%`;
231
+ }
232
+
233
+ function useTranslate(ctx) {
234
+ const t = React.useMemo(() => ctx.locale.bind(NS), [ctx]);
235
+ const [, bump] = React.useReducer((value) => value + 1, 0);
236
+ React.useEffect(() => ctx.locale.subscribe(bump), [ctx]);
237
+ return t;
238
+ }
239
+
240
+ function useSnapshot() {
241
+ const [snapshot, setSnapshot] = React.useState({ status: 'loading', data: null, error: null });
242
+ React.useEffect(() => {
243
+ let alive = true;
244
+ let timer = null;
245
+ const load = async () => {
246
+ try {
247
+ const response = await fetch(STATE_URL, { cache: 'no-store', headers: { accept: 'application/json' } });
248
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
249
+ const data = await response.json();
250
+ if (data?.error !== undefined && data?.facts === undefined) throw new Error(String(data.error));
251
+ if (alive) setSnapshot({ status: 'ok', data, error: null });
252
+ } catch (error) {
253
+ if (alive) {
254
+ setSnapshot((current) => current.status === 'ok'
255
+ ? current
256
+ : { status: 'error', data: null, error: String(error?.message ?? error) });
257
+ }
258
+ } finally {
259
+ if (alive) timer = setTimeout(load, 5000);
260
+ }
261
+ };
262
+ load();
263
+ return () => {
264
+ alive = false;
265
+ if (timer !== null) clearTimeout(timer);
266
+ };
267
+ }, []);
268
+ return snapshot;
269
+ }
270
+
271
+ function Badge({ children, tone }) {
272
+ const color = tone === 'ok'
273
+ ? 'var(--dsw-alias-state-success-primary)'
274
+ : tone === 'error'
275
+ ? 'var(--dsw-alias-state-error-primary)'
276
+ : 'var(--dsw-alias-state-idle-primary)';
277
+ return h('span', { style: S.badge }, h('span', { style: S.dot(color) }), children);
278
+ }
279
+
280
+ function Card({ title, hint, children }) {
281
+ return h('section', { style: S.card },
282
+ h('div', { style: S.cardHead },
283
+ h('h2', { style: S.h2 }, title),
284
+ hint ? h('p', { style: { ...S.hint, textAlign: 'right' } }, hint) : null),
285
+ children);
286
+ }
287
+
288
+ function KV({ label, value, mono }) {
289
+ return h('div', { style: S.kv },
290
+ h('span', { style: S.k }, label),
291
+ h('span', { style: mono ? S.mono : S.v }, value));
292
+ }
293
+
294
+ function BandBar({ t, policy }) {
295
+ const segments = [
296
+ { from: 0, to: policy.low, color: 'var(--dsw-alias-bg-layer-2)', opacity: 1 },
297
+ { from: policy.low, to: policy.setpoint, color: 'var(--dsw-alias-state-idle-primary)', opacity: 0.5 },
298
+ { from: policy.setpoint, to: policy.high, color: 'var(--dsw-alias-brand-primary)', opacity: 0.75 },
299
+ { from: policy.high, to: policy.emergency, color: 'var(--dsw-alias-state-warn-primary)', opacity: 0.7 },
300
+ { from: policy.emergency, to: 1, color: 'var(--dsw-alias-state-error-primary)', opacity: 0.7 },
301
+ ];
302
+ return h('div', { style: { marginTop: 14 } },
303
+ h('div', {
304
+ style: {
305
+ display: 'flex',
306
+ height: 20,
307
+ borderRadius: 10,
308
+ overflow: 'hidden',
309
+ border: '1px solid var(--dsw-alias-border-l1)',
310
+ },
311
+ }, ...segments.map((segment, index) => h('span', {
312
+ key: index,
313
+ style: {
314
+ width: `${Math.max(0, segment.to - segment.from) * 100}%`,
315
+ background: segment.color,
316
+ opacity: segment.opacity,
317
+ },
318
+ }))),
319
+ h('div', { style: { display: 'flex', justifyContent: 'space-between', marginTop: 6, fontSize: 11, color: 'var(--dsw-alias-label-secondary)' } },
320
+ h('span', null, '0'),
321
+ policy.setpoint !== policy.high
322
+ ? h('span', null, `${t('band.setpoint')} ${pct(policy.setpoint)}`)
323
+ : null,
324
+ h('span', null, `${t('band.high')} ${pct(policy.high)}`),
325
+ h('span', null, `100%`)));
326
+ }
327
+
328
+ function formatNumber(value) {
329
+ if (typeof value !== 'number' || !Number.isFinite(value)) return '—';
330
+ return value.toLocaleString();
331
+ }
332
+
333
+ function Projection({ t, projection }) {
334
+ if (projection === null || projection === undefined || projection.available !== true) {
335
+ return h('p', { style: { ...S.hint, marginTop: 12 } }, t('projection.source') + ': —');
336
+ }
337
+ const rows = [
338
+ ['projection.context', formatNumber(projection.contextWindow)],
339
+ ['projection.reserved', formatNumber(projection.reservedTokens)],
340
+ ['projection.message', formatNumber(projection.messageBudget)],
341
+ ['projection.threshold', formatNumber(projection.baseThresholdTokens)],
342
+ ['projection.retain', formatNumber(projection.baseRetainTokens)],
343
+ ['projection.pass', formatNumber(projection.basePassTokens)],
344
+ ['projection.setpoint', formatNumber(projection.bandSetpointTokens)],
345
+ ['projection.protect', formatNumber(projection.bandProtectTokens)],
346
+ ];
347
+ return h('div', null,
348
+ h('div', { style: S.grid }, ...rows.map(([key, value]) => h(KV, { key, label: t(key), value }))),
349
+ h('div', { style: { ...S.row, borderTop: 'none', paddingTop: 10 } },
350
+ h('span', { style: S.rowLabel }, t('projection.source')),
351
+ h('span', { style: S.mono }, `${projection.provider}/${projection.model} · ${projection.source}`)),
352
+ projection.baseValid === false
353
+ ? h('p', { style: { ...S.hint, marginTop: 4, color: 'var(--dsw-alias-state-error-primary)' } }, t('projection.invalid'))
354
+ : null);
355
+ }
356
+
357
+ function Passes({ t, passes }) {
358
+ if (!Array.isArray(passes) || passes.length === 0) {
359
+ return h('p', { style: { ...S.hint, marginTop: 10 } }, t('passes.empty'));
360
+ }
361
+ return h('div', { style: { marginTop: 8 } }, ...passes.map((pass, index) => h('div', {
362
+ key: `${pass.at}-${index}`,
363
+ style: S.row,
364
+ },
365
+ h('span', { style: S.rowLabel }, new Date(pass.at).toLocaleTimeString()),
366
+ h('span', { style: { display: 'flex', flexWrap: 'wrap', gap: '4px 14px' } },
367
+ h('span', { style: S.mono }, `seq ${pass.start}–${pass.end}`),
368
+ h('span', null, `${t('passes.count')} ${pass.count}`),
369
+ h('span', null, `${t('passes.tokens')} ${formatNumber(pass.tokens)}`),
370
+ h('span', { style: S.rowLabel }, pass.reason)))));
371
+ }
372
+
373
+ function PanelIcon({ size = 18, active = false }) {
374
+ return h('svg', {
375
+ width: size,
376
+ height: size,
377
+ viewBox: '0 0 24 24',
378
+ fill: 'none',
379
+ 'aria-hidden': true,
380
+ style: { display: 'block', opacity: active ? 1 : 0.78 },
381
+ },
382
+ h('path', {
383
+ d: 'M4 17.5c2.2-6.8 13.6-6.8 16 0',
384
+ stroke: 'currentColor',
385
+ strokeWidth: 1.8,
386
+ strokeLinecap: 'round',
387
+ }),
388
+ h('path', {
389
+ d: 'M3 20h18',
390
+ stroke: 'currentColor',
391
+ strokeWidth: 1.4,
392
+ strokeLinecap: 'round',
393
+ opacity: 0.45,
394
+ }),
395
+ h('circle', { cx: 12, cy: 8.2, r: 2.1, fill: 'currentColor' }));
396
+ }
397
+
398
+ /**
399
+ * 开始页「更多工具」里的入口。
400
+ * 注意:组件在 factory 作用域里,拿不到 apply(ctx) 的 ctx;
401
+ * 所以真正的打开动作由 apply 通过 inject 传进来(它才持有 ctx)。
402
+ */
403
+ function GuideTagsRow({ open }) {
404
+ const [status, setStatus] = React.useState('');
405
+ const onClick = () => {
406
+ try {
407
+ setStatus(String(open()));
408
+ } catch (error) {
409
+ setStatus('失败:' + String(error?.message ?? error));
410
+ }
411
+ };
412
+ return h('button', {
413
+ type: 'button',
414
+ style: {
415
+ display: 'flex', alignItems: 'center', gap: 8, width: '100%',
416
+ padding: '8px 10px', background: 'transparent', border: 'none',
417
+ color: 'inherit', cursor: 'pointer', textAlign: 'left', fontSize: 12,
418
+ },
419
+ onClick,
420
+ },
421
+ h('span', { style: { opacity: 0.7 } }, 'B'),
422
+ h('span', null, 'A / B / C / K 记忆标签'),
423
+ status ? h('span', { style: { marginLeft: 'auto', opacity: 0.65, fontSize: 10 } }, status) : null);
424
+ }
425
+
426
+ function FacetView({ panel, onShowTags }) {
427
+ const box = { padding: '8px 10px', fontSize: 12, lineHeight: 1.5 };
428
+ const row = { display: 'flex', gap: 6, alignItems: 'baseline', padding: '2px 0' };
429
+ const pair = { display: 'flex', gap: 6, alignItems: 'baseline', marginRight: 8 };
430
+ const current = panel?.currentTopic;
431
+ const curve = panel?.curve ?? [];
432
+ const max = Math.max(0.0001, ...curve.map((point) => point.smoothed ?? 0));
433
+ const points = curve.map((point, index) => {
434
+ const x = curve.length <= 1 ? 0 : (index / (curve.length - 1)) * 300;
435
+ const y = 54 - ((point.smoothed ?? 0) / max) * 48;
436
+ return x.toFixed(1) + ',' + y.toFixed(1);
437
+ }).join(' ');
438
+ return h('div', { style: box },
439
+ h('div', { style: { fontWeight: 600, marginBottom: 4 } },
440
+ '当前话题:' + (current?.alias ?? '—')),
441
+ h('div', { style: { display: 'flex', flexWrap: 'wrap', marginBottom: 8 } },
442
+ ...(current?.pairs ?? []).map((item) => h('span', { key: item.slot + ':' + item.value, style: pair },
443
+ h('span', { style: { opacity: 0.7 } }, item.slot), h('span', null, item.value),
444
+ h('span', { style: { opacity: 0.55 } }, item.turns + ' 轮')))),
445
+ h('div', { style: { opacity: 0.6, marginBottom: 8 } },
446
+ '总 ' + (panel?.totalTurns ?? 0) + ' turn · facet 计数 ' + (panel?.facetSum ?? 0) + ' · unknown ' + (panel?.unknown ?? 0)),
447
+ curve.length > 0 ? h('svg', { viewBox: '0 0 300 60', width: '100%', height: 60, style: { display: 'block', marginBottom: 8 } },
448
+ h('polyline', { points, fill: 'none', stroke: 'var(--dsw-alias-accent, #4ea1ff)', strokeWidth: 2 })) : null,
449
+ h('div', { style: { opacity: 0.55, fontSize: 11 } }, '曲线:当前话题在历史窗口的敏感度'),
450
+ h('button', {
451
+ type: 'button',
452
+ style: { ...S.button, marginTop: 8 },
453
+ onClick: onShowTags,
454
+ }, '显示 B / C / K 标签'));
455
+ }
456
+
457
+ /** 右侧栏「B / C / K 标签」页:只读计数。 */
458
+ function TagsPanel({ sessionId, useTabInfo } = {}) {
459
+ const [state, setState] = React.useState({ status: 'loading', data: null, error: null });
460
+ const tabInfo = typeof useTabInfo === 'function' ? useTabInfo() : null;
461
+ const closedRef = React.useRef(false);
462
+ React.useEffect(() => {
463
+ if (closedRef.current) return;
464
+ if (state.status !== 'ready' || !['not-continuity-session', 'persona-disabled'].includes(state.data?.error)) return;
465
+ closedRef.current = true;
466
+ try { tabInfo?.tab?.actions?.close?.(); } catch { /* ignore */ }
467
+ }, [state.status, state.data, tabInfo]);
468
+ React.useEffect(() => {
469
+ let alive = true;
470
+ const load = async () => {
471
+ try {
472
+ const url = sessionId ? `${BC_URL}?sessionId=${encodeURIComponent(sessionId)}` : BC_URL;
473
+ const response = await fetch(url, { cache: 'no-store', headers: { accept: 'application/json' } });
474
+ if (!response.ok) throw new Error('HTTP ' + response.status);
475
+ const data = await response.json();
476
+ if (alive) setState({ status: 'ready', data, error: null });
477
+ } catch (error) {
478
+ if (alive) setState({ status: 'error', data: null, error: String(error?.message ?? error) });
479
+ }
480
+ };
481
+ load();
482
+ const timer = setInterval(load, 5000);
483
+ return () => { alive = false; clearInterval(timer); };
484
+ }, [sessionId]);
485
+
486
+ const box = {
487
+ padding: '8px 10px',
488
+ fontSize: 12,
489
+ lineHeight: 1.5,
490
+ maxHeight: '100%',
491
+ minHeight: 0,
492
+ overflowY: 'auto',
493
+ overscrollBehavior: 'contain',
494
+ boxSizing: 'border-box',
495
+ scrollbarGutter: 'stable',
496
+ };
497
+ const row = { display: 'flex', gap: 6, alignItems: 'baseline', padding: '2px 0' };
498
+ const count = { marginLeft: 'auto', opacity: 0.55, fontVariantNumeric: 'tabular-nums' };
499
+
500
+ if (state.status === 'loading') return h('div', { style: box }, '读取中…');
501
+ if (state.status === 'error') return h('div', { style: box }, '读不到 bc-index.json:' + state.error);
502
+ const data = state.data;
503
+ if (data && data.available === false) {
504
+ return h('div', { style: box },
505
+ h('div', { style: { fontWeight: 600, marginBottom: 4 } }, 'A / B / C / K 标签'),
506
+ h('div', { style: { opacity: 0.65 } }, '还没有 B 索引:' + (data.error ?? 'unknown')));
507
+ }
508
+ return h('div', { style: box },
509
+ h('div', { style: { marginBottom: 10, paddingBottom: 6, borderBottom: '1px solid rgba(127,127,127,0.25)' } },
510
+ h('div', { style: { fontWeight: 600, marginBottom: 4 } }, 'A 概念(轮数=索引实算)'),
511
+ ...(data?.aLabels ?? []).length > 0
512
+ ? (data.aLabels ?? []).map((item) => h('div', {
513
+ key: 'A:' + item.label,
514
+ style: { ...row, flexDirection: 'column', alignItems: 'stretch', gap: 2 },
515
+ },
516
+ h('div', { style: { display: 'flex', justifyContent: 'space-between', gap: 6 } },
517
+ h('span', null, item.label),
518
+ h('span', { style: count }, (item.turns ?? 0) + ' 轮')),
519
+ h('div', { style: { opacity: 0.55, fontSize: 10 } }, item.description ?? '')))
520
+ : [h('div', { style: { opacity: 0.6 } }, '空表 · 本 session 还没有 A 概念')]),
521
+ h('div', { style: { fontWeight: 600, marginBottom: 4 } }, 'B 通用标签'),
522
+ h('div', { style: { opacity: 0.6, marginBottom: 8 } },
523
+ '覆盖 turn ' + (data?.firstTurn ?? '?') + '–' + (data?.lastTurn ?? '?')
524
+ + ' · 截至 ' + (data?.updatedAt ? new Date(data.updatedAt).toLocaleString() : '?')
525
+ + ' · ' + (data?.totalTurns ?? 0) + ' 轮 · weak ' + (data?.weakTurns ?? 0)),
526
+ ...((data?.labels ?? []).length > 0
527
+ ? (data.labels ?? []).map((item) => h('div', {
528
+ key: 'B:' + item.label,
529
+ style: { ...row, justifyContent: 'space-between' },
530
+ }, h('span', null, item.label), h('span', { style: count }, item.turns + ' 轮')))
531
+ : [h('div', { style: { opacity: 0.6 } }, '空表 · 本 session 还没有 B 标签')]),
532
+ h('div', { style: { marginTop: 10, paddingTop: 6, borderTop: '1px solid rgba(127,127,127,0.25)' } },
533
+ h('div', { style: { fontWeight: 600, marginBottom: 4 } }, 'K 特殊关键词'),
534
+ ...((data?.kLabels ?? []).length > 0
535
+ ? (data.kLabels ?? []).map((item) => h('div', {
536
+ key: 'K:' + item.label,
537
+ style: { ...row, justifyContent: 'space-between' },
538
+ }, h('span', null, item.label), h('span', { style: count }, item.turns + ' 轮')))
539
+ : [h('div', { style: { opacity: 0.6 } }, '空表 · 本 session 还没有 K 关键词')])),
540
+ h('div', { style: { marginTop: 10, paddingTop: 6, borderTop: '1px solid rgba(127,127,127,0.25)' } },
541
+ h('div', { style: { fontWeight: 600, marginBottom: 4 } }, 'C 用户词'),
542
+ ...((data?.cLabels ?? []).length > 0
543
+ ? (data.cLabels ?? []).map((item) => h('div', {
544
+ key: 'C:' + item.label,
545
+ style: { ...row, flexDirection: 'column', alignItems: 'stretch', gap: 2 },
546
+ },
547
+ h('div', { style: { display: 'flex', justifyContent: 'space-between', gap: 6 } },
548
+ h('span', null, item.label), h('span', { style: count }, item.turns + ' 轮')),
549
+ h('div', { style: { opacity: 0.55, fontSize: 10 } },
550
+ (item.concepts ?? []).join(' / ') + (item.summary ? ' (摘要)' : '')),
551
+ item.summary ? h('div', { style: { opacity: 0.5, fontSize: 10 } }, item.summary) : null))
552
+ : [h('div', { style: { opacity: 0.6 } }, '空表 · 本 session 还没有 C 用户词')])),
553
+ h('div', { style: { marginTop: 10, paddingTop: 6, borderTop: '1px solid rgba(127,127,127,0.25)', opacity: 0.7 } },
554
+ 'A / B / C / K 均为本 session 自己的表;空表是合法初始状态;点击追忆时再由 recall() 返回候选。'));
555
+ }
556
+
557
+ function TagsTitle() {
558
+ return h('span', null, 'A / B / C / K 标签');
559
+ }
560
+
561
+ function makePanel(ctx) {
562
+ return function ContinuityPanel() {
563
+ const t = useTranslate(ctx);
564
+ const snapshot = useSnapshot();
565
+ const data = snapshot.data;
566
+ const policy = data?.engine?.policy ?? {
567
+ setpoint: 0.60,
568
+ high: 0.60,
569
+ low: 0.40,
570
+ emergency: 0.65,
571
+ emergencyTo: 0.42,
572
+ k: 0.4,
573
+ maxPerBatch: 190000,
574
+ minAction: 24000,
575
+ protect: 0.30,
576
+ };
577
+
578
+ const header = h('header', null,
579
+ h('div', { style: S.eyebrow }, t('panel.eyebrow')),
580
+ h('h1', { style: S.h1 }, t('panel.title')),
581
+ h('p', { style: S.subtitle }, t('panel.subtitle')),
582
+ h('div', { style: S.badges },
583
+ data?.engine
584
+ ? h(Badge, { tone: 'ok' }, t('status.engine'))
585
+ : h(Badge, { tone: 'idle' }, t('status.inert')),
586
+ h(Badge, null, fmt(t('status.host'), { version: data?.plugin?.version ?? '—' })),
587
+ data?.generatedAt ? h(Badge, null, fmt(t('status.updated'), {
588
+ time: new Date(data.generatedAt).toLocaleTimeString(),
589
+ })) : null));
590
+
591
+ if (snapshot.status === 'error' && data === null) {
592
+ return h('div', { style: S.page }, h('div', { style: S.inner }, header,
593
+ h(Card, { title: t('error.title'), hint: null },
594
+ h('p', { style: { ...S.hint, marginTop: 8 } }, fmt(t('error.body'), { message: snapshot.error ?? 'unknown' })),
595
+ h('button', {
596
+ type: 'button',
597
+ style: { ...S.button, marginTop: 12 },
598
+ onClick: () => window.location.reload(),
599
+ }, t('error.reload')))));
600
+ }
601
+
602
+ return h('div', { style: S.page },
603
+ h('div', { style: S.inner },
604
+ header,
605
+
606
+ h(Card, { title: 'A / B / C / K 记忆标签', hint: 'A 展示 + B 通用概念 + C 用户词 + K 特殊关键词· 只读' },
607
+ h(TagsPanel, { compact: true }),
608
+ h('button', {
609
+ type: 'button',
610
+ style: { ...S.button, marginTop: 8 },
611
+ onClick: () => { try { ctx.sidebarRight?.openTab?.('continuityTags'); } catch { /* 右侧栏不可用时忽略 */ } },
612
+ }, '在右侧栏打开')),
613
+
614
+ h(Card, { title: t('band.title'), hint: t('band.hint') },
615
+ h(BandBar, { t, policy }),
616
+ h('div', { style: S.grid },
617
+ h(KV, { label: t('band.setpoint'), value: pct(policy.setpoint) }),
618
+ h(KV, { label: t('band.high'), value: pct(policy.high) }),
619
+ h(KV, { label: t('band.low'), value: pct(policy.low) }),
620
+ h(KV, { label: t('band.emergency'), value: pct(policy.emergency) }),
621
+ h(KV, { label: t('band.protect'), value: pct(policy.protect) }),
622
+ h(KV, { label: t('band.max'), value: formatNumber(policy.maxPerBatch) }),
623
+ h(KV, { label: t('band.min'), value: formatNumber(policy.minAction) }))),
624
+
625
+ h(Card, { title: t('projection.title'), hint: t('projection.hint') },
626
+ h(Projection, { t, projection: data?.projection ?? null })),
627
+
628
+ h(Card, { title: t('provenance.title'), hint: t('provenance.hint') },
629
+ h('p', { style: { ...S.k, marginTop: 10 } }, t('provenance.convention')),
630
+ h('pre', { style: S.pre }, data?.provenance?.convention ?? '—'),
631
+ h('p', { style: { ...S.k, marginTop: 12 } }, t('provenance.sample')),
632
+ h('pre', { style: S.pre }, data?.provenance?.sample ?? '—')),
633
+
634
+ h(Card, { title: t('passes.title'), hint: null },
635
+ h(Passes, { t, passes: data?.passes ?? [] })),
636
+
637
+ h(Card, { title: t('facts.title'), hint: t('facts.hint') },
638
+ h('div', { style: S.list }, ...(data?.facts ?? []).map((fact, index) => h('div', {
639
+ key: `${fact.label}-${index}`,
640
+ style: { ...S.row, ...(index === 0 ? { borderTop: 'none' } : {}) },
641
+ },
642
+ h('span', { style: S.rowLabel }, fact.label),
643
+ h('span', { style: S.mono }, fact.value))))),
644
+
645
+ h(Card, { title: t('try.title'), hint: null },
646
+ h('ol', { style: S.steps },
647
+ h('li', null, t('try.one')),
648
+ h('li', null, t('try.two')),
649
+ h('li', null, t('try.three')),
650
+ h('li', null, t('try.four')))),
651
+
652
+ h('p', { style: { ...S.hint, textAlign: 'center', margin: '4px 0 0' } }, t('footer'))));
653
+ };
654
+ }
655
+
656
+ // 宿主能力逐个检测 + 每个注册点单独兜底:
657
+ // 一个 API 缺失/签名不同,只跳过那一项,绝不把整个 renderer 拖崩
658
+ // (2026-10-29:rc2 上装右侧栏标签,宿主少一个服务 → Renderer boot failed)。
659
+ const safe = (label, fn) => {
660
+ try { return fn(); } catch (error) {
661
+ try {
662
+ console.warn('[dsh-continuity] 跳过 ' + label + ':' + String(error?.name ?? 'Error') + ': ' + String(error?.message ?? error));
663
+ } catch { /* console 不可用也不能抛 */ }
664
+ return undefined;
665
+ }
666
+ };
667
+ const effect = (ctx, fn) => safe('effect', () => {
668
+ const value = fn();
669
+ if (typeof ctx?.effect === 'function') return ctx.effect(() => value);
670
+ return value;
671
+ });
672
+
673
+ return {
674
+ // 必须声明:inject 是"等这些服务就绪再 apply"的机制。
675
+ // 去掉它(我试过)会让 apply 跑在服务注册之前,能力检测看到 undefined,
676
+ // 右侧栏标签整块被跳过——标签页就消失了(2026-10-29 实测)。
677
+ // 真正的防崩措施在 apply 内部:每项注册单独兜底,一项失败不连坐。
678
+ inject: ['slots', 'locale', 'sidebarRightTabs', 'sidebarRight'],
679
+ apply(ctx) {
680
+ const hasSlots = ctx?.slots !== undefined && ctx.slots !== null && typeof ctx.slots.inject === 'function';
681
+ const hasLocale = ctx?.locale !== undefined && ctx.locale !== null && typeof ctx.locale.register === 'function';
682
+ const hasTabs = ctx?.sidebarRightTabs !== undefined
683
+ && ctx.sidebarRightTabs !== null
684
+ && typeof ctx.sidebarRightTabs.register === 'function';
685
+
686
+ if (hasLocale) {
687
+ effect(ctx, () => {
688
+ const disposeEn = ctx.locale.register(NS, 'en', EN);
689
+ const disposeZh = ctx.locale.register(NS, 'zh', ZH);
690
+ return () => {
691
+ try { disposeZh(); } catch { /* ignore */ }
692
+ try { disposeEn(); } catch { /* ignore */ }
693
+ };
694
+ });
695
+ }
696
+
697
+ // 没有 slots 就什么都做不了:安静退出,不抛。
698
+ if (!hasSlots) return;
699
+
700
+ const translate = hasLocale && typeof ctx.locale.bind === 'function'
701
+ ? ctx.locale.bind(NS)
702
+ : (key) => key;
703
+
704
+ safe('sidebar.panellist', () => ctx.slots.inject('sidebar.panellist', () => ctx.slots.register({
705
+ name: 'sidebar.panellist',
706
+ id: 'continuity',
707
+ order: 20,
708
+ label: () => translate('panel.title'),
709
+ }, PanelIcon)));
710
+
711
+ // 右侧栏:宿主没有这个服务就整块跳过。
712
+ if (hasTabs) {
713
+ effect(ctx, () => ctx.sidebarRightTabs.register({
714
+ id: TAGS_TAB_ID,
715
+ kind: TAGS_TAB_KIND,
716
+ priority: 'builtin',
717
+ title: () => 'A / B / C / K 标签',
718
+ }));
719
+ }
720
+
721
+ // 开始页「更多工具」入口
722
+ safe('sidebar.right.tab.guide.actions', () => ctx.slots.inject('sidebar.right.tab.guide.actions', () => ctx.slots.register({
723
+ name: 'sidebar.right.tab.guide.actions',
724
+ id: 'continuity-tags-open',
725
+ order: 10,
726
+ locale: NS,
727
+ inject: () => ({
728
+ open: () => {
729
+ if (ctx.sidebarRight === undefined || typeof ctx.sidebarRight.openTab !== 'function') {
730
+ return '右侧栏服务不可用(这个宿主版本没有 sidebarRight)';
731
+ }
732
+ try {
733
+ ctx.sidebarRight.openTab(TAGS_TAB_KIND);
734
+ return '已调用 openTab(' + TAGS_TAB_KIND + ')';
735
+ } catch (error) {
736
+ return '失败:' + String(error?.message ?? error);
737
+ }
738
+ },
739
+ }),
740
+ }, GuideTagsRow)));
741
+
742
+ // tab 正文 + 标题(宿主没有 sidebarRightTabs 时这两个 seat 也没意义,但注册不抛就留着)
743
+ safe('sidebar.right.pane.tab', () => ctx.slots.inject('sidebar.right.pane.tab', () => ctx.slots.register({
744
+ name: 'sidebar.right.pane.tab',
745
+ key: TAGS_TAB_ID,
746
+ locale: NS,
747
+ }, TagsPanel)));
748
+ safe('sidebar.right.pane.tab.title', () => ctx.slots.inject('sidebar.right.pane.tab.title', () => ctx.slots.register({
749
+ name: 'sidebar.right.pane.tab.title',
750
+ key: TAGS_TAB_ID,
751
+ locale: NS,
752
+ }, TagsTitle)));
753
+ safe('main', () => ctx.slots.inject('main', () => ctx.slots.register({
754
+ name: 'main',
755
+ key: 'continuity',
756
+ }, makePanel(ctx))));
757
+ },
758
+ };
759
+ },
760
+ });