@modusensus/dsh-mneme 0.7.10 → 0.7.11

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 (154) hide show
  1. package/README.en.md +368 -0
  2. package/README.md +298 -169
  3. package/{dsh-mneme/cordis.patch.yml → cordis.patch.yml} +1 -0
  4. package/{dsh-mneme/lib → lib}/api.js +50 -295
  5. package/{dsh-mneme/lib → lib}/client.js +780 -936
  6. package/{dsh-mneme/src → lib}/config.js +6 -100
  7. package/{dsh-mneme/lib → lib}/dream/decisions.js +94 -143
  8. package/{dsh-mneme/lib → lib}/dream/sleep.js +12 -152
  9. package/{dsh-mneme/lib → lib}/dream.js +22 -168
  10. package/{dsh-mneme/src → lib}/hot-memory.js +0 -7
  11. package/{dsh-mneme/lib → lib}/index.js +6 -34
  12. package/{dsh-mneme/lib → lib}/inject.js +11 -53
  13. package/{dsh-mneme/src → lib}/mirror.js +1 -12
  14. package/{dsh-mneme/lib → lib}/quality-filter.js +1 -3
  15. package/{dsh-mneme/lib → lib}/reranker.js +1 -4
  16. package/{dsh-mneme/lib → lib}/service.js +20 -410
  17. package/{dsh-mneme/lib → lib}/settings.js +0 -70
  18. package/{dsh-mneme/lib → lib}/store.js +35 -450
  19. package/{dsh-mneme/lib → lib}/summarize.js +10 -10
  20. package/lib/tools.js +274 -0
  21. package/package.json +41 -19
  22. package/{dsh-mneme/scripts → scripts}/e2e-dsh.js +3 -4
  23. package/{dsh-mneme/scripts → scripts}/sync-lib.js +5 -10
  24. package/{dsh-mneme/src → src}/api.js +50 -295
  25. package/{dsh-mneme/lib → src}/config.js +6 -100
  26. package/{dsh-mneme/src → src}/dream/decisions.js +94 -143
  27. package/{dsh-mneme/src → src}/dream/sleep.js +12 -152
  28. package/{dsh-mneme/src → src}/dream.js +22 -168
  29. package/{dsh-mneme/lib → src}/hot-memory.js +0 -7
  30. package/{dsh-mneme/src → src}/index.js +6 -34
  31. package/{dsh-mneme/src → src}/inject.js +11 -53
  32. package/{dsh-mneme/lib → src}/mirror.js +1 -12
  33. package/{dsh-mneme/src → src}/quality-filter.js +1 -3
  34. package/{dsh-mneme/src → src}/reranker.js +1 -4
  35. package/{dsh-mneme/src → src}/service.js +20 -410
  36. package/{dsh-mneme/src → src}/settings.js +0 -70
  37. package/{dsh-mneme/src → src}/store.js +35 -450
  38. package/{dsh-mneme/src → src}/summarize.js +10 -10
  39. package/src/tools.js +274 -0
  40. package/{dsh-mneme/test → test}/api.test.js +1 -156
  41. package/{dsh-mneme/test → test}/client.test.js +10 -206
  42. package/{dsh-mneme/test → test}/config.test.js +0 -21
  43. package/{dsh-mneme/test → test}/dream.test.js +1 -160
  44. package/{dsh-mneme/test → test}/graph-api.test.js +0 -34
  45. package/{dsh-mneme/test → test}/hot-memory.test.js +0 -29
  46. package/test/inject.test.js +120 -0
  47. package/{dsh-mneme/test → test}/llm-audit.test.js +4 -4
  48. package/{dsh-mneme/test → test}/reasoning-effort.test.js +0 -27
  49. package/{dsh-mneme/test → test}/recall-layer.test.js +5 -26
  50. package/{dsh-mneme/test → test}/reranker.test.js +0 -43
  51. package/{dsh-mneme/test → test}/service-search.test.js +0 -25
  52. package/{dsh-mneme/test → test}/service.test.js +0 -106
  53. package/{dsh-mneme/test → test}/settings.test.js +0 -43
  54. package/{dsh-mneme/test → test}/sleep.test.js +4 -171
  55. package/{dsh-mneme/test → test}/store.test.js +0 -76
  56. package/{dsh-mneme/test → test}/summarize.test.js +16 -15
  57. package/test/tools.test.js +265 -0
  58. package/.github/workflows/publish.yml +0 -58
  59. package/.github/workflows/test.yml +0 -32
  60. package/CHANGELOG.md +0 -91
  61. package/CONTRIBUTING.md +0 -293
  62. package/SECURITY.md +0 -718
  63. package/docs/devlog/2026-08-14-dsh-mneme-dev-log.md +0 -247
  64. package/docs/devlog/2026-08-15-dsh-mneme-audit-stress-dev-log.md +0 -145
  65. package/docs/devlog/2026-08-15-dsh-mneme-pipeline-dev-log.md +0 -56
  66. package/docs/devlog/2026-08-15-dsh-mneme-reflection-dev-log.md +0 -77
  67. package/docs/devlog/2026-08-15-dsh-mneme-review-fixes-dev-log.md +0 -64
  68. package/docs/devlog/2026-08-15-dsh-mneme-semantic-dev-log.md +0 -90
  69. package/dsh-mneme/CHANGELOG.md +0 -419
  70. package/dsh-mneme/LICENSE +0 -21
  71. package/dsh-mneme/README.md +0 -501
  72. package/dsh-mneme/docs/AGENT_MEMORY_RESEARCH.md +0 -183
  73. package/dsh-mneme/docs/ENTITIES.md +0 -245
  74. package/dsh-mneme/docs/LOCAL_MODEL.md +0 -141
  75. package/dsh-mneme/docs/MIGRATION.md +0 -127
  76. package/dsh-mneme/docs/SEMANTIC.md +0 -256
  77. package/dsh-mneme/docs/SLEEP.md +0 -163
  78. package/dsh-mneme/lib/dream/tag-extractor.js +0 -156
  79. package/dsh-mneme/lib/heat.js +0 -136
  80. package/dsh-mneme/lib/parser/tag.js +0 -59
  81. package/dsh-mneme/lib/parser/wiki-link.js +0 -38
  82. package/dsh-mneme/lib/search/tag-boost.js +0 -61
  83. package/dsh-mneme/lib/tools.js +0 -458
  84. package/dsh-mneme/package-lock.json +0 -1936
  85. package/dsh-mneme/package.json +0 -83
  86. package/dsh-mneme/scripts/check-sync.js +0 -42
  87. package/dsh-mneme/src/client.js +0 -2242
  88. package/dsh-mneme/src/dream/tag-extractor.js +0 -156
  89. package/dsh-mneme/src/heat.js +0 -136
  90. package/dsh-mneme/src/parser/tag.js +0 -59
  91. package/dsh-mneme/src/parser/wiki-link.js +0 -38
  92. package/dsh-mneme/src/search/tag-boost.js +0 -61
  93. package/dsh-mneme/src/tools.js +0 -458
  94. package/dsh-mneme/test/boundary-v0625.test.js +0 -82
  95. package/dsh-mneme/test/directory.test.js +0 -134
  96. package/dsh-mneme/test/heat.test.js +0 -148
  97. package/dsh-mneme/test/inject.test.js +0 -206
  98. package/dsh-mneme/test/layered-types-stats.test.js +0 -144
  99. package/dsh-mneme/test/lib-smoke.test.js +0 -109
  100. package/dsh-mneme/test/normalize-decisions.test.js +0 -120
  101. package/dsh-mneme/test/provenance.test.js +0 -103
  102. package/dsh-mneme/test/recall-runs.test.js +0 -93
  103. package/dsh-mneme/test/sleep-heat.test.js +0 -112
  104. package/dsh-mneme/test/tag-boost.test.js +0 -125
  105. package/dsh-mneme/test/tag.test.js +0 -426
  106. package/dsh-mneme/test/tools.test.js +0 -674
  107. package/dsh-mneme/test/updated-at-semantics.test.js +0 -113
  108. package/dsh-mneme/test/wiki-link.test.js +0 -332
  109. package//346/250/252/345/271/205.png +0 -0
  110. /package/{dsh-mneme/lib → lib}/commands.js +0 -0
  111. /package/{dsh-mneme/lib → lib}/dream/clustering.js +0 -0
  112. /package/{dsh-mneme/lib → lib}/embedding.js +0 -0
  113. /package/{dsh-mneme/lib → lib}/entities/extractor.js +0 -0
  114. /package/{dsh-mneme/lib → lib}/local-embedder.js +0 -0
  115. /package/{dsh-mneme/lib → lib}/search/adaptive.js +0 -0
  116. /package/{dsh-mneme/lib → lib}/search/bm25.js +0 -0
  117. /package/{dsh-mneme/lib → lib}/vector-index.js +0 -0
  118. /package/{dsh-mneme/scripts → scripts}/benchmark-embed.js +0 -0
  119. /package/{dsh-mneme/scripts → scripts}/benchmark-recall.js +0 -0
  120. /package/{dsh-mneme/scripts → scripts}/benchmark-rerank.js +0 -0
  121. /package/{dsh-mneme/scripts → scripts}/stress-dsh.js +0 -0
  122. /package/{dsh-mneme/src → src}/commands.js +0 -0
  123. /package/{dsh-mneme/src → src}/dream/clustering.js +0 -0
  124. /package/{dsh-mneme/src → src}/embedding.js +0 -0
  125. /package/{dsh-mneme/src → src}/entities/extractor.js +0 -0
  126. /package/{dsh-mneme/src → src}/local-embedder.js +0 -0
  127. /package/{dsh-mneme/src → src}/search/adaptive.js +0 -0
  128. /package/{dsh-mneme/src → src}/search/bm25.js +0 -0
  129. /package/{dsh-mneme/src → src}/vector-index.js +0 -0
  130. /package/{dsh-mneme/test → test}/audit.test.js +0 -0
  131. /package/{dsh-mneme/test → test}/benchmark.test.js +0 -0
  132. /package/{dsh-mneme/test → test}/clustering.test.js +0 -0
  133. /package/{dsh-mneme/test → test}/commands.test.js +0 -0
  134. /package/{dsh-mneme/test → test}/conflict-freeze.test.js +0 -0
  135. /package/{dsh-mneme/test → test}/entities.test.js +0 -0
  136. /package/{dsh-mneme/test → test}/epistemic.test.js +0 -0
  137. /package/{dsh-mneme/test → test}/fnew-0112.test.js +0 -0
  138. /package/{dsh-mneme/test → test}/fnew-03.test.js +0 -0
  139. /package/{dsh-mneme/test → test}/helpers/dream-mock.js +0 -0
  140. /package/{dsh-mneme/test → test}/local-embedder.test.js +0 -0
  141. /package/{dsh-mneme/test → test}/mirror-dirty.test.js +0 -0
  142. /package/{dsh-mneme/test → test}/mirror-edit-digest.test.js +0 -0
  143. /package/{dsh-mneme/test → test}/mirror-generation.test.js +0 -0
  144. /package/{dsh-mneme/test → test}/mirror.test.js +0 -0
  145. /package/{dsh-mneme/test → test}/peer-blockers.test.js +0 -0
  146. /package/{dsh-mneme/test → test}/policy-epoch.test.js +0 -0
  147. /package/{dsh-mneme/test → test}/quality-filter.test.js +0 -0
  148. /package/{dsh-mneme/test → test}/recall-evals.test.js +0 -0
  149. /package/{dsh-mneme/test → test}/receipt-chain.test.js +0 -0
  150. /package/{dsh-mneme/test → test}/reflection.test.js +0 -0
  151. /package/{dsh-mneme/test → test}/search-fusion.test.js +0 -0
  152. /package/{dsh-mneme/test → test}/semantic.test.js +0 -0
  153. /package/{dsh-mneme/test → test}/stress.test.js +0 -0
  154. /package/{dsh-mneme/test → test}/vector-index.test.js +0 -0
@@ -39,6 +39,41 @@ window.__ModuleLoader__.load({
39
39
  h("circle", { cx: 12.4, cy: 12, r: 1.8, fill: "currentColor" })
40
40
  );
41
41
 
42
+ // Stroke icons, Lucide path data (ISC license) inlined as [tag, attrs]
43
+ // tuples — the plugin runtime cannot require third-party libraries, so
44
+ // the data ships with the bundle (the morphicons/lucide pairing the
45
+ // data package documents, minus the runtime dependency). Stroke picks
46
+ // up currentColor, so icons follow the host theme tokens.
47
+ const ICON_PATHS = {
48
+ database: [["ellipse", { cx: "12", cy: "5", rx: "9", ry: "3" }], ["path", { d: "M3 5V19A9 3 0 0 0 21 19V5" }], ["path", { d: "M3 12A9 3 0 0 0 21 12" }]],
49
+ waypoints: [["path", { d: "m10.586 5.414-5.172 5.172" }], ["path", { d: "m18.586 13.414-5.172 5.172" }], ["path", { d: "M6 12h12" }], ["circle", { cx: "12", cy: "20", r: "2" }], ["circle", { cx: "12", cy: "4", r: "2" }], ["circle", { cx: "20", cy: "12", r: "2" }], ["circle", { cx: "4", cy: "12", r: "2" }]],
50
+ settings: [["path", { d: "M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" }], ["circle", { cx: "12", cy: "12", r: "3" }]],
51
+ search: [["path", { d: "m21 21-4.34-4.34" }], ["circle", { cx: "11", cy: "11", r: "8" }]],
52
+ refresh: [["path", { d: "M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" }], ["path", { d: "M21 3v5h-5" }], ["path", { d: "M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" }], ["path", { d: "M8 16H3v5" }]],
53
+ chevronDown: [["path", { d: "m6 9 6 6 6-6" }]],
54
+ chevronRight: [["path", { d: "m9 18 6-6-6-6" }]],
55
+ copy: [["rect", { width: "14", height: "14", x: "8", y: "8", rx: "2", ry: "2" }], ["path", { d: "M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" }]],
56
+ check: [["path", { d: "M20 6 9 17l-5-5" }]],
57
+ inbox: [["polyline", { points: "22 12 16 12 14 15 10 15 8 12 2 12" }], ["path", { d: "M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" }]],
58
+ activity: [["path", { d: "M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2" }]]
59
+ };
60
+ const Icon = ({ name, size = 16, className }) => {
61
+ const parts = ICON_PATHS[name];
62
+ if (!parts) return null;
63
+ return h("svg", {
64
+ width: size,
65
+ height: size,
66
+ viewBox: "0 0 24 24",
67
+ fill: "none",
68
+ stroke: "currentColor",
69
+ strokeWidth: 2,
70
+ strokeLinecap: "round",
71
+ strokeLinejoin: "round",
72
+ className,
73
+ "aria-hidden": "true"
74
+ }, parts.map(([tag, attrs], i) => h(tag, { key: i, ...attrs })));
75
+ };
76
+
42
77
  // Unified API fetcher: attaches the optional apiToken (set in the settings
43
78
  // view, persisted in localStorage) as a Bearer header. When no token has
44
79
  // been configured the header is omitted and the API stays open (default).
@@ -65,8 +100,6 @@ window.__ModuleLoader__.load({
65
100
  "memory.tab.project": "项目",
66
101
  "memory.tab.decision": "决策",
67
102
  "memory.tab.history": "历史",
68
- "memory.tab.user": "用户",
69
- "memory.tab.fact": "事实",
70
103
  "memory.settings.title": "记忆库设置",
71
104
  "memory.settings.profile": "用户画像",
72
105
  "memory.settings.profileHint": "写一段自我介绍(角色、背景、偏好),Agent 每轮对话都会自动带上。",
@@ -74,16 +107,6 @@ window.__ModuleLoader__.load({
74
107
  "memory.settings.profileSaved": "画像已保存",
75
108
  "memory.settings.rules": "规则",
76
109
  "memory.settings.rulesHint": "给 Agent 立下必须遵守的规矩,随时增删,下一轮即生效。",
77
- "memory.settings.autoTagTitle": "自动打标签",
78
- "memory.settings.autoTagHint": "开启后,新记忆由轻量模型自动打标签,目录页按标签自动分类(opt-in,默认关)",
79
- "memory.settings.autoTagEnabled": "自动打标签",
80
- "memory.settings.autoTagSave": "保存",
81
- "memory.settings.autoTagSaved": "已保存",
82
- "memory.settings.sidebarTriggerTitle": "侧边栏入口按钮",
83
- "memory.settings.sidebarTriggerHint": "关闭后隐藏侧边栏底部(左下角)的记忆入口按钮。与其他插件 UI 冲突时建议关闭;记忆库仍可通过顶部「记忆库」标签访问",
84
- "memory.settings.sidebarTriggerEnabled": "显示左下角入口按钮",
85
- "memory.settings.sidebarTriggerSave": "保存",
86
- "memory.settings.sidebarTriggerSaved": "已保存",
87
110
  "memory.settings.ruleAdd": "添加规则",
88
111
  "memory.settings.rulePlaceholder": "例如:回答时总是先给结论",
89
112
  "memory.settings.commands": "自定义指令",
@@ -99,10 +122,23 @@ window.__ModuleLoader__.load({
99
122
  "memory.view.label": "记忆库",
100
123
  "memory.overlay.close": "关闭",
101
124
  "memory.explorer.tabMemory": "记忆",
102
- "memory.explorer.tabGraph": "图谱",
103
- "memory.explorer.tabDirectory": "目录",
125
+ "memory.explorer.tabEntities": "实体",
104
126
  "memory.explorer.tabSettings": "设置",
105
- "memory.explorer.tabOverview": "总览",
127
+ "memory.explorer.tabStatus": "状态",
128
+ "memory.entities.count": "{n} 个实体",
129
+ "memory.entities.pick": "从左侧选择一个实体,查看它的属性与关系",
130
+ "memory.entities.none": "暂无实体:开启实体抽取后,记忆里的人物 / 项目 / 概念会自动沉淀到这里",
131
+ "memory.entities.attrs": "属性",
132
+ "memory.entities.relations": "关系",
133
+ "memory.entities.mentions": "{n} 次提及",
134
+ "memory.entities.lastSeen": "最近出现",
135
+ "memory.entities.graph": "关系图谱",
136
+ "memory.entity.person": "人物",
137
+ "memory.entity.project": "项目",
138
+ "memory.entity.concept": "概念",
139
+ "memory.entity.technology": "技术",
140
+ "memory.entity.organization": "组织",
141
+ "memory.entity.other": "其他",
106
142
  "memory.explorer.search": "搜索标题或内容…",
107
143
  "memory.explorer.searchTitle": "语义检索",
108
144
  "memory.explorer.types": "分类",
@@ -112,34 +148,16 @@ window.__ModuleLoader__.load({
112
148
  "memory.explorer.copy": "复制全文",
113
149
  "memory.explorer.copied": "已复制",
114
150
  "memory.explorer.refresh": "刷新",
151
+ "memory.explorer.loadMore": "加载更多",
115
152
  "memory.explorer.count": "共 {n} 条",
116
153
  "memory.explorer.empty": "暂无记忆条目",
117
154
  "memory.explorer.source": "来源",
118
155
  "memory.explorer.created": "创建",
119
156
  "memory.explorer.updated": "更新",
120
157
  "memory.explorer.tags": "标签",
121
- "memory.explorer.tagAdd": "添加标签",
122
- "memory.explorer.tagRemove": "移除标签",
123
- "memory.explorer.tagPlaceholder": "输入标签,回车添加",
124
- "memory.explorer.tagsEmpty": "暂无标签",
125
158
  "memory.explorer.importance": "重要性",
126
159
  "memory.explorer.topK": "返回数量",
127
160
  "memory.explorer.topKOption": "返回 {n} 条",
128
- "memory.overview.empty": "还没有记忆。开始对话后 Agent 会自动沉淀记忆,这里会展示分层总览",
129
- "memory.overview.profileEmpty": "暂无用户画像记忆,可在对话中让 AI 记录",
130
- "memory.overview.distribution": "类型分布",
131
- "memory.overview.layers": "记忆分层",
132
- "memory.overview.trend": "近 7 天趋势",
133
- "memory.overview.none": "暂无",
134
- "memory.overview.badgeInject": "常注入",
135
- "memory.overview.badgeImportance": "按重要性",
136
- "memory.directory.untagged": "无标签",
137
- "memory.directory.loading": "加载中…",
138
- "memory.directory.empty": "暂无记忆条目",
139
- "directory.editToggle": "编辑模式(显示删除)",
140
- "directory.deleteConfirm": "确定删除这条记忆吗?",
141
- "directory.deleteConfirmShort": "确认?",
142
- "directory.deleteError": "删除失败",
143
161
  "memory.card.open": "在主区记忆库中查看全文",
144
162
  "memory.time.now": "刚刚",
145
163
  "memory.time.seconds": "{n}秒前",
@@ -158,7 +176,7 @@ window.__ModuleLoader__.load({
158
176
  "memory.graph.sourceMemory": "查看来源记忆",
159
177
  "memory.graph.hint": "拖拽节点调整布局 · 空白处拖动平移 · 滚轮缩放",
160
178
  "memory.graph.resetView": "重置视图",
161
- "memory.graph.viewInGraph": "在图谱中查看",
179
+ "memory.graph.viewInGraph": "在实体视图中打开",
162
180
  "memory.graph.loading": "加载中…",
163
181
  "memory.graph.distance": "距中心 {n} 跳",
164
182
  "memory.settings.vectorTitle": "向量搜索",
@@ -177,11 +195,19 @@ window.__ModuleLoader__.load({
177
195
  "memory.settings.apiTokenPlaceholder": "留空 = 不鉴权(默认)",
178
196
  "memory.settings.apiTokenSave": "保存 Token",
179
197
  "memory.settings.apiTokenSaved": "Token 已保存",
180
- "memory.wikilink.backlinks": "链接到此记忆",
181
- "memory.wikilink.forward": "此记忆链接到",
182
- "memory.wikilink.empty": "暂无关联记忆",
183
- "memory.wikilink.loading": "加载中…",
184
- "memory.wikilink.unresolved": "目标记忆不存在"
198
+ "memory.explorer.delete": "删除",
199
+ "memory.explorer.confirmDelete": "确认删除?",
200
+ "memory.explorer.cancel": "取消",
201
+ "memory.explorer.deleted": "已删除",
202
+ "memory.explorer.deleteFailed": "删除失败",
203
+ "memory.status.memories": "记忆总数",
204
+ "memory.status.entities": "实体",
205
+ "memory.status.vector": "向量索引",
206
+ "memory.status.vectorOn": "已启用",
207
+ "memory.status.vectorOff": "未启用",
208
+ "memory.status.llm": "LLM 消耗",
209
+ "memory.status.llmCalls": "近 7 天 · {n} 次调用",
210
+ "memory.status.error": "加载失败"
185
211
  },
186
212
  en: {
187
213
  "memory.panel.empty": "No memories yet",
@@ -191,8 +217,6 @@ window.__ModuleLoader__.load({
191
217
  "memory.tab.project": "Projects",
192
218
  "memory.tab.decision": "Decisions",
193
219
  "memory.tab.history": "History",
194
- "memory.tab.user": "User",
195
- "memory.tab.fact": "Facts",
196
220
  "memory.settings.title": "Memory Settings",
197
221
  "memory.settings.profile": "User Profile",
198
222
  "memory.settings.profileHint": "Describe yourself once — the agent reads it every turn.",
@@ -200,16 +224,6 @@ window.__ModuleLoader__.load({
200
224
  "memory.settings.profileSaved": "Profile saved",
201
225
  "memory.settings.rules": "Rules",
202
226
  "memory.settings.rulesHint": "Rules the agent must follow — add or remove anytime, effective next turn.",
203
- "memory.settings.autoTagTitle": "Auto-Tag",
204
- "memory.settings.autoTagHint": "When on, new memories are auto-tagged by a light model and the directory auto-grouped by tag (opt-in, off by default)",
205
- "memory.settings.autoTagEnabled": "Auto-tagging",
206
- "memory.settings.autoTagSave": "Save",
207
- "memory.settings.autoTagSaved": "Saved",
208
- "memory.settings.sidebarTriggerTitle": "Sidebar trigger",
209
- "memory.settings.sidebarTriggerHint": "Hide the memory entrance button at the sidebar footer (bottom-left) when it clashes with other plugins. The library stays reachable via the 记忆库 conversation tab",
210
- "memory.settings.sidebarTriggerEnabled": "Show the bottom-left trigger",
211
- "memory.settings.sidebarTriggerSave": "Save",
212
- "memory.settings.sidebarTriggerSaved": "Saved",
213
227
  "memory.settings.ruleAdd": "Add Rule",
214
228
  "memory.settings.rulePlaceholder": "e.g. Always lead with a conclusion",
215
229
  "memory.settings.commands": "Custom Commands",
@@ -225,10 +239,23 @@ window.__ModuleLoader__.load({
225
239
  "memory.view.label": "Memory",
226
240
  "memory.overlay.close": "Close",
227
241
  "memory.explorer.tabMemory": "Memories",
228
- "memory.explorer.tabGraph": "Graph",
229
- "memory.explorer.tabDirectory": "Directory",
242
+ "memory.explorer.tabEntities": "Entities",
230
243
  "memory.explorer.tabSettings": "Settings",
231
- "memory.explorer.tabOverview": "Overview",
244
+ "memory.explorer.tabStatus": "Status",
245
+ "memory.entities.count": "{n} entities",
246
+ "memory.entities.pick": "Select an entity to see its attributes and relations",
247
+ "memory.entities.none": "No entities yet — turn on entity extraction and people / projects / concepts will accumulate here",
248
+ "memory.entities.attrs": "Attributes",
249
+ "memory.entities.relations": "Relations",
250
+ "memory.entities.mentions": "{n} mentions",
251
+ "memory.entities.lastSeen": "Last seen",
252
+ "memory.entities.graph": "Relation graph",
253
+ "memory.entity.person": "People",
254
+ "memory.entity.project": "Projects",
255
+ "memory.entity.concept": "Concepts",
256
+ "memory.entity.technology": "Technologies",
257
+ "memory.entity.organization": "Organizations",
258
+ "memory.entity.other": "Others",
232
259
  "memory.explorer.search": "Search title or content…",
233
260
  "memory.explorer.searchTitle": "Semantic Search",
234
261
  "memory.explorer.types": "Types",
@@ -238,34 +265,16 @@ window.__ModuleLoader__.load({
238
265
  "memory.explorer.copy": "Copy content",
239
266
  "memory.explorer.copied": "Copied",
240
267
  "memory.explorer.refresh": "Refresh",
268
+ "memory.explorer.loadMore": "Load more",
241
269
  "memory.explorer.count": "{n} items",
242
270
  "memory.explorer.empty": "No memories yet",
243
271
  "memory.explorer.source": "Source",
244
272
  "memory.explorer.created": "Created",
245
273
  "memory.explorer.updated": "Updated",
246
274
  "memory.explorer.tags": "Tags",
247
- "memory.explorer.tagAdd": "Add tag",
248
- "memory.explorer.tagRemove": "Remove tag",
249
- "memory.explorer.tagPlaceholder": "Type a tag, press Enter",
250
- "memory.explorer.tagsEmpty": "No tags",
251
275
  "memory.explorer.importance": "Importance",
252
276
  "memory.explorer.topK": "Results limit",
253
277
  "memory.explorer.topKOption": "Return {n}",
254
- "memory.overview.empty": "No memories yet. Once you chat, the agent starts saving memories and the layered overview appears here",
255
- "memory.overview.profileEmpty": "No user-profile memories yet — ask the agent to remember things about you in chat",
256
- "memory.overview.distribution": "By Type",
257
- "memory.overview.layers": "Memory Layers",
258
- "memory.overview.trend": "Last 7 days",
259
- "memory.overview.none": "None",
260
- "memory.overview.badgeInject": "Auto-inject",
261
- "memory.overview.badgeImportance": "By importance",
262
- "memory.directory.untagged": "Untagged",
263
- "memory.directory.loading": "Loading…",
264
- "memory.directory.empty": "No memories yet",
265
- "directory.editToggle": "Edit mode",
266
- "directory.deleteConfirm": "Delete this memory?",
267
- "directory.deleteConfirmShort": "Confirm?",
268
- "directory.deleteError": "Delete failed",
269
278
  "memory.card.open": "Open full text in the Memory tab",
270
279
  "memory.time.now": "just now",
271
280
  "memory.time.seconds": "{n}s ago",
@@ -284,7 +293,7 @@ window.__ModuleLoader__.load({
284
293
  "memory.graph.sourceMemory": "View source memory",
285
294
  "memory.graph.hint": "Drag nodes to rearrange · drag the background to pan · scroll to zoom",
286
295
  "memory.graph.resetView": "Reset view",
287
- "memory.graph.viewInGraph": "View in graph",
296
+ "memory.graph.viewInGraph": "Open in entity explorer",
288
297
  "memory.graph.loading": "Loading…",
289
298
  "memory.graph.distance": "{n} hop(s) from root",
290
299
  "memory.settings.vectorTitle": "Vector Search",
@@ -303,11 +312,19 @@ window.__ModuleLoader__.load({
303
312
  "memory.settings.apiTokenPlaceholder": "Empty = no auth (default)",
304
313
  "memory.settings.apiTokenSave": "Save Token",
305
314
  "memory.settings.apiTokenSaved": "Token saved",
306
- "memory.wikilink.backlinks": "Links to this memory",
307
- "memory.wikilink.forward": "This memory links to",
308
- "memory.wikilink.empty": "No linked memories",
309
- "memory.wikilink.loading": "Loading…",
310
- "memory.wikilink.unresolved": "Target memory not found"
315
+ "memory.explorer.delete": "Delete",
316
+ "memory.explorer.confirmDelete": "Confirm delete?",
317
+ "memory.explorer.cancel": "Cancel",
318
+ "memory.explorer.deleted": "Deleted",
319
+ "memory.explorer.deleteFailed": "Delete failed",
320
+ "memory.status.memories": "Total memories",
321
+ "memory.status.entities": "Entities",
322
+ "memory.status.vector": "Vector index",
323
+ "memory.status.vectorOn": "Enabled",
324
+ "memory.status.vectorOff": "Disabled",
325
+ "memory.status.llm": "LLM Usage",
326
+ "memory.status.llmCalls": "Last 7 days · {n} calls",
327
+ "memory.status.error": "Failed to load"
311
328
  }
312
329
  };
313
330
 
@@ -375,17 +392,6 @@ window.__ModuleLoader__.load({
375
392
  ".mneme-hint{color:var(--dsw-alias-label-tertiary);padding:24px 0;text-align:center;font-size:13px}",
376
393
  ".mneme-entitychip{flex:none;height:26px;padding:0 10px;border-radius:8px;border:none;background:none;color:var(--dsw-alias-state-business-primary);cursor:pointer;font-family:inherit;font-size:12px;line-height:16px;display:inline-flex;align-items:center}",
377
394
  ".mneme-entitychip:hover{background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 10%,transparent)}",
378
- // --- tag chips in the detail meta (v0.6.2) ---
379
- ".mneme-tagrow{display:flex;flex-wrap:wrap;gap:6px;align-items:center}",
380
- ".mneme-tagchip{display:inline-flex;align-items:center;gap:4px;max-width:100%;height:22px;padding:0 4px 0 8px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base,transparent);color:var(--dsw-alias-state-business-primary);cursor:pointer;font-family:inherit;font-size:12px;line-height:16px}",
381
- ".mneme-tagchip:hover{background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 10%,transparent)}",
382
- ".mneme-tagremove{flex:none;display:inline-flex;align-items:center;justify-content:center;width:14px;height:14px;border:none;border-radius:4px;background:none;color:var(--dsw-alias-label-tertiary);cursor:pointer;font-family:inherit;font-size:12px;line-height:1;padding:0}",
383
- ".mneme-tagremove:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}",
384
- ".mneme-tagadd{border:none;background:none;color:var(--dsw-alias-label-secondary);cursor:pointer;font-family:inherit;font-size:12px;line-height:16px;padding:2px 8px;border-radius:6px;flex:none}",
385
- ".mneme-tagadd:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}",
386
- ".mneme-taginput{box-sizing:border-box;height:22px;padding:0 8px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base,transparent);color:var(--dsw-alias-label-primary);font-family:inherit;font-size:12px;outline:none;width:150px}",
387
- ".mneme-taginput:focus{border-color:var(--dsw-alias-state-business-primary)}",
388
- ".mneme-tagempty{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:16px}",
389
395
  // --- main-area memory library page ---
390
396
  ".mneme-x{flex:1;min-height:0;height:100%;width:100%;box-sizing:border-box;display:flex;flex-direction:column;background:var(--dsw-alias-bg-layer-1);--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2)}",
391
397
  ".mneme-xbar{flex:none;display:flex;align-items:center;gap:6px;border-bottom:1px solid var(--dsw-alias-border-l2);padding:0 16px}",
@@ -397,7 +403,11 @@ window.__ModuleLoader__.load({
397
403
  ".mneme-vtab.mneme-active::after{content:\"\";position:absolute;left:8px;right:8px;bottom:-1px;height:2px;border-radius:2px;background:var(--dsw-alias-state-business-primary)}",
398
404
  ".mneme-xtools{margin-left:auto;display:flex;align-items:center;gap:8px;padding:0 0 0 12px}",
399
405
  ".mneme-xcount{flex:none;font-size:12px;line-height:16px;color:var(--dsw-alias-label-tertiary);white-space:nowrap}",
400
- // --- memory explorer cards (column layout) ---
406
+ // --- three-column browse layout: hairline separators, no outer box ---
407
+ ".mneme-xmain{flex:1;min-height:0;display:flex;flex-direction:row;overflow:hidden}",
408
+ ".mneme-xside{flex:none;width:236px;min-width:0;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:8px}",
409
+ ".mneme-xside--filter{width:214px}",
410
+ ".mneme-xbrowse{flex:1;min-width:0;min-height:0;display:flex;flex-direction:column;overflow:hidden}",
401
411
  ".mneme-xrow{display:flex;align-items:center;gap:8px;flex-wrap:wrap}",
402
412
  ".mneme-xsearch{width:100%}",
403
413
  ".mneme-xselect{flex:1;min-width:0}",
@@ -422,35 +432,17 @@ window.__ModuleLoader__.load({
422
432
  ".mneme-xdot{flex:none;display:inline-block;width:8px;height:8px;border-radius:50%;background:currentColor}",
423
433
  ".mneme-xdot--all{background:transparent;border:1.5px solid currentColor;opacity:.55;box-sizing:border-box}",
424
434
  ".mneme-xitem .mneme-xdot{align-self:center}",
425
- // --- memory explorer root ---
426
- ".mneme-xmain{display:flex;flex-direction:column}",
427
- ".mneme-xfilter-bar{flex:none;height:48px;display:flex;align-items:center;gap:8px;padding:0 12px;border-bottom:1px solid var(--dsw-alias-border-l2,#ddd)}",
428
- ".mneme-xcards{flex:1;display:flex;flex-direction:row;gap:12px;padding:12px;overflow:hidden}",
429
- ".mneme-card{background:var(--dsw-alias-bg-base,#fafafa);border-radius:8px;box-shadow:0 1px 3px rgba(0,0,0,.06);padding:12px;display:flex;flex-direction:column;overflow:auto}",
430
- ".mneme-card--search{width:236px;flex:none}",
431
- ".mneme-card--tree{width:260px;flex:none}",
432
- ".mneme-card--detail{flex:1;min-width:0}",
435
+ // --- detail column (preview pane sits BELOW the timeline: the list is
436
+ // the primary scan target, the detail grows only when something is
437
+ // selected so the empty state never eats the viewport) ---
438
+ ".mneme-xdetail{flex:none;max-height:56%;min-height:0;overflow-y:auto;padding:12px 20px 16px;border-top:1px solid var(--dsw-alias-border-l2)}",
439
+ ".mneme-xtree{flex:1;min-height:0;overflow-y:auto;padding:8px 10px 28px}",
433
440
  ".mneme-xdinner{max-width:720px}",
434
441
  ".mneme-xdtitle{font-size:16px;font-weight:600;line-height:24px;color:var(--dsw-alias-label-primary);margin-bottom:10px;word-break:break-word}",
435
442
  ".mneme-xdmeta{display:flex;flex-wrap:wrap;gap:4px 14px;margin-bottom:6px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}",
436
443
  ".mneme-xdsrc{display:inline-block;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:bottom}",
437
444
  ".mneme-xdcontent{margin-top:14px;font-size:14px;line-height:1.75;color:var(--dsw-alias-label-primary);white-space:pre-wrap;word-break:break-word}",
438
445
  ".mneme-xdactions{display:flex;gap:8px;margin-top:18px}",
439
- // --- wiki-link backlinks panel (detail pane footer) ---
440
- ".mneme-backlinks{margin-top:18px;padding-top:14px;border-top:1px solid var(--dsw-alias-border-l1);display:flex;flex-direction:column;gap:12px}",
441
- ".mneme-bl-block{display:flex;flex-direction:column;gap:4px}",
442
- ".mneme-bl-head{font-size:12px;font-weight:500;color:var(--dsw-alias-label-tertiary)}",
443
- ".mneme-bl-list{display:flex;flex-wrap:wrap;gap:6px}",
444
- ".mneme-bl-link{display:inline-flex;align-items:center;gap:6px;max-width:100%;border:none;background:none;color:var(--dsw-alias-state-business-primary);cursor:pointer;font-family:inherit;font-size:13px;line-height:18px;padding:2px 8px;border-radius:6px;text-align:left}",
445
- ".mneme-bl-link:hover{background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 10%,transparent)}",
446
- ".mneme-bl-link.mneme-bl-link--dim{color:var(--dsw-alias-label-tertiary);cursor:default}",
447
- ".mneme-bl-link.mneme-bl-link--dim:hover{background:none}",
448
- ".mneme-bl-title{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
449
- ".mneme-bl-meta{flex:none;font-size:11px;color:var(--dsw-alias-label-tertiary)}",
450
- ".mneme-bl-empty{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}",
451
- // --- inline [[wiki-link]] links inside the detail content ---
452
- ".mneme-wikilink{display:inline;padding:0 2px;border:none;background:none;color:var(--dsw-alias-state-business-primary);cursor:pointer;font-family:inherit;font-size:inherit;line-height:inherit;text-decoration:underline;text-underline-offset:2px;border-radius:3px}",
453
- ".mneme-wikilink:hover{background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 8%,transparent)}",
454
446
  // --- graph sub-view (fills the content area under the tabs) ---
455
447
  ".mneme-graph{flex:1;min-height:0;width:100%;display:flex;flex-direction:column;padding:12px 16px 16px;box-sizing:border-box}",
456
448
  ".mneme-graphbar{display:flex;gap:8px;align-items:center;flex:none;margin-bottom:10px}",
@@ -472,38 +464,36 @@ window.__ModuleLoader__.load({
472
464
  ".mneme-gs-attrval{color:var(--dsw-alias-label-secondary);word-break:break-word;font-size:13px}",
473
465
  ".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}",
474
466
  ".mneme-gs-link:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}",
475
- // --- directory sub-view (v0.6.3): tag folders + memory entries ---
476
- ".mneme-directory{flex:1;min-height:0;overflow-y:auto;padding:8px 10px 28px}",
477
- ".mneme-edit-toggle{margin:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;cursor:pointer;user-select:none}",
478
- ".mneme-dir-item{position:relative;display:flex;align-items:center;gap:6px;padding:2px 6px;border-radius:4px}",
479
- ".mneme-dir-item:hover{background:rgba(127,127,127,.18)}",
480
- ".mneme-tree-line{width:14px;height:1px;border-top:1px dashed rgba(127,127,127,.5);flex:none}",
481
- ".mneme-dir-item .mneme-dir-del{opacity:0;transition:opacity .12s}",
482
- ".mneme-dir-item:hover .mneme-dir-del{opacity:1}",
483
- ".mneme-dir-del{margin-left:auto;background:none;border:none;color:#e5484d;cursor:pointer;font-size:13px;line-height:1;padding:2px 4px}",
484
- ".mneme-dir-del:hover{filter:brightness(1.25)}",
485
- ".mneme-edit-on .mneme-dir-del{opacity:1}",
486
- ".mneme-dir-del-confirm{font-weight:700;background:rgba(229,72,77,.12);border-radius:4px}",
487
- ".mneme-dir-item.mneme-dir-error{background:rgba(229,72,77,.14)}",
488
- ".mneme-dir-del-err{flex:none;font-size:12px;line-height:16px;color:#e5484d}",
489
- ".mneme-dir-ititle{background:none;border:none;padding:0;font:inherit;color:inherit;cursor:pointer;flex:1;text-align:left;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
490
- ".mneme-dir-folder{margin-bottom:4px}",
491
- ".mneme-dir-folderhead{display:flex;align-items:center;gap:6px;width:100%;padding:6px 8px;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}",
492
- ".mneme-dir-folderhead:hover{background:var(--dsw-alias-interactive-bg-hover)}",
493
- ".mneme-dir-caret{flex:none;width:10px;text-align:center;color:var(--dsw-alias-label-tertiary);font-size:12px}",
494
- ".mneme-dir-fname{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
495
- ".mneme-dir-fcount{flex:none;margin-left:auto;font-size:12px;line-height:16px;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums}",
496
- ".mneme-dir-fbody{margin:2px 0 4px 22px;padding-left:8px;border-left:1px solid var(--dsw-alias-border-l2)}",
497
- ".mneme-dir-item{display:flex;gap:8px;align-items:baseline;width:100%;padding:4px 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}",
498
- ".mneme-dir-item:hover{background:var(--dsw-alias-interactive-bg-hover)}",
499
- ".mneme-dir-item.mneme-active{background:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-primary)}",
500
- ".mneme-dir-ititle{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
501
- ".mneme-dir-itime{flex:none;font-size:12px;line-height:16px;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums}",
502
- ".mneme-dir-empty{color:var(--dsw-alias-label-tertiary);padding:24px 0;text-align:center;font-size:13px}",
503
- // --- settings sub-view ---
504
- ".mneme-xsettings{flex:1;min-height:0;overflow-y:auto;padding:8px 24px 48px;box-sizing:border-box}",
467
+ // --- entity explorer: rail + layered detail (entities attrs relations graph) ---
468
+ ".mneme-ent{flex:1;min-height:0;display:flex;overflow:hidden}",
469
+ ".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}",
470
+ ".mneme-enttype{display:flex;align-items:center;gap:6px;width:100%;padding:8px 8px 2px;border:none;background:none;color:var(--dsw-alias-label-tertiary);cursor:pointer;font-family:inherit;font-size:11px;font-weight:600;line-height:16px;text-align:left;letter-spacing:.02em}",
471
+ ".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}",
472
+ ".mneme-entitem:hover{background:var(--dsw-alias-interactive-bg-hover)}",
473
+ ".mneme-entitem.mneme-active{background:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-primary)}",
474
+ ".mneme-entname{min-width:0;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
475
+ ".mneme-entmentions{flex:none;font-size:11px;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums}",
476
+ ".mneme-entdetail{flex:1;min-width:0;min-height:0;overflow-y:auto;padding:20px 24px 32px;box-sizing:border-box}",
477
+ ".mneme-entinner{max-width:720px}",
478
+ ".mneme-enttitle{display:flex;align-items:center;gap:10px;font-size:17px;font-weight:600;line-height:24px;color:var(--dsw-alias-label-primary);word-break:break-word}",
479
+ ".mneme-enttitle .mneme-xdot{width:10px;height:10px}",
480
+ ".mneme-entmeta{display:flex;flex-wrap:wrap;gap:4px 14px;margin-top:6px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}",
481
+ ".mneme-layer{margin-top:24px}",
482
+ ".mneme-layerhead{display:flex;align-items:baseline;gap:8px;font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);margin-bottom:10px}",
483
+ ".mneme-layercount{font-size:12px;font-weight:400;color:var(--dsw-alias-label-tertiary)}",
484
+ ".mneme-attrgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:8px}",
485
+ ".mneme-attrcard{min-width:0;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:8px 12px}",
486
+ ".mneme-attrkey{font-size:11px;color:var(--dsw-alias-label-tertiary);margin-bottom:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
487
+ ".mneme-attrval{font-size:13px;line-height:19px;color:var(--dsw-alias-label-primary);word-break:break-word}",
488
+ ".mneme-relrow{display:flex;align-items:center;gap:10px;width:100%;padding:7px 10px;border:none;border-radius:10px;background:none;color:var(--dsw-alias-label-secondary);cursor:pointer;font-family:inherit;font-size:13px;line-height:20px;text-align:left}",
489
+ ".mneme-relrow:hover{background:var(--dsw-alias-interactive-bg-hover)}",
490
+ ".mneme-reltype{flex:none;font-size:11px;line-height:16px;color:var(--dsw-alias-state-business-primary);border:1px solid color-mix(in srgb,var(--dsw-alias-state-business-primary) 35%,transparent);border-radius:6px;padding:0 6px}",
491
+ ".mneme-relname{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
492
+ // --- settings: quiet stacked sections, one concern per section ---
493
+ ".mneme-set{flex:1;min-height:0;overflow-y:auto;padding:8px 24px 48px;box-sizing:border-box}",
494
+ ".mneme-set-inner{max-width:680px}",
505
495
  ".mneme-set-sec{padding:20px 0 24px;border-bottom:1px solid var(--dsw-alias-border-l2)}",
506
- ".mneme-xsettings .mneme-set-sec:last-child{border-bottom:none}",
496
+ ".mneme-set-sec:last-child{border-bottom:none}",
507
497
  ".mneme-set-title{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary);margin-bottom:4px}",
508
498
  ".mneme-set-desc{font-size:13px;line-height:19px;color:var(--dsw-alias-label-tertiary);margin-bottom:14px}",
509
499
  ".mneme-set-input{box-sizing:border-box;width:100%;height:34px;padding:0 12px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base,transparent);color:var(--dsw-alias-label-primary);font-family:inherit;font-size:13px;outline:none;margin-bottom:10px;transition:border-color .12s,box-shadow .12s}",
@@ -514,6 +504,7 @@ window.__ModuleLoader__.load({
514
504
  ".mneme-btn:hover{background:var(--dsw-alias-interactive-bg-hover)}",
515
505
  ".mneme-btn:disabled{opacity:.5;cursor:default}",
516
506
  ".mneme-saved{font-size:12px;color:var(--dsw-alias-state-success,#2a7)}",
507
+ // rule rows: numbered chips + hover-revealed delete
517
508
  ".mneme-set-row{display:flex;align-items:flex-start;gap:10px;padding:10px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;margin-bottom:8px;background:var(--dsw-alias-bg-base,transparent)}",
518
509
  ".mneme-set-idx{flex:none;width:20px;height:20px;border-radius:50%;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:18px;display:inline-flex;align-items:center;justify-content:center;font-variant-numeric:tabular-nums}",
519
510
  ".mneme-set-ruletext{flex:1;min-width:0;font-size:13px;line-height:20px;color:var(--dsw-alias-label-primary);word-break:break-word}",
@@ -523,47 +514,40 @@ window.__ModuleLoader__.load({
523
514
  ".mneme-set-cmd{flex:1;min-width:0}",
524
515
  ".mneme-set-cmdname{font-size:13px;font-weight:600;color:var(--dsw-alias-state-business-primary)}",
525
516
  ".mneme-set-cmddesc{font-size:12px;line-height:17px;color:var(--dsw-alias-label-tertiary);margin-top:1px;word-break:break-word}",
526
- ".mneme-xsettings-inner{max-width:640px}",
527
- // --- overview sub-view (layered stats dashboard, .mneme-ov-* only) ---
528
- ".mneme-ov-wrap{flex:1;min-height:0;overflow-y:auto;display:flex;flex-direction:column;gap:12px;padding:12px 16px 28px}",
529
- ".mneme-ov-profile{gap:8px}",
530
- ".mneme-ov-prow{display:flex;flex-direction:column;gap:2px;padding:4px 0;border-top:1px solid var(--dsw-alias-border-l1)}",
531
- ".mneme-ov-prow:first-of-type{border-top:none}",
532
- ".mneme-ov-ptitle{font-size:13px;font-weight:600;line-height:18px;color:var(--dsw-alias-label-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
533
- ".mneme-ov-ppreview{font-size:12px;line-height:17px;color:var(--dsw-alias-label-tertiary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
534
- ".mneme-ov-dist{display:flex;flex-direction:column;gap:6px}",
535
- ".mneme-ov-distrow{display:flex;align-items:center;gap:8px}",
536
- ".mneme-ov-distlabel{flex:none;width:76px;font-size:12px;line-height:16px;color:var(--dsw-alias-label-secondary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
537
- ".mneme-ov-distcount{flex:none;width:34px;font-size:12px;line-height:16px;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;text-align:right}",
538
- ".mneme-ov-disttrack{flex:1;min-width:0;height:8px;border-radius:4px;background:var(--dsw-alias-border-l2)}",
539
- ".mneme-ov-distbar{height:100%;min-width:2px;border-radius:4px;background:var(--dsw-alias-state-business-primary)}",
540
- ".mneme-ov-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(190px,1fr));gap:10px}",
541
- ".mneme-ov-tile{box-sizing:border-box;text-align:left;border:none;cursor:pointer;font-family:inherit;gap:6px;overflow:hidden}",
542
- ".mneme-ov-tile:hover{background:var(--dsw-alias-interactive-bg-hover)}",
543
- ".mneme-ov-tilehead{display:flex;align-items:center;gap:6px}",
544
- ".mneme-ov-tiletitle{min-width:0;font-size:13px;font-weight:600;line-height:18px;color:var(--dsw-alias-label-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
545
- ".mneme-ov-tilecount{flex:none;margin-left:auto;font-size:12px;line-height:16px;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums}",
546
- ".mneme-ov-badge{flex:none;font-size:11px;line-height:16px;padding:0 6px;border-radius:8px;background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 12%,transparent);color:var(--dsw-alias-state-business-primary)}",
547
- ".mneme-ov-badge--dim{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-tertiary)}",
548
- ".mneme-ov-tilelatest{font-size:12px;line-height:17px;color:var(--dsw-alias-label-secondary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
549
- ".mneme-ov-trend{display:flex;align-items:flex-end;gap:8px;padding:8px 4px 0}",
550
- ".mneme-ov-trendcol{flex:1;min-width:0;display:flex;flex-direction:column;align-items:center;gap:4px}",
551
- ".mneme-ov-trendbar{width:min(100%,30px);border-radius:4px 4px 0 0;background:var(--dsw-alias-state-business-primary)}",
552
- ".mneme-ov-trenddate{font-size:10px;line-height:12px;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;white-space:nowrap}",
517
+ // --- settings sub-view ---
518
+ ".mneme-set{flex:1;min-height:0;overflow-y:auto;padding:8px 24px 48px;box-sizing:border-box}",
553
519
  // --- hero fallback: full-viewport memory library when no tab ring exists ---
554
520
  // The host hides the whole conversation tab ring while a session is
555
521
  // blank (hero screen), so the sidebar entry cannot activate the tab
556
522
  // there. This surface reuses the exact MemoryExplorer UI at full size —
557
523
  // not a side drawer — so the library stays reachable from any state.
558
524
  ".mneme-overlay{position:fixed;inset:0;z-index:1000;display:flex;flex-direction:column;background:var(--dsw-alias-bg-layer-1);animation:mneme-fadein .12s ease-out}",
559
- // issue #41: overlay 顶栏右侧原本与宿主窗口标题栏的控制按钮(最小化/最大化/关闭)
560
- // 落在同一区域——宿主控制按钮浮在 web 内容最上层,抢占了 overlay"关闭"按钮的
561
- // 点击热区,导致记忆窗口无法关闭。改为左对齐(标题 + 关闭按钮并排),关闭按钮
562
- // 离开右上角宿主控制按钮区,任何窗口尺寸/宿主下都可见可点。
563
- ".mneme-overlaybar{flex:none;display:flex;align-items:center;justify-content:flex-start;gap:12px;height:44px;padding:0 12px 0 16px;border-bottom:1px solid var(--dsw-alias-border-l2)}",
525
+ ".mneme-overlaybar{flex:none;display:flex;align-items:center;justify-content:space-between;height:44px;padding:0 12px 0 16px;border-bottom:1px solid var(--dsw-alias-border-l2)}",
564
526
  ".mneme-overlaytitle{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary)}",
565
527
  ".mneme-overlaybody{flex:1;min-height:0;display:flex;flex-direction:column;overflow:hidden}",
566
- "@keyframes mneme-fadein{from{opacity:0}to{opacity:1}}"
528
+ "@keyframes mneme-fadein{from{opacity:0}to{opacity:1}}",
529
+ // --- explorer refresh: paged month tree, sticky headers, inline icons ---
530
+ ".mneme-vtabico{flex:none;opacity:.85}",
531
+ ".mneme-xsearchwrap{position:relative;flex:none}",
532
+ ".mneme-xsearchico{position:absolute;left:9px;top:50%;transform:translateY(-50%);color:var(--dsw-alias-label-tertiary);pointer-events:none}",
533
+ ".mneme-xsearchwrap .mneme-xsearch{padding-left:27px}",
534
+ ".mneme-footbtn{display:inline-flex;align-items:center;gap:4px}",
535
+ ".mneme-xmonth{position:sticky;top:0;z-index:2;background:var(--dsw-alias-bg-layer-1);display:flex;align-items:center;gap:6px}",
536
+ ".mneme-xmonthcount{margin-left:auto;color:var(--dsw-alias-label-tertiary);font-weight:400;font-variant-numeric:tabular-nums}",
537
+ ".mneme-xcaret{display:inline-flex;align-items:center;justify-content:center;color:var(--dsw-alias-label-tertiary)}",
538
+ ".mneme-xmore{display:flex;justify-content:center;align-items:center;padding:10px 0 24px;min-height:20px}",
539
+ ".mneme-xemptyico{display:block;margin:0 auto 6px;opacity:.7}",
540
+ // --- status sub-view: responsive stat-card grid (auto-fill, ~220px min) ---
541
+ ".mneme-status{flex:1;min-height:0;overflow-y:auto;padding:16px 20px 32px;box-sizing:border-box}",
542
+ ".mneme-statusgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px;max-width:960px}",
543
+ ".mneme-statuscard{min-width:0;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:14px}",
544
+ ".mneme-statusnum{font-size:24px;font-weight:600;line-height:32px;color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums}",
545
+ ".mneme-statuscap{margin-top:4px;font-size:13px;line-height:19px;color:var(--dsw-alias-label-tertiary);word-break:break-word}",
546
+ // --- destructive actions: red outline = delete, solid red = confirm ---
547
+ ".mneme-btndanger{color:var(--dsw-alias-state-error,#c33);border-color:var(--dsw-alias-state-error,#c33)}",
548
+ ".mneme-btndanger:hover{background:color-mix(in srgb,var(--dsw-alias-state-error,#c33) 12%,transparent)}",
549
+ ".mneme-btndangerconfirm{background:var(--dsw-alias-state-error,#c33);border-color:var(--dsw-alias-state-error,#c33);color:#fff}",
550
+ ".mneme-btndangerconfirm:hover{filter:brightness(.9)}"
567
551
  ].join("\n");
568
552
  if (typeof document !== "undefined" && document.querySelector(`style[data-plugin-css="${CSS_TAG}"]`) === null) {
569
553
  const tag = document.createElement("style");
@@ -599,32 +583,28 @@ window.__ModuleLoader__.load({
599
583
  project: "#22c55e",
600
584
  decision: "#3b82f6",
601
585
  summary: "#a855f7",
602
- user: "#f59e0b",
603
- fact: "#22c55e",
604
586
  history: "#94a3b8"
605
587
  };
606
588
  function memoryTypeColor(type) {
607
589
  return MEMORY_TYPE_COLORS[type] || "#94a3b8";
608
590
  }
609
591
 
610
- // Compact date for the detail meta ("2026/9/7"); the full timestamp
592
+ // Compact date for the detail meta ("2026/8/16"); the full timestamp
611
593
  // stays in the tooltip.
612
594
  function formatDateShort(value) {
613
595
  if (!value) return "—";
614
596
  const date = new Date(value);
615
597
  return Number.isNaN(date.getTime()) ? "—" : date.toLocaleDateString();
616
598
  }
599
+ // Graph canvas space: the svg's fixed viewBox. The simulation runs in
600
+ // these user units — never in CSS pixels — so node positions always land
601
+ // inside the drawn area; the browser scales the viewBox to whatever the
602
+ // element's CSS size is (small window, hidden tab, maximized #72).
603
+ const VIEW_W = 380, VIEW_H = 300;
604
+
617
605
  function nodeRadius(n) {
618
606
  // mention_count → area-ish growth, clamped so hubs stay legible.
619
- const base = 7 + Math.min(20, Math.max(1, n.mention_count ?? 1)) * 0.55;
620
- // v0.7.0 heat: hot entities render slightly larger; cold → base.
621
- if (n.heat == null) return base;
622
- return base + (Math.max(0, Math.min(1, n.heat)) - 0.5) * 4;
623
- }
624
- function nodeOpacity(n) {
625
- // v0.7.0 heat: hot entities are bright (opacity 1), cold fade to 0.4.
626
- if (n.heat == null) return 1;
627
- return 0.4 + 0.6 * Math.max(0, Math.min(1, n.heat));
607
+ return 7 + Math.min(20, Math.max(1, n.mention_count ?? 1)) * 0.55;
628
608
  }
629
609
 
630
610
  // Deterministic golden-angle spiral: no two nodes start overlapping, and
@@ -639,100 +619,17 @@ window.__ModuleLoader__.load({
639
619
  });
640
620
  }
641
621
 
642
- // --- wiki-link (v0.6.1) -------------------------------------------------
643
- // Splits detail content into literal text and clickable [[target]] /
644
- // [[display|target]] links. Mirrors src/parser/wiki-link.js: single pipe
645
- // only, empty / multi-pipe / unclosed markers stay literal text.
646
- function wikilinkSegments(text, onClick) {
647
- if (typeof text !== "string" || text.length === 0) return text;
648
- const out = [];
649
- let last = 0;
650
- let key = 0;
651
- const re = /\[\[([^\[\]]*)\]\]/g;
652
- let m;
653
- while ((m = re.exec(text)) !== null) {
654
- const parts = m[1].split("|");
655
- if (parts.length > 2) continue; // 多管道 → 非法,保留字面文本
656
- const target = (parts.length === 2 ? parts[1] : parts[0]).trim();
657
- if (!target) continue; // 空目标 → 非法
658
- if (m.index > last) out.push(text.slice(last, m.index));
659
- out.push(h("button", {
660
- key: `w${key++}`,
661
- type: "button",
662
- className: "mneme-wikilink",
663
- title: target,
664
- onClick: () => onClick(target)
665
- }, parts[0].trim() || target));
666
- last = m.index + m[0].length;
667
- }
668
- if (last < text.length) out.push(text.slice(last));
669
- return out.length ? out : text;
670
- }
671
-
672
- // Backlinks panel: the two wiki-link relation blocks under the detail pane.
673
- // Backlinks = memories whose content links to this one; forward = memories
674
- // this one links to (unresolved targets surface as dim, non-clickable).
675
- // Mounted per-selection (the surrounding Fragment keys on selected.id), so
676
- // state resets and the fetches re-run on every switch.
677
- function BacklinksPanel({ memory, t, onJump }) {
678
- const [back, setBack] = useState(null); // null = loading
679
- const [forward, setForward] = useState(null);
680
-
681
- useEffect(() => {
682
- let cancelled = false;
683
- if (!memory || !memory.id) return;
684
- apiFetch(`/api/dsh-mneme/wikilinks/backlinks?id=${encodeURIComponent(memory.id)}`)
685
- .then((r) => (r.ok ? r.json() : { backlinks: [] }))
686
- .then((j) => { if (!cancelled) setBack(Array.isArray(j.backlinks) ? j.backlinks : []); })
687
- .catch(() => { if (!cancelled) setBack([]); });
688
- apiFetch(`/api/dsh-mneme/wikilinks/forward?id=${encodeURIComponent(memory.id)}`)
689
- .then((r) => (r.ok ? r.json() : { links: [] }))
690
- .then((j) => { if (!cancelled) setForward(Array.isArray(j.links) ? j.links : []); })
691
- .catch(() => { if (!cancelled) setForward([]); });
692
- return () => { cancelled = true; };
693
- }, [memory && memory.id]);
694
-
695
- if (!memory || !memory.id) return null;
696
-
697
- const loading = back === null || forward === null;
698
- const row = (link, i) => {
699
- const canJump = !!link.id;
700
- return h("button", {
701
- key: link.id ?? `unresolved-${i}`,
702
- type: "button",
703
- className: canJump ? "mneme-bl-link" : "mneme-bl-link mneme-bl-link--dim",
704
- title: canJump ? t("memory.card.open") : t("memory.wikilink.unresolved"),
705
- disabled: !canJump,
706
- onClick: () => onJump && onJump({ id: link.id })
707
- },
708
- h("span", { className: "mneme-bl-title" }, link.title || "—"),
709
- link.type && h("span", { className: "mneme-bl-meta" }, typeLabel(t, link.type))
710
- );
711
- };
712
- const section = (label, items) =>
713
- h("div", { className: "mneme-bl-block" },
714
- h("div", { className: "mneme-bl-head" }, label),
715
- loading
716
- ? h("div", { className: "mneme-bl-empty" }, t("memory.wikilink.loading"))
717
- : items.length === 0
718
- ? h("div", { className: "mneme-bl-empty" }, t("memory.wikilink.empty"))
719
- : h("div", { className: "mneme-bl-list" }, items.map(row))
720
- );
721
-
722
- return h("div", { className: "mneme-backlinks" },
723
- section(t("memory.wikilink.backlinks"), back || []),
724
- section(t("memory.wikilink.forward"), forward || [])
725
- );
726
- }
727
-
728
- // --- Graph view: ego-graph of one entity, zero-dependency SVG force layout ---
729
- // The DSH client module table only whitelists platform modules, so a graph
730
- // library like vis-network cannot be required from a plugin. A hand-rolled
731
- // spring simulation (repulsion + edge springs + center gravity, damped) is
732
- // plenty for the ≤40 nodes the ego API returns.
733
- function GraphPanel({ t, focusEntity, onJumpMemory }) {
622
+ // --- Entity explorer: the entity-gene layers made visible ---
623
+ // Rail = the entity directory (grouped by type, mention-ranked inside a
624
+ // type). Detail = the layered structure from docs/ENTITIES.md:
625
+ // 属性 current snapshot of entity_attrs (saveAttr invalidates old rows)
626
+ // ② 关系 entity_relations touching the entity (from the 1-hop ego walk)
627
+ // ③ 关联记忆 memories recalled through the entity: prefix search
628
+ // ④ 关系图谱 the same ego graph the old graph-only tab drew
629
+ function EntityPanel({ t, focusEntity, onJumpMemory }) {
630
+ const [entities, setEntities] = useState(null); // null = still loading
631
+ const [railQuery, setRailQuery] = useState("");
734
632
  const [entityName, setEntityName] = useState(focusEntity || "");
735
- const [inputValue, setInputValue] = useState(focusEntity || "");
736
633
  const [depth, setDepth] = useState(1);
737
634
  const [data, setData] = useState(null);
738
635
  const [status, setStatus] = useState("idle"); // idle | loading | ready | notfound | error
@@ -745,14 +642,29 @@ window.__ModuleLoader__.load({
745
642
  const viewRef = useRef({ x: 0, y: 0, k: 1 }); // pan/zoom viewport, viewBox units
746
643
  const [frame, setFrame] = useState(0); // re-render tick driven by the simulation
747
644
 
645
+ useEffect(() => {
646
+ let cancelled = false;
647
+ apiFetch("/api/dsh-mneme/entities?limit=500")
648
+ .then((r) => (r.ok ? r.json() : { entities: [] }))
649
+ .then((j) => { if (!cancelled) setEntities(Array.isArray(j.entities) ? j.entities : []); })
650
+ .catch(() => { if (!cancelled) setEntities([]); });
651
+ return () => { cancelled = true; };
652
+ }, []);
653
+
748
654
  useEffect(() => {
749
655
  if (focusEntity && focusEntity !== entityName) {
750
656
  setEntityName(focusEntity);
751
- setInputValue(focusEntity);
752
657
  }
753
658
  // eslint-disable-next-line react-hooks/exhaustive-deps
754
659
  }, [focusEntity]);
755
660
 
661
+ // Auto-select the most recently seen entity so the layers are never a
662
+ // dead end on first open (the rail still allows picking another one).
663
+ useEffect(() => {
664
+ if (!entities || entityName || entities.length === 0) return;
665
+ setEntityName(entities[0].name);
666
+ }, [entities, entityName]);
667
+
756
668
  const load = useCallback(async (name, d) => {
757
669
  if (!name) { setData(null); setStatus("idle"); return; }
758
670
  setStatus("loading");
@@ -772,31 +684,32 @@ window.__ModuleLoader__.load({
772
684
 
773
685
  useEffect(() => { load(entityName, depth); }, [load, entityName, depth]);
774
686
 
775
- // Detail pane: node current attrs + entity: search for related memories.
687
+ // Layer data for the selected entity: current attrs + related memories.
776
688
  useEffect(() => {
777
689
  setAttrs([]);
778
690
  setRelated([]);
779
- if (!selected || selected.kind !== "node") return;
780
- const name = selected.node.name;
691
+ if (!entityName) return;
781
692
  let cancelled = false;
782
- apiFetch(`/api/dsh-mneme/semantic/graph/entity-attrs?entity=${encodeURIComponent(name)}`)
693
+ apiFetch(`/api/dsh-mneme/semantic/graph/entity-attrs?entity=${encodeURIComponent(entityName)}`)
783
694
  .then((r) => (r.ok ? r.json() : { attrs: [] }))
784
695
  .then((j) => { if (!cancelled) setAttrs(Array.isArray(j.attrs) ? j.attrs : []); })
785
696
  .catch(() => {});
786
- apiFetch(`/api/dsh-mneme/search?q=${encodeURIComponent("entity:" + name)}&limit=10`)
697
+ apiFetch(`/api/dsh-mneme/search?q=${encodeURIComponent("entity:" + entityName)}&limit=10`)
787
698
  .then((r) => (r.ok ? r.json() : { items: [] }))
788
699
  .then((j) => { if (!cancelled) setRelated(Array.isArray(j.items) ? j.items : []); })
789
700
  .catch(() => {});
790
701
  return () => { cancelled = true; };
791
- }, [selected]);
702
+ }, [entityName]);
792
703
 
793
704
  // Simulation: run in rAF against posRef, tick React only every few frames.
794
705
  // Re-seeds when data changes; dragging writes straight into posRef.
795
706
  useEffect(() => {
796
707
  if (!data || data.nodes.length === 0) return;
797
- const width = Math.max(280, svgRef.current?.clientWidth || 380);
798
- const height = 300;
799
- posRef.current = initialPositions(data.nodes, width, height);
708
+ // Bounds in viewBox units: measuring clientWidth here put the gravity
709
+ // center and clamp range outside the 380-wide viewBox once the window
710
+ // (or its hidden-then-shown container) got wide, and every node was
711
+ // clipped off-canvas — the maximized-invisible bug (#72).
712
+ posRef.current = initialPositions(data.nodes, VIEW_W, VIEW_H);
800
713
  const byId = new Map(posRef.current.map((n) => [n.id, n]));
801
714
  const edges = data.edges;
802
715
  let raf = 0;
@@ -829,12 +742,12 @@ window.__ModuleLoader__.load({
829
742
  let energy = 0;
830
743
  for (const node of nodes) {
831
744
  // gentle gravity toward center keeps the cloud from drifting off-canvas
832
- node.vx += (width / 2 - node.x) * 0.002;
833
- node.vy += (height / 2 - node.y) * 0.002;
745
+ node.vx += (VIEW_W / 2 - node.x) * 0.002;
746
+ node.vy += (VIEW_H / 2 - node.y) * 0.002;
834
747
  if (node.pinned || node === dragRef.current?.node) { node.vx = 0; node.vy = 0; continue; }
835
748
  node.vx *= 0.85; node.vy *= 0.85;
836
- node.x = Math.max(nodeRadius(node) + 4, Math.min(width - nodeRadius(node) - 4, node.x + node.vx));
837
- node.y = Math.max(nodeRadius(node) + 14, Math.min(height - nodeRadius(node) - 14, node.y + node.vy));
749
+ node.x = Math.max(nodeRadius(node) + 4, Math.min(VIEW_W - nodeRadius(node) - 4, node.x + node.vx));
750
+ node.y = Math.max(nodeRadius(node) + 14, Math.min(VIEW_H - nodeRadius(node) - 14, node.y + node.vy));
838
751
  energy += Math.abs(node.vx) + Math.abs(node.vy);
839
752
  }
840
753
  n++;
@@ -854,7 +767,7 @@ window.__ModuleLoader__.load({
854
767
  const origX = node.x, origY = node.y;
855
768
  const svg = svgRef.current;
856
769
  const rect = svg.getBoundingClientRect();
857
- const scale = 380 / Math.max(1, rect.width) / viewRef.current.k; // viewBox units per css px, zoom-aware
770
+ const scale = VIEW_W / Math.max(1, rect.width) / viewRef.current.k; // viewBox units per css px, zoom-aware
858
771
  const onMove = (ev) => {
859
772
  dragRef.current.moved = true;
860
773
  node.x = origX + (ev.clientX - startX) * scale;
@@ -870,6 +783,12 @@ window.__ModuleLoader__.load({
870
783
  window.addEventListener("mouseup", onUp);
871
784
  };
872
785
 
786
+ const onNodeClick = (node) => {
787
+ if (dragRef.current?.moved) return; // it was a drag, not a click
788
+ setSelected({ kind: "node", node });
789
+ };
790
+ const onEdgeClick = (edge) => setSelected({ kind: "edge", edge });
791
+
873
792
  // --- canvas pan & zoom ---
874
793
  // The viewport (translate + scale) lives in viewRef so the simulation
875
794
  // and the handlers share it without re-mounting; every mutation bumps
@@ -934,39 +853,71 @@ window.__ModuleLoader__.load({
934
853
  setFrame((f) => f + 1);
935
854
  };
936
855
 
937
- const onNodeClick = (node) => {
938
- if (dragRef.current?.moved) return; // it was a drag, not a click
939
- setSelected({ kind: "node", node });
940
- };
941
- const onEdgeClick = (edge) => setSelected({ kind: "edge", edge });
942
-
943
856
  const nodes = posRef.current;
944
857
  const nodeById = new Map(nodes.map((n) => [n.id, n]));
945
- const VIEW_W = 380, VIEW_H = 300;
946
858
 
859
+ // --- rail: entity directory grouped by type ---
860
+ const q = railQuery.trim().toLowerCase();
861
+ const visibleEntities = (entities || []).filter((e) => !q || (e.name || "").toLowerCase().includes(q));
862
+ const ENTITY_TYPE_ORDER = ["person", "project", "concept", "technology", "organization"];
863
+ const groups = [];
864
+ const byType = new Map();
865
+ for (const e of visibleEntities) {
866
+ const key = ENTITY_TYPE_ORDER.includes(e.type) ? e.type : "other";
867
+ if (!byType.has(key)) { const g = { key, items: [] }; byType.set(key, g); groups.push(g); }
868
+ byType.get(key).items.push(e);
869
+ }
870
+
871
+ const entity = entities?.find((e) => e.name === entityName) || null;
872
+ const rootEdges = (data?.edges || []).filter((e) => e.from === data?.root?.id || e.to === data?.root?.id);
873
+ const graphStatus = status === "ready" && data && data.nodes.length <= 1 ? "empty" : status;
874
+
875
+ const rail = h("div", { className: "mneme-entrail" },
876
+ h("div", { className: "mneme-xcolhead" }, t("memory.view.label")),
877
+ h("input", {
878
+ className: "mneme-search mneme-xsearch",
879
+ placeholder: t("memory.graph.placeholder"),
880
+ value: railQuery,
881
+ onChange: (e) => setRailQuery(e.target.value)
882
+ }),
883
+ entities === null && h("div", { className: "mneme-xempty" }, "…"),
884
+ entities !== null && entities.length === 0 && h("div", { className: "mneme-xempty" }, t("memory.entities.none")),
885
+ entities !== null && entities.length > 0 && h("div", { className: "mneme-xcount" },
886
+ t("memory.entities.count").replace("{n}", String(entities.length))),
887
+ groups.map((g) => h("div", { key: g.key },
888
+ h("div", { className: "mneme-enttype" },
889
+ h("span", { className: "mneme-xdot", style: { color: typeColor(g.key === "other" ? null : g.key) }, "aria-hidden": "true" }),
890
+ entityTypeLabel(t, g.key)
891
+ ),
892
+ g.items.map((e) => h("button", {
893
+ key: e.id,
894
+ className: e.name === entityName ? "mneme-entitem mneme-active" : "mneme-entitem",
895
+ onClick: () => setEntityName(e.name)
896
+ },
897
+ h("span", { className: "mneme-entname" }, e.name),
898
+ h("span", { className: "mneme-entmentions" }, `×${e.mention_count ?? 1}`)
899
+ ))
900
+ ))
901
+ );
902
+
903
+ // side card for a clicked (non-root) node or edge inside the graph layer
947
904
  const side = selected?.kind === "node"
948
905
  ? h("div", { className: "mneme-graphside" },
949
906
  h("div", { className: "mneme-gs-title" },
950
907
  h("span", null, selected.node.name),
951
908
  h("span", { className: "mneme-gs-meta" },
952
- `${typeLabel(t, selected.node.type || "concept")} · ★${selected.node.mention_count ?? 1}`)
909
+ `${entityTypeLabel(t, selected.node.type || "concept")} · ★${selected.node.mention_count ?? 1}`)
953
910
  ),
911
+ selected.node.name !== entityName && h("button", {
912
+ className: "mneme-footbtn",
913
+ onClick: () => setEntityName(selected.node.name)
914
+ }, t("memory.graph.viewInGraph")),
954
915
  attrs.length > 0 && h("div", null,
955
916
  h("div", { className: "mneme-gs-meta" }, t("memory.graph.attrs")),
956
917
  attrs.map((a, i) => h("div", { key: i, className: "mneme-gs-attr" },
957
918
  h("span", { className: "mneme-gs-attrkey" }, `${a.key}:`),
958
919
  h("span", { className: "mneme-gs-attrval" }, a.value)
959
920
  ))
960
- ),
961
- related.length > 0 && h("div", { style: { marginTop: 8 } },
962
- h("div", { className: "mneme-gs-meta" }, t("memory.graph.related")),
963
- related.map((m) => h("button", {
964
- key: m.id,
965
- type: "button",
966
- className: "mneme-gs-link",
967
- title: t("memory.card.open"),
968
- onClick: () => onJumpMemory && onJumpMemory(m)
969
- }, m.title || m.content?.slice(0, 60)))
970
921
  )
971
922
  )
972
923
  : selected?.kind === "edge"
@@ -984,77 +935,140 @@ window.__ModuleLoader__.load({
984
935
  )
985
936
  : null;
986
937
 
987
- return h("div", { className: "mneme-graph" },
988
- h("div", { className: "mneme-graphbar" },
989
- h("input", {
990
- className: "mneme-search",
991
- style: { flex: 1, minWidth: 0, maxWidth: 280 },
992
- placeholder: t("memory.graph.placeholder"),
993
- value: inputValue,
994
- onChange: (e) => setInputValue(e.target.value),
995
- onKeyDown: (e) => { if (e.key === "Enter") setEntityName(inputValue.trim()); }
996
- }),
997
- h("button", {
998
- className: depth === 2 ? "mneme-chip mneme-active" : "mneme-chip",
999
- title: t("memory.graph.depth"),
1000
- onClick: () => setDepth(depth === 1 ? 2 : 1)
1001
- }, `${depth} ${t("memory.graph.depth")}`),
1002
- h("button", {
1003
- className: "mneme-chip",
1004
- title: t("memory.graph.resetView"),
1005
- onClick: resetView
1006
- }, t("memory.graph.resetView"))
1007
- ),
1008
- status === "idle" && h("div", { className: "mneme-hint" }, t("memory.graph.empty")),
1009
- status === "loading" && h("div", { className: "mneme-hint" }, t("memory.graph.loading")),
1010
- status === "notfound" && h("div", { className: "mneme-hint" }, t("memory.graph.notFound")),
1011
- status === "error" && h("div", { className: "mneme-hint" }, t("memory.panel.empty")),
1012
- status === "ready" && (data.nodes.length <= 1
1013
- ? h("div", { className: "mneme-hint" }, t("memory.graph.empty"))
1014
- : h(react.Fragment, null,
1015
- h("svg", {
1016
- ref: svgRef,
1017
- className: "mneme-graphsvg",
1018
- viewBox: `0 0 ${VIEW_W} ${VIEW_H}`,
1019
- style: { height: 320 },
1020
- onMouseDown: onSurfaceMouseDown
1021
- },
1022
- h("g", {
1023
- transform: `translate(${viewRef.current.x},${viewRef.current.y}) scale(${viewRef.current.k})`
1024
- },
1025
- data.edges.map((e) => {
1026
- const a = nodeById.get(e.from), b = nodeById.get(e.to);
1027
- if (!a || !b) return null;
1028
- return h("line", {
1029
- key: e.id,
1030
- x1: a.x, y1: a.y, x2: b.x, y2: b.y,
1031
- className: e.memory_id ? "mneme-gedge" : "mneme-gedge mneme-gedge-dashed",
1032
- onClick: () => onEdgeClick(e)
1033
- });
1034
- }),
1035
- nodes.map((n) => h("g", {
1036
- key: n.id,
1037
- className: n.id === data.root.id ? "mneme-gnode mneme-groot" : "mneme-gnode",
1038
- transform: `translate(${n.x},${n.y})`,
1039
- onMouseDown: (e) => onNodeMouseDown(e, n),
1040
- onClick: () => onNodeClick(n),
1041
- "data-node": n.name
1042
- },
1043
- h("circle", { r: nodeRadius(n), fill: typeColor(n.type), fillOpacity: nodeOpacity(n) }),
1044
- h("text", { className: "mneme-glabel", y: nodeRadius(n) + 13 }, n.name)
1045
- ))
1046
- )
938
+ const detail = h("div", { className: "mneme-entdetail" },
939
+ h("div", { className: "mneme-entinner" },
940
+ status === "idle" && h("div", { className: "mneme-hint" }, t("memory.entities.pick")),
941
+ status === "loading" && h("div", { className: "mneme-hint" }, t("memory.graph.loading")),
942
+ status === "notfound" && h("div", { className: "mneme-hint" }, t("memory.graph.notFound")),
943
+ status === "error" && h("div", { className: "mneme-hint" }, t("memory.panel.empty")),
944
+ status === "ready" && entity && h(react.Fragment, null,
945
+ h("div", { className: "mneme-enttitle" },
946
+ h("span", { className: "mneme-xdot", style: { color: typeColor(entity.type) }, "aria-hidden": "true" }),
947
+ entity.name
948
+ ),
949
+ h("div", { className: "mneme-entmeta" },
950
+ h("span", null, entityTypeLabel(t, entity.type || "other")),
951
+ h("span", null, t("memory.entities.mentions").replace("{n}", String(entity.mention_count ?? 1))),
952
+ h("span", { title: formatDate(entity.last_seen) },
953
+ `${t("memory.entities.lastSeen")}: ${formatRelativeTime(entity.last_seen, t)}`)
954
+ ),
955
+ // ① 属性 — current snapshot (valid_until IS NULL)
956
+ h("div", { className: "mneme-layer" },
957
+ h("div", { className: "mneme-layerhead" },
958
+ t("memory.entities.attrs"),
959
+ h("span", { className: "mneme-layercount" }, String(attrs.length))
960
+ ),
961
+ attrs.length === 0
962
+ ? h("div", { className: "mneme-xempty" }, t("memory.settings.empty"))
963
+ : h("div", { className: "mneme-attrgrid" },
964
+ attrs.map((a, i) => h("div", { key: i, className: "mneme-attrcard", title: a.value },
965
+ h("div", { className: "mneme-attrkey" }, a.key),
966
+ h("div", { className: "mneme-attrval" }, a.value)
967
+ ))
968
+ )
969
+ ),
970
+ // 关系 both directions from the 1-hop ego walk
971
+ h("div", { className: "mneme-layer" },
972
+ h("div", { className: "mneme-layerhead" },
973
+ t("memory.entities.relations"),
974
+ h("span", { className: "mneme-layercount" }, String(rootEdges.length))
975
+ ),
976
+ rootEdges.length === 0
977
+ ? h("div", { className: "mneme-xempty" }, t("memory.settings.empty"))
978
+ : rootEdges.map((e) => {
979
+ const out = e.from === data.root.id;
980
+ const other = nodeById.get(out ? e.to : e.from);
981
+ const otherName = other?.name ?? (out ? e.to : e.from);
982
+ return h("button", {
983
+ key: e.id,
984
+ className: "mneme-relrow",
985
+ title: e.memory_id ? t("memory.graph.sourceMemory") : undefined,
986
+ onClick: () => { if (e.memory_id && onJumpMemory) onJumpMemory({ id: e.memory_id }); }
987
+ },
988
+ h("span", { className: "mneme-relname", style: { textAlign: "right", flex: 1 } }, entity.name),
989
+ h("span", { className: "mneme-reltype" }, e.relation_type),
990
+ h("span", { className: "mneme-relname", style: { flex: 1 } }, otherName)
991
+ );
992
+ })
993
+ ),
994
+ // 关联记忆 entity: prefix recall
995
+ related.length > 0 && h("div", { className: "mneme-layer" },
996
+ h("div", { className: "mneme-layerhead" },
997
+ t("memory.graph.related"),
998
+ h("span", { className: "mneme-layercount" }, String(related.length))
999
+ ),
1000
+ related.map((m) => h("button", {
1001
+ key: m.id,
1002
+ type: "button",
1003
+ className: "mneme-gs-link",
1004
+ title: t("memory.card.open"),
1005
+ onClick: () => onJumpMemory && onJumpMemory(m)
1006
+ }, m.title || m.content?.slice(0, 60)))
1007
+ ),
1008
+ // ④ 关系图谱 — the ego graph
1009
+ h("div", { className: "mneme-layer" },
1010
+ h("div", { className: "mneme-layerhead" },
1011
+ t("memory.entities.graph"),
1012
+ h("span", { className: "mneme-layercount" },
1013
+ h("button", {
1014
+ className: depth === 2 ? "mneme-chip mneme-active" : "mneme-chip",
1015
+ title: t("memory.graph.depth"),
1016
+ onClick: () => setDepth(depth === 1 ? 2 : 1)
1017
+ }, `${depth} ${t("memory.graph.depth")}`)),
1018
+ h("span", { className: "mneme-layercount" },
1019
+ h("button", {
1020
+ className: "mneme-chip",
1021
+ title: t("memory.graph.resetView"),
1022
+ onClick: resetView
1023
+ }, t("memory.graph.resetView")))
1047
1024
  ),
1048
- h("div", { className: "mneme-graphhint" }, t("memory.graph.hint"))
1049
- )),
1050
- side
1025
+ graphStatus === "empty"
1026
+ ? h("div", { className: "mneme-hint" }, t("memory.graph.empty"))
1027
+ : h(react.Fragment, null,
1028
+ h("svg", {
1029
+ ref: svgRef,
1030
+ className: "mneme-graphsvg",
1031
+ viewBox: `0 0 ${VIEW_W} ${VIEW_H}`,
1032
+ style: { height: 320 },
1033
+ onMouseDown: onSurfaceMouseDown
1034
+ },
1035
+ h("g", {
1036
+ transform: `translate(${viewRef.current.x},${viewRef.current.y}) scale(${viewRef.current.k})`
1037
+ },
1038
+ data.edges.map((e) => {
1039
+ const a = nodeById.get(e.from), b = nodeById.get(e.to);
1040
+ if (!a || !b) return null;
1041
+ return h("line", {
1042
+ key: e.id,
1043
+ x1: a.x, y1: a.y, x2: b.x, y2: b.y,
1044
+ className: e.memory_id ? "mneme-gedge" : "mneme-gedge mneme-gedge-dashed",
1045
+ onClick: () => onEdgeClick(e)
1046
+ });
1047
+ }),
1048
+ nodes.map((n) => h("g", {
1049
+ key: n.id,
1050
+ className: n.id === data.root.id ? "mneme-gnode mneme-groot" : "mneme-gnode",
1051
+ transform: `translate(${n.x},${n.y})`,
1052
+ onMouseDown: (e) => onNodeMouseDown(e, n),
1053
+ onClick: () => onNodeClick(n),
1054
+ "data-node": n.name
1055
+ },
1056
+ h("circle", { r: nodeRadius(n), fill: typeColor(n.type) }),
1057
+ h("text", { className: "mneme-glabel", y: nodeRadius(n) + 13 }, n.name)
1058
+ ))
1059
+ )
1060
+ ),
1061
+ h("div", { className: "mneme-graphhint" }, t("memory.graph.hint"))
1062
+ ),
1063
+ side
1064
+ )
1065
+ )
1066
+ )
1051
1067
  );
1068
+
1069
+ return h("div", { className: "mneme-ent" }, rail, detail);
1052
1070
  }
1053
1071
 
1054
- // --- Settings view: user profile, rules, custom commands, vector, token ---
1055
- const styles = {
1056
- footerButton: { padding: "4px 10px", borderRadius: 8, border: "1px solid var(--dsw-alias-border-l2, #ddd)", background: "none", cursor: "pointer", fontSize: 12, margin: "2px 8px", color: "var(--dsw-alias-label-secondary, #666)", fontFamily: "inherit" }
1057
- };
1058
1072
 
1059
1073
  function SettingsContent({ t }) {
1060
1074
  const [profile, setProfile] = react.useState("");
@@ -1072,10 +1086,6 @@ window.__ModuleLoader__.load({
1072
1086
  (typeof window !== "undefined" && window.localStorage) ? window.localStorage.getItem("dsh-mneme-api-token") || "" : ""
1073
1087
  );
1074
1088
  const [apiTokenSaved, setApiTokenSaved] = react.useState(false);
1075
- const [autoTag, setAutoTag] = react.useState(false);
1076
- const [autoTagSaved, setAutoTagSaved] = react.useState(false);
1077
- const [showSidebarTrigger, setShowSidebarTrigger] = react.useState(true);
1078
- const [sidebarTriggerSaved, setSidebarTriggerSaved] = react.useState(false);
1079
1089
 
1080
1090
  const load = react.useCallback(async () => {
1081
1091
  try {
@@ -1089,13 +1099,6 @@ window.__ModuleLoader__.load({
1089
1099
  setRules(Array.isArray(r.rules) ? r.rules : []);
1090
1100
  setCommands(Array.isArray(c.commands) ? c.commands : []);
1091
1101
  setVector(v.config || { enabled: false, baseUrl: "", apiKey: "", model: "" });
1092
- // autoTag loads independently so a config failure can't cascade-block the rest.
1093
- try {
1094
- const g = await apiFetch("/api/dsh-mneme/config").then((res) => res.json());
1095
- setAutoTag(g.config?.autoTagEnabled === true);
1096
- // issue #38: sidebar trigger defaults to visible (true).
1097
- setShowSidebarTrigger(g.config?.showSidebarTrigger !== false);
1098
- } catch { /* keep defaults */ }
1099
1102
  } catch { /* ignore */ }
1100
1103
  }, []);
1101
1104
 
@@ -1192,31 +1195,8 @@ window.__ModuleLoader__.load({
1192
1195
  } catch { /* ignore */ }
1193
1196
  }
1194
1197
 
1195
- async function saveAutoTag() {
1196
- try {
1197
- await apiFetch("/api/dsh-mneme/config", {
1198
- method: "PUT",
1199
- headers: { "Content-Type": "application/json" },
1200
- body: JSON.stringify({ autoTagEnabled: autoTag })
1201
- });
1202
- setAutoTagSaved(true);
1203
- setTimeout(() => setAutoTagSaved(false), 1500);
1204
- } catch { /* ignore */ }
1205
- }
1206
-
1207
- async function saveSidebarTrigger() {
1208
- try {
1209
- await apiFetch("/api/dsh-mneme/config", {
1210
- method: "PUT",
1211
- headers: { "Content-Type": "application/json" },
1212
- body: JSON.stringify({ showSidebarTrigger })
1213
- });
1214
- setSidebarTriggerSaved(true);
1215
- setTimeout(() => setSidebarTriggerSaved(false), 1500);
1216
- } catch { /* ignore */ }
1217
- }
1218
-
1219
1198
  return h("div", null,
1199
+ // 用户画像 — who the agent is talking to
1220
1200
  h("section", { className: "mneme-set-sec" },
1221
1201
  h("div", { className: "mneme-set-title" }, t("memory.settings.profile")),
1222
1202
  h("div", { className: "mneme-set-desc" }, t("memory.settings.profileHint")),
@@ -1231,6 +1211,7 @@ window.__ModuleLoader__.load({
1231
1211
  saved && h("span", { className: "mneme-saved" }, t("memory.settings.profileSaved"))
1232
1212
  )
1233
1213
  ),
1214
+ // 规则 — numbered rows, hover reveals the delete affordance
1234
1215
  h("section", { className: "mneme-set-sec" },
1235
1216
  h("div", { className: "mneme-set-title" }, t("memory.settings.rules")),
1236
1217
  h("div", { className: "mneme-set-desc" }, t("memory.settings.rulesHint")),
@@ -1259,6 +1240,7 @@ window.__ModuleLoader__.load({
1259
1240
  h("button", { className: "mneme-btn", onClick: addRule }, t("memory.settings.ruleAdd"))
1260
1241
  )
1261
1242
  ),
1243
+ // 自定义指令 — slash commands as titled rows
1262
1244
  h("section", { className: "mneme-set-sec" },
1263
1245
  h("div", { className: "mneme-set-title" }, t("memory.settings.commands")),
1264
1246
  h("div", { className: "mneme-set-desc" }, t("memory.settings.commandsHint")),
@@ -1289,30 +1271,7 @@ window.__ModuleLoader__.load({
1289
1271
  )
1290
1272
  )
1291
1273
  ),
1292
- h("section", { className: "mneme-set-sec" },
1293
- h("div", { className: "mneme-set-title" }, t("memory.settings.autoTagTitle")),
1294
- h("div", { className: "mneme-set-desc" }, t("memory.settings.autoTagHint")),
1295
- h("label", { style: { display: "flex", alignItems: "center", gap: 8, marginBottom: 10, fontSize: 13, color: "var(--dsw-alias-label-primary)" } },
1296
- h("input", { type: "checkbox", checked: autoTag, onChange: (e) => setAutoTag(e.target.checked) }),
1297
- h("span", null, t("memory.settings.autoTagEnabled"))
1298
- ),
1299
- h("div", { style: { display: "flex", alignItems: "center", gap: 8 } },
1300
- h("button", { className: "mneme-btn", onClick: saveAutoTag }, t("memory.settings.autoTagSave")),
1301
- autoTagSaved && h("span", { className: "mneme-saved" }, t("memory.settings.autoTagSaved"))
1302
- )
1303
- ),
1304
- h("section", { className: "mneme-set-sec" },
1305
- h("div", { className: "mneme-set-title" }, t("memory.settings.sidebarTriggerTitle")),
1306
- h("div", { className: "mneme-set-desc" }, t("memory.settings.sidebarTriggerHint")),
1307
- h("label", { style: { display: "flex", alignItems: "center", gap: 8, marginBottom: 10, fontSize: 13, color: "var(--dsw-alias-label-primary)" } },
1308
- h("input", { type: "checkbox", checked: showSidebarTrigger, onChange: (e) => setShowSidebarTrigger(e.target.checked) }),
1309
- h("span", null, t("memory.settings.sidebarTriggerEnabled"))
1310
- ),
1311
- h("div", { style: { display: "flex", alignItems: "center", gap: 8 } },
1312
- h("button", { className: "mneme-btn", onClick: saveSidebarTrigger }, t("memory.settings.sidebarTriggerSave")),
1313
- sidebarTriggerSaved && h("span", { className: "mneme-saved" }, t("memory.settings.sidebarTriggerSaved"))
1314
- )
1315
- ),
1274
+ // 向量搜索 semantic recall over an embeddings API
1316
1275
  h("section", { className: "mneme-set-sec" },
1317
1276
  h("div", { className: "mneme-set-title" }, t("memory.settings.vectorTitle")),
1318
1277
  h("div", { className: "mneme-set-desc" }, t("memory.settings.vectorHint")),
@@ -1330,6 +1289,7 @@ window.__ModuleLoader__.load({
1330
1289
  reindexMsg && h("span", { style: { fontSize: 12, color: "var(--dsw-alias-label-secondary, #666)" } }, reindexMsg)
1331
1290
  )
1332
1291
  ),
1292
+ // API Token — advanced, last
1333
1293
  h("section", { className: "mneme-set-sec" },
1334
1294
  h("div", { className: "mneme-set-title" }, t("memory.settings.apiTokenTitle")),
1335
1295
  h("div", { className: "mneme-set-desc" }, t("memory.settings.apiTokenHint")),
@@ -1377,9 +1337,6 @@ window.__ModuleLoader__.load({
1377
1337
  return matches;
1378
1338
  }
1379
1339
 
1380
- // Click the memory library tab and wait (bounded, rAF-polled) until the
1381
- // host actually marks it selected — a silent React re-render gap must not
1382
- // be mistaken for success, or the fallback would never kick in.
1383
1340
  // Click the memory library tab and wait (bounded, rAF-polled) until the
1384
1341
  // host marks it selected AND our explorer actually rendered (`.mneme-x`).
1385
1342
  // aria-selected alone cannot tell our tab from a same-labelled tab of
@@ -1454,314 +1411,179 @@ window.__ModuleLoader__.load({
1454
1411
  return tree;
1455
1412
  }
1456
1413
 
1457
- // --- directory sub-view (v0.6.3) ---
1458
- // A tag folder tree over the live memory set. One-level folders keyed by
1459
- // tag (handled on the server, getDirectory), each with a 二级 memory
1460
- // entry list (title + updated time); memories without a live tag land in
1461
- // the "untagged" folder. Collapse state lives in MemoryExplorer (lifted via
1462
- // `collapsed`), so switching tabs does not drop it. Clicking an entry jumps
1463
- // back into the browser with that memory selected (onJump = jumpToMemory).
1464
- function DirectoryPanel({ t, onJump, collapsed, setCollapsed }) {
1465
- const [dir, setDir] = useState(null); // { groups: [...], untagged: [...] }
1466
- const [status, setStatus] = useState("loading"); // loading | ready | error
1467
- const [editMode, setEditMode] = useState(false); // manual tag/delete UI gate
1468
- const [confirmingId, setConfirmingId] = useState(null); // row waiting for the 2nd confirm click
1469
- const [delErrorId, setDelErrorId] = useState(null); // row whose delete just failed
1470
- const confirmTimer = useRef(null);
1414
+ const EXPLORER_TYPES = ["preference", "project", "decision", "summary", "history"];
1415
+ const PAGE_SIZE = 100; // browse page size the tree grows by 100-row pages
1416
+
1417
+ // --- Status sub-view: a responsive grid of stat cards. Every card owns
1418
+ // its fetch, loading ("") and error state, so one failing endpoint
1419
+ // never blanks or blocks the others. ---
1420
+ function StatusCard({ t, title, loading, error, num, cap }) {
1421
+ return h("div", { className: "mneme-statuscard" },
1422
+ h("div", { className: "mneme-xcolhead" }, title),
1423
+ loading
1424
+ ? h("div", { className: "mneme-statusnum" }, "…")
1425
+ : error
1426
+ ? h("div", { className: "mneme-statuscap", style: { color: "var(--dsw-alias-state-error,#c33)" } }, t("memory.status.error"))
1427
+ : h(react.Fragment, null,
1428
+ h("div", { className: "mneme-statusnum" }, num),
1429
+ cap ? h("div", { className: "mneme-statuscap" }, cap) : null
1430
+ )
1431
+ );
1432
+ }
1471
1433
 
1434
+ // 记忆总数 — list total plus per-type subtotals; each subtotal request
1435
+ // may fail independently (rendered as "—") without sinking the card.
1436
+ function MemoriesStatusCard({ t }) {
1437
+ const [state, setState] = useState({ loading: true, error: false, total: 0, byType: [] });
1472
1438
  useEffect(() => {
1473
1439
  let cancelled = false;
1474
- setStatus("loading");
1475
- apiFetch("/api/dsh-mneme/directory")
1476
- .then((res) => (res.ok ? res.json() : null))
1477
- .then((d) => {
1478
- if (cancelled) return;
1479
- if (!d) { setDir(null); setStatus("error"); return; }
1480
- setDir({ groups: Array.isArray(d.groups) ? d.groups : [], untagged: Array.isArray(d.untagged) ? d.untagged : [] });
1481
- setStatus("ready");
1440
+ const one = (ty) =>
1441
+ apiFetch(`/api/dsh-mneme/list?limit=1&order=chrono${ty ? `&type=${encodeURIComponent(ty)}` : ""}`)
1442
+ .then((res) => { if (!res.ok) throw new Error("http"); return res.json(); });
1443
+ one()
1444
+ .then((all) => {
1445
+ if (cancelled) return null;
1446
+ const total = all.total || 0;
1447
+ return Promise.all(EXPLORER_TYPES.map((ty) =>
1448
+ one(ty).then((j) => [ty, j.total || 0]).catch(() => [ty, null])
1449
+ )).then((byType) => {
1450
+ if (!cancelled) setState({ loading: false, error: false, total, byType });
1451
+ });
1482
1452
  })
1483
- .catch(() => { if (!cancelled) { setDir(null); setStatus("error"); } });
1484
- apiFetch("/api/dsh-mneme/config")
1485
- .then((res) => (res.ok ? res.json() : null))
1486
- .then((d) => { if (!cancelled && d?.config && typeof d.config.manualTagEnabled === "boolean") setEditMode(d.config.manualTagEnabled); })
1487
- .catch(() => {});
1453
+ .catch(() => { if (!cancelled) setState({ loading: false, error: true, total: 0, byType: [] }); });
1488
1454
  return () => { cancelled = true; };
1489
1455
  }, []);
1456
+ const cap = state.byType
1457
+ .map(([ty, n]) => `${typeLabel(t, ty)} ${n === null ? "—" : n}`)
1458
+ .join(" · ");
1459
+ return h(StatusCard, {
1460
+ t,
1461
+ title: t("memory.status.memories"),
1462
+ loading: state.loading,
1463
+ error: state.error,
1464
+ num: state.total.toLocaleString(),
1465
+ cap
1466
+ });
1467
+ }
1490
1468
 
1491
- const toggle = (key) => setCollapsed((c) => ({ ...c, [key]: !c[key] }));
1492
-
1493
- const toggleEdit = () => {
1494
- const next = !editMode;
1495
- apiFetch("/api/dsh-mneme/config", {
1496
- method: "PUT",
1497
- headers: { "Content-Type": "application/json" },
1498
- body: JSON.stringify({ manualTagEnabled: next })
1499
- }).then((res) => { if (res.ok) setEditMode(next); }).catch(() => {});
1500
- };
1501
-
1502
- const resetConfirm = () => {
1503
- clearTimeout(confirmTimer.current);
1504
- setConfirmingId(null);
1505
- };
1506
-
1507
- const handleDelete = (memId, e) => {
1508
- e?.stopPropagation?.();
1509
- // 第一击进入「确认?」态,第二击才真正发 DELETE;宿主 web 环境里
1510
- // window.confirm 不可靠(可能被拦截或直接返回 false),点击会无反应,
1511
- // 所以改为面板内联两步确认,不依赖任何原生对话框。
1512
- if (confirmingId !== memId) {
1513
- setConfirmingId(memId);
1514
- clearTimeout(confirmTimer.current);
1515
- confirmTimer.current = setTimeout(() => setConfirmingId((cur) => (cur === memId ? null : cur)), 3000);
1516
- return;
1517
- }
1518
- clearTimeout(confirmTimer.current);
1519
- setConfirmingId(null);
1520
- apiFetch("/api/dsh-mneme/memories", {
1521
- method: "DELETE",
1522
- headers: { "Content-Type": "application/json" },
1523
- body: JSON.stringify({ id: memId })
1524
- })
1525
- .then((res) => (res.ok ? res.json() : null))
1526
- .then((d) => {
1527
- if (!d || d.deleted !== true) {
1528
- // 服务端未确认删除也给出可见提示,不静默吞掉
1529
- setDelErrorId(memId);
1530
- setTimeout(() => setDelErrorId((cur) => (cur === memId ? null : cur)), 3500);
1531
- return;
1469
+ // 实体 directory snapshot grouped by entity type.
1470
+ function EntitiesStatusCard({ t }) {
1471
+ const [state, setState] = useState({ loading: true, error: false, total: 0, byType: [] });
1472
+ useEffect(() => {
1473
+ let cancelled = false;
1474
+ const order = ["organization", "person", "project", "technology", "concept"];
1475
+ apiFetch("/api/dsh-mneme/entities?limit=500")
1476
+ .then((res) => { if (!res.ok) throw new Error("http"); return res.json(); })
1477
+ .then((j) => {
1478
+ if (cancelled) return;
1479
+ const list = Array.isArray(j.entities) ? j.entities : [];
1480
+ const counts = new Map();
1481
+ for (const e of list) {
1482
+ const key = order.includes(e.type) ? e.type : "other";
1483
+ counts.set(key, (counts.get(key) || 0) + 1);
1532
1484
  }
1533
- setDir((prev) => ({
1534
- groups: prev.groups.map((g) => ({ ...g, memories: g.memories.filter((m) => m.id !== memId) })),
1535
- untagged: prev.untagged.filter((m) => m.id !== memId)
1536
- }));
1485
+ const byType = order.concat("other")
1486
+ .filter((k) => counts.has(k))
1487
+ .map((k) => [k, counts.get(k)]);
1488
+ setState({ loading: false, error: false, total: list.length, byType });
1537
1489
  })
1538
- .catch(() => {
1539
- // 网络失败同样给出可见提示
1540
- setDelErrorId(memId);
1541
- setTimeout(() => setDelErrorId((cur) => (cur === memId ? null : cur)), 3500);
1542
- });
1543
- };
1544
-
1545
- const renderFolder = (name, memories, extraKey, emptyLabel) => {
1546
- const folderKey = extraKey || name;
1547
- const open = !collapsed[folderKey];
1548
- const rows = memories.map((m) => {
1549
- const confirming = confirmingId === m.id;
1550
- const err = delErrorId === m.id;
1551
- return h("div", {
1552
- key: m.id,
1553
- className: err ? "mneme-dir-item mneme-dir-error" : "mneme-dir-item",
1554
- // 点击行内其它地方取消「确认?」态
1555
- onClick: resetConfirm
1556
- },
1557
- h("span", { className: "mneme-tree-line" }),
1558
- h("button", { type: "button", className: "mneme-dir-ititle", onClick: () => onJump(m) }, m.title || m.content?.slice(0, 40)),
1559
- h("span", { className: "mneme-dir-itime" }, formatDate(m.updated_at || m.created_at)),
1560
- editMode && h("button", {
1561
- type: "button",
1562
- className: confirming ? "mneme-dir-del mneme-dir-del-confirm" : "mneme-dir-del",
1563
- "aria-label": "delete",
1564
- onClick: (e) => handleDelete(m.id, e)
1565
- }, confirming ? t("directory.deleteConfirmShort") : "✕"),
1566
- err && h("span", { className: "mneme-dir-del-err" }, t("directory.deleteError"))
1567
- );
1568
- });
1569
- return h("div", {
1570
- key: folderKey,
1571
- className: "mneme-dir-folder",
1572
- "data-expanded": String(open)
1573
- },
1574
- h("button", {
1575
- type: "button",
1576
- className: "mneme-dir-folderhead",
1577
- "aria-expanded": String(open),
1578
- onClick: () => toggle(folderKey)
1579
- },
1580
- h("span", { className: "mneme-dir-caret" }, open ? "▾" : "▸"),
1581
- h("span", { className: "mneme-dir-fname" }, `📁 ${name}`),
1582
- h("span", { className: "mneme-dir-fcount" }, String(memories.length))
1583
- ),
1584
- open && h("div", { className: "mneme-dir-fbody" },
1585
- rows.length > 0 ? rows : h("div", { className: "mneme-dir-empty" }, emptyLabel || t("memory.directory.empty"))
1586
- )
1587
- );
1588
- };
1589
-
1590
- if (status === "loading") {
1591
- return h("div", { className: "mneme-directory" }, h("div", { className: "mneme-dir-empty" }, t("memory.directory.loading")));
1592
- }
1593
- const hasAny = dir && (dir.groups.length > 0 || dir.untagged.length > 0);
1594
- if (status === "error" || !hasAny) {
1595
- return h("div", { className: "mneme-directory" }, h("div", { className: "mneme-dir-empty" }, t("memory.directory.empty")));
1596
- }
1597
- return h("div", { className: editMode ? "mneme-directory mneme-edit-on" : "mneme-directory" },
1598
- h("label", { className: "mneme-edit-toggle" },
1599
- h("input", { type: "checkbox", checked: editMode, onChange: toggleEdit }),
1600
- t("directory.editToggle")
1601
- ),
1602
- dir.groups.map((g) => renderFolder(g.tag, g.memories)),
1603
- dir.untagged.length > 0 && renderFolder(t("memory.directory.untagged"), dir.untagged, "mneme-untagged")
1604
- );
1490
+ .catch(() => { if (!cancelled) setState({ loading: false, error: true, total: 0, byType: [] }); });
1491
+ return () => { cancelled = true; };
1492
+ }, []);
1493
+ const cap = state.byType.map(([ty, n]) => `${entityTypeLabel(t, ty)} ${n}`).join(" · ");
1494
+ return h(StatusCard, {
1495
+ t,
1496
+ title: t("memory.status.entities"),
1497
+ loading: state.loading,
1498
+ error: state.error,
1499
+ num: state.total.toLocaleString(),
1500
+ cap
1501
+ });
1605
1502
  }
1606
1503
 
1607
- // --- Overview sub-view (layered stats dashboard) ---
1608
- // Card grid of every memory type (fixed display order, strays appended
1609
- // alphabetically), plus a user-profile card, a CSS-only type distribution
1610
- // bar chart and a 7-day creation trend. Data comes from /stats; each tile
1611
- // probes /list?type=X&limit=1 for a representative latest title (the list
1612
- // API sorts importance-first, so limit=1 is the type's most prominent row).
1613
- const OVERVIEW_TYPE_ORDER = ["preference", "project", "decision", "history", "user", "fact", "summary", "pattern"];
1614
- // Injection-priority tiers from service.js (summary > preference/user >
1615
- // importance): these layers ride into nearly every prompt.
1616
- const OVERVIEW_INJECT_TYPES = ["summary", "user", "preference"];
1617
-
1618
- function OverviewPanel({ t, onPickType }) {
1619
- const [stats, setStats] = useState(null); // null = loading
1620
- const [profile, setProfile] = useState(null); // user-type memories (list card)
1621
- const [latest, setLatest] = useState({}); // type -> representative memory | null
1622
-
1504
+ // 向量索引 whether semantic recall is switched on.
1505
+ function VectorStatusCard({ t }) {
1506
+ const [state, setState] = useState({ loading: true, error: false, enabled: false });
1623
1507
  useEffect(() => {
1624
1508
  let cancelled = false;
1625
- apiFetch("/api/dsh-mneme/stats?days=7")
1626
- .then((r) => (r.ok ? r.json() : null))
1627
- .then((d) => { if (!cancelled) setStats(d && d.byType ? d : { byType: {}, total: 0, recent: [] }); })
1628
- .catch(() => { if (!cancelled) setStats({ byType: {}, total: 0, recent: [] }); });
1629
- apiFetch("/api/dsh-mneme/list?type=user&limit=20")
1630
- .then((r) => (r.ok ? r.json() : { items: [] }))
1631
- .then((d) => { if (!cancelled) setProfile(Array.isArray(d.items) ? d.items : []); })
1632
- .catch(() => { if (!cancelled) setProfile([]); });
1509
+ apiFetch("/api/dsh-mneme/vector-config")
1510
+ .then((res) => { if (!res.ok) throw new Error("http"); return res.json(); })
1511
+ .then((j) => { if (!cancelled) setState({ loading: false, error: false, enabled: !!j.config?.enabled }); })
1512
+ .catch(() => { if (!cancelled) setState({ loading: false, error: true, enabled: false }); });
1633
1513
  return () => { cancelled = true; };
1634
1514
  }, []);
1515
+ return h(StatusCard, {
1516
+ t,
1517
+ title: t("memory.status.vector"),
1518
+ loading: state.loading,
1519
+ error: state.error,
1520
+ num: state.enabled ? t("memory.status.vectorOn") : t("memory.status.vectorOff"),
1521
+ cap: ""
1522
+ });
1523
+ }
1635
1524
 
1636
- const byType = stats ? stats.byType : {};
1637
- const gridTypes = stats
1638
- ? OVERVIEW_TYPE_ORDER.filter((k) => byType[k]).concat(
1639
- Object.keys(byType).filter((k) => !OVERVIEW_TYPE_ORDER.includes(k)).sort())
1640
- : [];
1641
-
1642
- // Representative-latest probe, one small request per visible layer.
1643
- // Derived purely from stats, so [stats] is the full dependency set.
1525
+ // LLM 消耗 calls + tokens over the trailing 7 days.
1526
+ function LlmStatusCard({ t }) {
1527
+ const [state, setState] = useState({ loading: true, error: false, calls: 0, tokens: 0 });
1644
1528
  useEffect(() => {
1645
- if (!stats || gridTypes.length === 0) return;
1646
1529
  let cancelled = false;
1647
- Promise.all(gridTypes.map((k) =>
1648
- apiFetch(`/api/dsh-mneme/list?type=${encodeURIComponent(k)}&limit=1`)
1649
- .then((r) => (r.ok ? r.json() : { items: [] }))
1650
- .then((d) => [k, (Array.isArray(d.items) && d.items[0]) || null])
1651
- .catch(() => [k, null])
1652
- )).then((pairs) => { if (!cancelled) setLatest(Object.fromEntries(pairs)); });
1530
+ apiFetch("/api/dsh-mneme/semantic/llm-audit/stats?days=7")
1531
+ .then((res) => { if (!res.ok) throw new Error("http"); return res.json(); })
1532
+ .then((j) => {
1533
+ if (cancelled) return;
1534
+ const s = j && typeof j === "object" ? (j.stats || j) : {};
1535
+ setState({ loading: false, error: false, calls: Number(s.total_calls ?? 0), tokens: Number(s.total_tokens ?? 0) });
1536
+ })
1537
+ .catch(() => { if (!cancelled) setState({ loading: false, error: true, calls: 0, tokens: 0 }); });
1653
1538
  return () => { cancelled = true; };
1654
- // eslint-disable-next-line react-hooks/exhaustive-deps
1655
- }, [stats]);
1656
-
1657
- if (!stats) return h("div", { className: "mneme-ov-wrap" }, h("div", { className: "mneme-hint" }, t("memory.directory.loading")));
1658
- if (stats.total === 0) return h("div", { className: "mneme-ov-wrap" }, h("div", { className: "mneme-hint" }, t("memory.overview.empty")));
1659
-
1660
- const distRow = (key) => {
1661
- const count = byType[key] || 0;
1662
- const pct = stats.total > 0 ? Math.round((count / stats.total) * 100) : 0;
1663
- return h("div", { key, className: "mneme-ov-distrow" },
1664
- h("span", { className: "mneme-ov-distlabel" }, typeLabel(t, key)),
1665
- h("span", { className: "mneme-ov-distcount" }, String(count)),
1666
- h("div", { className: "mneme-ov-disttrack" },
1667
- h("div", { className: "mneme-ov-distbar", style: { width: `${pct}%` } }))
1668
- );
1669
- };
1670
-
1671
- const tile = (key) => {
1672
- const inject = OVERVIEW_INJECT_TYPES.includes(key);
1673
- const last = latest[key];
1674
- return h("button", {
1675
- key,
1676
- type: "button",
1677
- className: "mneme-card mneme-ov-tile",
1678
- title: t("memory.card.open"),
1679
- onClick: () => onPickType && onPickType(key)
1680
- },
1681
- h("div", { className: "mneme-ov-tilehead" },
1682
- h("span", { className: "mneme-ov-tiletitle" }, typeLabel(t, key)),
1683
- h("span", { className: inject ? "mneme-ov-badge" : "mneme-ov-badge mneme-ov-badge--dim" },
1684
- inject ? t("memory.overview.badgeInject") : t("memory.overview.badgeImportance")),
1685
- h("span", { className: "mneme-ov-tilecount" }, String(byType[key] || 0))
1686
- ),
1687
- h("div", { className: "mneme-ov-tilelatest" },
1688
- (last && (last.title || (last.content || "").slice(0, 40))) || t("memory.overview.none"))
1689
- );
1690
- };
1691
-
1692
- // Trend bars scale to the busiest day; heights are fixed px so they do
1693
- // not depend on percentage resolution inside the flex columns.
1694
- const recent = Array.isArray(stats.recent) ? stats.recent : [];
1695
- const maxTrend = Math.max(1, ...recent.map((d) => d.count || 0));
1696
- const trendCol = (d) => {
1697
- const count = d.count || 0;
1698
- const px = count > 0 ? 6 + Math.round((count / maxTrend) * 90) : 0;
1699
- return h("div", {
1700
- key: d.date,
1701
- className: "mneme-ov-trendcol",
1702
- title: `${d.date} · ${t("memory.explorer.count").replace("{n}", String(count))}`
1703
- },
1704
- h("div", { className: "mneme-ov-trendbar", style: { height: `${px}px` } }),
1705
- h("div", { className: "mneme-ov-trenddate" }, (d.date || "").slice(5)) // MM-DD
1706
- );
1707
- };
1539
+ }, []);
1540
+ return h(StatusCard, {
1541
+ t,
1542
+ title: t("memory.status.llm"),
1543
+ loading: state.loading,
1544
+ error: state.error,
1545
+ num: state.tokens.toLocaleString(),
1546
+ cap: t("memory.status.llmCalls").replace("{n}", state.calls.toLocaleString())
1547
+ });
1548
+ }
1708
1549
 
1709
- return h("div", { className: "mneme-ov-wrap" },
1710
- h("div", { className: "mneme-card mneme-ov-profile", role: "region", "aria-label": t("memory.settings.profile") },
1711
- h("div", { className: "mneme-xcolhead" }, t("memory.settings.profile")),
1712
- profile === null
1713
- ? h("div", { className: "mneme-ov-ppreview" }, t("memory.directory.loading"))
1714
- : profile.length === 0
1715
- ? h("div", { className: "mneme-ov-ppreview" }, t("memory.overview.profileEmpty"))
1716
- : profile.map((m) => h("div", { key: m.id, className: "mneme-ov-prow" },
1717
- h("span", { className: "mneme-ov-ptitle" }, m.title || "—"),
1718
- h("span", { className: "mneme-ov-ppreview" }, (m.content_preview || m.content || "").slice(0, 100))
1719
- ))
1720
- ),
1721
- h("div", { className: "mneme-card", role: "region", "aria-label": t("memory.overview.distribution") },
1722
- h("div", { className: "mneme-xcolhead" }, t("memory.overview.distribution")),
1723
- h("div", { className: "mneme-ov-dist" }, gridTypes.map(distRow))
1724
- ),
1725
- h("div", { className: "mneme-xcolhead" }, t("memory.overview.layers")),
1726
- h("div", { className: "mneme-ov-grid" }, gridTypes.map(tile)),
1727
- h("div", { className: "mneme-card", role: "region", "aria-label": t("memory.overview.trend") },
1728
- h("div", { className: "mneme-xcolhead" }, t("memory.overview.trend")),
1729
- recent.length === 0
1730
- ? h("div", { className: "mneme-ov-tilelatest" }, t("memory.overview.none"))
1731
- : h("div", { className: "mneme-ov-trend" }, recent.map(trendCol))
1550
+ function StatusPanel({ t }) {
1551
+ return h("div", { className: "mneme-status" },
1552
+ h("div", { className: "mneme-statusgrid" },
1553
+ h(MemoriesStatusCard, { t }),
1554
+ h(EntitiesStatusCard, { t }),
1555
+ h(VectorStatusCard, { t }),
1556
+ h(LlmStatusCard, { t })
1732
1557
  )
1733
1558
  );
1734
1559
  }
1735
1560
 
1736
- const EXPLORER_TYPES = ["preference", "project", "decision", "summary", "history", "user", "fact"];
1737
1561
 
1738
1562
  function MemoryExplorer({ t }) {
1739
- const [view, setView] = useState("memory"); // memory | overview | directory | graph | settings
1740
- const [items, setItems] = useState([]);
1563
+ const [view, setView] = useState("memory"); // memory | entity | status | settings
1564
+ const [items, setItems] = useState([]); // loaded browse pages, chrono desc
1565
+ const [total, setTotal] = useState(0); // server count for the current type filter
1741
1566
  const [loading, setLoading] = useState(true);
1567
+ const [loadingMore, setLoadingMore] = useState(false);
1742
1568
  const [type, setType] = useState("all");
1569
+ const [minImp, setMinImp] = useState(0); // importance floor: 0 = all, else 3/4/5
1743
1570
  const [query, setQuery] = useState("");
1744
1571
  const [semantic, setSemantic] = useState(false);
1745
1572
  const [vecEnabled, setVecEnabled] = useState(false);
1746
1573
  const [searchTopK, setSearchTopK] = useState(20);
1747
1574
  const [remoteItems, setRemoteItems] = useState(null);
1748
1575
  const [selectedId, setSelectedId] = useState(null);
1576
+ const [expandedMonths, setExpandedMonths] = useState(null); // null = 仅最新一个月展开
1749
1577
  const [collapsed, setCollapsed] = useState({});
1750
- // v0.6.3 directory sub-view: its own folder-collapse state, kept in
1751
- // MemoryExplorer so switching tabs does not lose it and jumpToMemory's
1752
- // time-tree reset (setCollapsed({})) cannot clobber it.
1753
- const [dirCollapsed, setDirCollapsed] = useState({});
1754
1578
  const [copied, setCopied] = useState(false);
1755
1579
  const [reloadKey, setReloadKey] = useState(0);
1580
+ const [confirmDelete, setConfirmDelete] = useState(false); // two-step delete armed
1581
+ const [deleting, setDeleting] = useState(false);
1582
+ const [deleteError, setDeleteError] = useState(false);
1583
+ const [deleteMsg, setDeleteMsg] = useState(false); // transient "deleted" notice
1756
1584
  const [graphFocus, setGraphFocus] = useState("");
1757
- // v0.6.2 tag editing state for the detail pane. The authoritative tag set
1758
- // lives server-side (entity_attrs), so it is fetched per selected memory
1759
- // and refreshed after every write instead of trusting the list snapshot.
1760
- const [tagTags, setTagTags] = useState([]);
1761
- const [tagManual, setTagManual] = useState(true);
1762
- const [tagInput, setTagInput] = useState("");
1763
- const [tagAdding, setTagAdding] = useState(false);
1764
1585
  const itemRefs = useRef(new Map());
1586
+ const moreRef = useRef(null);
1765
1587
 
1766
1588
  useEffect(() => {
1767
1589
  apiFetch("/api/dsh-mneme/vector-config")
@@ -1770,60 +1592,112 @@ window.__ModuleLoader__.load({
1770
1592
  .catch(() => {});
1771
1593
  }, []);
1772
1594
 
1595
+ // One query string behind every browse-list fetch (initial page,
1596
+ // loadMore, auto-refresh): the type filter and the importance floor
1597
+ // always travel together, so counts and pagination stay consistent.
1598
+ const filterQS = (type === "all" ? "" : `&type=${encodeURIComponent(type)}`)
1599
+ + (minImp ? `&minImportance=${minImp}` : "");
1600
+ const listUrl = (offset) => `/api/dsh-mneme/list?limit=${PAGE_SIZE}&offset=${offset}${filterQS}&order=chrono`;
1601
+
1602
+ // Browse = paged, pure-chronological pages (order=chrono). The default
1603
+ // importance ordering would interleave months across pages and break
1604
+ // the month tree; type filters server-side so pages stay consistent
1605
+ // however large the store grows.
1773
1606
  useEffect(() => {
1774
1607
  let cancelled = false;
1775
1608
  setLoading(true);
1776
- apiFetch("/api/dsh-mneme/list?limit=500")
1777
- .then((res) => (res.ok ? res.json() : { items: [] }))
1778
- .then((d) => { if (!cancelled) { setItems(d.items || []); setLoading(false); } })
1779
- .catch(() => { if (!cancelled) { setItems([]); setLoading(false); } });
1609
+ apiFetch(listUrl(0))
1610
+ .then((res) => (res.ok ? res.json() : { items: [], total: 0 }))
1611
+ .then((d) => {
1612
+ if (cancelled) return;
1613
+ setItems(d.items || []);
1614
+ setTotal(d.total || 0);
1615
+ setExpandedMonths(null); // 新数据回到「仅最新月展开」
1616
+ setLoading(false);
1617
+ })
1618
+ .catch(() => { if (!cancelled) { setItems([]); setTotal(0); setLoading(false); } });
1780
1619
  return () => { cancelled = true; };
1781
- }, [reloadKey]);
1620
+ }, [reloadKey, filterQS]);
1621
+
1622
+ const canLoadMore = !query.trim() && !remoteItems && items.length < total;
1623
+ const loadMore = useCallback(() => {
1624
+ if (loadingMore || query.trim() || remoteItems) return;
1625
+ if (items.length >= total) return;
1626
+ setLoadingMore(true);
1627
+ apiFetch(listUrl(items.length))
1628
+ .then((res) => (res.ok ? res.json() : { items: [] }))
1629
+ .then((d) => setItems((cur) => cur.concat(d.items || [])))
1630
+ .catch(() => {})
1631
+ .finally(() => setLoadingMore(false));
1632
+ }, [items.length, total, loadingMore, query, remoteItems, filterQS]);
1782
1633
 
1783
- // Semantic search: server-side ranking replaces the client filter
1784
- // while enabled and a query is present (debounced). tag: queries always
1785
- // go server-side (searchMemories resolves them without vector support),
1786
- // so they bypass the semantic toggle and use the same search endpoint.
1634
+ // Infinite scroll: a sentinel just past the loaded rows pulls the next
1635
+ // page while browsing (search results arrive complete already).
1787
1636
  useEffect(() => {
1788
- const q = query.trim();
1789
- if ((!semantic && !q.startsWith("tag:")) || !q || q.startsWith("entity:")) { setRemoteItems(null); return; }
1637
+ const el = moreRef.current;
1638
+ if (!el || !canLoadMore) return undefined;
1639
+ const io = new IntersectionObserver((entries) => {
1640
+ if (entries.some((e) => e.isIntersecting)) loadMore();
1641
+ }, { root: el.closest(".mneme-xtree"), rootMargin: "240px" });
1642
+ io.observe(el);
1643
+ return () => io.disconnect();
1644
+ }, [canLoadMore, loadMore]);
1645
+
1646
+ // Search hits the server so matches are global — not limited to the
1647
+ // loaded pages: keyword = literal text; vector = semantic ranking
1648
+ // (the pipeline falls back to keyword when no embedder is available).
1649
+ useEffect(() => {
1650
+ const query0 = query.trim();
1651
+ if (!query0 || query0.startsWith("entity:")) { setRemoteItems(null); return; }
1790
1652
  let cancelled = false;
1791
1653
  const timer = setTimeout(() => {
1792
- apiFetch(`/api/dsh-mneme/search?q=${encodeURIComponent(q)}&mode=vector&topK=${searchTopK}`)
1654
+ const mode = semantic ? "vector" : "keyword";
1655
+ apiFetch(`/api/dsh-mneme/search?q=${encodeURIComponent(query0)}&mode=${mode}&topK=${searchTopK}`)
1793
1656
  .then((res) => (res.ok ? res.json() : { items: [] }))
1794
1657
  .then((d) => { if (!cancelled) setRemoteItems(d.items || []); })
1795
1658
  .catch(() => { if (!cancelled) setRemoteItems([]); });
1796
1659
  }, 250);
1797
1660
  return () => { cancelled = true; clearTimeout(timer); };
1798
- }, [semantic, query, searchTopK]);
1661
+ }, [query, semantic, searchTopK]);
1662
+
1663
+ // Live view: while the memory tab is open, quietly re-fetch page 1
1664
+ // every 30s (and on every activation) so fresh memories surface
1665
+ // without a manual refresh. Later pages stay as loaded.
1666
+ const refreshFirstPage = useCallback(() => {
1667
+ if (query.trim() || remoteItems) return;
1668
+ apiFetch(listUrl(0))
1669
+ .then((res) => (res.ok ? res.json() : null))
1670
+ .then((d) => {
1671
+ if (!d) return;
1672
+ setTotal(d.total || 0);
1673
+ setItems((cur) => {
1674
+ const fresh = d.items || [];
1675
+ const seen = new Set(fresh.map((m) => m.id));
1676
+ return fresh.concat(cur.filter((m) => !seen.has(m.id)));
1677
+ });
1678
+ })
1679
+ .catch(() => {});
1680
+ }, [query, remoteItems, filterQS]);
1681
+
1682
+ useEffect(() => {
1683
+ if (view !== "memory") return undefined;
1684
+ refreshFirstPage();
1685
+ const iv = setInterval(() => {
1686
+ if (document.visibilityState === "visible") refreshFirstPage();
1687
+ }, 30000);
1688
+ return () => clearInterval(iv);
1689
+ }, [view, refreshFirstPage]);
1799
1690
 
1800
1691
  useEffect(() => {
1801
1692
  if (!selectedId) return;
1802
1693
  itemRefs.current.get(selectedId)?.scrollIntoView({ block: "nearest" });
1803
1694
  }, [selectedId]);
1804
1695
 
1805
- // Load the authoritative tag set for the selected memory (entity_attrs-
1806
- // backed) plus the manualTagEnabled gate that hides editing when off.
1807
- const loadTags = (id) => {
1808
- if (!id) { setTagTags([]); setTagManual(true); setTagInput(""); return; }
1809
- apiFetch(`/api/dsh-mneme/memory/tags?id=${encodeURIComponent(id)}`)
1810
- .then((res) => (res.ok ? res.json() : null))
1811
- .then((j) => { if (j) { setTagTags(Array.isArray(j.tags) ? j.tags : []); setTagManual(j.manualTagEnabled !== false); } })
1812
- .catch(() => {});
1813
- };
1696
+ // The two-step delete arms per selection: switching or clearing the
1697
+ // target disarms it, so a stale confirm can never delete a new pick.
1814
1698
  useEffect(() => {
1815
- let cancelled = false;
1816
- if (selectedId) {
1817
- apiFetch(`/api/dsh-mneme/memory/tags?id=${encodeURIComponent(selectedId)}`)
1818
- .then((res) => (res.ok ? res.json() : null))
1819
- .then((j) => { if (!cancelled && j) { setTagTags(Array.isArray(j.tags) ? j.tags : []); setTagManual(j.manualTagEnabled !== false); } })
1820
- .catch(() => {});
1821
- } else {
1822
- setTagTags([]);
1823
- setTagManual(true);
1824
- }
1825
- setTagAdding(false);
1826
- return () => { cancelled = true; };
1699
+ setConfirmDelete(false);
1700
+ setDeleteError(false);
1827
1701
  }, [selectedId]);
1828
1702
 
1829
1703
  const q = query.trim().toLowerCase();
@@ -1833,13 +1707,12 @@ window.__ModuleLoader__.load({
1833
1707
  const entityQuery = query.trim().startsWith("entity:")
1834
1708
  ? query.trim().slice(7).trim()
1835
1709
  : "";
1710
+ // Search results were filtered server-side (global); the type rail
1711
+ // still narrows them. Browse mode serves the loaded pages as-is —
1712
+ // type filtering happened in the query.
1836
1713
  const visible = remoteItems
1837
- ? remoteItems
1838
- : items.filter((m) => {
1839
- if (type !== "all" && m.type !== type) return false;
1840
- if (!q) return true;
1841
- return (m.title || "").toLowerCase().includes(q) || (m.content || "").toLowerCase().includes(q);
1842
- });
1714
+ ? remoteItems.filter((m) => type === "all" || m.type === type)
1715
+ : items;
1843
1716
 
1844
1717
  const counts = {};
1845
1718
  for (const m of items) counts[m.type] = (counts[m.type] || 0) + 1;
@@ -1874,8 +1747,9 @@ window.__ModuleLoader__.load({
1874
1747
  }
1875
1748
  curDay.items.push(m);
1876
1749
  }
1750
+ for (const mo of months) mo.count = mo.days.reduce((s, d) => s + d.items.length, 0);
1877
1751
 
1878
- const selected = items.find((m) => m.id === selectedId) || null;
1752
+ const selected = visible.find((m) => m.id === selectedId) || null;
1879
1753
 
1880
1754
  const copyContent = () => {
1881
1755
  if (!selected) return;
@@ -1885,80 +1759,61 @@ window.__ModuleLoader__.load({
1885
1759
  );
1886
1760
  };
1887
1761
 
1888
- // v0.6.2 tag editing: a chip click jumps to a tag:xxx search (reusing
1889
- // the query-driven search flow), the + entry and per-chip × post the new
1890
- // tag set through the tags endpoint then re-fetch. A 409 from the server
1891
- // means manualTagEnabled turned offflip the gate so editing hides.
1892
- const submitTag = () => {
1893
- const tag = tagInput.trim();
1894
- if (!tag || !selectedId) return;
1895
- apiFetch("/api/dsh-mneme/memory/tags", {
1762
+ // Two-step delete (no window.confirm): the red button arms, a second
1763
+ // click commits POST /api/dsh-mneme/delete. On success the row leaves
1764
+ // every local view (browse pages + search results), the selection
1765
+ // clears and the total shrinks; failures 404 included surface as
1766
+ // a transient red note next to the actions.
1767
+ const deleteSelected = () => {
1768
+ if (!selected || deleting) return;
1769
+ const id = selected.id;
1770
+ setDeleting(true);
1771
+ setDeleteError(false);
1772
+ apiFetch("/api/dsh-mneme/delete", {
1896
1773
  method: "POST",
1897
1774
  headers: { "Content-Type": "application/json" },
1898
- body: JSON.stringify({ id: selectedId, tags: [...tagTags, tag] })
1899
- }).then((res) => {
1900
- if (res.status === 409) { setTagManual(false); setTagAdding(false); setTagInput(""); return; }
1901
- return res.json();
1902
- }).then((j) => {
1903
- if (j?.ok) { setTagAdding(false); setTagInput(""); loadTags(selectedId); }
1904
- }).catch(() => {});
1905
- };
1906
- const removeTag = (tag) => {
1907
- if (!selectedId) return;
1908
- apiFetch("/api/dsh-mneme/memory/tags", {
1909
- method: "POST",
1910
- headers: { "Content-Type": "application/json" },
1911
- body: JSON.stringify({ id: selectedId, tags: tagTags.filter((x) => x !== tag) })
1912
- }).then((res) => {
1913
- if (res.status === 409) { setTagManual(false); return; }
1914
- return res.json();
1915
- }).then((j) => {
1916
- if (j?.ok) loadTags(selectedId);
1917
- }).catch(() => {});
1918
- };
1919
- const onTagClick = (tag) => {
1920
- setType("all");
1921
- setQuery(`tag:${tag}`);
1775
+ body: JSON.stringify({ id })
1776
+ })
1777
+ .then((res) => { if (!res.ok) throw new Error("http"); })
1778
+ .then(() => {
1779
+ setItems((cur) => cur.filter((m) => m.id !== id));
1780
+ setRemoteItems((cur) => (cur ? cur.filter((m) => m.id !== id) : cur));
1781
+ setTotal((n) => Math.max(0, n - 1));
1782
+ setSelectedId(null);
1783
+ setConfirmDelete(false);
1784
+ setDeleteMsg(true);
1785
+ setTimeout(() => setDeleteMsg(false), 2500);
1786
+ })
1787
+ .catch(() => {
1788
+ setDeleteError(true);
1789
+ setTimeout(() => setDeleteError(false), 4000);
1790
+ })
1791
+ .finally(() => setDeleting(false));
1922
1792
  };
1923
1793
 
1924
1794
  // Graph → memory jump: land on the browser tab with filters reset so
1925
- // the target row is visible, selected and scrolled into view.
1795
+ // the target row is visible, selected and scrolled into view. A target
1796
+ // beyond the loaded pages renders as a single-row result set instead
1797
+ // of pulling page after page to find it.
1926
1798
  const jumpToMemory = (target) => {
1927
1799
  if (!target?.id) return;
1928
1800
  setView("memory");
1929
1801
  setType("all");
1930
1802
  setQuery("");
1931
- setCollapsed({});
1803
+ setRemoteItems(items.some((m) => m.id === target.id) ? null : [target]);
1804
+ setExpandedMonths(null);
1932
1805
  setSelectedId(target.id);
1933
1806
  };
1934
1807
 
1935
- // [[wiki-link]] click in the detail content: resolve the title to a
1936
- // memory (case-insensitive exact match) then jump to it. Unresolvable
1937
- // titles silently no-op — the link stays as display text.
1938
- const onWikilinkClick = (target) => {
1939
- if (!target) return;
1940
- apiFetch(`/api/dsh-mneme/wikilinks/resolve?title=${encodeURIComponent(target)}`)
1941
- .then((r) => (r.ok ? r.json() : null))
1942
- .then((j) => { if (j?.memory?.id) jumpToMemory(j.memory); })
1943
- .catch(() => {});
1944
- };
1945
-
1946
1808
  const openGraphFor = (name) => {
1947
1809
  setGraphFocus(name || "");
1948
- setView("graph");
1949
- };
1950
-
1951
- // Overview tile click: dive into the memory view filtered to that layer.
1952
- const pickLayerType = (key) => {
1953
- setType(key);
1954
- setView("memory");
1810
+ setView("entity");
1955
1811
  };
1956
1812
 
1957
1813
  const subviews = [
1958
1814
  { key: "memory", label: t("memory.explorer.tabMemory") },
1959
- { key: "overview", label: t("memory.explorer.tabOverview") },
1960
- { key: "directory", label: t("memory.explorer.tabDirectory") },
1961
- { key: "graph", label: t("memory.explorer.tabGraph") },
1815
+ { key: "entity", label: t("memory.explorer.tabEntities") },
1816
+ { key: "status", label: t("memory.explorer.tabStatus") },
1962
1817
  { key: "settings", label: t("memory.explorer.tabSettings") }
1963
1818
  ];
1964
1819
 
@@ -1973,26 +1828,66 @@ window.__ModuleLoader__.load({
1973
1828
  "aria-pressed": String(view === s.key),
1974
1829
  onClick: () => setView(s.key)
1975
1830
  },
1976
- s.key === "graph"
1977
- ? h(react.Fragment, null, h(GraphNodesIcon, { size: 16 }), s.label)
1978
- : s.label
1831
+ h(Icon, { name: s.key === "memory" ? "database" : s.key === "entity" ? "waypoints" : s.key === "status" ? "activity" : "settings", size: 14, className: "mneme-vtabico" }),
1832
+ s.label
1979
1833
  ))
1980
1834
  ),
1981
1835
  ),
1982
1836
  view === "memory" && h("div", { className: "mneme-xmain" },
1983
- // 1. 中间分类栏
1984
- h("div", { className: "mneme-xfilter-bar" },
1837
+ h("div", { className: "mneme-xside" },
1838
+ h("div", { className: "mneme-xcolhead" }, t("memory.explorer.searchTitle")),
1839
+ h("div", { className: "mneme-xsearchwrap" },
1840
+ h(Icon, { name: "search", size: 14, className: "mneme-xsearchico" }),
1841
+ h("input", {
1842
+ className: "mneme-search mneme-xsearch",
1843
+ placeholder: t("memory.explorer.search"),
1844
+ value: query,
1845
+ onChange: (e) => setQuery(e.target.value)
1846
+ })
1847
+ ),
1848
+ entityQuery && h("button", {
1849
+ className: "mneme-entitychip",
1850
+ style: { textAlign: "left", justifyContent: "flex-start" },
1851
+ onClick: () => openGraphFor(entityQuery)
1852
+ }, `${t("memory.graph.viewInGraph")} “${entityQuery}”`),
1853
+ h("div", { className: "mneme-xrow" },
1854
+ vecEnabled && h("button", {
1855
+ className: semantic ? "mneme-chip mneme-active" : "mneme-chip",
1856
+ title: t("memory.settings.vectorTitle"),
1857
+ onClick: () => setSemantic(!semantic)
1858
+ }, t("memory.panel.semantic")),
1859
+ h("select", {
1860
+ className: "mneme-select mneme-xselect",
1861
+ value: searchTopK,
1862
+ onChange: (e) => setSearchTopK(Number(e.target.value)),
1863
+ title: t("memory.explorer.topK")
1864
+ }, [5, 10, 20, 50].map((n) => h("option", { key: n, value: n }, t("memory.explorer.topKOption").replace("{n}", String(n)))))
1865
+ ),
1866
+ h("div", { className: "mneme-xrow" },
1867
+ h("span", { className: "mneme-xcount" }, t("memory.explorer.count").replace("{n}", String(visible.length))),
1868
+ h("button", { className: "mneme-footbtn", onClick: () => setReloadKey((k) => k + 1) },
1869
+ h(Icon, { name: "refresh", size: 12 }), t("memory.explorer.refresh"))
1870
+ )
1871
+ ),
1872
+ h("div", { className: "mneme-xside mneme-xside--filter" },
1873
+ h("div", { className: "mneme-xcolhead" }, t("memory.explorer.importance")),
1874
+ h("div", { className: "mneme-xrow" },
1875
+ [0, 3, 4, 5].map((v) =>
1876
+ h("button", {
1877
+ key: v,
1878
+ className: minImp === v ? "mneme-chip mneme-active" : "mneme-chip",
1879
+ onClick: () => setMinImp(v)
1880
+ }, v === 0 ? t("memory.tab.all") : `★${v}+`))
1881
+ ),
1985
1882
  h("div", { className: "mneme-xcolhead" }, t("memory.explorer.types")),
1986
1883
  h("button", {
1987
1884
  className: type === "all" ? "mneme-xtype mneme-active" : "mneme-xtype",
1988
- "aria-pressed": type === "all",
1989
1885
  onClick: () => setType("all")
1990
1886
  }, 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))),
1991
1887
  knownTypes.concat(extraTypes).map((key) =>
1992
1888
  h("button", {
1993
1889
  key,
1994
1890
  className: type === key ? "mneme-xtype mneme-active" : "mneme-xtype",
1995
- "aria-pressed": type === key,
1996
1891
  onClick: () => setType(key)
1997
1892
  },
1998
1893
  h("span", { className: "mneme-xdot", style: { color: memoryTypeColor(key) }, title: typeLabel(t, key), "aria-hidden": "true" }),
@@ -2000,66 +1895,41 @@ window.__ModuleLoader__.load({
2000
1895
  h("span", { className: "mneme-xcount2" }, String(counts[key]))
2001
1896
  ))
2002
1897
  ),
2003
- // 2. 底部三卡片
2004
- h("div", { className: "mneme-xcards" },
2005
- // 左卡:搜索
2006
- h("div", { className: "mneme-card mneme-card--search", role: "region", "aria-label": t("memory.explorer.searchTitle") },
2007
- h("div", { className: "mneme-xcolhead" }, t("memory.explorer.searchTitle")),
2008
- h("input", {
2009
- className: "mneme-search mneme-xsearch",
2010
- "aria-label": t("memory.explorer.search"),
2011
- placeholder: t("memory.explorer.search"),
2012
- value: query,
2013
- onChange: (e) => setQuery(e.target.value)
2014
- }),
2015
- entityQuery && h("button", {
2016
- className: "mneme-entitychip",
2017
- style: { textAlign: "left", justifyContent: "flex-start" },
2018
- onClick: () => openGraphFor(entityQuery)
2019
- }, `${t("memory.graph.viewInGraph")} “${entityQuery}”`),
2020
- h("div", { className: "mneme-xrow" },
2021
- vecEnabled && h("button", {
2022
- className: semantic ? "mneme-chip mneme-active" : "mneme-chip",
2023
- title: t("memory.settings.vectorTitle"),
2024
- onClick: () => setSemantic(!semantic)
2025
- }, t("memory.panel.semantic")),
2026
- h("select", {
2027
- className: "mneme-select mneme-xselect",
2028
- value: searchTopK,
2029
- onChange: (e) => setSearchTopK(Number(e.target.value)),
2030
- title: t("memory.explorer.topK")
2031
- }, [5, 10, 20, 50].map((n) => h("option", { key: n, value: n }, t("memory.explorer.topKOption").replace("{n}", String(n)))))
2032
- ),
2033
- h("div", { className: "mneme-xrow" },
2034
- h("span", { className: "mneme-xcount" }, t("memory.explorer.count").replace("{n}", String(visible.length))),
2035
- h("button", { className: "mneme-footbtn", onClick: () => setReloadKey((k) => k + 1) }, t("memory.explorer.refresh"))
2036
- )
2037
- ),
2038
- // 中卡:时间树
2039
- h("div", { className: "mneme-card mneme-card--tree", role: "region", "aria-label": t("memory.explorer.timeline") },
1898
+ h("div", { className: "mneme-xbrowse" },
1899
+ h("div", { className: "mneme-xtree" },
2040
1900
  h("div", { className: "mneme-xcolhead" }, t("memory.explorer.timeline")),
2041
1901
  loading
2042
1902
  ? h("div", { className: "mneme-xempty" }, "…")
2043
- : months.length === 0
2044
- ? h("div", { className: "mneme-xempty" }, t("memory.explorer.empty"))
2045
- : months.map((month) =>
2046
- h("div", { key: month.key },
1903
+ : visible.length === 0
1904
+ ? h("div", { className: "mneme-xempty" },
1905
+ h(Icon, { name: "inbox", size: 20, className: "mneme-xemptyico" }),
1906
+ t("memory.explorer.empty"))
1907
+ : months.map((month, mi) => {
1908
+ // Only the newest month starts expanded; older months
1909
+ // stay a one-line header until clicked — a several-
1910
+ // thousand-row store renders a handful of DOM nodes.
1911
+ const open = expandedMonths ? !!expandedMonths[month.key] : mi === 0;
1912
+ return h("div", { key: month.key },
2047
1913
  h("button", {
2048
1914
  className: "mneme-xmonth",
2049
- "aria-expanded": String(!collapsed[month.key]),
2050
- onClick: () => setCollapsed((c) => ({ ...c, [month.key]: !c[month.key] }))
1915
+ "aria-expanded": String(open),
1916
+ onClick: () => setExpandedMonths((c) => {
1917
+ const base = c || (months.length ? { [months[0].key]: true } : {});
1918
+ return { ...base, [month.key]: !open };
1919
+ })
2051
1920
  },
2052
- h("span", { className: "mneme-xcaret" }, collapsed[month.key] ? "" : ""),
2053
- month.label
1921
+ h("span", { className: "mneme-xcaret" }, h(Icon, { name: open ? "chevronDown" : "chevronRight", size: 12 })),
1922
+ h("span", null, month.label),
1923
+ h("span", { className: "mneme-xmonthcount" }, String(month.count))
2054
1924
  ),
2055
- !collapsed[month.key] && month.days.map((day) =>
1925
+ open && month.days.map((day) =>
2056
1926
  h("div", { key: day.key },
2057
1927
  h("button", {
2058
1928
  className: "mneme-xday",
2059
1929
  "aria-expanded": String(!collapsed[day.key]),
2060
1930
  onClick: () => setCollapsed((c) => ({ ...c, [day.key]: !c[day.key] }))
2061
1931
  },
2062
- h("span", { className: "mneme-xcaret" }, collapsed[day.key] ? "" : ""),
1932
+ h("span", { className: "mneme-xcaret" }, h(Icon, { name: collapsed[day.key] ? "chevronRight" : "chevronDown", size: 11 })),
2063
1933
  day.label
2064
1934
  ),
2065
1935
  !collapsed[day.key] && day.items.map((m) => {
@@ -2078,12 +1948,19 @@ window.__ModuleLoader__.load({
2078
1948
  h("span", { className: "mneme-xname" }, m.title || m.content?.slice(0, 40))
2079
1949
  );
2080
1950
  })
2081
- )
2082
- )
2083
- )
2084
- )),
2085
- // 右卡:详情
2086
- h("div", { className: "mneme-card mneme-card--detail", role: "region", "aria-label": t("memory.explorer.detail") },
1951
+ ))
1952
+ );
1953
+ }),
1954
+ h("div", { ref: moreRef, className: "mneme-xmore" },
1955
+ canLoadMore
1956
+ ? h("button", { className: "mneme-footbtn", disabled: loadingMore, onClick: loadMore },
1957
+ loadingMore ? "…" : `${t("memory.explorer.loadMore")}(${items.length}/${total})`)
1958
+ : (!loading && visible.length > 0 && !q && !remoteItems
1959
+ ? h("span", { className: "mneme-xcount" }, `${items.length} / ${total}`)
1960
+ : null)
1961
+ )
1962
+ ),
1963
+ h("div", { className: "mneme-xdetail" },
2087
1964
  selected
2088
1965
  ? h(react.Fragment, { key: selected.id },
2089
1966
  h("div", { className: "mneme-xdinner" },
@@ -2099,64 +1976,46 @@ window.__ModuleLoader__.load({
2099
1976
  h("span", { title: formatDate(selected.created_at) }, `${t("memory.explorer.created")}: ${formatDateShort(selected.created_at)}`),
2100
1977
  h("span", { title: formatDate(selected.updated_at) }, `${t("memory.explorer.updated")}: ${formatRelativeTime(selected.updated_at, t)}`)
2101
1978
  ),
2102
- h("div", { className: "mneme-xdmeta" },
2103
- h("span", null, t("memory.explorer.tags")),
2104
- h("div", { className: "mneme-tagrow" },
2105
- tagTags.length > 0
2106
- ? tagTags.map((tag) =>
2107
- h("span", {
2108
- key: tag,
2109
- className: "mneme-tagchip",
2110
- title: `tag:${tag}`,
2111
- onClick: () => onTagClick(tag)
2112
- },
2113
- tag,
2114
- tagManual && h("button", {
2115
- type: "button",
2116
- className: "mneme-tagremove",
2117
- "aria-label": `${t("memory.explorer.tagRemove")}: ${tag}`,
2118
- onClick: (e) => { e.stopPropagation(); removeTag(tag); }
2119
- }, "×")
2120
- )
2121
- )
2122
- : h("span", { className: "mneme-tagempty" }, t("memory.explorer.tagsEmpty")),
2123
- tagManual && (tagAdding
2124
- ? h("input", {
2125
- key: selectedId,
2126
- className: "mneme-taginput",
2127
- autoFocus: true,
2128
- value: tagInput,
2129
- placeholder: t("memory.explorer.tagPlaceholder"),
2130
- onChange: (e) => setTagInput(e.target.value),
2131
- onKeyDown: (e) => { if (e.key === "Enter") submitTag(); },
2132
- onBlur: () => { setTagAdding(false); setTagInput(""); }
2133
- })
2134
- : h("button", {
2135
- type: "button",
2136
- className: "mneme-tagadd",
2137
- onClick: () => setTagAdding(true)
2138
- }, `+ ${t("memory.explorer.tagAdd")}`))
2139
- )
2140
- ),
2141
- h("div", { className: "mneme-xdcontent" },
2142
- selected.content ? wikilinkSegments(selected.content, onWikilinkClick) : selected.content
1979
+ Array.isArray(selected.tags) && selected.tags.length > 0 && h("div", { className: "mneme-xdmeta" },
1980
+ h("span", null, `${t("memory.explorer.tags")}: ${selected.tags.join(" · ")}`)
2143
1981
  ),
1982
+ h("div", { className: "mneme-xdcontent" }, selected.content),
2144
1983
  h("div", { className: "mneme-xdactions" },
2145
1984
  h("button", { className: "mneme-footbtn", onClick: copyContent },
2146
- copied ? t("memory.explorer.copied") : t("memory.explorer.copy"))
2147
- ),
2148
- h(BacklinksPanel, { memory: selected, t, onJump: jumpToMemory })
1985
+ h(Icon, { name: copied ? "check" : "copy", size: 12 }),
1986
+ copied ? t("memory.explorer.copied") : t("memory.explorer.copy")),
1987
+ // two-step delete: red outline arms, solid red commits
1988
+ confirmDelete
1989
+ ? h(react.Fragment, null,
1990
+ h("button", {
1991
+ className: "mneme-btn mneme-btndangerconfirm",
1992
+ disabled: deleting,
1993
+ onClick: deleteSelected
1994
+ }, t("memory.explorer.confirmDelete")),
1995
+ h("button", {
1996
+ className: "mneme-btn",
1997
+ onClick: () => setConfirmDelete(false)
1998
+ }, t("memory.explorer.cancel"))
1999
+ )
2000
+ : h("button", {
2001
+ className: "mneme-btn mneme-btndanger",
2002
+ onClick: () => setConfirmDelete(true)
2003
+ }, t("memory.explorer.delete")),
2004
+ deleteError && h("span", { style: { fontSize: 12, color: "var(--dsw-alias-state-error,#c33)" } },
2005
+ t("memory.explorer.deleteFailed"))
2006
+ )
2149
2007
  )
2150
2008
  )
2151
- : h("div", { className: "mneme-xempty" }, t("memory.explorer.emptyDetail"))
2009
+ : h("div", { className: "mneme-xempty" },
2010
+ deleteMsg && h("div", { className: "mneme-saved", style: { marginBottom: 6 } }, t("memory.explorer.deleted")),
2011
+ t("memory.explorer.emptyDetail"))
2152
2012
  )
2153
2013
  )
2154
2014
  ),
2155
- view === "overview" && h(OverviewPanel, { t, onPickType: pickLayerType }),
2156
- view === "directory" && h(DirectoryPanel, { t, onJump: jumpToMemory, collapsed: dirCollapsed, setCollapsed: setDirCollapsed }),
2157
- view === "graph" && h(GraphPanel, { t, focusEntity: graphFocus, onJumpMemory: jumpToMemory }),
2158
- view === "settings" && h("div", { className: "mneme-xsettings" },
2159
- h("div", { className: "mneme-xsettings-inner" }, h(SettingsContent, { t }))
2015
+ view === "entity" && h(EntityPanel, { t, focusEntity: graphFocus, onJumpMemory: jumpToMemory }),
2016
+ view === "status" && h(StatusPanel, { t }),
2017
+ view === "settings" && h("div", { className: "mneme-set" },
2018
+ h("div", { className: "mneme-set-inner" }, h(SettingsContent, { t }))
2160
2019
  )
2161
2020
  );
2162
2021
  }
@@ -2167,22 +2026,7 @@ window.__ModuleLoader__.load({
2167
2026
  // opens the full-viewport overlay instead — the library stays reachable
2168
2027
  // from every conversation state.
2169
2028
  function SidebarTrigger({ wide, t }) {
2170
- // issue #38: the entrance button is now optional (settings-over-config
2171
- // toggle, persisted via /api/dsh-mneme/config). While the value loads we
2172
- // render the button (default = visible) so it never flashes out; if the
2173
- // user disabled it, it drops away after the config resolves. The library
2174
- // itself is unaffected — the 记忆库 conversation tab stays.
2175
- const [visible, setVisible] = useState(true);
2176
- useEffect(() => {
2177
- let cancelled = false;
2178
- apiFetch("/api/dsh-mneme/config")
2179
- .then((res) => res.json())
2180
- .then((d) => { if (!cancelled) setVisible(d.config?.showSidebarTrigger !== false); })
2181
- .catch(() => { if (!cancelled) setVisible(true); });
2182
- return () => { cancelled = true; };
2183
- }, []);
2184
2029
  const [, setOpen] = useOverlayOpen();
2185
- if (!visible) return null;
2186
2030
  return h("button", {
2187
2031
  type: "button",
2188
2032
  className: wide ? "mneme-trigger" : "mneme-trigger mneme-rail",