@modusensus/dsh-mneme 0.9.0 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/README.md +82 -19
  2. package/bin/cli.mjs +69 -9
  3. package/docs/CONFIGURATION.md +48 -0
  4. package/docs/SEMANTIC.md +3 -3
  5. package/lib/api-standalone.js +78 -12
  6. package/lib/api.js +339 -59
  7. package/lib/candidates.js +454 -454
  8. package/lib/client.js +669 -256
  9. package/lib/config.js +50 -2
  10. package/lib/dream/sleep.js +35 -12
  11. package/lib/dream.js +86 -14
  12. package/lib/embedding.js +153 -22
  13. package/lib/index.js +32 -0
  14. package/lib/inject.js +34 -3
  15. package/lib/lang.js +124 -0
  16. package/lib/pipeline-activities.js +178 -0
  17. package/lib/pipeline-attribution.js +165 -0
  18. package/lib/pipeline-command.js +279 -0
  19. package/lib/pipeline-confirm.js +142 -0
  20. package/lib/pipeline-exec.js +174 -0
  21. package/lib/pipeline-nudge.js +1281 -0
  22. package/lib/pipeline-observations.js +181 -0
  23. package/lib/pipeline-observe.js +133 -0
  24. package/lib/pipeline-runner.js +346 -0
  25. package/lib/pipeline-runs.js +442 -0
  26. package/lib/pipeline.js +128 -7
  27. package/lib/scope-state.js +277 -0
  28. package/lib/semantic.js +66 -10
  29. package/lib/service.js +80 -3
  30. package/lib/settings.js +139 -1
  31. package/lib/store.js +216 -3
  32. package/lib/summarize.js +2 -3
  33. package/lib/tools.js +71 -0
  34. package/package.json +3 -3
  35. package/scripts/changelog-prep.mjs +62 -7
  36. package/scripts/check-sync.js +8 -6
  37. package/scripts/release-docs.mjs +204 -0
  38. package/scripts/release-notes.mjs +162 -0
  39. package/src/api-standalone.js +78 -12
  40. package/src/api.js +339 -59
  41. package/src/candidates.js +454 -454
  42. package/src/config.js +50 -2
  43. package/src/dream/sleep.js +35 -12
  44. package/src/dream.js +86 -14
  45. package/src/embedding.js +153 -22
  46. package/src/index.js +32 -0
  47. package/src/inject.js +34 -3
  48. package/src/lang.js +124 -0
  49. package/src/pipeline-activities.js +178 -0
  50. package/src/pipeline-attribution.js +165 -0
  51. package/src/pipeline-command.js +279 -0
  52. package/src/pipeline-confirm.js +142 -0
  53. package/src/pipeline-exec.js +174 -0
  54. package/src/pipeline-nudge.js +1281 -0
  55. package/src/pipeline-observations.js +181 -0
  56. package/src/pipeline-observe.js +133 -0
  57. package/src/pipeline-runner.js +346 -0
  58. package/src/pipeline-runs.js +442 -0
  59. package/src/pipeline.js +128 -7
  60. package/src/scope-state.js +277 -0
  61. package/src/semantic.js +66 -10
  62. package/src/service.js +80 -3
  63. package/src/settings.js +139 -1
  64. package/src/store.js +216 -3
  65. package/src/summarize.js +2 -3
  66. package/src/tools.js +71 -0
  67. package/test/api-hardening.test.js +173 -0
  68. package/test/api.test.js +210 -1
  69. package/test/changelog-prep.test.js +74 -8
  70. package/test/cli-enums.test.js +37 -0
  71. package/test/cli-flag-value.test.js +86 -0
  72. package/test/client.test.js +216 -6
  73. package/test/distill-tx-embed-gap.test.js +110 -0
  74. package/test/docs-config-keys.test.js +65 -0
  75. package/test/dream-maxtokens-fallback.test.js +132 -0
  76. package/test/embed-text-reasons.test.js +112 -0
  77. package/test/helpers/write-admission-samples.js +6 -2
  78. package/test/lib-smoke.test.js +20 -0
  79. package/test/persona-api.test.js +172 -0
  80. package/test/persona-flag.test.js +36 -0
  81. package/test/persona-inject.test.js +145 -0
  82. package/test/persona-settings.test.js +145 -0
  83. package/test/pipeline-activities.test.js +155 -0
  84. package/test/pipeline-attribution.test.js +175 -0
  85. package/test/pipeline-command.test.js +243 -0
  86. package/test/pipeline-confirm.test.js +148 -0
  87. package/test/pipeline-exec.test.js +172 -0
  88. package/test/pipeline-nudge.test.js +1036 -0
  89. package/test/pipeline-observations.test.js +157 -0
  90. package/test/pipeline-observe.test.js +127 -0
  91. package/test/pipeline-r1r2.test.js +149 -0
  92. package/test/pipeline-runner.test.js +442 -0
  93. package/test/pipeline-runs.test.js +767 -0
  94. package/test/pipeline.test.js +137 -6
  95. package/test/readme-claims.test.js +68 -0
  96. package/test/reindex-backfill.test.js +83 -4
  97. package/test/release-docs.test.js +130 -0
  98. package/test/release-notes.test.js +240 -0
  99. package/test/release-workflow.test.js +16 -0
  100. package/test/route-policy.test.js +113 -0
  101. package/test/scope-state.test.js +481 -0
  102. package/test/security-doc.test.js +139 -0
  103. package/test/sensitive-scan.test.js +44 -0
  104. package/test/tools-dto-schema-matrix.test.js +12 -3
  105. package/test/tools.test.js +6 -6
  106. package/test/workflow-guards.test.js +156 -0
package/lib/client.js CHANGED
@@ -147,7 +147,11 @@ window.__ModuleLoader__.load({
147
147
  flame: [["path", { d: "M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z" }]],
148
148
  // 意见与反馈入口的行首图标(Lucide 数据,同上 ISC 惯例内联)
149
149
  bug: [["path", { d: "m8 2 1.88 1.88" }], ["path", { d: "M14.12 3.88 16 2" }], ["path", { d: "M9 7.13v-1a3.003 3.003 0 1 1 6 0v1" }], ["path", { d: "M12 20c-3.3 0-6-2.7-6-6v-3a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v3c0 3.3-2.7 6-6 6" }], ["path", { d: "M12 20v-9" }], ["path", { d: "M6.53 9C4.6 8.8 3 7.1 3 5" }], ["path", { d: "M6 13H2" }], ["path", { d: "M3 21c0-2.1 1.7-3.9 3.8-4" }], ["path", { d: "M20.97 5c0 2.1-1.6 3.8-3.5 4" }], ["path", { d: "M22 13h-4" }], ["path", { d: "M17.2 17c2.1.1 3.8 1.9 3.8 4" }]],
150
- mail: [["rect", { width: "20", height: "16", x: "2", y: "4", rx: "2" }], ["path", { d: "m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" }]]
150
+ mail: [["rect", { width: "20", height: "16", x: "2", y: "4", rx: "2" }], ["path", { d: "m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" }]],
151
+ // star 是设置页鼓励入口的行首图标(Lucide 数据,同上 ISC 惯例内联)。
152
+ // Icon 对未注册的名字静默 return null——漏了它就是一行缺图标、闸门全绿,
153
+ // 所以这条表与用法必须同处出现(见 test/client.test.js 的图标名锁)。
154
+ star: [["polygon", { points: "12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" }]]
151
155
  };
152
156
  const Icon = ({ name, size = 16, className }) => {
153
157
  const parts = ICON_PATHS[name];
@@ -277,6 +281,31 @@ window.__ModuleLoader__.load({
277
281
  "memory.settings.profileHint": "写一段自我介绍(角色、背景、偏好),Agent 每轮对话都会自动带上。",
278
282
  "memory.settings.profileSave": "保存画像",
279
283
  "memory.settings.profileSaved": "画像已保存",
284
+ "memory.settings.persona": "人格定义(beta)",
285
+ "memory.settings.personaHint": "给这位 Agent 一张角色卡:名字、性格、回复要求。开关打开且下方「人格定义」总闸开启时,每轮对话都会注入。",
286
+ "memory.settings.personaName": "名字",
287
+ "memory.settings.personaNamePlaceholder": "例如:Nyx",
288
+ "memory.settings.personaPersonality": "性格设定",
289
+ "memory.settings.personaPersonalityPlaceholder": "例如:轻松幽默,讲难题时用类比帮用户快速理解",
290
+ "memory.settings.personaReplyStyle": "回复要求",
291
+ "memory.settings.personaReplyStylePlaceholder": "例如:先给结论再解释;用人在用的词,不用机制名",
292
+ "memory.settings.personaSave": "保存人格",
293
+ "memory.settings.personaSaved": "人格已保存",
294
+ "memory.settings.personaSaveFailed": "保存失败",
295
+ "memory.settings.personaEnabledToggle": "启用这张卡",
296
+ "memory.settings.personaEnabledOn": "已启用——身份命中的会话会注入这张卡",
297
+ "memory.settings.personaEnabledOff": "未启用——这个身份的会话将使用默认卡",
298
+ "memory.settings.personaDefaultCard": "默认卡(兜底)",
299
+ "memory.settings.personaAdd": "添加卡",
300
+ "memory.settings.personaAddPlaceholder": "输入 agent 身份名(如 nyx),按它寻址注入",
301
+ "memory.settings.personaEditingDefault": "正在编辑:默认卡——没有命中任何身份卡的会话用它",
302
+ "memory.settings.personaEditingFor": "正在编辑身份卡:",
303
+ "memory.settings.personaDeleteCard": "删除这张卡",
304
+ "memory.settings.personaView": "Agent 眼中的你(注入生效文本)",
305
+ "memory.settings.personaViewHint": "以下是每一轮实际注入的用户画像与规则,只读。",
306
+ "memory.settings.personaViewEmpty": "画像与规则均为空——Agent 每轮只会看到记忆库。",
307
+ "memory.features.personaEnabled": "人格定义(beta)",
308
+ "memory.features.personaEnabled.hint": "把「设置 → 个性化」里的人格卡注入到每轮对话。默认关(beta);总闸与人格卡内的开关同时打开才生效。",
280
309
  "memory.settings.rules": "规则",
281
310
  "memory.settings.rulesHint": "给 Agent 立下必须遵守的规矩,随时增删,下一轮即生效。",
282
311
  "memory.settings.ruleAdd": "添加规则",
@@ -299,12 +328,14 @@ window.__ModuleLoader__.load({
299
328
  "memory.explorer.tabStatus": "状态",
300
329
  "memory.entities.count": "{n} 个实体",
301
330
  "memory.entities.pick": "从左侧选择一个实体,查看它的属性与关系",
331
+ // 搜索框有字但一条没筛出来时用它;与 entities.none(库里真没实体)分开,
332
+ // 否则「暂无实体」会把打错一个字误导成「你还没开抽取」。
333
+ "memory.entities.noMatch": "没有匹配的实体,换个词试试",
302
334
  "memory.entities.none": "暂无实体:开启实体抽取后,记忆里的人物 / 项目 / 概念会自动沉淀到这里",
303
335
  "memory.entities.attrs": "属性",
304
336
  "memory.entities.relations": "关系",
305
337
  "memory.entities.mentions": "{n} 次提及",
306
338
  "memory.entities.lastSeen": "最近出现",
307
- "memory.entities.graph": "关系图谱",
308
339
  "memory.entity.person": "人物",
309
340
  "memory.entity.project": "项目",
310
341
  "memory.entity.concept": "概念",
@@ -329,8 +360,6 @@ window.__ModuleLoader__.load({
329
360
  "memory.explorer.tags": "标签",
330
361
  "memory.explorer.importance": "重要性",
331
362
  "memory.explorer.heat": "热度",
332
- "memory.explorer.sourceFilter": "来源",
333
- "memory.explorer.sort": "排序",
334
363
  "memory.explorer.heatSort": "热度优先",
335
364
  "memory.explorer.heatSortHint": "对已加载条目按热度降序(页内排序,非全局序;时间树保持时间序)",
336
365
  "memory.explorer.filterDeposited": "沉淀",
@@ -351,7 +380,6 @@ window.__ModuleLoader__.load({
351
380
  "memory.graph.depth": "跳数",
352
381
  "memory.graph.empty": "图谱待积累:随对话记忆的沉淀,实体与关系会自动进入图谱",
353
382
  "memory.graph.notFound": "未找到该实体",
354
- "memory.graph.attrs": "属性",
355
383
  "memory.graph.related": "关联记忆",
356
384
  "memory.graph.relation": "关系",
357
385
  "memory.graph.sourceMemory": "查看来源记忆",
@@ -373,8 +401,8 @@ window.__ModuleLoader__.load({
373
401
  "memory.settings.vectorReindexing": "索引中…",
374
402
  "memory.settings.vectorReindexDone": "已索引 {n} 条",
375
403
  "memory.settings.apiTokenTitle": "API Token(可选)",
376
- "memory.settings.apiTokenHint": "给面板的写操作加一道锁:设置后,修改记忆或配置需要携带 Token,纯浏览不受影响。清空并保存可关闭。",
377
- "memory.settings.apiTokenPlaceholder": "留空 = 不鉴权(默认)",
404
+ "memory.settings.apiTokenHint": "这里只把 Token 存在当前浏览器(localStorage),供面板请求携带;服务端的锁是插件配置里的 apiToken,两边填同一个值才真正上锁。只填这里不改服务端 = 没上锁。",
405
+ "memory.settings.apiTokenPlaceholder": "与插件配置的 apiToken 保持一致",
378
406
  "memory.settings.apiTokenSave": "保存 Token",
379
407
  "memory.settings.apiTokenSaved": "Token 已保存",
380
408
  "memory.settings.feedback.title": "意见与反馈",
@@ -386,8 +414,16 @@ window.__ModuleLoader__.load({
386
414
  "memory.settings.feedback.browse": "浏览已知问题",
387
415
  "memory.settings.feedback.browse.sub": "先搜搜,省一份重复 issue",
388
416
  "memory.settings.feedback.hint": "反馈前先搜搜是否已有相同问题,能省一份重复的 issue~",
389
- "memory.settings.version.outdated": "🆕 有新版本 {v}(当前运行 {c})",
417
+ "memory.settings.update.check": "检测更新",
418
+ "memory.settings.update.latest": "已是最新版本 ✓",
419
+ "memory.settings.update.ahead": "当前版本比 registry 还新(本地开发版)",
420
+ "memory.settings.update.error": "检测失败:稍后再试",
421
+ // 检测更新点出的第三种结果(发现新版本);横幅形态已删,它现在只是
422
+ // 按需那一行的文案,占位 {v}/{c} 由 checkResultText 填。
423
+ "memory.settings.version.outdated": "有新版本 {v}(当前运行 {c})",
390
424
  "memory.settings.version.outdatedHint": "若安装时指定过版本号,常规升级不会跨大/小版本(pnpm 钉子)——请重新安装或在升级命令加 --latest;npm 已发布而市场暂未收录属正常(约 1 天延迟)。",
425
+ "memory.settings.star": "给项目点个 Star",
426
+ "memory.settings.star.sub": "它对你是好用的,就鼓励一下作者吧 (●'◡'●)",
391
427
  "memory.settings.mode.title": "运行模式",
392
428
  "memory.settings.mode.desc": "轻量模式只保留核心能力:记忆读写、自动注入、热记忆;巩固、实体抽取、语义搜索等全部关闭,适合低配机器或只想「记住偏好」的场景。标准模式开启全部功能。",
393
429
  "memory.settings.mode.light": "轻量",
@@ -430,6 +466,9 @@ window.__ModuleLoader__.load({
430
466
  // 「别的会话能不能看到」由同在首屏的 scope 组回答。
431
467
  "memory.features.group.basic": "常用(先动这几个)",
432
468
  "memory.features.group.core": "它会记住什么",
469
+ // group.mo 是 v0.9.0 新增的独立分组(座位闸门,不是行为开关);渲染
470
+ // 走 t(`memory.features.${g.key}`),缺这行会把键名直接写上屏。
471
+ "memory.features.group.mo": "它的工作流(M.O.)",
433
472
  "memory.features.group.enhance": "它什么时候写进去",
434
473
  "memory.features.group.dream": "它怎么用回来",
435
474
  "memory.features.group.scope": "不同会话之间",
@@ -456,6 +495,11 @@ window.__ModuleLoader__.load({
456
495
  "memory.features.tagDefault.hint": "这一项用的是出厂默认值,没有被你改过",
457
496
  "memory.features.tagChanged": "已改",
458
497
  "memory.features.tagChanged.hint": "你改过这一项,它不再用默认值",
498
+ // 「待重启」是第三态,不是「已改」的加强版:#481 之前面板只有「已改」,
499
+ // 用户改完总闸看到徽标亮起就以为生效了,实际运行时还在用启动时的旧值。
500
+ "memory.features.tagPending": "待重启",
501
+ "memory.features.tagPending.hint": "这一项已存进配置,但本次启动还没用上——重启 DSH(或在插件管理里关开一次本插件)后才生效",
502
+ "memory.features.pendingRestartNotice": "有改动还没生效:标着「待重启」的开关要等重启 DSH 后才起作用(在插件管理里把本插件关开一次也行)。",
459
503
  "memory.features.autoSummarize": "自动把对话记下来(自动注入之外)",
460
504
  "memory.features.autoSummarize.hint": "开:对话结束自动提炼成长期记忆条目(autoSummarize);关:只有你手动存或 Agent 主动存才有记忆",
461
505
  "memory.features.hotMemoryEnabled": "顺带带上最近几轮原话",
@@ -528,10 +572,13 @@ window.__ModuleLoader__.load({
528
572
  "memory.actions.credit": "复用",
529
573
  "memory.actions.creditValue": "{n} 次 · 上次 {d} 天前",
530
574
  "memory.actions.dep": "依赖",
575
+ "memory.actions.zoom.value": "缩放 {n}%",
531
576
  "memory.actions.zoom.in": "放大",
532
577
  "memory.actions.zoom.out": "缩小",
533
578
  "memory.actions.zoom.fit": "适应宽度",
534
579
  "memory.actions.zoom.hint": "Ctrl/⌘ + 滚轮也能缩放",
580
+ // 缩放百分比指示({n} 由画布工具条填);基线 4568614 起就缺这两行,
581
+ // 工具条上会直接显示键名。
535
582
  "memory.actions.list.heading": "这个项目里的工作流",
536
583
  "memory.actions.list.count": "工作流 {n} 条",
537
584
  "memory.actions.list.meta": "{steps} 步 · {gates} 道闸门 · 反事实 {cf}/{steps}",
@@ -634,6 +681,12 @@ window.__ModuleLoader__.load({
634
681
  "memory.features.sleepProvider": "睡眠 Provider",
635
682
  "memory.features.sleepModel": "睡眠模型",
636
683
  "memory.features.sleepModelHint": "留空 = 用巩固模型或当前模型;建议选非思考模型",
684
+ // Issue #478:max_tokens 预算键。hint 写清两件事:这是「单轮输出预算」,
685
+ // 以及服务商硬上限更小时后端会按报错自动降级重试(对齐 #478 的后端行为)。
686
+ "memory.features.dreamMaxTokens": "巩固单轮输出上限 (max_tokens)",
687
+ "memory.features.dreamMaxTokens.hint": "单轮巩固最多生成多少 token,256–131072。默认 131072 超过部分服务商硬上限时,后端会按报错里的上限自动降级重试",
688
+ "memory.features.sleepMaxTokens": "睡眠单轮输出上限 (max_tokens)",
689
+ "memory.features.sleepMaxTokens.hint": "睡眠阶段冲突/模式判断的单轮输出预算,256–131072;超上限同样自动降级",
637
690
  "memory.features.routeFollowDefault": "跟随默认路由",
638
691
  "memory.features.modelTest": "测试连通性",
639
692
  "memory.features.modelTesting": "测试中…",
@@ -911,6 +964,31 @@ window.__ModuleLoader__.load({
911
964
  "memory.settings.profileHint": "Describe yourself once — the agent reads it every turn.",
912
965
  "memory.settings.profileSave": "Save Profile",
913
966
  "memory.settings.profileSaved": "Profile saved",
967
+ "memory.settings.persona": "Persona (beta)",
968
+ "memory.settings.personaHint": "Give this agent a character card: name, personality, reply style. Injected every turn when the toggle below and the \"Persona\" master switch are both on.",
969
+ "memory.settings.personaName": "Name",
970
+ "memory.settings.personaNamePlaceholder": "e.g. Nyx",
971
+ "memory.settings.personaPersonality": "Personality",
972
+ "memory.settings.personaPersonalityPlaceholder": "e.g. relaxed and humorous; use analogies for hard topics",
973
+ "memory.settings.personaReplyStyle": "Reply style",
974
+ "memory.settings.personaReplyStylePlaceholder": "e.g. conclusion first; plain words over jargon",
975
+ "memory.settings.personaSave": "Save Persona",
976
+ "memory.settings.personaSaved": "Persona saved",
977
+ "memory.settings.personaSaveFailed": "Save failed",
978
+ "memory.settings.personaEnabledToggle": "Enable this card",
979
+ "memory.settings.personaEnabledOn": "On — sessions matching this identity get this card",
980
+ "memory.settings.personaEnabledOff": "Off — sessions with this identity fall back to the default card",
981
+ "memory.settings.personaDefaultCard": "Default (fallback)",
982
+ "memory.settings.personaAdd": "Add card",
983
+ "memory.settings.personaAddPlaceholder": "Agent identity (e.g. nyx) used to address injection",
984
+ "memory.settings.personaEditingDefault": "Editing: default card — used by sessions matching no identity card",
985
+ "memory.settings.personaEditingFor": "Editing identity card:",
986
+ "memory.settings.personaDeleteCard": "Delete this card",
987
+ "memory.settings.personaView": "You, as the agent sees you (injected text)",
988
+ "memory.settings.personaViewHint": "The profile and rules actually injected every turn. Read-only.",
989
+ "memory.settings.personaViewEmpty": "Profile and rules are both empty — the agent only sees the memory store.",
990
+ "memory.features.personaEnabled": "Persona (beta)",
991
+ "memory.features.personaEnabled.hint": "Inject the persona card (Settings → Personalization) into every turn. Off by default (beta); both this switch and the card's own toggle must be on.",
914
992
  "memory.settings.rules": "Rules",
915
993
  "memory.settings.rulesHint": "Rules the agent must follow — add or remove anytime, effective next turn.",
916
994
  "memory.settings.ruleAdd": "Add Rule",
@@ -933,12 +1011,12 @@ window.__ModuleLoader__.load({
933
1011
  "memory.explorer.tabStatus": "Status",
934
1012
  "memory.entities.count": "{n} entities",
935
1013
  "memory.entities.pick": "Select an entity to see its attributes and relations",
1014
+ "memory.entities.noMatch": "No entities match that search — try another word",
936
1015
  "memory.entities.none": "No entities yet — turn on entity extraction and people / projects / concepts will accumulate here",
937
1016
  "memory.entities.attrs": "Attributes",
938
1017
  "memory.entities.relations": "Relations",
939
1018
  "memory.entities.mentions": "{n} mentions",
940
1019
  "memory.entities.lastSeen": "Last seen",
941
- "memory.entities.graph": "Relation graph",
942
1020
  "memory.entity.person": "People",
943
1021
  "memory.entity.project": "Projects",
944
1022
  "memory.entity.concept": "Concepts",
@@ -963,8 +1041,6 @@ window.__ModuleLoader__.load({
963
1041
  "memory.explorer.tags": "Tags",
964
1042
  "memory.explorer.importance": "Importance",
965
1043
  "memory.explorer.heat": "Heat",
966
- "memory.explorer.sourceFilter": "Source",
967
- "memory.explorer.sort": "Sort",
968
1044
  "memory.explorer.heatSort": "By heat",
969
1045
  "memory.explorer.heatSortHint": "Sort loaded items by heat, in-page (not global; the month tree keeps chronological order)",
970
1046
  "memory.explorer.filterDeposited": "Deposited",
@@ -985,7 +1061,6 @@ window.__ModuleLoader__.load({
985
1061
  "memory.graph.depth": "hops",
986
1062
  "memory.graph.empty": "The graph is waiting for data: entities and relations accumulate as memories are extracted",
987
1063
  "memory.graph.notFound": "Entity not found",
988
- "memory.graph.attrs": "Attributes",
989
1064
  "memory.graph.related": "Related memories",
990
1065
  "memory.graph.relation": "Relation",
991
1066
  "memory.graph.sourceMemory": "View source memory",
@@ -1007,8 +1082,8 @@ window.__ModuleLoader__.load({
1007
1082
  "memory.settings.vectorReindexing": "Indexing…",
1008
1083
  "memory.settings.vectorReindexDone": "Indexed {n} items",
1009
1084
  "memory.settings.apiTokenTitle": "API Token (optional)",
1010
- "memory.settings.apiTokenHint": "Lock write operations behind a token — browsing stays open. Clear and save to disable.",
1011
- "memory.settings.apiTokenPlaceholder": "Empty = no auth (default)",
1085
+ "memory.settings.apiTokenHint": "This field only stores the token in this browser (localStorage) for the panel requests. The server-side lock is the plugin\u0027s apiToken config — set both to the same value; this field alone locks nothing.",
1086
+ "memory.settings.apiTokenPlaceholder": "Must match the plugin\u0027s apiToken",
1012
1087
  "memory.settings.apiTokenSave": "Save Token",
1013
1088
  "memory.settings.apiTokenSaved": "Token saved",
1014
1089
  "memory.settings.feedback.title": "Feedback",
@@ -1020,8 +1095,14 @@ window.__ModuleLoader__.load({
1020
1095
  "memory.settings.feedback.browse": "Browse known issues",
1021
1096
  "memory.settings.feedback.browse.sub": "Check if it's already reported",
1022
1097
  "memory.settings.feedback.hint": "Search for an existing issue first — it saves a duplicate.",
1023
- "memory.settings.version.outdated": "🆕 New version {v} available (running {c})",
1098
+ "memory.settings.update.check": "Check for updates",
1099
+ "memory.settings.update.latest": "Up to date ✓",
1100
+ "memory.settings.update.ahead": "Newer than the registry (local build)",
1101
+ "memory.settings.update.error": "Check failed: try again later",
1102
+ "memory.settings.version.outdated": "New version {v} available (running {c})",
1024
1103
  "memory.settings.version.outdatedHint": "If the plugin was installed with a pinned version, regular upgrades never cross minor/major lines — reinstall or pass --latest. A fresh npm release may take about a day to appear in the market.",
1104
+ "memory.settings.star": "Star the project",
1105
+ "memory.settings.star.sub": "If it serves you well, cheer the author on (●'◡'●)",
1025
1106
  "memory.settings.mode.title": "Runtime mode",
1026
1107
  "memory.settings.mode.desc": "Light mode keeps the core loop only: memory read/write, auto-injection and hot memory — consolidation, entity extraction and semantic search are all off. For modest machines or when you just want preferences remembered. Standard mode enables everything.",
1027
1108
  "memory.settings.mode.light": "Light",
@@ -1065,6 +1146,7 @@ window.__ModuleLoader__.load({
1065
1146
  // "can other sessions see this?" is answerable without opening anything.
1066
1147
  "memory.features.group.basic": "Common (start here)",
1067
1148
  "memory.features.group.core": "What gets remembered",
1149
+ "memory.features.group.mo": "Its workflow (M.O.)",
1068
1150
  "memory.features.group.enhance": "When it gets written",
1069
1151
  "memory.features.group.dream": "How it comes back",
1070
1152
  "memory.features.group.scope": "Between sessions",
@@ -1085,6 +1167,9 @@ window.__ModuleLoader__.load({
1085
1167
  "memory.features.tagDefault.hint": "This one is at its factory default — you have not changed it",
1086
1168
  "memory.features.tagChanged": "changed",
1087
1169
  "memory.features.tagChanged.hint": "You changed this one; it no longer uses the default",
1170
+ "memory.features.tagPending": "restart pending",
1171
+ "memory.features.tagPending.hint": "Saved, but not yet in effect for this session — restart DSH (or toggle this plugin off and on in the plugin manager) to apply it",
1172
+ "memory.features.pendingRestartNotice": "Some changes are not in effect yet: switches marked \"restart pending\" apply only after you restart DSH (toggling this plugin off and on in the plugin manager works too).",
1088
1173
  "memory.features.autoSummarize": "Turn conversations into memory automatically",
1089
1174
  "memory.features.autoSummarize.hint": "On: memory entries are distilled when a conversation ends (autoSummarize). Off: memory only exists if you save it, or the agent decides to",
1090
1175
  "memory.features.hotMemoryEnabled": "Carry the last few turns verbatim",
@@ -1155,6 +1240,7 @@ window.__ModuleLoader__.load({
1155
1240
  "memory.actions.credit": "Reuse",
1156
1241
  "memory.actions.creditValue": "{n} uses · last {d}d ago",
1157
1242
  "memory.actions.dep": "depends on",
1243
+ "memory.actions.zoom.value": "Zoom {n}%",
1158
1244
  "memory.actions.zoom.in": "Zoom in",
1159
1245
  "memory.actions.zoom.out": "Zoom out",
1160
1246
  "memory.actions.zoom.fit": "Fit width",
@@ -1257,6 +1343,12 @@ window.__ModuleLoader__.load({
1257
1343
  "memory.features.sleepProvider": "Sleep provider",
1258
1344
  "memory.features.sleepModel": "Sleep model",
1259
1345
  "memory.features.sleepModelHint": "Leave empty to reuse the consolidation model; a non-reasoning model is recommended",
1346
+ // Issue #478: max_tokens budget keys. Hint states both the scope (single-run
1347
+ // output budget) and the backend auto-downgrade on provider cap rejection.
1348
+ "memory.features.dreamMaxTokens": "Consolidation output cap (max_tokens)",
1349
+ "memory.features.dreamMaxTokens.hint": "Max tokens generated per consolidation run, 256–131072. When the default 131072 exceeds a provider's hard limit, the backend retries with the cap from the error",
1350
+ "memory.features.sleepMaxTokens": "Sleep output cap (max_tokens)",
1351
+ "memory.features.sleepMaxTokens.hint": "Single-run output budget for sleep conflict/mode calls, 256–131072; over-cap values are downgraded automatically",
1260
1352
  "memory.features.routeFollowDefault": "Follow default route",
1261
1353
  "memory.features.modelTest": "Test connectivity",
1262
1354
  "memory.features.modelTesting": "Testing…",
@@ -1635,17 +1727,26 @@ window.__ModuleLoader__.load({
1635
1727
  ".mneme-xcount{flex:none;font-size:12px;line-height:16px;color:var(--mneme-fg-dim);white-space:nowrap}",
1636
1728
  // --- three-column browse layout: hairline separators, no outer box ---
1637
1729
  ".mneme-xmain{flex:1;min-height:0;display:flex;flex-direction:row;overflow:hidden}",
1638
- ".mneme-xside{flex:none;width:236px;min-width:0;min-height:0;overflow-y:auto;padding:16px 14px;border-right:1px solid var(--dsw-alias-border-l2);box-sizing:border-box;display:flex;flex-direction:column;gap:8px}",
1639
- ".mneme-xside--filter{width:214px}",
1730
+ ".mneme-xside{flex:none;width:264px;min-width:0;min-height:0;overflow-y:auto;overflow-x:hidden;padding:16px 14px;border-right:1px solid var(--dsw-alias-border-l2);box-sizing:border-box;display:flex;flex-direction:column;gap:8px}",
1731
+ // 左栏下段=重要性+来源+热度:紧跟检索段之后(复验修订:不再钉底),
1732
+ // 顶部细线保留分段感;栏宽 264px 让 4 枚星级 chip 单行排布。
1733
+ ".mneme-xside-imp{flex:none;margin-top:10px;padding-top:12px;border-top:1px solid var(--dsw-alias-border-l1);display:flex;flex-direction:column;gap:8px}",
1734
+ // 内容区列容器 + 顶部工具条:类型 chips 横排 + 计数/刷新靠右;窄容器靠 wrap 换行。
1735
+ ".mneme-xcontent{flex:1;min-width:0;min-height:0;display:flex;flex-direction:column;overflow:hidden}",
1736
+ ".mneme-xbrowsebar{flex:none;display:flex;align-items:center;gap:6px;flex-wrap:wrap;padding:8px 16px;border-bottom:1px solid var(--dsw-alias-border-l2)}",
1737
+ // 窄容器(better-sidebar 工作台 ~500px):左栏收窄保内容区可用宽度。
1738
+ "@container (max-width: 640px){.mneme-xside{width:216px}}",
1739
+ // 类型筛选原为第二侧栏的竖排列表(三栏布局遗留,#面板两栏改版):
1740
+ // 两栏化后收进内容区工具条,改为自适应横排 chips,彩点与计数保留。
1741
+ ".mneme-xtype{display:inline-flex;justify-content:flex-start;align-items:center;gap:7px;padding:4px 9px;border:none;border-radius:8px;background:none;color:var(--dsw-alias-label-secondary);cursor:pointer;font-family:inherit;font-size:13px;line-height:18px;text-align:left}",
1640
1742
  ".mneme-xbrowse{flex:1;min-width:0;min-height:0;display:flex;flex-direction:column;overflow:hidden}",
1641
1743
  ".mneme-xrow{display:flex;align-items:center;gap:8px;flex-wrap:wrap}",
1642
1744
  ".mneme-xsearch{width:100%}",
1643
1745
  ".mneme-xselect{flex:1;min-width:0}",
1644
1746
  ".mneme-xcolhead{flex:none;font-size:12px;font-weight:500;margin:0;color:var(--mneme-fg-dim);padding:2px 8px 8px}",
1645
- ".mneme-xtype{display:flex;justify-content:flex-start;align-items:center;gap:8px;width:100%;padding:5px 8px;border:none;border-radius:8px;background:none;color:var(--dsw-alias-label-secondary);cursor:pointer;font-family:inherit;font-size:13px;line-height:18px;text-align:left}",
1646
1747
  ".mneme-xtype:hover{background:var(--dsw-alias-interactive-bg-hover)}",
1647
1748
  ".mneme-xtype.mneme-active{background:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-primary);font-weight:500}",
1648
- ".mneme-xcount2{flex:none;margin-left:auto;font-size:12px;color:var(--mneme-fg-dim)}",
1749
+ ".mneme-xcount2{flex:none;font-size:12px;color:var(--mneme-fg-dim)}",
1649
1750
  ".mneme-xmonth{display:flex;align-items:center;gap:4px;width:100%;padding:6px 8px 4px;border:none;border-radius:8px;background:none;color:var(--dsw-alias-label-primary);cursor:pointer;font-family:inherit;font-size:13px;font-weight:500;line-height:18px;text-align:left}",
1650
1751
  ".mneme-xmonth:first-child{margin-top:0}",
1651
1752
  ".mneme-xmonth:hover{background:var(--dsw-alias-interactive-bg-hover)}",
@@ -1687,20 +1788,26 @@ window.__ModuleLoader__.load({
1687
1788
  // 虚线态靠 dash 图案区分,不再叠 opacity:50% 的描边再乘 .7 后浅色画布上
1688
1789
  // 只剩 2.28,低于图形要求的 3:1(图案已经承担了「没有 memory_id」这个语义)。
1689
1790
  ".mneme-gedge-dashed{stroke-dasharray:5 4}",
1690
- ".mneme-graphhint{flex:none;color:var(--mneme-fg-dim);font-size:13px;text-align:center;padding:6px 0 2px}",
1691
- ".mneme-graphside{flex:none;max-height:220px;overflow-y:auto;border-top:1px solid var(--dsw-alias-border-l1);margin-top:10px;padding-top:10px}",
1791
+ // 图谱与图内侧卡的共同父容器:侧卡改浮层(#面板实体页去冗余)后需要
1792
+ // relative 锚点,否则 absolute 会逃逸到 .mneme-ent 根。
1793
+ ".mneme-entgraphwrap{position:relative;min-width:0}",
1794
+ // 侧卡从「SVG 下方文档流堆叠」(把图挤成邮票)改为图内左下浮层:
1795
+ // 反应面板与点击焦点同位,SVG 不再被推挤;背景用不透明 bg-layer-1,
1796
+ // 浮层下透出的图形不参与文字对比度计算。
1797
+ ".mneme-graphside{position:absolute;left:12px;bottom:12px;width:min(300px,60%);max-height:60%;overflow-y:auto;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-1);padding:10px 12px;z-index:2}",
1692
1798
  ".mneme-gs-title{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary);margin-bottom:4px;display:flex;justify-content:space-between;align-items:baseline;gap:8px}",
1693
1799
  ".mneme-gs-meta{font-size:13px;color:var(--mneme-fg-dim);margin-bottom:6px}",
1694
- ".mneme-gs-attr{display:flex;gap:6px;font-size:13px;padding:2px 0}",
1695
- ".mneme-gs-attrkey{flex:none;color:var(--mneme-fg-dim);font-size:13px}",
1696
- ".mneme-gs-attrval{color:var(--dsw-alias-label-secondary);word-break:break-word;font-size:13px}",
1697
1800
  ".mneme-gs-link{display:block;width:100%;text-align:left;border:none;background:none;color:var(--dsw-alias-label-secondary);cursor:pointer;font-family:inherit;font-size:13px;padding:3px 6px;border-radius:6px;word-break:break-word}",
1698
1801
  ".mneme-gs-link:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}",
1699
1802
  // --- entity explorer: rail + layered detail (entities → attrs → relations → graph) ---
1700
1803
  ".mneme-ent{flex:1;min-height:0;display:flex;overflow:hidden}",
1701
- ".mneme-entrail{flex:none;width:224px;min-height:0;overflow-y:auto;padding:12px;border-right:1px solid var(--dsw-alias-border-l2);box-sizing:border-box;display:flex;flex-direction:column;gap:4px}",
1702
- ".mneme-enttype{display:flex;align-items:center;gap:6px;width:100%;padding:8px 8px 2px;border:none;background:none;color:var(--mneme-fg-dim);cursor:pointer;font-family:inherit;font-size:11px;font-weight:600;line-height:16px;text-align:left;letter-spacing:.02em}",
1703
- ".mneme-entitem{display:flex;align-items:center;gap:8px;width:100%;padding:5px 8px;border:none;border-radius:8px;background:none;color:var(--dsw-alias-label-secondary);cursor:pointer;font-family:inherit;font-size:13px;line-height:18px;text-align:left}",
1804
+ ".mneme-entrail{flex:none;width:224px;min-height:0;overflow:hidden;padding:12px 0 0;border-right:1px solid var(--dsw-alias-border-l2);box-sizing:border-box;display:flex;flex-direction:column}",
1805
+ // 复验修订:搜索与计数固定在顶部不随列表滚动,列表独立滚动;
1806
+ // 类型分组头 sticky——长列表滚动时分组归属始终可见。
1807
+ ".mneme-entrailhead{flex:none;padding:0 12px 10px;display:flex;flex-direction:column;gap:4px;border-bottom:1px solid var(--dsw-alias-border-l1)}",
1808
+ ".mneme-entrailbody{flex:1;min-height:0;overflow-y:auto;overflow-x:hidden;padding:4px 12px 12px;display:flex;flex-direction:column;gap:4px}",
1809
+ ".mneme-enttype{position:sticky;top:0;z-index:1;background:var(--dsw-alias-bg-layer-1);display:flex;align-items:center;gap:6px;width:100%;box-sizing:border-box;padding:8px 8px 2px;border:none;color:var(--mneme-fg-dim);cursor:pointer;font-family:inherit;font-size:11px;font-weight:600;line-height:16px;text-align:left;letter-spacing:.02em}",
1810
+ ".mneme-entitem{display:flex;align-items:center;gap:8px;width:100%;box-sizing:border-box;padding:5px 8px;border:none;border-radius:8px;background:none;color:var(--dsw-alias-label-secondary);cursor:pointer;font-family:inherit;font-size:13px;line-height:18px;text-align:left}",
1704
1811
  ".mneme-entitem:hover{background:var(--dsw-alias-interactive-bg-hover)}",
1705
1812
  ".mneme-entitem.mneme-active{background:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-primary)}",
1706
1813
  ".mneme-entname{min-width:0;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
@@ -1743,6 +1850,13 @@ window.__ModuleLoader__.load({
1743
1850
  ".mneme-set-del{flex:none;opacity:0;transition:opacity .12s;border:none;background:none;color:var(--mneme-fg-dim);cursor:pointer;font-size:14px;line-height:20px;padding:0 4px;border-radius:6px}",
1744
1851
  ".mneme-set-row:hover .mneme-set-del,.mneme-set-row:focus-within .mneme-set-del{opacity:1}",
1745
1852
  ".mneme-set-del:hover{color:var(--mneme-danger);background:var(--dsw-alias-interactive-bg-hover)}",
1853
+ // 人格卡删除 × 独立类:不复用 mneme-set-del——其 opacity:0 仅在
1854
+ // .mneme-set-row:hover 解除,卡标签不在该容器里 → × 永远透明(v1.2
1855
+ // 实测踩中)。#457 曾用内联 opacity:1 覆写,但悬停变红仍走
1856
+ // .mneme-set-del:hover,连带常点亮了规则区的真删除按钮,被 #459 回滚。
1857
+ // 独立类常显,只沿用配色,规则区行为分毫不动。
1858
+ ".mneme-persona-del{flex:none;opacity:1;border:none;background:none;color:var(--mneme-fg-dim);cursor:pointer;font-size:16px;line-height:18px;padding:0 3px;border-radius:6px}",
1859
+ ".mneme-persona-del:hover{color:var(--mneme-danger);background:var(--dsw-alias-interactive-bg-hover)}",
1746
1860
  ".mneme-set-cmd{flex:1;min-width:0}",
1747
1861
  ".mneme-set-cmdname{font-size:13px;font-weight:600;color:var(--mneme-accent)}",
1748
1862
  ".mneme-set-cmddesc{font-size:12px;line-height:17px;color:var(--mneme-fg-dim);margin-top:1px;word-break:break-word}",
@@ -1880,6 +1994,13 @@ window.__ModuleLoader__.load({
1880
1994
  ".mneme-conflict-primary:disabled{opacity:.55;cursor:default}",
1881
1995
  ".mneme-conflict-primary:hover:not(:disabled){background:color-mix(in srgb,var(--mneme-accent) 85%,var(--dsw-alias-label-primary))}",
1882
1996
  ".mneme-badge--frozen{color:var(--mneme-warn);background:color-mix(in srgb,var(--mneme-warn) 12%,transparent)}",
1997
+ // 「待重启」徽标:沿用 frozen 的 warn 语义(都是「现状不是你刚设的样子」),
1998
+ // 但加边框把它和只读冻结态区分开——它需要用户动手,不是终态。
1999
+ ".mneme-badge--pending{color:var(--mneme-warn);background:color-mix(in srgb,var(--mneme-warn) 12%,transparent);border:1px solid color-mix(in srgb,var(--mneme-warn) 30%,transparent)}",
2000
+ ".mneme-pending{display:flex;align-items:flex-start;gap:8px;margin:8px 0 0;padding:8px 10px;border-radius:8px;font-size:12.5px;line-height:18px;color:var(--mneme-warn);background:color-mix(in srgb,var(--mneme-warn) 10%,transparent);border:1px solid color-mix(in srgb,var(--mneme-warn) 28%,transparent)}",
2001
+ // 圆点用当前色(currentColor)而不是再引一次 --mneme-warn:色值只有一个真源,
2002
+ // 改主题时提示行与圆点不会走散。
2003
+ ".mneme-pending-dot{flex:none;width:6px;height:6px;margin-top:6px;border-radius:50%;background:currentColor}",
1883
2004
  ".mneme-conflict-empty{border:1px dashed var(--dsw-alias-border-l2);border-radius:10px;padding:12px 14px;display:flex;flex-direction:column;gap:4px}",
1884
2005
  ".mneme-conflict-empty-title{font-size:12.5px;font-weight:600;color:var(--dsw-alias-label-secondary)}",
1885
2006
  ".mneme-conflict-empty-body{font-size:12px;line-height:18px;color:var(--mneme-fg-dim)}",
@@ -2005,8 +2126,14 @@ window.__ModuleLoader__.load({
2005
2126
  ".mneme-barcol:hover .mneme-bar{opacity:.8}",
2006
2127
  ".mneme-barlabels{display:flex;gap:8px;padding:6px 2px 0}",
2007
2128
  ".mneme-barlabels span{flex:1;min-width:0;text-align:center;font-size:10.5px;color:var(--mneme-fg-dim);font-variant-numeric:tabular-nums}",
2008
- ".mneme-heatgrid{display:grid;grid-auto-flow:column;grid-template-rows:repeat(7,12px);gap:3px;margin-top:12px}",
2009
- ".mneme-heatcell{width:12px;height:12px;border-radius:3px;background:var(--dsw-alias-interactive-bg-hover)}",
2129
+ // GitHub 贡献图式紧凑网格(复验第三轮「间距太大」):11px 格 + 2px 缝;
2130
+ // heatrows 是左侧星期标签列(一/三/五),heatmonths 是顶部月份标签行
2131
+ // (列主序网格下月份行须与格子列对齐,所以同用 auto-flow:column)。
2132
+ ".mneme-heatgrid{display:grid;grid-auto-flow:column;grid-template-rows:repeat(7,11px);gap:2px;margin-top:12px}",
2133
+ ".mneme-heatcell{width:11px;height:11px;border-radius:2px;background:var(--dsw-alias-interactive-bg-hover)}",
2134
+ ".mneme-heatwrap{display:flex;gap:4px}",
2135
+ ".mneme-heatrows{display:grid;grid-template-rows:repeat(7,11px);gap:2px;margin-top:12px;font-size:9.5px;line-height:11px;color:var(--mneme-fg-dim);text-align:right}",
2136
+ ".mneme-heatmonths{display:grid;grid-auto-flow:column;grid-auto-columns:11px;gap:2px;margin-top:0;font-size:9.5px;line-height:14px;color:var(--mneme-fg-dim)}",
2010
2137
  // 类名编号=视觉档位(l1 最浅…l4 最深),变量编号=business 占比降序,故交叉引用。
2011
2138
  ".mneme-heatcell.l1{background:var(--mneme-heat-4)}",
2012
2139
  ".mneme-heatcell.l2{background:var(--mneme-heat-3)}",
@@ -2563,31 +2690,39 @@ window.__ModuleLoader__.load({
2563
2690
  const graphStatus = status === "ready" && data && data.nodes.length <= 1 ? "empty" : status;
2564
2691
 
2565
2692
  const rail = h("div", { className: "mneme-entrail" },
2566
- h("div", { className: "mneme-xcolhead" }, t("memory.view.label")),
2567
- h("input", {
2568
- className: "mneme-search mneme-xsearch",
2569
- placeholder: t("memory.graph.placeholder"),
2570
- value: railQuery,
2571
- onChange: (e) => setRailQuery(e.target.value)
2572
- }),
2573
- entities === null && h("div", { className: "mneme-xempty" }, "…"),
2574
- entities !== null && entities.length === 0 && h("div", { className: "mneme-xempty" }, t("memory.entities.none")),
2575
- entities !== null && entities.length > 0 && h("div", { className: "mneme-xcount" },
2576
- t("memory.entities.count").replace("{n}", String(entities.length))),
2577
- groups.map((g) => h("div", { key: g.key },
2578
- h("div", { className: "mneme-enttype" },
2579
- h("span", { className: "mneme-xdot", style: { color: typeColor(g.key === "other" ? null : g.key) }, "aria-hidden": "true" }),
2580
- entityTypeLabel(t, g.key)
2581
- ),
2582
- g.items.map((e) => h("button", {
2583
- key: e.id,
2584
- className: e.name === entityName ? "mneme-entitem mneme-active" : "mneme-entitem",
2585
- onClick: () => setEntityName(e.name)
2586
- },
2587
- h("span", { className: "mneme-entname" }, e.name),
2588
- h("span", { className: "mneme-entmentions" }, `×${e.mention_count ?? 1}`)
2693
+ // 头部固定段:表头(本页名「实体」——原误用全局页签名「记忆库」,而
2694
+ // memory.view.label 仍被顶部 nav aria-label 使用,字典不动只换这里)
2695
+ // + 搜索 + 计数,不随列表滚动。
2696
+ h("div", { className: "mneme-entrailhead" },
2697
+ h("div", { className: "mneme-xcolhead" }, t("memory.explorer.tabEntities")),
2698
+ h("input", {
2699
+ className: "mneme-search mneme-xsearch",
2700
+ placeholder: t("memory.graph.placeholder"),
2701
+ value: railQuery,
2702
+ onChange: (e) => setRailQuery(e.target.value)
2703
+ }),
2704
+ entities !== null && entities.length > 0 && h("div", { className: "mneme-xcount" },
2705
+ t("memory.entities.count").replace("{n}", String(entities.length)))
2706
+ ),
2707
+ h("div", { className: "mneme-entrailbody" },
2708
+ entities === null && h("div", { className: "mneme-xempty" }, "…"),
2709
+ entities !== null && entities.length === 0 && h("div", { className: "mneme-xempty" }, t("memory.entities.none")),
2710
+ entities !== null && entities.length > 0 && visibleEntities.length === 0 && h("div", { className: "mneme-xempty" }, t("memory.entities.noMatch")),
2711
+ groups.map((g) => h("div", { key: g.key },
2712
+ h("div", { className: "mneme-enttype" },
2713
+ h("span", { className: "mneme-xdot", style: { color: typeColor(g.key === "other" ? null : g.key) }, "aria-hidden": "true" }),
2714
+ entityTypeLabel(t, g.key)
2715
+ ),
2716
+ g.items.map((e) => h("button", {
2717
+ key: e.id,
2718
+ className: e.name === entityName ? "mneme-entitem mneme-active" : "mneme-entitem",
2719
+ onClick: () => setEntityName(e.name)
2720
+ },
2721
+ h("span", { className: "mneme-entname" }, e.name),
2722
+ h("span", { className: "mneme-entmentions" }, `×${e.mention_count ?? 1}`)
2723
+ ))
2589
2724
  ))
2590
- ))
2725
+ )
2591
2726
  );
2592
2727
 
2593
2728
  // side card for a clicked (non-root) node or edge inside the graph layer
@@ -2603,14 +2738,7 @@ window.__ModuleLoader__.load({
2603
2738
  selected.node.name !== entityName && h("button", {
2604
2739
  className: "mneme-footbtn",
2605
2740
  onClick: () => setEntityName(selected.node.name)
2606
- }, t("memory.graph.viewInGraph")),
2607
- attrs.length > 0 && h("div", null,
2608
- h("div", { className: "mneme-gs-meta" }, t("memory.graph.attrs")),
2609
- attrs.map((a, i) => h("div", { key: i, className: "mneme-gs-attr" },
2610
- h("span", { className: "mneme-gs-attrkey" }, `${a.key}:`),
2611
- h("span", { className: "mneme-gs-attrval" }, a.value)
2612
- ))
2613
- )
2741
+ }, t("memory.graph.viewInGraph"))
2614
2742
  )
2615
2743
  : selected?.kind === "edge"
2616
2744
  ? h("div", { className: "mneme-graphside" },
@@ -2644,46 +2772,104 @@ window.__ModuleLoader__.load({
2644
2772
  h("span", { title: formatDate(entity.last_seen) },
2645
2773
  `${t("memory.entities.lastSeen")}: ${formatRelativeTime(entity.last_seen, t)}`)
2646
2774
  ),
2647
- // ① 属性 — current snapshot (valid_until IS NULL)
2775
+ // ① 关系图谱(前置):图形在属性/列表之前,是实体页的第一眼;
2776
+ // 深度/复位 chips 从层头移进图谱工具行(动作靠近作用对象),操作
2777
+ // 提示收进行尾小字不再独占一行;关系列表(跳来源记忆的唯一入口)
2778
+ // 保留为图谱的文本镜像、与图谱同层共享层头,消除信息双轨的分段感。
2648
2779
  h("div", { className: "mneme-layer" },
2649
2780
  h("div", { className: "mneme-layerhead" },
2650
- t("memory.entities.attrs"),
2651
- h("span", { className: "mneme-layercount" }, String(attrs.length))
2781
+ t("memory.entities.relations"),
2782
+ h("span", { className: "mneme-layercount" }, String(rootEdges.length))
2652
2783
  ),
2653
- attrs.length === 0
2654
- ? h("div", { className: "mneme-xempty" }, t("memory.settings.empty"))
2655
- : h("div", { className: "mneme-attrgrid" },
2656
- attrs.map((a, i) => h("div", { key: i, className: "mneme-attrcard", title: a.value },
2657
- h("div", { className: "mneme-attrkey" }, a.key),
2658
- h("div", { className: "mneme-attrval" }, a.value)
2659
- ))
2784
+ rootEdges.length === 0 && graphStatus === "empty"
2785
+ ? h("div", { className: "mneme-hint" }, t("memory.graph.empty"))
2786
+ : h(react.Fragment, null,
2787
+ graphStatus !== "empty" && h("div", { className: "mneme-graphbar" },
2788
+ h("button", {
2789
+ className: depth === 2 ? "mneme-chip mneme-active" : "mneme-chip",
2790
+ title: t("memory.graph.depth"),
2791
+ onClick: () => setDepth(depth === 1 ? 2 : 1)
2792
+ }, `${depth} ${t("memory.graph.depth")}`),
2793
+ h("button", {
2794
+ className: "mneme-chip",
2795
+ title: t("memory.graph.resetView"),
2796
+ onClick: resetView
2797
+ }, t("memory.graph.resetView")),
2798
+ h("span", { style: { marginLeft: "auto", fontSize: 12, color: "var(--mneme-fg-dim)" } }, t("memory.graph.hint"))
2799
+ ),
2800
+ graphStatus !== "empty" && h("div", { className: "mneme-entgraphwrap" },
2801
+ h("svg", {
2802
+ ref: svgRef,
2803
+ className: "mneme-graphsvg",
2804
+ viewBox: `0 0 ${VIEW_W} ${VIEW_H}`,
2805
+ style: { height: 440 },
2806
+ role: "img",
2807
+ "aria-label": t("memory.graph.summary")
2808
+ .replace("{entity}", (data.root && (data.root.label || data.root.name)) || "")
2809
+ .replace("{nodes}", String(data.nodes.length))
2810
+ .replace("{edges}", String(data.edges.length)),
2811
+ onMouseDown: onSurfaceMouseDown
2812
+ },
2813
+ h("g", {
2814
+ transform: `translate(${viewRef.current.x},${viewRef.current.y}) scale(${viewRef.current.k})`
2815
+ },
2816
+ data.edges.map((e) => {
2817
+ const a = nodeById.get(e.from), b = nodeById.get(e.to);
2818
+ if (!a || !b) return null;
2819
+ return h("line", {
2820
+ key: e.id,
2821
+ x1: a.x, y1: a.y, x2: b.x, y2: b.y,
2822
+ className: e.memory_id ? "mneme-gedge" : "mneme-gedge mneme-gedge-dashed",
2823
+ onClick: () => onEdgeClick(e)
2824
+ });
2825
+ }),
2826
+ nodes.map((n) => h("g", {
2827
+ key: n.id,
2828
+ className: n.id === data.root.id ? "mneme-gnode mneme-groot" : "mneme-gnode",
2829
+ transform: `translate(${n.x},${n.y})`,
2830
+ onMouseDown: (e) => onNodeMouseDown(e, n),
2831
+ onClick: () => onNodeClick(n),
2832
+ "data-node": n.name
2833
+ },
2834
+ h("circle", { r: nodeRadius(n), fill: typeColor(n.type), fillOpacity: nodeOpacity(n) }),
2835
+ h("text", { className: "mneme-glabel", y: nodeRadius(n) + 13 }, n.name)
2836
+ ))
2837
+ )
2838
+ ),
2839
+ side
2840
+ ),
2841
+ rootEdges.length > 0 && rootEdges.map((e) => {
2842
+ const out = e.from === data.root.id;
2843
+ const other = nodeById.get(out ? e.to : e.from);
2844
+ const otherName = other?.name ?? (out ? e.to : e.from);
2845
+ return h("button", {
2846
+ key: e.id,
2847
+ className: "mneme-relrow",
2848
+ title: e.memory_id ? t("memory.graph.sourceMemory") : undefined,
2849
+ onClick: () => { if (e.memory_id && onJumpMemory) onJumpMemory({ id: e.memory_id }); }
2850
+ },
2851
+ h("span", { className: "mneme-relname", style: { textAlign: "right", flex: 1 } }, entity.name),
2852
+ h("span", { className: "mneme-reltype" }, e.relation_type),
2853
+ h("span", { className: "mneme-relname", style: { flex: 1 } }, otherName)
2854
+ );
2855
+ })
2660
2856
  )
2661
2857
  ),
2662
- // ② 关系 — both directions from the 1-hop ego walk
2663
- h("div", { className: "mneme-layer" },
2858
+ // ② 属性 — current snapshot (valid_until IS NULL);空层整层隐藏
2859
+ // (原先渲染一行「暂无内容」占住高度,是零信息的堆积)
2860
+ attrs.length > 0 && h("div", { className: "mneme-layer" },
2664
2861
  h("div", { className: "mneme-layerhead" },
2665
- t("memory.entities.relations"),
2666
- h("span", { className: "mneme-layercount" }, String(rootEdges.length))
2862
+ t("memory.entities.attrs"),
2863
+ h("span", { className: "mneme-layercount" }, String(attrs.length))
2667
2864
  ),
2668
- rootEdges.length === 0
2669
- ? h("div", { className: "mneme-xempty" }, t("memory.settings.empty"))
2670
- : rootEdges.map((e) => {
2671
- const out = e.from === data.root.id;
2672
- const other = nodeById.get(out ? e.to : e.from);
2673
- const otherName = other?.name ?? (out ? e.to : e.from);
2674
- return h("button", {
2675
- key: e.id,
2676
- className: "mneme-relrow",
2677
- title: e.memory_id ? t("memory.graph.sourceMemory") : undefined,
2678
- onClick: () => { if (e.memory_id && onJumpMemory) onJumpMemory({ id: e.memory_id }); }
2679
- },
2680
- h("span", { className: "mneme-relname", style: { textAlign: "right", flex: 1 } }, entity.name),
2681
- h("span", { className: "mneme-reltype" }, e.relation_type),
2682
- h("span", { className: "mneme-relname", style: { flex: 1 } }, otherName)
2683
- );
2684
- })
2865
+ h("div", { className: "mneme-attrgrid" },
2866
+ attrs.map((a, i) => h("div", { key: i, className: "mneme-attrcard", title: a.value },
2867
+ h("div", { className: "mneme-attrkey" }, a.key),
2868
+ h("div", { className: "mneme-attrval" }, a.value)
2869
+ ))
2870
+ )
2685
2871
  ),
2686
- // ③ 关联记忆 — entity: prefix recall
2872
+ // ③ 关联记忆 — entity: prefix recall(entity: 之外的唯一记忆回链入口)
2687
2873
  related.length > 0 && h("div", { className: "mneme-layer" },
2688
2874
  h("div", { className: "mneme-layerhead" },
2689
2875
  t("memory.graph.related"),
@@ -2696,68 +2882,6 @@ window.__ModuleLoader__.load({
2696
2882
  title: t("memory.card.open"),
2697
2883
  onClick: () => onJumpMemory && onJumpMemory(m)
2698
2884
  }, m.title || m.content?.slice(0, 60)))
2699
- ),
2700
- // ④ 关系图谱 — the ego graph
2701
- h("div", { className: "mneme-layer" },
2702
- h("div", { className: "mneme-layerhead" },
2703
- t("memory.entities.graph"),
2704
- h("span", { className: "mneme-layercount" },
2705
- h("button", {
2706
- className: depth === 2 ? "mneme-chip mneme-active" : "mneme-chip",
2707
- title: t("memory.graph.depth"),
2708
- onClick: () => setDepth(depth === 1 ? 2 : 1)
2709
- }, `${depth} ${t("memory.graph.depth")}`)),
2710
- h("span", { className: "mneme-layercount" },
2711
- h("button", {
2712
- className: "mneme-chip",
2713
- title: t("memory.graph.resetView"),
2714
- onClick: resetView
2715
- }, t("memory.graph.resetView")))
2716
- ),
2717
- graphStatus === "empty"
2718
- ? h("div", { className: "mneme-hint" }, t("memory.graph.empty"))
2719
- : h(react.Fragment, null,
2720
- h("svg", {
2721
- ref: svgRef,
2722
- className: "mneme-graphsvg",
2723
- viewBox: `0 0 ${VIEW_W} ${VIEW_H}`,
2724
- style: { height: 320 },
2725
- role: "img",
2726
- "aria-label": t("memory.graph.summary")
2727
- .replace("{entity}", (data.root && (data.root.label || data.root.name)) || "")
2728
- .replace("{nodes}", String(data.nodes.length))
2729
- .replace("{edges}", String(data.edges.length)),
2730
- onMouseDown: onSurfaceMouseDown
2731
- },
2732
- h("g", {
2733
- transform: `translate(${viewRef.current.x},${viewRef.current.y}) scale(${viewRef.current.k})`
2734
- },
2735
- data.edges.map((e) => {
2736
- const a = nodeById.get(e.from), b = nodeById.get(e.to);
2737
- if (!a || !b) return null;
2738
- return h("line", {
2739
- key: e.id,
2740
- x1: a.x, y1: a.y, x2: b.x, y2: b.y,
2741
- className: e.memory_id ? "mneme-gedge" : "mneme-gedge mneme-gedge-dashed",
2742
- onClick: () => onEdgeClick(e)
2743
- });
2744
- }),
2745
- nodes.map((n) => h("g", {
2746
- key: n.id,
2747
- className: n.id === data.root.id ? "mneme-gnode mneme-groot" : "mneme-gnode",
2748
- transform: `translate(${n.x},${n.y})`,
2749
- onMouseDown: (e) => onNodeMouseDown(e, n),
2750
- onClick: () => onNodeClick(n),
2751
- "data-node": n.name
2752
- },
2753
- h("circle", { r: nodeRadius(n), fill: typeColor(n.type), fillOpacity: nodeOpacity(n) }),
2754
- h("text", { className: "mneme-glabel", y: nodeRadius(n) + 13 }, n.name)
2755
- ))
2756
- )
2757
- ),
2758
- h("div", { className: "mneme-graphhint" }, t("memory.graph.hint"))
2759
- ),
2760
- side
2761
2885
  )
2762
2886
  )
2763
2887
  )
@@ -2783,19 +2907,27 @@ window.__ModuleLoader__.load({
2783
2907
  // 用户敢动的总开关;group.scope 也在首屏——它回答新用户的第二个必答题
2784
2908
  // 「它记的东西会不会被别的会话看到」。其余组整组收进「全部设置」折叠
2785
2909
  // (showAll)。成员从原组里移走而不是渲染时过滤:同一项永远只渲染一次。
2786
- // 首屏可点项 = 3 总开关 + autoInject 的 2 个子开关 + scope 2 开关 +
2787
- // 「全部设置」按钮 = 8(验收线 ≤ 8;RuntimeProvisionBlock 配好即消失,
2788
- // 常态首屏更少)。
2789
- const FEATURE_FIRST_SCREEN = ["group.basic", "group.scope"];
2910
+ // 首屏可点项 = 3 总开关 + autoInject 的 2 个子开关 + M.O. 1 开关 +
2911
+ // scope 2 开关 + 「全部设置」按钮 = 9。复验修订:产品主人点名 M.O.
2912
+ // 「别藏在里面」,独立成组但仍折在「全部设置」后等于换个地方藏;
2913
+ // ≤8 是本仓库 #427 自设的软预算,owner 点名豁免一项,锁随注释同步改。
2914
+ // RuntimeProvisionBlock 配好即消失,常态首屏更少。
2915
+ const FEATURE_FIRST_SCREEN = ["group.basic", "group.mo", "group.scope"];
2790
2916
  const FEATURE_GROUPS = [
2791
2917
  { key: "group.basic", items: ["autoInject", "autoSummarize", "autoDream"] },
2792
2918
  { key: "group.core", items: ["hotMemoryEnabled", "injectTimePrefix", "memoryQualityFilter.enabled", "llmAudit.enabled", "writeAdmission.enabled", "writeAdmission.enforce", "preInjectGate.enabled", "preInjectGate.enforce", "sensitiveScanEnabled",
2793
2919
  // #399 的留存总闸(见下方 distillSub:天数行是它的子块)。放在组末,
2794
2920
  // 子块才能紧跟在它后面展开——items.map 之后的子块是按组尾渲染的。
2795
2921
  "distillRecords.enabled"] },
2796
- // v0.9.0 M.O.(beta):开关挂在 enhance 组。它不是行为开关而是
2797
- // 座位闸门——关=一个座位都不注册(见 apply 内的 M.O. 块)。
2798
- { key: "group.enhance", items: ["entityExtractionEnabled", "codingRetrospect", "rerankEnabled", "resilientModelDownload", "searchSemanticDedup", "bm25SearchEnabled", "heatEnabled", "documentMemoryEnabled", "actionPipelineEnabled"] },
2922
+ // v0.9.0 M.O.(beta)复验修订:独立分组行——它不是行为开关而是座位闸门
2923
+ // (关=一个座位都不注册,见 apply 内的 M.O. 块)。独立成组只解决「混在
2924
+ // enhance 末位」;可见层由 FEATURE_FIRST_SCREEN 决定(见其注释),
2925
+ // 本轮已把组名提进首屏——组成员资格不决定可见性。
2926
+ { key: "group.mo", items: ["actionPipelineEnabled"] },
2927
+ { key: "group.enhance", items: ["entityExtractionEnabled", "codingRetrospect", "rerankEnabled", "resilientModelDownload", "searchSemanticDedup", "bm25SearchEnabled", "heatEnabled", "documentMemoryEnabled",
2928
+ // 人格定义页(beta):注入总闸。分组随 enhance(行为开关,非座位闸门);
2929
+ // 人格内容在设置页「个性化」区的人格卡。
2930
+ "personaEnabled"] },
2799
2931
  { key: "group.dream", items: ["sleepModeEnabled"] },
2800
2932
  // v0.8.0 A4(issue #17):作用域隔离组——标注总开关 + 严格硬过滤。
2801
2933
  { key: "group.scope", items: ["scopeEnabled", "strictScope"] }
@@ -2812,7 +2944,7 @@ window.__ModuleLoader__.load({
2812
2944
  // 后端白名单、面板够不着——而 CONFIGURATION.md 明写「面板可启停」。
2813
2945
  // 其余整数键(429 调速器参数、distillMaxChars、codingBoostFactor)是有意
2814
2946
  // 不上 UI 的调优噪音,见上方注释,别顺手搬进来。
2815
- const FEATURE_NUMBERS = ["distillRecords.retentionDays"];
2947
+ const FEATURE_NUMBERS = ["distillRecords.retentionDays", "dreamMaxTokens", "sleepMaxTokens"];
2816
2948
  const EMBED_PROVIDERS = ["openai", "local", "ollama"];
2817
2949
  // 实体抽取思考强度(issue #109):与后端 FEATURE_FLAG_ENUMS 枚举对齐。
2818
2950
  const ENTITY_REASONING = ["none", "low", "medium", "high"];
@@ -2965,11 +3097,25 @@ window.__ModuleLoader__.load({
2965
3097
  // = 值仍来自 config schema 的默认值。面板自己判断,不加后端字段、
2966
3098
  // 不动 config.js(#427 的验收红线之一)。
2967
3099
  const ovr = (state && state.overrides) || {};
2968
- const flagTag = (k) => ({
2969
- kind: Object.prototype.hasOwnProperty.call(ovr, k) ? "mneme-badge--changed" : "mneme-badge--default",
2970
- text: t(Object.prototype.hasOwnProperty.call(ovr, k) ? "memory.features.tagChanged" : "memory.features.tagDefault"),
2971
- title: t(Object.prototype.hasOwnProperty.call(ovr, k) ? "memory.features.tagChanged.hint" : "memory.features.tagDefault.hint")
2972
- });
3100
+ // 「待重启」是第三态,必须由后端算:#481 的坑正是「面板显示已改 ≠ 运行时
3101
+ // 已改」——开关在宿主启动时合并进 cfg 并冻结,此后改 kv 只是持久化。这个
3102
+ // 落差前端看不见(overrides 里只有「改了」这一个事实),所以后端另比一次
3103
+ // 冻结值并给出 pendingRestart 名单;本面板只负责把它显示出来。
3104
+ const pending = new Set((state && state.pendingRestart) || []);
3105
+ const flagTag = (k) => {
3106
+ if (pending.has(k)) {
3107
+ return {
3108
+ kind: "mneme-badge--pending",
3109
+ text: t("memory.features.tagPending"),
3110
+ title: t("memory.features.tagPending.hint")
3111
+ };
3112
+ }
3113
+ return {
3114
+ kind: Object.prototype.hasOwnProperty.call(ovr, k) ? "mneme-badge--changed" : "mneme-badge--default",
3115
+ text: t(Object.prototype.hasOwnProperty.call(ovr, k) ? "memory.features.tagChanged" : "memory.features.tagDefault"),
3116
+ title: t(Object.prototype.hasOwnProperty.call(ovr, k) ? "memory.features.tagChanged.hint" : "memory.features.tagDefault.hint")
3117
+ };
3118
+ };
2973
3119
  const boolRow = (k, opts = {}) => h(FeatureRow, {
2974
3120
  key: k,
2975
3121
  name: t(`memory.features.${k}`),
@@ -3158,14 +3304,19 @@ window.__ModuleLoader__.load({
3158
3304
  // Issue #239 第 4 项镜像到巩固:高峰时段串(空 = 关闭)。放在 autoDream
3159
3305
  // 子块内——它是巩固的排程,开关关掉时不该还在界面上留着可编辑的输入框。
3160
3306
  strRow("dreamPeakHours"),
3161
- h("div", { className: "mneme-featsubhint" }, t("memory.features.dreamPeakHours.hint"))
3307
+ h("div", { className: "mneme-featsubhint" }, t("memory.features.dreamPeakHours.hint")),
3308
+ // Issue #478:单轮巩固的 max_tokens 预算。后端白名单(256–131072)早就有,
3309
+ // 修复前 UI 够不着;默认 131072 超过很多 OpenAI 兼容网关的硬上限又看不见调不了。
3310
+ numRow("dreamMaxTokens")
3162
3311
  );
3163
3312
 
3164
3313
  // 睡眠模型:sleepModeEnabled 开着才展开(sleepProvider/sleepModel 随本版
3165
3314
  // 进白名单;下拉与测试复用同一 /llm-providers 数据源)。
3166
3315
  const sleepSub = eff.sleepModeEnabled && Array.isArray(routes) && h("div", { className: "mneme-featsub" },
3167
3316
  routeSelects("sleepProvider", "sleepModel", sleepTest, setSleepTest, "sleepReasoningEffort"),
3168
- h("div", { className: "mneme-featsubhint" }, t("memory.features.sleepModelHint"))
3317
+ h("div", { className: "mneme-featsubhint" }, t("memory.features.sleepModelHint")),
3318
+ // Issue #478:睡眠侧同款预算键(#257 引入后端白名单,UI 一直缺席)。
3319
+ numRow("sleepMaxTokens")
3169
3320
  );
3170
3321
 
3171
3322
  // 实体抽取路由与思考强度(issue #109):entityExtractionEnabled 开着才
@@ -3227,6 +3378,14 @@ window.__ModuleLoader__.load({
3227
3378
  savedTick && h("span", { className: "mneme-saved", style: { marginLeft: 8, fontWeight: 400 } }, t("memory.features.restartHint"))
3228
3379
  ),
3229
3380
  h("div", { className: "mneme-set-desc" }, t("memory.features.desc")),
3381
+ // #481:改了但没生效的开关,在这张卡里明说。放在 desc 之后、错误行之前
3382
+ // ——它是常态提示而非错误,但比错误更该先被看见(用户刚改完就会看这里)。
3383
+ // 行级 badge(flagTag 的第三态)标出具体是哪几行,这条只负责说清「有事
3384
+ // 没生效」和「怎么办」。
3385
+ pending.size > 0 && h("div", { className: "mneme-pending" },
3386
+ h("span", { className: "mneme-pending-dot" }),
3387
+ h("span", null, t("memory.features.pendingRestartNotice"))
3388
+ ),
3230
3389
  // 见 saveError 的声明处:这条分支不能跟着加载失败那条走。
3231
3390
  saveError && h("div", { className: "mneme-mainterr" }, saveError),
3232
3391
  h(RuntimeProvisionBlock, { t }),
@@ -3310,6 +3469,14 @@ window.__ModuleLoader__.load({
3310
3469
  const [newCmd, setNewCmd] = react.useState({ name: "", description: "", instruction: "" });
3311
3470
  const [saved, setSaved] = react.useState(false);
3312
3471
  const [cmdError, setCmdError] = react.useState("");
3472
+ // 人格卡(beta,v1.2 多卡):default 卡草稿 + byAgent 卡列表 + 当前编辑位;
3473
+ // provenance 是 v1.5 协作式人格的预留位,v1 只透传不编辑。
3474
+ const [personaDefault, setPersonaDefault] = react.useState({ name: "", personality: "", replyStyle: "", enabled: false });
3475
+ const [personaByAgent, setPersonaByAgent] = react.useState({});
3476
+ const [personaEditing, setPersonaEditing] = react.useState("default");
3477
+ const [personaNewName, setPersonaNewName] = react.useState("");
3478
+ const [personaSaved, setPersonaSaved] = react.useState(false);
3479
+ const [personaError, setPersonaError] = react.useState("");
3313
3480
  const [vector, setVector] = react.useState({ enabled: false, baseUrl: "", apiKey: "", model: "" });
3314
3481
  const [vectorSaved, setVectorSaved] = react.useState(false);
3315
3482
  const [reindexing, setReindexing] = react.useState(false);
@@ -3320,9 +3487,39 @@ window.__ModuleLoader__.load({
3320
3487
  const [apiTokenSaved, setApiTokenSaved] = react.useState(false);
3321
3488
  // 反馈入口的插件版本(GET /info)。失败保持 "unknown",链接仍可用。
3322
3489
  const [pkgVersion, setPkgVersion] = react.useState("unknown");
3323
- // 版本自检(#174 后续):运行版本 vs registry latest。仅 outdated 渲染
3324
- // 横幅,其余状态(up-to-date / ahead / unknown / 查询失败)零渲染零打扰。
3490
+ // 版本自检(复验修订:原为进入设置页自动请求 + 顶部横幅,现改为底部
3491
+ // 「支持与鼓励」卡里的「检测更新」按钮手动触发,结果内联显示——自动
3492
+ // 请求与横幅常驻都是打扰面)。status 共四种:outdated / latest / error / null(未检测)。
3325
3493
  const [updateInfo, setUpdateInfo] = react.useState(null);
3494
+ const [updateChecking, setUpdateChecking] = react.useState(false);
3495
+ function checkForUpdate() {
3496
+ if (updateChecking) return;
3497
+ setUpdateChecking(true);
3498
+ apiFetch("/api/dsh-mneme/version-check")
3499
+ .then((res) => (res.ok ? res.json() : null))
3500
+ .then((j) => {
3501
+ setUpdateInfo(j && typeof j.latest === "string" && typeof j.version === "string"
3502
+ ? { status: j.status, latest: j.latest, current: j.version }
3503
+ : { status: "error" });
3504
+ setUpdateChecking(false);
3505
+ })
3506
+ .catch(() => { setUpdateInfo({ status: "error" }); setUpdateChecking(false); });
3507
+ }
3508
+ // 检测结果一行文案:按 status 取既有或新增的 i18n key。
3509
+ function checkResultText() {
3510
+ if (!updateInfo) return "";
3511
+ if (updateInfo.status === "outdated") {
3512
+ return t("memory.settings.version.outdated")
3513
+ .replace("{v}", updateInfo.latest)
3514
+ .replace("{c}", updateInfo.current);
3515
+ }
3516
+ // 后端 classify(src/version-check.js)返回的是 "up-to-date"(连字符)。
3517
+ // 前端状态名必须与它逐字对齐:此前误写 "latest"/"up_to_date" 两个后端
3518
+ // 从未返回过的名字,检测成功也掉进 error 兜底(复验实测踩中)。
3519
+ if (updateInfo.status === "up-to-date") return t("memory.settings.update.latest");
3520
+ if (updateInfo.status === "ahead") return t("memory.settings.update.ahead");
3521
+ return t("memory.settings.update.error");
3522
+ }
3326
3523
  // 运行模式 — light vs standard; null = still loading. The card keeps
3327
3524
  // its own busy/saved/error state so it never blocks the others.
3328
3525
  const [mode, setMode] = react.useState(null);
@@ -3343,16 +3540,33 @@ window.__ModuleLoader__.load({
3343
3540
 
3344
3541
  const load = react.useCallback(async () => {
3345
3542
  try {
3346
- const [p, r, c, v] = await Promise.all([
3543
+ const [p, r, c, v, ps] = await Promise.all([
3347
3544
  apiFetch("/api/dsh-mneme/profile").then((res) => res.json()),
3348
3545
  apiFetch("/api/dsh-mneme/rules").then((res) => res.json()),
3349
3546
  apiFetch("/api/dsh-mneme/commands").then((res) => res.json()),
3350
- apiFetch("/api/dsh-mneme/vector-config").then((res) => res.json())
3547
+ apiFetch("/api/dsh-mneme/vector-config").then((res) => res.json()),
3548
+ apiFetch("/api/dsh-mneme/persona").then((res) => res.json()).catch(() => null)
3351
3549
  ]);
3352
3550
  setProfile(p.profile || "");
3353
3551
  setRules(Array.isArray(r.rules) ? r.rules : []);
3354
3552
  setCommands(Array.isArray(c.commands) ? c.commands : []);
3355
3553
  setVector(v.config || { enabled: false, baseUrl: "", apiKey: "", model: "" });
3554
+ // 人格卡:旧后端无 /persona(404)时保持默认空卡,不阻塞其它卡片。
3555
+ // v1.2 多卡形 {default, byAgent};旧单对象形读作 default(后端已兼容,
3556
+ // 这里双保险再判一次形状)。
3557
+ const pj = ps && ps.persona && typeof ps.persona === "object" ? ps.persona : {};
3558
+ const isMulti = "default" in pj || "byAgent" in pj;
3559
+ const dRaw = isMulti ? (pj.default ?? {}) : pj;
3560
+ const bRaw = isMulti && pj.byAgent && typeof pj.byAgent === "object" ? pj.byAgent : {};
3561
+ const norm = (c) => ({
3562
+ name: typeof c?.name === "string" ? c.name : "",
3563
+ personality: typeof c?.personality === "string" ? c.personality : "",
3564
+ replyStyle: typeof c?.replyStyle === "string" ? c.replyStyle : "",
3565
+ enabled: c?.enabled === true
3566
+ });
3567
+ setPersonaDefault(norm(dRaw));
3568
+ setPersonaByAgent(Object.fromEntries(Object.entries(bRaw).map(([k, c]) => [k, norm(c)])));
3569
+ setPersonaEditing("default");
3356
3570
  } catch { /* ignore */ }
3357
3571
  }, []);
3358
3572
 
@@ -3368,21 +3582,6 @@ window.__ModuleLoader__.load({
3368
3582
  return () => { cancelled = true; };
3369
3583
  }, []);
3370
3584
 
3371
- // 版本自检:只读 /version-check(宿主围栏鉴权)。非 outdated 一律归
3372
- // null——横幅不出现即是「无需处理」;接口失败也静默,不新增故障面。
3373
- react.useEffect(() => {
3374
- let cancelled = false;
3375
- apiFetch("/api/dsh-mneme/version-check")
3376
- .then((res) => (res.ok ? res.json() : null))
3377
- .then((j) => {
3378
- if (!cancelled && j && j.status === "outdated" && typeof j.latest === "string" && typeof j.version === "string") {
3379
- setUpdateInfo({ latest: j.latest, current: j.version });
3380
- }
3381
- })
3382
- .catch(() => {});
3383
- return () => { cancelled = true; };
3384
- }, []);
3385
-
3386
3585
  // 反馈链接预填:环境信息(插件版本 + 平台)+ 问题描述骨架。邮件主题带
3387
3586
  // 版本/平台方便归档,正文同款模板;纯链接零后端成本。
3388
3587
  const feedbackEnv = `**插件版本**: ${pkgVersion}\n**平台**: ${platformLabel()}\n`;
@@ -3443,6 +3642,65 @@ window.__ModuleLoader__.load({
3443
3642
  } catch { /* ignore */ }
3444
3643
  }
3445
3644
 
3645
+ // 人格卡保存(v1.2 多卡):全量多卡形 PUT({default, byAgent},后端全量
3646
+ // 替换语义)。保存失败必须冒提示——v1 的静默 catch 曾把 404 吞成「看起来
3647
+ // 保存了」(后端未重装配时 PUT 打 404),桉桉实测踩中,此处不再静默。
3648
+ async function savePersonaCards(nextDefault, nextByAgent) {
3649
+ try {
3650
+ const res = await apiFetch("/api/dsh-mneme/persona", {
3651
+ method: "PUT",
3652
+ headers: { "Content-Type": "application/json" },
3653
+ body: JSON.stringify({ persona: { default: nextDefault, byAgent: nextByAgent } })
3654
+ });
3655
+ if (!res.ok) throw new Error("HTTP " + res.status);
3656
+ setPersonaSaved(true);
3657
+ setPersonaError("");
3658
+ announce(t("memory.settings.personaSaved"));
3659
+ setTimeout(() => setPersonaSaved(false), 1500);
3660
+ } catch (err) {
3661
+ setPersonaError((err && err.message) || "failed");
3662
+ setTimeout(() => setPersonaError(""), 4000);
3663
+ }
3664
+ }
3665
+ // 当前编辑位的卡:default 或 byAgent[editing];草稿即编辑位状态本身。
3666
+ const personaEditingCard = personaEditing === "default"
3667
+ ? personaDefault
3668
+ : personaByAgent[personaEditing] || { name: "", personality: "", replyStyle: "", enabled: false };
3669
+ const updatePersonaCard = (key, value) => {
3670
+ if (personaEditing === "default") setPersonaDefault((c) => ({ ...c, [key]: value }));
3671
+ else setPersonaByAgent((c) => ({ ...c, [personaEditing]: { ...(c[personaEditing] || { name: "", personality: "", replyStyle: "", enabled: false }), [key]: value } }));
3672
+ };
3673
+ const saveEditingPersona = () => savePersonaCards(personaDefault, personaByAgent);
3674
+ const toggleEditingPersona = (checked) => {
3675
+ if (personaEditing === "default") {
3676
+ const next = { ...personaDefault, enabled: checked };
3677
+ setPersonaDefault(next);
3678
+ savePersonaCards(next, personaByAgent);
3679
+ } else {
3680
+ const next = { ...(personaByAgent[personaEditing] || { name: "", personality: "", replyStyle: "", enabled: false }), enabled: checked };
3681
+ setPersonaByAgent((c) => ({ ...c, [personaEditing]: next }));
3682
+ savePersonaCards(personaDefault, { ...personaByAgent, [personaEditing]: next });
3683
+ }
3684
+ };
3685
+ const addPersonaCard = () => {
3686
+ const id = personaNewName.trim();
3687
+ if (!id || id === "default" || personaByAgent[id]) return;
3688
+ const fresh = { name: id, personality: "", replyStyle: "", enabled: false };
3689
+ const next = { ...personaByAgent, [id]: fresh };
3690
+ setPersonaByAgent(next);
3691
+ setPersonaNewName("");
3692
+ setPersonaEditing(id);
3693
+ savePersonaCards(personaDefault, next);
3694
+ };
3695
+ const removePersonaCard = (id) => {
3696
+ if (id === "default" || !personaByAgent[id]) return;
3697
+ const next = { ...personaByAgent };
3698
+ delete next[id];
3699
+ setPersonaByAgent(next);
3700
+ if (personaEditing === id) setPersonaEditing("default");
3701
+ savePersonaCards(personaDefault, next);
3702
+ };
3703
+
3446
3704
  async function putRules(next) {
3447
3705
  await apiFetch("/api/dsh-mneme/rules", {
3448
3706
  method: "PUT",
@@ -3624,18 +3882,7 @@ window.__ModuleLoader__.load({
3624
3882
  }
3625
3883
 
3626
3884
  return h("div", null,
3627
- // 版本自检横幅 — 仅 outdated 时渲染(up-to-date/ahead/unknown/失败
3628
- // 全部零渲染)。钉子警示:安装时指定过版本号的 profile 会被 pnpm
3629
- // 挡住常规升级(#174 报障者的实际成因);市场收录新发布约有 1 天延迟。
3630
- updateInfo && h("section", { className: "mneme-set-card" },
3631
- h("div", { className: "mneme-set-title" },
3632
- t("memory.settings.version.outdated")
3633
- .replace("{v}", updateInfo.latest)
3634
- .replace("{c}", updateInfo.current)
3635
- ),
3636
- h("div", { className: "mneme-featsubhint" }, t("memory.settings.version.outdatedHint"))
3637
- ),
3638
- // —— 个性化:画像 / 规则 / 自定义指令(Agent 怎么对你) ——
3885
+ // —— 个性化:画像 / 人格 / 规则 / 自定义指令(Agent 怎么对你) ——
3639
3886
  h("div", { className: "mneme-set-grouphead" }, t("memory.settings.group.personal")),
3640
3887
  // 用户画像 — who the agent is talking to
3641
3888
  h("section", { className: "mneme-set-card" },
@@ -3652,6 +3899,105 @@ window.__ModuleLoader__.load({
3652
3899
  saved && h("span", { className: "mneme-saved" }, t("memory.settings.profileSaved"))
3653
3900
  )
3654
3901
  ),
3902
+ // 人格卡(beta,v1.2 多卡)— who the agent is:多张角色卡按会话身份注入。
3903
+ // 卡内开关只是该卡 enabled 半边;总闸 personaEnabled 在功能开关组,双闸同真。
3904
+ h("section", { className: "mneme-set-card" },
3905
+ h("div", { className: "mneme-set-title" }, t("memory.settings.persona")),
3906
+ h("div", { className: "mneme-set-desc" }, t("memory.settings.personaHint")),
3907
+ // 卡列表:default + 各 byAgent 卡,点选切换编辑位;default 标注兜底位
3908
+ h("div", { style: { display: "flex", flexWrap: "wrap", gap: 6 } },
3909
+ h("button", {
3910
+ type: "button",
3911
+ className: "mneme-btn" + (personaEditing === "default" ? " mneme-btn--active" : ""),
3912
+ onClick: () => setPersonaEditing("default")
3913
+ }, t("memory.settings.personaDefaultCard")),
3914
+ Object.keys(personaByAgent).map((id) =>
3915
+ h("span", { key: id, style: { display: "inline-flex", alignItems: "center", gap: 2 } },
3916
+ h("button", {
3917
+ type: "button",
3918
+ className: "mneme-btn" + (personaEditing === id ? " mneme-btn--active" : ""),
3919
+ onClick: () => setPersonaEditing(id)
3920
+ }, id),
3921
+ // 卡删除 × 常显:用独立类 mneme-persona-del(见上方样式定义处的
3922
+ // 注释——为何不复用 mneme-set-del,以及 #457→#459 一修一回滚的教训)。
3923
+ h("button", {
3924
+ type: "button",
3925
+ className: "mneme-persona-del",
3926
+ title: t("memory.settings.personaDeleteCard"),
3927
+ "aria-label": t("memory.settings.personaDeleteCard") + " " + id,
3928
+ onClick: () => removePersonaCard(id)
3929
+ }, "×")
3930
+ ))
3931
+ ),
3932
+ // 添加卡:输入 agent 身份名(注入时按它寻址)
3933
+ h("div", { style: { display: "flex", gap: 8 } },
3934
+ h("input", {
3935
+ className: "mneme-set-input",
3936
+ style: { marginBottom: 0 },
3937
+ value: personaNewName,
3938
+ placeholder: t("memory.settings.personaAddPlaceholder"),
3939
+ onChange: (e) => setPersonaNewName(e.target.value),
3940
+ onKeyDown: (e) => { if (e.key === "Enter") addPersonaCard(); }
3941
+ }),
3942
+ h("button", { className: "mneme-btn", onClick: addPersonaCard }, t("memory.settings.personaAdd"))
3943
+ ),
3944
+ // 编辑位标题:default 卡 vs 具体身份卡
3945
+ h("div", { className: "mneme-set-desc" },
3946
+ personaEditing === "default"
3947
+ ? t("memory.settings.personaEditingDefault")
3948
+ : t("memory.settings.personaEditingFor") + " " + personaEditing),
3949
+ // 可见字段标签(v1.1 反馈:aria-label 看不见,框上要有字)
3950
+ h("div", { className: "mneme-featnumlabel" }, t("memory.settings.personaEnabledToggle")),
3951
+ h("div", { className: "mneme-set-row" },
3952
+ h("input", {
3953
+ type: "checkbox",
3954
+ checked: personaEditingCard.enabled === true,
3955
+ "aria-label": t("memory.settings.personaEnabledToggle"),
3956
+ onChange: (e) => toggleEditingPersona(e.target.checked)
3957
+ }),
3958
+ h("span", { className: "mneme-set-ruletext" },
3959
+ personaEditingCard.enabled === true
3960
+ ? t("memory.settings.personaEnabledOn")
3961
+ : t("memory.settings.personaEnabledOff"))
3962
+ ),
3963
+ h("div", { className: "mneme-featnumlabel" }, t("memory.settings.personaName")),
3964
+ h("input", {
3965
+ className: "mneme-set-input",
3966
+ value: personaEditingCard.name,
3967
+ placeholder: t("memory.settings.personaNamePlaceholder"),
3968
+ onChange: (e) => updatePersonaCard("name", e.target.value)
3969
+ }),
3970
+ h("div", { className: "mneme-featnumlabel" }, t("memory.settings.personaPersonality")),
3971
+ h("textarea", {
3972
+ className: "mneme-set-input mneme-set-input--area",
3973
+ value: personaEditingCard.personality,
3974
+ placeholder: t("memory.settings.personaPersonalityPlaceholder"),
3975
+ onChange: (e) => updatePersonaCard("personality", e.target.value)
3976
+ }),
3977
+ h("div", { className: "mneme-featnumlabel" }, t("memory.settings.personaReplyStyle")),
3978
+ h("textarea", {
3979
+ className: "mneme-set-input mneme-set-input--area",
3980
+ value: personaEditingCard.replyStyle,
3981
+ placeholder: t("memory.settings.personaReplyStylePlaceholder"),
3982
+ onChange: (e) => updatePersonaCard("replyStyle", e.target.value)
3983
+ }),
3984
+ h("div", { style: { display: "flex", alignItems: "center", gap: 8 } },
3985
+ h("button", { className: "mneme-btn", onClick: saveEditingPersona }, t("memory.settings.personaSave")),
3986
+ personaSaved && h("span", { className: "mneme-saved" }, t("memory.settings.personaSaved")),
3987
+ personaError && h("span", { className: "mneme-saved", style: { color: "var(--dsw-danger, #d33)" } },
3988
+ t("memory.settings.personaSaveFailed") + " (" + personaError + ")")
3989
+ )
3990
+ ),
3991
+ // Agent 眼中的画像(只读)— 注入生效文本:profile + rules 原样展示。
3992
+ // 纯前端拼接(GET 已在 load 里拿到 profile/rules),零后端新请求。
3993
+ h("section", { className: "mneme-set-card" },
3994
+ h("div", { className: "mneme-set-title" }, t("memory.settings.personaView")),
3995
+ h("div", { className: "mneme-set-desc" }, t("memory.settings.personaViewHint")),
3996
+ (profile.trim() || rules.length)
3997
+ ? h("pre", { className: "mneme-set-input mneme-set-input--area", style: { whiteSpace: "pre-wrap", minHeight: 0 } },
3998
+ [profile.trim() ? "- " + profile.trim() : "", ...rules.map((r) => "- " + r)].filter(Boolean).join("\n"))
3999
+ : h("div", { className: "mneme-xempty" }, t("memory.settings.personaViewEmpty"))
4000
+ ),
3655
4001
  // 规则 — numbered rows, hover reveals the delete affordance
3656
4002
  h("section", { className: "mneme-set-card" },
3657
4003
  h("div", { className: "mneme-set-title" }, t("memory.settings.rules")),
@@ -3879,7 +4225,7 @@ window.__ModuleLoader__.load({
3879
4225
  ),
3880
4226
  apiTokenSaved && h("div", { style: { marginTop: 8 } }, h("span", { className: "mneme-saved" }, t("memory.settings.apiTokenSaved")))
3881
4227
  ),
3882
- // —— 帮助:意见与反馈(图标行 + 版本 footer,兼当「关于」收尾) ——
4228
+ // —— 帮助:支持与鼓励(图标行 + 检测更新 + star,兼当「关于」收尾) ——
3883
4229
  h("div", { className: "mneme-set-grouphead" }, t("memory.settings.group.help")),
3884
4230
  // 反馈三入口:新建 issue 预填(版本+平台)/ 邮件 / 浏览已知问题。
3885
4231
  // 纯前端链接零后端成本;插件版本来自 /info,平台取 userAgent。GitHub
@@ -3895,6 +4241,21 @@ window.__ModuleLoader__.load({
3895
4241
  feedRow("search", t("memory.settings.feedback.browse"), t("memory.settings.feedback.browse.sub"),
3896
4242
  { href: "https://github.com/slow-stack/mneme/issues", target: "_blank", rel: "noopener noreferrer" })
3897
4243
  ),
4244
+ // 检测更新:手动触发(复验修订——原顶部自动横幅移除),结果内联;
4245
+ // outdated 时提示带 pnpm 钉子警示(#174 报障成因)。
4246
+ h("div", { style: { display: "flex", alignItems: "center", gap: 8, marginTop: 12 } },
4247
+ h("button", { className: "mneme-btn", onClick: checkForUpdate, disabled: updateChecking },
4248
+ updateChecking ? "…" : t("memory.settings.update.check")),
4249
+ updateInfo && h("span", { className: "mneme-feedsub" }, checkResultText())
4250
+ ),
4251
+ updateInfo?.status === "outdated" && h("div", { className: "mneme-set-hint", style: { marginTop: 8 } },
4252
+ t("memory.settings.version.outdatedHint")),
4253
+ // star 鼓励入口:链接到仓库主页而非 stargazers——前者右上角就有
4254
+ // Star 按钮,后者只是「看谁点过」的名单页(复验实测反馈)。
4255
+ h("div", { className: "mneme-feedlist", style: { marginTop: 12 } },
4256
+ feedRow("star", t("memory.settings.star"), t("memory.settings.star.sub"),
4257
+ { href: "https://github.com/slow-stack/mneme", target: "_blank", rel: "noopener noreferrer" })
4258
+ ),
3898
4259
  h("div", { className: "mneme-set-foot" },
3899
4260
  h("span", null, `dsh-mneme v${pkgVersion}`)
3900
4261
  )
@@ -4580,7 +4941,18 @@ window.__ModuleLoader__.load({
4580
4941
  if (n >= 1e3) return `${Math.round(n / 1e3)}K`;
4581
4942
  return String(n);
4582
4943
  };
4583
- const heatLevel = (n) => (n > 2500000 ? 4 : n > 1500000 ? 3 : n > 750000 ? 2 : n > 0 ? 1 : 0);
4944
+ // 热力分档:固定阈值(75万/150万/250万)在低用量库里整片落在 l1,档位失真
4945
+ // (复验第三轮「颜色不怎么明显」)。改按 56 天窗口内的非零日做三分位切四档
4946
+ // ——GitHub 贡献图同思路,档次随库的实际用量自适应拉开;全零/无数据退化
4947
+ // 为全 0 档(不留空档位)。边界是三个分位值(q 只返回 3 个):n>p75 → l4、
4948
+ // >p50 → l3、>p25 → l2、其余非零 → l1,四档都有 populated。
4949
+ const heatLevel = (n, q) => (n <= 0 ? 0 : n > q[2] ? 4 : n > q[1] ? 3 : n > q[0] ? 2 : 1);
4950
+ const heatQuartiles = (days) => {
4951
+ const pos = days.map((d) => d.total).filter((v) => v > 0).sort((a, b) => a - b);
4952
+ if (!pos.length) return [0, 0, 0];
4953
+ const pick = (p) => pos[Math.min(pos.length - 1, Math.floor(p * pos.length))];
4954
+ return [pick(0.25), pick(0.5), pick(0.75)];
4955
+ };
4584
4956
 
4585
4957
  function TokenFlowSection({ t }) {
4586
4958
  const [stats, setStats] = useState(null);
@@ -4690,6 +5062,33 @@ window.__ModuleLoader__.load({
4690
5062
  heatCells.push({ key, total: byDayMap.get(key) ?? 0 });
4691
5063
  }
4692
5064
  }
5065
+ // 分档阈值取自本窗口真实分布(见 heatQuartiles 注释),空窗口退化全 0 档。
5066
+ const heatQ = heatQuartiles(heatCells);
5067
+ // 月份标签:GitHub 同款——按周列贴,列首日期属于哪个月,那列就标那个
5068
+ // 月;连续同月的列合并成一段(gridColumn: from / span)。月不在列首
5069
+ // 开始时其首列仍标上一月(列首归属优先),这是贡献图的通行近似。
5070
+ const monthLabels = (() => {
5071
+ if (!heatCells.length) return [];
5072
+ const spans = [];
5073
+ heatCells.forEach((c, i) => {
5074
+ if (i % 7 !== 0) return; // 只看列首
5075
+ const ym = c.key.slice(0, 7);
5076
+ const col = Math.floor(i / 7) + 1;
5077
+ const label = `${Number(ym.slice(5, 7))}月`;
5078
+ const lastSpan = spans[spans.length - 1];
5079
+ if (lastSpan && lastSpan.key === ym) { lastSpan.last = col; return; }
5080
+ spans.push({ key: ym, label, from: col, last: col });
5081
+ });
5082
+ return spans.map((m) => ({ ...m, span: m.last - m.from + 1 }));
5083
+ })();
5084
+ // 行标签:行 r 的星期 = (首列星期 + r) % 7(getUTCDay 周日=0)。一/三/五
5085
+ // 三行有字(GitHub 同款),其余留空。
5086
+ const heatRowLabels = (() => {
5087
+ const startDow = heatCells.length ? new Date(`${heatCells[0].key}T00:00:00Z`).getUTCDay() : 0;
5088
+ const rows = new Array(7).fill("");
5089
+ for (const d of [1, 3, 5]) rows[(d - startDow + 7) % 7] = ["一", "三", "五"][[1, 3, 5].indexOf(d)];
5090
+ return rows;
5091
+ })();
4693
5092
  const kpi = (title, num, cap) =>
4694
5093
  h(StatusCard, { t, title, loading, error: statsError, num, cap });
4695
5094
  const segClass = (s) =>
@@ -4783,12 +5182,22 @@ window.__ModuleLoader__.load({
4783
5182
  days.map((d) => h("span", { key: d.day }, d.day.slice(5)))
4784
5183
  ),
4785
5184
  h("h3", { className: "mneme-xcolhead" }, t("memory.status.token.heat")),
4786
- h("div", { className: "mneme-heatgrid" },
4787
- heatCells.map((c) => h("div", {
4788
- key: c.key,
4789
- className: heatLevel(c.total) ? `mneme-heatcell l${heatLevel(c.total)}` : "mneme-heatcell",
4790
- title: `${c.key} · ${c.total.toLocaleString()}`
4791
- }))
5185
+ // 月份行与格子列同用列主序网格对齐:每月占的列数 = 该月在这个
5186
+ // 56 天窗口里的列数(按列的起始日期算),标签只在该列起点画一次。
5187
+ h("div", { className: "mneme-heatmonths" },
5188
+ monthLabels.map((m) => h("span", { key: m.key, style: { gridColumn: `${m.from} / span ${m.span}` } }, m.label))
5189
+ ),
5190
+ h("div", { className: "mneme-heatwrap" },
5191
+ h("div", { className: "mneme-heatrows", "aria-hidden": "true" },
5192
+ heatRowLabels.map((d, i) => h("span", { key: i }, d))
5193
+ ),
5194
+ h("div", { className: "mneme-heatgrid" },
5195
+ heatCells.map((c) => h("div", {
5196
+ key: c.key,
5197
+ className: heatLevel(c.total, heatQ) ? `mneme-heatcell l${heatLevel(c.total, heatQ)}` : "mneme-heatcell",
5198
+ title: `${c.key} · ${c.total.toLocaleString()}`
5199
+ }))
5200
+ )
4792
5201
  ),
4793
5202
  h("div", { className: "mneme-heatlegend" },
4794
5203
  h("span", null, t("memory.status.token.heatLess")),
@@ -6267,6 +6676,9 @@ window.__ModuleLoader__.load({
6267
6676
  ),
6268
6677
  ),
6269
6678
  view === "memory" && h("div", { className: "mneme-xmain" },
6679
+ // 两栏布局(桉桉规格):左栏上段=语义检索(怎么搜),下段=重要性/
6680
+ // 来源/热度(留什么);原第二侧栏的类型列表与计数刷新下沉到内容区
6681
+ // 工具条(.mneme-xbrowsebar),五个功能入口一个不丢。
6270
6682
  h("div", { className: "mneme-xside" },
6271
6683
  h("div", { className: "mneme-xcolhead" }, t("memory.explorer.searchTitle")),
6272
6684
  h("div", { className: "mneme-xsearchwrap" },
@@ -6307,39 +6719,28 @@ window.__ModuleLoader__.load({
6307
6719
  ["all", "7d", "30d", "90d"].map((k) =>
6308
6720
  h("option", { key: k, value: k }, t(`memory.explorer.date.${k}`))))
6309
6721
  ),
6310
- h("div", { className: "mneme-xrow" },
6311
- h("span", { className: "mneme-xcount" }, t("memory.explorer.count").replace("{n}", String(visible.length))),
6312
- h("button", { className: "mneme-footbtn", onClick: () => setReloadKey((k) => k + 1) },
6313
- h(Icon, { name: "refresh", size: 12 }), t("memory.explorer.refresh"))
6314
- )
6315
- ),
6316
- h("div", { className: "mneme-xside mneme-xside--filter" },
6317
- h("div", { className: "mneme-xcolhead" }, t("memory.explorer.importance")),
6318
- h("div", { className: "mneme-xrow" },
6319
- [0, 3, 4, 5].map((v) =>
6320
- h("button", {
6321
- key: v,
6322
- className: minImp === v ? "mneme-chip mneme-active" : "mneme-chip",
6323
- onClick: () => setMinImp(v)
6324
- }, v === 0 ? t("memory.tab.all") : h(react.Fragment, null, h(StarGlyph, { size: 11 }), ` ${v}+`)))
6325
- ),
6326
- h("div", { className: "mneme-xcolhead" }, t("memory.explorer.sourceFilter")),
6327
- h("div", { className: "mneme-xrow" },
6328
- h("button", {
6329
- className: depositedOnly ? "mneme-chip mneme-active" : "mneme-chip",
6330
- title: t("memory.explorer.filterDepositedHint"),
6331
- onClick: () => setDepositedOnly(!depositedOnly)
6332
- }, t("memory.explorer.filterDeposited")),
6333
- h("button", {
6334
- className: archivedOnly ? "mneme-chip mneme-active" : "mneme-chip",
6335
- title: t("memory.explorer.filterArchivedHint"),
6336
- onClick: () => setArchivedOnly(!archivedOnly)
6337
- }, t("memory.explorer.filterArchived"))
6338
- ),
6339
- heatAvailable && h(react.Fragment, null,
6340
- h("div", { className: "mneme-xcolhead" }, t("memory.explorer.sort")),
6722
+ h("div", { className: "mneme-xside-imp" },
6723
+ h("div", { className: "mneme-xcolhead" }, t("memory.explorer.importance")),
6724
+ h("div", { className: "mneme-xrow" },
6725
+ [0, 3, 4, 5].map((v) =>
6726
+ h("button", {
6727
+ key: v,
6728
+ className: minImp === v ? "mneme-chip mneme-active" : "mneme-chip",
6729
+ onClick: () => setMinImp(v)
6730
+ }, v === 0 ? t("memory.tab.all") : h(react.Fragment, null, h(StarGlyph, { size: 11 }), ` ${v}+`)))
6731
+ ),
6341
6732
  h("div", { className: "mneme-xrow" },
6342
6733
  h("button", {
6734
+ className: depositedOnly ? "mneme-chip mneme-active" : "mneme-chip",
6735
+ title: t("memory.explorer.filterDepositedHint"),
6736
+ onClick: () => setDepositedOnly(!depositedOnly)
6737
+ }, t("memory.explorer.filterDeposited")),
6738
+ h("button", {
6739
+ className: archivedOnly ? "mneme-chip mneme-active" : "mneme-chip",
6740
+ title: t("memory.explorer.filterArchivedHint"),
6741
+ onClick: () => setArchivedOnly(!archivedOnly)
6742
+ }, t("memory.explorer.filterArchived")),
6743
+ heatAvailable && h("button", {
6343
6744
  className: heatSort ? "mneme-chip mneme-active" : "mneme-chip",
6344
6745
  title: t("memory.explorer.heatSortHint"),
6345
6746
  onClick: () => {
@@ -6348,23 +6749,28 @@ window.__ModuleLoader__.load({
6348
6749
  }
6349
6750
  }, t("memory.explorer.heatSort"))
6350
6751
  )
6351
- ),
6352
- h("div", { className: "mneme-xcolhead" }, t("memory.explorer.types")),
6353
- h("button", {
6354
- className: type === "all" ? "mneme-xtype mneme-active" : "mneme-xtype",
6355
- onClick: () => setType("all")
6356
- }, h("span", { className: "mneme-xdot mneme-xdot--all", "aria-hidden": "true" }), h("span", null, t("memory.tab.all")), h("span", { className: "mneme-xcount2" }, String(items.length))),
6357
- knownTypes.concat(extraTypes).map((key) =>
6358
- h("button", {
6359
- key,
6360
- className: type === key ? "mneme-xtype mneme-active" : "mneme-xtype",
6361
- onClick: () => setType(key)
6362
- },
6363
- h("span", { className: "mneme-xdot", style: { color: memoryTypeColor(key) }, title: typeLabel(t, key), "aria-hidden": "true" }),
6364
- h("span", null, typeLabel(t, key)),
6365
- h("span", { className: "mneme-xcount2" }, String(counts[key]))
6366
- ))
6752
+ )
6367
6753
  ),
6754
+ h("div", { className: "mneme-xcontent" },
6755
+ h("div", { className: "mneme-xbrowsebar" },
6756
+ h("button", {
6757
+ className: type === "all" ? "mneme-xtype mneme-active" : "mneme-xtype",
6758
+ onClick: () => setType("all")
6759
+ }, h("span", { className: "mneme-xdot mneme-xdot--all", "aria-hidden": "true" }), h("span", null, t("memory.tab.all")), h("span", { className: "mneme-xcount2" }, String(items.length))),
6760
+ knownTypes.concat(extraTypes).map((key) =>
6761
+ h("button", {
6762
+ key,
6763
+ className: type === key ? "mneme-xtype mneme-active" : "mneme-xtype",
6764
+ onClick: () => setType(key)
6765
+ },
6766
+ h("span", { className: "mneme-xdot", style: { color: memoryTypeColor(key) }, title: typeLabel(t, key), "aria-hidden": "true" }),
6767
+ h("span", null, typeLabel(t, key)),
6768
+ h("span", { className: "mneme-xcount2" }, String(counts[key]))
6769
+ )),
6770
+ h("span", { className: "mneme-xcount", style: { marginLeft: "auto" } }, t("memory.explorer.count").replace("{n}", String(visible.length))),
6771
+ h("button", { className: "mneme-footbtn", onClick: () => setReloadKey((k) => k + 1) },
6772
+ h(Icon, { name: "refresh", size: 12 }), t("memory.explorer.refresh"))
6773
+ ),
6368
6774
  viewMode === "cards"
6369
6775
  ? h("div", { className: "mneme-cards" },
6370
6776
  loading
@@ -6469,8 +6875,11 @@ window.__ModuleLoader__.load({
6469
6875
  )
6470
6876
  ),
6471
6877
  ),
6878
+ ),
6472
6879
  // 详情抽屉:卡片/时间线共用,选中即从右侧滑出(替代旧底部面板)。
6473
6880
  // 两步删除确认在抽屉内完成;编辑/归档经 /update 落库并同步本地视图。
6881
+ // 保持为 .mneme-xmain 直接子元素:抽屉锚在 xmain 的 relative 层上,随
6882
+ // 两栏改版内容区变宽,抽屉锚点不变。
6474
6883
  selected && h(MemoryDrawer, {
6475
6884
  key: selected.id,
6476
6885
  t,
@@ -7873,3 +8282,7 @@ window.__ModuleLoader__.load({
7873
8282
  return module.exports;
7874
8283
  }
7875
8284
  });
8285
+
8286
+
8287
+
8288
+