@a9i5k4/dsh-auto-memory 3.0.1 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/README.md +13 -8
  2. package/README.zh-CN.md +13 -8
  3. package/docs/HANDBOOK.md +92 -52
  4. package/docs/USER-GUIDE.en.md +9 -9
  5. package/docs/USER-GUIDE.zh-CN.md +9 -9
  6. package/docs/screenshots/promo/promo-0-banner-v4.png +0 -0
  7. package/docs/screenshots/promo/promo-1b-auto-recall.png +0 -0
  8. package/lib/activation-host.js +6 -1
  9. package/lib/client.js +1376 -255
  10. package/lib/config-io.js +59 -6
  11. package/lib/context-host.js +17 -2
  12. package/lib/episodic-store.js +90 -16
  13. package/lib/evidence-store.js +27 -1
  14. package/lib/fact-store.js +463 -41
  15. package/lib/hub-io.js +217 -0
  16. package/lib/index.js +904 -87
  17. package/lib/intent-clean-safe.js +1 -1
  18. package/lib/jsonl-tail-cursor.js +75 -0
  19. package/lib/m7-index-sync-host.js +4 -6
  20. package/lib/memory-hub.js +62 -7
  21. package/lib/migrate-pack.js +351 -0
  22. package/lib/note-status.js +9 -1
  23. package/lib/procedure-store.js +252 -31
  24. package/lib/procedure-switch.js +38 -0
  25. package/lib/python-setup.js +109 -27
  26. package/lib/python-sidecar-client.js +285 -8
  27. package/lib/recall-stats.js +242 -0
  28. package/lib/rules-layer.js +106 -15
  29. package/lib/semantic-js.js +25 -7
  30. package/lib/shadow-host.js +41 -2
  31. package/package.json +6 -2
  32. package/docs/internal/ACCEPT-35-LIVE.md +0 -143
  33. package/docs/internal/ACCEPTANCE-20260914.md +0 -90
  34. package/docs/internal/ARCH-REVIEW-BRIEF.md +0 -411
  35. package/docs/internal/ARCH-REVIEW-REQUEST.md +0 -201
  36. package/docs/internal/ARCH-REVIEW-ROUND2.md +0 -169
  37. package/docs/internal/ARCH-REVIEW-ROUND3.md +0 -206
  38. package/docs/internal/ARCHITECTURE-FOR-ZCODE-20260920.md +0 -397
  39. package/docs/internal/ART-DIRECTION-DEEPSEEK-20260920.md +0 -351
  40. package/docs/internal/ART-DIRECTION-WIREFRAME.md +0 -191
  41. package/docs/internal/ART-DIRECTION-WIREFRAME.md.bak-superseded +0 -181
  42. package/docs/internal/AUDIT-WB-GRAPH-FULL-20260916.md +0 -314
  43. package/docs/internal/BATTLE-PLAN-20260917.md +0 -871
  44. package/docs/internal/CONCURRENCY-INVESTIGATION-20260917.md +0 -192
  45. package/docs/internal/CROSS-SESSION-SEARCH-PATH-DECISION.md +0 -72
  46. package/docs/internal/CROSS-SESSION-SEARCH-RESEARCH.md +0 -131
  47. package/docs/internal/CUA-VISION-FIX-NOTES.md +0 -78
  48. package/docs/internal/DECISIONS-20260914-SESSION.md +0 -269
  49. package/docs/internal/DESIGN-OVERHAUL-PRE-RESEARCH.md +0 -292
  50. package/docs/internal/DESIGN-P1-STATE-COMMIT-20260915.md +0 -219
  51. package/docs/internal/DIRECTION-CHECK-WB-GRAPH-20260916.md +0 -132
  52. package/docs/internal/FEATURE-INVENTORY.md +0 -531
  53. package/docs/internal/FEEDBACK-TO-DSHAPI-RELAY.md +0 -13
  54. package/docs/internal/G-SERIES-EXECUTION-20260917.md +0 -248
  55. package/docs/internal/G3-DESIGN-20260918.md +0 -82
  56. package/docs/internal/G3-DISK-FORMAT-GAP-20260919.md +0 -92
  57. package/docs/internal/GH-DISCUSSION-5732-COMMENT.md +0 -74
  58. package/docs/internal/GPT-ACCEPTANCE-PROMPT-20260916.md +0 -352
  59. package/docs/internal/GPT-REVIEW-PROMPT.md +0 -216
  60. package/docs/internal/GROUP-DIGEST-SETUP.md +0 -62
  61. package/docs/internal/GROUP-LISTENER-SETUP.md +0 -49
  62. package/docs/internal/GROUP-WEBHOOK-SETUP.md +0 -93
  63. package/docs/internal/HANDOFF-TO-ZCODE-20260920.md +0 -309
  64. package/docs/internal/HANDOFF-TO-ZCODE.md +0 -168
  65. package/docs/internal/HERMES-DATA-VERIFICATION-20260919.md +0 -120
  66. package/docs/internal/HERMES-LEGACY-STATUS-20260919.md +0 -74
  67. package/docs/internal/ISSUE-55-58-VERIFICATION-20260918.md +0 -175
  68. package/docs/internal/ISSUE10-FIX-EXECUTION-20260919.md +0 -389
  69. package/docs/internal/ISSUE10-PLAN-20260919.md +0 -254
  70. package/docs/internal/ISSUE10B-FORENSICS-20260919.md +0 -468
  71. package/docs/internal/ISSUE9-PURGE-AND-R1-PLAIN-20260919.md +0 -150
  72. package/docs/internal/ISSUE9-RESIDUAL-FORENSICS-20260919.md +0 -114
  73. package/docs/internal/KICKOFF-P0.md +0 -254
  74. package/docs/internal/LESSON-TO-CANDIDATE-STATUS-20260919.md +0 -79
  75. package/docs/internal/MASTER-PLAN-3.0.md +0 -411
  76. package/docs/internal/MEMORY-GOVERNANCE-20260917.md +0 -309
  77. package/docs/internal/MEMORY-MUTATION-AND-INDEX-DESIGN.md +0 -85
  78. package/docs/internal/MERGE-CONFLICT-SCAN-20260914.md +0 -222
  79. package/docs/internal/NEXT-VERSION-TODO.md +0 -95
  80. package/docs/internal/OFFICIAL-DISCUSSION-DRAFT.md +0 -80
  81. package/docs/internal/PENDING-FIXES-20260916.md +0 -289
  82. package/docs/internal/PRE-FRONTEND-CHECKLIST-20260919.md +0 -705
  83. package/docs/internal/PRE-FRONTEND-CHECKLIST-20260919.md.bak-s10 +0 -649
  84. package/docs/internal/PROCEDURAL-MEMORY-AND-APPROVAL-DESIGN-20260918.md +0 -225
  85. package/docs/internal/PROGRESS-20260917.md +0 -93
  86. package/docs/internal/PROMPT-GAP-AUDIT-20260920.md +0 -128
  87. package/docs/internal/R1-DEGRADE-AUDIT-20260918.md +0 -163
  88. package/docs/internal/R1-READABILITY-FORENSICS-20260919.md +0 -127
  89. package/docs/internal/R2-EVIDENCE-DEEP-AUDIT-20260918.md +0 -140
  90. package/docs/internal/R3-DEGRADE-LEDGER-DESIGN-20260918.md +0 -138
  91. package/docs/internal/R4-RECALL-QUOTA-PLAN-20260918.md +0 -218
  92. package/docs/internal/RAG-KARPATHY-PROGRAM.md +0 -229
  93. package/docs/internal/RELEASE-PROCESS.md +0 -99
  94. package/docs/internal/REPORT-P0-NIGHTLY.md +0 -212
  95. package/docs/internal/REPORT-P5-ACCEPTANCE.md +0 -31
  96. package/docs/internal/REPORT-WB-GRAPH-NIGHTLY.md +0 -153
  97. package/docs/internal/RESUME-20260918.md +0 -171
  98. package/docs/internal/RESUME-20260919.md +0 -104
  99. package/docs/internal/REVIEW-WB-GRAPH-SELF.md +0 -81
  100. package/docs/internal/RHINELAB-TO-DEEPSEEK-FEASIBILITY.md +0 -198
  101. package/docs/internal/ROADMAP-20260917-WEEK.md +0 -439
  102. package/docs/internal/ROADMAP.md +0 -106
  103. package/docs/internal/RUN-P0-NIGHTLY.md +0 -227
  104. package/docs/internal/S10-CONSTRUCTION-HANDOFF-20260917.md +0 -185
  105. package/docs/internal/S10-GAP-INVENTORY-20260917.md +0 -239
  106. package/docs/internal/S10-GAPS-PLAIN-20260917.md +0 -125
  107. package/docs/internal/SEMANTIC-ARCHITECTURE-SPEC.md +0 -360
  108. package/docs/internal/SESSION-FILE-REPAIR-PROTOCOL.md +0 -90
  109. package/docs/internal/SUBAGENT-REPORT-ROUTING-PRE-RESEARCH.md +0 -261
  110. package/docs/internal/T6-EXECUTION-20260920.md +0 -130
  111. package/docs/internal/TELEMETRY-EFFECT-REPORT-DESIGN-20260918.md +0 -146
  112. package/docs/internal/THESIS-GAP-ANALYSIS-20260918.md +0 -89
  113. package/docs/internal/THESIS-OUTLINE-20260918.md +0 -147
  114. package/docs/internal/THREE-LAYER-CONTRACT.md +0 -219
  115. package/docs/internal/TODO-BACKLOG.md +0 -263
  116. package/docs/internal/TODO-GRAPH.html +0 -715
  117. package/docs/internal/TODO-GRAPH.html.bak-20260914-v2 +0 -493
  118. package/docs/internal/TODO-GRAPH.html.bak-20260915-alsfix +0 -710
  119. package/docs/internal/TODO-GRAPH.html.bak-20260915-p1 +0 -710
  120. package/docs/internal/TODO-GRAPH.html.bak-20260915-p6a-rev +0 -703
  121. package/docs/internal/TODO-GRAPH.html.bak-20260915-wshint +0 -710
  122. package/docs/internal/TODO-GRAPH.html.bak-20260916-batch +0 -715
  123. package/docs/internal/UPSTREAM-ISSUE-PR-TRIAGE-20260919.md +0 -297
  124. package/docs/internal/UPSTREAM-ISSUES-3RD-AUDIT-20260920.md +0 -104
  125. package/docs/internal/WB-FORMAT-CONVENTION.md +0 -112
  126. package/docs/internal/WB-GRAPH-DECISIONS-20260914.md +0 -71
  127. package/docs/internal/WB-GRAPH-INTEGRATION-PLAN.md +0 -386
  128. package/docs/internal/WB-GRAPH-RESEARCH-BRIEF.md +0 -118
  129. package/docs/internal/WB-GRAPH-RESEARCH-EXTERNAL.md +0 -228
  130. package/docs/internal/WB-GRAPH-RESEARCH-LOCAL.md +0 -190
  131. package/docs/internal/reviews/CLAIM-VERIFICATION-20260914.md +0 -56
  132. package/docs/internal/reviews/PLAN-gpt6astra-round2-20260914.md +0 -787
  133. package/docs/internal/reviews/REVIEW-gpt6astra-20260914.md +0 -112
  134. package/docs/internal/reviews/ROUND3-REVIEW-INTEGRATION-20260914.md +0 -230
package/lib/client.js CHANGED
@@ -7,7 +7,7 @@
7
7
  * 3. settings.section — 自动记忆设置页(存储位置、注入、反思风格)
8
8
  * Data flows over /api/dsh-auto-memory/* (loopback-only host routes).
9
9
  */
10
- console.log('[dsh-auto-memory] client v3.0.1 fingerprint: index-self-heal')
10
+ console.log('[dsh-auto-memory] client v3.1.1 fingerprint: migrate-pack-quiet-logs')
11
11
  window.__ModuleLoader__.load({
12
12
  id: '@a9i5k4/dsh-auto-memory',
13
13
  factory: (require) => {
@@ -43,6 +43,21 @@ window.__ModuleLoader__.load({
43
43
  try { pinned = localStorage.getItem(PIN_KEY) === '1' } catch (ePin) {}
44
44
  // 几何状态:left/top/width/height,持久化到 localStorage(用户可拖动/缩放)
45
45
  var GEOM_KEY = 'dsh-auto-memory.panel.geom'
46
+ // 承载面(2026-09-21 用户拍板 · 二次修正):'bottom-left'(默认,左下角浮层) | 'page'(会话页) | 'both'(两者共存)。
47
+ // ★修正原因(用户实测反馈原话):「它现在是浮在整个页面上方的,而不是和对话轨迹、上下文、白板看板在一起,作为一个单独的一页」。
48
+ // 根因:首版把「顶部」做成 shell.overlay 覆盖条 —— 宿主只给插件 4 个槽位(sidebar/main/rightbar/shell.overlay),
49
+ // 浮层永远只能"盖"在界面上,做不出"并列的一页";要成为并列页必须走 conversation.view
50
+ // (官方「对话轨迹」dsh-client-ui-trajectory 与本插件「白板看板」都用它注册)。
51
+ // 两种承载面共享同一份内容/页签/数据(见下面的 panelTab 共享状态);本机偏好,仅 localStorage。
52
+ var POS_KEY = 'dsh-auto-memory.panel.pos'
53
+ var PANEL_POS_VALUES = { 'bottom-left': 1, page: 1, both: 1 }
54
+ var panelPos = 'both'
55
+ try {
56
+ var savedPos = localStorage.getItem(POS_KEY)
57
+ // 兼容上一版:旧值 'top'(覆盖条形态)已废弃 ⇒ 自动迁移为 'page'(会话页形态),用户不必手动重选
58
+ if (savedPos === 'top') savedPos = 'page'
59
+ if (PANEL_POS_VALUES[savedPos]) panelPos = savedPos
60
+ } catch (ePos) {}
46
61
  var DEFAULT_W = 440
47
62
  var DEFAULT_H = 560
48
63
  var DEFAULT_GAP = 16
@@ -117,6 +132,16 @@ window.__ModuleLoader__.load({
117
132
  return null
118
133
  }
119
134
  function emit() { listeners.forEach(function (fn) { try { fn() } catch (e) {} }) }
135
+ // 共享页签状态(2026-09-21):浮层与「记忆」会话页读同一个模块级变量 ⇒ 两处切页互相同步
136
+ // (用户诉求原话:「两者共存…要保证同步」)。不用两份 useState,从根上杜绝状态漂移。
137
+ // 两个承载面都订阅 emit()(走 useTick),所以 setPanelTab 只要 emit 就能让两处同时重渲染。
138
+ var panelTab = 'overview'
139
+ function setPanelTab(v) { if (panelTab !== v) { panelTab = v; emit() } }
140
+ // 承载面切换钩子(2026-09-21):conversation.view 的注册/注销在 apply 作用域内(refreshSurfaces),
141
+ // 而 setPanelPos 在本模块作用域 ⇒ 用一个模块级钩子搭桥:切到「会话页/两者」时当场把页签加上,
142
+ // 切回「左下角浮层」时把它摘掉(不必等重启或重进会话)。
143
+ var surfacesRefreshHook = null
144
+ function setSurfacesRefreshHook(fn) { surfacesRefreshHook = fn }
120
145
  var controller = {
121
146
  isOpen: function () { return panelOpen },
122
147
  isClosing: function () { return panelClosing },
@@ -155,6 +180,22 @@ window.__ModuleLoader__.load({
155
180
  try { localStorage.setItem(PIN_KEY, pinned ? '1' : '0') } catch (e) {}
156
181
  emit()
157
182
  },
183
+ // 承载面(2026-09-21):左下角浮层 / 会话页 / 两者共存。设置页改动立即生效(即时回显)。
184
+ panelPos: function () { return panelPos },
185
+ setPanelPos: function (v) {
186
+ panelPos = PANEL_POS_VALUES[v] ? v : 'both'
187
+ try { localStorage.setItem(POS_KEY, panelPos) } catch (e) {}
188
+ // 承载面切换要即时增删「记忆」会话页签(见 setSurfacesRefreshHook 的说明)
189
+ try { if (surfacesRefreshHook) surfacesRefreshHook() } catch (eRf) {}
190
+ // 即时回显(2026-09-21 用户实测:「切到两者共存后上面什么都没出现」):
191
+ // 位置改在设置页里,而面板若关着,整个 MemoryPanel 渲染 null ⇒ 用户看不到任何变化。
192
+ // 故切档时面板若关着就自动打开,让新位置当场可见(切回旧档同样打开,保证每次切换都可感知)。
193
+ if (!panelOpen) controller.open()
194
+ else emit()
195
+ },
196
+ // 共享页签(浮层 ↔ 会话页)
197
+ panelTab: function () { return panelTab },
198
+ setPanelTab: setPanelTab,
158
199
  }
159
200
 
160
201
  // ───────────────────────── 国际化 ─────────────────────────
@@ -164,10 +205,41 @@ window.__ModuleLoader__.load({
164
205
  memoryPanel: '记忆面板',
165
206
  memory: '记忆',
166
207
  autoMemory: '自动记忆',
167
- overview: '概览', logs: '日志', refineTab: '唤起回顾', notes: '笔记', reflections: '反思', connect: '接续', calendar: '日历', search: '检索', workspaces: '工作区', hubTab: '记忆中枢', storageTab: '存储管理', planTab: '白板', planTitle: '白板 · PLAN.md(项目全貌)', planEmpty: '(白板还是空的——模型理解项目全貌后会自动写;也可在对话中让它用 memory_note(kind=plan) 写)', planVersions: '白板历史版本', planLedgers: '交接账本时间线', planDisabled: '交接白板未启用(设置 → 自动化)。', planWsUnbound: '当前会话未绑定工作区——白板与交接账本暂不可用(会话的工作区身份解析不出来;把会话开进项目文件夹或绑定工作区后自动恢复)。', planFileTitle: '查看',
208
+ overview: '概览',
209
+ statsTab: '统计',
210
+ statsTitle: '召回统计(谁在被想起来)',
211
+ statsSubtitle: '只统计、不改排序——数据攒够后再决定要不要加权。',
212
+ statsQueries: '检索次数',
213
+ statsZeroHit: '零命中',
214
+ statsDistinct: '被召回条目',
215
+ statsTotalHits: '命中总次数',
216
+ statsWarm: '只被召回一次',
217
+ statsByLayer: '按记忆层分布',
218
+ statsByDay: '按天走势',
219
+ statsSources: '按通路分布',
220
+ statsTop: '召回最多的条目',
221
+ statsEmpty: '(还没有统计数据——用 memory_recall 检索几次后再回来看)',
222
+ statsReset: '清零统计',
223
+ statsResetDone: '已清零',
224
+ statsChannels: '三条通路(分开统计,含义不同)',
225
+ statsChOverview: '各通路占多少',
226
+ statsChModel: '① 模型主动检索',
227
+ statsChModelHint: '模型明确想找 → 高信号:哪些内容真的被需要',
228
+ statsChInject: '② 每轮自动注入',
229
+ statsChInjectHint: '系统一直在塞 → 成本视角:注入预算花在哪一段',
230
+ statsChShadow: '③ 主动唤起',
231
+ statsChShadowHint: '系统判断该唤起 → 质量视角:唤起了但没命中的比例',
232
+ statsEvents: '发生次数',
233
+ statsNoInject: '还没有注入记录(宿主重启后开始累积)',
234
+ statsSegChars: '按字符数(谁最占预算)',
235
+ statsSegCount: '按出现次数',
236
+ statsSince: '开始记录于',
237
+ statsNever: '从未', logs: '日志', refineTab: '唤起回顾', notes: '笔记', reflections: '反思', connect: '接续', calendar: '日历', search: '检索', workspaces: '工作区', hubTab: '记忆中枢', storageTab: '存储管理', planTab: '白板', planTitle: '白板 · PLAN.md(项目全貌)', planEmpty: '(白板尚未建立——宿主会在该工作区第一次会话结束时自动落一版骨架,模型理解项目全貌后把它重写成真内容;也可在对话中让它用 memory_note(kind=plan) 写)', planVersions: '白板历史版本', planLedgers: '交接账本时间线', planDisabled: '交接白板未启用(设置 → 自动化)。', planWsUnbound: '当前会话未绑定工作区——白板与交接账本暂不可用(会话的工作区身份解析不出来;把会话开进项目文件夹或绑定工作区后自动恢复)。', planFileTitle: '查看',
168
238
  hubSkills: '技能 (Procedural)', hubSkillsEmpty: '暂无已固化的技能。反复成功的流程会自动固化为技能并在相似场景召回。', hubFacts: '事实 (Semantic)', hubFactsEmpty: '暂无固化的事实。', hubConflicts: '待决冲突', hubEpisodic: '经历 (Episodic)', hubEpisodicEmpty: '暂无已巩固的经历。',
169
239
  storageScanHint: '语料健康 = 逐源比对索引(sidecar)与正文的 digest。手动改动记忆文件后索引会失配,该记忆会退出检索直到重建索引。',
170
240
  storageDeleteHint: '删除记忆 = 正文原子删除 + 在途唤起包清理 + 派生事实撤销(三联动)。已产生的 seen 证据不改写。',
241
+ migTitle: '迁移搬包', migHint: '把某个工作区的记忆打包带走,到另一台机器导入即可(路径变了会自动重写内部引用)。', migExport: '导出本工作区', migImport: '导入包', migPickPack: '选择 .dam-pack 文件', migPreview: '预览差异(不改任何文件)', migApply: '确认导入',
242
+ migPathChanged: '路径变化:将重写内部引用', migSamePath: '路径相同:不做任何重写', migConflictKeep: '冲突时保留本机', migConflictOverwrite: '冲突时用包内版本覆盖', migConflictRename: '冲突时改名为 .from-pack', migBackup: '已备份到', migWritten: '已写入文件数',
171
243
  secSemantic: '自动记忆引擎', semMode: '检索模式', semAuto: '自动(推荐)', semLexOnly: '仅词法', semJs: '内置语义', semPy: '高级 Python',
172
244
  pyWizTitle: 'Python 引擎一键安装', pyWizStep1: '① 检测 Python 环境', pyWizStep2: '② 创建独立虚拟环境(~/.dsh/python-engine/.venv,不污染系统)', pyWizStep3: '③ 安装引擎依赖(transformers + onnxruntime + torch)', pyWizStep4: '④ 下载 BGE-M3 int8 模型(~539MB,断点续传,失败自动换源)',
173
245
  pyWizDetect: '开始检测', pyWizRedetect: '重新检测', pyWizCreate: '创建虚拟环境', pyWizInstall: '安装依赖', pyWizDownload: '开始下载', pyWizCancel: '取消下载', pyWizDone: '全部就绪 ✓ — 回上方启用即可', pyWizOk: 'ok', pyWizMissing: '未找到', pyWizTooOld: '版本不兼容(需 3.9-3.12)', pyWizVenvRec: '推荐', pyWizEta: '剩余', pyWizSec: '秒', pyWizModelHint: '模型与虚拟环境安装在 ~/.dsh/python-engine/(用户目录),升级/重装插件不受影响。',
@@ -216,6 +288,13 @@ window.__ModuleLoader__.load({
216
288
  kvAvail: '可用', kvUnavail: '不可用', kvDup: '项目笔记重复标题', kvDupCount: ' 个',
217
289
  kvMem: '记忆文件', kvMemUser: '用户', kvMemNotes: '笔记', kvMemLog: '日志', kvMemMissing: '缺失',
218
290
  kvWs: '当前工作区', kvWsUnknown: '(未取到)', kvApi: 'API 连通', unknown: '(未知)',
291
+ // ★P0-2/P0-3(2026-09-22):把后端早已算好、前端零消费者的两组可观测性接到界面上。
292
+ dbgHubIo: '三层记忆落盘', dbgPrune: '事实淘汰(保留上限)',
293
+ dbgLogs: '诊断日志',
294
+ dbgLogsNone: '尚未生成(本次运行还没有诊断输出)',
295
+ dbgLogsHint: '崩溃/异常时请把下面路径的文件附给维护者(只读展示,不含日志正文)',
296
+ kvPyTitle: 'Python 语义侧车', kvPyOff: '未启用(JS 端语义不受影响)', kvPyAlive: '运行中',
297
+ kvPyWatchdog: '看门狗击杀 / 重生', kvPyStderr: '最近 stderr 尾部', kvPyStderrTrunc: '(已截断)',
219
298
  updateTitle: 'dsh-auto-memory 已更新', updateSub: '本次更新内容:', gotIt: '知道了', noticeOpen: '了解更多',
220
299
  refresh: '刷新', refreshing: '刷新中…', close: '关闭', dragMove: '拖动移动', dragResize: '拖动调整大小', resetPos: '恢复默认位置', pinPanel: '钉住面板(点桌面其他地方不收起)', unpinPanel: '取消钉住(点面板外自动收起)',
221
300
  generated: '已生成', failed: '失败: ',
@@ -251,8 +330,13 @@ window.__ModuleLoader__.load({
251
330
  hubWhyCorrectionRate: function (r, cap) { return '它的纠正率是 ' + r + ',超过了上限 ' + cap + ' —— 说明这套流程本身有问题,需要先改掉。' },
252
331
  hubWhyHasCorrection: function (n) { return '它被纠正过 ' + n + ' 次 —— 有纠正记录说明这流程是错的,不能固化。' },
253
332
  hubWhyNoCriteria: '它没写「怎么算成功」—— 没有验收标准的流程不算技能。',
254
- hubWhyHighRisk: '它是高风险操作,需要你确认后才会固化。',
333
+ hubWhyHighRisk: '它是高风险操作,需要你人工确认才会固化 —— 点下面这个「批准(人工)」即可。',
255
334
  hubWhyObservationOnly: '它只是「看到过一件事」的线索,不是真正的流程 —— 不会自动晋升。',
335
+ // ★C(2026-09-22):三态说清 —— 「没有按钮」必须自己解释清楚,而不是让用户猜。
336
+ hubWhyStructural: '这道门是结构性的(内容本身不构成一条技能),人工也不能越过;补齐内容才会重新判定。',
337
+ hubWhyOverridable: '以上只是「证据还没攒够」的统计门,不是内容不合格 —— 你可以点右侧按钮人工越过。',
338
+ hubForcePromote: '强制晋升(人工)',
339
+ hubForcePromoteTitle: '人工越过跨会话数/成功次数两道统计门。结构门不会越过,且会在条目上留下「人工授权」痕迹。',
256
340
  hubEvSeen: '见过', hubEvSuccess: '成功', hubEvSessions: '跨会话', hubEvReuse: '被复用', hubEvCorrection: '被纠正',
257
341
  hubEvLine: function (ev) { return '见过 ' + (ev.seen || 0) + ' 次 · 成功 ' + (ev.success || 0) + ' 次 · 在 ' + (ev.sessions || 0) + ' 个会话里用过' + ((ev.correction || 0) ? ' · 被纠正过 ' + ev.correction + ' 次' : '') },
258
342
  hubInjectPreview: '晋升后会注入这些(点开看)',
@@ -293,6 +377,8 @@ window.__ModuleLoader__.load({
293
377
  saved: '已保存',
294
378
  settingsHeader: '记忆存储与行为设置(保存到 DSH 主目录 dsh-auto-memory.json):',
295
379
  fFontSize: '界面字号', fFontSizeHint: '记忆面板文字大小(立即生效,仅本机)', fsSm: '小', fsMd: '标准', fsLg: '大', fsXl: '特大',
380
+ fPanelPos: '记忆承载面', fPanelPosHint: '记忆内容的呈现方式:左下角浮层(可拖动缩放)/ 会话页(与「对话轨迹」「上下文」并列的一页,在会话页顶栏切换)/ 两者共存。两种承载面共享同一份内容与页签,实时同步;仅本机生效。',
381
+ posBottomLeft: '左下角(浮层)', posPage: '会话页(与对话轨迹并列)', posBoth: '两者共存',
296
382
  fUserDir: '用户记忆目录', fUserDirHint: '跨项目规则存放处,支持 ~ 开头;需有文件写权限。',
297
383
  fProjectDir: '项目记忆目录', fProjectDirHint: '相对各工作区的目录名(默认 .dsh-memory)。',
298
384
  fInject: '注入记忆上下文', fInjectHint: '每次组装提示词时自动注入 <memory_system> 块。',
@@ -303,6 +389,7 @@ window.__ModuleLoader__.load({
303
389
  fSnapGap: '快照最小间隔(轮)', fSnapGapHint: '动态记忆快照内容变化后,至少隔 N 轮才重新注入,避免每轮日志微变都追加快照导致历史膨胀。默认 5;0=每轮都尝试(仍受内容变化约束)。',
304
390
  fReinjectOnCompact: '压缩后立即重注入快照', fReinjectOnCompactHint: '上下文被压缩/截断(通常伴随 contextVersion 重置)后,快照会被清掉;开启后强制立即重注入一次,确保记忆背景重建。默认开。',
305
391
  fPromptCustom: '自定义记忆注入 prompt', fPromptCustomHint: '可覆盖各层提示文案(小众功能)。支持占位符 {date} {ws} {budget} {n}。改坏了可一键恢复默认。',
392
+ fPromptSections: '高级:逐段注入控制…', fPromptSectionsHint: '控制每一轮注入里包含哪些段落。默认全部开启;不推荐修改 —— 关掉某段会让模型少掉对应的上下文或纪律。',
306
393
  fDays: '注入最近日志天数', fDaysHint: '会话开始时注入最近 N 天的工作日志尾部。默认 1。',
307
394
  fExtBudget: '外部记忆注入预算(字符)', fExtBudgetHint: '外部记忆来源在上下文中的注入预算。默认 1400(路径模式下影响有限)。',
308
395
  fConsolidateMin: '自动沉淀内容门槛(字符)', fConsolidateMinHint: '本轮 user+assistant 总字符低于此值视为寒暄跳过。默认 240。',
@@ -325,6 +412,14 @@ window.__ModuleLoader__.load({
325
412
  fWaterAuto: '水位自动骨架账本', fWaterAutoHint: '越阈时自动写一篇系统骨架账本(每会话一次),防止模型忽视建议时交接材料缺失。',
326
413
  fWaterThreshold: '水位建议阈值', fWaterThresholdHint: '水位比例 = 上下文实占 ÷ 官方声明窗口(不再扣预留输出);超过该值(0.1-1.5)即触发交接建议/骨架账本;默认 0.75——官方自动压缩阈值是 80%,必须留出余量才来得及走完交接。',
327
414
  fHandoffPlan: '白板注入预算(字符)', fHandoffPlanHint: 'PLAN.md 全貌注入动态快照的硬截断预算;全文可经 memory_read 或面板白板页查看。默认 1200。',
415
+ fProcInject: '流程记忆注入(总闸)', fProcInjectHint: '★流程记忆(技能固化)的总闸:关闭后既不写入流程记忆,也不把它注入提示词。默认开。注意:设置页此前的旧键「技能固化与晋升」宿主已不再读取,要改请改这一项。',
416
+ fFactRetention: '事实条数上限', fFactRetentionHint: '事实层最多保留多少条,超出时自动淘汰:先淘汰已撤销的,再按写入时间从旧到新,重要项(置顶 / 用户级 / 显式写入 / 高置信度)最后淘汰。默认 1000。',
417
+ fTier0Catalog: '目录层(Tier-0 摘要)', fTier0CatalogHint: '开启时每轮注入一段「记忆目录」摘要(条目名 + 一句话结论),让模型先看到全局再按需下探;关闭则完全不注入目录层,省 token。默认开。',
418
+ fTier0Max: '目录层 token 上限', fTier0MaxHint: '目录层单次注入的 token 硬上限(与「注入预算 × 目录层占比」取小生效)。默认 400。需要装下更多语料时提高这里,而不是把注入预算整体推高。',
419
+ fRulesLayering: '规则段分层', fRulesLayeringHint: '开启后规则类记忆单独成段、每轮在场且不参与裁剪(旧行为是把规则混进记忆流一起裁)。关闭 = 完全回到旧行为。默认开。',
420
+ fCriteriaGate: '账本判据门', fCriteriaGateHint: '开启时白板/账本小节必须满足判据(H1-H4 / S1-S4)才被采纳,防止把思考过程当结论固化。关闭只退掉这道质量门,骨架 fail-soft 仍无条件生效。默认开。',
421
+ fWsDiscover: '工作区发现上限', fWsDiscoverHint: '扫描本机会话时最多识别多少个工作区(按最近会话时间降序取前 N 个)。默认 200;机器上历史工作区很多时可调小以缩短扫描时间。',
422
+ fMemFileIndex: '记忆文件索引快照', fMemFileIndexHint: '开启后额外维护一份只读的记忆文件索引快照(供外部查询),有少量 IO 开销;默认关,关时零额外开销。',
328
423
  fHandoffLedger: '账本注入预算(字符)', fHandoffLedgerHint: '最新一篇交接账本注入动态快照的硬截断预算。默认 800。',
329
424
  fConsSchedule: '定时做梦式固化', fConsScheduleHint: '每天到点自动读最近日志发散提炼长期要点,写入项目笔记/用户级记忆(等价 memory_consolidate)。',
330
425
  fConsScheduleTime: '固化触发时间', fConsScheduleTimeHint: 'HH:MM,插件日界内每天一次;默认 09:30。命中时刻需宿主在线。',
@@ -352,10 +447,41 @@ window.__ModuleLoader__.load({
352
447
  memoryPanel: 'Memory Panel',
353
448
  memory: 'Memory',
354
449
  autoMemory: 'Auto Memory',
355
- overview: 'Overview', logs: 'Logs', refineTab: 'Recall review', notes: 'Notes', reflections: 'Reflections', connect: 'Connect', calendar: 'Calendar', search: 'Search', workspaces: 'Workspaces', hubTab: 'Memory Hub', storageTab: 'Storage', planTab: 'Whiteboard', planTitle: 'Whiteboard · PLAN.md (project plan)', planEmpty: '(empty — the model fills this in once it grasps the full picture; or ask it to write memory_note kind=plan)', planVersions: 'Plan versions', planLedgers: 'Handoff ledger timeline', planDisabled: 'Handoff whiteboard is disabled (Settings → Automation).', planWsUnbound: 'This session has no bound workspace — whiteboard and handoff ledgers are unavailable (the session workspace could not be resolved; open the session inside a project folder or bind a workspace to restore).', planFileTitle: 'View',
450
+ overview: 'Overview',
451
+ statsTab: 'Stats',
452
+ statsTitle: 'Recall statistics (what gets remembered)',
453
+ statsSubtitle: 'Recording only — ranking untouched. Weighting comes after the data proves the metric.',
454
+ statsQueries: 'Queries',
455
+ statsZeroHit: 'Zero-hit',
456
+ statsDistinct: 'Distinct recalled',
457
+ statsTotalHits: 'Total hits',
458
+ statsWarm: 'Recalled once',
459
+ statsByLayer: 'By layer',
460
+ statsByDay: 'Per day',
461
+ statsSources: 'By channel',
462
+ statsTop: 'Most recalled',
463
+ statsEmpty: '(no data yet — run memory_recall a few times and come back)',
464
+ statsReset: 'Reset stats',
465
+ statsResetDone: 'Cleared',
466
+ statsChannels: 'Three channels (counted separately — they mean different things)',
467
+ statsChOverview: 'Share per channel',
468
+ statsChModel: '1) Model-initiated recall',
469
+ statsChModelHint: 'The model explicitly looked for it -> high signal: what is actually needed',
470
+ statsChInject: '2) Auto-injected every turn',
471
+ statsChInjectHint: 'The system keeps feeding it -> cost view: which section eats the budget',
472
+ statsChShadow: '3) Proactive shadow recall',
473
+ statsChShadowHint: 'System judged it worth surfacing -> quality view: how often it came up empty',
474
+ statsEvents: 'Events',
475
+ statsNoInject: 'No injection recorded yet (accumulates after host restart)',
476
+ statsSegChars: 'By characters (who eats the budget)',
477
+ statsSegCount: 'By occurrences',
478
+ statsSince: 'Recording since',
479
+ statsNever: 'never', logs: 'Logs', refineTab: 'Recall review', notes: 'Notes', reflections: 'Reflections', connect: 'Connect', calendar: 'Calendar', search: 'Search', workspaces: 'Workspaces', hubTab: 'Memory Hub', storageTab: 'Storage', planTab: 'Whiteboard', planTitle: 'Whiteboard · PLAN.md (project plan)', planEmpty: '(not created yet — the host seeds a skeleton board when a turn first ends in this workspace; the model rewrites it with real content once it grasps the picture, or ask it to write memory_note kind=plan)', planVersions: 'Plan versions', planLedgers: 'Handoff ledger timeline', planDisabled: 'Handoff whiteboard is disabled (Settings → Automation).', planWsUnbound: 'This session has no bound workspace — whiteboard and handoff ledgers are unavailable (the session workspace could not be resolved; open the session inside a project folder or bind a workspace to restore).', planFileTitle: 'View',
356
480
  hubSkills: 'Skills (Procedural)', hubSkillsEmpty: 'No solidified skills yet. Repeatedly-successful workflows become skills and are recalled in similar contexts.', hubFacts: 'Facts (Semantic)', hubFactsEmpty: 'No solidified facts yet.', hubConflicts: 'Pending conflicts', hubEpisodic: 'Episodes (Episodic)', hubEpisodicEmpty: 'No consolidated episodes yet.',
357
481
  storageScanHint: 'Corpus health = compare each source\'s index (sidecar) against its body digest. After you hand-edit a memory file the index no longer matches, and that memory drops out of retrieval until the index is rebuilt.',
358
482
  storageDeleteHint: 'Delete = atomic body removal + in-flight activation purge + derived-fact revocation (cascading). Evidence already recorded (seen) is never rewritten.',
483
+ migTitle: 'Migrate pack', migHint: 'Pack one workspace memory and carry it to another machine; when the path changes, internal references are rewritten automatically.', migExport: 'Export this workspace', migImport: 'Import pack', migPickPack: 'Pick a .dam-pack file', migPreview: 'Preview diff (writes nothing)', migApply: 'Confirm import',
484
+ migPathChanged: 'Path changed: internal references will be rewritten', migSamePath: 'Same path: no rewrite at all', migConflictKeep: 'On conflict: keep this machine', migConflictOverwrite: 'On conflict: overwrite with pack', migConflictRename: 'On conflict: rename to .from-pack', migBackup: 'Backed up to', migWritten: 'Files written',
359
485
  secSemantic: 'Semantic engine', semMode: 'Retrieval mode', semAuto: 'Auto (recommended)', semLexOnly: 'Lexical only', semJs: 'Built-in semantic', semPy: 'Advanced Python',
360
486
  pyWizTitle: 'One-click Python engine setup', pyWizStep1: '1. Detect Python environment', pyWizStep2: '2. Create isolated venv (~/.dsh/python-engine/.venv, system untouched)', pyWizStep3: '3. Install engine deps (transformers + onnxruntime + torch)', pyWizStep4: '4. Download BGE-M3 int8 model (~539MB, resumable, auto mirror failover)',
361
487
  pyWizDetect: 'Detect', pyWizRedetect: 'Re-detect', pyWizCreate: 'Create venv', pyWizInstall: 'Install deps', pyWizDownload: 'Start download', pyWizCancel: 'Cancel download', pyWizDone: 'All ready - enable it above', pyWizOk: 'ok', pyWizMissing: 'not found', pyWizTooOld: 'incompatible (need 3.9-3.12)', pyWizVenvRec: 'recommended', pyWizEta: 'eta', pyWizSec: 's', pyWizModelHint: 'Model and venv live in ~/.dsh/python-engine/ (user dir) - plugin upgrades never touch them.',
@@ -403,6 +529,12 @@ window.__ModuleLoader__.load({
403
529
  kvAvail: 'available', kvUnavail: 'unavailable', kvDup: 'Duplicate note headings', kvDupCount: '',
404
530
  kvMem: 'Memory files', kvMemUser: 'user', kvMemNotes: 'notes', kvMemLog: 'log', kvMemMissing: 'missing',
405
531
  kvWs: 'Current workspace', kvWsUnknown: '(unknown)', kvApi: 'API probes', unknown: '(unknown)',
532
+ dbgHubIo: 'Layer-3 memory persistence', dbgPrune: 'Fact eviction (retention cap)',
533
+ dbgLogs: 'Diagnostic log',
534
+ dbgLogsNone: 'not created yet (no diagnostic output this run)',
535
+ dbgLogsHint: 'On a crash/exception, attach the file at the path below (read-only; log body not shown)',
536
+ kvPyTitle: 'Python sidecar', kvPyOff: 'disabled (JS-side semantics unaffected)', kvPyAlive: 'running',
537
+ kvPyWatchdog: 'Watchdog kills / respawns', kvPyStderr: 'Last stderr tail', kvPyStderrTrunc: ' (truncated)',
406
538
  updateTitle: 'dsh-auto-memory Updated', updateSub: 'What\'s new in this update:', gotIt: 'Got it', noticeOpen: 'Learn more',
407
539
  refresh: 'Refresh', refreshing: 'Refreshing…', close: 'Close', dragMove: 'Drag to move', dragResize: 'Drag to resize', resetPos: 'Reset position', pinPanel: 'Pin panel (stays open when clicking elsewhere)', unpinPanel: 'Unpin (auto-hide on outside click)',
408
540
  generated: 'Generated', failed: 'Failed: ',
@@ -436,8 +568,12 @@ window.__ModuleLoader__.load({
436
568
  hubWhyCorrectionRate: function (r, cap) { return 'Correction rate ' + r + ' exceeds cap ' + cap + '.' },
437
569
  hubWhyHasCorrection: function (n) { return 'Has ' + n + ' correction(s) — a corrected flow must not be crystallized.' },
438
570
  hubWhyNoCriteria: 'No success criteria — a flow without acceptance criteria is not a skill.',
439
- hubWhyHighRisk: 'High risk — needs your confirmation before crystallizing.',
571
+ hubWhyHighRisk: 'High risk — it needs your manual approval. Click "approve (manual)" below to crystallize it.',
440
572
  hubWhyObservationOnly: 'This is an observation, not a real flow — it never auto-promotes.',
573
+ hubWhyStructural: 'This gate is structural (the entry is not a skill yet) — a human cannot override it either. Fill in the content and it gets re-evaluated.',
574
+ hubWhyOverridable: 'The above is only a statistical gate (not enough evidence yet), not a content failure — you can override it with the button on the right.',
575
+ hubForcePromote: 'Force promote (manual)',
576
+ hubForcePromoteTitle: 'Manually bypass the cross-session / success-count statistical gates. Structural gates are NOT bypassed, and a "manual authorization" trace is left on the entry.',
441
577
  hubEvSeen: 'seen', hubEvSuccess: 'success', hubEvSessions: 'sessions', hubEvReuse: 'reused', hubEvCorrection: 'corrected',
442
578
  hubEvLine: function (ev) { return 'seen ' + (ev.seen || 0) + ' · success ' + (ev.success || 0) + ' · used in ' + (ev.sessions || 0) + ' session(s)' + ((ev.correction || 0) ? ' · corrected ' + ev.correction + 'x' : '') },
443
579
  hubInjectPreview: 'This is what gets injected once promoted',
@@ -478,6 +614,8 @@ window.__ModuleLoader__.load({
478
614
  saved: 'Saved',
479
615
  settingsHeader: 'Memory storage & behavior (saved to ~/.dsh/dsh-auto-memory.json):',
480
616
  fFontSize: 'Panel font size', fFontSizeHint: 'Memory panel text size (applies immediately, this device only)', fsSm: 'Small', fsMd: 'Normal', fsLg: 'Large', fsXl: 'Extra large',
617
+ fPanelPos: 'Memory surface', fPanelPosHint: 'How memory content is presented: bottom-left floating card (draggable/resizable), a session page (a tab alongside "Trajectory"/"Context" in the conversation header), or both. Both surfaces share the same content and tabs, live-synced; this device only.',
618
+ posBottomLeft: 'Bottom-left (floating)', posPage: 'Session page (alongside Trajectory)', posBoth: 'Both, side by side',
481
619
  fUserDir: 'User memory dir', fUserDirHint: 'Cross-project rules; supports ~ prefix; needs write permission.',
482
620
  fProjectDir: 'Project memory dir', fProjectDirHint: 'Directory name relative to each workspace (default .dsh-memory).',
483
621
  fInject: 'Inject memory context', fInjectHint: 'Auto-inject <memory_system> block into every prompt.',
@@ -488,6 +626,7 @@ window.__ModuleLoader__.load({
488
626
  fSnapGap: 'Snapshot min gap (turns)', fSnapGapHint: 'After the dynamic memory snapshot changes, re-inject at least N turns later, so small per-turn log changes do not append a new snapshot every turn (history bloat). Default 5; 0 = try every turn (still change-gated).',
489
627
  fReinjectOnCompact: 'Re-inject snapshot immediately after compaction', fReinjectOnCompactHint: 'When the context is compacted/truncated (usually a contextVersion reset), the snapshot is cleared; on = force re-inject once so the memory background rebuilds. Default on.',
490
628
  fPromptCustom: 'Customize memory-injection prompt', fPromptCustomHint: 'Override any prompt layer (power feature). Placeholders: {date} {ws} {budget} {n}. One-click reset restores defaults.',
629
+ fPromptSections: 'Advanced: per-section injection…', fPromptSectionsHint: 'Controls which sections each round injects. All on by default; not recommended to change — turning one off drops the matching context or discipline.',
491
630
  fDays: 'Recent days injected', fDaysHint: 'Inject tails of the last N days of work logs at session start. Default 1.',
492
631
  fExtBudget: 'External memory injection budget (chars)', fExtBudgetHint: 'Budget for external memory sources in context. Default 1400 (limited effect with path mode).',
493
632
  fConsolidateMin: 'Auto-consolidation content threshold (chars)', fConsolidateMinHint: 'Turns with fewer combined user+assistant chars are treated as chit-chat and skipped. Default 240.',
@@ -510,6 +649,14 @@ window.__ModuleLoader__.load({
510
649
  fWaterAuto: 'Auto skeleton ledger', fWaterAutoHint: 'Past the threshold, auto-writes one system skeleton ledger per session as a safety net if the model ignores the advisory.',
511
650
  fWaterThreshold: 'Water-level threshold', fWaterThresholdHint: 'The ratio counts session tokens against the official declared window (the reserved output budget is no longer deducted); past this value (0.1-1.5) the handoff advisory / skeleton ledger fires; default 0.75 — the official auto-compaction threshold is 80%, so a 5% margin is needed to finish the handoff first.',
512
651
  fHandoffPlan: 'Plan injection budget (chars)', fHandoffPlanHint: 'Hard truncation budget for injecting the PLAN.md snapshot into the dynamic memory; full text via memory_read or the Whiteboard tab. Default 1200.',
652
+ fProcInject: 'Procedure memory injection (master switch)', fProcInjectHint: 'Master switch for procedure memory (skill crystallization): when off, none is written and none is injected. On by default. The older "Skill crystallization" key is no longer read by the host, so change this one instead.',
653
+ fFactRetention: 'Fact retention limit', fFactRetentionHint: 'Maximum facts kept; when exceeded, revoked facts are pruned first, then oldest-first by write order, and important ones (pinned / user-level / explicit / high-confidence) last. Default 1000.',
654
+ fTier0Catalog: 'Tier-0 catalog layer', fTier0CatalogHint: 'When on, a short memory catalog (entry titles plus a one-line takeaway) is injected every round so the model sees the whole picture before drilling down. Off injects no catalog layer at all, saving tokens. On by default.',
655
+ fTier0Max: 'Tier-0 token cap', fTier0MaxHint: 'Hard per-injection token cap for the catalog layer, taking the minimum with (injection budget x catalog share). Default 400. Raise this rather than the whole injection budget when more corpus must fit.',
656
+ fRulesLayering: 'Layered rules section', fRulesLayeringHint: 'When on, rule-type memories form their own section that is always present and never trimmed (the legacy behavior mixed rules into the trimmable memory stream). Off is exactly the legacy behavior. On by default.',
657
+ fCriteriaGate: 'Ledger criteria gate', fCriteriaGateHint: 'When on, whiteboard/ledger sections must satisfy the criteria (H1-H4 / S1-S4) before being accepted, which prevents storing reasoning as conclusions. Off drops only this quality gate; the skeleton fail-soft path always applies. On by default.',
658
+ fWsDiscover: 'Workspace discovery cap', fWsDiscoverHint: 'Maximum workspaces identified when scanning local sessions (newest session time first). Default 200; lower it to speed up scanning when many historical workspaces exist.',
659
+ fMemFileIndex: 'Memory file index snapshot', fMemFileIndexHint: 'When on, an extra read-only index snapshot of memory files is maintained for external queries, at a small IO cost. Off by default, and off means zero overhead.',
513
660
  fHandoffLedger: 'Ledger injection budget (chars)', fHandoffLedgerHint: 'Hard truncation budget for injecting the latest handoff ledger. Default 800.',
514
661
  fConsSchedule: 'Scheduled deep consolidation', fConsScheduleHint: 'Once a day at the set time, read recent logs and distill long-term value into project notes / user memory (same flow as memory_consolidate).',
515
662
  fConsScheduleTime: 'Consolidation time', fConsScheduleTimeHint: 'HH:MM, once per plugin day; default 09:30. The host must be online at that moment.',
@@ -626,7 +773,7 @@ window.__ModuleLoader__.load({
626
773
  + '【收尾自检】本轮有实质产出才做;纯只读/闲聊轮跳过。三个方向 + 分类别丢:\n'
627
774
  + '① 写记忆文件 memory_log —— kind 按性质选,**不要一律 fact**:rule=用户约束/约定(会被规则层当硬约束注入)、preference=偏好、fact=事实记录、todo=待办;\n'
628
775
  + '② 更新白板与账本 —— ★**硬映射(满足即必须做,别自行判断"算不算"变化)**:\n · 本轮**改过 lib/ 下任何文件**(含测试/工具) ⇒ 必写 **memory_note(kind=handoff)** 四段式账本;\n · 白板"当前进度"与本轮结束时的**事实不符**(回归数字/已完成项/下一步) ⇒ 必做 **memory_note(kind=plan)** 重写白板;\n · 仅补充一条可复用结论 ⇒ 才用 **memory_note(kind=note, action=append)**。\n ⚠️ **kind=note 不等于白板/账本**——只写 note 却宣称"已更新白板与账本"是**失职**;write 直接写 docs/ 的 md 也**不算**插件记忆。\n'
629
- + '③ 长期记忆判断(三个去处)—— 跨会话有用的 → memory_note / 跨项目规则 → memory_user / **跑通且可复用的多步流程 → memory_procedure**(技能库唯一模型入口,不写就没有;建议填 successCriteria);\n'
776
+ + '③ 长期记忆判断(三个去处)—— 跨会话有用的 → memory_note / 跨项目规则 → memory_user / **规则条目增删改(用户级硬约束,含过时条目真删)→ memory_rules** / **跑通且可复用的多步流程 → memory_procedure**(技能库唯一模型入口,不写就没有;建议填 successCriteria);\n'
630
777
  + '④ 结论失效/被取代 —— 传 `supersedes=` 旧条目 mem_id 标 **superseded(被更新结论取代)**;**当时就做错了要撤回**则传 `retract=` 标 **retracted(撤回)**并尽量附 `retractReason=` 说明错在哪(两者不同:前者有后继,后者本身就是教训);标错了用 `restore=` 撤回;\n'
631
778
  + '⑤ 看板落列 —— 白板/账本内容要进面板看板泳道,须在标题或正文写 tag:`type:goal` / `type:state` / `type:dead-end` / `type:progress`(5 条泳道含「版本归档」,靠归档动作而非 tag);不写 tag 系统只能按标题文字猜,常落空;\n'
632
779
  + '⑥ 语体 —— 客观陈述、第三人称,只留可复用的事实/决策/规则/路径(不写"我考虑/我排查/我想")。',
@@ -659,6 +806,44 @@ window.__ModuleLoader__.load({
659
806
 
660
807
  // ───────────────────────── 更新弹窗 / 首次指导 ─────────────────────────
661
808
  var CHANGELOG = {
809
+ '3.1.1': { zh: [
810
+ '新增:记忆搬包 —— 把某个工作区的记忆整体搬到另一台机器。「存储管理」页签新增入口:导出本工作区得到 .dam-pack,到新机器导入。',
811
+ '导入是三步向导:选包 → 预览差异(只读,不动任何文件) → 确认导入。预览会列出路径是否变化、新增/覆盖几个文件、内部引用要重写多少处。',
812
+ '换机器会自动重写内部路径:包内指向旧路径/旧 slug 的引用(含正文里的路径字样)按 4 种写法逐一替换成新机器真实路径;路径相同则一个字都不改。',
813
+ '冲突默认保留本机(不覆盖);也可选覆盖或改名为 .from-pack 另存。四道底线:导入前整体备份(失败即中止)、sha256 校验不过即拒、拒绝路径穿越、workspaces-summary.json 走合并(不冲掉你其它工作区)。',
814
+ '修复:PowerShell/控制台不再被自动记忆的日志刷屏 —— 根因是诊断输出在落盘后又打了一遍控制台,而它是唯一每轮都会调用的日志出口。现在只落盘、不上控制台。',
815
+ '修复:诊断日志不再无限增长 —— 此前无任何轮转,实测单文件已达 11 MB / 11.2 万行;现在超过 2 MB 自动轮转,只保留当前 + 1 份历史。',
816
+ '新增:诊断日志现在可以在「设置 → 诊断」页签里查 —— 显示完整路径(可复制)、大小、行数,并提示崩溃时把该文件附给维护者。',
817
+ '说明:两台机器同步本轮只做一次性搬包。后续不发明私有协议 —— 把导出目录指到你的云盘(OneDrive/Dropbox/坚果云等),靠包内校验和 + 显式导入来对齐。',
818
+ '修复(上游回流 PR #124):记忆中枢的「批内合并落盘」此前只挂在宿主喂数循环上 ⇒ 其它的批量入口(HTTP action=feed,面板/外部重放器一次喂一整批)仍是每行一次整份写盘(写放大)。现已把批语义下沉到「拥有这批行」的那一层,任何批量入口都自动只落一次。',
819
+ ], en: [
820
+ 'New: memory pack — move one workspace\'s memory to another machine. A migrate card in the Storage tab exports this workspace to a .dam-pack; import it on the new machine.',
821
+ 'Import is a 3-step wizard: pick pack -> preview diff (read-only, writes nothing) -> confirm. The preview lists whether paths changed, how many files are added or overwritten, and how many internal references will be rewritten.',
822
+ 'Paths are rewritten automatically on a new machine: references to the old path and slug (including path text inside bodies) are replaced with the real new path in 4 spellings; if the path is identical, nothing is changed at all.',
823
+ 'Conflicts default to keeping yours (no overwrite); you may instead overwrite or save alongside as .from-pack. Four safeguards: full backup before import (abort on failure), sha256 check rejects tampered packs, path traversal rejected, and workspaces-summary.json is MERGED so your other workspaces are preserved.',
824
+ 'Fixed: PowerShell and the console are no longer flooded by auto-memory logs — the diagnostic writer logged to the console again after writing the file, and it is the only log outlet invoked every turn. It now writes to file only.',
825
+ 'Fixed: the diagnostic log no longer grows without bound — there was no rotation at all and the file had reached 11 MB and 112k lines. It now rotates at 2 MB, keeping the current file plus one history file.',
826
+ 'New: the diagnostic log is now viewable under Settings -> Diagnostics, showing the full path (copyable), size and line count, plus a note to attach that file when reporting a crash.',
827
+ 'Note: two-machine sync in this release is one-shot packing only. No private sync protocol is invented — point the export folder at your cloud drive and reconcile via the pack checksum plus explicit import.',
828
+ 'Fixed (upstream backport PR #124): batch-merged persistence in the memory hub used to hang off the host feed loop only, so other batch entries (HTTP action=feed, panel or external replay feeding a whole batch) still wrote the entire snapshot once per row. Batch semantics now live in the layer that owns the rows, so every batch entry persists once.',
829
+ ] },
830
+ '3.1.0': { zh: [
831
+ '新增:记忆中枢「**人工越权晋升**」——给「证据还没攒够」被挡住的自沉淀条目一个真正可用的人工通道。两类门现在分得很清:**统计门**(跨会话数/成功次数没攒够)=内容没问题、只是证据不足 ⇒ 给按钮,你可以人工越过;**结构门**(已弃用/只是观察线索/有纠正记录/没写「怎么算成功」/高风险未批准)=内容本身还不构成技能 ⇒ 不给按钮,并直接写明为什么。所以不会再出现「点了没反应」的假按钮。越权过的条目会留下「人工授权」痕迹,可审计。',
832
+ '新增:设置页按用途重排为 9 个分区(语义记忆总开关 / 记忆窗口(注入什么) / 记忆容量与归档 / 自动沉淀成技能 / 长会话接续 / 自动化与免打扰 / 存储与外部记忆 / 外观与交互 / 关于与诊断)。**控件一字未改**,只是搬进对应分区并改了分组标题——此前「记忆中枢的判定参数」和「注入配置」混在一个分区里,找东西全靠翻。',
833
+ '修复:「手动晋升的通道被关掉了」的真相——判定函数对线上 30 条自沉淀条目**全部**返回「不晋升」(跨会话数不足 13 / 已弃用 8 / 只是观察线索 8 / 成功次数不足 1),而按钮此前只在「判定=可晋升」时出现,所以一条也点不亮。现已按上面的语义重做。',
834
+ '说明:更深一层的源头**本轮只缓解、未根治**——证据里的「跨会话数」恒为 0(唯一写入方依赖一个默认关闭的桥接开关),所以证据永远攒不够;人工通道解决的是「攒不够时能不能人工放行」,不是「证据永不累积」本身。',
835
+ '优化:宽屏下卡片流转为多列,记忆页改成左侧分区导航 + 右侧内容区(窄屏自动回到顶部横排);顶栏与浮层两种承载面**出厂即共存**——更新后直接在会话页顶栏就能看到记忆入口。',
836
+ '优化:日志列顺序调整——「硬性约束(每轮必注入)」与「用户笔记」提到最上面,每日日志折进可展开的折叠区。',
837
+ '说明:宿主侧(记忆中枢判定链)有改动,**建议重启 dsh web** 让其生效;界面半边刷新页面即可。',
838
+ ], en: [
839
+ 'New: manual promotion override in the memory hub — a genuinely working human channel for self-consolidated entries blocked by "not enough evidence yet". The two kinds of gates are now distinct: **statistical** (cross-session count / success count not yet reached) = the content is fine, only the evidence is thin ⇒ a button is shown and you may override; **structural** (deprecated / mere observation / has corrections / no success criteria / high-risk not approved) = the content is not a skill yet ⇒ no button, with a plain-language reason instead. So the UI no longer offers a fake button that does nothing. Overridden entries keep an auditable "manual authorization" trace.',
840
+ 'New: the settings page is regrouped into 9 purpose-named sections (memory engine / memory window / capacity & retention / skills & promotion / handoff & continuation / automation / storage / appearance / about). **No control was changed** — they were only moved into the matching section with clearer group titles; previously promotion parameters and injection settings shared one section, so finding anything meant scrolling.',
841
+ 'Fix: the real reason the manual promotion channel appeared "off" — the decision function returned "do not promote" for **all 30** self-consolidated entries (13 not enough cross-session usage / 8 deprecated / 8 observation-only / 1 not enough successes), and the button only appeared for "promotable" rows, so not one row could light up. Rebuilt around the semantics above.',
842
+ 'Note: the deeper cause is **mitigated, not cured** in this release — the cross-session count in the evidence record is permanently 0 (its only writer depends on a bridge switch that defaults to off), so evidence never accumulates. The manual channel answers "can a human override when evidence is thin", not "why evidence never accumulates".',
843
+ 'Improved: card flows are multi-column on wide screens, and the memory page uses a left section nav with content on the right (narrow screens fall back to a top strip). The titlebar and overlay surfaces now **coexist out of the box** — the memory entry is right there in the session titlebar after updating.',
844
+ 'Improved: log column order — hard constraints (injected every turn) and user notes moved to the top, with the daily log folded into an expandable section.',
845
+ 'Note: the host side (promotion decision chain) changed — **a dsh web restart is recommended**; the UI half only needs a page refresh.',
846
+ ] },
662
847
  '3.0.1': { zh: [
663
848
  '修复:宿主控制台被 `restoreLastAgent: candidate rejected` **无限刷屏**(每 15 秒一条,打开就停不下来),同时「最近会话」恢复失效——刷新仪式与自动接续的兜底路径都受影响。根因是归属判据过宽:代码把「有父会话」等同于「是子代理」并一律排除,但**你点过一次接续后产生的新会话其实也有父会话**,它是你的正常会话,却被当成子代理拒掉了;列表里只剩它一个时,每个心跳周期都白试一次。现在改为按 `origin`/`delegationDepth` 精确识别子代理,接续会话正常放行;另给诊断信息加了 5 分钟节流兜底。',
664
849
  '顺带修好两处同样的误判:接续会话里的对话此前**不会自动沉淀**、工作区状态也**不刷新**——都因为同一条过宽判据。',
@@ -1136,6 +1321,7 @@ window.__ModuleLoader__.load({
1136
1321
  update: ROUTE_PREFIX + '/update',
1137
1322
  config: ROUTE_PREFIX + '/config',
1138
1323
  reflect: ROUTE_PREFIX + '/reflect',
1324
+ recallStats: ROUTE_PREFIX + '/recall-stats',
1139
1325
  reflectAuto: ROUTE_PREFIX + '/reflect-auto',
1140
1326
  note: ROUTE_PREFIX + '/note',
1141
1327
  // ★R7:用户级硬性约束的条目级读写
@@ -1170,6 +1356,9 @@ window.__ModuleLoader__.load({
1170
1356
  reviewFeedback: ROUTE_PREFIX + '/review-feedback',
1171
1357
  memoryHub: ROUTE_PREFIX + '/memory-hub',
1172
1358
  storageManage: ROUTE_PREFIX + '/storage-manage',
1359
+ migrateExport: ROUTE_PREFIX + '/migrate-export',
1360
+ migrateInspect: ROUTE_PREFIX + '/migrate-inspect',
1361
+ migrateImport: ROUTE_PREFIX + '/migrate-import',
1173
1362
  }
1174
1363
  function query(params) {
1175
1364
  var search = new URLSearchParams()
@@ -1235,6 +1424,11 @@ window.__ModuleLoader__.load({
1235
1424
  // 视觉:液态玻璃(毛玻璃)—— backdrop-filter + DSH 主题令牌(--dsw-alias-*),
1236
1425
  // 跟随亮/暗主题与页面背景自适应;位置/尺寸由 JS 几何状态驱动(可拖动、可缩放)。
1237
1426
  var CSS = [
1427
+ // ★2026-09-22 动效刻度作用域(MOTION-SPEC-20260922 §0.2):刻度此前只定义在 [data-dam-page] 内,
1428
+ // 而浮层 [data-dam-panel] 不是它的后代 ⇒ 浮层那组取不到变量,只能写字面量、无法与页面统一。
1429
+ // 本条把同一套刻度同时挂到两个承载面;page 侧原有的那份定义原样保留(不删不搬,避免层叠行为变化)。
1430
+ '[data-dam-panel], [data-dam-page] { --dam-dur-quick: 150ms; --dam-dur-fast: 250ms; --dam-dur-slow: 400ms; --dam-dur-stagger: 40ms;',
1431
+ ' --dam-ease-out: cubic-bezier(.22, 1, .36, 1); --dam-ease-inout: cubic-bezier(.65, 0, .35, 1); --dam-ease-pop: cubic-bezier(.34, 1.36, .64, 1); }',
1238
1432
  '[data-dam-panel] { position: fixed; left: 16px; width: 440px; height: 560px; --dam-scale: 1;',
1239
1433
  ' max-width: calc(100vw - 32px); max-height: calc(100vh - 32px);',
1240
1434
  ' display: flex; flex-direction: column; overflow: hidden; z-index: 3000; pointer-events: auto;',
@@ -1244,10 +1438,10 @@ window.__ModuleLoader__.load({
1244
1438
  ' border: 1px solid color-mix(in srgb, var(--dsw-alias-border-l1, rgba(128,128,128,.35)) 65%, transparent);',
1245
1439
  ' box-shadow: 0 24px 64px rgba(0,0,0,.22), 0 4px 16px rgba(0,0,0,.10), inset 0 1px 0 rgba(255,255,255,.22);',
1246
1440
  ' color: var(--dsw-alias-label-primary, #1f2328);',
1247
- ' animation: dam-in .2s cubic-bezier(.2,.9,.3,1.15) both; transform-origin: left bottom; }',
1441
+ ' animation: dam-in var(--dam-dur-fast) cubic-bezier(.2,.9,.3,1.15) both; transform-origin: left bottom; }',
1248
1442
  '@keyframes dam-in { from { opacity: 0; transform: scale(.92) translateY(10px); } to { opacity: 1; transform: none; } }',
1249
1443
  '@keyframes dam-out { from { opacity: 1; transform: none; } to { opacity: 0; transform: scale(.95) translateY(6px); } }',
1250
- '[data-dam-panel][data-closing="true"] { animation: dam-out .16s ease-in both; }',
1444
+ '[data-dam-panel][data-closing="true"] { animation: dam-out var(--dam-dur-quick) var(--dam-ease-out) both; }',
1251
1445
  '[data-dam-panel][data-scale="sm"] { --dam-scale: .9; }',
1252
1446
  '[data-dam-panel][data-scale="lg"] { --dam-scale: 1.15; }',
1253
1447
  '[data-dam-panel][data-scale="xl"] { --dam-scale: 1.3; }',
@@ -1264,14 +1458,121 @@ window.__ModuleLoader__.load({
1264
1458
  ' opacity: .55; z-index: 2; border-radius: 0 0 16px 0;',
1265
1459
  ' background: linear-gradient(135deg, transparent 50%, color-mix(in srgb, var(--dsw-alias-label-secondary, #666) 45%, transparent) 50%); }',
1266
1460
  '[data-dam-resize]:hover { opacity: 1; }',
1461
+ // ★2026-09-21 二次修正:原「顶部通栏停靠条」CSS 已随该形态废弃而删除 —— 它曾把面板做成 shell.overlay 里的
1462
+ // 覆盖条(用户实测:「浮在整个页面上方,而不是和对话轨迹…并列的一页」)。现在第二承载面是 conversation.view 会话页,
1463
+ // 窄容器/宽容器两套样式见下面 [data-dam-page] 一组。
1464
+ // ── 会话页大屏适配(2026-09-22「大屏批」) ───────────────────────────────
1465
+ // 设计刻度:间距 4/8/12/16/24/32、圆角 8/12/16、字号 = 用户档位 × 视口台阶。
1466
+ // 动效刻度(transitions-motion token 表):150 / 250 / 400ms + smooth-out 曲线、错开 40ms。
1467
+ // 作用域只有 [data-dam-page](会话页) —— 浮层 [data-dam-panel] 保持逐字节不变。
1468
+ '[data-dam-page] { display: flex; flex-direction: column; width: 100%; height: 100%; min-height: 0;',
1469
+ ' --dam-scale: var(--dam-user-scale, 1);',
1470
+ ' --dam-measure: 1440px;',
1471
+ ' --dam-pad-x: clamp(16px, 1.6vw + 6px, 44px);',
1472
+ ' --dam-pad-y: clamp(14px, .8vw + 6px, 26px);',
1473
+ ' --dam-gap: clamp(10px, .5vw + 6px, 18px);',
1474
+ ' --dam-dur-quick: 150ms; --dam-dur-fast: 250ms; --dam-dur-slow: 400ms;',
1475
+ ' --dam-ease-out: cubic-bezier(.22, 1, .36, 1);',
1476
+ ' --dam-ease-inout: cubic-bezier(.65, 0, .35, 1);',
1477
+ ' --dam-ease-pop: cubic-bezier(.34, 1.36, .64, 1);',
1478
+ ' color: var(--dsw-alias-label-primary, #1f2328); font: 13px/1.55 system-ui, "Segoe UI", sans-serif; }',
1479
+ '[data-dam-page-head] { display: flex; align-items: center; gap: 8px; padding: 10px 4px;',
1480
+ ' border-bottom: 1px solid color-mix(in srgb, var(--dsw-alias-border-l1, rgba(128,128,128,.25)) 55%, transparent); }',
1481
+ '[data-dam-page-head] strong { font-size: calc(15px * var(--dam-scale)); }',
1482
+ '[data-dam-page-head] .dam-spacer { flex: 1; }',
1483
+ // ★2026-09-22 左侧侧栏(方向文档 PANEL-PAGE-MIGRATION-DIRECTION §2/§3):顶部横向滚动页签 → 左侧固定侧栏;
1484
+ // 内容区独立成列独立滚动。窄窗(<900px)自动降级为顶部横条(只换方向,不注销任何分区)。
1485
+ '[data-dam-page-main] { display: flex; flex: 1; min-height: 0; }',
1486
+ '[data-dam-page-nav] { flex: 0 0 auto; width: 170px; overflow: auto; padding: 10px 8px; display: flex; flex-direction: column; gap: 2px;',
1487
+ ' border-right: 1px solid color-mix(in srgb, var(--dsw-alias-border-l1, rgba(128,128,128,.25)) 55%, transparent); }',
1488
+ '[data-dam-page-nav] button { flex: 0 0 auto; border: 0; background: transparent; color: inherit; text-align: left; cursor: pointer;',
1489
+ ' padding: 7px 10px; border-radius: 8px; font: inherit; font-size: calc(12.5px * var(--dam-scale)); opacity: .68; white-space: nowrap;',
1490
+ ' transition: background-color var(--dam-dur-quick) var(--dam-ease-out), color var(--dam-dur-quick) var(--dam-ease-out), opacity var(--dam-dur-quick) var(--dam-ease-out); }',
1491
+ '[data-dam-page-nav] button[data-active="true"] { opacity: 1; font-weight: 650; color: var(--dam-accent, var(--dsw-alias-brand-primary, #4f7cff));',
1492
+ ' background: color-mix(in srgb, var(--dam-accent, #4f7cff) 12%, transparent); }',
1493
+ '[data-dam-page-content] { flex: 1; min-width: 0; display: flex; flex-direction: column; min-height: 0; }',
1494
+ '[data-dam-page-nav], [data-dam-page-content], [data-dam-page-main] { flex-shrink: 1; }',
1495
+ '[data-dam-page] [data-dam-section] { min-width: 0; }',
1496
+ // 折叠标题(日志列表用):整行可点, 箭头用 transform 旋转(不换字符,避免宽度跳动)。
1497
+ '[data-dam-page] [data-dam-fold] { display: flex; align-items: center; gap: 8px; width: 100%; text-align: left; border: 0; cursor: pointer;',
1498
+ ' background: color-mix(in srgb, var(--dsw-alias-label-secondary, #666) 8%, transparent); color: inherit; font: inherit;',
1499
+ ' font-size: calc(12.5px * var(--dam-scale)); font-weight: 650; padding: 9px 12px; border-radius: 9px;',
1500
+ ' transition: background-color var(--dam-dur-quick) var(--dam-ease-out); }',
1501
+ '[data-dam-page] [data-dam-fold]:hover { background: color-mix(in srgb, var(--dsw-alias-label-secondary, #666) 14%, transparent); }',
1502
+ '[data-dam-page] [data-dam-fold] .dam-fold-caret { display: inline-block; transition: transform var(--dam-dur-fast) var(--dam-ease-inout); }',
1503
+ '[data-dam-page] [data-dam-fold][data-open="true"] .dam-fold-caret { transform: rotate(90deg); }',
1504
+ '[data-dam-page] [data-dam-fold] .dam-fold-count { margin-left: auto; opacity: .55; font-weight: 400; }',
1505
+ '@media (max-width: 900px) {',
1506
+ ' [data-dam-page-main] { flex-direction: column; }',
1507
+ ' [data-dam-page-nav] { width: auto; flex-direction: row; gap: 4px; overflow-x: auto; overflow-y: hidden; padding: 6px 8px;',
1508
+ ' border-right: 0; border-bottom: 1px solid color-mix(in srgb, var(--dsw-alias-border-l1, rgba(128,128,128,.25)) 55%, transparent); } }',
1509
+ '[data-dam-page] [data-dam-body] { flex: 1; min-height: 0; overflow: auto;',
1510
+ ' padding: var(--dam-pad-y) var(--dam-pad-x) 28px;',
1511
+ ' padding-inline: max(var(--dam-pad-x), calc((100% - var(--dam-measure)) / 2));',
1512
+ ' scrollbar-width: thin;',
1513
+ ' scrollbar-color: color-mix(in srgb, var(--dsw-alias-label-secondary, #666) 34%, transparent) transparent; }',
1514
+ '[data-dam-page] [data-dam-body]::-webkit-scrollbar { width: 10px; height: 10px; }',
1515
+ '[data-dam-page] [data-dam-body]::-webkit-scrollbar-track { background: transparent; }',
1516
+ '[data-dam-page] [data-dam-body]::-webkit-scrollbar-thumb { border: 3px solid transparent; background-clip: content-box;',
1517
+ ' border-radius: 8px; background-color: color-mix(in srgb, var(--dsw-alias-label-secondary, #666) 28%, transparent); }',
1518
+ '[data-dam-page] [data-dam-body]::-webkit-scrollbar-thumb:hover {',
1519
+ ' background-color: color-mix(in srgb, var(--dsw-alias-label-secondary, #666) 48%, transparent); }',
1520
+ // 大屏台阶:字号随视口抬两档, 与用户档位相乘, 不覆盖用户选择。
1521
+ '@media (min-width: 1400px) { [data-dam-page] { --dam-scale: calc(var(--dam-user-scale, 1) * 1.06); } }',
1522
+ '@media (min-width: 1800px) { [data-dam-page] { --dam-scale: calc(var(--dam-user-scale, 1) * 1.12); } }',
1523
+ // 首帧/页签切换:主体淡入 + 6px 上浮(只动 opacity/transform, 不触发布局与重绘)。
1524
+ '[data-dam-page] [data-dam-body] > :not([data-dam-flow]) { animation: dam-page-in var(--dam-dur-fast) var(--dam-ease-out) both; }',
1525
+ '@keyframes dam-page-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }',
1526
+ // ★卡片流分列(大屏核心收益):只对打了 [data-dam-flow] 标记的页签生效;
1527
+ // 非卡片子元素一律整行占据 ⇒ 不打标记的页签 = 逐字节旧行为, 打了标记也只有卡片进列。
1528
+ '[data-dam-page] [data-dam-flow] { display: grid; gap: var(--dam-gap); align-items: start; align-content: start;',
1529
+ ' grid-template-columns: repeat(auto-fit, minmax(min(100%, 380px), 1fr)); }',
1530
+ '[data-dam-page] [data-dam-flow] > * { grid-column: 1 / -1; min-width: 0; animation: dam-card-in var(--dam-dur-fast) var(--dam-ease-out) both; }',
1531
+ // ★2026-09-22 动效统一(MOTION-SPEC-20260922 §1.2/§2.2/§2.3):
1532
+ // ① 时长 400ms → var(--dam-dur-fast) 250ms —— 用途是「页面前进」,400ms 是「面板打开」刻度,用在切页签会拖。
1533
+ // ② 补 both —— 原缺 animation-fill-mode,而下面 nth-child 带 40~200ms 延迟;fill-mode 默认 none 时
1534
+ // 延迟期内动画不生效 ⇒ 卡片按终态(opacity:1)渲染 ⇒ 第 2~6 张「先闪一下 → 跳空 → 再淡入」。
1535
+ // 同文件的 dam-rise 写了 both(:1466/:1472),属漏写不是设计。
1536
+ // ③ 错开值改走 --dam-dur-stagger(数值不变,只刻度化)。
1537
+ '[data-dam-page] [data-dam-flow] > [data-dam-card] { grid-column: auto; margin-bottom: 0;',
1538
+ ' animation: dam-card-in var(--dam-dur-fast) var(--dam-ease-out) both; }',
1539
+ '@keyframes dam-stat-num { from { opacity: 0; transform: translateY(4px); filter: blur(2px); } to { opacity: 1; transform: none; filter: none; } }',
1540
+ '@keyframes dam-stat-reveal { from { opacity: 0; filter: blur(2px); } to { opacity: 1; filter: none; } }',
1541
+ '@keyframes dam-stat-grow { from { transform: scaleX(.02); transform-origin: left center; } to { transform: none; } }',
1542
+ '@keyframes dam-stat-pop { from { opacity: 0; transform: scale(.92); } to { opacity: 1; transform: none; } }',
1543
+ '@keyframes dam-stat-pulse { 0%, 100% { opacity: .45; } 50% { opacity: .85; } }',
1544
+ '@keyframes dam-stat-arc { from { opacity: 0; } to { opacity: 1; } }',
1545
+ '@keyframes dam-card-in { from { opacity: 0; transform: translateY(8px) scale(.988); } to { opacity: 1; transform: none; } }',
1546
+ '[data-dam-page] [data-dam-flow] > [data-dam-card]:nth-child(2) { animation-delay: calc(var(--dam-dur-stagger) * 1); }',
1547
+ '[data-dam-page] [data-dam-flow] > [data-dam-card]:nth-child(3) { animation-delay: calc(var(--dam-dur-stagger) * 2); }',
1548
+ '[data-dam-page] [data-dam-flow] > [data-dam-card]:nth-child(4) { animation-delay: calc(var(--dam-dur-stagger) * 3); }',
1549
+ '[data-dam-page] [data-dam-flow] > [data-dam-card]:nth-child(5) { animation-delay: calc(var(--dam-dur-stagger) * 4); }',
1550
+ '[data-dam-page] [data-dam-flow] > [data-dam-card]:nth-child(n+6) { animation-delay: calc(var(--dam-dur-stagger) * 5); }',
1551
+ // 交互反馈:按压即时(150ms 刻度);悬停只在精确指针设备上生效, 避免触屏误触发。
1552
+ '[data-dam-page] [data-dam-btn], [data-dam-page] [data-dam-tab] {',
1553
+ ' transition: background-color var(--dam-dur-quick) var(--dam-ease-out), color var(--dam-dur-quick) var(--dam-ease-out),',
1554
+ ' opacity var(--dam-dur-quick) var(--dam-ease-out), transform var(--dam-dur-quick) var(--dam-ease-out); }',
1555
+ '[data-dam-page] [data-dam-btn]:active, [data-dam-page] [data-dam-tab]:active { transform: scale(.97); }',
1556
+ '[data-dam-page] [data-dam-tab] { padding: 8px 11px; }',
1557
+ '[data-dam-page] :focus-visible { outline: 2px solid color-mix(in srgb, var(--dam-accent, #2456c4) 72%, transparent); outline-offset: 2px; }',
1558
+ '@media (hover: hover) and (pointer: fine) { [data-dam-page] [data-dam-tab]:hover { opacity: .92; } }',
1559
+ // 减动效闸门:新增动效逐条归零(与文件既有那段同源, 此处只覆盖本批新增的选择器)。
1560
+ '@media (prefers-reduced-motion: reduce) {',
1561
+ ' [data-dam-page] [data-dam-body] > *, [data-dam-page] [data-dam-flow] > [data-dam-card] { animation: none !important; }',
1562
+ ' [data-dam-page] [data-dam-btn], [data-dam-page] [data-dam-tab] { transition: none !important; }',
1563
+ ' [data-dam-page] [data-dam-btn]:active, [data-dam-page] [data-dam-tab]:active { transform: none; } }',
1267
1564
  '[data-dam-btn] { border: none; background: transparent; cursor: pointer; color: inherit; opacity: .75; font-size: calc(13px * var(--dam-scale)); padding: 4px 8px; border-radius: 6px; }',
1268
1565
  '[data-dam-btn]:hover { opacity: 1; background: color-mix(in srgb, var(--dsw-alias-label-secondary, #666) 16%, transparent); }',
1269
1566
  '[data-dam-btn][data-pin="on"] { opacity: 1; background: color-mix(in srgb, var(--dam-accent, #4f7cff) 24%, transparent); }',
1270
1567
  '[data-dam-tabs-wrap] { display: flex; align-items: center; min-width: 0; border-bottom: 1px solid color-mix(in srgb, var(--dsw-alias-border-l1, rgba(128,128,128,.2)) 55%, transparent); }',
1271
1568
  '[data-dam-tabs] { flex: 1; min-width: 0; overflow: hidden; padding: 5px 4px; scrollbar-width: none; -ms-overflow-style: none; }',
1272
1569
  '[data-dam-tabs]::-webkit-scrollbar { display: none; }',
1570
+ // ★2026-09-22 取证记录(MOTION-SPEC-20260922 §3.3,本批**不改值**):下面这条 transition: transform 目前是
1571
+ // **死声明** —— 全文件只有 3 处提到 data-dam-tab-strip(CSS 定义 / 减动效闸门 / :4714 创建该 div),
1572
+ // 滚动由父容器原生 scrollTo/scrollBy 驱动(:4704-4705/:4710),没有任何代码写过它的 transform。
1573
+ // 保留原样:删它对本轮问题零贡献,却可能挡住「将来把滚动改回 transform 驱动」。要动它请单开一批。
1273
1574
  '[data-dam-tab-strip] { display: flex; width: max-content; transition: transform .42s cubic-bezier(.22,.8,.2,1); will-change: transform; }',
1274
- '[data-dam-tab] { flex: 0 0 auto; min-width: max-content; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; border: none; background: transparent; cursor: pointer; color: inherit; opacity: .68; padding: 6px 9px; border-radius: 7px; font-size: calc(12.5px * var(--dam-scale)); transition: color .25s ease, background .25s ease, opacity .25s ease, transform .25s ease; }',
1575
+ '[data-dam-tab] { flex: 0 0 auto; min-width: max-content; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; border: none; background: transparent; cursor: pointer; color: inherit; opacity: .68; padding: 6px 9px; border-radius: 7px; font-size: calc(12.5px * var(--dam-scale)); transition: color var(--dam-dur-quick) var(--dam-ease-out), background var(--dam-dur-quick) var(--dam-ease-out), opacity var(--dam-dur-quick) var(--dam-ease-out), transform var(--dam-dur-quick) var(--dam-ease-out); }',
1275
1576
  '[data-dam-tabs-arrow] { flex: 0 0 26px; height: 30px; border: 0; background: transparent; color: inherit; cursor: pointer; opacity: .55; font-size: 18px; }',
1276
1577
  '[data-dam-tabs-arrow]:hover:not(:disabled) { opacity: 1; background: color-mix(in srgb, var(--dam-accent, #2456c4) 16%, transparent); transform: scale(1.12); }',
1277
1578
  '[data-dam-tabs-arrow]:active:not(:disabled) { transform: scale(.92); }',
@@ -1308,21 +1609,31 @@ window.__ModuleLoader__.load({
1308
1609
  ' background: color-mix(in srgb, var(--dsw-alias-bg-layer-1, rgba(128,128,128,.06)) 40%, transparent); }',
1309
1610
  '[data-dam-card] .dam-date { font-weight: 600; margin-bottom: 4px; }',
1310
1611
  '[data-dam-card] .dam-content { white-space: pre-wrap; word-break: break-word; max-height: 220px; overflow: auto; }',
1311
- '[data-dam-disclosure] { overflow: hidden; opacity: 0; max-height: 0; transform: translateY(-5px) scale(.985); transition: max-height .32s cubic-bezier(.22,.8,.2,1), opacity .2s ease, transform .28s cubic-bezier(.22,.8,.2,1); }',
1612
+ '[data-dam-disclosure] { overflow: hidden; opacity: 0; max-height: 0; transform: translateY(-5px) scale(.985); transition: max-height var(--dam-dur-fast) var(--dam-ease-out), opacity var(--dam-dur-fast) var(--dam-ease-out), transform var(--dam-dur-fast) var(--dam-ease-out); }',
1312
1613
  '[data-dam-disclosure][data-phase="open"] { opacity: 1; max-height: 1800px; transform: translateY(0) scale(1); }',
1313
1614
  '[data-dam-disclosure][data-phase="closing"] { opacity: 0; max-height: 0; transform: translateY(-5px) scale(.985); }',
1314
- '[data-dam-card], [data-dam-banner] { animation: dam-rise .34s cubic-bezier(.22,.8,.2,1) both; }',
1615
+ '[data-dam-card], [data-dam-banner] { animation: dam-rise var(--dam-dur-fast) var(--dam-ease-out) both; }',
1315
1616
  '@keyframes dam-rise { from { opacity: 0; transform: translateY(7px) scale(.988); } to { opacity: 1; transform: translateY(0) scale(1); } }',
1316
1617
  '@media (prefers-reduced-motion: reduce) { [data-dam-tab-strip], [data-dam-tab], [data-dam-disclosure], [data-dam-card], [data-dam-banner], [data-dam-tour-orb-wrap] *, [data-dam-update-box] * { transition: none !important; animation: none !important; }',
1317
1618
  ' [data-dam-tour-bokeh], [data-dam-tour-slab], [data-dam-tour-orb-core] { opacity: 1 !important; transform: none !important; filter: none !important; }',
1318
1619
  ' [data-dam-update-stage] { display: none !important; }',
1319
1620
  ' [data-dam-update-content] { opacity: 1 !important; transform: none !important; animation: none !important; } }',
1320
- '[data-dam-calendar] { animation: dam-rise .34s cubic-bezier(.22,.8,.2,1) both; }',
1321
- '[data-dam-calendar] [data-dam-calendar-day] { transition: transform .18s ease, border-color .2s ease, background .2s ease, box-shadow .2s ease; }',
1621
+ // ★2026-09-22 减动效闸门(第二道) —— MOTION-SPEC-20260922 §1.6:上面那道只覆盖 8 个选择器,
1622
+ // 漏了浮层/日历三件/页签交互/折叠头/侧栏/保存条。本批新增与改动的每一条动效都必须在两道里被归零。
1623
+ '@media (prefers-reduced-motion: reduce) {',
1624
+ ' [data-dam-panel], [data-dam-calendar], [data-dam-calendar-event], [data-dam-calendar-modal],',
1625
+ ' [data-dam-calendar] [data-dam-calendar-day], [data-dam-tabs-arrow], [data-dam-btn],',
1626
+ ' [data-dam-page-nav] button, [data-dam-page] [data-dam-fold], [data-dam-page] [data-dam-fold] .dam-fold-caret,',
1627
+ ' [data-dam-savebar], [data-dam-savebar] button { transition: none !important; animation: none !important; }',
1628
+ ' [data-dam-tab][data-active="true"], [data-dam-calendar] [data-dam-calendar-day]:hover,',
1629
+ ' [data-dam-page] [data-dam-flow] > * { animation: none !important; }',
1630
+ ' [data-dam-tab]:active, [data-dam-btn]:active, [data-dam-tabs-arrow]:active:not(:disabled) { transform: none !important; } }',
1631
+ '[data-dam-calendar] { animation: dam-rise var(--dam-dur-fast) var(--dam-ease-out) both; }',
1632
+ '[data-dam-calendar] [data-dam-calendar-day] { transition: transform var(--dam-dur-quick) var(--dam-ease-out), border-color .2s ease, background .2s ease, box-shadow .2s ease; }',
1322
1633
  '[data-dam-calendar] [data-dam-calendar-day]:hover { transform: translateY(-1px); border-color: var(--dam-accent, #1d4ed8) !important; box-shadow: 0 4px 12px rgba(0,0,0,.08); }',
1323
- '[data-dam-calendar-event] { animation: dam-event-in .25s cubic-bezier(.22,.8,.2,1) both; transition: transform .18s ease, filter .18s ease; }',
1634
+ '[data-dam-calendar-event] { animation: dam-event-in var(--dam-dur-fast) var(--dam-ease-out) both; transition: transform var(--dam-dur-quick) var(--dam-ease-out), filter var(--dam-dur-quick) var(--dam-ease-out); }',
1324
1635
  '[data-dam-calendar-event]:hover { transform: translateX(2px); filter: brightness(1.06); }',
1325
- '[data-dam-calendar-modal] { animation: dam-modal-in .26s cubic-bezier(.22,.8,.2,1) both; }',
1636
+ '[data-dam-calendar-modal] { animation: dam-modal-in var(--dam-dur-fast) var(--dam-ease-out) both; }',
1326
1637
  '@keyframes dam-event-in { from { opacity: 0; transform: translateX(-5px); } to { opacity: 1; transform: none; } }',
1327
1638
  '@keyframes dam-modal-in { from { opacity: 0; transform: translateY(10px) scale(.98); } to { opacity: 1; transform: none; } }',
1328
1639
  '[data-dam-banner] { border: 1px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary, #e6a23c) 55%, transparent);',
@@ -3027,12 +3338,15 @@ window.__ModuleLoader__.load({
3027
3338
  })
3028
3339
  ) : null,
3029
3340
  groups.map(function (grp) {
3030
- return h('div', { key: grp.day, style: { marginBottom: '16px' } },
3341
+ // ★大屏分列(2026-09-22):判定队列每天一组的卡片在宽屏排成多列。
3342
+ // CSS 规则是 `[data-dam-flow] > [data-dam-card]`(直接子元素) ⇒ 标记必须打在**分组容器**上,
3343
+ // 打在页签根上卡片不是直接子元素,只会退化成整行占位(无效接线)。
3344
+ return h('div', { key: grp.day, 'data-dam-flow': '', style: { marginBottom: '16px' } },
3031
3345
  h('div', { style: { fontSize: 'calc(11.5px * var(--dam-scale))', fontWeight: 700, opacity: .7, margin: '2px 2px 8px' } }, grp.day),
3032
3346
  grp.rows.map(function (row, i) {
3033
3347
  var rc = row.reasonCodes || []
3034
3348
  var del = row.delivery
3035
- return h('div', { key: row.observationId || i, style: card },
3349
+ return h('div', { key: row.observationId || i, 'data-dam-card': '', style: card },
3036
3350
  h('div', { style: { display: 'flex', alignItems: 'center', gap: '7px', marginBottom: '5px' } },
3037
3351
  (function () { var lk = row.lane === 'explicit' ? 'explicit' : 'proactive'; var c = laneBg[lk] || ['rgba(128,128,128,.16)', null]; return badge(locale === 'zh' ? (lk === 'explicit' ? '明确召回' : '主动观测') : lk, c[0], c[1]) })(),
3038
3352
  (function () { var dk = String(row.decision); var c = decBg[dk] || ['rgba(128,128,128,.14)', null]; return badge(dk.toUpperCase(), c[0], c[1]) })(),
@@ -3064,6 +3378,11 @@ window.__ModuleLoader__.load({
3064
3378
  var userTruncPair = useState(false)
3065
3379
  var userTruncated = userTruncPair[0]
3066
3380
  var setUserTruncated = userTruncPair[1]
3381
+ // ★2026-09-22:日志列表改折叠(用户要求「日志可以做成折叠的形式,可以展开」)。默认收起 ——
3382
+ // 首屏留给「硬性约束 + 用户笔记」这两块每轮无条件注入的稳定内容,日志属于翻查型内容。
3383
+ var logsOpenPair = useState(false)
3384
+ var logsOpen = logsOpenPair[0]
3385
+ var setLogsOpen = logsOpenPair[1]
3067
3386
  useEffect(function () {
3068
3387
  var alive = true
3069
3388
  apiGet(API.list).then(function (d) { if (alive) setData(d) }).catch(function () {})
@@ -3083,28 +3402,44 @@ window.__ModuleLoader__.load({
3083
3402
  h('div', { 'data-dam-content': '' }, content || t('empty')),
3084
3403
  h('button', { 'data-dam-btn': '', onClick: function () { setOpen(null); setContent('') } }, t('back'))))
3085
3404
  } else {
3086
- rows.push(h('div', { 'data-dam-hint': '' }, t('clickDateViewLog')))
3087
- for (var i = 0; i < data.logs.length; i++) {
3088
- (function (log) {
3089
- rows.push(h(Card, { title: log.date + ' · ' + fmtSize(log.size) },
3090
- h('button', { 'data-dam-btn': '', onClick: function () { setOpen(log.date + '.md') } }, t('view'))))
3091
- })(data.logs[i])
3092
- }
3093
- // 三级记忆之用户级:当日日志下方展示用户画像(跨项目规则/偏好)
3094
- rows.push(h('div', { 'data-dam-hint': '', style: { marginTop: '12px' } }, t('userMemoryBlock')))
3095
- rows.push(h(Card, { title: 'MEMORY.md (' + (locale === 'zh' ? '用户级' : 'user-level') + ')' },
3096
- h('div', { 'data-dam-content': '' }, userText || t('userMemoryEmpty')),
3097
- userTruncated ? h('div', { 'data-dam-hint': '' }, t('userMemoryTruncated')) : null))
3098
- // ★R7:硬性约束编辑面板(用户要求「能自行增删改」)
3099
- // ★2026-09-20 崩溃修复:本处原写 `{ nonce: nonce }`,但 **LogsTab 不接收任何 props**
3100
- // (`function LogsTab()`,调用点 `h(LogsTab)` 未传参),而 `nonce` 只在 MemoryPanel
3101
- // 作用域内定义(L3969-3970)⇒ 打开日志页签时求值 nonce 抛
3102
- // ReferenceError: nonce is not defined,整个窗格崩溃。
3103
- // RulesEditPanel 本就把它当可选(`props && props.nonce ? … : 0`),故传空对象即可。
3104
- rows.push(h(RulesEditPanel, {}))
3105
- }
3106
- return h('div', null, rows)
3405
+ // ★2026-09-22 版式调整(用户要求「把上面的那一堆东西和下面的对调」):
3406
+ // ① 硬性约束 与 用户笔记 提到最上面 —— 两者都是**每轮无条件注入**的稳定内容,使用者最该先看到;
3407
+ // ② 日志列表下移并改折叠 —— 日志是翻查型内容,不该占据首屏。
3408
+ // 两块都用 [data-dam-section] 包裹:它在 [data-dam-flow] 栅格里整行占位,不会被分列。
3409
+ rows.push(h('div', { 'data-dam-section': '' }, h(RulesEditPanel, {})))
3410
+ rows.push(h('div', { 'data-dam-section': '', style: { marginTop: '14px' } },
3411
+ h('div', { 'data-dam-hint': '', style: { marginBottom: '6px' } }, t('userMemoryBlock')),
3412
+ h(Card, { title: 'MEMORY.md (' + (locale === 'zh' ? '用户级' : 'user-level') + ')' },
3413
+ h('div', { 'data-dam-content': '' }, userText || t('userMemoryEmpty')),
3414
+ userTruncated ? h('div', { 'data-dam-hint': '' }, t('userMemoryTruncated')) : null)))
3415
+ rows.push(h('div', { 'data-dam-section': '', style: { marginTop: '14px' } }, h(FoldableLogs, {
3416
+ logs: data.logs || [], open: logsOpen, setOpen: setLogsOpen, onOpenFile: setOpen,
3417
+ })))
3418
+ }
3419
+ // 大屏分列:卡片流页签(见 CSS [data-dam-flow]);非卡片子元素仍整行。
3420
+ return h('div', { 'data-dam-flow': '' }, rows)
3107
3421
  }
3422
+ // ★2026-09-22 日志折叠块(从 LogsTab 抽出, 便于独立校验括号):标题可点, 内容用既有 AnimatedDisclosure。
3423
+ function FoldableLogs(props) {
3424
+ var open = !!props.open
3425
+ var list = props.logs || []
3426
+ return h('div', null,
3427
+ h('button', {
3428
+ 'data-dam-fold': '', 'data-open': open ? 'true' : 'false', 'aria-expanded': open ? 'true' : 'false',
3429
+ onClick: function () { props.setOpen(!open) },
3430
+ },
3431
+ h('span', { className: 'dam-fold-caret' }, '▸'),
3432
+ h('span', null, t('logs')),
3433
+ h('span', { className: 'dam-fold-count' }, list.length + (locale === 'zh' ? ' 篇' : ' files'))),
3434
+ h(AnimatedDisclosure, { open: open },
3435
+ h('div', { style: { paddingTop: '8px' } },
3436
+ h('div', { 'data-dam-hint': '' }, t('clickDateViewLog')),
3437
+ list.map(function (log) {
3438
+ return h(Card, { key: log.date, title: log.date + ' · ' + fmtSize(log.size) },
3439
+ h('button', { 'data-dam-btn': '', onClick: function () { props.onOpenFile(log.date + '.md') } }, t('view')))
3440
+ }))))
3441
+ }
3442
+
3108
3443
  function pathName(p) { var parts = String(p).split(/[\\/]/); return parts[parts.length - 1] }
3109
3444
 
3110
3445
  // ── ★R1–R6(2026-09-20 用户「看不懂只能盲确认」)审批面人话化辅助 ────────
@@ -3132,6 +3467,14 @@ window.__ModuleLoader__.load({
3132
3467
  if (c === 'model-authorized') continue
3133
3468
  }
3134
3469
  if (promotion.decision === 'promote') return t('hubWhyCanPromote')
3470
+ // ★C(2026-09-22):把「关在哪一类门」说透 —— 统计门可人工越过,结构门不可。
3471
+ // 没有这一段时,界面上"一个按钮都没有",用户只能猜(这正是"手动通道被关掉"的观感来源)。
3472
+ if (promotion.decision === 'keep') {
3473
+ var head = codes.length ? codes.join(' · ') : ''
3474
+ if (promotion.overridable === true) return head + ' | ' + t('hubWhyOverridable')
3475
+ if (promotion.gateKind === 'structural') return head + ' | ' + t('hubWhyStructural')
3476
+ return head
3477
+ }
3135
3478
  return codes.length ? codes.join(' · ') : ''
3136
3479
  }
3137
3480
 
@@ -3327,7 +3670,29 @@ window.__ModuleLoader__.load({
3327
3670
  h('div', { style: { display: 'flex', gap: '6px', marginTop: '5px' } },
3328
3671
  // issue #30:纯 episode 观察行**结构上**不可能晋升(无 successCriteria),
3329
3672
  // 继续显示"晋升"按钮只会让人反复点击却看不到变化 ⇒ 直接隐藏并打观察标。
3330
- !p.observationOnly && h('button', { 'data-dam-btn': '', style: { fontSize: 'calc(10.5px * var(--dam-scale))', padding: '3px 10px' }, onClick: function () { hubAct('promote', p.procedureId) } }, locale === 'zh' ? '晋升' : 'promote'),
3673
+ // ★ 2026-09-21 bugfix(A-9):原条件只挡 observationOnly,与 store 真实门限**不一致** ——
3674
+ // 缺 successCriteria / 有 correction / correctionRate 超限的条目照样显示按钮,
3675
+ // 用户点下去只得到一行 keep 灰字,感受即「批准不了」。
3676
+ // 现改为**直接依赖只读投影** `p.promotion`(宿主 memory-hub 用 evaluatePromotion 生成,
3677
+ // 纯只读、不写盘):decision==='promote' 才给按钮,否则界面已由 R2 区块说明原因。
3678
+ // fail-soft:投影缺失(旧宿主/异常)时退回原 observationOnly 判据,不做过度拦截。
3679
+ (!p.observationOnly && (!p.promotion || p.promotion.decision === 'promote'))
3680
+ && h('button', { 'data-dam-btn': '', style: { fontSize: 'calc(10.5px * var(--dam-scale))', padding: '3px 10px' }, onClick: function () { hubAct('promote', p.procedureId) } }, locale === 'zh' ? '晋升' : 'promote'),
3681
+ // ★2026-09-22 补接线(前端接线审计 P0-1):宿主已提供 approve 原语
3682
+ // (index.js 白名单含 action:'approve' + procedure-store-pre approve('user')),
3683
+ // 而上面「晋升」按钮门控在 decision==='promote' ⇒ 高风险条目(decision==='ask')
3684
+ // 在界面上是死路:用户看得到「待人工批准」却没有任何按钮可点。
3685
+ (p.promotion && p.promotion.decision === 'ask')
3686
+ && h('button', { 'data-dam-btn': '', style: { fontSize: 'calc(10.5px * var(--dam-scale))', padding: '3px 10px', fontWeight: 650 }, onClick: function () { hubAct('approve', p.procedureId) } }, locale === 'zh' ? '批准(人工)' : 'approve (manual)'),
3687
+ // ★B/C(2026-09-22):人工**强制晋升** —— 只在「拦它的门**全是统计门**」时给出。
3688
+ // 判据来自宿主只读投影 p.promotion.overridable(memory-hub 用 promotionOverrideView 生成,
3689
+ // 只读、零副作用、且**不重复实现门限** ⇒ 结构上不可能与 promote() 漂移)。
3690
+ // 结构门拦(已弃用 / 观察型 / 无 successCriteria / 有纠正记录)时**不给按钮** ——
3691
+ // 否则就是再造一个「点了没反应」的假通道(A-9 收掉旧按钮的正是这个原因)。
3692
+ // 动作走 index.js 白名单 `force-promote` → promote(pid, {}, { authorizedBy:'user' }),
3693
+ // 只越过 diversity/success 两道统计门,并在条目上留 `authorizedBy:'user'` 痕迹。
3694
+ (p.promotion && p.promotion.overridable === true)
3695
+ && h('button', { 'data-dam-btn': '', title: t('hubForcePromoteTitle'), style: { fontSize: 'calc(10.5px * var(--dam-scale))', padding: '3px 10px', fontWeight: 650 }, onClick: function () { hubAct('force-promote', p.procedureId) } }, t('hubForcePromote')),
3331
3696
  !p.observationOnly && p.stage === 'validated' && h('button', { 'data-dam-btn': '', style: { fontSize: 'calc(10.5px * var(--dam-scale))', padding: '3px 10px' }, onClick: function () { hubAct('activate', p.procedureId) } }, locale === 'zh' ? '直接激活' : 'activate'),
3332
3697
  h('button', { 'data-dam-btn': '', style: { fontSize: 'calc(10.5px * var(--dam-scale))', padding: '3px 10px', opacity: .75 }, onClick: function () { hubAct('deprecate', p.procedureId) } }, locale === 'zh' ? '弃用' : 'deprecate'),
3333
3698
  h('button', { 'data-dam-btn': '', style: { fontSize: 'calc(10.5px * var(--dam-scale))', padding: '3px 10px', opacity: .75 }, onClick: function () { hubAct('pin', p.procedureId, !p.pinned) } }, p.pinned ? (locale === 'zh' ? '取消置顶' : 'unpin') : (locale === 'zh' ? '置顶' : 'pin'))))
@@ -3341,6 +3706,18 @@ window.__ModuleLoader__.load({
3341
3706
  factList.length ? factList.slice(0, 6).map(function (f) {
3342
3707
  return h('div', { 'data-dam-content': '', key: f.factId, style: { padding: '3px 0' } }, f.subject + ' · ' + f.predicate + (f.object ? ' · ' + f.object : ''))
3343
3708
  }) : h('div', { 'data-dam-content': '' }, t('hubFactsEmpty'))))
3709
+ // ★P0-3a 补接线(2026-09-22):事实保留上限淘汰台账。宿主早已把它放进 /memory-hub 应答
3710
+ // (memory-hub.js:245 → stores.facts.getStats()),前端此前零渲染
3711
+ // ⇒「事实被悄悄淘汰了」在界面上无处可查。pruneProtected>0 表示已轮到删「重要项」⇒ 上限偏低。
3712
+ var factStats = (facts && facts.stats) || null
3713
+ if (factStats && (Number(factStats.pruned) > 0 || Number(factStats.pruneProtected) > 0)) {
3714
+ var fpPruned = Number(factStats.pruned) || 0
3715
+ var fpProtected = Number(factStats.pruneProtected) || 0
3716
+ rows.push(h('div', { 'data-dam-hint': '', style: { marginTop: '6px', color: fpProtected > 0 ? 'var(--dsw-alias-state-warn-primary, #e8c584)' : undefined } },
3717
+ locale === 'zh'
3718
+ ? '已按保留上限淘汰 ' + fpPruned + ' 条事实' + (fpProtected > 0 ? ',其中 ' + fpProtected + ' 条属重要/未撤销条目 —— 上限偏低,建议到 设置 → 自动记忆引擎 调高「事实保留上限」' : '')
3719
+ : fpPruned + ' fact(s) evicted by the retention cap' + (fpProtected > 0 ? ', incl. ' + fpProtected + ' important/unrevoked — cap too low; raise "Fact retention limit" under Settings → Semantic engine' : '')))
3720
+ }
3344
3721
  if (facts && facts.pendingConflicts && facts.pendingConflicts.length) {
3345
3722
  rows.push(h('div', { 'data-dam-hint': '', style: { color: 'var(--dsw-alias-state-warn-primary, #e8c584)' } }, t('hubConflicts') + ': ' + facts.pendingConflicts.length))
3346
3723
  }
@@ -3355,7 +3732,8 @@ window.__ModuleLoader__.load({
3355
3732
  // 统计
3356
3733
  rows.push(h('div', { 'data-dam-hint': '', style: { marginTop: '12px', opacity: .7 } },
3357
3734
  (locale === 'zh' ? '中枢统计: ' : 'Hub stats: ') + (data.stats ? JSON.stringify(data.stats) : '')))
3358
- return h('div', null, rows)
3735
+ // 大屏分列:卡片流页签(见 CSS [data-dam-flow]);非卡片子元素仍整行。
3736
+ return h('div', { 'data-dam-flow': '' }, rows)
3359
3737
  }
3360
3738
 
3361
3739
  // M10 存储管理页签(2026-08-30 P3):数据源=/storage-manage(loopback 只读投影 + 三类动作)。
@@ -3378,6 +3756,100 @@ window.__ModuleLoader__.load({
3378
3756
  var filePair = useState('')
3379
3757
  var delFile = filePair[0]
3380
3758
  var setDelFile = filePair[1]
3759
+ // 迁移搬包(P4 前端接线):导出/三步向导的全部状态。只用 useState,不引新依赖。
3760
+ var migOutPair = useState('')
3761
+ var migOut = migOutPair[0]
3762
+ var setMigOut = migOutPair[1]
3763
+ var migPackPair = useState('')
3764
+ var migPack = migPackPair[0]
3765
+ var setMigPack = migPackPair[1]
3766
+ var migOutPickingPair = useState(false)
3767
+ var migOutPicking = migOutPickingPair[0]
3768
+ var setMigOutPicking = migOutPickingPair[1]
3769
+ var migPackPickingPair = useState(false)
3770
+ var migPackPicking = migPackPickingPair[0]
3771
+ var setMigPackPicking = migPackPickingPair[1]
3772
+ var migBusyPair = useState('')
3773
+ var migBusy = migBusyPair[0]
3774
+ var setMigBusy = migBusyPair[1]
3775
+ var migErrStatePair = useState('')
3776
+ var migErr = migErrStatePair[0]
3777
+ var setMigErr = migErrStatePair[1]
3778
+ var migNotePair = useState('')
3779
+ var migNote = migNotePair[0]
3780
+ var setMigNote = migNotePair[1]
3781
+ var migPlanPair = useState(null)
3782
+ var migPlan = migPlanPair[0]
3783
+ var setMigPlan = migPlanPair[1]
3784
+ var migResultPair = useState(null)
3785
+ var migResult = migResultPair[0]
3786
+ var setMigResult = migResultPair[1]
3787
+ var migConflictPair = useState('keep')
3788
+ var migConflict = migConflictPair[0]
3789
+ var setMigConflict = migConflictPair[1]
3790
+ // ok===false 时把 error / errors / warnings 一并显示,绝不静默。
3791
+ function migFail(tag, j, e) {
3792
+ var parts = []
3793
+ if (e) parts.push(String(e && e.message ? e.message : e))
3794
+ if (j && j.error) parts.push(String(j.error))
3795
+ if (j && j.errors) parts.push(Array.isArray(j.errors) ? j.errors.join(' | ') : String(j.errors))
3796
+ if (j && j.warnings && j.warnings.length) parts.push((locale === 'zh' ? '警告: ' : 'warnings: ') + j.warnings.join(' | '))
3797
+ if (j && j.skipped && j.skipped.length) parts.push((locale === 'zh' ? '跳过: ' : 'skipped: ') + j.skipped.join(' | '))
3798
+ setMigErr(tag + ' ✗ ' + (parts.length ? parts.join(' · ') : (locale === 'zh' ? '未知错误' : 'unknown error')))
3799
+ }
3800
+ // 选择目录:沿用本文件的 pickDir 原生选择器形态;拿不到目录就提示手填输入框(不阻塞)。
3801
+ function migPickInto(setVal, setPicking) {
3802
+ setPicking(true); setMigErr('')
3803
+ apiPost(API.pickDir, {}).then(function (d) {
3804
+ setPicking(false)
3805
+ if (d && d.dir) { setVal(d.dir); setMigNote((locale === 'zh' ? '已选择目录: ' : 'folder picked: ') + d.dir) }
3806
+ else { setMigNote(locale === 'zh' ? '系统文件夹选择器不可用,请手动填写路径。' : 'Native folder picker unavailable — type the path manually.') }
3807
+ }).catch(function () {
3808
+ setPicking(false)
3809
+ setMigNote(locale === 'zh' ? '系统文件夹选择器不可用,请手动填写路径。' : 'Native folder picker unavailable — type the path manually.')
3810
+ })
3811
+ }
3812
+ function migExport(opts) {
3813
+ if (migBusy) return
3814
+ opts = opts || {}
3815
+ setMigBusy('export'); setMigErr(''); setMigNote('')
3816
+ apiPost(API.migrateExport, { outPath: migOut || undefined }).then(function (j) {
3817
+ setMigBusy('')
3818
+ if (!j || j.ok === false) { migFail(t('migExport'), j, null); return }
3819
+ var s = j.stats || {}
3820
+ setMigNote(t('migExport') + ' ✓ ' + String(j.path || '') +
3821
+ ' · ' + (locale === 'zh' ? '文件 ' : 'files ') + String(s.fileCount || 0) +
3822
+ ' · ' + (locale === 'zh' ? '原始 ' : 'raw ') + String(s.bytes || 0) + ' B' +
3823
+ ' · ' + (locale === 'zh' ? '压缩后 ' : 'packed ') + String(s.packedBytes || 0) + ' B' +
3824
+ (j.checksum ? ' · sha256 ' + String(j.checksum).slice(0, 12) : '') +
3825
+ (j.skipped && j.skipped.length ? ' · ' + (locale === 'zh' ? '跳过 ' : 'skipped ') + j.skipped.length : '') +
3826
+ (j.warnings && j.warnings.length ? ' · ' + (locale === 'zh' ? '警告 ' : 'warnings ') + j.warnings.length : ''))
3827
+ }).catch(function (e) { setMigBusy(''); migFail(t('migExport'), null, e) })
3828
+ }
3829
+ function migPreview() {
3830
+ if (migBusy) return
3831
+ if (!migPack) { setMigErr(t('migPickPack')); return }
3832
+ setMigBusy('preview'); setMigErr(''); setMigNote(''); setMigPlan(null); setMigResult(null)
3833
+ apiPost(API.migrateInspect, { packPath: migPack }).then(function (j) {
3834
+ setMigBusy('')
3835
+ if (!j || j.ok === false) { migFail(t('migPreview'), j, null); return }
3836
+ var p = j.plan || {}
3837
+ setMigPlan(p)
3838
+ setMigNote(t('migPreview') + ' ✓ ' + (p.pathChanged ? t('migPathChanged') : t('migSamePath')) +
3839
+ (j.sourceMissing ? (locale === 'zh' ? ' · ⚠ 源工作区已不存在' : ' · warning: source workspace missing') : ''))
3840
+ }).catch(function (e) { setMigBusy(''); migFail(t('migPreview'), null, e) })
3841
+ }
3842
+ function migApply() {
3843
+ if (migBusy) return
3844
+ if (!migPack) { setMigErr(t('migPickPack')); return }
3845
+ setMigBusy('apply'); setMigErr(''); setMigNote('')
3846
+ apiPost(API.migrateImport, { packPath: migPack, onConflict: migConflict }).then(function (j) {
3847
+ setMigBusy('')
3848
+ if (!j || j.ok === false) { migFail(t('migApply'), j, null); return }
3849
+ setMigResult(j)
3850
+ setMigNote(t('migApply') + ' ✓ ' + String(j.targetWs || ''))
3851
+ }).catch(function (e) { setMigBusy(''); migFail(t('migApply'), null, e) })
3852
+ }
3381
3853
  useEffect(function () {
3382
3854
  var alive = true
3383
3855
  fetch(API.storageManage).then(function (r) { return r.json() }).then(function (j) {
@@ -3444,7 +3916,60 @@ window.__ModuleLoader__.load({
3444
3916
  rows.push(h('div', { 'data-dam-hint': '', style: { marginTop: '12px', opacity: .7 } },
3445
3917
  (locale === 'zh' ? '最近动作: ' : 'recent: ') + audit.map(function (a) { return a.action }).join(', ')))
3446
3918
  }
3447
- return h('div', null, rows)
3919
+ // 迁移搬包(P4):StorageTab 的「迁移搬包」卡片 —— 导出 + 导入三步向导(选包 → 预览差异 → 确认导入)。
3920
+ // 版式:只用 flexWrap / 百分比 / calc 字号,长路径 wordBreak:break-all ⇒ 窄浮层与宽页签都不横向溢出。
3921
+ var migOutPlaceholder = locale === 'zh' ? '输出目录(留空 = 默认导出目录)' : 'output dir (blank = default export dir)'
3922
+ var migRows = []
3923
+ if (migErr) migRows.push(h('div', { 'data-dam-error': '', key: 'merr', style: { wordBreak: 'break-all' } }, migErr))
3924
+ if (migNote) migRows.push(h('div', { 'data-dam-hint': '', key: 'mnote', style: { wordBreak: 'break-all' } }, migNote))
3925
+ migRows.push(h(Card, { title: t('migTitle') },
3926
+ h('div', { 'data-dam-hint': '', style: { marginBottom: '6px', wordBreak: 'break-all' } }, t('migHint')),
3927
+ h('div', { 'data-dam-row': '' },
3928
+ h('input', { 'data-dam-input': '', value: migOut, placeholder: migOutPlaceholder,
3929
+ onChange: function (e) { setMigOut(e.target.value) }, style: { flex: '1 1 100%' } }),
3930
+ h('button', { 'data-dam-btn': '', disabled: !!migBusy, onClick: function () { migPickInto(setMigOut, setMigOutPicking) } },
3931
+ migOutPicking ? t('loading') : (locale === 'zh' ? '选择目录' : 'pick folder')),
3932
+ h('button', { 'data-dam-btn': '', disabled: !!migBusy, style: { opacity: migBusy ? .5 : 1 },
3933
+ onClick: function () { migExport() } }, migBusy === 'export' ? t('saving') : t('migExport'))),
3934
+ h('div', { 'data-dam-row': '', style: { marginTop: '6px' } },
3935
+ h('input', { 'data-dam-input': '', value: migPack, placeholder: t('migPickPack'),
3936
+ onChange: function (e) { setMigPack(e.target.value) }, style: { flex: '1 1 100%' } }),
3937
+ h('button', { 'data-dam-btn': '', disabled: !!migBusy, onClick: function () { migPickInto(setMigPack, setMigPackPicking) } },
3938
+ migPackPicking ? t('loading') : t('migPickPack')),
3939
+ h('button', { 'data-dam-btn': '', disabled: !!migBusy, style: { opacity: migBusy ? .5 : 1 },
3940
+ onClick: migPreview }, migBusy === 'preview' ? t('loading') : t('migPreview'))),
3941
+ migPlan ? h('div', { style: { wordBreak: 'break-all', fontSize: 'calc(11.5px * var(--dam-scale))' } },
3942
+ h('div', null, migPlan.pathChanged ? t('migPathChanged') : t('migSamePath')),
3943
+ h('div', { 'data-dam-hint': '' }, (locale === 'zh' ? '路径: ' : 'path: ') + String(migPlan.fromPath || '') + ' → ' + String(migPlan.toPath || '')),
3944
+ h('div', { 'data-dam-hint': '' }, (locale === 'zh' ? '工作区标识: ' : 'slug: ') + String(migPlan.fromSlug || '') + ' → ' + String(migPlan.toSlug || '')),
3945
+ h('div', null, (locale === 'zh' ? '新增 ' : 'additions ') + String((migPlan.additions || []).length) +
3946
+ ' · ' + (locale === 'zh' ? '覆盖 ' : 'overwrites ') + String((migPlan.overwrites || []).length) +
3947
+ ' · ' + (locale === 'zh' ? '将写入 ' : 'will write ') + String((migPlan.stats && migPlan.stats.willWrite) || 0) +
3948
+ ' · ' + (locale === 'zh' ? '重写命中 ' : 'rewrite hits ') + String((migPlan.rewrite && migPlan.rewrite.totalHits) || 0) +
3949
+ (locale === 'zh' ? ' 处 / 涉及文件 ' : ' across files ') + String((migPlan.rewrite && migPlan.rewrite.fileCount) || 0)),
3950
+ (migPlan.additions || []).length ? h('div', { 'data-dam-hint': '' },
3951
+ (locale === 'zh' ? '新增(前 10): ' : 'additions (top 10): ') + (migPlan.additions || []).slice(0, 10).map(function (a) { return String(a && a.path) + '(' + String((a && a.bytes) || 0) + 'B)' }).join(' , ')) : null,
3952
+ (migPlan.overwrites || []).length ? h('div', { 'data-dam-hint': '' },
3953
+ (locale === 'zh' ? '覆盖: ' : 'overwrites: ') + (migPlan.overwrites || []).map(function (o) { return String(o && o.path) + ' ' + String((o && o.oldBytes) || 0) + 'B→' + String((o && o.newBytes) || 0) + 'B' }).join(' , ')) : null,
3954
+ (migPlan.rewrite && (migPlan.rewrite.files || []).length) ? h('div', { 'data-dam-hint': '' },
3955
+ (locale === 'zh' ? '重写文件(前 10): ' : 'rewrite files (top 10): ') + (migPlan.rewrite.files || []).slice(0, 10).map(function (f) { return String(f && f.path) + '(' + String((f && f.hits) || 0) + ')' }).join(' , ')) : null,
3956
+ (migPlan.warnings && migPlan.warnings.length) ? h('div', { 'data-dam-hint': '' }, (locale === 'zh' ? '警告: ' : 'warnings: ') + migPlan.warnings.join(' | ')) : null,
3957
+ h('div', { 'data-dam-row': '', style: { marginTop: '6px' } },
3958
+ h('button', { 'data-dam-btn': '', style: { opacity: migConflict === 'keep' ? 1 : .5 }, onClick: function () { setMigConflict('keep') } }, (migConflict === 'keep' ? '● ' : '○ ') + t('migConflictKeep')),
3959
+ h('button', { 'data-dam-btn': '', style: { opacity: migConflict === 'overwrite' ? 1 : .5 }, onClick: function () { setMigConflict('overwrite') } }, (migConflict === 'overwrite' ? '● ' : '○ ') + t('migConflictOverwrite')),
3960
+ h('button', { 'data-dam-btn': '', style: { opacity: migConflict === 'rename' ? 1 : .5 }, onClick: function () { setMigConflict('rename') } }, (migConflict === 'rename' ? '● ' : '○ ') + t('migConflictRename'))),
3961
+ h('button', { 'data-dam-btn': '', disabled: !!migBusy, style: { marginTop: '6px', opacity: migBusy ? .5 : 1 }, onClick: migApply },
3962
+ migBusy === 'apply' ? t('saving') : t('migApply'))) : null,
3963
+ migResult ? h('div', { style: { wordBreak: 'break-all', fontSize: 'calc(11.5px * var(--dam-scale))' } },
3964
+ h('div', null, t('migWritten') + ': ' + String(migResult.written || 0) +
3965
+ ((migResult.writtenFiles || []).length ? ' (' + migResult.writtenFiles.slice(0, 10).join(' , ') + ')' : '')),
3966
+ h('div', { 'data-dam-hint': '' }, t('migBackup') + ': ' + String(migResult.backup || '')),
3967
+ h('div', { 'data-dam-hint': '' }, (locale === 'zh' ? '冲突策略: ' : 'conflict: ') + String(migResult.onConflict || migConflict) +
3968
+ ' · ' + (locale === 'zh' ? '目标工作区: ' : 'target: ') + String(migResult.targetWs || '')),
3969
+ h('div', null, String(migResult.summaryAction || '') + (migResult.summaryError ? ' ✗ ' + String(migResult.summaryError) : ''))) : null))
3970
+ rows.push(h('div', { 'data-dam-flow': '' }, migRows))
3971
+ // 大屏分列:卡片流页签(见 CSS [data-dam-flow]);非卡片子元素仍整行。
3972
+ return h('div', { 'data-dam-flow': '' }, rows)
3448
3973
  }
3449
3974
 
3450
3975
  function NotesTab() {
@@ -3872,7 +4397,8 @@ window.__ModuleLoader__.load({
3872
4397
  }
3873
4398
  // 2026-09-14 修复:开关卡此前只在「未启用」分支被返回 ⇒ 白板一旦开启,白板页就再也看不到开关(开得了、关不掉)。
3874
4399
  // 开关卡必须在**两种**状态下都渲染 —— 它是这个页面上唯一的开关入口。
3875
- return h('div', null, [switchCard].concat(rows))
4400
+ // 大屏分列:卡片流页签(见 CSS [data-dam-flow]);非卡片子元素仍整行。
4401
+ return h('div', { 'data-dam-flow': '' }, [switchCard].concat(rows))
3876
4402
  }
3877
4403
 
3878
4404
  function ReflectionsTab() {
@@ -3928,7 +4454,8 @@ window.__ModuleLoader__.load({
3928
4454
  })(data.reflections[i])
3929
4455
  }
3930
4456
  }
3931
- return h('div', null, rows)
4457
+ // 大屏分列:卡片流页签(见 CSS [data-dam-flow]);非卡片子元素仍整行。
4458
+ return h('div', { 'data-dam-flow': '' }, rows)
3932
4459
  }
3933
4460
 
3934
4461
  function SearchTab() {
@@ -3956,7 +4483,8 @@ window.__ModuleLoader__.load({
3956
4483
  apiPost(API.smartRecall, { query: q.trim() }).then(function (d) { setSmart(d); setBusy(false) })
3957
4484
  .catch(function (e) { setSmart({ answer: t('searchFailed') + e.message, keywords: [], hits: [] }); setBusy(false) })
3958
4485
  }
3959
- return h('div', null,
4486
+ // 大屏分列:卡片流页签(见 CSS [data-dam-flow]);非卡片子元素仍整行。
4487
+ return h('div', { 'data-dam-flow': '' },
3960
4488
  h('div', { 'data-dam-row': '' },
3961
4489
  h('input', { 'data-dam-input': '', placeholder: t('searchPlaceholder'), value: q, onChange: function (e) { setQ(e.target.value) }, onKeyDown: function (e) { if (e.key === 'Enter') search() } }),
3962
4490
  h('button', { 'data-dam-btn': '', onClick: search, disabled: busy }, t('searchBtn')),
@@ -3966,7 +4494,16 @@ window.__ModuleLoader__.load({
3966
4494
  h('div', { 'data-dam-content': '' }, smart.answer || t('empty')),
3967
4495
  smart.keywords && smart.keywords.length ? h('div', { 'data-dam-hint': '', style: { marginTop: '6px' } }, t('keywordsLabel') + smart.keywords.join(' / ')) : null,
3968
4496
  Array.isArray(smart.hits) && smart.hits.length ? h('div', { 'data-dam-hint': '', style: { marginTop: '6px' } },
3969
- smart.hits.map(function (hit, i) { return hit && typeof hit === 'object' ? h('div', { key: i, style: { marginTop: '3px', overflowWrap: 'anywhere' } }, '· [' + String(hit.where || '') + '] ' + String(hit.line || '')) : null })) : null)
4497
+ // ★P1-2 补接线(2026-09-22):宿主**早已**把结论层标记放进 smartRecall 的 hits 投影
4498
+ // (index.js:6469 → l0-extract.js:295+,文案形如「⚠已撤回」),前端此前零渲染
4499
+ // ⇒「这条结论已失效」在界面上完全看不出来,用户会把它当有效结论读。mark 自带文案 ⇒ 零 i18n。
4500
+ smart.hits.map(function (hit, i) {
4501
+ if (!hit || typeof hit !== 'object') return null
4502
+ var mark = hit.mark === undefined || hit.mark === null ? '' : String(hit.mark).trim()
4503
+ return h('div', { key: i, style: { marginTop: '3px', overflowWrap: 'anywhere' } },
4504
+ mark ? h('span', { 'data-dam-hint': '', style: { marginRight: '5px', fontWeight: 600, color: 'var(--dsw-alias-state-warn-primary, #e8c584)' } }, mark) : null,
4505
+ '· [' + String(hit.where || '') + '] ' + String(hit.line || ''))
4506
+ })) : null)
3970
4507
  : null,
3971
4508
  result ? h(Card, { title: t('resultTitle') }, h('div', { 'data-dam-content': '' }, result)) : null)
3972
4509
  }
@@ -4054,7 +4591,235 @@ window.__ModuleLoader__.load({
4054
4591
  }))))
4055
4592
  )
4056
4593
  }
4057
- function WorkspaceTab() {
4594
+ // ─────────────────────────────────────────────────────────────────────────
4595
+ // 统计页签(2026-09-22)—— 召回统计的可视化。
4596
+ //
4597
+ // 为什么单独一页:用户要「看哪个召回得更多、哪个更重要」,这是一眼看的活,
4598
+ // 不适合塞进已有 12 个页签的任意一个里(会把那一页的语义冲淡)。
4599
+ //
4600
+ // 立场:纯只读展示 + 一个清零按钮。**不改召回排序**(加权是后续独立批次,
4601
+ // 等这份数据证明口径可信之后再做)——故本页不做任何排序相关的写操作。
4602
+ //
4603
+ // 视觉:复用面板既有 --dam-* 令牌与 [data-dam-card] 入场,不引第三方图表库
4604
+ // (零构建环境下引库=多一个包+多一套主题;四种图手绘 SVG 足够表达)。
4605
+ // ─────────────────────────────────────────────────────────────────────────
4606
+
4607
+ /** 环形图:按层占比。用 stroke-dasharray 画弧,避免 path 三角计算。 */
4608
+ function DamDonut(props) {
4609
+ var items = props.items || []
4610
+ var total = items.reduce(function (n, it) { return n + (it.count || 0) }, 0)
4611
+ var R = 34, C = 2 * Math.PI * R
4612
+ var PALETTE = ['#5b8def', '#3fa96a', '#d4a94f', '#c061c0', '#4aa8b8', '#8a8f98']
4613
+ var acc = 0
4614
+ var arcs = items.slice(0, 6).map(function (it, i) {
4615
+ var frac = total ? (it.count || 0) / total : 0
4616
+ var seg = h('circle', {
4617
+ key: 'a' + i, cx: 44, cy: 44, r: R, fill: 'none',
4618
+ stroke: PALETTE[i % PALETTE.length], strokeWidth: 12,
4619
+ strokeDasharray: (frac * C) + ' ' + C,
4620
+ strokeDashoffset: -acc * C,
4621
+ transform: 'rotate(-90 44 44)',
4622
+ style: { animation: 'dam-stat-arc var(--dam-dur-slow) var(--dam-ease-out) both' },
4623
+ })
4624
+ acc += frac
4625
+ return seg
4626
+ })
4627
+ return h('div', { 'data-dam-stat-donut': '', style: { display: 'flex', gap: '14px', alignItems: 'center', flexWrap: 'wrap' } },
4628
+ h('svg', { width: 88, height: 88, viewBox: '0 0 88 88', role: 'img', 'aria-label': props.label }, arcs),
4629
+ h('div', { style: { display: 'flex', flexDirection: 'column', gap: '4px', minWidth: 0 } },
4630
+ items.slice(0, 6).map(function (it, i) {
4631
+ var pct = total ? Math.round(((it.count || 0) / total) * 100) : 0
4632
+ return h('div', { key: 'l' + i, style: { display: 'flex', alignItems: 'center', gap: '6px', fontSize: '12px' } },
4633
+ h('span', { style: { width: 8, height: 8, borderRadius: 8, background: PALETTE[i % PALETTE.length], flex: '0 0 auto' } }),
4634
+ h('span', null, it.key + ' · ' + it.count + ' (' + pct + '%)'))
4635
+ })))
4636
+ }
4637
+
4638
+ /** 横向柱状:召回最多的条目。条宽用百分比,随容器自适应。 */
4639
+ function DamBars(props) {
4640
+ var items = props.items || []
4641
+ var max = items.reduce(function (n, it) { return Math.max(n, it.count || 0) }, 0) || 1
4642
+ return h('div', { 'data-dam-stat-bars': '', style: { display: 'flex', flexDirection: 'column', gap: '6px' } },
4643
+ items.map(function (it, i) {
4644
+ var pct = Math.max(2, Math.round(((it.count || 0) / max) * 100))
4645
+ return h('div', { key: 'b' + i, style: { display: 'flex', alignItems: 'center', gap: '8px', fontSize: '12px' } },
4646
+ h('code', { style: { flex: '0 0 auto', opacity: .75, fontSize: '11px' } }, String(it.id || '').slice(0, 12)),
4647
+ h('div', { style: { flex: '1 1 auto', height: 8, borderRadius: 6, background: 'color-mix(in srgb, currentColor 10%, transparent)', overflow: 'hidden' } },
4648
+ h('div', {
4649
+ style: {
4650
+ height: '100%', width: pct + '%', borderRadius: 6,
4651
+ background: 'linear-gradient(90deg, #5b8def, #4aa8b8)',
4652
+ animation: 'dam-stat-grow var(--dam-dur-slow) var(--dam-ease-out) both',
4653
+ },
4654
+ })),
4655
+ h('span', { style: { flex: '0 0 auto', fontVariantNumeric: 'tabular-nums' } }, '×' + it.count),
4656
+ it.layer ? h('span', { style: { flex: '0 0 auto', opacity: .6, fontSize: '11px' } }, it.layer) : null)
4657
+ }))
4658
+ }
4659
+
4660
+ /** 折线 + 面积:按天走势。 */
4661
+ function DamSpark(props) {
4662
+ var days = props.items || []
4663
+ var W = 320, H = 68, P = 4
4664
+ if (!days.length) return h('div', { style: { opacity: .6, fontSize: '12px' } }, props.empty)
4665
+ var max = days.reduce(function (n, d) { return Math.max(n, d.count || 0) }, 0) || 1
4666
+ var step = days.length > 1 ? (W - P * 2) / (days.length - 1) : 0
4667
+ var pts = days.map(function (d, i) {
4668
+ var x = P + i * step
4669
+ var y = H - P - ((d.count || 0) / max) * (H - P * 2)
4670
+ return [x, y]
4671
+ })
4672
+ var line = pts.map(function (p, i) { return (i ? 'L' : 'M') + p[0].toFixed(1) + ' ' + p[1].toFixed(1) }).join(' ')
4673
+ var area = line + ' L' + (P + (days.length - 1) * step).toFixed(1) + ' ' + (H - P) + ' L' + P + ' ' + (H - P) + ' Z'
4674
+ return h('svg', {
4675
+ 'data-dam-stat-spark': '', width: '100%', height: H, viewBox: '0 0 ' + W + ' ' + H,
4676
+ preserveAspectRatio: 'none', role: 'img', 'aria-label': props.label,
4677
+ style: { animation: 'dam-stat-reveal var(--dam-dur-slow) var(--dam-ease-inout) both' },
4678
+ },
4679
+ h('path', { d: area, fill: 'color-mix(in srgb, #5b8def 22%, transparent)', stroke: 'none' }),
4680
+ h('path', { d: line, fill: 'none', stroke: '#5b8def', strokeWidth: 2, strokeLinejoin: 'round', vectorEffect: 'non-scaling-stroke' }))
4681
+ }
4682
+
4683
+ /** 热力格:天 × 次数。一眼看出「哪天被想起来的次数多」。 */
4684
+ function DamHeat(props) {
4685
+ var days = (props.items || []).slice(-42)
4686
+ if (!days.length) return h('div', { style: { opacity: .6, fontSize: '12px' } }, props.empty)
4687
+ var max = days.reduce(function (n, d) { return Math.max(n, d.count || 0) }, 0) || 1
4688
+ return h('div', { 'data-dam-stat-heat': '', style: { display: 'flex', flexWrap: 'wrap', gap: 3 } },
4689
+ days.map(function (d, i) {
4690
+ var t = (d.count || 0) / max
4691
+ return h('div', {
4692
+ key: 'h' + i, title: d.day + ' · ' + d.count,
4693
+ style: {
4694
+ width: 12, height: 12, borderRadius: 3,
4695
+ background: 'color-mix(in srgb, #5b8def ' + Math.round(18 + t * 82) + '%, transparent)',
4696
+ animation: 'dam-stat-pop var(--dam-dur-fast) var(--dam-ease-out) both',
4697
+ animationDelay: Math.min(i * 12, 240) + 'ms',
4698
+ },
4699
+ })
4700
+ }))
4701
+ }
4702
+
4703
+ /** 数字卡:用途=「数字弹入」⇒ --dam-dur-slow + --dam-ease-out + 2px 模糊(skill number-pop-in 判据)。 */
4704
+ function DamStat(props) {
4705
+ return h('div', { 'data-dam-stat': '', style: { display: 'flex', flexDirection: 'column', gap: 2, minWidth: 92 } },
4706
+ h('div', {
4707
+ style: {
4708
+ fontSize: '22px', fontWeight: 600, fontVariantNumeric: 'tabular-nums',
4709
+ animation: 'dam-stat-num var(--dam-dur-slow) var(--dam-ease-out) both',
4710
+ },
4711
+ }, String(props.value)),
4712
+ h('div', { style: { fontSize: '11px', opacity: .7 } }, props.label))
4713
+ }
4714
+
4715
+ function StatsTab() {
4716
+ var s = useState(null)
4717
+ var data = s[0], setData = s[1]
4718
+ var e = useState('')
4719
+ var err = e[0], setErr = e[1]
4720
+ var r = useState(0)
4721
+ var tick = r[0], setTick = r[1]
4722
+
4723
+ useEffect(function () {
4724
+ var alive = true
4725
+ fetch(API.recallStats)
4726
+ .then(function (res) { return res.json() })
4727
+ .then(function (j) { if (alive) setData(j) })
4728
+ .catch(function (ex) { if (alive) setErr(String((ex && ex.message) || ex)) })
4729
+ return function () { alive = false }
4730
+ }, [tick])
4731
+
4732
+ function reset() {
4733
+ fetch(API.recallStats + '?reset=1', { method: 'POST' })
4734
+ .then(function () { setTick(function (n) { return n + 1 }) })
4735
+ .catch(function (ex) { setErr(String((ex && ex.message) || ex)) })
4736
+ }
4737
+
4738
+ if (err) return h(Card, { title: t('statsTitle') }, h('div', { style: { opacity: .8 } }, err))
4739
+ // 骨架态:用途 =「骨架屏内容揭示」⇒ 脉冲用 --dam-dur-slow(与 skill 同用途判据一致)
4740
+ if (!data) return h(Card, { title: t('statsTitle') },
4741
+ h('div', { 'data-dam-stat-skeleton': '', style: { display: 'flex', flexDirection: 'column', gap: 8 } },
4742
+ [0, 1, 2].map(function (i) {
4743
+ return h('div', {
4744
+ key: 'sk' + i,
4745
+ style: {
4746
+ height: 14, borderRadius: 7, width: (70 - i * 12) + '%',
4747
+ background: 'color-mix(in srgb, currentColor 11%, transparent)',
4748
+ animation: 'dam-stat-pulse var(--dam-dur-slow) var(--dam-ease-linear) infinite',
4749
+ },
4750
+ })
4751
+ })))
4752
+ if (!data.enabled) return h(Card, { title: t('statsTitle') }, h('div', { style: { opacity: .75 } }, t('statsEmpty')))
4753
+
4754
+ var chs = data.channels || {}
4755
+ var chIds = data.channelIds || []
4756
+ // 三条通路各自一格;标签用中文/英文 i18n 键,绝不硬编码文案
4757
+ var META = {
4758
+ model: { name: t('statsChModel'), hint: t('statsChModelHint') },
4759
+ inject: { name: t('statsChInject'), hint: t('statsChInjectHint') },
4760
+ shadow: { name: t('statsChShadow'), hint: t('statsChShadowHint') },
4761
+ }
4762
+ var pick = function (id) { return chs[id] || {} }
4763
+ var model = pick('model'), inject = pick('inject'), shadow = pick('shadow')
4764
+ var totalEvents = chIds.reduce(function (n, id) { return n + (pick(id).events || 0) }, 0)
4765
+ var emptyAll = !totalEvents
4766
+ var since = data.since ? new Date(data.since).toLocaleString() : t('statsNever')
4767
+
4768
+ // 单条通路卡片:数字行 + 可选图表。**图表都走既有 Dam* 组件**(零新依赖)。
4769
+ function chCard(ch, id, extra) {
4770
+ var m = META[id] || {}
4771
+ var body = [
4772
+ h('div', { style: { opacity: .72, fontSize: '12px', marginBottom: 2 } }, m.hint),
4773
+ h('div', { style: { display: 'flex', gap: 18, flexWrap: 'wrap', marginTop: 4 } },
4774
+ h(DamStat, { value: ch.events || 0, label: t('statsEvents') }),
4775
+ h(DamStat, { value: ch.hits || 0, label: t('statsTotalHits') }),
4776
+ h(DamStat, { value: ch.distinct || 0, label: t('statsDistinct') }),
4777
+ h(DamStat, { value: ch.zeroHit || 0, label: t('statsZeroHit') })),
4778
+ ]
4779
+ return h(Card, { title: m.name }, h('div', { style: { display: 'flex', flexDirection: 'column', gap: 12 } },
4780
+ extra ? extra(ch) : null,
4781
+ body,
4782
+ (ch.top && ch.top.length) ? h('div', { style: { marginTop: 4 } }, h(DamBars, { items: ch.top.slice(0, 12) }))
4783
+ : h('div', { style: { opacity: .5, fontSize: '12px' } }, t('statsNoInject'))))
4784
+ }
4785
+
4786
+ return h('div', null,
4787
+ h(Card, { title: t('statsTitle') },
4788
+ h('div', { style: { display: 'flex', flexDirection: 'column', gap: 12 } },
4789
+ h('div', { style: { opacity: .75, fontSize: '12px' } }, t('statsSubtitle')),
4790
+ emptyAll ? h('div', { style: { opacity: .7 } }, t('statsEmpty')) : null,
4791
+ // 总览:三条通路各占多少(饼图)—— 一眼看出"哪一条在动"
4792
+ emptyAll ? null : h(DamDonut, {
4793
+ items: chIds.map(function (id) { return { id: (META[id] || {}).name || id, count: pick(id).events || 0 } }),
4794
+ label: t('statsChOverview'),
4795
+ }),
4796
+ h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 10 } },
4797
+ h('div', { style: { fontSize: '11px', opacity: .6 } }, t('statsSince') + ': ' + since),
4798
+ h('button', {
4799
+ type: 'button', onClick: reset,
4800
+ style: { fontSize: '12px', padding: '4px 10px', borderRadius: 8, cursor: 'pointer' },
4801
+ }, t('statsReset'))))),
4802
+ // ① 模型主动检索:高信号。附按层分布 + 按天走势
4803
+ chCard(model, 'model', function (ch) {
4804
+ return h('div', { style: { display: 'flex', flexDirection: 'column', gap: 10 } },
4805
+ (ch.byLayer && ch.byLayer.length) ? h(DamDonut, { items: ch.byLayer, label: t('statsByLayer') }) : null,
4806
+ (ch.byDay && ch.byDay.length) ? h(DamSpark, { items: ch.byDay, label: t('statsByDay'), empty: t('statsEmpty') }) : null,
4807
+ (ch.byDay && ch.byDay.length) ? h(DamHeat, { items: ch.byDay, empty: t('statsEmpty') }) : null)
4808
+ }),
4809
+ // ② 每轮注入:成本视角。**条长按字符数**(回答"哪一段最占预算")
4810
+ chCard(inject, 'inject', function (ch) {
4811
+ var segs = (ch.top || []).map(function (it) { return { id: it.id, count: it.score || 0, n: it.count || 0 } })
4812
+ segs.sort(function (a, b) { return b.count - a.count })
4813
+ if (!segs.length) return null
4814
+ return h('div', { style: { display: 'flex', flexDirection: 'column', gap: 6 } },
4815
+ h('div', { style: { opacity: .7, fontSize: '12px' } }, t('statsSegChars')),
4816
+ h(DamBars, { items: segs.slice(0, 14) }))
4817
+ }),
4818
+ // ③ 主动唤起:系统判断质量。没开 shadow 时该卡自然为 0,不谎报
4819
+ chCard(shadow, 'shadow', null))
4820
+ }
4821
+
4822
+ function WorkspaceTab() {
4058
4823
  var dataPair = useState(null)
4059
4824
  var data = dataPair[0]
4060
4825
  var setData = dataPair[1]
@@ -4088,7 +4853,8 @@ window.__ModuleLoader__.load({
4088
4853
  h('div', { 'data-dam-graph-toolbar': '' }, h('span', { 'data-dam-hint': '' }, locale === 'zh' ? '思维导图大小' : 'Map size'), h('input', { type: 'range', min: '0.55', max: '1.75', step: '0.05', value: graphScale, 'aria-label': locale === 'zh' ? '调整思维导图大小' : 'Adjust map size', onChange: function (e) { setGraphScale(Number(e.target.value) || 1.0) } }), h('span', { 'data-dam-hint': '' }, Math.round(graphScale * 100) + '%'), h('button', { 'data-dam-btn': '', title: locale === 'zh' ? '重置视图' : 'Reset view', onClick: function () { setGraphScale(1.0) } }, '⌂')),
4089
4854
  h('div', { 'data-dam-hint': '' }, locale === 'zh' ? '点击工作区卡片查看摘要;拖动滑块调整图的大小。' : 'Select a workspace card for details; use the slider to resize the map.'),
4090
4855
  h(WorkspaceGraph, { workspaces: workspaces, graph: data.graph, scale: graphScale, onSelect: function (ws) { setSelected(ws); var n = Object.assign({}, collapsedByPath); n[ws.path] = false; setCollapsedByPath(n) } }),
4091
- h('div', { style: { marginTop: '12px' } }, workspaces.map(function (ws) {
4856
+ // ★大屏分列(2026-09-22):工作区卡片(已带 [data-dam-card])在宽屏排成多列。
4857
+ h('div', { 'data-dam-flow': '', style: { marginTop: '12px' } }, workspaces.map(function (ws) {
4092
4858
  var open = collapsedByPath[ws.path] === false
4093
4859
  return h('div', { key: ws.path, 'data-dam-card': '', style: { marginBottom: '8px', padding: '0' } },
4094
4860
  h('button', { 'data-dam-btn': '', 'aria-expanded': open ? 'true' : 'false', onClick: function () { var n = Object.assign({}, collapsedByPath); n[ws.path] = !open; setCollapsedByPath(n); setSelected(ws) }, style: { width: '100%', textAlign: 'left', padding: '10px 12px', fontWeight: 650 } }, (open ? '▾ ' : '▸ ') + ws.name + (ws.dateRange ? ' · ' + ws.dateRange : '')),
@@ -4438,7 +5204,9 @@ window.__ModuleLoader__.load({
4438
5204
  : null)))
4439
5205
  })(sources[i])
4440
5206
  }
4441
- return h('div', null,
5207
+ // ★大屏分列(2026-09-22):本组件的 Card 由 `cards)` 直接铺进根节点 ⇒ 是根的**直接子元素**,
5208
+ // 在根上打 [data-dam-flow] 即可生效(与日志/中枢页签同款)。非卡片子元素(hint/按钮行/msg/err)仍整行。
5209
+ return h('div', { 'data-dam-flow': '' },
4442
5210
  h('div', { 'data-dam-hint': '' }, t('connectHint')),
4443
5211
  h('div', { 'data-dam-row': '' },
4444
5212
  h('button', { 'data-dam-btn': '', onClick: importAll }, t('importAll')),
@@ -4524,12 +5292,98 @@ window.__ModuleLoader__.load({
4524
5292
  h('button', { 'data-dam-tabs-arrow': '', title: locale === 'zh' ? '向右滚动模块' : 'Scroll modules right', 'aria-label': locale === 'zh' ? '向右滚动模块' : 'Scroll modules right', disabled: !canRight, onClick: function () { step(1) } }, '›'))
4525
5293
  }
4526
5294
 
5295
+ // ══════════════════════════════════════════════════════════════════════════
5296
+ // 共享渲染件(2026-09-21):浮层与「记忆」会话页复用同一份页签表与内容体。
5297
+ // 抽出来的动机:两种承载面必须"同内容、同页签、同数据"(用户要求同步),
5298
+ // 若各写一份 body 分支,页签一多必然漂移 ⇒ 只留一处事实。
5299
+ // ══════════════════════════════════════════════════════════════════════════
5300
+ function MemoryTabBody(tab, nonce) {
5301
+ if (tab === 'overview') return h(OverviewTab, { nonce: nonce })
5302
+ if (tab === 'logs') return h(LogsTab)
5303
+ if (tab === 'refine') return h(RefineTab)
5304
+ if (tab === 'hub') return h(MemoryHubTab, { nonce: nonce })
5305
+ if (tab === 'storage') return h(StorageTab, { nonce: nonce })
5306
+ if (tab === 'notes') return h(NotesTab)
5307
+ if (tab === 'plan') return h(PlanTab)
5308
+ if (tab === 'reflections') return h(ReflectionsTab)
5309
+ if (tab === 'connect') return h(ConnectTab)
5310
+ if (tab === 'calendar') return h(CalendarTab)
5311
+ if (tab === 'workspaces') return h(WorkspaceTab)
5312
+ if (tab === 'stats') return h(StatsTab)
5313
+ return h(SearchTab)
5314
+ }
5315
+ function MEMORY_TABS() {
5316
+ return [['overview', t('overview')], ['logs', t('logs')], ['refine', t('refineTab')], ['hub', t('hubTab')], ['storage', t('storageTab')], ['notes', t('notes')], ['plan', t('planTab')], ['reflections', t('reflections')], ['connect', t('connect')], ['calendar', t('calendar')], ['search', t('search')], ['workspaces', t('workspaces')], ['stats', t('statsTab')]]
5317
+ }
5318
+
5319
+ // 「记忆」会话页(2026-09-21 二次修正) —— 注册在 conversation.view 槽位,与「对话轨迹」「上下文」「白板看板」并列。
5320
+ // 用户诉求原话:「它现在是浮在整个页面上方的,而不是和对话轨迹、上下文、白板看板在一起,作为一个单独的一页」。
5321
+ // 宿主只给 shell.overlay 这类"盖在界面上"的槽位,做不出并列页 ⇒ 必须走 conversation.view。
5322
+ // 与浮层的关系:同一份 panelTab/数据/刷新 nonce,切页签两边同步;只在承载面上不同(页更宽)。
5323
+ function MemoryPageView(props) {
5324
+ var tick = useTick()
5325
+ var tab = controller.panelTab()
5326
+ var noncePair = useState(0)
5327
+ var nonce = noncePair[0]
5328
+ var setNonce = noncePair[1]
5329
+ var verPair = useState(null)
5330
+ var ver = verPair[0]
5331
+ var setVer = verPair[1]
5332
+ useEffect(function () { return controller.subscribe(tick[1]) }, [])
5333
+ useEffect(function () {
5334
+ var alive = true
5335
+ apiGet(API.updateCheck).then(function (d) { if (alive && d && d.current) setVer(d.current) }).catch(function () {})
5336
+ return function () { alive = false }
5337
+ }, [])
5338
+ var zh = locale === 'zh'
5339
+ var verBadge = ver ? 'v' + ver + '-ui' : ''
5340
+ var body = MemoryTabBody(tab, nonce)
5341
+ // 浮层开关(page ↔ both 互切):两个方向都不注销本页签(修自毁按钮)。
5342
+ var floatOn = controller.panelPos() === 'both'
5343
+ var floatLabel = floatOn ? (zh ? '收起浮层' : 'Hide card') : (zh ? '打开浮层' : 'Open card')
5344
+ var floatTitle = floatOn ? (zh ? '收起左下角浮层(保留本页)' : 'Hide bottom-left card (keep this page)') : (zh ? '同时打开左下角浮层(保留本页)' : 'Also open bottom-left card (keep this page)')
5345
+ var onFloatToggle = function () { controller.setPanelPos(floatOn ? 'page' : 'both') }
5346
+ var scaleAttr = fontScale in FONT_SCALES ? fontScale : 'md'
5347
+ return h('div', {
5348
+ 'data-dam-page': '',
5349
+ 'data-scale': scaleAttr,
5350
+ style: {
5351
+ '--dam-user-scale': FONT_SCALE_VALUES[fontScale] || '1',
5352
+ '--dam-accent': ACCENT_VALUES[accentTheme] || ACCENT_VALUES.deepseek,
5353
+ },
5354
+ },
5355
+ h('header', { 'data-dam-page-head': '' },
5356
+ h('strong', null, t('autoMemory')),
5357
+ h('span', { 'data-dam-hint': '', style: { fontSize: '10px', opacity: .55 } }, verBadge),
5358
+ h('span', { className: 'dam-spacer' }),
5359
+ h('button', { 'data-dam-btn': '', title: t('refresh'), onClick: function () { setNonce(nonce + 1) } }, '⟳'),
5360
+ // ★2026-09-22 修「自毁按钮」(方向文档 §3.1 硬性要求 1):原实现在 page 档点一下即把本页签注销
5361
+ // (conversation.view 仅在 page/both 档注册)⇒ 用户实测「上面的东西连按钮一起消失」。
5362
+ // 现改为 page ↔ both 互切:both 档页签始终保留,两个方向都不注销当前页。
5363
+ h('button', {
5364
+ 'data-dam-btn': '', 'data-dam-float-toggle': floatOn ? 'on' : 'off',
5365
+ title: floatTitle,
5366
+ onClick: onFloatToggle,
5367
+ }, floatLabel)),
5368
+ // ★2026-09-22 版式:顶部横向滚动页签 → 左侧固定侧栏(用户原话「把原本放在上面的那个切换横条
5369
+ // 放到左侧,这样排版会更科学一点」)。浮层仍用 TabScroller(窄容器保持原版式),
5370
+ // 两者共享同一份 panelTab 状态 ⇒ 切分区两边同步(既有契约不变)。
5371
+ h('div', { 'data-dam-page-main': '' },
5372
+ h('nav', { 'data-dam-page-nav': '', 'aria-label': zh ? '记忆分区' : 'Memory sections' },
5373
+ MEMORY_TABS().map(function (item) {
5374
+ return h('button', { key: item[0], 'data-dam-nav-item': '', 'data-active': tab === item[0] ? 'true' : undefined, onClick: function () { controller.setPanelTab(item[0]) } }, item[1])
5375
+ })),
5376
+ h('div', { 'data-dam-page-content': '' },
5377
+ h('div', { 'data-dam-body': '' }, body))))
5378
+ }
5379
+
4527
5380
  function MemoryPanel() {
4528
5381
  var tick = useTick()
4529
5382
  var panelRef = useRef(null)
4530
- var tabPair = useState('overview')
4531
- var tab = tabPair[0]
4532
- var setTab = tabPair[1]
5383
+ // 页签状态走模块级共享(2026-09-21):浮层与「记忆」会话页读同一个 panelTab ⇒ 两处切页互相同步。
5384
+ // (用户诉求原话:「两者共存…要保证同步」。不用两份 useState,从根上杜绝状态漂移。)
5385
+ var tab = controller.panelTab()
5386
+ var setTab = controller.setPanelTab
4533
5387
  // 刷新 nonce:每次打开面板 / 点 ⟳ 时递增,驱动各页签重拉数据
4534
5388
  var noncePair = useState(0)
4535
5389
  var nonce = noncePair[0]
@@ -4538,6 +5392,7 @@ window.__ModuleLoader__.load({
4538
5392
  var ver = verPair[0]
4539
5393
  var setVer = verPair[1]
4540
5394
  var g = controller.geom()
5395
+ var pos = controller.panelPos()
4541
5396
  useEffect(function () { return controller.subscribe(tick[1]) }, [])
4542
5397
  // 真实版本号(#15):面板头徽标不再硬编码,从 update-check 的 current(读包 version)取,失败留空
4543
5398
  useEffect(function () {
@@ -4549,81 +5404,91 @@ window.__ModuleLoader__.load({
4549
5404
  // 主题令牌 --dsw-alias-bg-layer-2 本身就是半透明的,面板文字几乎不可读。判别信号用
4550
5405
  // 「令牌自身的 alpha」(增强模式显著低于普通模式的 0.86-0.9):< 0.65 时把面板背景提升到
4551
5406
  // 0.96(保留色相)并弱化顶部高光。普通模式令牌不透明 → 不触发,液态玻璃观感零变化。
5407
+ // 2026-09-21:两种形态(浮层/顶部停靠条)各查一次 —— panelRef 只绑浮层,停靠条用选择器兜底,
5408
+ // 否则增强模式下顶栏模式会保持半透明、可读性退化。
4552
5409
  useEffect(function () {
4553
5410
  if (!panelOpen) return
4554
- var el = panelRef.current
4555
- if (!el) return
4556
- try {
4557
- var cs = getComputedStyle(el)
4558
- var token = cs.getPropertyValue('--dsw-alias-bg-layer-2')
4559
- var col = parseCssColor((token || '').trim() || cs.backgroundColor)
4560
- if (col && col.a < 0.65) {
4561
- el.style.background = 'rgba(' + col.r + ', ' + col.g + ', ' + col.b + ', 0.96)'
4562
- el.setAttribute('data-solid', 'true')
4563
- } else {
4564
- el.style.background = ''
4565
- el.removeAttribute('data-solid')
4566
- }
4567
- } catch (e) {}
5411
+ // 2026-09-21 二次修正:原「顶部停靠条」第二形态已废弃(改走 conversation.view 会话页),
5412
+ // 故此处只剩浮层一个元素需要兜底 —— 会话页在正常文档流里,背景由宿主给,无需提升不透明度。
5413
+ var els = panelRef.current ? [panelRef.current] : []
5414
+ for (var j = 0; j < els.length; j++) {
5415
+ var el = els[j]
5416
+ if (!el) continue
5417
+ try {
5418
+ var cs = getComputedStyle(el)
5419
+ var token = cs.getPropertyValue('--dsw-alias-bg-layer-2')
5420
+ var col = parseCssColor((token || '').trim() || cs.backgroundColor)
5421
+ if (col && col.a < 0.65) {
5422
+ el.style.background = 'rgba(' + col.r + ', ' + col.g + ', ' + col.b + ', 0.96)'
5423
+ el.setAttribute('data-solid', 'true')
5424
+ } else {
5425
+ el.style.background = ''
5426
+ el.removeAttribute('data-solid')
5427
+ }
5428
+ } catch (eEl) {}
5429
+ }
4568
5430
  }, [panelOpen])
4569
5431
  if (!panelOpen && !panelClosing) return null
4570
- var body
4571
- if (tab === 'overview') body = h(OverviewTab, { nonce: nonce })
4572
- else if (tab === 'logs') body = h(LogsTab)
4573
- else if (tab === 'refine') body = h(RefineTab)
4574
- else if (tab === 'hub') body = h(MemoryHubTab, { nonce: nonce })
4575
- else if (tab === 'storage') body = h(StorageTab, { nonce: nonce })
4576
- else if (tab === 'notes') body = h(NotesTab)
4577
- else if (tab === 'plan') body = h(PlanTab)
4578
- else if (tab === 'reflections') body = h(ReflectionsTab)
4579
- else if (tab === 'connect') body = h(ConnectTab)
4580
- else if (tab === 'calendar') body = h(CalendarTab)
4581
- else if (tab === 'workspaces') body = h(WorkspaceTab)
4582
- else body = h(SearchTab)
4583
- var tabs = [['overview', t('overview')], ['logs', t('logs')], ['refine', t('refineTab')], ['hub', t('hubTab')], ['storage', t('storageTab')], ['notes', t('notes')], ['plan', t('planTab')], ['reflections', t('reflections')], ['connect', t('connect')], ['calendar', t('calendar')], ['search', t('search')], ['workspaces', t('workspaces')]]
4584
- var style = {
4585
- left: g.left + 'px',
4586
- top: g.top + 'px',
4587
- width: g.width + 'px',
4588
- height: g.height + 'px',
4589
- '--dam-scale': FONT_SCALE_VALUES[fontScale] || '1',
4590
- '--dam-accent': ACCENT_VALUES[accentTheme] || ACCENT_VALUES.deepseek,
4591
- }
5432
+ var body = MemoryTabBody(tab, nonce)
5433
+ var tabs = MEMORY_TABS()
5434
+ // 承载面(2026-09-21):①左下角浮层(可拖拽/缩放) ②「记忆」会话页(conversation.view,与「对话轨迹」并列)。
5435
+ // 两者共用同一份 tab(模块级 panelTab)/数据/刷新 nonce ⇒ 天然同步(用户要求);本函数只负责浮层节点。
4592
5436
  var dragMove = startPointerDrag(function (dx, dy) {
4593
5437
  controller.setGeom({ left: g.left + dx, top: g.top + dy })
4594
5438
  }, '[data-dam-btn], [data-dam-tab], [data-dam-input], [data-dam-select], textarea')
4595
5439
  var resizeMove = startPointerDrag(function (dx, dy) {
4596
5440
  controller.setGeom({ width: g.width + dx, height: g.height + dy })
4597
5441
  })
4598
- return h('div', {
4599
- 'data-dam-panel': '',
4600
- 'data-scale': fontScale in FONT_SCALES ? fontScale : 'md',
4601
- ref: panelRef,
4602
- style: style,
4603
- 'data-closing': panelClosing ? 'true' : undefined,
4604
- 'data-dragging': dragActive ? 'true' : undefined,
4605
- },
4606
- h('header', {
4607
- title: t('dragMove'),
4608
- onPointerDown: dragMove,
5442
+ var scaleAttr = fontScale in FONT_SCALES ? fontScale : 'md'
5443
+ var verBadge = ver ? 'v' + ver + '-ui' : ''
5444
+ var nodes = []
5445
+ if (pos === 'bottom-left' || pos === 'both') {
5446
+ var floatStyle = {
5447
+ left: g.left + 'px',
5448
+ top: g.top + 'px',
5449
+ width: g.width + 'px',
5450
+ height: g.height + 'px',
5451
+ '--dam-scale': FONT_SCALE_VALUES[fontScale] || '1',
5452
+ '--dam-accent': ACCENT_VALUES[accentTheme] || ACCENT_VALUES.deepseek,
5453
+ }
5454
+ nodes.push(h('div', {
5455
+ key: 'float',
5456
+ 'data-dam-panel': '',
5457
+ 'data-pos': 'float',
5458
+ 'data-scale': scaleAttr,
5459
+ ref: panelRef,
5460
+ style: floatStyle,
5461
+ 'data-closing': panelClosing ? 'true' : undefined,
5462
+ 'data-dragging': dragActive ? 'true' : undefined,
4609
5463
  },
4610
- h('strong', null, t('autoMemory')),
4611
- h('span', { 'data-dam-hint': '', style: { fontSize: '10px', opacity: .55 } }, ver ? 'v' + ver + '-ui' : ''),
4612
- h('span', { className: 'dam-spacer' }),
4613
- // 悬浮钉(2026-09-08):钉住后点面板外不再自动收起;图标为线描 SVG,与 ⤾ ⟳ ✕ 同画风
4614
- h('button', {
4615
- 'data-dam-btn': '', 'data-pin': pinned ? 'on' : 'off',
4616
- title: pinned ? t('unpinPanel') : t('pinPanel'),
4617
- 'aria-pressed': pinned ? 'true' : 'false',
4618
- style: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', padding: '4px 6px' },
4619
- onClick: function () { controller.togglePin() },
4620
- }, h(PinIcon, { filled: pinned })),
4621
- h('button', { 'data-dam-btn': '', title: t('resetPos'), onClick: function () { controller.resetGeom() } }, '⤾'),
4622
- h('button', { 'data-dam-btn': '', title: t('refresh'), onClick: function () { setNonce(nonce + 1) } }, '⟳'),
4623
- h('button', { 'data-dam-btn': '', title: t('close'), onClick: function () { controller.close() } }, '✕')),
4624
- h(TabScroller, { tabs: tabs, tab: tab, setTab: setTab }),
4625
- h('div', { 'data-dam-body': '' }, body),
4626
- h('div', { 'data-dam-resize': '', title: t('dragResize'), onPointerDown: resizeMove }))
5464
+ h('header', {
5465
+ title: t('dragMove'),
5466
+ onPointerDown: dragMove,
5467
+ },
5468
+ h('strong', null, t('autoMemory')),
5469
+ h('span', { 'data-dam-hint': '', style: { fontSize: '10px', opacity: .55 } }, verBadge),
5470
+ h('span', { className: 'dam-spacer' }),
5471
+ // 悬浮钉(2026-09-08):钉住后点面板外不再自动收起;图标为线描 SVG,与 ⤾ ⟳ ✕ 同画风
5472
+ h('button', {
5473
+ 'data-dam-btn': '', 'data-pin': pinned ? 'on' : 'off',
5474
+ title: pinned ? t('unpinPanel') : t('pinPanel'),
5475
+ 'aria-pressed': pinned ? 'true' : 'false',
5476
+ style: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', padding: '4px 6px' },
5477
+ onClick: function () { controller.togglePin() },
5478
+ }, h(PinIcon, { filled: pinned })),
5479
+ h('button', { 'data-dam-btn': '', title: t('resetPos'), onClick: function () { controller.resetGeom() } }, '⤾'),
5480
+ h('button', { 'data-dam-btn': '', title: t('refresh'), onClick: function () { setNonce(nonce + 1) } }, '⟳'),
5481
+ h('button', { 'data-dam-btn': '', title: t('close'), onClick: function () { controller.close() } }, '✕')),
5482
+ h(TabScroller, { key: 'ftabs', tabs: tabs, tab: tab, setTab: setTab }),
5483
+ h('div', { key: 'fbody', 'data-dam-body': '' }, body),
5484
+ h('div', { 'data-dam-resize': '', title: t('dragResize'), onPointerDown: resizeMove })))
5485
+ }
5486
+ // ★2026-09-21 二次修正:「顶部通栏覆盖条」形态已**废弃**。
5487
+ // 用户实测反馈原话:「它现在是浮在整个页面上方的,而不是和对话轨迹、上下文、白板看板在一起,作为一个单独的一页」。
5488
+ // 根因:宿主只给插件 4 个槽位(sidebar/main/rightbar/shell.overlay),放在 overlay 里的东西**只能盖在界面上**,
5489
+ // 做不出「并列的一页」;要成为并列页必须注册到 conversation.view(见下面 MemoryPageView + registerSurfaces)。
5490
+ // 故此处只剩浮层一个节点;'page' / 'both' 档的「会话页」由 conversation.view 槽位承载。
5491
+ return nodes.length === 1 ? nodes[0] : h('div', { 'data-dam-panel-host': '' }, nodes)
4627
5492
  }
4628
5493
 
4629
5494
  // ───────────────────────── 调试中心(为提 issue 提供诊断) ─────────────────────────
@@ -4686,6 +5551,100 @@ window.__ModuleLoader__.load({
4686
5551
  var hb = data.heartbeat || {}
4687
5552
  var hbAge = hb.exists && hb.heartbeatAt ? Math.round((Date.now() - hb.heartbeatAt) / 1000) : -1
4688
5553
  var apiBad = probes && Object.keys(probes).some(function (k) { return probes[k].status === 404 || probes[k].error })
5554
+ // ★P0-2(2026-09-22):hub 三层记忆(episodes/facts/procedures)落盘 IO 健康度。
5555
+ // 宿主 debugInfo().associativeMemory.hubIo 早已产出(lib/index.js:6959 → _hubIoViewSnapshot),
5556
+ // 但前端零消费者 ⇒ save 失败被静默吞掉时,面板/日志/返回值三处都看不出「记忆到底写进磁盘了没有」。
5557
+ // 防御式取值:宿主未升级 / 字段缺失时整行不渲染,绝不因此报错。
5558
+ var am = data.associativeMemory && typeof data.associativeMemory === 'object' ? data.associativeMemory : {}
5559
+ var hubIo = am.hubIo && typeof am.hubIo === 'object' ? am.hubIo : null
5560
+ var hubIoWarn = false
5561
+ var hubIoText = ''
5562
+ if (hubIo) {
5563
+ var hubErrs = Number(hubIo.errors) || 0
5564
+ var hubLoads = Number(hubIo.loads) || 0
5565
+ var hubSaves = Number(hubIo.saves) || 0
5566
+ var hubClears = Number(hubIo.clears) || 0
5567
+ hubIoWarn = hubErrs > 0 || hubIo.verdict === 'io-error'
5568
+ if (hubErrs > 0) {
5569
+ hubIoText = (locale === 'zh' ? '⚠ 累计失败 ' + hubErrs + ' 次 · 最近: ' : '⚠ ' + hubErrs + ' failure(s) · latest: ') +
5570
+ (hubIo.lastError || t('unknown')) +
5571
+ (hubIo.lastErrorAt ? ' (' + new Date(hubIo.lastErrorAt).toLocaleTimeString() + ')' : '')
5572
+ } else if (hubLoads + hubSaves + hubClears > 0) {
5573
+ hubIoText = locale === 'zh'
5574
+ ? '✓ 正常(读 ' + hubLoads + ' · 写 ' + hubSaves + ' · 清 ' + hubClears + ',共 ' + (hubLoads + hubSaves + hubClears) + ' 次落盘操作无失败)'
5575
+ : '✓ healthy (load ' + hubLoads + ' · save ' + hubSaves + ' · clear ' + hubClears + ', ' + (hubLoads + hubSaves + hubClears) + ' ops, 0 failures)'
5576
+ } else {
5577
+ hubIoText = locale === 'zh' ? '尚无落盘动作(本次运行还没有写入/读取)' : 'no disk activity yet this run'
5578
+ }
5579
+ // 无失败时优先采用宿主给出的中文整句 summary(如「三层记忆已正常落盘(本轮无写入失败)」)——
5580
+ // 不在前端重拼一遍人话,避免两侧口径分叉。有失败时保留上面的详细行(含条数与人话原因 + 时间)。
5581
+ if (hubErrs === 0 && hubIo.summary) hubIoText = String(hubIo.summary)
5582
+ }
5583
+ var hubIoByFile = hubIo && hubIoWarn && hubIo.byFile && typeof hubIo.byFile === 'object'
5584
+ ? Object.keys(hubIo.byFile).map(function (k) {
5585
+ var rec = hubIo.byFile[k] || {}
5586
+ return k + ' × ' + (Number(rec.count) || 0)
5587
+ }).join(' ')
5588
+ : ''
5589
+ // ★P0-3(2026-09-22):事实保留上限淘汰台账。
5590
+ // getLastPrune()/retentionLimit()(fact-store.js:830/834)此前在宿主与前端**双侧**零调用方,
5591
+ // 「事实被淘汰了」无处可查。只展示聚合数(条数/上限/超额/最旧时间),不展示任何事实正文。
5592
+ var fp = am.factsPrune && typeof am.factsPrune === 'object' ? am.factsPrune : null
5593
+ var fpWarn = false
5594
+ var fpText = ''
5595
+ if (fp) {
5596
+ var fpPruned = Number(fp.pruned) || 0
5597
+ var fpLimit = fp.limit === undefined || fp.limit === null ? '—' : fp.limit
5598
+ fpWarn = fpPruned > 0
5599
+ if (fpPruned > 0) {
5600
+ fpText = locale === 'zh'
5601
+ ? '上次淘汰 ' + fpPruned + ' 条(上限 ' + fpLimit + (fp.over ? ',超出 ' + fp.over : '') + ')' +
5602
+ (fp.protected ? ' · 另有 ' + fp.protected + ' 条重要/未撤销条目受保护未删' : '') +
5603
+ (fp.at ? ' · ' + new Date(fp.at).toLocaleTimeString() : '')
5604
+ : 'last prune removed ' + fpPruned + ' (cap ' + fpLimit + (fp.over ? ', over ' + fp.over : '') + ')' +
5605
+ (fp.protected ? ' · ' + fp.protected + ' protected (important/unrevoked) kept' : '') +
5606
+ (fp.at ? ' · ' + new Date(fp.at).toLocaleTimeString() : '')
5607
+ } else {
5608
+ fpText = locale === 'zh'
5609
+ ? '✓ 本次运行未发生淘汰(上限 ' + fpLimit + ')'
5610
+ : '✓ no eviction yet this run (cap ' + fpLimit + ')'
5611
+ }
5612
+ }
5613
+ // ★P1-1 补接线(2026-09-22):Python 语义侧车看门狗 + stderr 尾部。
5614
+ // 宿主 debugInfo().associativeMemory.pythonBackend 早已产出(lib/index.js:6944 →
5615
+ // Object.assign({ enabled }, client.debugView())),前端此前零渲染 ⇒ 侧车被看门狗
5616
+ // 击杀/重生、以及 Python traceback 原文,用户一概看不到;现象只剩「语义唤回突然不好使了」,
5617
+ // 无法归因。★铁律:Python 档是可选进阶项,未启用**不影响** JS 端语义 —— 文案必须写明,
5618
+ // 否则用户会误判成「记忆坏了」。防御式取值:宿主未升级/字段缺失时整块不渲染,绝不报错。
5619
+ var pySide = am.pythonBackend && typeof am.pythonBackend === 'object' ? am.pythonBackend : null
5620
+ var pySideWarn = false
5621
+ var pySideState = ''
5622
+ var pySideWd = ''
5623
+ var pySideTail = ''
5624
+ var pySideHasTail = false
5625
+ var pySideTrunc = false
5626
+ if (pySide) {
5627
+ var pyKills = Number(pySide.watchdog && pySide.watchdog.kills) || 0
5628
+ var pyRespawns = Number(pySide.watchdog && pySide.watchdog.respawns) || 0
5629
+ var pyGen = Number(pySide.generation) || 0
5630
+ var pyReason = pySide.watchdog && pySide.watchdog.lastReason ? String(pySide.watchdog.lastReason) : ''
5631
+ var pyBytes = Number(pySide.stderrTailBytes) || 0
5632
+ pySideWarn = pyKills > 0
5633
+ pySideState = pySide.enabled !== true
5634
+ ? t('kvPyOff')
5635
+ : (pySide.started === true
5636
+ ? t('kvPyAlive') + (pyGen > 0 ? (locale === 'zh' ? '(第 ' + pyGen + ' 代)' : ' (gen ' + pyGen + ')') : '')
5637
+ : (locale === 'zh' ? '未运行(惰性启动:首次请求才拉起进程)' : 'not running (lazy: spawned on first request)'))
5638
+ pySideWd = pyKills === 0
5639
+ ? (locale === 'zh' ? '无(未触发)' : 'none')
5640
+ : (locale === 'zh'
5641
+ ? '击杀 ' + pyKills + ' 次 · 重生 ' + pyRespawns + ' 次' + (pyReason ? '(最近:' + pyReason + ')' : '')
5642
+ : 'kills ' + pyKills + ' · respawns ' + pyRespawns + (pyReason ? ' (latest: ' + pyReason + ')' : ''))
5643
+ if (pySide.lastStderrTail) { pySideTail = String(pySide.lastStderrTail); pySideHasTail = true }
5644
+ else if (pyBytes === 0) pySideTail = locale === 'zh' ? '确实没有输出(不是被截断)' : 'no output at all (not truncated)'
5645
+ else pySideTail = locale === 'zh' ? '有 stderr 输出但无可显示行(全为空白行)' : 'stderr output present but no printable line (blank only)'
5646
+ pySideTrunc = pySide.lastStderrTailTruncated === true
5647
+ }
4689
5648
  return h('div', null,
4690
5649
  h('div', { 'data-dam-row': '' },
4691
5650
  h('button', { 'data-dam-btn': '', onClick: refresh, disabled: busy }, busy ? t('dbgRefreshing') : t('dbgRefresh')),
@@ -4702,6 +5661,35 @@ window.__ModuleLoader__.load({
4702
5661
  kv(t('kvMem'), t('kvMemUser') + ' ' + (data.memoryFiles.user.exists ? fmtSize(data.memoryFiles.user.size) : t('kvMemMissing')) + ' · ' + t('kvMemNotes') + ' ' + (data.memoryFiles.notes.exists ? fmtSize(data.memoryFiles.notes.size) : t('kvMemMissing')) + ' · ' + t('kvMemLog') + ' ' + (data.memoryFiles.log.exists ? fmtSize(data.memoryFiles.log.size) : t('kvMemMissing')), !data.memoryFiles.log.exists),
4703
5662
  kv(t('kvWs'), currentWs() || t('kvWsUnknown'), false),
4704
5663
  kv(t('kvApi'), probes ? Object.keys(probes).map(function (k) { return k + '=' + (probes[k].status !== undefined ? probes[k].status : probes[k].error) }).join(' ') : '…', apiBad),
5664
+ hubIo ? kv(t('dbgHubIo'), hubIoText, hubIoWarn) : null,
5665
+ // ★2026-09-22(用户拍板):诊断日志只在**这里**看,不再往控制台刷。
5666
+ // 路径可选中复制;含「崩溃时附这个文件」的人话提示。只读投影,不含日志正文。
5667
+ (function () {
5668
+ var lg = data.logs && typeof data.logs === 'object' ? data.logs : null
5669
+ if (!lg) return null
5670
+ if (!lg.exists) return kv(t('dbgLogs'), t('dbgLogsNone'), false)
5671
+ var lgText = []
5672
+ if (lg.sizeText) lgText.push(lg.sizeText)
5673
+ if (lg.lines !== null && lg.lines !== undefined) lgText.push(lg.lines + ' ' + (locale === 'zh' ? '行' : 'lines'))
5674
+ if (lg.historyBytes > 0) lgText.push((locale === 'zh' ? '历史 ' : 'prev ') + fmtSize(lg.historyBytes))
5675
+ return [
5676
+ kv(t('dbgLogs'), lgText.join(' · '), false),
5677
+ h('div', { 'data-dam-hint': '', style: { marginLeft: '140px', wordBreak: 'break-all' } },
5678
+ (locale === 'zh' ? '路径:' : 'Path: ') + (lg.path || '—')),
5679
+ h('div', { 'data-dam-hint': '', style: { marginLeft: '140px', wordBreak: 'break-all' } }, t('dbgLogsHint')),
5680
+ ]
5681
+ })(),
5682
+ hubIoByFile ? h('div', { 'data-dam-hint': '', style: { marginLeft: '140px', wordBreak: 'break-all' } },
5683
+ (locale === 'zh' ? '按层拆分(失败次数):' : 'per-store failures: ') + hubIoByFile) : null,
5684
+ fp ? kv(t('dbgPrune'), fpText, fpWarn) : null,
5685
+ pySide ? h('div', { 'data-dam-row': '', style: { marginTop: '3px', flexDirection: 'column', alignItems: 'stretch' } },
5686
+ kv(t('kvPyTitle'), pySideState, false),
5687
+ kv(t('kvPyWatchdog'), pySideWd, pySideWarn),
5688
+ h('div', { 'data-dam-hint': '', style: { marginTop: '3px' } },
5689
+ t('kvPyStderr') + (pySideTrunc ? t('kvPyStderrTrunc') : '') + ':' + pySideTail,
5690
+ pySideHasTail
5691
+ ? h('pre', { style: { margin: '3px 0 0 0', padding: '6px 8px', maxHeight: '200px', overflow: 'auto', whiteSpace: 'pre-wrap', wordBreak: 'break-all', background: 'rgba(127,127,127,.10)', borderRadius: '6px', fontSize: 'calc(11px * var(--dam-scale))' } }, pySideTail)
5692
+ : null)) : null,
4705
5693
  h('div', { 'data-dam-row': '', style: { marginTop: '6px', alignItems: 'flex-start', flexDirection: 'column' } },
4706
5694
  h('div', { 'data-dam-row': '' },
4707
5695
  h('button', { 'data-dam-btn': '', title: 'prion-scan 式四类启发式,只报位置不含正文', onClick: runScan, disabled: scanBusy }, scanBusy ? (locale === 'zh' ? '扫描中…' : 'Scanning…') : (locale === 'zh' ? '扫描脏 token' : 'Scan dirty tokens')),
@@ -4878,7 +5866,9 @@ window.__ModuleLoader__.load({
4878
5866
  toggles: [
4879
5867
  { key: 'activationEmitMode', name: locale === 'zh' ? '记忆唤起(链中检测→插入)' : 'Memory recall (detect → inject)', sub: locale === 'zh' ? '开=canary 档(显式回忆注入,推荐)/ 关=shadow 档(只记录不注入)' : 'On = canary (inject on explicit recall, recommended) / Off = shadow (record only)', mode: true, rec: true, where: locale === 'zh' ? '自动记忆引擎' : 'Semantic engine' },
4880
5868
  { key: 'autoConsolidate', name: locale === 'zh' ? '记忆固化(自动沉淀)' : 'Memory consolidation', sub: locale === 'zh' ? '每轮对话结束自动把结论沉淀进每日日志与记忆店(推荐开)' : 'Consolidate conclusions into the log & stores each turn (recommended)', rec: true, where: locale === 'zh' ? '自动化' : 'Automation' },
4881
- { key: 'procedurePromotionEnabled', name: locale === 'zh' ? '技能固化与晋升' : 'Skill crystallization', sub: locale === 'zh' ? '重复流程固化为 checklist 自动附上;跨会话验证后晋升(「记忆中枢」页审批)' : 'Turn repeated flows into auto-attached checklists; promote after validation (Memory Hub tab)', where: locale === 'zh' ? '记忆中枢' : 'Memory Hub' },
5869
+ // ★2026-09-22 改键(设置页缺口审计 P0-2):旧键 procedurePromotionEnabled 因 DEFAULT_CONFIG
5870
+ // 恒供给布尔 procedureInjectEnabled 而**结构性不可达** ⇒ 该开关此前是摆设(会翻面、写盘成功、宿主读不到)。
5871
+ { key: 'procedureInjectEnabled', name: locale === 'zh' ? '技能固化与注入' : 'Skill crystallization', sub: locale === 'zh' ? '重复流程固化为 checklist 自动附上;跨会话验证后晋升(「记忆中枢」页审批)' : 'Turn repeated flows into auto-attached checklists; promote after validation (Memory Hub tab)', where: locale === 'zh' ? '记忆中枢' : 'Memory Hub' },
4882
5872
  // ★T10(2026-09-20):机械 procedure 切片开关。
4883
5873
  // 背景:`crossFeed()` 会把 episode 意图**机械截断**成技能名与步骤(无模型介入),
4884
5874
  // 产出的观察行「既不像技能名也不像步骤」——用户报「审批里看不懂」的来源。
@@ -5334,6 +6324,33 @@ window.__ModuleLoader__.load({
5334
6324
  }
5335
6325
  return null
5336
6326
  }
6327
+ // ★P10-T1/T2(2026-09-22):13 个注入分区的**显示文案**。成员与顺序以宿主下发的
6328
+ // `promptSections` 为准(宿主常量 PROMPT_SECTION_KEYS_V1 是唯一真源),本表只提供文案;
6329
+ // 键集合必须与宿主常量相等 —— 由守卫 smoke-test-t1-prompt-sections-pre.mjs 断言。
6330
+ // offZh/offEn 只给 must 段(宿主 PROMPT_SECTION_MUST_V1),其余用通用提示。
6331
+ var PROMPT_SECTION_TEXT = {
6332
+ 'rules-section': { zh: '规则段(用户级硬约束)', en: 'Rules section (hard constraints)' },
6333
+ 'tier0-catalog': { zh: 'Tier-0 常驻目录(指引层)', en: 'Tier-0 catalog (index layer)' },
6334
+ 'whiteboard-plan': { zh: '白板 PLAN 快照', en: 'Whiteboard (PLAN) snapshot' },
6335
+ 'handoff-ledger': { zh: '交接账本', en: 'Handoff ledger' },
6336
+ 'calendar': { zh: '日历与日程', en: 'Calendar & schedule' },
6337
+ 'external-memory': { zh: '其他 AI 工具记忆', en: 'Other AI tools’ memory' },
6338
+ 'external-sessions': { zh: '历史会话索引', en: 'Past session index' },
6339
+ 'workspace-map': { zh: '其他工作区记忆索引', en: 'Other workspace index' },
6340
+ 'welcome-title': { zh: '新工作区欢迎语(标题)', en: 'New-workspace greeting (title)' },
6341
+ 'welcome-body': { zh: '新工作区欢迎语(正文)', en: 'New-workspace greeting (body)' },
6342
+ 'plan-update-request': {
6343
+ zh: '白板更新请求(硬性要求)', en: 'Whiteboard update request (mandatory)',
6344
+ offZh: '关掉后:模型不再被要求更新白板 ⇒ 白板会逐渐与项目现实脱节。',
6345
+ offEn: 'When off: the model is no longer asked to update the whiteboard — it will drift from reality.',
6346
+ },
6347
+ 'water-advisory': {
6348
+ zh: '上下文水位提醒(硬性要求)', en: 'Context water-level advisory (mandatory)',
6349
+ offZh: '关掉后:不再提醒收尾写账本/反思 ⇒ 长任务跨窗口续命会变差。',
6350
+ offEn: 'When off: no reminder to write the ledger/reflection at wrap-up — cross-window continuity suffers.',
6351
+ },
6352
+ 'handoff-pointer': { zh: '未绑定工作区时的账本指针', en: 'Ledger pointer (unbound workspace)' },
6353
+ }
5337
6354
  function SettingsPage() {
5338
6355
  var tickPair = useTick()
5339
6356
  var cfgPair = useState(null)
@@ -5354,7 +6371,7 @@ window.__ModuleLoader__.load({
5354
6371
  var dbgOpenPair = useState(false)
5355
6372
  var dbgOpen = dbgOpenPair[0]
5356
6373
  var setDbgOpen = dbgOpenPair[1]
5357
- var settingsSectionPair = useState('appearance')
6374
+ var settingsSectionPair = useState('engine')
5358
6375
  var settingsSection = settingsSectionPair[0]
5359
6376
  var setSettingsSection = settingsSectionPair[1]
5360
6377
  var browseOpenPair = useState(false)
@@ -5491,7 +6508,13 @@ window.__ModuleLoader__.load({
5491
6508
  useEffect(function () { return controller.subscribe(tickPair[1]) }, [])
5492
6509
  useEffect(function () {
5493
6510
  var alive = true
5494
- apiGet(API.config).then(function (d) { if (alive) setCfg(d.config) }).catch(function (e) { setErr(e.message) })
6511
+ apiGet(API.config).then(function (d) {
6512
+ if (!alive) return
6513
+ setCfg(d.config)
6514
+ // ★P10-T1:分区清单随 config 应答体下发(宿主唯一真源;取不到就留空 ⇒ 面板给 fail-soft 提示)
6515
+ setPsecKeys(Array.isArray(d.promptSections) ? d.promptSections : [])
6516
+ setPsecMust(Array.isArray(d.promptSectionMust) ? d.promptSectionMust : [])
6517
+ }).catch(function (e) { setErr(e.message) })
5495
6518
  // 打开设置页自动检查更新(host 有 12h 缓存,不重复查网)
5496
6519
  apiGet(API.updateCheck).then(function (d) { if (alive) setVerInfo(d) }).catch(function () {})
5497
6520
  return function () { alive = false }
@@ -5521,6 +6544,18 @@ window.__ModuleLoader__.load({
5521
6544
  var detBusyPair = useState(false)
5522
6545
  var detBusy = detBusyPair[0]
5523
6546
  var setDetBusy = detBusyPair[1]
6547
+ // ★P10-T1(2026-09-22):注入分区开关的**二级页**状态 + 清单。
6548
+ // 清单来自宿主常量(经 /config 应答体 promptSections 下发),前端不硬编码成员。
6549
+ // 同 det* 的理由:必须挂在下方 if (!cfg) 早退之前,否则 hooks 数量不一致 → error #310。
6550
+ var psecOpenPair = useState(false)
6551
+ var psecOpen = psecOpenPair[0]
6552
+ var setPsecOpen = psecOpenPair[1]
6553
+ var psecKeysPair = useState([])
6554
+ var psecKeys = psecKeysPair[0]
6555
+ var setPsecKeys = psecKeysPair[1]
6556
+ var psecMustPair = useState([])
6557
+ var psecMust = psecMustPair[0]
6558
+ var setPsecMust = psecMustPair[1]
5524
6559
  useEffect(function () {
5525
6560
  refreshSem(setSem, function () { setSem({ loaded: true, ready: false }) })
5526
6561
  return function () {}
@@ -5635,12 +6670,15 @@ window.__ModuleLoader__.load({
5635
6670
  } catch (_) {}
5636
6671
  }
5637
6672
  var sectionLabels = {
5638
- semantic: locale === 'zh' ? '自动记忆引擎' : 'Semantic engine',
5639
- memoryHub: locale === 'zh' ? '记忆中枢' : 'Memory Hub',
5640
- appearance: locale === 'zh' ? '外观' : 'Appearance', storage: locale === 'zh' ? '存储' : 'Storage',
5641
- injection: locale === 'zh' ? '记忆窗口' : 'Memory window', automation: locale === 'zh' ? '自动化' : 'Automation',
5642
- context: locale === 'zh' ? '上下文管理' : 'Context management',
5643
- maintenance: locale === 'zh' ? '维护' : 'Maintenance'
6673
+ engine: locale === 'zh' ? '语义记忆总开关' : 'Memory engine',
6674
+ window: locale === 'zh' ? '记忆窗口(注入什么)' : 'Memory window',
6675
+ capacity: locale === 'zh' ? '记忆容量与归档' : 'Capacity & retention',
6676
+ skills: locale === 'zh' ? '自动沉淀成技能' : 'Skills & promotion',
6677
+ handoff: locale === 'zh' ? '长会话接续' : 'Handoff & continuation',
6678
+ auto: locale === 'zh' ? '自动化与免打扰' : 'Automation',
6679
+ store: locale === 'zh' ? '存储与外部记忆' : 'Storage',
6680
+ look: locale === 'zh' ? '外观与交互' : 'Appearance',
6681
+ about: locale === 'zh' ? '关于与诊断' : 'About',
5644
6682
  }
5645
6683
  function section(key, title, content) { return h('section', { id: 'dam-settings-' + key, 'data-dam-settings-group': '' }, h('h3', null, title), content) }
5646
6684
  function jumpToSection(key) {
@@ -5652,7 +6690,7 @@ window.__ModuleLoader__.load({
5652
6690
  return h('button', { key: key, 'data-dam-btn': '', 'data-active': settingsSection === key ? 'true' : undefined, onClick: function () { jumpToSection(key) } }, sectionLabels[key])
5653
6691
  })),
5654
6692
  h('div', { 'data-dam-settings-content': '' },
5655
- section('semantic', sectionLabels.semantic, [
6693
+ section('engine', sectionLabels.engine, [
5656
6694
  field(t('fAssocEngine'), h('input', { type: 'checkbox', checked: !!cfg.associativeMemoryEnabled, onChange: function (e) { set('associativeMemoryEnabled', e.target.checked) } }), t('fAssocEngineHint')),
5657
6695
  field(t('fAnchorIndex'), h('input', { type: 'checkbox', checked: !!cfg.memoryAnchorEnabled, onChange: function (e) { set('memoryAnchorEnabled', e.target.checked) } }), t('fAnchorIndexHint')),
5658
6696
  field(t('fJsCooldown'), h('input', { 'data-dam-input': '', type: 'number', min: 0, max: 60, value: cfg.jsDecideCooldownRounds === undefined ? 1 : cfg.jsDecideCooldownRounds, onChange: function (e) { set('jsDecideCooldownRounds', (function () { var v = Number(e.target.value); return (Number.isFinite(v) && v >= 0) ? v : 1 })()) /* #19: 0=不冷却 是合法值,|| 1 会吃掉 0 */ } }), t('fJsCooldownHint')),
@@ -5679,14 +6717,6 @@ window.__ModuleLoader__.load({
5679
6717
  h('button', { 'data-dam-btn': '', title: locale === 'zh' ? '打开/收起安装向导(按当前引擎选择:JS 模型 或 Python 引擎)' : 'Open/collapse setup wizard (follows the selected engine)', onClick: function () { setGuide(guide ? '' : ((cfg.semanticEngineMode === 'python') ? 'python' : 'js')) } },
5680
6718
  guide ? (locale === 'zh' ? '收起向导' : 'Hide wizard') : (locale === 'zh' ? '🧩 安装向导' : '🧩 Setup')),
5681
6719
  ]), t('semModeHint')),
5682
- sem.loaded ? h('div', { 'data-dam-hint': '', style: { marginTop: '-4px', marginBottom: '8px' } },
5683
- t('semResolved') + ': ' + (sem.resolvedTier === 'c2' ? t('tierC2') + ' ✓' : sem.resolvedTier === 'c3' ? t('tierC3') + ' ✓' : t('tierC1')))
5684
- : null,
5685
- // 2026-09-08 检测联动:所选模式与资产就绪状态不符时显式标注(不再静默掉回词法兜底)
5686
- (sem.loaded && cfg.semanticEngineMode === 'js' && sem.ready === false) ? h('div', { 'data-dam-error': '', style: { marginTop: '-4px', marginBottom: '8px' } },
5687
- locale === 'zh' ? '⚠ 已选 JS 引擎,但本地模型未就绪——当前实际生效:词法兜底。下方安装引导卡可下载模型(约130MB),完成后自动启用。' : '⚠ JS engine selected but the local model is not ready — lexical fallback is active. Use the setup card below to download the model (~130MB); it engages automatically when done.') : null,
5688
- (sem.loaded && cfg.semanticEngineMode === 'python' && sem.pythonInt8Present === false) ? h('div', { 'data-dam-error': '', style: { marginTop: '-4px', marginBottom: '8px' } },
5689
- locale === 'zh' ? '⚠ 已选 Python 引擎,但 BGE-M3 int8 模型未就绪——当前实际生效:词法兜底。下方安装向导可引导安装;若本机无法安装(如架构不支持),请改用 JS 引擎或 auto。' : '⚠ Python engine selected but the BGE-M3 int8 model is not ready — lexical fallback is active. Use the wizard below; if this machine cannot install it, switch to the JS engine or auto.') : null,
5690
6720
  // M-CM6-B v3·环境检测面板:⟳ 按钮触发,自动检索环境(快检+深扫+热接入),联动安装助手;美学对齐安装向导卡
5691
6721
  detOpen ? (function () {
5692
6722
  var panelStyle = { border: '1px solid color-mix(in srgb, var(--dsw-alias-border-l1, rgba(128,128,128,.25)) 60%, transparent)', borderRadius: '10px', padding: '12px', marginBottom: '10px', background: 'color-mix(in srgb, var(--dsw-alias-bg-layer-1, rgba(128,128,128,.06)) 55%, transparent)', backdropFilter: 'blur(14px) saturate(1.3)', WebkitBackdropFilter: 'blur(14px) saturate(1.3)' }
@@ -5794,35 +6824,135 @@ window.__ModuleLoader__.load({
5794
6824
  h('option', { value: 'auto' }, t('mAuto')),
5795
6825
  h('option', { value: 'cn' }, t('mCn')),
5796
6826
  h('option', { value: 'intl' }, t('mIntl'))) : null,
5797
- ready ? h('button', { 'data-dam-btn': '', onClick: function () { setGuide(''); var n2 = Object.assign({}, cfg); n2.semanticEngineMode = guide; if (guide === 'js') { n2.activationSource = 'js'; n2.contextSinkMode = 'null' } else if (guide === 'python') { n2.activationSource = 'python'; n2.contextSinkMode = 'python'; try { var gp = localStorage.getItem('dsh-auto-memory.pyGpu'); if (gp === '1') n2.pythonGpu = true } catch (eL) {} } setCfg(n2); setDirty(true) } }, locale === 'zh' ? '启用并继续' : 'Enable & continue') : null,
6827
+ // ★2026-09-22 删死键(设置页缺口审计 P0-7):pythonGpu 不在 DEFAULT_CONFIG 白名单,
6828
+ // /config 会静默丢弃、宿主 lib/ 零读取 ⇒ 写入无任何效果,故移除该段(行为零变化)。
6829
+ ready ? h('button', { 'data-dam-btn': '', onClick: function () { setGuide(''); var n2 = Object.assign({}, cfg); n2.semanticEngineMode = guide; if (guide === 'js') { n2.activationSource = 'js'; n2.contextSinkMode = 'null' } else if (guide === 'python') { n2.activationSource = 'python'; n2.contextSinkMode = 'python' } setCfg(n2); setDirty(true) } }, locale === 'zh' ? '启用并继续' : 'Enable & continue') : null,
5798
6830
  !ready && isJs && !dlActive ? h('button', { 'data-dam-btn': '', onClick: startDl }, (dl.phase === 'error' || dl.phase === 'cancelled') ? t('semDlRetry') : t('semDlStart')) : null,
5799
6831
  !ready && isJs && dlActive ? h('button', { 'data-dam-btn': '', onClick: cancelDl }, t('semDlCancel')) : null,
5800
6832
  h('button', { 'data-dam-btn': '', onClick: function () { setGuide('') } }, t('gotIt'))))
5801
6833
  })()
5802
6834
  : null,
6835
+ sem.loaded ? h('div', { 'data-dam-hint': '', style: { marginTop: '-4px', marginBottom: '8px' } },
6836
+ t('semResolved') + ': ' + (sem.resolvedTier === 'c2' ? t('tierC2') + ' ✓' : sem.resolvedTier === 'c3' ? t('tierC3') + ' ✓' : t('tierC1')))
6837
+ : null,
6838
+ // 2026-09-08 检测联动:所选模式与资产就绪状态不符时显式标注(不再静默掉回词法兜底)
6839
+ (sem.loaded && cfg.semanticEngineMode === 'js' && sem.ready === false) ? h('div', { 'data-dam-error': '', style: { marginTop: '-4px', marginBottom: '8px' } },
6840
+ locale === 'zh' ? '⚠ 已选 JS 引擎,但本地模型未就绪——当前实际生效:词法兜底。下方安装引导卡可下载模型(约130MB),完成后自动启用。' : '⚠ JS engine selected but the local model is not ready — lexical fallback is active. Use the setup card below to download the model (~130MB); it engages automatically when done.') : null,
6841
+ (sem.loaded && cfg.semanticEngineMode === 'python' && sem.pythonInt8Present === false) ? h('div', { 'data-dam-error': '', style: { marginTop: '-4px', marginBottom: '8px' } },
6842
+ locale === 'zh' ? '⚠ 已选 Python 引擎,但 BGE-M3 int8 模型未就绪——当前实际生效:词法兜底。下方安装向导可引导安装;若本机无法安装(如架构不支持),请改用 JS 引擎或 auto。' : '⚠ Python engine selected but the BGE-M3 int8 model is not ready — lexical fallback is active. Use the wizard below; if this machine cannot install it, switch to the JS engine or auto.') : null,
5803
6843
  field(t('fReasoning'), h('input', { type: 'checkbox', checked: !!cfg.reasoningObserverEnabled, onChange: function (e) { set('reasoningObserverEnabled', e.target.checked) } }), t('fReasoningHint')),
5804
- field(t('fChildObs'), h('input', { type: 'checkbox', checked: !!cfg.contextBridgeObserveChildSessions, onChange: function (e) { set('contextBridgeObserveChildSessions', e.target.checked) } }), t('fChildObsHint')),
5805
6844
  field(locale === 'zh' ? '唤起阈值(校准策略)' : 'Activation thresholds (calibrated)', h('div', null,
5806
6845
  h('span', null, 'tauHi 0.45 · tauLo 0.35 · deltaExp 0.03 · deltaPro 0.05' + (sem.loaded ? ((locale === 'zh' ? ' · 发射模式:' : ' · emit: ') + (sem.activationEmitMode || 'shadow')) : ''))),
5807
6846
  t('fTuningHint')),
5808
6847
  ]),
5809
- section('memoryHub', sectionLabels.memoryHub, [
6848
+ section('window', sectionLabels.window, [
6849
+ // ★P10-T1/T2/T3(2026-09-22)注入分区开关:**一级只放入口**(中性文案,不推荐用户改),
6850
+ // 13 个开关全在二级页;成员/顺序取自宿主下发的 psecKeys(前端不硬编码)。
6851
+ field(t('fPromptSections'), h('button', { 'data-dam-btn': '', onClick: function () { setPsecOpen(!psecOpen) } }, psecOpen ? (locale === 'zh' ? '收起' : 'Collapse') : (locale === 'zh' ? '高级:逐段注入控制…' : 'Advanced: per-section injection…')), t('fPromptSectionsHint')),
6852
+ psecOpen ? (function () {
6853
+ var toggles = (cfg.promptSectionToggles && typeof cfg.promptSectionToggles === 'object') ? cfg.promptSectionToggles : {}
6854
+ var pText = function (k) { var m = PROMPT_SECTION_TEXT[k]; return m ? (locale === 'zh' ? m.zh : m.en) : k }
6855
+ var pOff = function (k) {
6856
+ var m = PROMPT_SECTION_TEXT[k]
6857
+ var z = m ? (locale === 'zh' ? m.offZh : m.offEn) : ''
6858
+ if (z) return z
6859
+ return locale === 'zh' ? '关掉后本轮不再注入该段落。' : 'When off, this section is not injected this round.'
6860
+ }
6861
+ return h('div', { 'data-dam-prompt-sections': '', style: { border: '1px solid color-mix(in srgb, var(--dsw-alias-border-l1, rgba(128,128,128,.25)) 60%, transparent)', borderRadius: '10px', padding: '12px', marginBottom: '10px', background: 'color-mix(in srgb, var(--dsw-alias-bg-layer-1, rgba(128,128,128,.06)) 55%, transparent)' } }, [
6862
+ h('div', { style: { display: 'flex', gap: '8px', alignItems: 'center', marginBottom: '6px' } }, [
6863
+ h('b', { style: { flex: 1, fontSize: 'calc(12px * var(--dam-scale))' } }, locale === 'zh' ? '逐段注入控制(高级)' : 'Per-section injection (advanced)'),
6864
+ h('button', { 'data-dam-btn': '', onClick: function () { set('promptSectionToggles', {}) } }, locale === 'zh' ? '全部恢复全开' : 'Restore all'),
6865
+ h('button', { 'data-dam-btn': '', onClick: function () { setPsecOpen(false) } }, t('close')),
6866
+ ]),
6867
+ h('div', { 'data-dam-hint': '', style: { marginBottom: '8px' } }, locale === 'zh'
6868
+ ? '控制每一轮注入里包含哪些段落。默认全部开启;不推荐修改 —— 关掉某段会让模型少掉对应的上下文或纪律。'
6869
+ : 'Which sections each round injects. All on by default; not recommended to change — turning one off drops the matching context or discipline.'),
6870
+ psecKeys.length ? psecKeys.map(function (k) {
6871
+ return h('div', { key: k, 'data-dam-row': '', style: { alignItems: 'flex-start' } }, [
6872
+ h('label', { style: { display: 'flex', gap: '6px', alignItems: 'center', flex: 1, cursor: 'pointer' } }, [
6873
+ h('input', { type: 'checkbox', 'data-dam-pswitch': k, checked: toggles[k] !== false, onChange: function (e) {
6874
+ var t2 = Object.assign({}, toggles)
6875
+ if (e.target.checked) delete t2[k]; else t2[k] = false
6876
+ set('promptSectionToggles', t2)
6877
+ } }),
6878
+ h('span', { style: { fontSize: 'calc(12px * var(--dam-scale))' } }, pText(k) + (psecMust.indexOf(k) >= 0 ? (locale === 'zh' ? ' · 硬性要求' : ' · mandatory') : '')),
6879
+ ]),
6880
+ h('div', { 'data-dam-hint': '', style: { flex: 1, opacity: .8 } }, pOff(k)),
6881
+ ])
6882
+ }) : h('div', { 'data-dam-hint': '' }, locale === 'zh' ? '未能从宿主取到分区清单(请重启宿主后重试)。' : 'Section list unavailable (restart the host and retry).'),
6883
+ ])
6884
+ })() : null,
6885
+ field(t('fInject'), h('input', { type: 'checkbox', checked: !!cfg.injectEnabled, onChange: function (e) { set('injectEnabled', e.target.checked) } }), t('fInjectHint')),
6886
+ field(t('fBudget'), h('input', { 'data-dam-input': '', type: 'number', min: 400, value: cfg.injectBudgetChars, onChange: function (e) { set('injectBudgetChars', Number(e.target.value) || 2400) } }), t('fBudgetHint')),
6887
+ // 2026-09-22 补接线(设置页缺口审计 P0-3/P0-4/P0-6):目录层与规则分层此前只有后端实现。
6888
+ field(t('fTier0Catalog'), h('input', { type: 'checkbox', 'data-dam-key': 'tier0CatalogEnabled', checked: cfg.tier0CatalogEnabled !== false, onChange: function (e) { set('tier0CatalogEnabled', e.target.checked) } }), t('fTier0CatalogHint')),
6889
+ field(t('fTier0Max'), h('input', { 'data-dam-input': '', type: 'number', min: 100, value: cfg.tier0MaxTokens === undefined ? 400 : cfg.tier0MaxTokens, onChange: function (e) { set('tier0MaxTokens', Math.max(100, Number(e.target.value) || 400)) } }), t('fTier0MaxHint')),
6890
+ field(t('fRulesLayering'), h('input', { type: 'checkbox', 'data-dam-key': 'rulesLayeringMode', checked: String(cfg.rulesLayeringMode || 'self').toLowerCase() !== 'off', onChange: function (e) { set('rulesLayeringMode', e.target.checked ? 'self' : 'off') } }), t('fRulesLayeringHint')),
6891
+ field(t('fDays'), h('input', { 'data-dam-input': '', type: 'number', min: 1, max: 14, value: cfg.recentDaysInjected, onChange: function (e) { set('recentDaysInjected', Number(e.target.value) || 1) } }), t('fDaysHint')),
6892
+ field(t('fExtBudget'), h('input', { 'data-dam-input': '', type: 'number', min: 200, value: cfg.externalInjectionChars === undefined ? 1400 : cfg.externalInjectionChars, onChange: function (e) { set('externalInjectionChars', Number(e.target.value) || 1400) } }), t('fExtBudgetHint')),
6893
+ field(t('fSnapGap'), h('input', { 'data-dam-input': '', type: 'number', min: 0, max: 50, value: cfg.snapshotMinGapRounds === undefined ? 5 : cfg.snapshotMinGapRounds, onChange: function (e) { set('snapshotMinGapRounds', Number(e.target.value) || 5) } }), t('fSnapGapHint')),
6894
+ field(t('fReinjectOnCompact'), h('input', { type: 'checkbox', checked: cfg.snapshotReinjectOnCompact !== false, onChange: function (e) { set('snapshotReinjectOnCompact', e.target.checked) } }), t('fReinjectOnCompactHint')),
6895
+ field(t('fPromptCustom'), h('button', { 'data-dam-btn': '', onClick: function () { setPromptEditOpen(!promptEditOpen) } }, (promptEditOpen ? (locale === 'zh' ? '收起' : 'Collapse') : (locale === 'zh' ? '编辑 prompt 层' : 'Edit prompt layers'))), t('fPromptCustomHint')),
6896
+ promptEditOpen ? [
6897
+ h('div', { key: '__layers', style: { padding: '6px 0 2px', width: '100%' } },
6898
+ (Object.keys(DEFAULT_PROMPT_LAYERS_CLIENT)).map(function (k) {
6899
+ return h('div', { key: k, style: { marginBottom: '6px' } },
6900
+ h('div', { 'data-dam-hint': '', style: { fontWeight: 700, marginBottom: '2px' } }, k),
6901
+ h('textarea', { 'data-dam-input': '', rows: 2, style: { width: '100%', fontFamily: 'monospace', fontSize: 'calc(11px * var(--dam-scale))' }, value: (cfg.promptLayerOverrides || {})[k] || '', placeholder: DEFAULT_PROMPT_LAYERS_CLIENT[k] || '(默认文案)', onChange: function (e) { var ov = Object.assign({}, cfg.promptLayerOverrides || {}); if (e.target.value.trim() === '') delete ov[k]; else ov[k] = e.target.value; set('promptLayerOverrides', ov) } }))
6902
+ })),
6903
+ h('div', { key: '__reset', 'data-dam-row': '', style: { marginTop: '6px' } },
6904
+ h('button', { 'data-dam-btn': '', onClick: function () { set('promptLayerOverrides', {}) } }, locale === 'zh' ? '一键恢复默认' : 'Reset to defaults'))
6905
+ ] : null,
6906
+ ]),
6907
+ section('capacity', sectionLabels.capacity, [
6908
+ field(t('fChildObs'), h('input', { type: 'checkbox', checked: !!cfg.contextBridgeObserveChildSessions, onChange: function (e) { set('contextBridgeObserveChildSessions', e.target.checked) } }), t('fChildObsHint')),
6909
+ field(t('fEpisodicRet'), h('input', { 'data-dam-input': '', type: 'number', min: 16, max: 4096, value: cfg.episodicRetention === undefined ? 256 : cfg.episodicRetention, onChange: function (e) { set('episodicRetention', Math.max(16, Math.min(4096, Number(e.target.value) || 256))) } }), t('fEpisodicRetHint')),
6910
+ // 2026-09-22 补接线(P0-4):facts 淘汰上限(fact-store.js pruneIfNeeded)。
6911
+ field(t('fFactRetention'), h('input', { 'data-dam-input': '', type: 'number', min: 10, value: cfg.factRetentionMax === undefined ? 1000 : cfg.factRetentionMax, onChange: function (e) { set('factRetentionMax', Math.max(10, Number(e.target.value) || 1000)) } }), t('fFactRetentionHint')),
6912
+ field(t('fNoteCap'), h('input', { 'data-dam-input': '', type: 'number', min: 500, value: cfg.noteCapacityChars === undefined ? 24000 : cfg.noteCapacityChars, onChange: function (e) { set('noteCapacityChars', Number(e.target.value) || 24000) } }), t('fNoteCapHint')),
6913
+ field(t('fUserCap'), h('input', { 'data-dam-input': '', type: 'number', min: 500, value: cfg.userCapacityChars === undefined ? 24000 : cfg.userCapacityChars, onChange: function (e) { set('userCapacityChars', Number(e.target.value) || 24000) } }), t('fUserCapHint')),
6914
+ field(t('fConsolidateMin'), h('input', { 'data-dam-input': '', type: 'number', min: 80, value: cfg.autoConsolidateMinChars === undefined ? 240 : cfg.autoConsolidateMinChars, onChange: function (e) { set('autoConsolidateMinChars', Number(e.target.value) || 240) } }), t('fConsolidateMinHint')),
6915
+ field(t('fAutoConsolidate'), h('input', { type: 'checkbox', checked: cfg.autoConsolidate !== false, onChange: function (e) { set('autoConsolidate', e.target.checked) } }), t('fAutoConsolidateHint')),
6916
+ field(t('fConsolidate'), h('input', { 'data-dam-input': '', type: 'number', min: 5, value: cfg.autoConsolidateCooldownMinutes === undefined ? 30 : cfg.autoConsolidateCooldownMinutes, onChange: function (e) { set('autoConsolidateCooldownMinutes', Number(e.target.value) || 30) } }), t('fConsolidateHint')),
6917
+ field(t('fConsolidateMax'), h('input', { 'data-dam-input': '', type: 'number', min: 1, max: 50, value: cfg.autoConsolidateDailyMax === undefined ? 8 : cfg.autoConsolidateDailyMax, onChange: function (e) { set('autoConsolidateDailyMax', Number(e.target.value) || 8) } }), t('fConsolidateMaxHint')),
6918
+ field(t('fMemFileIndex'), h('input', { type: 'checkbox', 'data-dam-key': 'memoryFileIndexEnabled', checked: cfg.memoryFileIndexEnabled === true, onChange: function (e) { set('memoryFileIndexEnabled', e.target.checked) } }), t('fMemFileIndexHint')),
6919
+ // 2026-09-14 解耦:交接白板与自动接续各自独立成项(旧实现只在白板页有接续按钮,且 enabled 还被
6920
+ // handoffEnabled 二次与运算)。两者出厂默认均为 false(功能仍在测试期),这里与白板页读写完全相同的配置键。
6921
+ ]),
6922
+ section('skills', sectionLabels.skills, [
5810
6923
  h('div', { 'data-dam-hint': '' }, t('secMemoryHubHint')),
5811
6924
  field(t('fMemoryHub'), h('input', { type: 'checkbox', checked: !!cfg.memoryHubEnabled, onChange: function (e) { set('memoryHubEnabled', e.target.checked) } }), t('fMemoryHubHint')),
5812
6925
  field(t('fEpisodicMin'), h('input', { 'data-dam-input': '', type: 'number', min: 1, max: 16, value: cfg.episodicMinSegments === undefined ? 2 : cfg.episodicMinSegments, onChange: function (e) { set('episodicMinSegments', Math.max(1, Math.min(16, Number(e.target.value) || 2))) } }), t('fEpisodicMinHint')),
5813
- field(t('fEpisodicRet'), h('input', { 'data-dam-input': '', type: 'number', min: 16, max: 4096, value: cfg.episodicRetention === undefined ? 256 : cfg.episodicRetention, onChange: function (e) { set('episodicRetention', Math.max(16, Math.min(4096, Number(e.target.value) || 256))) } }), t('fEpisodicRetHint')),
5814
6926
  field(t('fProcSessions'), h('input', { 'data-dam-input': '', type: 'number', min: 1, max: 20, value: cfg.procedureMinSessions === undefined ? 3 : cfg.procedureMinSessions, onChange: function (e) { set('procedureMinSessions', Math.max(1, Math.min(20, Number(e.target.value) || 3))) } }), t('fProcSessionsHint')),
5815
6927
  field(t('fProcSuccess'), h('input', { 'data-dam-input': '', type: 'number', min: 1, max: 20, value: cfg.procedureMinSuccess === undefined ? 2 : cfg.procedureMinSuccess, onChange: function (e) { set('procedureMinSuccess', Math.max(1, Math.min(20, Number(e.target.value) || 2))) } }), t('fProcSuccessHint')),
5816
6928
  field(t('fProcCorr'), h('input', { 'data-dam-input': '', type: 'number', min: 0, max: 1, step: 0.05, value: cfg.procedureCorrectionCap === undefined ? 0.3 : cfg.procedureCorrectionCap, onChange: function (e) { set('procedureCorrectionCap', Math.max(0, Math.min(1, Number(e.target.value) || 0.3))) } }), t('fProcCorrHint')),
5817
6929
  field(t('fProcRisk'), h('input', { type: 'checkbox', checked: cfg.procedureHighRiskApproval !== false, onChange: function (e) { set('procedureHighRiskApproval', e.target.checked) } }), t('fProcRiskHint')),
6930
+ // 2026-09-22 补接线(设置页缺口审计 P0-1):procedureInjectEnabled 才是宿主真读的总闸
6931
+ // (procedure-switch.js 先判它);此前设置页只有旧键 ⇒ 用户改了也无效(灰控件)。
6932
+ field(t('fProcInject'), h('input', { type: 'checkbox', 'data-dam-key': 'procedureInjectEnabled', checked: cfg.procedureInjectEnabled !== false, onChange: function (e) { set('procedureInjectEnabled', e.target.checked) } }), t('fProcInjectHint')),
5818
6933
  field(t('fProcLevel'), h('select', { 'data-dam-select': '', value: cfg.procedureActiveLevel || 'checklist', onChange: function (e) { set('procedureActiveLevel', e.target.value) } },
5819
6934
  h('option', { value: 'checklist' }, locale === 'zh' ? 'checklist 完整步骤' : 'checklist (full steps)'),
5820
6935
  h('option', { value: 'excerpt' }, locale === 'zh' ? 'excerpt 摘要' : 'excerpt (summary)'),
5821
6936
  h('option', { value: 'hint' }, locale === 'zh' ? 'hint 仅提示' : 'hint (hint only)')), t('fProcLevelHint')),
5822
6937
  h('div', { 'data-dam-hint': '', style: { marginTop: '4px' } }, t('memoryHubViewHint')),
5823
6938
  ]),
5824
- section('appearance', sectionLabels.appearance, [
5825
- h('div', { 'data-dam-hint': '' }, t('settingsHeader')),
6939
+ section('handoff', sectionLabels.handoff, [
6940
+ field(t('fHandoff'), h('input', { type: 'checkbox', 'data-dam-key': 'handoffEnabled', checked: cfg.handoffEnabled !== false, onChange: function (e) { set('handoffEnabled', e.target.checked) } }), t('fHandoffHint')),
6941
+ field(t('fAutoContinue'), h('input', { type: 'checkbox', 'data-dam-key': 'autoContinueEnabled', checked: cfg.autoContinueEnabled !== false, onChange: function (e) { set('autoContinueEnabled', e.target.checked) } }), t('fAutoContinueHint')),
6942
+ field(t('fHandoffPlan'), h('input', { 'data-dam-input': '', type: 'number', min: 200, value: cfg.handoffPlanChars === undefined ? 1200 : cfg.handoffPlanChars, onChange: function (e) { set('handoffPlanChars', Number(e.target.value) || 1200) } }), t('fHandoffPlanHint')),
6943
+ // 2026-09-22 补接线(设置页缺口审计 P0-5):criteriaGate 此前只能改文件才能退。
6944
+ field(t('fCriteriaGate'), h('input', { type: 'checkbox', 'data-dam-key': 'criteriaGate', checked: cfg.criteriaGate !== false, onChange: function (e) { set('criteriaGate', e.target.checked) } }), t('fCriteriaGateHint')),
6945
+ field(t('fHandoffLedger'), h('input', { 'data-dam-input': '', type: 'number', min: 100, value: cfg.handoffLedgerChars === undefined ? 800 : cfg.handoffLedgerChars, onChange: function (e) { set('handoffLedgerChars', Number(e.target.value) || 800) } }), t('fHandoffLedgerHint')),
6946
+ field(t('fWaterWindow'), h('input', { 'data-dam-input': '', type: 'number', min: 0, value: cfg.waterLevelWindowTokens === undefined ? 65536 : cfg.waterLevelWindowTokens, onChange: function (e) { set('waterLevelWindowTokens', Number(e.target.value) || 0) } }), t('fWaterWindowHint')),
6947
+ field(t('fWaterThreshold'), h('input', { 'data-dam-input': '', type: 'number', min: 0.1, max: 1.5, step: 0.05, value: cfg.waterLevelThreshold === undefined ? 0.75 : cfg.waterLevelThreshold, onChange: function (e) { set('waterLevelThreshold', Number(e.target.value) || 0.75) } }), t('fWaterThresholdHint')),
6948
+ field(t('fWaterAdvisory'), h('input', { type: 'checkbox', checked: cfg.waterLevelAdvisory !== false, onChange: function (e) { set('waterLevelAdvisory', e.target.checked) } }), t('fWaterAdvisoryHint')),
6949
+ field(t('fWaterAuto'), h('input', { type: 'checkbox', checked: cfg.waterLevelAutoHandoff !== false, onChange: function (e) { set('waterLevelAutoHandoff', e.target.checked) } }), t('fWaterAutoHint')),
6950
+ field(locale === 'zh' ? '子代理痕迹回收' : 'Subagent trace recycle', h('input', { type: 'checkbox', checked: cfg.subagentGcEnabled !== false, onChange: function (e) { set('subagentGcEnabled', e.target.checked) } }),
6951
+ locale === 'zh' ? '本插件的一次性子代理(自动沉淀 / 时段总结 / 问候 / 蒸馏)每跑一次都会在 ~/.dsh/sessions 留下一个持久化会话;数量上千后会话列表会明显变慢。开启后任务一结束就把这些痕迹移入 ~/.dsh/subagent-gc-backup 备份(可回滚),不影响子代理结果。默认开。' : 'Every one-shot subagent this plugin spawns (auto-consolidation, scheduled summary, greeting, distillation) leaves a persisted session under ~/.dsh/sessions; hundreds of them slow the session list down. When on, each trace is moved into ~/.dsh/subagent-gc-backup right after the task ends (reversible), never affecting the subagent result. On by default.'),
6952
+ field(locale === 'zh' ? '兜底回收保留天数' : 'Fallback recycle keep days', h('input', { 'data-dam-input': '', type: 'number', min: 0, value: cfg.subagentGcKeepDays === undefined ? 3 : cfg.subagentGcKeepDays, onChange: function (e) { set('subagentGcKeepDays', Number(e.target.value) || 0) } }),
6953
+ locale === 'zh' ? '每天巡检一次,回收超过该天数仍残留的痕迹(比如任务异常中断没删掉的);0 = 不按时间,只靠任务结束即删。' : 'A daily sweep recycles traces older than this many days (e.g. leftovers from interrupted tasks); 0 = rely on the end-of-task recycle only.'),
6954
+ ]),
6955
+ section('auto', sectionLabels.auto, [
5826
6956
  // 欢迎向导:开关(首启自动播放)+ 立即重看按钮(闭包内直调 openDialog——同一作用域,点击立即弹;
5827
6957
  // 不走 window 全局入口,避免多实例时序导致"点了没反应要刷新")+ 查看更新日志(走 update 弹窗,
5828
6958
  // 带 Logo 开场动画;versions 取 CHANGELOG 最新一条)
@@ -5837,132 +6967,106 @@ window.__ModuleLoader__.load({
5837
6967
  openDialog({ kind: 'update', versions: [{ version: latest, items: CHANGELOG[latest] }], currentVersion: latest })
5838
6968
  } catch (eLog) {}
5839
6969
  } }, locale === 'zh' ? '查看更新日志' : 'View changelog')), t('fWelcomeTourHint')),
6970
+ field(t('fAutoPopup'), h('input', { type: 'checkbox', checked: cfg.autoPopupEnabled !== false, onChange: function (e) { set('autoPopupEnabled', e.target.checked) } }), t('fAutoPopupHint')),
6971
+ field(t('fUnattended'), h('input', { type: 'checkbox', checked: !!cfg.unattendedMode, onChange: function (e) { set('unattendedMode', e.target.checked) } }), t('fUnattendedHint')),
6972
+ field(t('fUnattendedAuto'), h('input', { type: 'checkbox', checked: !!cfg.unattendedAuto, onChange: function (e) { set('unattendedAuto', e.target.checked) } }), t('fUnattendedAutoHint')),
6973
+ field(t('fAway'), h('input', { 'data-dam-input': '', type: 'number', min: 0, value: (Number(cfg.awayMinutes) === 0 ? 0 : (cfg.awayMinutes || 60)), onChange: function (e) { var v = Number(e.target.value); set('awayMinutes', Number.isFinite(v) && v >= 0 ? v : 60) } }), t('fAwayHint')),
6974
+ field(t('fAutoSum'), h('input', { 'data-dam-input': '', value: (cfg.autoSummaryTimes || []).join(','), onChange: function (e) { set('autoSummaryTimes', String(e.target.value || '').split(',').map(function (s) { return s.trim() }).filter(Boolean)) } }), t('fAutoSumHint')),
6975
+ field(t('fConsSchedule'), h('input', { type: 'checkbox', checked: cfg.consolidateScheduleEnabled !== false, onChange: function (e) { set('consolidateScheduleEnabled', e.target.checked) } }), t('fConsScheduleHint')),
6976
+ field(t('fConsScheduleTime'), h('input', { 'data-dam-input': '', placeholder: '09:30', value: cfg.consolidateScheduleTime === undefined ? '09:30' : cfg.consolidateScheduleTime, onChange: function (e) { set('consolidateScheduleTime', String(e.target.value || '').trim()) } }), t('fConsScheduleTimeHint')),
6977
+ field(t('fConsScheduleDays'), h('input', { 'data-dam-input': '', type: 'number', min: 1, max: 60, value: cfg.consolidateScheduleDays === undefined ? 7 : cfg.consolidateScheduleDays, onChange: function (e) { set('consolidateScheduleDays', Number(e.target.value) || 7) } }), t('fConsScheduleDaysHint')),
6978
+ field(t('fMaintSchedule'), h('input', { type: 'checkbox', checked: cfg.maintainScheduleEnabled !== false, onChange: function (e) { set('maintainScheduleEnabled', e.target.checked) } }), t('fMaintScheduleHint')),
6979
+ field(t('fMaintScheduleTime'), h('input', { 'data-dam-input': '', placeholder: '10:00', value: cfg.maintainScheduleTime === undefined ? '10:00' : cfg.maintainScheduleTime, onChange: function (e) { set('maintainScheduleTime', String(e.target.value || '').trim()) } }), t('fMaintScheduleTimeHint')),
6980
+ ]),
6981
+ section('store', sectionLabels.store, [
6982
+ field(t('fUserDir'), h('input', { 'data-dam-input': '', value: cfg.userMemoryDir, onChange: function (e) { set('userMemoryDir', e.target.value) } }), t('fUserDirHint')),
6983
+ field(t('fProjectDir'), h('input', { 'data-dam-input': '', value: cfg.projectMemoryDir, onChange: function (e) { set('projectMemoryDir', e.target.value) } }), t('fProjectDirHint')),
6984
+ field(t('fMemoryRoot'), h('div', { 'data-dam-row': '', style: { flex: 1 } },
6985
+ h('input', { 'data-dam-input': '', style: { flex: 1 }, value: cfg.memoryRoot || '', onChange: function (e) { set('memoryRoot', e.target.value) } }),
6986
+ h('button', { 'data-dam-btn': '', onClick: function () { openBrowser() } }, t('fBrowse'))), t('fMemoryRootHint')),
6987
+ browseOpen ? h('div', { style: { border: '1px solid color-mix(in srgb, var(--dsw-alias-border-l1, rgba(128,128,128,.25)) 60%, transparent)', borderRadius: '8px', padding: '8px', marginBottom: '8px', background: 'color-mix(in srgb, var(--dsw-alias-bg-layer-1, rgba(128,128,128,.06)) 40%, transparent)' } },
6988
+ h('div', { 'data-dam-row': '' },
6989
+ h('b', { style: { fontSize: 'calc(12px * var(--dam-scale))', wordBreak: 'break-all', flex: 1 } }, browsePath || '…'),
6990
+ h('button', { 'data-dam-btn': '', onClick: function () { browseTo(browseParent) }, disabled: browseParent === browsePath }, t('fUp'))),
6991
+ h('div', { style: { maxHeight: '160px', overflow: 'auto', marginTop: '4px' } },
6992
+ (browseDirs || []).map(function (d) {
6993
+ return h('button', { key: d.path, 'data-dam-btn': '', style: { display: 'block', width: '100%', textAlign: 'left', padding: '3px 6px' }, onClick: function () { browseTo(d.path) } }, '📁 ' + d.name)
6994
+ }).concat(browseDirs && !browseDirs.length ? [h('div', { key: 'e', 'data-dam-hint': '' }, t('empty'))] : [])),
6995
+ h('div', { 'data-dam-row': '', style: { marginTop: '6px' } },
6996
+ h('button', { 'data-dam-btn': '', onClick: function () { set('memoryRoot', browsePath); setBrowseOpen(false) } }, t('fSelectDir')),
6997
+ h('button', { 'data-dam-btn': '', onClick: function () { setBrowseOpen(false) } }, t('close'))))
6998
+ : null,
6999
+ field(t('fDayBoundary'), h('input', { 'data-dam-input': '', type: 'number', min: 0, max: 1439, value: (cfg.dayBoundaryMinutes === undefined ? 450 : cfg.dayBoundaryMinutes), onChange: function (e) { set('dayBoundaryMinutes', parseInt(e.target.value || '0', 10)) } }), t('fDayBoundaryHint')),
7000
+ field(t('fReflect'), h('input', { type: 'checkbox', checked: !!cfg.reflectEnabled, onChange: function (e) { set('reflectEnabled', e.target.checked) } }), t('fReflectHint')),
7001
+ field(t('fStyle'), h('select', { 'data-dam-select': '', value: cfg.reflectStyle, onChange: function (e) { set('reflectStyle', e.target.value) } },
7002
+ STYLE_IDS.map(function (id) { return h('option', { key: id, value: id }, t('style' + id.charAt(0).toUpperCase() + id.slice(1))) })), t('fStyleHint')),
7003
+ // 2026-09-22 补接线(设置页缺口审计 P1/P2):扫描上限与索引快照前端零命中。
7004
+ field(t('fWsDiscover'), h('input', { 'data-dam-input': '', type: 'number', min: 5, max: 2000, value: cfg.workspaceDiscoverMax === undefined ? 200 : cfg.workspaceDiscoverMax, onChange: function (e) { set('workspaceDiscoverMax', Math.max(5, Math.min(2000, Number(e.target.value) || 200))) } }), t('fWsDiscoverHint')),
7005
+ ]),
7006
+ section('look', sectionLabels.look, [
7007
+ h('div', { 'data-dam-hint': '' }, t('settingsHeader')),
5840
7008
  field(t('fLocale'), h('select', { 'data-dam-select': '', value: cfg.locale || 'system', onChange: function (e) { set('locale', e.target.value) } },
5841
7009
  LOCALE_IDS_LIST.map(function (id) { return h('option', { key: id, value: id }, id === 'system' ? t('followSystem') : t(id)) })), t('fLocaleHint')),
5842
7010
  field(t('fFontSize'), h('select', { 'data-dam-select': '', value: fontScale, onChange: function (e) { fontScale = e.target.value; try { localStorage.setItem('dsh-auto-memory.fontScale.v2', fontScale) } catch (ee) {}; try { var pp = document.querySelector('[data-dam-panel]'); if (pp) pp.style.setProperty('--dam-scale', FONT_SCALE_VALUES[fontScale] || '1') } catch (ee2) {}; emit() } },
5843
7011
  Object.keys(FONT_SCALES).map(function (k) { return h('option', { key: k, value: k }, t('fs' + k.charAt(0).toUpperCase() + k.slice(1))) })), t('fFontSizeHint')),
7012
+ // 面板位置(2026-09-21):左下角 / 顶部 / 两者共存。改动立即生效并同步两处形态(用户硬规则:即时回显)。
7013
+ field(t('fPanelPos'), h('select', { 'data-dam-select': '', value: controller.panelPos(), onChange: function (e) { controller.setPanelPos(e.target.value) } },
7014
+ h('option', { value: 'bottom-left' }, t('posBottomLeft')), h('option', { value: 'page' }, t('posPage')), h('option', { value: 'both' }, t('posBoth'))), t('fPanelPosHint')),
5844
7015
  field(locale === 'zh' ? '强调色' : 'Accent color', h('select', { 'data-dam-select': '', value: accentTheme, onChange: function (e) { setAccent(e.target.value) } },
5845
7016
  h('option', { value: 'deepseek' }, locale === 'zh' ? 'DeepSeek 蓝' : 'DeepSeek blue'), h('option', { value: 'graphite' }, locale === 'zh' ? '石墨灰' : 'Graphite'), h('option', { value: 'violet' }, locale === 'zh' ? '雾紫' : 'Violet')), locale === 'zh' ? '默认使用 DeepSeek 蓝;日历与状态颜色保持语义色。' : 'DeepSeek blue by default; calendar and status colors stay semantic.'),
5846
7017
  field(locale === 'zh' ? '关系图密度' : 'Graph density', h('select', { 'data-dam-select': '', value: graphDensity, onChange: function (e) { setDensity(e.target.value) } },
5847
- h('option', { value: 'relaxed' }, locale === 'zh' ? '舒展' : 'Relaxed'), h('option', { value: 'compact' }, locale === 'zh' ? '紧凑' : 'Compact')), locale === 'zh' ? '影响工作区关系图的节点间距和显示数量。' : 'Controls node spacing and detail in the workspace graph.')
5848
- ]),
5849
- section('storage', sectionLabels.storage, [field(t('fUserDir'), h('input', { 'data-dam-input': '', value: cfg.userMemoryDir, onChange: function (e) { set('userMemoryDir', e.target.value) } }), t('fUserDirHint')),
5850
- field(t('fProjectDir'), h('input', { 'data-dam-input': '', value: cfg.projectMemoryDir, onChange: function (e) { set('projectMemoryDir', e.target.value) } }), t('fProjectDirHint')),
5851
- field(t('fMemoryRoot'), h('div', { 'data-dam-row': '', style: { flex: 1 } },
5852
- h('input', { 'data-dam-input': '', style: { flex: 1 }, value: cfg.memoryRoot || '', onChange: function (e) { set('memoryRoot', e.target.value) } }),
5853
- h('button', { 'data-dam-btn': '', onClick: function () { openBrowser() } }, t('fBrowse'))), t('fMemoryRootHint')),
5854
- browseOpen ? h('div', { style: { border: '1px solid color-mix(in srgb, var(--dsw-alias-border-l1, rgba(128,128,128,.25)) 60%, transparent)', borderRadius: '8px', padding: '8px', marginBottom: '8px', background: 'color-mix(in srgb, var(--dsw-alias-bg-layer-1, rgba(128,128,128,.06)) 40%, transparent)' } },
5855
- h('div', { 'data-dam-row': '' },
5856
- h('b', { style: { fontSize: 'calc(12px * var(--dam-scale))', wordBreak: 'break-all', flex: 1 } }, browsePath || '…'),
5857
- h('button', { 'data-dam-btn': '', onClick: function () { browseTo(browseParent) }, disabled: browseParent === browsePath }, t('fUp'))),
5858
- h('div', { style: { maxHeight: '160px', overflow: 'auto', marginTop: '4px' } },
5859
- (browseDirs || []).map(function (d) {
5860
- return h('button', { key: d.path, 'data-dam-btn': '', style: { display: 'block', width: '100%', textAlign: 'left', padding: '3px 6px' }, onClick: function () { browseTo(d.path) } }, '📁 ' + d.name)
5861
- }).concat(browseDirs && !browseDirs.length ? [h('div', { key: 'e', 'data-dam-hint': '' }, t('empty'))] : [])),
5862
- h('div', { 'data-dam-row': '', style: { marginTop: '6px' } },
5863
- h('button', { 'data-dam-btn': '', onClick: function () { set('memoryRoot', browsePath); setBrowseOpen(false) } }, t('fSelectDir')),
5864
- h('button', { 'data-dam-btn': '', onClick: function () { setBrowseOpen(false) } }, t('close'))))
5865
- : null]),
5866
- section('injection', sectionLabels.injection, [field(t('fInject'), h('input', { type: 'checkbox', checked: !!cfg.injectEnabled, onChange: function (e) { set('injectEnabled', e.target.checked) } }), t('fInjectHint')),
5867
- field(t('fBudget'), h('input', { 'data-dam-input': '', type: 'number', min: 400, value: cfg.injectBudgetChars, onChange: function (e) { set('injectBudgetChars', Number(e.target.value) || 2400) } }), t('fBudgetHint')),
5868
- // 群反馈第 4 条:排除来源(每行一条)。textarea 便于粘贴多行路径;
5869
- // 空行/纯空白在写回时被过滤,避免"看起来配了其实没配"。
5870
- field(t('fExclude'), h('textarea', {
5871
- 'data-dam-input': '', rows: 3,
5872
- style: { width: '100%', resize: 'vertical', fontFamily: 'inherit' },
5873
- value: (Array.isArray(cfg.injectExcludeSources) ? cfg.injectExcludeSources : []).join('\n'),
5874
- placeholder: 'mem_0123456789abcdef0123456789abcdef\nD:\\ws\\.dsh-memory\\archive\\\nlog',
5875
- onChange: function (e) {
5876
- var lines = String(e.target.value || '').split('\n').map(function (x) { return x.trim() }).filter(Boolean)
5877
- set('injectExcludeSources', lines)
5878
- },
5879
- }), t('fExcludeHint')),
5880
- field(t('fNoteCap'), h('input', { 'data-dam-input': '', type: 'number', min: 500, value: cfg.noteCapacityChars === undefined ? 24000 : cfg.noteCapacityChars, onChange: function (e) { set('noteCapacityChars', Number(e.target.value) || 24000) } }), t('fNoteCapHint')),
5881
- field(t('fUserCap'), h('input', { 'data-dam-input': '', type: 'number', min: 500, value: cfg.userCapacityChars === undefined ? 24000 : cfg.userCapacityChars, onChange: function (e) { set('userCapacityChars', Number(e.target.value) || 24000) } }), t('fUserCapHint')),
5882
- field(t('fDays'), h('input', { 'data-dam-input': '', type: 'number', min: 1, max: 14, value: cfg.recentDaysInjected, onChange: function (e) { set('recentDaysInjected', Number(e.target.value) || 1) } }), t('fDaysHint')),
5883
- field(t('fExtBudget'), h('input', { 'data-dam-input': '', type: 'number', min: 200, value: cfg.externalInjectionChars === undefined ? 1400 : cfg.externalInjectionChars, onChange: function (e) { set('externalInjectionChars', Number(e.target.value) || 1400) } }), t('fExtBudgetHint')),
5884
- field(t('fSnapGap'), h('input', { 'data-dam-input': '', type: 'number', min: 0, max: 50, value: cfg.snapshotMinGapRounds === undefined ? 5 : cfg.snapshotMinGapRounds, onChange: function (e) { set('snapshotMinGapRounds', Number(e.target.value) || 5) } }), t('fSnapGapHint')),
5885
- field(t('fReinjectOnCompact'), h('input', { type: 'checkbox', checked: cfg.snapshotReinjectOnCompact !== false, onChange: function (e) { set('snapshotReinjectOnCompact', e.target.checked) } }), t('fReinjectOnCompactHint')),
5886
- field(t('fPromptCustom'), h('button', { 'data-dam-btn': '', onClick: function () { setPromptEditOpen(!promptEditOpen) } }, (promptEditOpen ? (locale === 'zh' ? '收起' : 'Collapse') : (locale === 'zh' ? '编辑 prompt 层' : 'Edit prompt layers'))), t('fPromptCustomHint')),
5887
- promptEditOpen ? [
5888
- h('div', { key: '__layers', style: { padding: '6px 0 2px', width: '100%' } },
5889
- (Object.keys(DEFAULT_PROMPT_LAYERS_CLIENT)).map(function (k) {
5890
- return h('div', { key: k, style: { marginBottom: '6px' } },
5891
- h('div', { 'data-dam-hint': '', style: { fontWeight: 700, marginBottom: '2px' } }, k),
5892
- h('textarea', { 'data-dam-input': '', rows: 2, style: { width: '100%', fontFamily: 'monospace', fontSize: 'calc(11px * var(--dam-scale))' }, value: (cfg.promptLayerOverrides || {})[k] || '', placeholder: DEFAULT_PROMPT_LAYERS_CLIENT[k] || '(默认文案)', onChange: function (e) { var ov = Object.assign({}, cfg.promptLayerOverrides || {}); if (e.target.value.trim() === '') delete ov[k]; else ov[k] = e.target.value; set('promptLayerOverrides', ov) } }))
5893
- })),
5894
- h('div', { key: '__reset', 'data-dam-row': '', style: { marginTop: '6px' } },
5895
- h('button', { 'data-dam-btn': '', onClick: function () { set('promptLayerOverrides', {}) } }, locale === 'zh' ? '一键恢复默认' : 'Reset to defaults'))
5896
- ] : null]),
5897
- section('automation', sectionLabels.automation, [field(t('fConsolidateMin'), h('input', { 'data-dam-input': '', type: 'number', min: 80, value: cfg.autoConsolidateMinChars === undefined ? 240 : cfg.autoConsolidateMinChars, onChange: function (e) { set('autoConsolidateMinChars', Number(e.target.value) || 240) } }), t('fConsolidateMinHint')),
5898
- field(t('fAutoConsolidate'), h('input', { type: 'checkbox', checked: cfg.autoConsolidate !== false, onChange: function (e) { set('autoConsolidate', e.target.checked) } }), t('fAutoConsolidateHint')),
5899
- field(t('fConsolidate'), h('input', { 'data-dam-input': '', type: 'number', min: 5, value: cfg.autoConsolidateCooldownMinutes === undefined ? 30 : cfg.autoConsolidateCooldownMinutes, onChange: function (e) { set('autoConsolidateCooldownMinutes', Number(e.target.value) || 30) } }), t('fConsolidateHint')),
5900
- field(t('fConsolidateMax'), h('input', { 'data-dam-input': '', type: 'number', min: 1, max: 50, value: cfg.autoConsolidateDailyMax === undefined ? 8 : cfg.autoConsolidateDailyMax, onChange: function (e) { set('autoConsolidateDailyMax', Number(e.target.value) || 8) } }), t('fConsolidateMaxHint')),
5901
- field(t('fAutoPopup'), h('input', { type: 'checkbox', checked: cfg.autoPopupEnabled !== false, onChange: function (e) { set('autoPopupEnabled', e.target.checked) } }), t('fAutoPopupHint')),
5902
- field(t('fUnattended'), h('input', { type: 'checkbox', checked: !!cfg.unattendedMode, onChange: function (e) { set('unattendedMode', e.target.checked) } }), t('fUnattendedHint')),
5903
- field(t('fUnattendedAuto'), h('input', { type: 'checkbox', checked: !!cfg.unattendedAuto, onChange: function (e) { set('unattendedAuto', e.target.checked) } }), t('fUnattendedAutoHint')),
5904
- field(t('fAway'), h('input', { 'data-dam-input': '', type: 'number', min: 0, value: (Number(cfg.awayMinutes) === 0 ? 0 : (cfg.awayMinutes || 60)), onChange: function (e) { var v = Number(e.target.value); set('awayMinutes', Number.isFinite(v) && v >= 0 ? v : 60) } }), t('fAwayHint')),
5905
- field(t('fAutoSum'), h('input', { 'data-dam-input': '', value: (cfg.autoSummaryTimes || []).join(','), onChange: function (e) { set('autoSummaryTimes', String(e.target.value || '').split(',').map(function (s) { return s.trim() }).filter(Boolean)) } }), t('fAutoSumHint')),
5906
- field(t('fDayBoundary'), h('input', { 'data-dam-input': '', type: 'number', min: 0, max: 1439, value: (cfg.dayBoundaryMinutes === undefined ? 450 : cfg.dayBoundaryMinutes), onChange: function (e) { set('dayBoundaryMinutes', parseInt(e.target.value || '0', 10)) } }), t('fDayBoundaryHint')),
5907
- field(t('fReflect'), h('input', { type: 'checkbox', checked: !!cfg.reflectEnabled, onChange: function (e) { set('reflectEnabled', e.target.checked) } }), t('fReflectHint')),
5908
- field(t('fConsSchedule'), h('input', { type: 'checkbox', checked: cfg.consolidateScheduleEnabled !== false, onChange: function (e) { set('consolidateScheduleEnabled', e.target.checked) } }), t('fConsScheduleHint')),
5909
- field(t('fConsScheduleTime'), h('input', { 'data-dam-input': '', placeholder: '09:30', value: cfg.consolidateScheduleTime === undefined ? '09:30' : cfg.consolidateScheduleTime, onChange: function (e) { set('consolidateScheduleTime', String(e.target.value || '').trim()) } }), t('fConsScheduleTimeHint')),
5910
- field(t('fConsScheduleDays'), h('input', { 'data-dam-input': '', type: 'number', min: 1, max: 60, value: cfg.consolidateScheduleDays === undefined ? 7 : cfg.consolidateScheduleDays, onChange: function (e) { set('consolidateScheduleDays', Number(e.target.value) || 7) } }), t('fConsScheduleDaysHint')),
5911
- field(t('fMaintSchedule'), h('input', { type: 'checkbox', checked: cfg.maintainScheduleEnabled !== false, onChange: function (e) { set('maintainScheduleEnabled', e.target.checked) } }), t('fMaintScheduleHint')),
5912
- field(t('fMaintScheduleTime'), h('input', { 'data-dam-input': '', placeholder: '10:00', value: cfg.maintainScheduleTime === undefined ? '10:00' : cfg.maintainScheduleTime, onChange: function (e) { set('maintainScheduleTime', String(e.target.value || '').trim()) } }), t('fMaintScheduleTimeHint')),
5913
- field(t('fStyle'), h('select', { 'data-dam-select': '', value: cfg.reflectStyle, onChange: function (e) { set('reflectStyle', e.target.value) } },
5914
- STYLE_IDS.map(function (id) { return h('option', { key: id, value: id }, t('style' + id.charAt(0).toUpperCase() + id.slice(1))) })), t('fStyleHint')),
5915
- field(locale === 'zh' ? '子代理模型 / 思考强度' : 'Subagent model & reasoning effort', h('div', { 'data-dam-row': '', style: { flex: 1 } },
5916
- h('span', { style: { flex: 1, fontSize: 'calc(12px * var(--dam-scale))', wordBreak: 'break-all', opacity: cfg.subagentModel ? 1 : 0.6 } }, (cfg.subagentModel || (locale === 'zh' ? '跟随路由默认' : 'routing default')) + (String(cfg.subagentReasoningEffort || '') ? ' · ' + String(cfg.subagentReasoningEffort) : '')),
5917
- h('button', { 'data-dam-btn': '', onClick: openModels }, locale === 'zh' ? '选择模型 / 强度' : 'Pick model / effort')),
5918
- locale === 'zh' ? '用于时段总结、问候语、自动沉淀、蒸馏等 subagent 功能;模型与思考强度(off/low/high/max)都经 DSH 的 agentOptions 下发,留空跟随路由与模型默认。保存后生效。' : 'For scheduled summaries, greetings, auto-consolidation and distillation subagents; model and reasoning effort (off/low/high/max) are delivered via DSH agentOptions. Empty follows routing and model defaults. Applies after saving.'),
5919
- mdlOpen ? buildModelDrawer() : null,
7018
+ h('option', { value: 'relaxed' }, locale === 'zh' ? '舒展' : 'Relaxed'), h('option', { value: 'compact' }, locale === 'zh' ? '紧凑' : 'Compact')), locale === 'zh' ? '影响工作区关系图的节点间距和显示数量。' : 'Controls node spacing and detail in the workspace graph.'),
7019
+ // 群反馈第 4 条:排除来源(每行一条)。textarea 便于粘贴多行路径;
7020
+ // 空行/纯空白在写回时被过滤,避免"看起来配了其实没配"。
7021
+ field(t('fExclude'), h('textarea', {
7022
+ 'data-dam-input': '', rows: 3,
7023
+ style: { width: '100%', resize: 'vertical', fontFamily: 'inherit' },
7024
+ value: (Array.isArray(cfg.injectExcludeSources) ? cfg.injectExcludeSources : []).join('\n'),
7025
+ placeholder: 'mem_0123456789abcdef0123456789abcdef\nD:\\ws\\.dsh-memory\\archive\\\nlog',
7026
+ onChange: function (e) {
7027
+ var lines = String(e.target.value || '').split('\n').map(function (x) { return x.trim() }).filter(Boolean)
7028
+ set('injectExcludeSources', lines)
7029
+ },
7030
+ }), t('fExcludeHint')),
7031
+ field(locale === 'zh' ? '子代理模型 / 思考强度' : 'Subagent model & reasoning effort', h('div', { 'data-dam-row': '', style: { flex: 1 } },
7032
+ h('span', { style: { flex: 1, fontSize: 'calc(12px * var(--dam-scale))', wordBreak: 'break-all', opacity: cfg.subagentModel ? 1 : 0.6 } }, (cfg.subagentModel || (locale === 'zh' ? '跟随路由默认' : 'routing default')) + (String(cfg.subagentReasoningEffort || '') ? ' · ' + String(cfg.subagentReasoningEffort) : '')),
7033
+ h('button', { 'data-dam-btn': '', onClick: openModels }, locale === 'zh' ? '选择模型 / 强度' : 'Pick model / effort')),
7034
+ locale === 'zh' ? '用于时段总结、问候语、自动沉淀、蒸馏等 subagent 功能;模型与思考强度(off/low/high/max)都经 DSH 的 agentOptions 下发,留空跟随路由与模型默认。保存后生效。' : 'For scheduled summaries, greetings, auto-consolidation and distillation subagents; model and reasoning effort (off/low/high/max) are delivered via DSH agentOptions. Empty follows routing and model defaults. Applies after saving.'),
7035
+ mdlOpen ? buildModelDrawer() : null,
5920
7036
  ]),
5921
- // 2026-09-14 解耦:交接白板与自动接续各自独立成项(旧实现只在白板页有接续按钮,且 enabled 还被
5922
- // handoffEnabled 二次与运算)。两者出厂默认均为 false(功能仍在测试期),这里与白板页读写完全相同的配置键。
5923
- section('context', sectionLabels.context, [field(t('fHandoff'), h('input', { type: 'checkbox', 'data-dam-key': 'handoffEnabled', checked: cfg.handoffEnabled !== false, onChange: function (e) { set('handoffEnabled', e.target.checked) } }), t('fHandoffHint')),
5924
- field(t('fAutoContinue'), h('input', { type: 'checkbox', 'data-dam-key': 'autoContinueEnabled', checked: cfg.autoContinueEnabled !== false, onChange: function (e) { set('autoContinueEnabled', e.target.checked) } }), t('fAutoContinueHint')),
5925
- field(t('fHandoffPlan'), h('input', { 'data-dam-input': '', type: 'number', min: 200, value: cfg.handoffPlanChars === undefined ? 1200 : cfg.handoffPlanChars, onChange: function (e) { set('handoffPlanChars', Number(e.target.value) || 1200) } }), t('fHandoffPlanHint')),
5926
- field(t('fHandoffLedger'), h('input', { 'data-dam-input': '', type: 'number', min: 100, value: cfg.handoffLedgerChars === undefined ? 800 : cfg.handoffLedgerChars, onChange: function (e) { set('handoffLedgerChars', Number(e.target.value) || 800) } }), t('fHandoffLedgerHint')),
5927
- field(t('fWaterWindow'), h('input', { 'data-dam-input': '', type: 'number', min: 0, value: cfg.waterLevelWindowTokens === undefined ? 65536 : cfg.waterLevelWindowTokens, onChange: function (e) { set('waterLevelWindowTokens', Number(e.target.value) || 0) } }), t('fWaterWindowHint')),
5928
- field(t('fWaterThreshold'), h('input', { 'data-dam-input': '', type: 'number', min: 0.1, max: 1.5, step: 0.05, value: cfg.waterLevelThreshold === undefined ? 0.75 : cfg.waterLevelThreshold, onChange: function (e) { set('waterLevelThreshold', Number(e.target.value) || 0.75) } }), t('fWaterThresholdHint')),
5929
- field(t('fWaterAdvisory'), h('input', { type: 'checkbox', checked: cfg.waterLevelAdvisory !== false, onChange: function (e) { set('waterLevelAdvisory', e.target.checked) } }), t('fWaterAdvisoryHint')),
5930
- field(t('fWaterAuto'), h('input', { type: 'checkbox', checked: cfg.waterLevelAutoHandoff !== false, onChange: function (e) { set('waterLevelAutoHandoff', e.target.checked) } }), t('fWaterAutoHint')),
5931
- // WB-GRAPH 白板线一键切换(2026-09-16, board_mode_v1): 旧版白板(默认) / 新版看板(dsh-graph vendor
5932
- // + sidecar + 遍历工具, 工具数 14→16)。开关解耦: 只管白板线; 切换需重启 dsh web 生效(工具注册在启动期)。
5933
- field(locale === 'zh' ? '白板模式(旧版白板 / 新版看板)' : 'Board mode (legacy / graph)', (function () {
5934
- var cur = cfg.boardMode === 'graph' ? 'graph' : 'legacy'
5935
- // ★2026-09-16 修 BUG-3(设置页切换不落盘): 旧实现 `set('boardMode', m)` 只改**本地 React 态**
5936
- // 然后 reload —— 配置从未写到服务端, 刷新后仍是旧值(界面回跳原档), 用户会判定"开关坏了"。
5937
- // 正解: 走与接续面板同一个写入路径 `saveConfigPatch(API.config, patch)`(见 :999 定义),
5938
- // 写盘成功后再 reload; 失败则在界面提示, 不静默假装成功。
5939
- // 同时满足用户硬性偏好: 开关类改动必须**即时回显**(写盘后立即 reload 呈现新档)。
5940
- function pick(m) {
5941
- set('boardMode', m) // 先本地回显(即时反馈)
5942
- saveConfigPatch({ boardMode: m }, {
5943
- onSaved: function () { window.setTimeout(function () { window.location.reload() }, 350) },
5944
- onError: function (e) { setErr(String((e && e.message) || e || 'save failed')) },
5945
- })
5946
- }
5947
- return h('div', { 'data-dam-row': '' },
5948
- h('button', { 'data-dam-btn': '', 'data-dam-key': 'boardMode', style: { fontWeight: cur === 'legacy' ? 700 : 400, opacity: cur === 'legacy' ? 1 : 0.65 }, onClick: function () { if (cur !== 'legacy') pick('legacy') } }, locale === 'zh' ? '旧版白板' : 'Legacy board'),
5949
- h('button', { 'data-dam-btn': '', 'data-dam-key': 'boardModeGraph', style: { marginLeft: '6px', fontWeight: cur === 'graph' ? 700 : 400, opacity: cur === 'graph' ? 1 : 0.65 }, onClick: function () { if (cur !== 'graph') pick('graph') } }, locale === 'zh' ? '新版看板(dsh-graph)' : 'Graph board (dsh-graph)'))
5950
- })(),
5951
- locale === 'zh' ? '一键切换白板形态。默认「旧版白板」=一切行为不变;切到「新版看板」后启用 dsh-graph 看板、白板结构化索引(handoff/index.json)与两个遍历工具(memory_expand / memory_trace,工具数 14→16)。切换后需重启 dsh web 生效;再点回旧版即完全回滚。' : 'One-click switch for the board form. Default "Legacy board" keeps everything unchanged; "Graph board" enables the dsh-graph kanban, the structured handoff index (handoff/index.json) and two traversal tools (memory_expand / memory_trace, tool count 14→16). Restart dsh web after switching; switch back to fully roll back.'),
5952
- field(locale === 'zh' ? '子代理痕迹回收' : 'Subagent trace recycle', h('input', { type: 'checkbox', checked: cfg.subagentGcEnabled !== false, onChange: function (e) { set('subagentGcEnabled', e.target.checked) } }),
5953
- locale === 'zh' ? '本插件的一次性子代理(自动沉淀 / 时段总结 / 问候 / 蒸馏)每跑一次都会在 ~/.dsh/sessions 留下一个持久化会话;数量上千后会话列表会明显变慢。开启后任务一结束就把这些痕迹移入 ~/.dsh/subagent-gc-backup 备份(可回滚),不影响子代理结果。默认开。' : 'Every one-shot subagent this plugin spawns (auto-consolidation, scheduled summary, greeting, distillation) leaves a persisted session under ~/.dsh/sessions; hundreds of them slow the session list down. When on, each trace is moved into ~/.dsh/subagent-gc-backup right after the task ends (reversible), never affecting the subagent result. On by default.'),
5954
- field(locale === 'zh' ? '兜底回收保留天数' : 'Fallback recycle keep days', h('input', { 'data-dam-input': '', type: 'number', min: 0, value: cfg.subagentGcKeepDays === undefined ? 3 : cfg.subagentGcKeepDays, onChange: function (e) { set('subagentGcKeepDays', Number(e.target.value) || 0) } }),
5955
- locale === 'zh' ? '每天巡检一次,回收超过该天数仍残留的痕迹(比如任务异常中断没删掉的);0 = 不按时间,只靠任务结束即删。' : 'A daily sweep recycles traces older than this many days (e.g. leftovers from interrupted tasks); 0 = rely on the end-of-task recycle only.')]),
5956
- section('maintenance', sectionLabels.maintenance, [field(t('fVersion'), h('div', { 'data-dam-row': '' },
5957
- h('span', { style: { flex: 1 } }, verInfo ? (verInfo.current || '?') + (verInfo.latest ? ' → ' + verInfo.latest + (verInfo.upToDate ? ' ' + t('upToDate') : ' ' + t('hasUpdate')) : '') : (checkingUpdate ? t('checking') : '—')),
5958
- h('button', { 'data-dam-btn': '', onClick: checkUpdate, disabled: checkingUpdate }, checkingUpdate ? t('checking') : t('checkUpdate')),
5959
- (verInfo && verInfo.latest && !verInfo.upToDate && verInfo.installKind === 'registry') ? h('button', { 'data-dam-btn': '', onClick: doUpdate, disabled: upBusy, style: { marginLeft: '4px' } }, upBusy ? t('updating') : t('updateNow')) : null,
5960
- upMsg ? h('span', { 'data-dam-hint': '' }, upMsg) : null),
5961
- t('versionCmdHint') + (verInfo && verInfo.error ? ' ' + t('versionError') + verInfo.error : '') + (verInfo && verInfo.installKind === 'dev-link' ? ' ' + t('devLinkHint') : '') + (verInfo && !verInfo.installKind ? ' ' + t('noProfileHint') : '')),
7037
+ section('about', sectionLabels.about, [
7038
+ // WB-GRAPH 白板线一键切换(2026-09-16, board_mode_v1): 旧版白板(默认) / 新版看板(dsh-graph vendor
7039
+ // + sidecar + 遍历工具, 工具数 14→16)。开关解耦: 只管白板线; 切换需重启 dsh web 生效(工具注册在启动期)。
7040
+ field(locale === 'zh' ? '白板模式(旧版白板 / 新版看板)' : 'Board mode (legacy / graph)', (function () {
7041
+ var cur = cfg.boardMode === 'graph' ? 'graph' : 'legacy'
7042
+ // ★2026-09-16 修 BUG-3(设置页切换不落盘): 旧实现 `set('boardMode', m)` 只改**本地 React 态**
7043
+ // 然后 reload —— 配置从未写到服务端, 刷新后仍是旧值(界面回跳原档), 用户会判定"开关坏了"。
7044
+ // 正解: 走与接续面板同一个写入路径 `saveConfigPatch(API.config, patch)`(见 :999 定义),
7045
+ // 写盘成功后再 reload; 失败则在界面提示, 不静默假装成功。
7046
+ // 同时满足用户硬性偏好: 开关类改动必须**即时回显**(写盘后立即 reload 呈现新档)。
7047
+ function pick(m) {
7048
+ set('boardMode', m) // 先本地回显(即时反馈)
7049
+ saveConfigPatch({ boardMode: m }, {
7050
+ onSaved: function () { window.setTimeout(function () { window.location.reload() }, 350) },
7051
+ onError: function (e) { setErr(String((e && e.message) || e || 'save failed')) },
7052
+ })
7053
+ }
7054
+ return h('div', { 'data-dam-row': '' },
7055
+ h('button', { 'data-dam-btn': '', 'data-dam-key': 'boardMode', style: { fontWeight: cur === 'legacy' ? 700 : 400, opacity: cur === 'legacy' ? 1 : 0.65 }, onClick: function () { if (cur !== 'legacy') pick('legacy') } }, locale === 'zh' ? '旧版白板' : 'Legacy board'),
7056
+ h('button', { 'data-dam-btn': '', 'data-dam-key': 'boardModeGraph', style: { marginLeft: '6px', fontWeight: cur === 'graph' ? 700 : 400, opacity: cur === 'graph' ? 1 : 0.65 }, onClick: function () { if (cur !== 'graph') pick('graph') } }, locale === 'zh' ? '新版看板(dsh-graph)' : 'Graph board (dsh-graph)'))
7057
+ })(),
7058
+ locale === 'zh' ? '一键切换白板形态。默认「旧版白板」=一切行为不变;切到「新版看板」后启用 dsh-graph 看板、白板结构化索引(handoff/index.json)与两个遍历工具(memory_expand / memory_trace,工具数 14→16)。切换后需重启 dsh web 生效;再点回旧版即完全回滚。' : 'One-click switch for the board form. Default "Legacy board" keeps everything unchanged; "Graph board" enables the dsh-graph kanban, the structured handoff index (handoff/index.json) and two traversal tools (memory_expand / memory_trace, tool count 14→16). Restart dsh web after switching; switch back to fully roll back.'),
7059
+ field(t('fVersion'), h('div', { 'data-dam-row': '' },
7060
+ h('span', { style: { flex: 1 } }, verInfo ? (verInfo.current || '?') + (verInfo.latest ? ' → ' + verInfo.latest + (verInfo.upToDate ? ' ' + t('upToDate') : ' ' + t('hasUpdate')) : '') : (checkingUpdate ? t('checking') : '—')),
7061
+ h('button', { 'data-dam-btn': '', onClick: checkUpdate, disabled: checkingUpdate }, checkingUpdate ? t('checking') : t('checkUpdate')),
7062
+ (verInfo && verInfo.latest && !verInfo.upToDate && verInfo.installKind === 'registry') ? h('button', { 'data-dam-btn': '', onClick: doUpdate, disabled: upBusy, style: { marginLeft: '4px' } }, upBusy ? t('updating') : t('updateNow')) : null,
7063
+ upMsg ? h('span', { 'data-dam-hint': '' }, upMsg) : null),
7064
+ t('versionCmdHint') + (verInfo && verInfo.error ? ' ' + t('versionError') + verInfo.error : '') + (verInfo && verInfo.installKind === 'dev-link' ? ' ' + t('devLinkHint') : '') + (verInfo && !verInfo.installKind ? ' ' + t('noProfileHint') : '')),
5962
7065
  field(locale === 'zh' ? '交流群' : 'Community', h('div', { 'data-dam-row': '' },
5963
7066
  h('span', { style: { flex: 1 } }, locale === 'zh' ? '反馈问题、交流使用技巧——加群响应比 issue 更快。' : 'Share feedback and tips with other users — the group responds faster than GitHub issues.'),
5964
7067
  h('a', { href: 'https://qm.qq.com/q/v7Asxn6vPa', target: '_blank', rel: 'noreferrer', style: { textDecoration: 'none', color: 'var(--dam-accent, #4f7cff)' } }, locale === 'zh' ? '点击加入 QQ 交流群' : 'Join the QQ community group')),
5965
- locale === 'zh' ? '点击链接一键加群。' : 'One-click join via the QQ invite link.')]),
7068
+ locale === 'zh' ? '点击链接一键加群。' : 'One-click join via the QQ invite link.'),
7069
+ ]),
5966
7070
  h('div', { 'data-dam-savebar': '' },
5967
7071
  h('button', { 'data-dam-btn': '', 'data-dirty': dirty ? 'true' : undefined, onClick: save, disabled: busy }, busy ? t('saving') : (dirty ? (locale === 'zh' ? '保存更改' : 'Save changes') : t('saveSettings'))),
5968
7072
  dirty ? h('span', { 'data-dam-hint': '' }, locale === 'zh' ? '有未保存的更改' : 'Unsaved changes') : null,
@@ -6236,6 +7340,19 @@ window.__ModuleLoader__.load({
6236
7340
  surfaceDisposers.push(slots.inject('settings.section', function () {
6237
7341
  return slots.register({ name: 'settings.section', id: 'auto-memory', order: 25, label: t('autoMemory') + ' (pre)' }, function (props) { return h(SettingsPage, { close: props && props.close }) })
6238
7342
  }))
7343
+ // ★2026-09-21 二次修正:「记忆」会话页 —— 与「对话轨迹」「上下文」「白板看板」并列的一页。
7344
+ // 用户实测反馈:「它现在是浮在整个页面上方的,而不是和对话轨迹、上下文、白板看板在一起,作为一个单独的一页」。
7345
+ // 根因:宿主只给 4 个槽位(sidebar/main/rightbar/shell.overlay),overlay 里的东西只能"盖"在界面上;
7346
+ // 要成为并列页必须走 conversation.view(官方对话轨迹与本插件白板看板都注册在这里)。
7347
+ // 出现条件:承载面 = 会话页 / 两者共存;切回「左下角浮层」时移除该页签(由 refreshSurfaces 重注册)。
7348
+ if (controller.panelPos() === 'page' || controller.panelPos() === 'both') {
7349
+ surfaceDisposers.push(slots.inject('conversation.view', function () {
7350
+ return slots.register(
7351
+ { name: 'conversation.view', id: 'auto-memory-panel', order: 78, label: function () { return locale === 'zh' ? '记忆' : 'Memory' } },
7352
+ function (props) { return h(MemoryPageView, props || {}) },
7353
+ )
7354
+ }))
7355
+ }
6239
7356
  // 2026-09-16「双承载面」: 会话页顶栏再挂一个**整页白板看板**(与侧边面板共存)。
6240
7357
  // 面板 440px 装不下矩阵, 整页 ~1000px+ 可以 ⇒ 两者按容器宽度各用其形态。
6241
7358
  // 与 boardMode 闸门解耦: 组件内部自己取数, 非 graph 档拿到 enabled:false 就显示提示,
@@ -6276,8 +7393,12 @@ window.__ModuleLoader__.load({
6276
7393
  surfaceDisposers = []
6277
7394
  registerSurfaces()
6278
7395
  }
7396
+ // 把「重注册承载面」暴露给模块作用域:设置页切换承载面时,「记忆」会话页签要当场增/删(见 setSurfacesRefreshHook)
7397
+ setSurfacesRefreshHook(refreshSurfaces)
7398
+ // ★2026-09-21 二次修正:syncTitlebarVar() 及其调用点已一并删除 —— 它只为「顶部通栏覆盖条」让位 Windows
7399
+ // 自绘标题栏而存在,该形态废弃后无消费者(会话页在正常文档流里,由宿主布局负责)。
6279
7400
  registerSurfaces()
6280
- console.log('[dsh-auto-memory] client ready: sidebar entry + panel + settings page')
7401
+ console.log('[dsh-auto-memory] client ready: sidebar entry + floating card / memory session page (switchable) + settings page')
6281
7402
  }
6282
7403
 
6283
7404
  exports.inject = ['slots', 'sessions', 'remote', 'remote.session']