@modusensus/dsh-mneme 0.6.7 → 0.6.8

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 (117) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +463 -463
  3. package/cordis.patch.yml +15 -15
  4. package/lib/api.js +783 -783
  5. package/lib/client.js +1757 -1757
  6. package/lib/commands.js +64 -64
  7. package/lib/config.js +288 -288
  8. package/lib/dream/clustering.js +118 -118
  9. package/lib/dream/decisions.js +439 -439
  10. package/lib/dream/sleep.js +561 -554
  11. package/lib/dream/tag-extractor.js +156 -156
  12. package/lib/dream.js +935 -929
  13. package/lib/embedding.js +154 -154
  14. package/lib/entities/extractor.js +242 -242
  15. package/lib/hot-memory.js +53 -53
  16. package/lib/index.js +361 -361
  17. package/lib/inject.js +208 -208
  18. package/lib/local-embedder.js +282 -282
  19. package/lib/mirror.js +170 -170
  20. package/lib/parser/tag.js +59 -59
  21. package/lib/parser/wiki-link.js +38 -38
  22. package/lib/quality-filter.js +123 -123
  23. package/lib/reranker.js +218 -218
  24. package/lib/search/adaptive.js +22 -22
  25. package/lib/search/bm25.js +96 -96
  26. package/lib/search/tag-boost.js +61 -61
  27. package/lib/service.js +1726 -1726
  28. package/lib/settings.js +172 -172
  29. package/lib/store.js +2238 -2238
  30. package/lib/summarize.js +236 -236
  31. package/lib/tools.js +290 -290
  32. package/lib/vector-index.js +116 -116
  33. package/package.json +80 -80
  34. package/scripts/benchmark-embed.js +201 -201
  35. package/scripts/benchmark-recall.js +133 -133
  36. package/scripts/benchmark-rerank.js +166 -166
  37. package/scripts/e2e-dsh.js +218 -218
  38. package/scripts/stress-dsh.js +255 -255
  39. package/scripts/sync-lib.js +52 -52
  40. package/src/api.js +783 -783
  41. package/src/commands.js +64 -64
  42. package/src/config.js +288 -288
  43. package/src/dream/clustering.js +118 -118
  44. package/src/dream/decisions.js +439 -439
  45. package/src/dream/sleep.js +561 -554
  46. package/src/dream/tag-extractor.js +156 -156
  47. package/src/dream.js +935 -929
  48. package/src/embedding.js +154 -154
  49. package/src/entities/extractor.js +242 -242
  50. package/src/hot-memory.js +53 -53
  51. package/src/index.js +361 -361
  52. package/src/inject.js +208 -208
  53. package/src/local-embedder.js +282 -282
  54. package/src/mirror.js +170 -170
  55. package/src/parser/tag.js +59 -59
  56. package/src/parser/wiki-link.js +38 -38
  57. package/src/quality-filter.js +123 -123
  58. package/src/reranker.js +218 -218
  59. package/src/search/adaptive.js +22 -22
  60. package/src/search/bm25.js +96 -96
  61. package/src/search/tag-boost.js +61 -61
  62. package/src/service.js +1726 -1726
  63. package/src/settings.js +172 -172
  64. package/src/store.js +2238 -2238
  65. package/src/summarize.js +236 -236
  66. package/src/tools.js +290 -290
  67. package/src/vector-index.js +116 -116
  68. package/test/api.test.js +594 -594
  69. package/test/audit.test.js +448 -448
  70. package/test/benchmark.test.js +35 -35
  71. package/test/boundary-v0625.test.js +82 -82
  72. package/test/client.test.js +368 -368
  73. package/test/clustering.test.js +100 -100
  74. package/test/commands.test.js +69 -69
  75. package/test/config.test.js +50 -50
  76. package/test/conflict-freeze.test.js +290 -290
  77. package/test/directory.test.js +134 -134
  78. package/test/dream.test.js +903 -901
  79. package/test/entities.test.js +522 -522
  80. package/test/epistemic.test.js +298 -298
  81. package/test/fnew-0112.test.js +311 -311
  82. package/test/fnew-03.test.js +422 -422
  83. package/test/graph-api.test.js +175 -175
  84. package/test/helpers/dream-mock.js +82 -82
  85. package/test/hot-memory.test.js +174 -174
  86. package/test/inject.test.js +103 -103
  87. package/test/llm-audit.test.js +279 -279
  88. package/test/local-embedder.test.js +227 -227
  89. package/test/mirror-dirty.test.js +424 -424
  90. package/test/mirror-edit-digest.test.js +187 -187
  91. package/test/mirror-generation.test.js +499 -499
  92. package/test/mirror.test.js +249 -249
  93. package/test/normalize-decisions.test.js +120 -120
  94. package/test/peer-blockers.test.js +190 -190
  95. package/test/policy-epoch.test.js +259 -259
  96. package/test/provenance.test.js +103 -103
  97. package/test/quality-filter.test.js +118 -118
  98. package/test/reasoning-effort.test.js +199 -199
  99. package/test/recall-evals.test.js +235 -235
  100. package/test/recall-layer.test.js +315 -315
  101. package/test/receipt-chain.test.js +451 -451
  102. package/test/reflection.test.js +226 -226
  103. package/test/reranker.test.js +240 -240
  104. package/test/search-fusion.test.js +90 -90
  105. package/test/semantic.test.js +124 -124
  106. package/test/service-search.test.js +199 -199
  107. package/test/service.test.js +435 -435
  108. package/test/settings.test.js +118 -118
  109. package/test/sleep.test.js +365 -365
  110. package/test/store.test.js +436 -436
  111. package/test/stress.test.js +209 -209
  112. package/test/summarize.test.js +191 -191
  113. package/test/tag-boost.test.js +125 -125
  114. package/test/tag.test.js +312 -312
  115. package/test/tools.test.js +285 -285
  116. package/test/vector-index.test.js +221 -221
  117. package/test/wiki-link.test.js +332 -332
package/lib/client.js CHANGED
@@ -1,1757 +1,1757 @@
1
- window.__ModuleLoader__.load({
2
- id: "@modusensus/dsh-mneme",
3
- factory: (require) => {
4
- var module = { exports: {} };
5
- var exports = module.exports;
6
-
7
- let react = require("react");
8
- let primitives = require("@deepseek-ai/dsh-client-ui-primitives");
9
- let { useState, useEffect, useCallback, useRef } = react;
10
- const IconArchiveOutline20 = primitives.IconArchiveOutline20;
11
-
12
- // Portal target for the hero fallback surface. The host whitelists
13
- // react-dom for its own bundles (dsh-client-ui-trajectory requires it);
14
- // when the runtime rejects it for plugins the overlay renders in place —
15
- // position:fixed keeps it viewport-sized either way.
16
- let reactDom = null;
17
- try { reactDom = require("react-dom"); } catch { reactDom = null; }
18
-
19
- // Node-graph pictogram (three nodes joined by edges). The primitives kit
20
- // ships no network/graph icon, and its share-style glyph reads as
21
- // "share" — exactly the confusion this custom 16px replacement avoids.
22
- const GraphNodesIcon = ({ size = 16, className }) => h("svg", {
23
- width: size,
24
- height: size,
25
- className,
26
- viewBox: "0 0 16 16",
27
- fill: "none",
28
- xmlns: "http://www.w3.org/2000/svg"
29
- },
30
- h("path", {
31
- d: "M8 5.2 4.6 10.4M8 5.2l3.4 5.2M5.2 12h5.6",
32
- stroke: "currentColor",
33
- strokeWidth: "1.2",
34
- strokeLinecap: "round",
35
- strokeLinejoin: "round"
36
- }),
37
- h("circle", { cx: 8, cy: 3.4, r: 1.8, fill: "currentColor" }),
38
- h("circle", { cx: 3.6, cy: 12, r: 1.8, fill: "currentColor" }),
39
- h("circle", { cx: 12.4, cy: 12, r: 1.8, fill: "currentColor" })
40
- );
41
-
42
- // Unified API fetcher: attaches the optional apiToken (set in the settings
43
- // view, persisted in localStorage) as a Bearer header. When no token has
44
- // been configured the header is omitted and the API stays open (default).
45
- const API_TOKEN_KEY = "dsh-mneme-api-token";
46
- function apiFetch(path, opts = {}) {
47
- const token = (typeof window !== "undefined" && window.localStorage)
48
- ? window.localStorage.getItem(API_TOKEN_KEY) || ""
49
- : "";
50
- const headers = { ...(opts.headers || {}) };
51
- if (token) headers["Authorization"] = `Bearer ${token}`;
52
- return fetch(path, { ...opts, headers });
53
- }
54
-
55
- const inject = ["slots", "locale"];
56
-
57
- const NS = "memory";
58
-
59
- const dictionaries = {
60
- zh: {
61
- "memory.panel.empty": "暂无记忆条目",
62
- "memory.panel.open": "记忆",
63
- "memory.tab.all": "全部",
64
- "memory.tab.preference": "偏好",
65
- "memory.tab.project": "项目",
66
- "memory.tab.decision": "决策",
67
- "memory.tab.history": "历史",
68
- "memory.settings.title": "记忆库设置",
69
- "memory.settings.profile": "用户画像",
70
- "memory.settings.profileHint": "描述你自己(角色、背景、偏好),Agent 会在每轮遵循",
71
- "memory.settings.profileSave": "保存画像",
72
- "memory.settings.profileSaved": "画像已保存",
73
- "memory.settings.rules": "规则",
74
- "memory.settings.rulesHint": "Agent 必须遵守的行为规则,每轮注入",
75
- "memory.settings.autoTagTitle": "自动打标签",
76
- "memory.settings.autoTagHint": "开启后,新记忆由轻量模型自动打标签,目录页按标签自动分类(opt-in,默认关)",
77
- "memory.settings.autoTagEnabled": "自动打标签",
78
- "memory.settings.autoTagSave": "保存",
79
- "memory.settings.autoTagSaved": "已保存",
80
- "memory.settings.ruleAdd": "添加规则",
81
- "memory.settings.rulePlaceholder": "例如:回答时总是先给结论",
82
- "memory.settings.commands": "自定义指令",
83
- "memory.settings.commandsHint": "注册斜杠命令(/名称),触发时把指令内容交给 Agent",
84
- "memory.settings.cmdName": "命令名",
85
- "memory.settings.cmdDesc": "描述",
86
- "memory.settings.cmdInstruction": "指令内容",
87
- "memory.settings.cmdAdd": "添加命令",
88
- "memory.settings.cmdDelete": "删除",
89
- "memory.settings.empty": "暂无内容",
90
- "memory.panel.semantic": "语义",
91
- "memory.sidebar.aria": "打开记忆库",
92
- "memory.view.label": "记忆库",
93
- "memory.overlay.close": "关闭",
94
- "memory.explorer.tabMemory": "记忆",
95
- "memory.explorer.tabGraph": "图谱",
96
- "memory.explorer.tabDirectory": "目录",
97
- "memory.explorer.tabSettings": "设置",
98
- "memory.explorer.search": "搜索标题或内容…",
99
- "memory.explorer.searchTitle": "语义检索",
100
- "memory.explorer.types": "分类",
101
- "memory.explorer.timeline": "时间树",
102
- "memory.explorer.detail": "详情",
103
- "memory.explorer.emptyDetail": "在时间树中选择一条记忆查看全文",
104
- "memory.explorer.copy": "复制全文",
105
- "memory.explorer.copied": "已复制",
106
- "memory.explorer.refresh": "刷新",
107
- "memory.explorer.count": "共 {n} 条",
108
- "memory.explorer.empty": "暂无记忆条目",
109
- "memory.explorer.source": "来源",
110
- "memory.explorer.created": "创建",
111
- "memory.explorer.updated": "更新",
112
- "memory.explorer.tags": "标签",
113
- "memory.explorer.tagAdd": "添加标签",
114
- "memory.explorer.tagRemove": "移除标签",
115
- "memory.explorer.tagPlaceholder": "输入标签,回车添加",
116
- "memory.explorer.tagsEmpty": "暂无标签",
117
- "memory.explorer.importance": "重要性",
118
- "memory.explorer.topK": "返回数量",
119
- "memory.explorer.topKOption": "返回 {n} 条",
120
- "memory.directory.untagged": "无标签",
121
- "memory.directory.loading": "加载中…",
122
- "memory.directory.empty": "暂无记忆条目",
123
- "directory.editToggle": "编辑模式(显示删除)",
124
- "directory.deleteConfirm": "确定删除这条记忆吗?",
125
- "memory.card.open": "在主区记忆库中查看全文",
126
- "memory.time.now": "刚刚",
127
- "memory.time.seconds": "{n}秒前",
128
- "memory.time.minutes": "{n}分钟前",
129
- "memory.time.hours": "{n}小时前",
130
- "memory.time.days": "{n}天前",
131
- "memory.graph.title": "记忆图谱",
132
- "memory.graph.aria": "记忆图谱",
133
- "memory.graph.placeholder": "输入实体名,查看关联网络…",
134
- "memory.graph.depth": "跳数",
135
- "memory.graph.empty": "图谱待积累:随对话记忆的沉淀,实体与关系会自动进入图谱",
136
- "memory.graph.notFound": "未找到该实体",
137
- "memory.graph.attrs": "属性",
138
- "memory.graph.related": "关联记忆",
139
- "memory.graph.relation": "关系",
140
- "memory.graph.sourceMemory": "查看来源记忆",
141
- "memory.graph.hint": "点击节点展开 · 拖拽调整布局",
142
- "memory.graph.viewInGraph": "在图谱中查看",
143
- "memory.graph.loading": "加载中…",
144
- "memory.graph.distance": "距中心 {n} 跳",
145
- "memory.settings.vectorTitle": "向量搜索",
146
- "memory.settings.vectorHint": "接入 OpenAI 兼容的 embeddings API 做语义搜索,可匹配字面不同但语义相近的记忆",
147
- "memory.settings.vectorEnabled": "启用向量搜索",
148
- "memory.settings.vectorBaseUrl": "API 地址 (Base URL)",
149
- "memory.settings.vectorApiKey": "API Key",
150
- "memory.settings.vectorModel": "模型名",
151
- "memory.settings.vectorSave": "保存配置",
152
- "memory.settings.vectorSaved": "配置已保存",
153
- "memory.settings.vectorReindex": "重建索引",
154
- "memory.settings.vectorReindexing": "索引中…",
155
- "memory.settings.vectorReindexDone": "已索引 {n} 条",
156
- "memory.settings.apiTokenTitle": "API Token(可选)",
157
- "memory.settings.apiTokenHint": "设置后,写操作与密钥接口(画像/规则/命令/向量配置)需携带 Authorization: Bearer <token>;面板只读操作不受影响。清空并保存可关闭鉴权。",
158
- "memory.settings.apiTokenPlaceholder": "留空 = 不鉴权(默认)",
159
- "memory.settings.apiTokenSave": "保存 Token",
160
- "memory.settings.apiTokenSaved": "Token 已保存",
161
- "memory.wikilink.backlinks": "链接到此记忆",
162
- "memory.wikilink.forward": "此记忆链接到",
163
- "memory.wikilink.empty": "暂无关联记忆",
164
- "memory.wikilink.loading": "加载中…",
165
- "memory.wikilink.unresolved": "目标记忆不存在"
166
- },
167
- en: {
168
- "memory.panel.empty": "No memories yet",
169
- "memory.panel.open": "Memory",
170
- "memory.tab.all": "All",
171
- "memory.tab.preference": "Preferences",
172
- "memory.tab.project": "Projects",
173
- "memory.tab.decision": "Decisions",
174
- "memory.tab.history": "History",
175
- "memory.settings.title": "Memory Settings",
176
- "memory.settings.profile": "User Profile",
177
- "memory.settings.profileHint": "Describe yourself — the agent follows this every turn",
178
- "memory.settings.profileSave": "Save Profile",
179
- "memory.settings.profileSaved": "Profile saved",
180
- "memory.settings.rules": "Rules",
181
- "memory.settings.rulesHint": "Behavior rules the agent must follow every turn",
182
- "memory.settings.autoTagTitle": "Auto-Tag",
183
- "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)",
184
- "memory.settings.autoTagEnabled": "Auto-tagging",
185
- "memory.settings.autoTagSave": "Save",
186
- "memory.settings.autoTagSaved": "Saved",
187
- "memory.settings.ruleAdd": "Add Rule",
188
- "memory.settings.rulePlaceholder": "e.g. Always lead with a conclusion",
189
- "memory.settings.commands": "Custom Commands",
190
- "memory.settings.commandsHint": "Register slash commands (/name) whose instruction is handed to the agent",
191
- "memory.settings.cmdName": "Name",
192
- "memory.settings.cmdDesc": "Description",
193
- "memory.settings.cmdInstruction": "Instruction",
194
- "memory.settings.cmdAdd": "Add Command",
195
- "memory.settings.cmdDelete": "Delete",
196
- "memory.settings.empty": "Nothing yet",
197
- "memory.panel.semantic": "Semantic",
198
- "memory.sidebar.aria": "Open memory panel",
199
- "memory.view.label": "Memory",
200
- "memory.overlay.close": "Close",
201
- "memory.explorer.tabMemory": "Memories",
202
- "memory.explorer.tabGraph": "Graph",
203
- "memory.explorer.tabDirectory": "Directory",
204
- "memory.explorer.tabSettings": "Settings",
205
- "memory.explorer.search": "Search title or content…",
206
- "memory.explorer.searchTitle": "Semantic Search",
207
- "memory.explorer.types": "Types",
208
- "memory.explorer.timeline": "Timeline",
209
- "memory.explorer.detail": "Details",
210
- "memory.explorer.emptyDetail": "Select a memory in the timeline to read it",
211
- "memory.explorer.copy": "Copy content",
212
- "memory.explorer.copied": "Copied",
213
- "memory.explorer.refresh": "Refresh",
214
- "memory.explorer.count": "{n} items",
215
- "memory.explorer.empty": "No memories yet",
216
- "memory.explorer.source": "Source",
217
- "memory.explorer.created": "Created",
218
- "memory.explorer.updated": "Updated",
219
- "memory.explorer.tags": "Tags",
220
- "memory.explorer.tagAdd": "Add tag",
221
- "memory.explorer.tagRemove": "Remove tag",
222
- "memory.explorer.tagPlaceholder": "Type a tag, press Enter",
223
- "memory.explorer.tagsEmpty": "No tags",
224
- "memory.explorer.importance": "Importance",
225
- "memory.explorer.topK": "Results limit",
226
- "memory.explorer.topKOption": "Return {n}",
227
- "memory.directory.untagged": "Untagged",
228
- "memory.directory.loading": "Loading…",
229
- "memory.directory.empty": "No memories yet",
230
- "directory.editToggle": "Edit mode",
231
- "directory.deleteConfirm": "Delete this memory?",
232
- "memory.card.open": "Open full text in the Memory tab",
233
- "memory.time.now": "just now",
234
- "memory.time.seconds": "{n}s ago",
235
- "memory.time.minutes": "{n}m ago",
236
- "memory.time.hours": "{n}h ago",
237
- "memory.time.days": "{n}d ago",
238
- "memory.graph.title": "Memory Graph",
239
- "memory.graph.aria": "Memory graph",
240
- "memory.graph.placeholder": "Type an entity name to see its network…",
241
- "memory.graph.depth": "hops",
242
- "memory.graph.empty": "The graph is waiting for data: entities and relations accumulate as memories are extracted",
243
- "memory.graph.notFound": "Entity not found",
244
- "memory.graph.attrs": "Attributes",
245
- "memory.graph.related": "Related memories",
246
- "memory.graph.relation": "Relation",
247
- "memory.graph.sourceMemory": "View source memory",
248
- "memory.graph.hint": "Click a node to expand · drag to rearrange",
249
- "memory.graph.viewInGraph": "View in graph",
250
- "memory.graph.loading": "Loading…",
251
- "memory.graph.distance": "{n} hop(s) from root",
252
- "memory.settings.vectorTitle": "Vector Search",
253
- "memory.settings.vectorHint": "Connect an OpenAI-compatible embeddings API for semantic search by meaning, not just keywords",
254
- "memory.settings.vectorEnabled": "Enable vector search",
255
- "memory.settings.vectorBaseUrl": "Base URL",
256
- "memory.settings.vectorApiKey": "API Key",
257
- "memory.settings.vectorModel": "Model",
258
- "memory.settings.vectorSave": "Save Config",
259
- "memory.settings.vectorSaved": "Config saved",
260
- "memory.settings.vectorReindex": "Reindex",
261
- "memory.settings.vectorReindexing": "Indexing…",
262
- "memory.settings.vectorReindexDone": "Indexed {n} items",
263
- "memory.settings.apiTokenTitle": "API Token (optional)",
264
- "memory.settings.apiTokenHint": "When set, write operations and secret endpoints (profile/rules/commands/vector config) require Authorization: Bearer <token>. Read-only panel calls stay open. Save empty to disable.",
265
- "memory.settings.apiTokenPlaceholder": "Empty = no auth (default)",
266
- "memory.settings.apiTokenSave": "Save Token",
267
- "memory.settings.apiTokenSaved": "Token saved",
268
- "memory.wikilink.backlinks": "Links to this memory",
269
- "memory.wikilink.forward": "This memory links to",
270
- "memory.wikilink.empty": "No linked memories",
271
- "memory.wikilink.loading": "Loading…",
272
- "memory.wikilink.unresolved": "Target memory not found"
273
- }
274
- };
275
-
276
- function typeLabel(t, type) {
277
- const key = `memory.tab.${type}`;
278
- const label = t(key);
279
- return label && label !== key ? label : String(type);
280
- }
281
-
282
- function formatDate(value) {
283
- if (!value) return "—";
284
- const date = new Date(value);
285
- return Number.isNaN(date.getTime()) ? "—" : date.toLocaleString();
286
- }
287
-
288
- // Relative time ("2分钟前") keeps cards scannable; anything older than a
289
- // week falls back to the absolute date. The card meta shows the relative
290
- // form and carries the full timestamp in a title tooltip.
291
- function formatRelativeTime(value, t) {
292
- if (!value) return "—";
293
- const ms = new Date(value).getTime();
294
- if (Number.isNaN(ms)) return "—";
295
- const diff = Date.now() - ms;
296
- if (diff < 60_000) return t("memory.time.now");
297
- const minutes = Math.floor(diff / 60_000);
298
- if (minutes < 60) return t("memory.time.minutes").replace("{n}", String(minutes));
299
- const hours = Math.floor(minutes / 60);
300
- if (hours < 24) return t("memory.time.hours").replace("{n}", String(hours));
301
- const days = Math.floor(hours / 24);
302
- if (days < 7) return t("memory.time.days").replace("{n}", String(days));
303
- return new Date(ms).toLocaleDateString();
304
- }
305
-
306
- // Memory-library stylesheet, injected once per page following the host's
307
- // data-plugin-css convention. Every value resolves to the host's design
308
- // tokens: background layers, label/border/interactive aliases and the
309
- // --dsw-font-* scale, so the page reads as a first-party view beside
310
- // Chat / Trajectory (flat layer-1 canvas, hairline column separators,
311
- // text-turns-brand-blue active states — no boxed panels).
312
- const CSS_TAG = "@modusensus/dsh-mneme/drawer.css";
313
- const css = [
314
- // --- sidebar foot trigger (wide row / collapsed rail icon) ---
315
- ".mneme-trigger{box-sizing:border-box;cursor:pointer;width:calc(100% + 4px);height:42px;color:var(--dsw-alias-label-primary);background:0 0;border:none;border-radius:12px;flex:none;align-items:center;gap:8px;margin:4px -2px;padding:0 10px 0 8px;font-family:inherit;font-size:14px;line-height:22px;display:flex;overflow:hidden}",
316
- ".mneme-trigger:hover{background:var(--dsw-alias-interactive-bg-hover)}",
317
- ".mneme-trigger.mneme-rail{border-radius:50%;justify-content:center;gap:0;width:36px;height:36px;margin:8px 0 10px;padding:0}",
318
- ".mneme-trigger-label{white-space:nowrap;overflow:hidden}",
319
- // --- shared controls ---
320
- ".mneme-search{box-sizing:border-box;height:30px;padding:0 10px;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;transition:border-color .12s,box-shadow .12s}",
321
- ".mneme-search:focus{border-color:var(--dsw-alias-state-business-primary);box-shadow:0 0 0 3px color-mix(in srgb,var(--dsw-alias-state-business-primary) 15%,transparent)}",
322
- ".mneme-search::placeholder{color:var(--dsw-alias-label-tertiary)}",
323
- ".mneme-chip{height:26px;padding:0 10px;border-radius:8px;border:1px solid transparent;background:none;color:var(--dsw-alias-label-secondary);cursor:pointer;font-family:inherit;font-size:12px;line-height:16px;display:inline-flex;align-items:center}",
324
- ".mneme-chip:hover{background:var(--dsw-alias-interactive-bg-hover)}",
325
- ".mneme-chip.mneme-active{color:var(--dsw-alias-state-business-primary);background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 10%,transparent)}",
326
- ".mneme-select{box-sizing:border-box;height:30px;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:13px;outline:none}",
327
- ".mneme-footbtn{border:none;background:none;color:var(--dsw-alias-label-secondary);cursor:pointer;font-family:inherit;font-size:12px;line-height:16px;padding:3px 8px;border-radius:6px}",
328
- ".mneme-footbtn:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}",
329
- ".mneme-hint{color:var(--dsw-alias-label-tertiary);padding:24px 0;text-align:center;font-size:13px}",
330
- ".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}",
331
- ".mneme-entitychip:hover{background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 10%,transparent)}",
332
- // --- tag chips in the detail meta (v0.6.2) ---
333
- ".mneme-tagrow{display:flex;flex-wrap:wrap;gap:6px;align-items:center}",
334
- ".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}",
335
- ".mneme-tagchip:hover{background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 10%,transparent)}",
336
- ".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}",
337
- ".mneme-tagremove:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}",
338
- ".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}",
339
- ".mneme-tagadd:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}",
340
- ".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}",
341
- ".mneme-taginput:focus{border-color:var(--dsw-alias-state-business-primary)}",
342
- ".mneme-tagempty{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:16px}",
343
- // --- main-area memory library page ---
344
- ".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)}",
345
- ".mneme-xbar{flex:none;display:flex;align-items:center;gap:6px;border-bottom:1px solid var(--dsw-alias-border-l2);padding:0 16px}",
346
- ".mneme-filterbar{flex:none;display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:8px 16px;border-bottom:1px solid var(--dsw-alias-border-l2)}",
347
- ".mneme-vtabs{display:flex;align-items:stretch;height:38px}",
348
- ".mneme-vtab{position:relative;border:0;background:none;cursor:pointer;padding:0 10px;color:var(--dsw-alias-label-tertiary);font-family:inherit;font-size:13px;font-weight:500;line-height:16px;display:inline-flex;align-items:center;gap:6px}",
349
- ".mneme-vtab:hover{color:var(--dsw-alias-label-primary)}",
350
- ".mneme-vtab.mneme-active{color:var(--dsw-alias-state-business-primary)}",
351
- ".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)}",
352
- ".mneme-xtools{margin-left:auto;display:flex;align-items:center;gap:8px;padding:0 0 0 12px}",
353
- ".mneme-xcount{flex:none;font-size:12px;line-height:16px;color:var(--dsw-alias-label-tertiary);white-space:nowrap}",
354
- // --- three-column browse layout: hairline separators, no outer box ---
355
- ".mneme-xmain{flex:1;min-height:0;display:flex;flex-direction:row;overflow:hidden}",
356
- ".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}",
357
- ".mneme-xside--filter{width:214px}",
358
- ".mneme-xbrowse{flex:1;min-width:0;min-height:0;display:flex;flex-direction:column;overflow:hidden}",
359
- ".mneme-xrow{display:flex;align-items:center;gap:8px;flex-wrap:wrap}",
360
- ".mneme-xsearch{width:100%}",
361
- ".mneme-xselect{flex:1;min-width:0}",
362
- ".mneme-xcolhead{flex:none;font-size:12px;font-weight:500;color:var(--dsw-alias-label-tertiary);padding:2px 8px 8px}",
363
- ".mneme-xtype{display:flex;justify-content:space-between;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}",
364
- ".mneme-xtype:hover{background:var(--dsw-alias-interactive-bg-hover)}",
365
- ".mneme-xtype.mneme-active{background:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-primary);font-weight:500}",
366
- ".mneme-xcount2{flex:none;font-size:12px;color:var(--dsw-alias-label-tertiary)}",
367
- ".mneme-xmonth{display:flex;align-items:center;gap:4px;width:100%;padding:6px 8px 4px;border:none;border-radius:8px;background:none;color:var(--dsw-alias-label-primary);cursor:pointer;font-family:inherit;font-size:13px;font-weight:500;line-height:18px;text-align:left}",
368
- ".mneme-xmonth:first-child{margin-top:0}",
369
- ".mneme-xmonth:hover{background:var(--dsw-alias-interactive-bg-hover)}",
370
- ".mneme-xcaret{flex:none;font-size:10px;color:var(--dsw-alias-label-tertiary);width:10px}",
371
- ".mneme-xday{display:flex;align-items:center;gap:4px;margin:6px 0 2px 22px;padding:2px 6px;border:none;border-radius:6px;background:none;font-family:inherit;font-size:12px;line-height:16px;color:var(--dsw-alias-label-tertiary);cursor:pointer;text-align:left}",
372
- ".mneme-xday:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}",
373
- ".mneme-xitem{display:flex;gap:8px;align-items:baseline;width:calc(100% - 22px);margin-left:22px;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}",
374
- ".mneme-xitem:hover{background:var(--dsw-alias-interactive-bg-hover)}",
375
- ".mneme-xitem.mneme-active{background:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-primary)}",
376
- ".mneme-xtime{flex:none;font-size:12px;line-height:16px;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums}",
377
- ".mneme-xname{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
378
- ".mneme-xempty{padding:32px 16px;text-align:center;color:var(--dsw-alias-label-tertiary);font-size:13px}",
379
- // --- detail column ---
380
- ".mneme-xdetail{flex:none;max-height:44%;min-height:0;overflow-y:auto;padding:14px 20px 16px;border-bottom:1px solid var(--dsw-alias-border-l2)}",
381
- ".mneme-xtree{flex:1;min-height:0;overflow-y:auto;padding:8px 10px 28px}",
382
- ".mneme-xmain{display:flex;flex-direction:column}",
383
- ".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)}",
384
- ".mneme-xcards{flex:1;display:flex;flex-direction:row;gap:12px;padding:12px;overflow:hidden}",
385
- ".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}",
386
- ".mneme-card--search{width:236px;flex:none}",
387
- ".mneme-card--tree{width:260px;flex:none}",
388
- ".mneme-card--detail{flex:1;min-width:0}",
389
- ".mneme-xdinner{max-width:720px}",
390
- ".mneme-xdtitle{font-size:16px;font-weight:600;line-height:24px;color:var(--dsw-alias-label-primary);margin-bottom:10px;word-break:break-word}",
391
- ".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)}",
392
- ".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}",
393
- ".mneme-xdactions{display:flex;gap:8px;margin-top:18px}",
394
- // --- wiki-link backlinks panel (detail pane footer) ---
395
- ".mneme-backlinks{margin-top:18px;padding-top:14px;border-top:1px solid var(--dsw-alias-border-l1);display:flex;flex-direction:column;gap:12px}",
396
- ".mneme-bl-block{display:flex;flex-direction:column;gap:4px}",
397
- ".mneme-bl-head{font-size:12px;font-weight:500;color:var(--dsw-alias-label-tertiary)}",
398
- ".mneme-bl-list{display:flex;flex-wrap:wrap;gap:6px}",
399
- ".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}",
400
- ".mneme-bl-link:hover{background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 10%,transparent)}",
401
- ".mneme-bl-link.mneme-bl-link--dim{color:var(--dsw-alias-label-tertiary);cursor:default}",
402
- ".mneme-bl-link.mneme-bl-link--dim:hover{background:none}",
403
- ".mneme-bl-title{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
404
- ".mneme-bl-meta{flex:none;font-size:11px;color:var(--dsw-alias-label-tertiary)}",
405
- ".mneme-bl-empty{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}",
406
- // --- inline [[wiki-link]] links inside the detail content ---
407
- ".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}",
408
- ".mneme-wikilink:hover{background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 8%,transparent)}",
409
- // --- graph sub-view (fills the content area under the tabs) ---
410
- ".mneme-graph{flex:1;min-height:0;width:100%;display:flex;flex-direction:column;padding:12px 16px 16px;box-sizing:border-box}",
411
- ".mneme-graphbar{display:flex;gap:8px;align-items:center;flex:none;margin-bottom:10px}",
412
- ".mneme-graphsvg{flex:1;min-height:180px;width:100%;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-button-elevated-fill);cursor:grab;touch-action:none}",
413
- ".mneme-gnode{cursor:pointer}",
414
- ".mneme-gnode circle{stroke:var(--dsw-alias-bg-layer-2);stroke-width:2;transition:stroke-width .12s}",
415
- ".mneme-gnode:hover circle{stroke-width:4}",
416
- ".mneme-gnode.mneme-groot circle{stroke:var(--dsw-alias-state-business-primary);stroke-width:3}",
417
- ".mneme-glabel{fill:var(--dsw-alias-label-secondary);font-size:11px;text-anchor:middle;pointer-events:none;user-select:none}",
418
- ".mneme-gedge{stroke:var(--dsw-alias-label-dimmed);stroke-width:1.5;cursor:pointer}",
419
- ".mneme-gedge:hover{stroke:var(--dsw-alias-state-business-primary)}",
420
- ".mneme-gedge-dashed{stroke-dasharray:5 4;opacity:.7}",
421
- ".mneme-graphhint{flex:none;color:var(--dsw-alias-label-tertiary);font-size:13px;text-align:center;padding:6px 0 2px}",
422
- ".mneme-graphside{flex:none;max-height:220px;overflow-y:auto;border-top:1px solid var(--dsw-alias-border-l1);margin-top:10px;padding-top:10px}",
423
- ".mneme-gs-title{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary);margin-bottom:4px;display:flex;justify-content:space-between;align-items:baseline;gap:8px}",
424
- ".mneme-gs-meta{font-size:13px;color:var(--dsw-alias-label-tertiary);margin-bottom:6px}",
425
- ".mneme-gs-attr{display:flex;gap:6px;font-size:13px;padding:2px 0}",
426
- ".mneme-gs-attrkey{flex:none;color:var(--dsw-alias-label-tertiary);font-size:13px}",
427
- ".mneme-gs-attrval{color:var(--dsw-alias-label-secondary);word-break:break-word;font-size:13px}",
428
- ".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}",
429
- ".mneme-gs-link:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}",
430
- // --- directory sub-view (v0.6.3): tag folders + memory entries ---
431
- ".mneme-directory{flex:1;min-height:0;overflow-y:auto;padding:8px 10px 28px}",
432
- ".mneme-edit-toggle{margin:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;cursor:pointer;user-select:none}",
433
- ".mneme-dir-item{position:relative;display:flex;align-items:center;gap:6px;padding:2px 6px;border-radius:4px}",
434
- ".mneme-dir-item:hover{background:rgba(127,127,127,.18)}",
435
- ".mneme-tree-line{width:14px;height:1px;border-top:1px dashed rgba(127,127,127,.5);flex:none}",
436
- ".mneme-dir-item .mneme-dir-del{opacity:0;transition:opacity .12s}",
437
- ".mneme-dir-item:hover .mneme-dir-del{opacity:1}",
438
- ".mneme-dir-del{margin-left:auto;background:none;border:none;color:#e5484d;cursor:pointer;font-size:13px;line-height:1;padding:2px 4px}",
439
- ".mneme-dir-del:hover{filter:brightness(1.25)}",
440
- ".mneme-edit-on .mneme-dir-del{opacity:1}",
441
- ".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}",
442
- ".mneme-dir-folder{margin-bottom:4px}",
443
- ".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}",
444
- ".mneme-dir-folderhead:hover{background:var(--dsw-alias-interactive-bg-hover)}",
445
- ".mneme-dir-caret{flex:none;width:10px;text-align:center;color:var(--dsw-alias-label-tertiary);font-size:12px}",
446
- ".mneme-dir-fname{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
447
- ".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}",
448
- ".mneme-dir-fbody{margin:2px 0 4px 22px;padding-left:8px;border-left:1px solid var(--dsw-alias-border-l2)}",
449
- ".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}",
450
- ".mneme-dir-item:hover{background:var(--dsw-alias-interactive-bg-hover)}",
451
- ".mneme-dir-item.mneme-active{background:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-primary)}",
452
- ".mneme-dir-ititle{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
453
- ".mneme-dir-itime{flex:none;font-size:12px;line-height:16px;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums}",
454
- ".mneme-dir-empty{color:var(--dsw-alias-label-tertiary);padding:24px 0;text-align:center;font-size:13px}",
455
- // --- settings sub-view ---
456
- ".mneme-xsettings{flex:1;min-height:0;overflow-y:auto;padding:24px 24px 48px}",
457
- ".mneme-xsettings-inner{max-width:640px}",
458
- // --- hero fallback: full-viewport memory library when no tab ring exists ---
459
- // The host hides the whole conversation tab ring while a session is
460
- // blank (hero screen), so the sidebar entry cannot activate the tab
461
- // there. This surface reuses the exact MemoryExplorer UI at full size —
462
- // not a side drawer — so the library stays reachable from any state.
463
- ".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}",
464
- ".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)}",
465
- ".mneme-overlaytitle{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary)}",
466
- ".mneme-overlaybody{flex:1;min-height:0;display:flex;flex-direction:column;overflow:hidden}",
467
- "@keyframes mneme-fadein{from{opacity:0}to{opacity:1}}"
468
- ].join("\n");
469
- if (typeof document !== "undefined" && document.querySelector(`style[data-plugin-css="${CSS_TAG}"]`) === null) {
470
- const tag = document.createElement("style");
471
- tag.dataset.plugin = "@modusensus/dsh-mneme";
472
- tag.dataset.pluginCss = CSS_TAG;
473
- tag.textContent = css;
474
- document.head.appendChild(tag);
475
- }
476
-
477
- const h = react.createElement;
478
-
479
- // --- graph view constants ---
480
- // Entity type → node fill. The host has no palette token for categorical
481
- // data, so these are fixed hues tuned for both light and dark themes
482
- // (medium saturation, similar luminance).
483
- const TYPE_COLORS = {
484
- person: "#3b82f6",
485
- project: "#22c55e",
486
- concept: "#a855f7",
487
- technology: "#f59e0b",
488
- organization: "#06b6d4"
489
- };
490
- function typeColor(type) {
491
- return TYPE_COLORS[type] || "#94a3b8";
492
- }
493
- function nodeRadius(n) {
494
- // mention_count → area-ish growth, clamped so hubs stay legible.
495
- return 7 + Math.min(20, Math.max(1, n.mention_count ?? 1)) * 0.55;
496
- }
497
-
498
- // Deterministic golden-angle spiral: no two nodes start overlapping, and
499
- // re-running the layout for the same data is stable (no random seeding).
500
- function initialPositions(nodes, width, height) {
501
- const cx = width / 2;
502
- const cy = height / 2;
503
- return nodes.map((n, i) => {
504
- const r = i === 0 ? 0 : 34 + 13 * Math.sqrt(i);
505
- const a = i * 2.39996;
506
- return { ...n, x: cx + r * Math.cos(a), y: cy + r * Math.sin(a), vx: 0, vy: 0, pinned: false };
507
- });
508
- }
509
-
510
- // --- wiki-link (v0.6.1) -------------------------------------------------
511
- // Splits detail content into literal text and clickable [[target]] /
512
- // [[display|target]] links. Mirrors src/parser/wiki-link.js: single pipe
513
- // only, empty / multi-pipe / unclosed markers stay literal text.
514
- function wikilinkSegments(text, onClick) {
515
- if (typeof text !== "string" || text.length === 0) return text;
516
- const out = [];
517
- let last = 0;
518
- let key = 0;
519
- const re = /\[\[([^\[\]]*)\]\]/g;
520
- let m;
521
- while ((m = re.exec(text)) !== null) {
522
- const parts = m[1].split("|");
523
- if (parts.length > 2) continue; // 多管道 → 非法,保留字面文本
524
- const target = (parts.length === 2 ? parts[1] : parts[0]).trim();
525
- if (!target) continue; // 空目标 → 非法
526
- if (m.index > last) out.push(text.slice(last, m.index));
527
- out.push(h("button", {
528
- key: `w${key++}`,
529
- type: "button",
530
- className: "mneme-wikilink",
531
- title: target,
532
- onClick: () => onClick(target)
533
- }, parts[0].trim() || target));
534
- last = m.index + m[0].length;
535
- }
536
- if (last < text.length) out.push(text.slice(last));
537
- return out.length ? out : text;
538
- }
539
-
540
- // Backlinks panel: the two wiki-link relation blocks under the detail pane.
541
- // Backlinks = memories whose content links to this one; forward = memories
542
- // this one links to (unresolved targets surface as dim, non-clickable).
543
- // Mounted per-selection (the surrounding Fragment keys on selected.id), so
544
- // state resets and the fetches re-run on every switch.
545
- function BacklinksPanel({ memory, t, onJump }) {
546
- const [back, setBack] = useState(null); // null = loading
547
- const [forward, setForward] = useState(null);
548
-
549
- useEffect(() => {
550
- let cancelled = false;
551
- if (!memory || !memory.id) return;
552
- apiFetch(`/api/dsh-mneme/wikilinks/backlinks?id=${encodeURIComponent(memory.id)}`)
553
- .then((r) => (r.ok ? r.json() : { backlinks: [] }))
554
- .then((j) => { if (!cancelled) setBack(Array.isArray(j.backlinks) ? j.backlinks : []); })
555
- .catch(() => { if (!cancelled) setBack([]); });
556
- apiFetch(`/api/dsh-mneme/wikilinks/forward?id=${encodeURIComponent(memory.id)}`)
557
- .then((r) => (r.ok ? r.json() : { links: [] }))
558
- .then((j) => { if (!cancelled) setForward(Array.isArray(j.links) ? j.links : []); })
559
- .catch(() => { if (!cancelled) setForward([]); });
560
- return () => { cancelled = true; };
561
- }, [memory && memory.id]);
562
-
563
- if (!memory || !memory.id) return null;
564
-
565
- const loading = back === null || forward === null;
566
- const row = (link, i) => {
567
- const canJump = !!link.id;
568
- return h("button", {
569
- key: link.id ?? `unresolved-${i}`,
570
- type: "button",
571
- className: canJump ? "mneme-bl-link" : "mneme-bl-link mneme-bl-link--dim",
572
- title: canJump ? t("memory.card.open") : t("memory.wikilink.unresolved"),
573
- disabled: !canJump,
574
- onClick: () => onJump && onJump({ id: link.id })
575
- },
576
- h("span", { className: "mneme-bl-title" }, link.title || "—"),
577
- link.type && h("span", { className: "mneme-bl-meta" }, typeLabel(t, link.type))
578
- );
579
- };
580
- const section = (label, items) =>
581
- h("div", { className: "mneme-bl-block" },
582
- h("div", { className: "mneme-bl-head" }, label),
583
- loading
584
- ? h("div", { className: "mneme-bl-empty" }, t("memory.wikilink.loading"))
585
- : items.length === 0
586
- ? h("div", { className: "mneme-bl-empty" }, t("memory.wikilink.empty"))
587
- : h("div", { className: "mneme-bl-list" }, items.map(row))
588
- );
589
-
590
- return h("div", { className: "mneme-backlinks" },
591
- section(t("memory.wikilink.backlinks"), back || []),
592
- section(t("memory.wikilink.forward"), forward || [])
593
- );
594
- }
595
-
596
- // --- Graph view: ego-graph of one entity, zero-dependency SVG force layout ---
597
- // The DSH client module table only whitelists platform modules, so a graph
598
- // library like vis-network cannot be required from a plugin. A hand-rolled
599
- // spring simulation (repulsion + edge springs + center gravity, damped) is
600
- // plenty for the ≤40 nodes the ego API returns.
601
- function GraphPanel({ t, focusEntity, onJumpMemory }) {
602
- const [entityName, setEntityName] = useState(focusEntity || "");
603
- const [inputValue, setInputValue] = useState(focusEntity || "");
604
- const [depth, setDepth] = useState(1);
605
- const [data, setData] = useState(null);
606
- const [status, setStatus] = useState("idle"); // idle | loading | ready | notfound | error
607
- const [selected, setSelected] = useState(null); // { kind: "node"|"edge", node?|edge? }
608
- const [attrs, setAttrs] = useState([]);
609
- const [related, setRelated] = useState([]);
610
- const svgRef = useRef(null);
611
- const posRef = useRef([]); // live simulation positions, not React state
612
- const dragRef = useRef(null); // { id, moved }
613
- const [frame, setFrame] = useState(0); // re-render tick driven by the simulation
614
-
615
- useEffect(() => {
616
- if (focusEntity && focusEntity !== entityName) {
617
- setEntityName(focusEntity);
618
- setInputValue(focusEntity);
619
- }
620
- // eslint-disable-next-line react-hooks/exhaustive-deps
621
- }, [focusEntity]);
622
-
623
- const load = useCallback(async (name, d) => {
624
- if (!name) { setData(null); setStatus("idle"); return; }
625
- setStatus("loading");
626
- setSelected(null);
627
- try {
628
- const res = await apiFetch(`/api/dsh-mneme/semantic/graph/ego?entity=${encodeURIComponent(name)}&depth=${d}`);
629
- if (res.status === 404) { setData(null); setStatus("notfound"); return; }
630
- if (!res.ok) { setData(null); setStatus("error"); return; }
631
- const json = await res.json();
632
- setData(json);
633
- setStatus("ready");
634
- } catch {
635
- setData(null);
636
- setStatus("error");
637
- }
638
- }, []);
639
-
640
- useEffect(() => { load(entityName, depth); }, [load, entityName, depth]);
641
-
642
- // Detail pane: node → current attrs + entity: search for related memories.
643
- useEffect(() => {
644
- setAttrs([]);
645
- setRelated([]);
646
- if (!selected || selected.kind !== "node") return;
647
- const name = selected.node.name;
648
- let cancelled = false;
649
- apiFetch(`/api/dsh-mneme/semantic/graph/entity-attrs?entity=${encodeURIComponent(name)}`)
650
- .then((r) => (r.ok ? r.json() : { attrs: [] }))
651
- .then((j) => { if (!cancelled) setAttrs(Array.isArray(j.attrs) ? j.attrs : []); })
652
- .catch(() => {});
653
- apiFetch(`/api/dsh-mneme/search?q=${encodeURIComponent("entity:" + name)}&limit=10`)
654
- .then((r) => (r.ok ? r.json() : { items: [] }))
655
- .then((j) => { if (!cancelled) setRelated(Array.isArray(j.items) ? j.items : []); })
656
- .catch(() => {});
657
- return () => { cancelled = true; };
658
- }, [selected]);
659
-
660
- // Simulation: run in rAF against posRef, tick React only every few frames.
661
- // Re-seeds when data changes; dragging writes straight into posRef.
662
- useEffect(() => {
663
- if (!data || data.nodes.length === 0) return;
664
- const width = Math.max(280, svgRef.current?.clientWidth || 380);
665
- const height = 300;
666
- posRef.current = initialPositions(data.nodes, width, height);
667
- const byId = new Map(posRef.current.map((n) => [n.id, n]));
668
- const edges = data.edges;
669
- let raf = 0;
670
- let n = 0;
671
- const tick = () => {
672
- const nodes = posRef.current;
673
- // pairwise repulsion, capped so far nodes don't explode
674
- for (let i = 0; i < nodes.length; i++) {
675
- for (let j = i + 1; j < nodes.length; j++) {
676
- const a = nodes[i], b = nodes[j];
677
- let dx = b.x - a.x, dy = b.y - a.y;
678
- let d2 = dx * dx + dy * dy;
679
- if (d2 < 1) { dx = (Math.random() - 0.5) || 1; dy = (Math.random() - 0.5) || 1; d2 = dx * dx + dy * dy; }
680
- const d = Math.sqrt(d2);
681
- const f = Math.min(2200 / d2, 6);
682
- const fx = (dx / d) * f, fy = (dy / d) * f;
683
- a.vx -= fx; a.vy -= fy; b.vx += fx; b.vy += fy;
684
- }
685
- }
686
- // edge springs pull toward the target length
687
- for (const e of edges) {
688
- const a = byId.get(e.from), b = byId.get(e.to);
689
- if (!a || !b) continue;
690
- const dx = b.x - a.x, dy = b.y - a.y;
691
- const d = Math.sqrt(dx * dx + dy * dy) || 1;
692
- const f = (d - 90) * 0.02;
693
- const fx = (dx / d) * f, fy = (dy / d) * f;
694
- a.vx += fx; a.vy += fy; b.vx -= fx; b.vy -= fy;
695
- }
696
- let energy = 0;
697
- for (const node of nodes) {
698
- // gentle gravity toward center keeps the cloud from drifting off-canvas
699
- node.vx += (width / 2 - node.x) * 0.002;
700
- node.vy += (height / 2 - node.y) * 0.002;
701
- if (node.pinned || node === dragRef.current?.node) { node.vx = 0; node.vy = 0; continue; }
702
- node.vx *= 0.85; node.vy *= 0.85;
703
- node.x = Math.max(nodeRadius(node) + 4, Math.min(width - nodeRadius(node) - 4, node.x + node.vx));
704
- node.y = Math.max(nodeRadius(node) + 14, Math.min(height - nodeRadius(node) - 14, node.y + node.vy));
705
- energy += Math.abs(node.vx) + Math.abs(node.vy);
706
- }
707
- n++;
708
- if (n % 3 === 0) setFrame((f) => f + 1);
709
- if (n < 300 && energy > 0.4) raf = requestAnimationFrame(tick);
710
- };
711
- raf = requestAnimationFrame(tick);
712
- return () => cancelAnimationFrame(raf);
713
- }, [data]);
714
-
715
- // drag handling on the svg surface
716
- const onNodeMouseDown = (e, node) => {
717
- e.preventDefault();
718
- dragRef.current = { node, moved: false };
719
- const startX = e.clientX, startY = e.clientY;
720
- const origX = node.x, origY = node.y;
721
- const svg = svgRef.current;
722
- const rect = svg.getBoundingClientRect();
723
- const scale = 380 / Math.max(1, rect.width); // viewBox width / css width
724
- const onMove = (ev) => {
725
- dragRef.current.moved = true;
726
- node.x = origX + (ev.clientX - startX) * scale;
727
- node.y = origY + (ev.clientY - startY) * scale;
728
- setFrame((f) => f + 1);
729
- };
730
- const onUp = () => {
731
- window.removeEventListener("mousemove", onMove);
732
- window.removeEventListener("mouseup", onUp);
733
- setTimeout(() => { dragRef.current = null; }, 0);
734
- };
735
- window.addEventListener("mousemove", onMove);
736
- window.addEventListener("mouseup", onUp);
737
- };
738
-
739
- const onNodeClick = (node) => {
740
- if (dragRef.current?.moved) return; // it was a drag, not a click
741
- setSelected({ kind: "node", node });
742
- };
743
- const onEdgeClick = (edge) => setSelected({ kind: "edge", edge });
744
-
745
- const nodes = posRef.current;
746
- const nodeById = new Map(nodes.map((n) => [n.id, n]));
747
- const VIEW_W = 380, VIEW_H = 300;
748
-
749
- const side = selected?.kind === "node"
750
- ? h("div", { className: "mneme-graphside" },
751
- h("div", { className: "mneme-gs-title" },
752
- h("span", null, selected.node.name),
753
- h("span", { className: "mneme-gs-meta" },
754
- `${typeLabel(t, selected.node.type || "concept")} · ★${selected.node.mention_count ?? 1}`)
755
- ),
756
- attrs.length > 0 && h("div", null,
757
- h("div", { className: "mneme-gs-meta" }, t("memory.graph.attrs")),
758
- attrs.map((a, i) => h("div", { key: i, className: "mneme-gs-attr" },
759
- h("span", { className: "mneme-gs-attrkey" }, `${a.key}:`),
760
- h("span", { className: "mneme-gs-attrval" }, a.value)
761
- ))
762
- ),
763
- related.length > 0 && h("div", { style: { marginTop: 8 } },
764
- h("div", { className: "mneme-gs-meta" }, t("memory.graph.related")),
765
- related.map((m) => h("button", {
766
- key: m.id,
767
- type: "button",
768
- className: "mneme-gs-link",
769
- title: t("memory.card.open"),
770
- onClick: () => onJumpMemory && onJumpMemory(m)
771
- }, m.title || m.content?.slice(0, 60)))
772
- )
773
- )
774
- : selected?.kind === "edge"
775
- ? h("div", { className: "mneme-graphside" },
776
- h("div", { className: "mneme-gs-title" },
777
- h("span", null,
778
- `${nodeById.get(selected.edge.from)?.name ?? "?"} → ${selected.edge.relation_type} → ${nodeById.get(selected.edge.to)?.name ?? "?"}`)
779
- ),
780
- h("div", { className: "mneme-gs-meta" },
781
- `${t("memory.graph.relation")} · ${formatRelativeTime(selected.edge.created_at, t)}`),
782
- selected.edge.memory_id && h("button", {
783
- className: "mneme-footbtn",
784
- onClick: () => onJumpMemory && onJumpMemory({ id: selected.edge.memory_id })
785
- }, t("memory.graph.sourceMemory"))
786
- )
787
- : null;
788
-
789
- return h("div", { className: "mneme-graph" },
790
- h("div", { className: "mneme-graphbar" },
791
- h("input", {
792
- className: "mneme-search",
793
- style: { flex: 1, minWidth: 0, maxWidth: 280 },
794
- placeholder: t("memory.graph.placeholder"),
795
- value: inputValue,
796
- onChange: (e) => setInputValue(e.target.value),
797
- onKeyDown: (e) => { if (e.key === "Enter") setEntityName(inputValue.trim()); }
798
- }),
799
- h("button", {
800
- className: depth === 2 ? "mneme-chip mneme-active" : "mneme-chip",
801
- title: t("memory.graph.depth"),
802
- onClick: () => setDepth(depth === 1 ? 2 : 1)
803
- }, `${depth} ${t("memory.graph.depth")}`)
804
- ),
805
- status === "idle" && h("div", { className: "mneme-hint" }, t("memory.graph.empty")),
806
- status === "loading" && h("div", { className: "mneme-hint" }, t("memory.graph.loading")),
807
- status === "notfound" && h("div", { className: "mneme-hint" }, t("memory.graph.notFound")),
808
- status === "error" && h("div", { className: "mneme-hint" }, t("memory.panel.empty")),
809
- status === "ready" && (data.nodes.length <= 1
810
- ? h("div", { className: "mneme-hint" }, t("memory.graph.empty"))
811
- : h(react.Fragment, null,
812
- h("svg", {
813
- ref: svgRef,
814
- className: "mneme-graphsvg",
815
- viewBox: `0 0 ${VIEW_W} ${VIEW_H}`
816
- },
817
- data.edges.map((e) => {
818
- const a = nodeById.get(e.from), b = nodeById.get(e.to);
819
- if (!a || !b) return null;
820
- return h("line", {
821
- key: e.id,
822
- x1: a.x, y1: a.y, x2: b.x, y2: b.y,
823
- className: e.memory_id ? "mneme-gedge" : "mneme-gedge mneme-gedge-dashed",
824
- onClick: () => onEdgeClick(e)
825
- });
826
- }),
827
- nodes.map((n) => h("g", {
828
- key: n.id,
829
- className: n.id === data.root.id ? "mneme-gnode mneme-groot" : "mneme-gnode",
830
- transform: `translate(${n.x},${n.y})`,
831
- onMouseDown: (e) => onNodeMouseDown(e, n),
832
- onClick: () => onNodeClick(n),
833
- "data-node": n.name
834
- },
835
- h("circle", { r: nodeRadius(n), fill: typeColor(n.type) }),
836
- h("text", { className: "mneme-glabel", y: nodeRadius(n) + 13 }, n.name)
837
- ))
838
- ),
839
- h("div", { className: "mneme-graphhint" }, t("memory.graph.hint"))
840
- )),
841
- side
842
- );
843
- }
844
-
845
- // --- Settings view: user profile, rules, custom commands, vector, token ---
846
- const styles = {
847
- 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" }
848
- };
849
-
850
- function SettingsContent({ t }) {
851
- const [profile, setProfile] = react.useState("");
852
- const [rules, setRules] = react.useState([]);
853
- const [commands, setCommands] = react.useState([]);
854
- const [newRule, setNewRule] = react.useState("");
855
- const [newCmd, setNewCmd] = react.useState({ name: "", description: "", instruction: "" });
856
- const [saved, setSaved] = react.useState(false);
857
- const [cmdError, setCmdError] = react.useState("");
858
- const [vector, setVector] = react.useState({ enabled: false, baseUrl: "", apiKey: "", model: "" });
859
- const [vectorSaved, setVectorSaved] = react.useState(false);
860
- const [reindexing, setReindexing] = react.useState(false);
861
- const [reindexMsg, setReindexMsg] = react.useState("");
862
- const [apiToken, setApiToken] = react.useState(() =>
863
- (typeof window !== "undefined" && window.localStorage) ? window.localStorage.getItem("dsh-mneme-api-token") || "" : ""
864
- );
865
- const [apiTokenSaved, setApiTokenSaved] = react.useState(false);
866
- const [autoTag, setAutoTag] = react.useState(false);
867
- const [autoTagSaved, setAutoTagSaved] = react.useState(false);
868
-
869
- const load = react.useCallback(async () => {
870
- try {
871
- const [p, r, c, v] = await Promise.all([
872
- apiFetch("/api/dsh-mneme/profile").then((res) => res.json()),
873
- apiFetch("/api/dsh-mneme/rules").then((res) => res.json()),
874
- apiFetch("/api/dsh-mneme/commands").then((res) => res.json()),
875
- apiFetch("/api/dsh-mneme/vector-config").then((res) => res.json())
876
- ]);
877
- setProfile(p.profile || "");
878
- setRules(Array.isArray(r.rules) ? r.rules : []);
879
- setCommands(Array.isArray(c.commands) ? c.commands : []);
880
- setVector(v.config || { enabled: false, baseUrl: "", apiKey: "", model: "" });
881
- // autoTag loads independently so a config failure can't cascade-block the rest.
882
- try {
883
- const g = await apiFetch("/api/dsh-mneme/config").then((res) => res.json());
884
- setAutoTag(g.config?.autoTagEnabled === true);
885
- } catch { /* keep default off */ }
886
- } catch { /* ignore */ }
887
- }, []);
888
-
889
- react.useEffect(() => { load(); }, [load]);
890
-
891
- async function saveProfile() {
892
- try {
893
- await apiFetch("/api/dsh-mneme/profile", {
894
- method: "PUT",
895
- headers: { "Content-Type": "application/json" },
896
- body: JSON.stringify({ profile })
897
- });
898
- setSaved(true);
899
- setTimeout(() => setSaved(false), 1500);
900
- } catch { /* ignore */ }
901
- }
902
-
903
- async function putRules(next) {
904
- await apiFetch("/api/dsh-mneme/rules", {
905
- method: "PUT",
906
- headers: { "Content-Type": "application/json" },
907
- body: JSON.stringify({ rules: next })
908
- });
909
- }
910
-
911
- async function addRule() {
912
- const text = newRule.trim();
913
- if (!text) return;
914
- const next = [...rules, text];
915
- await putRules(next);
916
- setRules(next);
917
- setNewRule("");
918
- }
919
-
920
- async function removeRule(index) {
921
- const next = rules.filter((_, i) => i !== index);
922
- await putRules(next);
923
- setRules(next);
924
- }
925
-
926
- async function addCommand() {
927
- const name = newCmd.name.trim();
928
- const instruction = newCmd.instruction.trim();
929
- if (!name || !instruction) return;
930
- try {
931
- const res = await apiFetch("/api/dsh-mneme/commands", {
932
- method: "POST",
933
- headers: { "Content-Type": "application/json" },
934
- body: JSON.stringify({ name, description: newCmd.description, instruction })
935
- });
936
- const data = await res.json();
937
- if (!res.ok) { setCmdError(data.error || "failed"); return; }
938
- setCmdError("");
939
- setCommands([...commands, data.command]);
940
- setNewCmd({ name: "", description: "", instruction: "" });
941
- } catch { setCmdError("failed"); }
942
- }
943
-
944
- async function removeCommand(id) {
945
- await apiFetch(`/api/dsh-mneme/commands?id=${encodeURIComponent(id)}`, { method: "DELETE" });
946
- setCommands(commands.filter((c) => c.id !== id));
947
- }
948
-
949
- async function saveVector() {
950
- try {
951
- await apiFetch("/api/dsh-mneme/vector-config", {
952
- method: "PUT",
953
- headers: { "Content-Type": "application/json" },
954
- body: JSON.stringify(vector)
955
- });
956
- setVectorSaved(true);
957
- setTimeout(() => setVectorSaved(false), 1500);
958
- } catch { /* ignore */ }
959
- }
960
-
961
- async function reindex() {
962
- setReindexing(true);
963
- setReindexMsg("");
964
- try {
965
- const res = await apiFetch("/api/dsh-mneme/vector-reindex");
966
- const data = await res.json();
967
- const n = data.indexed ?? 0;
968
- setReindexMsg(t("memory.settings.vectorReindexDone").replace("{n}", String(n)));
969
- } catch { setReindexMsg(""); }
970
- setReindexing(false);
971
- }
972
-
973
- function saveToken() {
974
- try {
975
- if (apiToken.trim()) window.localStorage.setItem("dsh-mneme-api-token", apiToken.trim());
976
- else window.localStorage.removeItem("dsh-mneme-api-token");
977
- setApiTokenSaved(true);
978
- setTimeout(() => setApiTokenSaved(false), 1500);
979
- } catch { /* ignore */ }
980
- }
981
-
982
- async function saveAutoTag() {
983
- try {
984
- await apiFetch("/api/dsh-mneme/config", {
985
- method: "PUT",
986
- headers: { "Content-Type": "application/json" },
987
- body: JSON.stringify({ autoTagEnabled: autoTag })
988
- });
989
- setAutoTagSaved(true);
990
- setTimeout(() => setAutoTagSaved(false), 1500);
991
- } catch { /* ignore */ }
992
- }
993
-
994
- const inputStyle = { boxSizing: "border-box", width: "100%", height: 34, padding: "0 12px", borderRadius: 10, border: "1px solid var(--dsw-alias-border-l2, #ddd)", background: "var(--dsw-alias-bg-base, transparent)", color: "var(--dsw-alias-label-primary)", fontFamily: "inherit", fontSize: 13, outline: "none", marginBottom: 8 };
995
- const labelStyle = { fontSize: 12, fontWeight: 600, margin: "12px 0 4px", color: "var(--dsw-alias-label-primary)" };
996
- const hintStyle = { fontSize: 11, color: "var(--dsw-alias-label-tertiary, #999)", marginBottom: 8 };
997
- const rowStyle = { display: "flex", justifyContent: "space-between", alignItems: "center", gap: 8, padding: "6px 0", borderBottom: "1px solid var(--dsw-alias-border-l1, #eee)" };
998
-
999
- return h("div", { style: { paddingBottom: 8 } },
1000
- // api token (optional)
1001
- h("div", { style: { ...labelStyle, marginTop: 20 } }, t("memory.settings.apiTokenTitle")),
1002
- h("div", { style: hintStyle }, t("memory.settings.apiTokenHint")),
1003
- h("div", { style: { display: "flex", gap: 6 } },
1004
- h("input", { style: { ...inputStyle, flex: 1, marginBottom: 0 }, type: "password", value: apiToken, placeholder: t("memory.settings.apiTokenPlaceholder"), onChange: (e) => setApiToken(e.target.value) }),
1005
- h("button", { style: styles.footerButton, onClick: saveToken }, t("memory.settings.apiTokenSave"))
1006
- ),
1007
- apiTokenSaved && h("div", { style: { fontSize: 12, color: "var(--dsw-alias-state-success, #2a7)" } }, t("memory.settings.apiTokenSaved")),
1008
- // profile
1009
- h("div", { style: labelStyle }, t("memory.settings.profile")),
1010
- h("div", { style: hintStyle }, t("memory.settings.profileHint")),
1011
- h("textarea", {
1012
- style: { ...inputStyle, minHeight: 72, resize: "vertical", fontFamily: "inherit", padding: "8px 12px", height: "auto" },
1013
- value: profile,
1014
- placeholder: t("memory.settings.profile"),
1015
- onChange: (e) => setProfile(e.target.value)
1016
- }),
1017
- h("div", null,
1018
- h("button", { style: styles.footerButton, onClick: saveProfile }, t("memory.settings.profileSave")),
1019
- saved && h("span", { style: { fontSize: 12, color: "var(--dsw-alias-state-success, #2a7)" } }, t("memory.settings.profileSaved"))
1020
- ),
1021
- // rules
1022
- h("div", { style: labelStyle }, t("memory.settings.rules")),
1023
- h("div", { style: hintStyle }, t("memory.settings.rulesHint")),
1024
- rules.length === 0 && h("div", { style: { fontSize: 12, color: "var(--dsw-alias-label-tertiary, #999)", padding: "8px 0" } }, t("memory.settings.empty")),
1025
- rules.map((rule, i) =>
1026
- h("div", { key: i, style: rowStyle },
1027
- h("span", { style: { fontSize: 13, flex: 1 } }, rule),
1028
- h("button", { style: { ...styles.footerButton, color: "var(--dsw-alias-state-error, #c33)" }, onClick: () => removeRule(i) }, "×")
1029
- )
1030
- ),
1031
- h("div", { style: { display: "flex", gap: 6 } },
1032
- h("input", {
1033
- style: { ...inputStyle, flex: 1, marginBottom: 0 },
1034
- value: newRule,
1035
- placeholder: t("memory.settings.rulePlaceholder"),
1036
- onChange: (e) => setNewRule(e.target.value)
1037
- }),
1038
- h("button", { style: styles.footerButton, onClick: addRule }, t("memory.settings.ruleAdd"))
1039
- ),
1040
- // custom commands
1041
- h("div", { style: labelStyle }, t("memory.settings.commands")),
1042
- h("div", { style: hintStyle }, t("memory.settings.commandsHint")),
1043
- commands.length === 0 && h("div", { style: { fontSize: 12, color: "var(--dsw-alias-label-tertiary, #999)", padding: "8px 0" } }, t("memory.settings.empty")),
1044
- commands.map((cmd) =>
1045
- h("div", { key: cmd.id, style: rowStyle },
1046
- h("div", { style: { flex: 1 } },
1047
- h("div", { style: { fontSize: 13, fontWeight: 600 } }, `/${cmd.name}`),
1048
- h("div", { style: { fontSize: 11, color: "var(--dsw-alias-label-tertiary, #999)" } }, cmd.description || cmd.instruction)
1049
- ),
1050
- h("button", { style: { ...styles.footerButton, color: "var(--dsw-alias-state-error, #c33)" }, onClick: () => removeCommand(cmd.id) }, t("memory.settings.cmdDelete"))
1051
- )
1052
- ),
1053
- h("div", { style: { display: "grid", gap: 6, marginTop: 6 } },
1054
- h("input", { style: { ...inputStyle, marginBottom: 0 }, value: newCmd.name, placeholder: t("memory.settings.cmdName"), onChange: (e) => setNewCmd({ ...newCmd, name: e.target.value }) }),
1055
- h("input", { style: { ...inputStyle, marginBottom: 0 }, value: newCmd.description, placeholder: t("memory.settings.cmdDesc"), onChange: (e) => setNewCmd({ ...newCmd, description: e.target.value }) }),
1056
- h("textarea", { style: { ...inputStyle, marginBottom: 0, minHeight: 48, resize: "vertical", fontFamily: "inherit", padding: "8px 12px", height: "auto" }, value: newCmd.instruction, placeholder: t("memory.settings.cmdInstruction"), onChange: (e) => setNewCmd({ ...newCmd, instruction: e.target.value }) }),
1057
- h("button", { style: styles.footerButton, onClick: addCommand }, t("memory.settings.cmdAdd")),
1058
- cmdError && h("div", { style: { fontSize: 12, color: "var(--dsw-alias-state-error, #c33)" } }, cmdError)
1059
- ),
1060
- // auto-tag switch
1061
- h("div", { style: { ...labelStyle, marginTop: 20 } }, t("memory.settings.autoTagTitle")),
1062
- h("div", { style: hintStyle }, t("memory.settings.autoTagHint")),
1063
- h("label", { style: { display: "flex", alignItems: "center", gap: 6, marginBottom: 8, fontSize: 13 } },
1064
- h("input", { type: "checkbox", checked: autoTag, onChange: (e) => setAutoTag(e.target.checked) }),
1065
- h("span", null, t("memory.settings.autoTagEnabled"))
1066
- ),
1067
- h("div", { style: { display: "flex", alignItems: "center", gap: 6 } },
1068
- h("button", { style: styles.footerButton, onClick: saveAutoTag }, t("memory.settings.autoTagSave")),
1069
- autoTagSaved && h("span", { style: { fontSize: 12, color: "var(--dsw-alias-state-success, #2a7)" } }, t("memory.settings.autoTagSaved"))
1070
- ),
1071
- // vector search
1072
- h("div", { style: { ...labelStyle, marginTop: 20 } }, t("memory.settings.vectorTitle")),
1073
- h("div", { style: hintStyle }, t("memory.settings.vectorHint")),
1074
- h("label", { style: { display: "flex", alignItems: "center", gap: 6, marginBottom: 8, fontSize: 13 } },
1075
- h("input", { type: "checkbox", checked: !!vector.enabled, onChange: (e) => setVector({ ...vector, enabled: e.target.checked }) }),
1076
- h("span", null, t("memory.settings.vectorEnabled"))
1077
- ),
1078
- h("input", { style: inputStyle, value: vector.baseUrl, placeholder: t("memory.settings.vectorBaseUrl"), onChange: (e) => setVector({ ...vector, baseUrl: e.target.value }) }),
1079
- h("input", { style: inputStyle, type: "password", value: vector.apiKey, placeholder: t("memory.settings.vectorApiKey"), onChange: (e) => setVector({ ...vector, apiKey: e.target.value }) }),
1080
- h("input", { style: inputStyle, value: vector.model, placeholder: t("memory.settings.vectorModel"), onChange: (e) => setVector({ ...vector, model: e.target.value }) }),
1081
- h("div", { style: { display: "flex", alignItems: "center", gap: 6, flexWrap: "wrap" } },
1082
- h("button", { style: styles.footerButton, onClick: saveVector }, t("memory.settings.vectorSave")),
1083
- vectorSaved && h("span", { style: { fontSize: 12, color: "var(--dsw-alias-state-success, #2a7)" } }, t("memory.settings.vectorSaved")),
1084
- h("button", { style: styles.footerButton, onClick: reindex, disabled: reindexing }, reindexing ? t("memory.settings.vectorReindexing") : t("memory.settings.vectorReindex")),
1085
- reindexMsg && h("span", { style: { fontSize: 12, color: "var(--dsw-alias-label-secondary, #666)" } }, reindexMsg)
1086
- )
1087
- );
1088
- }
1089
-
1090
- // --- Main-area memory library: the single home for every memory feature ---
1091
- // Registered under conversation.view beside Chat / Trajectory. The old
1092
- // right-hand drawer is gone: the sidebar foot entry activates this tab
1093
- // directly. The framework keeps the active-view setter private to the
1094
- // conversation package, and the DOM tab click is the one stable
1095
- // activation path available to plugins.
1096
- //
1097
- // One host constraint remains: the conversation header (and with it the
1098
- // whole tab ring) is hidden while a session is blank — the new-chat hero
1099
- // screen. There the tab simply does not exist, so activateExplorerTab
1100
- // resolves false and the sidebar entry falls back to the full-viewport
1101
- // overlay surface below.
1102
- function findExplorerTab(label) {
1103
- const tabs = document.querySelectorAll('[role="tab"]');
1104
- for (const tab of tabs) {
1105
- if ((tab.textContent || "").trim() === label) return tab;
1106
- }
1107
- return null;
1108
- }
1109
-
1110
- // Click the memory library tab and wait (bounded, rAF-polled) until the
1111
- // host actually marks it selected — a silent React re-render gap must not
1112
- // be mistaken for success, or the fallback would never kick in.
1113
- function activateExplorerTab(label) {
1114
- return new Promise((resolve) => {
1115
- const tab = findExplorerTab(label);
1116
- if (!tab) { resolve(false); return; }
1117
- tab.click();
1118
- const deadline = Date.now() + 400;
1119
- (function check() {
1120
- if (tab.getAttribute("aria-selected") === "true") { resolve(true); return; }
1121
- if (Date.now() >= deadline) { resolve(false); return; }
1122
- requestAnimationFrame(check);
1123
- })();
1124
- });
1125
- }
1126
-
1127
- // --- Hero fallback overlay state (module-level pub/sub) ---
1128
- const overlayListeners = new Set();
1129
- let overlayOpenState = false;
1130
- function setOverlayOpen(v) {
1131
- if (v === overlayOpenState) return;
1132
- overlayOpenState = v;
1133
- for (const fn of overlayListeners) fn();
1134
- }
1135
- function useOverlayOpen() {
1136
- const [open, setOpen] = useState(overlayOpenState);
1137
- useEffect(() => {
1138
- const fn = () => setOpen(overlayOpenState);
1139
- overlayListeners.add(fn);
1140
- return () => { overlayListeners.delete(fn); };
1141
- }, []);
1142
- return [open, setOverlayOpen];
1143
- }
1144
-
1145
- // Full-viewport memory library for states without a tab ring (new-chat
1146
- // hero). Same MemoryExplorer component as the tab view — identical
1147
- // three-column layout, graph and settings — plus a slim top bar with a
1148
- // close affordance. Esc closes. Portalled to <body> so sidebar stacking
1149
- // contexts cannot clip it.
1150
- function MemoryOverlay({ t }) {
1151
- const [open, setOpen] = useOverlayOpen();
1152
- useEffect(() => {
1153
- if (!open) return undefined;
1154
- const onKey = (e) => { if (e.key === "Escape") setOpen(false); };
1155
- window.addEventListener("keydown", onKey);
1156
- return () => window.removeEventListener("keydown", onKey);
1157
- }, [open]);
1158
- if (!open) return null;
1159
- const tree = h("div", { className: "mneme-overlay", role: "region", "aria-label": t("memory.view.label") },
1160
- h("div", { className: "mneme-overlaybar" },
1161
- h("span", { className: "mneme-overlaytitle" }, t("memory.view.label")),
1162
- h("button", {
1163
- type: "button",
1164
- className: "mneme-footbtn",
1165
- onClick: () => setOpen(false)
1166
- }, t("memory.overlay.close"))
1167
- ),
1168
- h("div", { className: "mneme-overlaybody" }, h(MemoryExplorer, { t }))
1169
- );
1170
- if (reactDom && typeof document !== "undefined") return reactDom.createPortal(tree, document.body);
1171
- return tree;
1172
- }
1173
-
1174
- // --- directory sub-view (v0.6.3) ---
1175
- // A tag folder tree over the live memory set. One-level folders keyed by
1176
- // tag (handled on the server, getDirectory), each with a 二级 memory
1177
- // entry list (title + updated time); memories without a live tag land in
1178
- // the "untagged" folder. Collapse state lives in MemoryExplorer (lifted via
1179
- // `collapsed`), so switching tabs does not drop it. Clicking an entry jumps
1180
- // back into the browser with that memory selected (onJump = jumpToMemory).
1181
- function DirectoryPanel({ t, onJump, collapsed, setCollapsed }) {
1182
- const [dir, setDir] = useState(null); // { groups: [...], untagged: [...] }
1183
- const [status, setStatus] = useState("loading"); // loading | ready | error
1184
- const [editMode, setEditMode] = useState(false); // manual tag/delete UI gate
1185
-
1186
- useEffect(() => {
1187
- let cancelled = false;
1188
- setStatus("loading");
1189
- apiFetch("/api/dsh-mneme/directory")
1190
- .then((res) => (res.ok ? res.json() : null))
1191
- .then((d) => {
1192
- if (cancelled) return;
1193
- if (!d) { setDir(null); setStatus("error"); return; }
1194
- setDir({ groups: Array.isArray(d.groups) ? d.groups : [], untagged: Array.isArray(d.untagged) ? d.untagged : [] });
1195
- setStatus("ready");
1196
- })
1197
- .catch(() => { if (!cancelled) { setDir(null); setStatus("error"); } });
1198
- apiFetch("/api/dsh-mneme/config")
1199
- .then((res) => (res.ok ? res.json() : null))
1200
- .then((d) => { if (!cancelled && d?.config && typeof d.config.manualTagEnabled === "boolean") setEditMode(d.config.manualTagEnabled); })
1201
- .catch(() => {});
1202
- return () => { cancelled = true; };
1203
- }, []);
1204
-
1205
- const toggle = (key) => setCollapsed((c) => ({ ...c, [key]: !c[key] }));
1206
-
1207
- const toggleEdit = () => {
1208
- const next = !editMode;
1209
- apiFetch("/api/dsh-mneme/config", {
1210
- method: "PUT",
1211
- headers: { "Content-Type": "application/json" },
1212
- body: JSON.stringify({ manualTagEnabled: next })
1213
- }).then((res) => { if (res.ok) setEditMode(next); }).catch(() => {});
1214
- };
1215
-
1216
- const handleDelete = (memId, e) => {
1217
- e?.stopPropagation?.();
1218
- if (typeof window === "undefined" || !window.confirm(t("directory.deleteConfirm"))) return;
1219
- apiFetch("/api/dsh-mneme/memories", {
1220
- method: "DELETE",
1221
- headers: { "Content-Type": "application/json" },
1222
- body: JSON.stringify({ id: memId })
1223
- })
1224
- .then((res) => (res.ok ? res.json() : null))
1225
- .then((d) => {
1226
- if (!d || d.deleted !== true) return;
1227
- setDir((prev) => ({
1228
- groups: prev.groups.map((g) => ({ ...g, memories: g.memories.filter((m) => m.id !== memId) })),
1229
- untagged: prev.untagged.filter((m) => m.id !== memId)
1230
- }));
1231
- })
1232
- .catch(() => {});
1233
- };
1234
-
1235
- const renderFolder = (name, memories, extraKey, emptyLabel) => {
1236
- const folderKey = extraKey || name;
1237
- const open = !collapsed[folderKey];
1238
- const rows = memories.map((m) =>
1239
- h("div", { key: m.id, className: "mneme-dir-item" },
1240
- h("span", { className: "mneme-tree-line" }),
1241
- h("button", { type: "button", className: "mneme-dir-ititle", onClick: () => onJump(m) }, m.title || m.content?.slice(0, 40)),
1242
- h("span", { className: "mneme-dir-itime" }, formatDate(m.updated_at || m.created_at)),
1243
- editMode && h("button", { type: "button", className: "mneme-dir-del", "aria-label": "delete", onClick: (e) => handleDelete(m.id, e) }, "✕")
1244
- )
1245
- );
1246
- return h("div", {
1247
- key: folderKey,
1248
- className: "mneme-dir-folder",
1249
- "data-expanded": String(open)
1250
- },
1251
- h("button", {
1252
- type: "button",
1253
- className: "mneme-dir-folderhead",
1254
- "aria-expanded": String(open),
1255
- onClick: () => toggle(folderKey)
1256
- },
1257
- h("span", { className: "mneme-dir-caret" }, open ? "▾" : "▸"),
1258
- h("span", { className: "mneme-dir-fname" }, `📁 ${name}`),
1259
- h("span", { className: "mneme-dir-fcount" }, String(memories.length))
1260
- ),
1261
- open && h("div", { className: "mneme-dir-fbody" },
1262
- rows.length > 0 ? rows : h("div", { className: "mneme-dir-empty" }, emptyLabel || t("memory.directory.empty"))
1263
- )
1264
- );
1265
- };
1266
-
1267
- if (status === "loading") {
1268
- return h("div", { className: "mneme-directory" }, h("div", { className: "mneme-dir-empty" }, t("memory.directory.loading")));
1269
- }
1270
- const hasAny = dir && (dir.groups.length > 0 || dir.untagged.length > 0);
1271
- if (status === "error" || !hasAny) {
1272
- return h("div", { className: "mneme-directory" }, h("div", { className: "mneme-dir-empty" }, t("memory.directory.empty")));
1273
- }
1274
- return h("div", { className: editMode ? "mneme-directory mneme-edit-on" : "mneme-directory" },
1275
- h("label", { className: "mneme-edit-toggle" },
1276
- h("input", { type: "checkbox", checked: editMode, onChange: toggleEdit }),
1277
- t("directory.editToggle")
1278
- ),
1279
- dir.groups.map((g) => renderFolder(g.tag, g.memories)),
1280
- dir.untagged.length > 0 && renderFolder(t("memory.directory.untagged"), dir.untagged, "mneme-untagged")
1281
- );
1282
- }
1283
-
1284
- const EXPLORER_TYPES = ["preference", "project", "decision", "summary", "history"];
1285
-
1286
- function MemoryExplorer({ t }) {
1287
- const [view, setView] = useState("memory"); // memory | directory | graph | settings
1288
- const [items, setItems] = useState([]);
1289
- const [loading, setLoading] = useState(true);
1290
- const [type, setType] = useState("all");
1291
- const [query, setQuery] = useState("");
1292
- const [semantic, setSemantic] = useState(false);
1293
- const [vecEnabled, setVecEnabled] = useState(false);
1294
- const [searchTopK, setSearchTopK] = useState(20);
1295
- const [remoteItems, setRemoteItems] = useState(null);
1296
- const [selectedId, setSelectedId] = useState(null);
1297
- const [collapsed, setCollapsed] = useState({});
1298
- // v0.6.3 directory sub-view: its own folder-collapse state, kept in
1299
- // MemoryExplorer so switching tabs does not lose it and jumpToMemory's
1300
- // time-tree reset (setCollapsed({})) cannot clobber it.
1301
- const [dirCollapsed, setDirCollapsed] = useState({});
1302
- const [copied, setCopied] = useState(false);
1303
- const [reloadKey, setReloadKey] = useState(0);
1304
- const [graphFocus, setGraphFocus] = useState("");
1305
- // v0.6.2 tag editing state for the detail pane. The authoritative tag set
1306
- // lives server-side (entity_attrs), so it is fetched per selected memory
1307
- // and refreshed after every write instead of trusting the list snapshot.
1308
- const [tagTags, setTagTags] = useState([]);
1309
- const [tagManual, setTagManual] = useState(true);
1310
- const [tagInput, setTagInput] = useState("");
1311
- const [tagAdding, setTagAdding] = useState(false);
1312
- const itemRefs = useRef(new Map());
1313
-
1314
- useEffect(() => {
1315
- apiFetch("/api/dsh-mneme/vector-config")
1316
- .then((res) => res.json())
1317
- .then((d) => setVecEnabled(!!d.config?.enabled))
1318
- .catch(() => {});
1319
- }, []);
1320
-
1321
- useEffect(() => {
1322
- let cancelled = false;
1323
- setLoading(true);
1324
- apiFetch("/api/dsh-mneme/list?limit=500")
1325
- .then((res) => (res.ok ? res.json() : { items: [] }))
1326
- .then((d) => { if (!cancelled) { setItems(d.items || []); setLoading(false); } })
1327
- .catch(() => { if (!cancelled) { setItems([]); setLoading(false); } });
1328
- return () => { cancelled = true; };
1329
- }, [reloadKey]);
1330
-
1331
- // Semantic search: server-side ranking replaces the client filter
1332
- // while enabled and a query is present (debounced). tag: queries always
1333
- // go server-side (searchMemories resolves them without vector support),
1334
- // so they bypass the semantic toggle and use the same search endpoint.
1335
- useEffect(() => {
1336
- const q = query.trim();
1337
- if ((!semantic && !q.startsWith("tag:")) || !q || q.startsWith("entity:")) { setRemoteItems(null); return; }
1338
- let cancelled = false;
1339
- const timer = setTimeout(() => {
1340
- apiFetch(`/api/dsh-mneme/search?q=${encodeURIComponent(q)}&mode=vector&topK=${searchTopK}`)
1341
- .then((res) => (res.ok ? res.json() : { items: [] }))
1342
- .then((d) => { if (!cancelled) setRemoteItems(d.items || []); })
1343
- .catch(() => { if (!cancelled) setRemoteItems([]); });
1344
- }, 250);
1345
- return () => { cancelled = true; clearTimeout(timer); };
1346
- }, [semantic, query, searchTopK]);
1347
-
1348
- useEffect(() => {
1349
- if (!selectedId) return;
1350
- itemRefs.current.get(selectedId)?.scrollIntoView({ block: "nearest" });
1351
- }, [selectedId]);
1352
-
1353
- // Load the authoritative tag set for the selected memory (entity_attrs-
1354
- // backed) plus the manualTagEnabled gate that hides editing when off.
1355
- const loadTags = (id) => {
1356
- if (!id) { setTagTags([]); setTagManual(true); setTagInput(""); return; }
1357
- apiFetch(`/api/dsh-mneme/memory/tags?id=${encodeURIComponent(id)}`)
1358
- .then((res) => (res.ok ? res.json() : null))
1359
- .then((j) => { if (j) { setTagTags(Array.isArray(j.tags) ? j.tags : []); setTagManual(j.manualTagEnabled !== false); } })
1360
- .catch(() => {});
1361
- };
1362
- useEffect(() => {
1363
- let cancelled = false;
1364
- if (selectedId) {
1365
- apiFetch(`/api/dsh-mneme/memory/tags?id=${encodeURIComponent(selectedId)}`)
1366
- .then((res) => (res.ok ? res.json() : null))
1367
- .then((j) => { if (!cancelled && j) { setTagTags(Array.isArray(j.tags) ? j.tags : []); setTagManual(j.manualTagEnabled !== false); } })
1368
- .catch(() => {});
1369
- } else {
1370
- setTagTags([]);
1371
- setTagManual(true);
1372
- }
1373
- setTagAdding(false);
1374
- return () => { cancelled = true; };
1375
- }, [selectedId]);
1376
-
1377
- const q = query.trim().toLowerCase();
1378
- // "entity:" is the graph entry grammar: typing it means the user wants
1379
- // the entity's neighborhood, not a memory list. Offer the jump instead
1380
- // of auto-switching so the list stays predictable.
1381
- const entityQuery = query.trim().startsWith("entity:")
1382
- ? query.trim().slice(7).trim()
1383
- : "";
1384
- const visible = remoteItems
1385
- ? remoteItems
1386
- : items.filter((m) => {
1387
- if (type !== "all" && m.type !== type) return false;
1388
- if (!q) return true;
1389
- return (m.title || "").toLowerCase().includes(q) || (m.content || "").toLowerCase().includes(q);
1390
- });
1391
-
1392
- const counts = {};
1393
- for (const m of items) counts[m.type] = (counts[m.type] || 0) + 1;
1394
- const knownTypes = EXPLORER_TYPES.filter((k) => counts[k]);
1395
- const extraTypes = Object.keys(counts)
1396
- .filter((k) => !EXPLORER_TYPES.includes(k))
1397
- .sort((a, b) => counts[b] - counts[a]);
1398
-
1399
- // Time tree: sort newest first, then group month → day in one pass so
1400
- // the grouping follows the sort order instead of re-sorting buckets.
1401
- const sorted = [...visible].sort((a, b) =>
1402
- new Date(b.updated_at || b.created_at || 0) - new Date(a.updated_at || a.created_at || 0));
1403
- const months = [];
1404
- let curMonth = null, curDay = null;
1405
- for (const m of sorted) {
1406
- const d = new Date(m.updated_at || m.created_at || 0);
1407
- const valid = !Number.isNaN(d.getTime());
1408
- const mk = valid ? `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}` : "unknown";
1409
- if (!curMonth || curMonth.key !== mk) {
1410
- curMonth = {
1411
- key: mk,
1412
- label: valid ? d.toLocaleDateString(undefined, { year: "numeric", month: "long" }) : "—",
1413
- days: []
1414
- };
1415
- months.push(curMonth);
1416
- curDay = null;
1417
- }
1418
- const dk = valid ? `${mk}-${String(d.getDate())}` : "unknown";
1419
- if (!curDay || curDay.key !== dk) {
1420
- curDay = { key: dk, label: valid ? d.toLocaleDateString(undefined, { day: "numeric" }) : "—", items: [] };
1421
- curMonth.days.push(curDay);
1422
- }
1423
- curDay.items.push(m);
1424
- }
1425
-
1426
- const selected = items.find((m) => m.id === selectedId) || null;
1427
-
1428
- const copyContent = () => {
1429
- if (!selected) return;
1430
- navigator.clipboard?.writeText(selected.content || "").then(
1431
- () => { setCopied(true); setTimeout(() => setCopied(false), 1500); },
1432
- () => {}
1433
- );
1434
- };
1435
-
1436
- // v0.6.2 tag editing: a chip click jumps to a tag:xxx search (reusing
1437
- // the query-driven search flow), the + entry and per-chip × post the new
1438
- // tag set through the tags endpoint then re-fetch. A 409 from the server
1439
- // means manualTagEnabled turned off — flip the gate so editing hides.
1440
- const submitTag = () => {
1441
- const tag = tagInput.trim();
1442
- if (!tag || !selectedId) return;
1443
- apiFetch("/api/dsh-mneme/memory/tags", {
1444
- method: "POST",
1445
- headers: { "Content-Type": "application/json" },
1446
- body: JSON.stringify({ id: selectedId, tags: [...tagTags, tag] })
1447
- }).then((res) => {
1448
- if (res.status === 409) { setTagManual(false); setTagAdding(false); setTagInput(""); return; }
1449
- return res.json();
1450
- }).then((j) => {
1451
- if (j?.ok) { setTagAdding(false); setTagInput(""); loadTags(selectedId); }
1452
- }).catch(() => {});
1453
- };
1454
- const removeTag = (tag) => {
1455
- if (!selectedId) return;
1456
- apiFetch("/api/dsh-mneme/memory/tags", {
1457
- method: "POST",
1458
- headers: { "Content-Type": "application/json" },
1459
- body: JSON.stringify({ id: selectedId, tags: tagTags.filter((x) => x !== tag) })
1460
- }).then((res) => {
1461
- if (res.status === 409) { setTagManual(false); return; }
1462
- return res.json();
1463
- }).then((j) => {
1464
- if (j?.ok) loadTags(selectedId);
1465
- }).catch(() => {});
1466
- };
1467
- const onTagClick = (tag) => {
1468
- setType("all");
1469
- setQuery(`tag:${tag}`);
1470
- };
1471
-
1472
- // Graph → memory jump: land on the browser tab with filters reset so
1473
- // the target row is visible, selected and scrolled into view.
1474
- const jumpToMemory = (target) => {
1475
- if (!target?.id) return;
1476
- setView("memory");
1477
- setType("all");
1478
- setQuery("");
1479
- setCollapsed({});
1480
- setSelectedId(target.id);
1481
- };
1482
-
1483
- // [[wiki-link]] click in the detail content: resolve the title to a
1484
- // memory (case-insensitive exact match) then jump to it. Unresolvable
1485
- // titles silently no-op — the link stays as display text.
1486
- const onWikilinkClick = (target) => {
1487
- if (!target) return;
1488
- apiFetch(`/api/dsh-mneme/wikilinks/resolve?title=${encodeURIComponent(target)}`)
1489
- .then((r) => (r.ok ? r.json() : null))
1490
- .then((j) => { if (j?.memory?.id) jumpToMemory(j.memory); })
1491
- .catch(() => {});
1492
- };
1493
-
1494
- const openGraphFor = (name) => {
1495
- setGraphFocus(name || "");
1496
- setView("graph");
1497
- };
1498
-
1499
- const subviews = [
1500
- { key: "memory", label: t("memory.explorer.tabMemory") },
1501
- { key: "directory", label: t("memory.explorer.tabDirectory") },
1502
- { key: "graph", label: t("memory.explorer.tabGraph") },
1503
- { key: "settings", label: t("memory.explorer.tabSettings") }
1504
- ];
1505
-
1506
- return h("div", { className: "mneme-x" },
1507
- h("div", { className: "mneme-xbar" },
1508
- h("nav", { className: "mneme-vtabs", "aria-label": t("memory.view.label") },
1509
- subviews.map((s) =>
1510
- h("button", {
1511
- key: s.key,
1512
- type: "button",
1513
- className: view === s.key ? "mneme-vtab mneme-active" : "mneme-vtab",
1514
- "aria-pressed": String(view === s.key),
1515
- onClick: () => setView(s.key)
1516
- },
1517
- s.key === "graph"
1518
- ? h(react.Fragment, null, h(GraphNodesIcon, { size: 16 }), s.label)
1519
- : s.label
1520
- ))
1521
- ),
1522
- ),
1523
- view === "memory" && h("div", { className: "mneme-xmain" },
1524
- // 1. 中间分类栏
1525
- h("div", { className: "mneme-xfilter-bar" },
1526
- h("div", { className: "mneme-xcolhead" }, t("memory.explorer.types")),
1527
- h("button", {
1528
- className: type === "all" ? "mneme-xtype mneme-active" : "mneme-xtype",
1529
- onClick: () => setType("all")
1530
- }, h("span", null, t("memory.tab.all")), h("span", { className: "mneme-xcount2" }, String(items.length))),
1531
- knownTypes.concat(extraTypes).map((key) =>
1532
- h("button", {
1533
- key,
1534
- className: type === key ? "mneme-xtype mneme-active" : "mneme-xtype",
1535
- onClick: () => setType(key)
1536
- },
1537
- h("span", null, typeLabel(t, key)),
1538
- h("span", { className: "mneme-xcount2" }, String(counts[key]))
1539
- ))
1540
- ),
1541
- // 2. 底部三卡片
1542
- h("div", { className: "mneme-xcards" },
1543
- // 左卡:搜索
1544
- h("div", { className: "mneme-card mneme-card--search" },
1545
- h("div", { className: "mneme-xcolhead" }, t("memory.explorer.searchTitle")),
1546
- h("input", {
1547
- className: "mneme-search mneme-xsearch",
1548
- placeholder: t("memory.explorer.search"),
1549
- value: query,
1550
- onChange: (e) => setQuery(e.target.value)
1551
- }),
1552
- entityQuery && h("button", {
1553
- className: "mneme-entitychip",
1554
- style: { textAlign: "left", justifyContent: "flex-start" },
1555
- onClick: () => openGraphFor(entityQuery)
1556
- }, `${t("memory.graph.viewInGraph")} “${entityQuery}”`),
1557
- h("div", { className: "mneme-xrow" },
1558
- vecEnabled && h("button", {
1559
- className: semantic ? "mneme-chip mneme-active" : "mneme-chip",
1560
- title: t("memory.settings.vectorTitle"),
1561
- onClick: () => setSemantic(!semantic)
1562
- }, t("memory.panel.semantic")),
1563
- h("select", {
1564
- className: "mneme-select mneme-xselect",
1565
- value: searchTopK,
1566
- onChange: (e) => setSearchTopK(Number(e.target.value)),
1567
- title: t("memory.explorer.topK")
1568
- }, [5, 10, 20, 50].map((n) => h("option", { key: n, value: n }, t("memory.explorer.topKOption").replace("{n}", String(n)))))
1569
- ),
1570
- h("div", { className: "mneme-xrow" },
1571
- h("span", { className: "mneme-xcount" }, t("memory.explorer.count").replace("{n}", String(visible.length))),
1572
- h("button", { className: "mneme-footbtn", onClick: () => setReloadKey((k) => k + 1) }, t("memory.explorer.refresh"))
1573
- )
1574
- ),
1575
- // 中卡:时间树
1576
- h("div", { className: "mneme-card mneme-card--tree" },
1577
- h("div", { className: "mneme-xcolhead" }, t("memory.explorer.timeline")),
1578
- loading
1579
- ? h("div", { className: "mneme-xempty" }, "…")
1580
- : months.length === 0
1581
- ? h("div", { className: "mneme-xempty" }, t("memory.explorer.empty"))
1582
- : months.map((month) =>
1583
- h("div", { key: month.key },
1584
- h("button", {
1585
- className: "mneme-xmonth",
1586
- "aria-expanded": String(!collapsed[month.key]),
1587
- onClick: () => setCollapsed((c) => ({ ...c, [month.key]: !c[month.key] }))
1588
- },
1589
- h("span", { className: "mneme-xcaret" }, collapsed[month.key] ? "▸" : "▾"),
1590
- month.label
1591
- ),
1592
- !collapsed[month.key] && month.days.map((day) =>
1593
- h("div", { key: day.key },
1594
- h("button", {
1595
- className: "mneme-xday",
1596
- "aria-expanded": String(!collapsed[day.key]),
1597
- onClick: () => setCollapsed((c) => ({ ...c, [day.key]: !c[day.key] }))
1598
- },
1599
- h("span", { className: "mneme-xcaret" }, collapsed[day.key] ? "▸" : "▾"),
1600
- day.label
1601
- ),
1602
- !collapsed[day.key] && day.items.map((m) => {
1603
- const d = new Date(m.updated_at || m.created_at || 0);
1604
- const time = Number.isNaN(d.getTime())
1605
- ? ""
1606
- : d.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" });
1607
- return h("button", {
1608
- key: m.id,
1609
- ref: (el) => { if (el) itemRefs.current.set(m.id, el); else itemRefs.current.delete(m.id); },
1610
- className: m.id === selectedId ? "mneme-xitem mneme-active" : "mneme-xitem",
1611
- onClick: () => setSelectedId(m.id)
1612
- },
1613
- h("span", { className: "mneme-xtime" }, time),
1614
- h("span", { className: "mneme-xname" }, m.title || m.content?.slice(0, 40))
1615
- );
1616
- })
1617
- )
1618
- )
1619
- )
1620
- )),
1621
- // 右卡:详情
1622
- h("div", { className: "mneme-card mneme-card--detail" },
1623
- selected
1624
- ? h(react.Fragment, { key: selected.id },
1625
- h("div", { className: "mneme-xdinner" },
1626
- h("div", { className: "mneme-xdtitle" }, selected.title),
1627
- h("div", { className: "mneme-xdmeta" },
1628
- h("span", null, `${typeLabel(t, selected.type)} · ${t("memory.explorer.importance")} ★${selected.importance}`),
1629
- selected.source && h("span", null, `${t("memory.explorer.source")}: ${selected.source}`),
1630
- h("span", { title: formatDate(selected.created_at) }, `${t("memory.explorer.created")}: ${formatDate(selected.created_at)}`),
1631
- h("span", { title: formatDate(selected.updated_at) }, `${t("memory.explorer.updated")}: ${formatDate(selected.updated_at)}`)
1632
- ),
1633
- h("div", { className: "mneme-xdmeta" },
1634
- h("span", null, t("memory.explorer.tags")),
1635
- h("div", { className: "mneme-tagrow" },
1636
- tagTags.length > 0
1637
- ? tagTags.map((tag) =>
1638
- h("span", {
1639
- key: tag,
1640
- className: "mneme-tagchip",
1641
- title: `tag:${tag}`,
1642
- onClick: () => onTagClick(tag)
1643
- },
1644
- tag,
1645
- tagManual && h("button", {
1646
- type: "button",
1647
- className: "mneme-tagremove",
1648
- "aria-label": `${t("memory.explorer.tagRemove")}: ${tag}`,
1649
- onClick: (e) => { e.stopPropagation(); removeTag(tag); }
1650
- }, "×")
1651
- )
1652
- )
1653
- : h("span", { className: "mneme-tagempty" }, t("memory.explorer.tagsEmpty")),
1654
- tagManual && (tagAdding
1655
- ? h("input", {
1656
- key: selectedId,
1657
- className: "mneme-taginput",
1658
- autoFocus: true,
1659
- value: tagInput,
1660
- placeholder: t("memory.explorer.tagPlaceholder"),
1661
- onChange: (e) => setTagInput(e.target.value),
1662
- onKeyDown: (e) => { if (e.key === "Enter") submitTag(); },
1663
- onBlur: () => { setTagAdding(false); setTagInput(""); }
1664
- })
1665
- : h("button", {
1666
- type: "button",
1667
- className: "mneme-tagadd",
1668
- onClick: () => setTagAdding(true)
1669
- }, `+ ${t("memory.explorer.tagAdd")}`))
1670
- )
1671
- ),
1672
- h("div", { className: "mneme-xdcontent" },
1673
- selected.content ? wikilinkSegments(selected.content, onWikilinkClick) : selected.content
1674
- ),
1675
- h("div", { className: "mneme-xdactions" },
1676
- h("button", { className: "mneme-footbtn", onClick: copyContent },
1677
- copied ? t("memory.explorer.copied") : t("memory.explorer.copy"))
1678
- ),
1679
- h(BacklinksPanel, { memory: selected, t, onJump: jumpToMemory })
1680
- )
1681
- )
1682
- : h("div", { className: "mneme-xempty" }, t("memory.explorer.emptyDetail"))
1683
- )
1684
- )
1685
- ),
1686
- view === "directory" && h(DirectoryPanel, { t, onJump: jumpToMemory, collapsed: dirCollapsed, setCollapsed: setDirCollapsed }),
1687
- view === "graph" && h(GraphPanel, { t, focusEntity: graphFocus, onJumpMemory: jumpToMemory }),
1688
- view === "settings" && h("div", { className: "mneme-xsettings" },
1689
- h("div", { className: "mneme-xsettings-inner" }, h(SettingsContent, { t }))
1690
- )
1691
- );
1692
- }
1693
-
1694
- // --- Sidebar foot entry: the wide row / rail icon that activates the
1695
- // main-area memory library tab. When the tab ring is absent (new-chat
1696
- // hero screen hides the conversation header entirely) the same click
1697
- // opens the full-viewport overlay instead — the library stays reachable
1698
- // from every conversation state.
1699
- function SidebarTrigger({ wide, t }) {
1700
- const [, setOpen] = useOverlayOpen();
1701
- return h("button", {
1702
- type: "button",
1703
- className: wide ? "mneme-trigger" : "mneme-trigger mneme-rail",
1704
- "aria-label": t("memory.sidebar.aria"),
1705
- title: t("memory.sidebar.aria"),
1706
- onClick: () => {
1707
- activateExplorerTab(t("memory.view.label")).then((ok) => { if (!ok) setOpen(true); });
1708
- }
1709
- },
1710
- h(IconArchiveOutline20, { size: wide ? 16 : 18 }),
1711
- wide && h("span", { className: "mneme-trigger-label" }, t("memory.panel.open"))
1712
- );
1713
- }
1714
-
1715
- function apply(ctx) {
1716
- ctx.effect(() => ctx.locale.register(NS, dictionaries), "dsh-mneme: dictionaries");
1717
-
1718
- // Register the memory entry beside Settings at the sidebar foot. The
1719
- // entry renders a wide row (icon + label) when the sidebar is expanded
1720
- // and a rail icon when collapsed; clicking activates the memory
1721
- // library conversation view.
1722
- ctx.slots.inject("sidebar.footer.action", () => {
1723
- const t = ctx.locale.bind(NS);
1724
- return ctx.slots.register({
1725
- name: "sidebar.footer.action",
1726
- id: "dsh-mneme",
1727
- order: 0,
1728
- label: () => t("memory.panel.open")
1729
- }, (props) => h(react.Fragment, null,
1730
- h(SidebarTrigger, { ...props, t }),
1731
- // The overlay mounts from the always-rendered sidebar slot so it
1732
- // survives conversation switches; the portal moves it to <body>.
1733
- h(MemoryOverlay, { t })
1734
- ));
1735
- });
1736
-
1737
- // Register the memory library as a conversation view tab, beside
1738
- // Chat / Trajectory. The view ignores its session props: the library
1739
- // reads the memory store over HTTP. It hosts every memory feature —
1740
- // browse, graph and settings — in the main content area.
1741
- ctx.slots.inject("conversation.view", () => {
1742
- const t = ctx.locale.bind(NS);
1743
- return ctx.slots.register({
1744
- name: "conversation.view",
1745
- id: "dsh-mneme-memory",
1746
- order: 30,
1747
- locale: NS,
1748
- label: () => t("memory.view.label")
1749
- }, () => h(MemoryExplorer, { t }));
1750
- });
1751
- }
1752
-
1753
- exports.apply = apply;
1754
- exports.inject = inject;
1755
- return module.exports;
1756
- }
1757
- });
1
+ window.__ModuleLoader__.load({
2
+ id: "@modusensus/dsh-mneme",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+
7
+ let react = require("react");
8
+ let primitives = require("@deepseek-ai/dsh-client-ui-primitives");
9
+ let { useState, useEffect, useCallback, useRef } = react;
10
+ const IconArchiveOutline20 = primitives.IconArchiveOutline20;
11
+
12
+ // Portal target for the hero fallback surface. The host whitelists
13
+ // react-dom for its own bundles (dsh-client-ui-trajectory requires it);
14
+ // when the runtime rejects it for plugins the overlay renders in place —
15
+ // position:fixed keeps it viewport-sized either way.
16
+ let reactDom = null;
17
+ try { reactDom = require("react-dom"); } catch { reactDom = null; }
18
+
19
+ // Node-graph pictogram (three nodes joined by edges). The primitives kit
20
+ // ships no network/graph icon, and its share-style glyph reads as
21
+ // "share" — exactly the confusion this custom 16px replacement avoids.
22
+ const GraphNodesIcon = ({ size = 16, className }) => h("svg", {
23
+ width: size,
24
+ height: size,
25
+ className,
26
+ viewBox: "0 0 16 16",
27
+ fill: "none",
28
+ xmlns: "http://www.w3.org/2000/svg"
29
+ },
30
+ h("path", {
31
+ d: "M8 5.2 4.6 10.4M8 5.2l3.4 5.2M5.2 12h5.6",
32
+ stroke: "currentColor",
33
+ strokeWidth: "1.2",
34
+ strokeLinecap: "round",
35
+ strokeLinejoin: "round"
36
+ }),
37
+ h("circle", { cx: 8, cy: 3.4, r: 1.8, fill: "currentColor" }),
38
+ h("circle", { cx: 3.6, cy: 12, r: 1.8, fill: "currentColor" }),
39
+ h("circle", { cx: 12.4, cy: 12, r: 1.8, fill: "currentColor" })
40
+ );
41
+
42
+ // Unified API fetcher: attaches the optional apiToken (set in the settings
43
+ // view, persisted in localStorage) as a Bearer header. When no token has
44
+ // been configured the header is omitted and the API stays open (default).
45
+ const API_TOKEN_KEY = "dsh-mneme-api-token";
46
+ function apiFetch(path, opts = {}) {
47
+ const token = (typeof window !== "undefined" && window.localStorage)
48
+ ? window.localStorage.getItem(API_TOKEN_KEY) || ""
49
+ : "";
50
+ const headers = { ...(opts.headers || {}) };
51
+ if (token) headers["Authorization"] = `Bearer ${token}`;
52
+ return fetch(path, { ...opts, headers });
53
+ }
54
+
55
+ const inject = ["slots", "locale"];
56
+
57
+ const NS = "memory";
58
+
59
+ const dictionaries = {
60
+ zh: {
61
+ "memory.panel.empty": "暂无记忆条目",
62
+ "memory.panel.open": "记忆",
63
+ "memory.tab.all": "全部",
64
+ "memory.tab.preference": "偏好",
65
+ "memory.tab.project": "项目",
66
+ "memory.tab.decision": "决策",
67
+ "memory.tab.history": "历史",
68
+ "memory.settings.title": "记忆库设置",
69
+ "memory.settings.profile": "用户画像",
70
+ "memory.settings.profileHint": "描述你自己(角色、背景、偏好),Agent 会在每轮遵循",
71
+ "memory.settings.profileSave": "保存画像",
72
+ "memory.settings.profileSaved": "画像已保存",
73
+ "memory.settings.rules": "规则",
74
+ "memory.settings.rulesHint": "Agent 必须遵守的行为规则,每轮注入",
75
+ "memory.settings.autoTagTitle": "自动打标签",
76
+ "memory.settings.autoTagHint": "开启后,新记忆由轻量模型自动打标签,目录页按标签自动分类(opt-in,默认关)",
77
+ "memory.settings.autoTagEnabled": "自动打标签",
78
+ "memory.settings.autoTagSave": "保存",
79
+ "memory.settings.autoTagSaved": "已保存",
80
+ "memory.settings.ruleAdd": "添加规则",
81
+ "memory.settings.rulePlaceholder": "例如:回答时总是先给结论",
82
+ "memory.settings.commands": "自定义指令",
83
+ "memory.settings.commandsHint": "注册斜杠命令(/名称),触发时把指令内容交给 Agent",
84
+ "memory.settings.cmdName": "命令名",
85
+ "memory.settings.cmdDesc": "描述",
86
+ "memory.settings.cmdInstruction": "指令内容",
87
+ "memory.settings.cmdAdd": "添加命令",
88
+ "memory.settings.cmdDelete": "删除",
89
+ "memory.settings.empty": "暂无内容",
90
+ "memory.panel.semantic": "语义",
91
+ "memory.sidebar.aria": "打开记忆库",
92
+ "memory.view.label": "记忆库",
93
+ "memory.overlay.close": "关闭",
94
+ "memory.explorer.tabMemory": "记忆",
95
+ "memory.explorer.tabGraph": "图谱",
96
+ "memory.explorer.tabDirectory": "目录",
97
+ "memory.explorer.tabSettings": "设置",
98
+ "memory.explorer.search": "搜索标题或内容…",
99
+ "memory.explorer.searchTitle": "语义检索",
100
+ "memory.explorer.types": "分类",
101
+ "memory.explorer.timeline": "时间树",
102
+ "memory.explorer.detail": "详情",
103
+ "memory.explorer.emptyDetail": "在时间树中选择一条记忆查看全文",
104
+ "memory.explorer.copy": "复制全文",
105
+ "memory.explorer.copied": "已复制",
106
+ "memory.explorer.refresh": "刷新",
107
+ "memory.explorer.count": "共 {n} 条",
108
+ "memory.explorer.empty": "暂无记忆条目",
109
+ "memory.explorer.source": "来源",
110
+ "memory.explorer.created": "创建",
111
+ "memory.explorer.updated": "更新",
112
+ "memory.explorer.tags": "标签",
113
+ "memory.explorer.tagAdd": "添加标签",
114
+ "memory.explorer.tagRemove": "移除标签",
115
+ "memory.explorer.tagPlaceholder": "输入标签,回车添加",
116
+ "memory.explorer.tagsEmpty": "暂无标签",
117
+ "memory.explorer.importance": "重要性",
118
+ "memory.explorer.topK": "返回数量",
119
+ "memory.explorer.topKOption": "返回 {n} 条",
120
+ "memory.directory.untagged": "无标签",
121
+ "memory.directory.loading": "加载中…",
122
+ "memory.directory.empty": "暂无记忆条目",
123
+ "directory.editToggle": "编辑模式(显示删除)",
124
+ "directory.deleteConfirm": "确定删除这条记忆吗?",
125
+ "memory.card.open": "在主区记忆库中查看全文",
126
+ "memory.time.now": "刚刚",
127
+ "memory.time.seconds": "{n}秒前",
128
+ "memory.time.minutes": "{n}分钟前",
129
+ "memory.time.hours": "{n}小时前",
130
+ "memory.time.days": "{n}天前",
131
+ "memory.graph.title": "记忆图谱",
132
+ "memory.graph.aria": "记忆图谱",
133
+ "memory.graph.placeholder": "输入实体名,查看关联网络…",
134
+ "memory.graph.depth": "跳数",
135
+ "memory.graph.empty": "图谱待积累:随对话记忆的沉淀,实体与关系会自动进入图谱",
136
+ "memory.graph.notFound": "未找到该实体",
137
+ "memory.graph.attrs": "属性",
138
+ "memory.graph.related": "关联记忆",
139
+ "memory.graph.relation": "关系",
140
+ "memory.graph.sourceMemory": "查看来源记忆",
141
+ "memory.graph.hint": "点击节点展开 · 拖拽调整布局",
142
+ "memory.graph.viewInGraph": "在图谱中查看",
143
+ "memory.graph.loading": "加载中…",
144
+ "memory.graph.distance": "距中心 {n} 跳",
145
+ "memory.settings.vectorTitle": "向量搜索",
146
+ "memory.settings.vectorHint": "接入 OpenAI 兼容的 embeddings API 做语义搜索,可匹配字面不同但语义相近的记忆",
147
+ "memory.settings.vectorEnabled": "启用向量搜索",
148
+ "memory.settings.vectorBaseUrl": "API 地址 (Base URL)",
149
+ "memory.settings.vectorApiKey": "API Key",
150
+ "memory.settings.vectorModel": "模型名",
151
+ "memory.settings.vectorSave": "保存配置",
152
+ "memory.settings.vectorSaved": "配置已保存",
153
+ "memory.settings.vectorReindex": "重建索引",
154
+ "memory.settings.vectorReindexing": "索引中…",
155
+ "memory.settings.vectorReindexDone": "已索引 {n} 条",
156
+ "memory.settings.apiTokenTitle": "API Token(可选)",
157
+ "memory.settings.apiTokenHint": "设置后,写操作与密钥接口(画像/规则/命令/向量配置)需携带 Authorization: Bearer <token>;面板只读操作不受影响。清空并保存可关闭鉴权。",
158
+ "memory.settings.apiTokenPlaceholder": "留空 = 不鉴权(默认)",
159
+ "memory.settings.apiTokenSave": "保存 Token",
160
+ "memory.settings.apiTokenSaved": "Token 已保存",
161
+ "memory.wikilink.backlinks": "链接到此记忆",
162
+ "memory.wikilink.forward": "此记忆链接到",
163
+ "memory.wikilink.empty": "暂无关联记忆",
164
+ "memory.wikilink.loading": "加载中…",
165
+ "memory.wikilink.unresolved": "目标记忆不存在"
166
+ },
167
+ en: {
168
+ "memory.panel.empty": "No memories yet",
169
+ "memory.panel.open": "Memory",
170
+ "memory.tab.all": "All",
171
+ "memory.tab.preference": "Preferences",
172
+ "memory.tab.project": "Projects",
173
+ "memory.tab.decision": "Decisions",
174
+ "memory.tab.history": "History",
175
+ "memory.settings.title": "Memory Settings",
176
+ "memory.settings.profile": "User Profile",
177
+ "memory.settings.profileHint": "Describe yourself — the agent follows this every turn",
178
+ "memory.settings.profileSave": "Save Profile",
179
+ "memory.settings.profileSaved": "Profile saved",
180
+ "memory.settings.rules": "Rules",
181
+ "memory.settings.rulesHint": "Behavior rules the agent must follow every turn",
182
+ "memory.settings.autoTagTitle": "Auto-Tag",
183
+ "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)",
184
+ "memory.settings.autoTagEnabled": "Auto-tagging",
185
+ "memory.settings.autoTagSave": "Save",
186
+ "memory.settings.autoTagSaved": "Saved",
187
+ "memory.settings.ruleAdd": "Add Rule",
188
+ "memory.settings.rulePlaceholder": "e.g. Always lead with a conclusion",
189
+ "memory.settings.commands": "Custom Commands",
190
+ "memory.settings.commandsHint": "Register slash commands (/name) whose instruction is handed to the agent",
191
+ "memory.settings.cmdName": "Name",
192
+ "memory.settings.cmdDesc": "Description",
193
+ "memory.settings.cmdInstruction": "Instruction",
194
+ "memory.settings.cmdAdd": "Add Command",
195
+ "memory.settings.cmdDelete": "Delete",
196
+ "memory.settings.empty": "Nothing yet",
197
+ "memory.panel.semantic": "Semantic",
198
+ "memory.sidebar.aria": "Open memory panel",
199
+ "memory.view.label": "Memory",
200
+ "memory.overlay.close": "Close",
201
+ "memory.explorer.tabMemory": "Memories",
202
+ "memory.explorer.tabGraph": "Graph",
203
+ "memory.explorer.tabDirectory": "Directory",
204
+ "memory.explorer.tabSettings": "Settings",
205
+ "memory.explorer.search": "Search title or content…",
206
+ "memory.explorer.searchTitle": "Semantic Search",
207
+ "memory.explorer.types": "Types",
208
+ "memory.explorer.timeline": "Timeline",
209
+ "memory.explorer.detail": "Details",
210
+ "memory.explorer.emptyDetail": "Select a memory in the timeline to read it",
211
+ "memory.explorer.copy": "Copy content",
212
+ "memory.explorer.copied": "Copied",
213
+ "memory.explorer.refresh": "Refresh",
214
+ "memory.explorer.count": "{n} items",
215
+ "memory.explorer.empty": "No memories yet",
216
+ "memory.explorer.source": "Source",
217
+ "memory.explorer.created": "Created",
218
+ "memory.explorer.updated": "Updated",
219
+ "memory.explorer.tags": "Tags",
220
+ "memory.explorer.tagAdd": "Add tag",
221
+ "memory.explorer.tagRemove": "Remove tag",
222
+ "memory.explorer.tagPlaceholder": "Type a tag, press Enter",
223
+ "memory.explorer.tagsEmpty": "No tags",
224
+ "memory.explorer.importance": "Importance",
225
+ "memory.explorer.topK": "Results limit",
226
+ "memory.explorer.topKOption": "Return {n}",
227
+ "memory.directory.untagged": "Untagged",
228
+ "memory.directory.loading": "Loading…",
229
+ "memory.directory.empty": "No memories yet",
230
+ "directory.editToggle": "Edit mode",
231
+ "directory.deleteConfirm": "Delete this memory?",
232
+ "memory.card.open": "Open full text in the Memory tab",
233
+ "memory.time.now": "just now",
234
+ "memory.time.seconds": "{n}s ago",
235
+ "memory.time.minutes": "{n}m ago",
236
+ "memory.time.hours": "{n}h ago",
237
+ "memory.time.days": "{n}d ago",
238
+ "memory.graph.title": "Memory Graph",
239
+ "memory.graph.aria": "Memory graph",
240
+ "memory.graph.placeholder": "Type an entity name to see its network…",
241
+ "memory.graph.depth": "hops",
242
+ "memory.graph.empty": "The graph is waiting for data: entities and relations accumulate as memories are extracted",
243
+ "memory.graph.notFound": "Entity not found",
244
+ "memory.graph.attrs": "Attributes",
245
+ "memory.graph.related": "Related memories",
246
+ "memory.graph.relation": "Relation",
247
+ "memory.graph.sourceMemory": "View source memory",
248
+ "memory.graph.hint": "Click a node to expand · drag to rearrange",
249
+ "memory.graph.viewInGraph": "View in graph",
250
+ "memory.graph.loading": "Loading…",
251
+ "memory.graph.distance": "{n} hop(s) from root",
252
+ "memory.settings.vectorTitle": "Vector Search",
253
+ "memory.settings.vectorHint": "Connect an OpenAI-compatible embeddings API for semantic search by meaning, not just keywords",
254
+ "memory.settings.vectorEnabled": "Enable vector search",
255
+ "memory.settings.vectorBaseUrl": "Base URL",
256
+ "memory.settings.vectorApiKey": "API Key",
257
+ "memory.settings.vectorModel": "Model",
258
+ "memory.settings.vectorSave": "Save Config",
259
+ "memory.settings.vectorSaved": "Config saved",
260
+ "memory.settings.vectorReindex": "Reindex",
261
+ "memory.settings.vectorReindexing": "Indexing…",
262
+ "memory.settings.vectorReindexDone": "Indexed {n} items",
263
+ "memory.settings.apiTokenTitle": "API Token (optional)",
264
+ "memory.settings.apiTokenHint": "When set, write operations and secret endpoints (profile/rules/commands/vector config) require Authorization: Bearer <token>. Read-only panel calls stay open. Save empty to disable.",
265
+ "memory.settings.apiTokenPlaceholder": "Empty = no auth (default)",
266
+ "memory.settings.apiTokenSave": "Save Token",
267
+ "memory.settings.apiTokenSaved": "Token saved",
268
+ "memory.wikilink.backlinks": "Links to this memory",
269
+ "memory.wikilink.forward": "This memory links to",
270
+ "memory.wikilink.empty": "No linked memories",
271
+ "memory.wikilink.loading": "Loading…",
272
+ "memory.wikilink.unresolved": "Target memory not found"
273
+ }
274
+ };
275
+
276
+ function typeLabel(t, type) {
277
+ const key = `memory.tab.${type}`;
278
+ const label = t(key);
279
+ return label && label !== key ? label : String(type);
280
+ }
281
+
282
+ function formatDate(value) {
283
+ if (!value) return "—";
284
+ const date = new Date(value);
285
+ return Number.isNaN(date.getTime()) ? "—" : date.toLocaleString();
286
+ }
287
+
288
+ // Relative time ("2分钟前") keeps cards scannable; anything older than a
289
+ // week falls back to the absolute date. The card meta shows the relative
290
+ // form and carries the full timestamp in a title tooltip.
291
+ function formatRelativeTime(value, t) {
292
+ if (!value) return "—";
293
+ const ms = new Date(value).getTime();
294
+ if (Number.isNaN(ms)) return "—";
295
+ const diff = Date.now() - ms;
296
+ if (diff < 60_000) return t("memory.time.now");
297
+ const minutes = Math.floor(diff / 60_000);
298
+ if (minutes < 60) return t("memory.time.minutes").replace("{n}", String(minutes));
299
+ const hours = Math.floor(minutes / 60);
300
+ if (hours < 24) return t("memory.time.hours").replace("{n}", String(hours));
301
+ const days = Math.floor(hours / 24);
302
+ if (days < 7) return t("memory.time.days").replace("{n}", String(days));
303
+ return new Date(ms).toLocaleDateString();
304
+ }
305
+
306
+ // Memory-library stylesheet, injected once per page following the host's
307
+ // data-plugin-css convention. Every value resolves to the host's design
308
+ // tokens: background layers, label/border/interactive aliases and the
309
+ // --dsw-font-* scale, so the page reads as a first-party view beside
310
+ // Chat / Trajectory (flat layer-1 canvas, hairline column separators,
311
+ // text-turns-brand-blue active states — no boxed panels).
312
+ const CSS_TAG = "@modusensus/dsh-mneme/drawer.css";
313
+ const css = [
314
+ // --- sidebar foot trigger (wide row / collapsed rail icon) ---
315
+ ".mneme-trigger{box-sizing:border-box;cursor:pointer;width:calc(100% + 4px);height:42px;color:var(--dsw-alias-label-primary);background:0 0;border:none;border-radius:12px;flex:none;align-items:center;gap:8px;margin:4px -2px;padding:0 10px 0 8px;font-family:inherit;font-size:14px;line-height:22px;display:flex;overflow:hidden}",
316
+ ".mneme-trigger:hover{background:var(--dsw-alias-interactive-bg-hover)}",
317
+ ".mneme-trigger.mneme-rail{border-radius:50%;justify-content:center;gap:0;width:36px;height:36px;margin:8px 0 10px;padding:0}",
318
+ ".mneme-trigger-label{white-space:nowrap;overflow:hidden}",
319
+ // --- shared controls ---
320
+ ".mneme-search{box-sizing:border-box;height:30px;padding:0 10px;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;transition:border-color .12s,box-shadow .12s}",
321
+ ".mneme-search:focus{border-color:var(--dsw-alias-state-business-primary);box-shadow:0 0 0 3px color-mix(in srgb,var(--dsw-alias-state-business-primary) 15%,transparent)}",
322
+ ".mneme-search::placeholder{color:var(--dsw-alias-label-tertiary)}",
323
+ ".mneme-chip{height:26px;padding:0 10px;border-radius:8px;border:1px solid transparent;background:none;color:var(--dsw-alias-label-secondary);cursor:pointer;font-family:inherit;font-size:12px;line-height:16px;display:inline-flex;align-items:center}",
324
+ ".mneme-chip:hover{background:var(--dsw-alias-interactive-bg-hover)}",
325
+ ".mneme-chip.mneme-active{color:var(--dsw-alias-state-business-primary);background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 10%,transparent)}",
326
+ ".mneme-select{box-sizing:border-box;height:30px;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:13px;outline:none}",
327
+ ".mneme-footbtn{border:none;background:none;color:var(--dsw-alias-label-secondary);cursor:pointer;font-family:inherit;font-size:12px;line-height:16px;padding:3px 8px;border-radius:6px}",
328
+ ".mneme-footbtn:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}",
329
+ ".mneme-hint{color:var(--dsw-alias-label-tertiary);padding:24px 0;text-align:center;font-size:13px}",
330
+ ".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}",
331
+ ".mneme-entitychip:hover{background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 10%,transparent)}",
332
+ // --- tag chips in the detail meta (v0.6.2) ---
333
+ ".mneme-tagrow{display:flex;flex-wrap:wrap;gap:6px;align-items:center}",
334
+ ".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}",
335
+ ".mneme-tagchip:hover{background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 10%,transparent)}",
336
+ ".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}",
337
+ ".mneme-tagremove:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}",
338
+ ".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}",
339
+ ".mneme-tagadd:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}",
340
+ ".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}",
341
+ ".mneme-taginput:focus{border-color:var(--dsw-alias-state-business-primary)}",
342
+ ".mneme-tagempty{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:16px}",
343
+ // --- main-area memory library page ---
344
+ ".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)}",
345
+ ".mneme-xbar{flex:none;display:flex;align-items:center;gap:6px;border-bottom:1px solid var(--dsw-alias-border-l2);padding:0 16px}",
346
+ ".mneme-filterbar{flex:none;display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:8px 16px;border-bottom:1px solid var(--dsw-alias-border-l2)}",
347
+ ".mneme-vtabs{display:flex;align-items:stretch;height:38px}",
348
+ ".mneme-vtab{position:relative;border:0;background:none;cursor:pointer;padding:0 10px;color:var(--dsw-alias-label-tertiary);font-family:inherit;font-size:13px;font-weight:500;line-height:16px;display:inline-flex;align-items:center;gap:6px}",
349
+ ".mneme-vtab:hover{color:var(--dsw-alias-label-primary)}",
350
+ ".mneme-vtab.mneme-active{color:var(--dsw-alias-state-business-primary)}",
351
+ ".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)}",
352
+ ".mneme-xtools{margin-left:auto;display:flex;align-items:center;gap:8px;padding:0 0 0 12px}",
353
+ ".mneme-xcount{flex:none;font-size:12px;line-height:16px;color:var(--dsw-alias-label-tertiary);white-space:nowrap}",
354
+ // --- three-column browse layout: hairline separators, no outer box ---
355
+ ".mneme-xmain{flex:1;min-height:0;display:flex;flex-direction:row;overflow:hidden}",
356
+ ".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}",
357
+ ".mneme-xside--filter{width:214px}",
358
+ ".mneme-xbrowse{flex:1;min-width:0;min-height:0;display:flex;flex-direction:column;overflow:hidden}",
359
+ ".mneme-xrow{display:flex;align-items:center;gap:8px;flex-wrap:wrap}",
360
+ ".mneme-xsearch{width:100%}",
361
+ ".mneme-xselect{flex:1;min-width:0}",
362
+ ".mneme-xcolhead{flex:none;font-size:12px;font-weight:500;color:var(--dsw-alias-label-tertiary);padding:2px 8px 8px}",
363
+ ".mneme-xtype{display:flex;justify-content:space-between;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}",
364
+ ".mneme-xtype:hover{background:var(--dsw-alias-interactive-bg-hover)}",
365
+ ".mneme-xtype.mneme-active{background:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-primary);font-weight:500}",
366
+ ".mneme-xcount2{flex:none;font-size:12px;color:var(--dsw-alias-label-tertiary)}",
367
+ ".mneme-xmonth{display:flex;align-items:center;gap:4px;width:100%;padding:6px 8px 4px;border:none;border-radius:8px;background:none;color:var(--dsw-alias-label-primary);cursor:pointer;font-family:inherit;font-size:13px;font-weight:500;line-height:18px;text-align:left}",
368
+ ".mneme-xmonth:first-child{margin-top:0}",
369
+ ".mneme-xmonth:hover{background:var(--dsw-alias-interactive-bg-hover)}",
370
+ ".mneme-xcaret{flex:none;font-size:10px;color:var(--dsw-alias-label-tertiary);width:10px}",
371
+ ".mneme-xday{display:flex;align-items:center;gap:4px;margin:6px 0 2px 22px;padding:2px 6px;border:none;border-radius:6px;background:none;font-family:inherit;font-size:12px;line-height:16px;color:var(--dsw-alias-label-tertiary);cursor:pointer;text-align:left}",
372
+ ".mneme-xday:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}",
373
+ ".mneme-xitem{display:flex;gap:8px;align-items:baseline;width:calc(100% - 22px);margin-left:22px;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}",
374
+ ".mneme-xitem:hover{background:var(--dsw-alias-interactive-bg-hover)}",
375
+ ".mneme-xitem.mneme-active{background:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-primary)}",
376
+ ".mneme-xtime{flex:none;font-size:12px;line-height:16px;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums}",
377
+ ".mneme-xname{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
378
+ ".mneme-xempty{padding:32px 16px;text-align:center;color:var(--dsw-alias-label-tertiary);font-size:13px}",
379
+ // --- detail column ---
380
+ ".mneme-xdetail{flex:none;max-height:44%;min-height:0;overflow-y:auto;padding:14px 20px 16px;border-bottom:1px solid var(--dsw-alias-border-l2)}",
381
+ ".mneme-xtree{flex:1;min-height:0;overflow-y:auto;padding:8px 10px 28px}",
382
+ ".mneme-xmain{display:flex;flex-direction:column}",
383
+ ".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)}",
384
+ ".mneme-xcards{flex:1;display:flex;flex-direction:row;gap:12px;padding:12px;overflow:hidden}",
385
+ ".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}",
386
+ ".mneme-card--search{width:236px;flex:none}",
387
+ ".mneme-card--tree{width:260px;flex:none}",
388
+ ".mneme-card--detail{flex:1;min-width:0}",
389
+ ".mneme-xdinner{max-width:720px}",
390
+ ".mneme-xdtitle{font-size:16px;font-weight:600;line-height:24px;color:var(--dsw-alias-label-primary);margin-bottom:10px;word-break:break-word}",
391
+ ".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)}",
392
+ ".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}",
393
+ ".mneme-xdactions{display:flex;gap:8px;margin-top:18px}",
394
+ // --- wiki-link backlinks panel (detail pane footer) ---
395
+ ".mneme-backlinks{margin-top:18px;padding-top:14px;border-top:1px solid var(--dsw-alias-border-l1);display:flex;flex-direction:column;gap:12px}",
396
+ ".mneme-bl-block{display:flex;flex-direction:column;gap:4px}",
397
+ ".mneme-bl-head{font-size:12px;font-weight:500;color:var(--dsw-alias-label-tertiary)}",
398
+ ".mneme-bl-list{display:flex;flex-wrap:wrap;gap:6px}",
399
+ ".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}",
400
+ ".mneme-bl-link:hover{background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 10%,transparent)}",
401
+ ".mneme-bl-link.mneme-bl-link--dim{color:var(--dsw-alias-label-tertiary);cursor:default}",
402
+ ".mneme-bl-link.mneme-bl-link--dim:hover{background:none}",
403
+ ".mneme-bl-title{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
404
+ ".mneme-bl-meta{flex:none;font-size:11px;color:var(--dsw-alias-label-tertiary)}",
405
+ ".mneme-bl-empty{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}",
406
+ // --- inline [[wiki-link]] links inside the detail content ---
407
+ ".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}",
408
+ ".mneme-wikilink:hover{background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 8%,transparent)}",
409
+ // --- graph sub-view (fills the content area under the tabs) ---
410
+ ".mneme-graph{flex:1;min-height:0;width:100%;display:flex;flex-direction:column;padding:12px 16px 16px;box-sizing:border-box}",
411
+ ".mneme-graphbar{display:flex;gap:8px;align-items:center;flex:none;margin-bottom:10px}",
412
+ ".mneme-graphsvg{flex:1;min-height:180px;width:100%;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-button-elevated-fill);cursor:grab;touch-action:none}",
413
+ ".mneme-gnode{cursor:pointer}",
414
+ ".mneme-gnode circle{stroke:var(--dsw-alias-bg-layer-2);stroke-width:2;transition:stroke-width .12s}",
415
+ ".mneme-gnode:hover circle{stroke-width:4}",
416
+ ".mneme-gnode.mneme-groot circle{stroke:var(--dsw-alias-state-business-primary);stroke-width:3}",
417
+ ".mneme-glabel{fill:var(--dsw-alias-label-secondary);font-size:11px;text-anchor:middle;pointer-events:none;user-select:none}",
418
+ ".mneme-gedge{stroke:var(--dsw-alias-label-dimmed);stroke-width:1.5;cursor:pointer}",
419
+ ".mneme-gedge:hover{stroke:var(--dsw-alias-state-business-primary)}",
420
+ ".mneme-gedge-dashed{stroke-dasharray:5 4;opacity:.7}",
421
+ ".mneme-graphhint{flex:none;color:var(--dsw-alias-label-tertiary);font-size:13px;text-align:center;padding:6px 0 2px}",
422
+ ".mneme-graphside{flex:none;max-height:220px;overflow-y:auto;border-top:1px solid var(--dsw-alias-border-l1);margin-top:10px;padding-top:10px}",
423
+ ".mneme-gs-title{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary);margin-bottom:4px;display:flex;justify-content:space-between;align-items:baseline;gap:8px}",
424
+ ".mneme-gs-meta{font-size:13px;color:var(--dsw-alias-label-tertiary);margin-bottom:6px}",
425
+ ".mneme-gs-attr{display:flex;gap:6px;font-size:13px;padding:2px 0}",
426
+ ".mneme-gs-attrkey{flex:none;color:var(--dsw-alias-label-tertiary);font-size:13px}",
427
+ ".mneme-gs-attrval{color:var(--dsw-alias-label-secondary);word-break:break-word;font-size:13px}",
428
+ ".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}",
429
+ ".mneme-gs-link:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}",
430
+ // --- directory sub-view (v0.6.3): tag folders + memory entries ---
431
+ ".mneme-directory{flex:1;min-height:0;overflow-y:auto;padding:8px 10px 28px}",
432
+ ".mneme-edit-toggle{margin:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;cursor:pointer;user-select:none}",
433
+ ".mneme-dir-item{position:relative;display:flex;align-items:center;gap:6px;padding:2px 6px;border-radius:4px}",
434
+ ".mneme-dir-item:hover{background:rgba(127,127,127,.18)}",
435
+ ".mneme-tree-line{width:14px;height:1px;border-top:1px dashed rgba(127,127,127,.5);flex:none}",
436
+ ".mneme-dir-item .mneme-dir-del{opacity:0;transition:opacity .12s}",
437
+ ".mneme-dir-item:hover .mneme-dir-del{opacity:1}",
438
+ ".mneme-dir-del{margin-left:auto;background:none;border:none;color:#e5484d;cursor:pointer;font-size:13px;line-height:1;padding:2px 4px}",
439
+ ".mneme-dir-del:hover{filter:brightness(1.25)}",
440
+ ".mneme-edit-on .mneme-dir-del{opacity:1}",
441
+ ".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}",
442
+ ".mneme-dir-folder{margin-bottom:4px}",
443
+ ".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}",
444
+ ".mneme-dir-folderhead:hover{background:var(--dsw-alias-interactive-bg-hover)}",
445
+ ".mneme-dir-caret{flex:none;width:10px;text-align:center;color:var(--dsw-alias-label-tertiary);font-size:12px}",
446
+ ".mneme-dir-fname{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
447
+ ".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}",
448
+ ".mneme-dir-fbody{margin:2px 0 4px 22px;padding-left:8px;border-left:1px solid var(--dsw-alias-border-l2)}",
449
+ ".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}",
450
+ ".mneme-dir-item:hover{background:var(--dsw-alias-interactive-bg-hover)}",
451
+ ".mneme-dir-item.mneme-active{background:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-primary)}",
452
+ ".mneme-dir-ititle{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
453
+ ".mneme-dir-itime{flex:none;font-size:12px;line-height:16px;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums}",
454
+ ".mneme-dir-empty{color:var(--dsw-alias-label-tertiary);padding:24px 0;text-align:center;font-size:13px}",
455
+ // --- settings sub-view ---
456
+ ".mneme-xsettings{flex:1;min-height:0;overflow-y:auto;padding:24px 24px 48px}",
457
+ ".mneme-xsettings-inner{max-width:640px}",
458
+ // --- hero fallback: full-viewport memory library when no tab ring exists ---
459
+ // The host hides the whole conversation tab ring while a session is
460
+ // blank (hero screen), so the sidebar entry cannot activate the tab
461
+ // there. This surface reuses the exact MemoryExplorer UI at full size —
462
+ // not a side drawer — so the library stays reachable from any state.
463
+ ".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}",
464
+ ".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)}",
465
+ ".mneme-overlaytitle{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary)}",
466
+ ".mneme-overlaybody{flex:1;min-height:0;display:flex;flex-direction:column;overflow:hidden}",
467
+ "@keyframes mneme-fadein{from{opacity:0}to{opacity:1}}"
468
+ ].join("\n");
469
+ if (typeof document !== "undefined" && document.querySelector(`style[data-plugin-css="${CSS_TAG}"]`) === null) {
470
+ const tag = document.createElement("style");
471
+ tag.dataset.plugin = "@modusensus/dsh-mneme";
472
+ tag.dataset.pluginCss = CSS_TAG;
473
+ tag.textContent = css;
474
+ document.head.appendChild(tag);
475
+ }
476
+
477
+ const h = react.createElement;
478
+
479
+ // --- graph view constants ---
480
+ // Entity type → node fill. The host has no palette token for categorical
481
+ // data, so these are fixed hues tuned for both light and dark themes
482
+ // (medium saturation, similar luminance).
483
+ const TYPE_COLORS = {
484
+ person: "#3b82f6",
485
+ project: "#22c55e",
486
+ concept: "#a855f7",
487
+ technology: "#f59e0b",
488
+ organization: "#06b6d4"
489
+ };
490
+ function typeColor(type) {
491
+ return TYPE_COLORS[type] || "#94a3b8";
492
+ }
493
+ function nodeRadius(n) {
494
+ // mention_count → area-ish growth, clamped so hubs stay legible.
495
+ return 7 + Math.min(20, Math.max(1, n.mention_count ?? 1)) * 0.55;
496
+ }
497
+
498
+ // Deterministic golden-angle spiral: no two nodes start overlapping, and
499
+ // re-running the layout for the same data is stable (no random seeding).
500
+ function initialPositions(nodes, width, height) {
501
+ const cx = width / 2;
502
+ const cy = height / 2;
503
+ return nodes.map((n, i) => {
504
+ const r = i === 0 ? 0 : 34 + 13 * Math.sqrt(i);
505
+ const a = i * 2.39996;
506
+ return { ...n, x: cx + r * Math.cos(a), y: cy + r * Math.sin(a), vx: 0, vy: 0, pinned: false };
507
+ });
508
+ }
509
+
510
+ // --- wiki-link (v0.6.1) -------------------------------------------------
511
+ // Splits detail content into literal text and clickable [[target]] /
512
+ // [[display|target]] links. Mirrors src/parser/wiki-link.js: single pipe
513
+ // only, empty / multi-pipe / unclosed markers stay literal text.
514
+ function wikilinkSegments(text, onClick) {
515
+ if (typeof text !== "string" || text.length === 0) return text;
516
+ const out = [];
517
+ let last = 0;
518
+ let key = 0;
519
+ const re = /\[\[([^\[\]]*)\]\]/g;
520
+ let m;
521
+ while ((m = re.exec(text)) !== null) {
522
+ const parts = m[1].split("|");
523
+ if (parts.length > 2) continue; // 多管道 → 非法,保留字面文本
524
+ const target = (parts.length === 2 ? parts[1] : parts[0]).trim();
525
+ if (!target) continue; // 空目标 → 非法
526
+ if (m.index > last) out.push(text.slice(last, m.index));
527
+ out.push(h("button", {
528
+ key: `w${key++}`,
529
+ type: "button",
530
+ className: "mneme-wikilink",
531
+ title: target,
532
+ onClick: () => onClick(target)
533
+ }, parts[0].trim() || target));
534
+ last = m.index + m[0].length;
535
+ }
536
+ if (last < text.length) out.push(text.slice(last));
537
+ return out.length ? out : text;
538
+ }
539
+
540
+ // Backlinks panel: the two wiki-link relation blocks under the detail pane.
541
+ // Backlinks = memories whose content links to this one; forward = memories
542
+ // this one links to (unresolved targets surface as dim, non-clickable).
543
+ // Mounted per-selection (the surrounding Fragment keys on selected.id), so
544
+ // state resets and the fetches re-run on every switch.
545
+ function BacklinksPanel({ memory, t, onJump }) {
546
+ const [back, setBack] = useState(null); // null = loading
547
+ const [forward, setForward] = useState(null);
548
+
549
+ useEffect(() => {
550
+ let cancelled = false;
551
+ if (!memory || !memory.id) return;
552
+ apiFetch(`/api/dsh-mneme/wikilinks/backlinks?id=${encodeURIComponent(memory.id)}`)
553
+ .then((r) => (r.ok ? r.json() : { backlinks: [] }))
554
+ .then((j) => { if (!cancelled) setBack(Array.isArray(j.backlinks) ? j.backlinks : []); })
555
+ .catch(() => { if (!cancelled) setBack([]); });
556
+ apiFetch(`/api/dsh-mneme/wikilinks/forward?id=${encodeURIComponent(memory.id)}`)
557
+ .then((r) => (r.ok ? r.json() : { links: [] }))
558
+ .then((j) => { if (!cancelled) setForward(Array.isArray(j.links) ? j.links : []); })
559
+ .catch(() => { if (!cancelled) setForward([]); });
560
+ return () => { cancelled = true; };
561
+ }, [memory && memory.id]);
562
+
563
+ if (!memory || !memory.id) return null;
564
+
565
+ const loading = back === null || forward === null;
566
+ const row = (link, i) => {
567
+ const canJump = !!link.id;
568
+ return h("button", {
569
+ key: link.id ?? `unresolved-${i}`,
570
+ type: "button",
571
+ className: canJump ? "mneme-bl-link" : "mneme-bl-link mneme-bl-link--dim",
572
+ title: canJump ? t("memory.card.open") : t("memory.wikilink.unresolved"),
573
+ disabled: !canJump,
574
+ onClick: () => onJump && onJump({ id: link.id })
575
+ },
576
+ h("span", { className: "mneme-bl-title" }, link.title || "—"),
577
+ link.type && h("span", { className: "mneme-bl-meta" }, typeLabel(t, link.type))
578
+ );
579
+ };
580
+ const section = (label, items) =>
581
+ h("div", { className: "mneme-bl-block" },
582
+ h("div", { className: "mneme-bl-head" }, label),
583
+ loading
584
+ ? h("div", { className: "mneme-bl-empty" }, t("memory.wikilink.loading"))
585
+ : items.length === 0
586
+ ? h("div", { className: "mneme-bl-empty" }, t("memory.wikilink.empty"))
587
+ : h("div", { className: "mneme-bl-list" }, items.map(row))
588
+ );
589
+
590
+ return h("div", { className: "mneme-backlinks" },
591
+ section(t("memory.wikilink.backlinks"), back || []),
592
+ section(t("memory.wikilink.forward"), forward || [])
593
+ );
594
+ }
595
+
596
+ // --- Graph view: ego-graph of one entity, zero-dependency SVG force layout ---
597
+ // The DSH client module table only whitelists platform modules, so a graph
598
+ // library like vis-network cannot be required from a plugin. A hand-rolled
599
+ // spring simulation (repulsion + edge springs + center gravity, damped) is
600
+ // plenty for the ≤40 nodes the ego API returns.
601
+ function GraphPanel({ t, focusEntity, onJumpMemory }) {
602
+ const [entityName, setEntityName] = useState(focusEntity || "");
603
+ const [inputValue, setInputValue] = useState(focusEntity || "");
604
+ const [depth, setDepth] = useState(1);
605
+ const [data, setData] = useState(null);
606
+ const [status, setStatus] = useState("idle"); // idle | loading | ready | notfound | error
607
+ const [selected, setSelected] = useState(null); // { kind: "node"|"edge", node?|edge? }
608
+ const [attrs, setAttrs] = useState([]);
609
+ const [related, setRelated] = useState([]);
610
+ const svgRef = useRef(null);
611
+ const posRef = useRef([]); // live simulation positions, not React state
612
+ const dragRef = useRef(null); // { id, moved }
613
+ const [frame, setFrame] = useState(0); // re-render tick driven by the simulation
614
+
615
+ useEffect(() => {
616
+ if (focusEntity && focusEntity !== entityName) {
617
+ setEntityName(focusEntity);
618
+ setInputValue(focusEntity);
619
+ }
620
+ // eslint-disable-next-line react-hooks/exhaustive-deps
621
+ }, [focusEntity]);
622
+
623
+ const load = useCallback(async (name, d) => {
624
+ if (!name) { setData(null); setStatus("idle"); return; }
625
+ setStatus("loading");
626
+ setSelected(null);
627
+ try {
628
+ const res = await apiFetch(`/api/dsh-mneme/semantic/graph/ego?entity=${encodeURIComponent(name)}&depth=${d}`);
629
+ if (res.status === 404) { setData(null); setStatus("notfound"); return; }
630
+ if (!res.ok) { setData(null); setStatus("error"); return; }
631
+ const json = await res.json();
632
+ setData(json);
633
+ setStatus("ready");
634
+ } catch {
635
+ setData(null);
636
+ setStatus("error");
637
+ }
638
+ }, []);
639
+
640
+ useEffect(() => { load(entityName, depth); }, [load, entityName, depth]);
641
+
642
+ // Detail pane: node → current attrs + entity: search for related memories.
643
+ useEffect(() => {
644
+ setAttrs([]);
645
+ setRelated([]);
646
+ if (!selected || selected.kind !== "node") return;
647
+ const name = selected.node.name;
648
+ let cancelled = false;
649
+ apiFetch(`/api/dsh-mneme/semantic/graph/entity-attrs?entity=${encodeURIComponent(name)}`)
650
+ .then((r) => (r.ok ? r.json() : { attrs: [] }))
651
+ .then((j) => { if (!cancelled) setAttrs(Array.isArray(j.attrs) ? j.attrs : []); })
652
+ .catch(() => {});
653
+ apiFetch(`/api/dsh-mneme/search?q=${encodeURIComponent("entity:" + name)}&limit=10`)
654
+ .then((r) => (r.ok ? r.json() : { items: [] }))
655
+ .then((j) => { if (!cancelled) setRelated(Array.isArray(j.items) ? j.items : []); })
656
+ .catch(() => {});
657
+ return () => { cancelled = true; };
658
+ }, [selected]);
659
+
660
+ // Simulation: run in rAF against posRef, tick React only every few frames.
661
+ // Re-seeds when data changes; dragging writes straight into posRef.
662
+ useEffect(() => {
663
+ if (!data || data.nodes.length === 0) return;
664
+ const width = Math.max(280, svgRef.current?.clientWidth || 380);
665
+ const height = 300;
666
+ posRef.current = initialPositions(data.nodes, width, height);
667
+ const byId = new Map(posRef.current.map((n) => [n.id, n]));
668
+ const edges = data.edges;
669
+ let raf = 0;
670
+ let n = 0;
671
+ const tick = () => {
672
+ const nodes = posRef.current;
673
+ // pairwise repulsion, capped so far nodes don't explode
674
+ for (let i = 0; i < nodes.length; i++) {
675
+ for (let j = i + 1; j < nodes.length; j++) {
676
+ const a = nodes[i], b = nodes[j];
677
+ let dx = b.x - a.x, dy = b.y - a.y;
678
+ let d2 = dx * dx + dy * dy;
679
+ if (d2 < 1) { dx = (Math.random() - 0.5) || 1; dy = (Math.random() - 0.5) || 1; d2 = dx * dx + dy * dy; }
680
+ const d = Math.sqrt(d2);
681
+ const f = Math.min(2200 / d2, 6);
682
+ const fx = (dx / d) * f, fy = (dy / d) * f;
683
+ a.vx -= fx; a.vy -= fy; b.vx += fx; b.vy += fy;
684
+ }
685
+ }
686
+ // edge springs pull toward the target length
687
+ for (const e of edges) {
688
+ const a = byId.get(e.from), b = byId.get(e.to);
689
+ if (!a || !b) continue;
690
+ const dx = b.x - a.x, dy = b.y - a.y;
691
+ const d = Math.sqrt(dx * dx + dy * dy) || 1;
692
+ const f = (d - 90) * 0.02;
693
+ const fx = (dx / d) * f, fy = (dy / d) * f;
694
+ a.vx += fx; a.vy += fy; b.vx -= fx; b.vy -= fy;
695
+ }
696
+ let energy = 0;
697
+ for (const node of nodes) {
698
+ // gentle gravity toward center keeps the cloud from drifting off-canvas
699
+ node.vx += (width / 2 - node.x) * 0.002;
700
+ node.vy += (height / 2 - node.y) * 0.002;
701
+ if (node.pinned || node === dragRef.current?.node) { node.vx = 0; node.vy = 0; continue; }
702
+ node.vx *= 0.85; node.vy *= 0.85;
703
+ node.x = Math.max(nodeRadius(node) + 4, Math.min(width - nodeRadius(node) - 4, node.x + node.vx));
704
+ node.y = Math.max(nodeRadius(node) + 14, Math.min(height - nodeRadius(node) - 14, node.y + node.vy));
705
+ energy += Math.abs(node.vx) + Math.abs(node.vy);
706
+ }
707
+ n++;
708
+ if (n % 3 === 0) setFrame((f) => f + 1);
709
+ if (n < 300 && energy > 0.4) raf = requestAnimationFrame(tick);
710
+ };
711
+ raf = requestAnimationFrame(tick);
712
+ return () => cancelAnimationFrame(raf);
713
+ }, [data]);
714
+
715
+ // drag handling on the svg surface
716
+ const onNodeMouseDown = (e, node) => {
717
+ e.preventDefault();
718
+ dragRef.current = { node, moved: false };
719
+ const startX = e.clientX, startY = e.clientY;
720
+ const origX = node.x, origY = node.y;
721
+ const svg = svgRef.current;
722
+ const rect = svg.getBoundingClientRect();
723
+ const scale = 380 / Math.max(1, rect.width); // viewBox width / css width
724
+ const onMove = (ev) => {
725
+ dragRef.current.moved = true;
726
+ node.x = origX + (ev.clientX - startX) * scale;
727
+ node.y = origY + (ev.clientY - startY) * scale;
728
+ setFrame((f) => f + 1);
729
+ };
730
+ const onUp = () => {
731
+ window.removeEventListener("mousemove", onMove);
732
+ window.removeEventListener("mouseup", onUp);
733
+ setTimeout(() => { dragRef.current = null; }, 0);
734
+ };
735
+ window.addEventListener("mousemove", onMove);
736
+ window.addEventListener("mouseup", onUp);
737
+ };
738
+
739
+ const onNodeClick = (node) => {
740
+ if (dragRef.current?.moved) return; // it was a drag, not a click
741
+ setSelected({ kind: "node", node });
742
+ };
743
+ const onEdgeClick = (edge) => setSelected({ kind: "edge", edge });
744
+
745
+ const nodes = posRef.current;
746
+ const nodeById = new Map(nodes.map((n) => [n.id, n]));
747
+ const VIEW_W = 380, VIEW_H = 300;
748
+
749
+ const side = selected?.kind === "node"
750
+ ? h("div", { className: "mneme-graphside" },
751
+ h("div", { className: "mneme-gs-title" },
752
+ h("span", null, selected.node.name),
753
+ h("span", { className: "mneme-gs-meta" },
754
+ `${typeLabel(t, selected.node.type || "concept")} · ★${selected.node.mention_count ?? 1}`)
755
+ ),
756
+ attrs.length > 0 && h("div", null,
757
+ h("div", { className: "mneme-gs-meta" }, t("memory.graph.attrs")),
758
+ attrs.map((a, i) => h("div", { key: i, className: "mneme-gs-attr" },
759
+ h("span", { className: "mneme-gs-attrkey" }, `${a.key}:`),
760
+ h("span", { className: "mneme-gs-attrval" }, a.value)
761
+ ))
762
+ ),
763
+ related.length > 0 && h("div", { style: { marginTop: 8 } },
764
+ h("div", { className: "mneme-gs-meta" }, t("memory.graph.related")),
765
+ related.map((m) => h("button", {
766
+ key: m.id,
767
+ type: "button",
768
+ className: "mneme-gs-link",
769
+ title: t("memory.card.open"),
770
+ onClick: () => onJumpMemory && onJumpMemory(m)
771
+ }, m.title || m.content?.slice(0, 60)))
772
+ )
773
+ )
774
+ : selected?.kind === "edge"
775
+ ? h("div", { className: "mneme-graphside" },
776
+ h("div", { className: "mneme-gs-title" },
777
+ h("span", null,
778
+ `${nodeById.get(selected.edge.from)?.name ?? "?"} → ${selected.edge.relation_type} → ${nodeById.get(selected.edge.to)?.name ?? "?"}`)
779
+ ),
780
+ h("div", { className: "mneme-gs-meta" },
781
+ `${t("memory.graph.relation")} · ${formatRelativeTime(selected.edge.created_at, t)}`),
782
+ selected.edge.memory_id && h("button", {
783
+ className: "mneme-footbtn",
784
+ onClick: () => onJumpMemory && onJumpMemory({ id: selected.edge.memory_id })
785
+ }, t("memory.graph.sourceMemory"))
786
+ )
787
+ : null;
788
+
789
+ return h("div", { className: "mneme-graph" },
790
+ h("div", { className: "mneme-graphbar" },
791
+ h("input", {
792
+ className: "mneme-search",
793
+ style: { flex: 1, minWidth: 0, maxWidth: 280 },
794
+ placeholder: t("memory.graph.placeholder"),
795
+ value: inputValue,
796
+ onChange: (e) => setInputValue(e.target.value),
797
+ onKeyDown: (e) => { if (e.key === "Enter") setEntityName(inputValue.trim()); }
798
+ }),
799
+ h("button", {
800
+ className: depth === 2 ? "mneme-chip mneme-active" : "mneme-chip",
801
+ title: t("memory.graph.depth"),
802
+ onClick: () => setDepth(depth === 1 ? 2 : 1)
803
+ }, `${depth} ${t("memory.graph.depth")}`)
804
+ ),
805
+ status === "idle" && h("div", { className: "mneme-hint" }, t("memory.graph.empty")),
806
+ status === "loading" && h("div", { className: "mneme-hint" }, t("memory.graph.loading")),
807
+ status === "notfound" && h("div", { className: "mneme-hint" }, t("memory.graph.notFound")),
808
+ status === "error" && h("div", { className: "mneme-hint" }, t("memory.panel.empty")),
809
+ status === "ready" && (data.nodes.length <= 1
810
+ ? h("div", { className: "mneme-hint" }, t("memory.graph.empty"))
811
+ : h(react.Fragment, null,
812
+ h("svg", {
813
+ ref: svgRef,
814
+ className: "mneme-graphsvg",
815
+ viewBox: `0 0 ${VIEW_W} ${VIEW_H}`
816
+ },
817
+ data.edges.map((e) => {
818
+ const a = nodeById.get(e.from), b = nodeById.get(e.to);
819
+ if (!a || !b) return null;
820
+ return h("line", {
821
+ key: e.id,
822
+ x1: a.x, y1: a.y, x2: b.x, y2: b.y,
823
+ className: e.memory_id ? "mneme-gedge" : "mneme-gedge mneme-gedge-dashed",
824
+ onClick: () => onEdgeClick(e)
825
+ });
826
+ }),
827
+ nodes.map((n) => h("g", {
828
+ key: n.id,
829
+ className: n.id === data.root.id ? "mneme-gnode mneme-groot" : "mneme-gnode",
830
+ transform: `translate(${n.x},${n.y})`,
831
+ onMouseDown: (e) => onNodeMouseDown(e, n),
832
+ onClick: () => onNodeClick(n),
833
+ "data-node": n.name
834
+ },
835
+ h("circle", { r: nodeRadius(n), fill: typeColor(n.type) }),
836
+ h("text", { className: "mneme-glabel", y: nodeRadius(n) + 13 }, n.name)
837
+ ))
838
+ ),
839
+ h("div", { className: "mneme-graphhint" }, t("memory.graph.hint"))
840
+ )),
841
+ side
842
+ );
843
+ }
844
+
845
+ // --- Settings view: user profile, rules, custom commands, vector, token ---
846
+ const styles = {
847
+ 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" }
848
+ };
849
+
850
+ function SettingsContent({ t }) {
851
+ const [profile, setProfile] = react.useState("");
852
+ const [rules, setRules] = react.useState([]);
853
+ const [commands, setCommands] = react.useState([]);
854
+ const [newRule, setNewRule] = react.useState("");
855
+ const [newCmd, setNewCmd] = react.useState({ name: "", description: "", instruction: "" });
856
+ const [saved, setSaved] = react.useState(false);
857
+ const [cmdError, setCmdError] = react.useState("");
858
+ const [vector, setVector] = react.useState({ enabled: false, baseUrl: "", apiKey: "", model: "" });
859
+ const [vectorSaved, setVectorSaved] = react.useState(false);
860
+ const [reindexing, setReindexing] = react.useState(false);
861
+ const [reindexMsg, setReindexMsg] = react.useState("");
862
+ const [apiToken, setApiToken] = react.useState(() =>
863
+ (typeof window !== "undefined" && window.localStorage) ? window.localStorage.getItem("dsh-mneme-api-token") || "" : ""
864
+ );
865
+ const [apiTokenSaved, setApiTokenSaved] = react.useState(false);
866
+ const [autoTag, setAutoTag] = react.useState(false);
867
+ const [autoTagSaved, setAutoTagSaved] = react.useState(false);
868
+
869
+ const load = react.useCallback(async () => {
870
+ try {
871
+ const [p, r, c, v] = await Promise.all([
872
+ apiFetch("/api/dsh-mneme/profile").then((res) => res.json()),
873
+ apiFetch("/api/dsh-mneme/rules").then((res) => res.json()),
874
+ apiFetch("/api/dsh-mneme/commands").then((res) => res.json()),
875
+ apiFetch("/api/dsh-mneme/vector-config").then((res) => res.json())
876
+ ]);
877
+ setProfile(p.profile || "");
878
+ setRules(Array.isArray(r.rules) ? r.rules : []);
879
+ setCommands(Array.isArray(c.commands) ? c.commands : []);
880
+ setVector(v.config || { enabled: false, baseUrl: "", apiKey: "", model: "" });
881
+ // autoTag loads independently so a config failure can't cascade-block the rest.
882
+ try {
883
+ const g = await apiFetch("/api/dsh-mneme/config").then((res) => res.json());
884
+ setAutoTag(g.config?.autoTagEnabled === true);
885
+ } catch { /* keep default off */ }
886
+ } catch { /* ignore */ }
887
+ }, []);
888
+
889
+ react.useEffect(() => { load(); }, [load]);
890
+
891
+ async function saveProfile() {
892
+ try {
893
+ await apiFetch("/api/dsh-mneme/profile", {
894
+ method: "PUT",
895
+ headers: { "Content-Type": "application/json" },
896
+ body: JSON.stringify({ profile })
897
+ });
898
+ setSaved(true);
899
+ setTimeout(() => setSaved(false), 1500);
900
+ } catch { /* ignore */ }
901
+ }
902
+
903
+ async function putRules(next) {
904
+ await apiFetch("/api/dsh-mneme/rules", {
905
+ method: "PUT",
906
+ headers: { "Content-Type": "application/json" },
907
+ body: JSON.stringify({ rules: next })
908
+ });
909
+ }
910
+
911
+ async function addRule() {
912
+ const text = newRule.trim();
913
+ if (!text) return;
914
+ const next = [...rules, text];
915
+ await putRules(next);
916
+ setRules(next);
917
+ setNewRule("");
918
+ }
919
+
920
+ async function removeRule(index) {
921
+ const next = rules.filter((_, i) => i !== index);
922
+ await putRules(next);
923
+ setRules(next);
924
+ }
925
+
926
+ async function addCommand() {
927
+ const name = newCmd.name.trim();
928
+ const instruction = newCmd.instruction.trim();
929
+ if (!name || !instruction) return;
930
+ try {
931
+ const res = await apiFetch("/api/dsh-mneme/commands", {
932
+ method: "POST",
933
+ headers: { "Content-Type": "application/json" },
934
+ body: JSON.stringify({ name, description: newCmd.description, instruction })
935
+ });
936
+ const data = await res.json();
937
+ if (!res.ok) { setCmdError(data.error || "failed"); return; }
938
+ setCmdError("");
939
+ setCommands([...commands, data.command]);
940
+ setNewCmd({ name: "", description: "", instruction: "" });
941
+ } catch { setCmdError("failed"); }
942
+ }
943
+
944
+ async function removeCommand(id) {
945
+ await apiFetch(`/api/dsh-mneme/commands?id=${encodeURIComponent(id)}`, { method: "DELETE" });
946
+ setCommands(commands.filter((c) => c.id !== id));
947
+ }
948
+
949
+ async function saveVector() {
950
+ try {
951
+ await apiFetch("/api/dsh-mneme/vector-config", {
952
+ method: "PUT",
953
+ headers: { "Content-Type": "application/json" },
954
+ body: JSON.stringify(vector)
955
+ });
956
+ setVectorSaved(true);
957
+ setTimeout(() => setVectorSaved(false), 1500);
958
+ } catch { /* ignore */ }
959
+ }
960
+
961
+ async function reindex() {
962
+ setReindexing(true);
963
+ setReindexMsg("");
964
+ try {
965
+ const res = await apiFetch("/api/dsh-mneme/vector-reindex");
966
+ const data = await res.json();
967
+ const n = data.indexed ?? 0;
968
+ setReindexMsg(t("memory.settings.vectorReindexDone").replace("{n}", String(n)));
969
+ } catch { setReindexMsg(""); }
970
+ setReindexing(false);
971
+ }
972
+
973
+ function saveToken() {
974
+ try {
975
+ if (apiToken.trim()) window.localStorage.setItem("dsh-mneme-api-token", apiToken.trim());
976
+ else window.localStorage.removeItem("dsh-mneme-api-token");
977
+ setApiTokenSaved(true);
978
+ setTimeout(() => setApiTokenSaved(false), 1500);
979
+ } catch { /* ignore */ }
980
+ }
981
+
982
+ async function saveAutoTag() {
983
+ try {
984
+ await apiFetch("/api/dsh-mneme/config", {
985
+ method: "PUT",
986
+ headers: { "Content-Type": "application/json" },
987
+ body: JSON.stringify({ autoTagEnabled: autoTag })
988
+ });
989
+ setAutoTagSaved(true);
990
+ setTimeout(() => setAutoTagSaved(false), 1500);
991
+ } catch { /* ignore */ }
992
+ }
993
+
994
+ const inputStyle = { boxSizing: "border-box", width: "100%", height: 34, padding: "0 12px", borderRadius: 10, border: "1px solid var(--dsw-alias-border-l2, #ddd)", background: "var(--dsw-alias-bg-base, transparent)", color: "var(--dsw-alias-label-primary)", fontFamily: "inherit", fontSize: 13, outline: "none", marginBottom: 8 };
995
+ const labelStyle = { fontSize: 12, fontWeight: 600, margin: "12px 0 4px", color: "var(--dsw-alias-label-primary)" };
996
+ const hintStyle = { fontSize: 11, color: "var(--dsw-alias-label-tertiary, #999)", marginBottom: 8 };
997
+ const rowStyle = { display: "flex", justifyContent: "space-between", alignItems: "center", gap: 8, padding: "6px 0", borderBottom: "1px solid var(--dsw-alias-border-l1, #eee)" };
998
+
999
+ return h("div", { style: { paddingBottom: 8 } },
1000
+ // api token (optional)
1001
+ h("div", { style: { ...labelStyle, marginTop: 20 } }, t("memory.settings.apiTokenTitle")),
1002
+ h("div", { style: hintStyle }, t("memory.settings.apiTokenHint")),
1003
+ h("div", { style: { display: "flex", gap: 6 } },
1004
+ h("input", { style: { ...inputStyle, flex: 1, marginBottom: 0 }, type: "password", value: apiToken, placeholder: t("memory.settings.apiTokenPlaceholder"), onChange: (e) => setApiToken(e.target.value) }),
1005
+ h("button", { style: styles.footerButton, onClick: saveToken }, t("memory.settings.apiTokenSave"))
1006
+ ),
1007
+ apiTokenSaved && h("div", { style: { fontSize: 12, color: "var(--dsw-alias-state-success, #2a7)" } }, t("memory.settings.apiTokenSaved")),
1008
+ // profile
1009
+ h("div", { style: labelStyle }, t("memory.settings.profile")),
1010
+ h("div", { style: hintStyle }, t("memory.settings.profileHint")),
1011
+ h("textarea", {
1012
+ style: { ...inputStyle, minHeight: 72, resize: "vertical", fontFamily: "inherit", padding: "8px 12px", height: "auto" },
1013
+ value: profile,
1014
+ placeholder: t("memory.settings.profile"),
1015
+ onChange: (e) => setProfile(e.target.value)
1016
+ }),
1017
+ h("div", null,
1018
+ h("button", { style: styles.footerButton, onClick: saveProfile }, t("memory.settings.profileSave")),
1019
+ saved && h("span", { style: { fontSize: 12, color: "var(--dsw-alias-state-success, #2a7)" } }, t("memory.settings.profileSaved"))
1020
+ ),
1021
+ // rules
1022
+ h("div", { style: labelStyle }, t("memory.settings.rules")),
1023
+ h("div", { style: hintStyle }, t("memory.settings.rulesHint")),
1024
+ rules.length === 0 && h("div", { style: { fontSize: 12, color: "var(--dsw-alias-label-tertiary, #999)", padding: "8px 0" } }, t("memory.settings.empty")),
1025
+ rules.map((rule, i) =>
1026
+ h("div", { key: i, style: rowStyle },
1027
+ h("span", { style: { fontSize: 13, flex: 1 } }, rule),
1028
+ h("button", { style: { ...styles.footerButton, color: "var(--dsw-alias-state-error, #c33)" }, onClick: () => removeRule(i) }, "×")
1029
+ )
1030
+ ),
1031
+ h("div", { style: { display: "flex", gap: 6 } },
1032
+ h("input", {
1033
+ style: { ...inputStyle, flex: 1, marginBottom: 0 },
1034
+ value: newRule,
1035
+ placeholder: t("memory.settings.rulePlaceholder"),
1036
+ onChange: (e) => setNewRule(e.target.value)
1037
+ }),
1038
+ h("button", { style: styles.footerButton, onClick: addRule }, t("memory.settings.ruleAdd"))
1039
+ ),
1040
+ // custom commands
1041
+ h("div", { style: labelStyle }, t("memory.settings.commands")),
1042
+ h("div", { style: hintStyle }, t("memory.settings.commandsHint")),
1043
+ commands.length === 0 && h("div", { style: { fontSize: 12, color: "var(--dsw-alias-label-tertiary, #999)", padding: "8px 0" } }, t("memory.settings.empty")),
1044
+ commands.map((cmd) =>
1045
+ h("div", { key: cmd.id, style: rowStyle },
1046
+ h("div", { style: { flex: 1 } },
1047
+ h("div", { style: { fontSize: 13, fontWeight: 600 } }, `/${cmd.name}`),
1048
+ h("div", { style: { fontSize: 11, color: "var(--dsw-alias-label-tertiary, #999)" } }, cmd.description || cmd.instruction)
1049
+ ),
1050
+ h("button", { style: { ...styles.footerButton, color: "var(--dsw-alias-state-error, #c33)" }, onClick: () => removeCommand(cmd.id) }, t("memory.settings.cmdDelete"))
1051
+ )
1052
+ ),
1053
+ h("div", { style: { display: "grid", gap: 6, marginTop: 6 } },
1054
+ h("input", { style: { ...inputStyle, marginBottom: 0 }, value: newCmd.name, placeholder: t("memory.settings.cmdName"), onChange: (e) => setNewCmd({ ...newCmd, name: e.target.value }) }),
1055
+ h("input", { style: { ...inputStyle, marginBottom: 0 }, value: newCmd.description, placeholder: t("memory.settings.cmdDesc"), onChange: (e) => setNewCmd({ ...newCmd, description: e.target.value }) }),
1056
+ h("textarea", { style: { ...inputStyle, marginBottom: 0, minHeight: 48, resize: "vertical", fontFamily: "inherit", padding: "8px 12px", height: "auto" }, value: newCmd.instruction, placeholder: t("memory.settings.cmdInstruction"), onChange: (e) => setNewCmd({ ...newCmd, instruction: e.target.value }) }),
1057
+ h("button", { style: styles.footerButton, onClick: addCommand }, t("memory.settings.cmdAdd")),
1058
+ cmdError && h("div", { style: { fontSize: 12, color: "var(--dsw-alias-state-error, #c33)" } }, cmdError)
1059
+ ),
1060
+ // auto-tag switch
1061
+ h("div", { style: { ...labelStyle, marginTop: 20 } }, t("memory.settings.autoTagTitle")),
1062
+ h("div", { style: hintStyle }, t("memory.settings.autoTagHint")),
1063
+ h("label", { style: { display: "flex", alignItems: "center", gap: 6, marginBottom: 8, fontSize: 13 } },
1064
+ h("input", { type: "checkbox", checked: autoTag, onChange: (e) => setAutoTag(e.target.checked) }),
1065
+ h("span", null, t("memory.settings.autoTagEnabled"))
1066
+ ),
1067
+ h("div", { style: { display: "flex", alignItems: "center", gap: 6 } },
1068
+ h("button", { style: styles.footerButton, onClick: saveAutoTag }, t("memory.settings.autoTagSave")),
1069
+ autoTagSaved && h("span", { style: { fontSize: 12, color: "var(--dsw-alias-state-success, #2a7)" } }, t("memory.settings.autoTagSaved"))
1070
+ ),
1071
+ // vector search
1072
+ h("div", { style: { ...labelStyle, marginTop: 20 } }, t("memory.settings.vectorTitle")),
1073
+ h("div", { style: hintStyle }, t("memory.settings.vectorHint")),
1074
+ h("label", { style: { display: "flex", alignItems: "center", gap: 6, marginBottom: 8, fontSize: 13 } },
1075
+ h("input", { type: "checkbox", checked: !!vector.enabled, onChange: (e) => setVector({ ...vector, enabled: e.target.checked }) }),
1076
+ h("span", null, t("memory.settings.vectorEnabled"))
1077
+ ),
1078
+ h("input", { style: inputStyle, value: vector.baseUrl, placeholder: t("memory.settings.vectorBaseUrl"), onChange: (e) => setVector({ ...vector, baseUrl: e.target.value }) }),
1079
+ h("input", { style: inputStyle, type: "password", value: vector.apiKey, placeholder: t("memory.settings.vectorApiKey"), onChange: (e) => setVector({ ...vector, apiKey: e.target.value }) }),
1080
+ h("input", { style: inputStyle, value: vector.model, placeholder: t("memory.settings.vectorModel"), onChange: (e) => setVector({ ...vector, model: e.target.value }) }),
1081
+ h("div", { style: { display: "flex", alignItems: "center", gap: 6, flexWrap: "wrap" } },
1082
+ h("button", { style: styles.footerButton, onClick: saveVector }, t("memory.settings.vectorSave")),
1083
+ vectorSaved && h("span", { style: { fontSize: 12, color: "var(--dsw-alias-state-success, #2a7)" } }, t("memory.settings.vectorSaved")),
1084
+ h("button", { style: styles.footerButton, onClick: reindex, disabled: reindexing }, reindexing ? t("memory.settings.vectorReindexing") : t("memory.settings.vectorReindex")),
1085
+ reindexMsg && h("span", { style: { fontSize: 12, color: "var(--dsw-alias-label-secondary, #666)" } }, reindexMsg)
1086
+ )
1087
+ );
1088
+ }
1089
+
1090
+ // --- Main-area memory library: the single home for every memory feature ---
1091
+ // Registered under conversation.view beside Chat / Trajectory. The old
1092
+ // right-hand drawer is gone: the sidebar foot entry activates this tab
1093
+ // directly. The framework keeps the active-view setter private to the
1094
+ // conversation package, and the DOM tab click is the one stable
1095
+ // activation path available to plugins.
1096
+ //
1097
+ // One host constraint remains: the conversation header (and with it the
1098
+ // whole tab ring) is hidden while a session is blank — the new-chat hero
1099
+ // screen. There the tab simply does not exist, so activateExplorerTab
1100
+ // resolves false and the sidebar entry falls back to the full-viewport
1101
+ // overlay surface below.
1102
+ function findExplorerTab(label) {
1103
+ const tabs = document.querySelectorAll('[role="tab"]');
1104
+ for (const tab of tabs) {
1105
+ if ((tab.textContent || "").trim() === label) return tab;
1106
+ }
1107
+ return null;
1108
+ }
1109
+
1110
+ // Click the memory library tab and wait (bounded, rAF-polled) until the
1111
+ // host actually marks it selected — a silent React re-render gap must not
1112
+ // be mistaken for success, or the fallback would never kick in.
1113
+ function activateExplorerTab(label) {
1114
+ return new Promise((resolve) => {
1115
+ const tab = findExplorerTab(label);
1116
+ if (!tab) { resolve(false); return; }
1117
+ tab.click();
1118
+ const deadline = Date.now() + 400;
1119
+ (function check() {
1120
+ if (tab.getAttribute("aria-selected") === "true") { resolve(true); return; }
1121
+ if (Date.now() >= deadline) { resolve(false); return; }
1122
+ requestAnimationFrame(check);
1123
+ })();
1124
+ });
1125
+ }
1126
+
1127
+ // --- Hero fallback overlay state (module-level pub/sub) ---
1128
+ const overlayListeners = new Set();
1129
+ let overlayOpenState = false;
1130
+ function setOverlayOpen(v) {
1131
+ if (v === overlayOpenState) return;
1132
+ overlayOpenState = v;
1133
+ for (const fn of overlayListeners) fn();
1134
+ }
1135
+ function useOverlayOpen() {
1136
+ const [open, setOpen] = useState(overlayOpenState);
1137
+ useEffect(() => {
1138
+ const fn = () => setOpen(overlayOpenState);
1139
+ overlayListeners.add(fn);
1140
+ return () => { overlayListeners.delete(fn); };
1141
+ }, []);
1142
+ return [open, setOverlayOpen];
1143
+ }
1144
+
1145
+ // Full-viewport memory library for states without a tab ring (new-chat
1146
+ // hero). Same MemoryExplorer component as the tab view — identical
1147
+ // three-column layout, graph and settings — plus a slim top bar with a
1148
+ // close affordance. Esc closes. Portalled to <body> so sidebar stacking
1149
+ // contexts cannot clip it.
1150
+ function MemoryOverlay({ t }) {
1151
+ const [open, setOpen] = useOverlayOpen();
1152
+ useEffect(() => {
1153
+ if (!open) return undefined;
1154
+ const onKey = (e) => { if (e.key === "Escape") setOpen(false); };
1155
+ window.addEventListener("keydown", onKey);
1156
+ return () => window.removeEventListener("keydown", onKey);
1157
+ }, [open]);
1158
+ if (!open) return null;
1159
+ const tree = h("div", { className: "mneme-overlay", role: "region", "aria-label": t("memory.view.label") },
1160
+ h("div", { className: "mneme-overlaybar" },
1161
+ h("span", { className: "mneme-overlaytitle" }, t("memory.view.label")),
1162
+ h("button", {
1163
+ type: "button",
1164
+ className: "mneme-footbtn",
1165
+ onClick: () => setOpen(false)
1166
+ }, t("memory.overlay.close"))
1167
+ ),
1168
+ h("div", { className: "mneme-overlaybody" }, h(MemoryExplorer, { t }))
1169
+ );
1170
+ if (reactDom && typeof document !== "undefined") return reactDom.createPortal(tree, document.body);
1171
+ return tree;
1172
+ }
1173
+
1174
+ // --- directory sub-view (v0.6.3) ---
1175
+ // A tag folder tree over the live memory set. One-level folders keyed by
1176
+ // tag (handled on the server, getDirectory), each with a 二级 memory
1177
+ // entry list (title + updated time); memories without a live tag land in
1178
+ // the "untagged" folder. Collapse state lives in MemoryExplorer (lifted via
1179
+ // `collapsed`), so switching tabs does not drop it. Clicking an entry jumps
1180
+ // back into the browser with that memory selected (onJump = jumpToMemory).
1181
+ function DirectoryPanel({ t, onJump, collapsed, setCollapsed }) {
1182
+ const [dir, setDir] = useState(null); // { groups: [...], untagged: [...] }
1183
+ const [status, setStatus] = useState("loading"); // loading | ready | error
1184
+ const [editMode, setEditMode] = useState(false); // manual tag/delete UI gate
1185
+
1186
+ useEffect(() => {
1187
+ let cancelled = false;
1188
+ setStatus("loading");
1189
+ apiFetch("/api/dsh-mneme/directory")
1190
+ .then((res) => (res.ok ? res.json() : null))
1191
+ .then((d) => {
1192
+ if (cancelled) return;
1193
+ if (!d) { setDir(null); setStatus("error"); return; }
1194
+ setDir({ groups: Array.isArray(d.groups) ? d.groups : [], untagged: Array.isArray(d.untagged) ? d.untagged : [] });
1195
+ setStatus("ready");
1196
+ })
1197
+ .catch(() => { if (!cancelled) { setDir(null); setStatus("error"); } });
1198
+ apiFetch("/api/dsh-mneme/config")
1199
+ .then((res) => (res.ok ? res.json() : null))
1200
+ .then((d) => { if (!cancelled && d?.config && typeof d.config.manualTagEnabled === "boolean") setEditMode(d.config.manualTagEnabled); })
1201
+ .catch(() => {});
1202
+ return () => { cancelled = true; };
1203
+ }, []);
1204
+
1205
+ const toggle = (key) => setCollapsed((c) => ({ ...c, [key]: !c[key] }));
1206
+
1207
+ const toggleEdit = () => {
1208
+ const next = !editMode;
1209
+ apiFetch("/api/dsh-mneme/config", {
1210
+ method: "PUT",
1211
+ headers: { "Content-Type": "application/json" },
1212
+ body: JSON.stringify({ manualTagEnabled: next })
1213
+ }).then((res) => { if (res.ok) setEditMode(next); }).catch(() => {});
1214
+ };
1215
+
1216
+ const handleDelete = (memId, e) => {
1217
+ e?.stopPropagation?.();
1218
+ if (typeof window === "undefined" || !window.confirm(t("directory.deleteConfirm"))) return;
1219
+ apiFetch("/api/dsh-mneme/memories", {
1220
+ method: "DELETE",
1221
+ headers: { "Content-Type": "application/json" },
1222
+ body: JSON.stringify({ id: memId })
1223
+ })
1224
+ .then((res) => (res.ok ? res.json() : null))
1225
+ .then((d) => {
1226
+ if (!d || d.deleted !== true) return;
1227
+ setDir((prev) => ({
1228
+ groups: prev.groups.map((g) => ({ ...g, memories: g.memories.filter((m) => m.id !== memId) })),
1229
+ untagged: prev.untagged.filter((m) => m.id !== memId)
1230
+ }));
1231
+ })
1232
+ .catch(() => {});
1233
+ };
1234
+
1235
+ const renderFolder = (name, memories, extraKey, emptyLabel) => {
1236
+ const folderKey = extraKey || name;
1237
+ const open = !collapsed[folderKey];
1238
+ const rows = memories.map((m) =>
1239
+ h("div", { key: m.id, className: "mneme-dir-item" },
1240
+ h("span", { className: "mneme-tree-line" }),
1241
+ h("button", { type: "button", className: "mneme-dir-ititle", onClick: () => onJump(m) }, m.title || m.content?.slice(0, 40)),
1242
+ h("span", { className: "mneme-dir-itime" }, formatDate(m.updated_at || m.created_at)),
1243
+ editMode && h("button", { type: "button", className: "mneme-dir-del", "aria-label": "delete", onClick: (e) => handleDelete(m.id, e) }, "✕")
1244
+ )
1245
+ );
1246
+ return h("div", {
1247
+ key: folderKey,
1248
+ className: "mneme-dir-folder",
1249
+ "data-expanded": String(open)
1250
+ },
1251
+ h("button", {
1252
+ type: "button",
1253
+ className: "mneme-dir-folderhead",
1254
+ "aria-expanded": String(open),
1255
+ onClick: () => toggle(folderKey)
1256
+ },
1257
+ h("span", { className: "mneme-dir-caret" }, open ? "▾" : "▸"),
1258
+ h("span", { className: "mneme-dir-fname" }, `📁 ${name}`),
1259
+ h("span", { className: "mneme-dir-fcount" }, String(memories.length))
1260
+ ),
1261
+ open && h("div", { className: "mneme-dir-fbody" },
1262
+ rows.length > 0 ? rows : h("div", { className: "mneme-dir-empty" }, emptyLabel || t("memory.directory.empty"))
1263
+ )
1264
+ );
1265
+ };
1266
+
1267
+ if (status === "loading") {
1268
+ return h("div", { className: "mneme-directory" }, h("div", { className: "mneme-dir-empty" }, t("memory.directory.loading")));
1269
+ }
1270
+ const hasAny = dir && (dir.groups.length > 0 || dir.untagged.length > 0);
1271
+ if (status === "error" || !hasAny) {
1272
+ return h("div", { className: "mneme-directory" }, h("div", { className: "mneme-dir-empty" }, t("memory.directory.empty")));
1273
+ }
1274
+ return h("div", { className: editMode ? "mneme-directory mneme-edit-on" : "mneme-directory" },
1275
+ h("label", { className: "mneme-edit-toggle" },
1276
+ h("input", { type: "checkbox", checked: editMode, onChange: toggleEdit }),
1277
+ t("directory.editToggle")
1278
+ ),
1279
+ dir.groups.map((g) => renderFolder(g.tag, g.memories)),
1280
+ dir.untagged.length > 0 && renderFolder(t("memory.directory.untagged"), dir.untagged, "mneme-untagged")
1281
+ );
1282
+ }
1283
+
1284
+ const EXPLORER_TYPES = ["preference", "project", "decision", "summary", "history"];
1285
+
1286
+ function MemoryExplorer({ t }) {
1287
+ const [view, setView] = useState("memory"); // memory | directory | graph | settings
1288
+ const [items, setItems] = useState([]);
1289
+ const [loading, setLoading] = useState(true);
1290
+ const [type, setType] = useState("all");
1291
+ const [query, setQuery] = useState("");
1292
+ const [semantic, setSemantic] = useState(false);
1293
+ const [vecEnabled, setVecEnabled] = useState(false);
1294
+ const [searchTopK, setSearchTopK] = useState(20);
1295
+ const [remoteItems, setRemoteItems] = useState(null);
1296
+ const [selectedId, setSelectedId] = useState(null);
1297
+ const [collapsed, setCollapsed] = useState({});
1298
+ // v0.6.3 directory sub-view: its own folder-collapse state, kept in
1299
+ // MemoryExplorer so switching tabs does not lose it and jumpToMemory's
1300
+ // time-tree reset (setCollapsed({})) cannot clobber it.
1301
+ const [dirCollapsed, setDirCollapsed] = useState({});
1302
+ const [copied, setCopied] = useState(false);
1303
+ const [reloadKey, setReloadKey] = useState(0);
1304
+ const [graphFocus, setGraphFocus] = useState("");
1305
+ // v0.6.2 tag editing state for the detail pane. The authoritative tag set
1306
+ // lives server-side (entity_attrs), so it is fetched per selected memory
1307
+ // and refreshed after every write instead of trusting the list snapshot.
1308
+ const [tagTags, setTagTags] = useState([]);
1309
+ const [tagManual, setTagManual] = useState(true);
1310
+ const [tagInput, setTagInput] = useState("");
1311
+ const [tagAdding, setTagAdding] = useState(false);
1312
+ const itemRefs = useRef(new Map());
1313
+
1314
+ useEffect(() => {
1315
+ apiFetch("/api/dsh-mneme/vector-config")
1316
+ .then((res) => res.json())
1317
+ .then((d) => setVecEnabled(!!d.config?.enabled))
1318
+ .catch(() => {});
1319
+ }, []);
1320
+
1321
+ useEffect(() => {
1322
+ let cancelled = false;
1323
+ setLoading(true);
1324
+ apiFetch("/api/dsh-mneme/list?limit=500")
1325
+ .then((res) => (res.ok ? res.json() : { items: [] }))
1326
+ .then((d) => { if (!cancelled) { setItems(d.items || []); setLoading(false); } })
1327
+ .catch(() => { if (!cancelled) { setItems([]); setLoading(false); } });
1328
+ return () => { cancelled = true; };
1329
+ }, [reloadKey]);
1330
+
1331
+ // Semantic search: server-side ranking replaces the client filter
1332
+ // while enabled and a query is present (debounced). tag: queries always
1333
+ // go server-side (searchMemories resolves them without vector support),
1334
+ // so they bypass the semantic toggle and use the same search endpoint.
1335
+ useEffect(() => {
1336
+ const q = query.trim();
1337
+ if ((!semantic && !q.startsWith("tag:")) || !q || q.startsWith("entity:")) { setRemoteItems(null); return; }
1338
+ let cancelled = false;
1339
+ const timer = setTimeout(() => {
1340
+ apiFetch(`/api/dsh-mneme/search?q=${encodeURIComponent(q)}&mode=vector&topK=${searchTopK}`)
1341
+ .then((res) => (res.ok ? res.json() : { items: [] }))
1342
+ .then((d) => { if (!cancelled) setRemoteItems(d.items || []); })
1343
+ .catch(() => { if (!cancelled) setRemoteItems([]); });
1344
+ }, 250);
1345
+ return () => { cancelled = true; clearTimeout(timer); };
1346
+ }, [semantic, query, searchTopK]);
1347
+
1348
+ useEffect(() => {
1349
+ if (!selectedId) return;
1350
+ itemRefs.current.get(selectedId)?.scrollIntoView({ block: "nearest" });
1351
+ }, [selectedId]);
1352
+
1353
+ // Load the authoritative tag set for the selected memory (entity_attrs-
1354
+ // backed) plus the manualTagEnabled gate that hides editing when off.
1355
+ const loadTags = (id) => {
1356
+ if (!id) { setTagTags([]); setTagManual(true); setTagInput(""); return; }
1357
+ apiFetch(`/api/dsh-mneme/memory/tags?id=${encodeURIComponent(id)}`)
1358
+ .then((res) => (res.ok ? res.json() : null))
1359
+ .then((j) => { if (j) { setTagTags(Array.isArray(j.tags) ? j.tags : []); setTagManual(j.manualTagEnabled !== false); } })
1360
+ .catch(() => {});
1361
+ };
1362
+ useEffect(() => {
1363
+ let cancelled = false;
1364
+ if (selectedId) {
1365
+ apiFetch(`/api/dsh-mneme/memory/tags?id=${encodeURIComponent(selectedId)}`)
1366
+ .then((res) => (res.ok ? res.json() : null))
1367
+ .then((j) => { if (!cancelled && j) { setTagTags(Array.isArray(j.tags) ? j.tags : []); setTagManual(j.manualTagEnabled !== false); } })
1368
+ .catch(() => {});
1369
+ } else {
1370
+ setTagTags([]);
1371
+ setTagManual(true);
1372
+ }
1373
+ setTagAdding(false);
1374
+ return () => { cancelled = true; };
1375
+ }, [selectedId]);
1376
+
1377
+ const q = query.trim().toLowerCase();
1378
+ // "entity:" is the graph entry grammar: typing it means the user wants
1379
+ // the entity's neighborhood, not a memory list. Offer the jump instead
1380
+ // of auto-switching so the list stays predictable.
1381
+ const entityQuery = query.trim().startsWith("entity:")
1382
+ ? query.trim().slice(7).trim()
1383
+ : "";
1384
+ const visible = remoteItems
1385
+ ? remoteItems
1386
+ : items.filter((m) => {
1387
+ if (type !== "all" && m.type !== type) return false;
1388
+ if (!q) return true;
1389
+ return (m.title || "").toLowerCase().includes(q) || (m.content || "").toLowerCase().includes(q);
1390
+ });
1391
+
1392
+ const counts = {};
1393
+ for (const m of items) counts[m.type] = (counts[m.type] || 0) + 1;
1394
+ const knownTypes = EXPLORER_TYPES.filter((k) => counts[k]);
1395
+ const extraTypes = Object.keys(counts)
1396
+ .filter((k) => !EXPLORER_TYPES.includes(k))
1397
+ .sort((a, b) => counts[b] - counts[a]);
1398
+
1399
+ // Time tree: sort newest first, then group month → day in one pass so
1400
+ // the grouping follows the sort order instead of re-sorting buckets.
1401
+ const sorted = [...visible].sort((a, b) =>
1402
+ new Date(b.updated_at || b.created_at || 0) - new Date(a.updated_at || a.created_at || 0));
1403
+ const months = [];
1404
+ let curMonth = null, curDay = null;
1405
+ for (const m of sorted) {
1406
+ const d = new Date(m.updated_at || m.created_at || 0);
1407
+ const valid = !Number.isNaN(d.getTime());
1408
+ const mk = valid ? `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}` : "unknown";
1409
+ if (!curMonth || curMonth.key !== mk) {
1410
+ curMonth = {
1411
+ key: mk,
1412
+ label: valid ? d.toLocaleDateString(undefined, { year: "numeric", month: "long" }) : "—",
1413
+ days: []
1414
+ };
1415
+ months.push(curMonth);
1416
+ curDay = null;
1417
+ }
1418
+ const dk = valid ? `${mk}-${String(d.getDate())}` : "unknown";
1419
+ if (!curDay || curDay.key !== dk) {
1420
+ curDay = { key: dk, label: valid ? d.toLocaleDateString(undefined, { day: "numeric" }) : "—", items: [] };
1421
+ curMonth.days.push(curDay);
1422
+ }
1423
+ curDay.items.push(m);
1424
+ }
1425
+
1426
+ const selected = items.find((m) => m.id === selectedId) || null;
1427
+
1428
+ const copyContent = () => {
1429
+ if (!selected) return;
1430
+ navigator.clipboard?.writeText(selected.content || "").then(
1431
+ () => { setCopied(true); setTimeout(() => setCopied(false), 1500); },
1432
+ () => {}
1433
+ );
1434
+ };
1435
+
1436
+ // v0.6.2 tag editing: a chip click jumps to a tag:xxx search (reusing
1437
+ // the query-driven search flow), the + entry and per-chip × post the new
1438
+ // tag set through the tags endpoint then re-fetch. A 409 from the server
1439
+ // means manualTagEnabled turned off — flip the gate so editing hides.
1440
+ const submitTag = () => {
1441
+ const tag = tagInput.trim();
1442
+ if (!tag || !selectedId) return;
1443
+ apiFetch("/api/dsh-mneme/memory/tags", {
1444
+ method: "POST",
1445
+ headers: { "Content-Type": "application/json" },
1446
+ body: JSON.stringify({ id: selectedId, tags: [...tagTags, tag] })
1447
+ }).then((res) => {
1448
+ if (res.status === 409) { setTagManual(false); setTagAdding(false); setTagInput(""); return; }
1449
+ return res.json();
1450
+ }).then((j) => {
1451
+ if (j?.ok) { setTagAdding(false); setTagInput(""); loadTags(selectedId); }
1452
+ }).catch(() => {});
1453
+ };
1454
+ const removeTag = (tag) => {
1455
+ if (!selectedId) return;
1456
+ apiFetch("/api/dsh-mneme/memory/tags", {
1457
+ method: "POST",
1458
+ headers: { "Content-Type": "application/json" },
1459
+ body: JSON.stringify({ id: selectedId, tags: tagTags.filter((x) => x !== tag) })
1460
+ }).then((res) => {
1461
+ if (res.status === 409) { setTagManual(false); return; }
1462
+ return res.json();
1463
+ }).then((j) => {
1464
+ if (j?.ok) loadTags(selectedId);
1465
+ }).catch(() => {});
1466
+ };
1467
+ const onTagClick = (tag) => {
1468
+ setType("all");
1469
+ setQuery(`tag:${tag}`);
1470
+ };
1471
+
1472
+ // Graph → memory jump: land on the browser tab with filters reset so
1473
+ // the target row is visible, selected and scrolled into view.
1474
+ const jumpToMemory = (target) => {
1475
+ if (!target?.id) return;
1476
+ setView("memory");
1477
+ setType("all");
1478
+ setQuery("");
1479
+ setCollapsed({});
1480
+ setSelectedId(target.id);
1481
+ };
1482
+
1483
+ // [[wiki-link]] click in the detail content: resolve the title to a
1484
+ // memory (case-insensitive exact match) then jump to it. Unresolvable
1485
+ // titles silently no-op — the link stays as display text.
1486
+ const onWikilinkClick = (target) => {
1487
+ if (!target) return;
1488
+ apiFetch(`/api/dsh-mneme/wikilinks/resolve?title=${encodeURIComponent(target)}`)
1489
+ .then((r) => (r.ok ? r.json() : null))
1490
+ .then((j) => { if (j?.memory?.id) jumpToMemory(j.memory); })
1491
+ .catch(() => {});
1492
+ };
1493
+
1494
+ const openGraphFor = (name) => {
1495
+ setGraphFocus(name || "");
1496
+ setView("graph");
1497
+ };
1498
+
1499
+ const subviews = [
1500
+ { key: "memory", label: t("memory.explorer.tabMemory") },
1501
+ { key: "directory", label: t("memory.explorer.tabDirectory") },
1502
+ { key: "graph", label: t("memory.explorer.tabGraph") },
1503
+ { key: "settings", label: t("memory.explorer.tabSettings") }
1504
+ ];
1505
+
1506
+ return h("div", { className: "mneme-x" },
1507
+ h("div", { className: "mneme-xbar" },
1508
+ h("nav", { className: "mneme-vtabs", "aria-label": t("memory.view.label") },
1509
+ subviews.map((s) =>
1510
+ h("button", {
1511
+ key: s.key,
1512
+ type: "button",
1513
+ className: view === s.key ? "mneme-vtab mneme-active" : "mneme-vtab",
1514
+ "aria-pressed": String(view === s.key),
1515
+ onClick: () => setView(s.key)
1516
+ },
1517
+ s.key === "graph"
1518
+ ? h(react.Fragment, null, h(GraphNodesIcon, { size: 16 }), s.label)
1519
+ : s.label
1520
+ ))
1521
+ ),
1522
+ ),
1523
+ view === "memory" && h("div", { className: "mneme-xmain" },
1524
+ // 1. 中间分类栏
1525
+ h("div", { className: "mneme-xfilter-bar" },
1526
+ h("div", { className: "mneme-xcolhead" }, t("memory.explorer.types")),
1527
+ h("button", {
1528
+ className: type === "all" ? "mneme-xtype mneme-active" : "mneme-xtype",
1529
+ onClick: () => setType("all")
1530
+ }, h("span", null, t("memory.tab.all")), h("span", { className: "mneme-xcount2" }, String(items.length))),
1531
+ knownTypes.concat(extraTypes).map((key) =>
1532
+ h("button", {
1533
+ key,
1534
+ className: type === key ? "mneme-xtype mneme-active" : "mneme-xtype",
1535
+ onClick: () => setType(key)
1536
+ },
1537
+ h("span", null, typeLabel(t, key)),
1538
+ h("span", { className: "mneme-xcount2" }, String(counts[key]))
1539
+ ))
1540
+ ),
1541
+ // 2. 底部三卡片
1542
+ h("div", { className: "mneme-xcards" },
1543
+ // 左卡:搜索
1544
+ h("div", { className: "mneme-card mneme-card--search" },
1545
+ h("div", { className: "mneme-xcolhead" }, t("memory.explorer.searchTitle")),
1546
+ h("input", {
1547
+ className: "mneme-search mneme-xsearch",
1548
+ placeholder: t("memory.explorer.search"),
1549
+ value: query,
1550
+ onChange: (e) => setQuery(e.target.value)
1551
+ }),
1552
+ entityQuery && h("button", {
1553
+ className: "mneme-entitychip",
1554
+ style: { textAlign: "left", justifyContent: "flex-start" },
1555
+ onClick: () => openGraphFor(entityQuery)
1556
+ }, `${t("memory.graph.viewInGraph")} “${entityQuery}”`),
1557
+ h("div", { className: "mneme-xrow" },
1558
+ vecEnabled && h("button", {
1559
+ className: semantic ? "mneme-chip mneme-active" : "mneme-chip",
1560
+ title: t("memory.settings.vectorTitle"),
1561
+ onClick: () => setSemantic(!semantic)
1562
+ }, t("memory.panel.semantic")),
1563
+ h("select", {
1564
+ className: "mneme-select mneme-xselect",
1565
+ value: searchTopK,
1566
+ onChange: (e) => setSearchTopK(Number(e.target.value)),
1567
+ title: t("memory.explorer.topK")
1568
+ }, [5, 10, 20, 50].map((n) => h("option", { key: n, value: n }, t("memory.explorer.topKOption").replace("{n}", String(n)))))
1569
+ ),
1570
+ h("div", { className: "mneme-xrow" },
1571
+ h("span", { className: "mneme-xcount" }, t("memory.explorer.count").replace("{n}", String(visible.length))),
1572
+ h("button", { className: "mneme-footbtn", onClick: () => setReloadKey((k) => k + 1) }, t("memory.explorer.refresh"))
1573
+ )
1574
+ ),
1575
+ // 中卡:时间树
1576
+ h("div", { className: "mneme-card mneme-card--tree" },
1577
+ h("div", { className: "mneme-xcolhead" }, t("memory.explorer.timeline")),
1578
+ loading
1579
+ ? h("div", { className: "mneme-xempty" }, "…")
1580
+ : months.length === 0
1581
+ ? h("div", { className: "mneme-xempty" }, t("memory.explorer.empty"))
1582
+ : months.map((month) =>
1583
+ h("div", { key: month.key },
1584
+ h("button", {
1585
+ className: "mneme-xmonth",
1586
+ "aria-expanded": String(!collapsed[month.key]),
1587
+ onClick: () => setCollapsed((c) => ({ ...c, [month.key]: !c[month.key] }))
1588
+ },
1589
+ h("span", { className: "mneme-xcaret" }, collapsed[month.key] ? "▸" : "▾"),
1590
+ month.label
1591
+ ),
1592
+ !collapsed[month.key] && month.days.map((day) =>
1593
+ h("div", { key: day.key },
1594
+ h("button", {
1595
+ className: "mneme-xday",
1596
+ "aria-expanded": String(!collapsed[day.key]),
1597
+ onClick: () => setCollapsed((c) => ({ ...c, [day.key]: !c[day.key] }))
1598
+ },
1599
+ h("span", { className: "mneme-xcaret" }, collapsed[day.key] ? "▸" : "▾"),
1600
+ day.label
1601
+ ),
1602
+ !collapsed[day.key] && day.items.map((m) => {
1603
+ const d = new Date(m.updated_at || m.created_at || 0);
1604
+ const time = Number.isNaN(d.getTime())
1605
+ ? ""
1606
+ : d.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" });
1607
+ return h("button", {
1608
+ key: m.id,
1609
+ ref: (el) => { if (el) itemRefs.current.set(m.id, el); else itemRefs.current.delete(m.id); },
1610
+ className: m.id === selectedId ? "mneme-xitem mneme-active" : "mneme-xitem",
1611
+ onClick: () => setSelectedId(m.id)
1612
+ },
1613
+ h("span", { className: "mneme-xtime" }, time),
1614
+ h("span", { className: "mneme-xname" }, m.title || m.content?.slice(0, 40))
1615
+ );
1616
+ })
1617
+ )
1618
+ )
1619
+ )
1620
+ )),
1621
+ // 右卡:详情
1622
+ h("div", { className: "mneme-card mneme-card--detail" },
1623
+ selected
1624
+ ? h(react.Fragment, { key: selected.id },
1625
+ h("div", { className: "mneme-xdinner" },
1626
+ h("div", { className: "mneme-xdtitle" }, selected.title),
1627
+ h("div", { className: "mneme-xdmeta" },
1628
+ h("span", null, `${typeLabel(t, selected.type)} · ${t("memory.explorer.importance")} ★${selected.importance}`),
1629
+ selected.source && h("span", null, `${t("memory.explorer.source")}: ${selected.source}`),
1630
+ h("span", { title: formatDate(selected.created_at) }, `${t("memory.explorer.created")}: ${formatDate(selected.created_at)}`),
1631
+ h("span", { title: formatDate(selected.updated_at) }, `${t("memory.explorer.updated")}: ${formatDate(selected.updated_at)}`)
1632
+ ),
1633
+ h("div", { className: "mneme-xdmeta" },
1634
+ h("span", null, t("memory.explorer.tags")),
1635
+ h("div", { className: "mneme-tagrow" },
1636
+ tagTags.length > 0
1637
+ ? tagTags.map((tag) =>
1638
+ h("span", {
1639
+ key: tag,
1640
+ className: "mneme-tagchip",
1641
+ title: `tag:${tag}`,
1642
+ onClick: () => onTagClick(tag)
1643
+ },
1644
+ tag,
1645
+ tagManual && h("button", {
1646
+ type: "button",
1647
+ className: "mneme-tagremove",
1648
+ "aria-label": `${t("memory.explorer.tagRemove")}: ${tag}`,
1649
+ onClick: (e) => { e.stopPropagation(); removeTag(tag); }
1650
+ }, "×")
1651
+ )
1652
+ )
1653
+ : h("span", { className: "mneme-tagempty" }, t("memory.explorer.tagsEmpty")),
1654
+ tagManual && (tagAdding
1655
+ ? h("input", {
1656
+ key: selectedId,
1657
+ className: "mneme-taginput",
1658
+ autoFocus: true,
1659
+ value: tagInput,
1660
+ placeholder: t("memory.explorer.tagPlaceholder"),
1661
+ onChange: (e) => setTagInput(e.target.value),
1662
+ onKeyDown: (e) => { if (e.key === "Enter") submitTag(); },
1663
+ onBlur: () => { setTagAdding(false); setTagInput(""); }
1664
+ })
1665
+ : h("button", {
1666
+ type: "button",
1667
+ className: "mneme-tagadd",
1668
+ onClick: () => setTagAdding(true)
1669
+ }, `+ ${t("memory.explorer.tagAdd")}`))
1670
+ )
1671
+ ),
1672
+ h("div", { className: "mneme-xdcontent" },
1673
+ selected.content ? wikilinkSegments(selected.content, onWikilinkClick) : selected.content
1674
+ ),
1675
+ h("div", { className: "mneme-xdactions" },
1676
+ h("button", { className: "mneme-footbtn", onClick: copyContent },
1677
+ copied ? t("memory.explorer.copied") : t("memory.explorer.copy"))
1678
+ ),
1679
+ h(BacklinksPanel, { memory: selected, t, onJump: jumpToMemory })
1680
+ )
1681
+ )
1682
+ : h("div", { className: "mneme-xempty" }, t("memory.explorer.emptyDetail"))
1683
+ )
1684
+ )
1685
+ ),
1686
+ view === "directory" && h(DirectoryPanel, { t, onJump: jumpToMemory, collapsed: dirCollapsed, setCollapsed: setDirCollapsed }),
1687
+ view === "graph" && h(GraphPanel, { t, focusEntity: graphFocus, onJumpMemory: jumpToMemory }),
1688
+ view === "settings" && h("div", { className: "mneme-xsettings" },
1689
+ h("div", { className: "mneme-xsettings-inner" }, h(SettingsContent, { t }))
1690
+ )
1691
+ );
1692
+ }
1693
+
1694
+ // --- Sidebar foot entry: the wide row / rail icon that activates the
1695
+ // main-area memory library tab. When the tab ring is absent (new-chat
1696
+ // hero screen hides the conversation header entirely) the same click
1697
+ // opens the full-viewport overlay instead — the library stays reachable
1698
+ // from every conversation state.
1699
+ function SidebarTrigger({ wide, t }) {
1700
+ const [, setOpen] = useOverlayOpen();
1701
+ return h("button", {
1702
+ type: "button",
1703
+ className: wide ? "mneme-trigger" : "mneme-trigger mneme-rail",
1704
+ "aria-label": t("memory.sidebar.aria"),
1705
+ title: t("memory.sidebar.aria"),
1706
+ onClick: () => {
1707
+ activateExplorerTab(t("memory.view.label")).then((ok) => { if (!ok) setOpen(true); });
1708
+ }
1709
+ },
1710
+ h(IconArchiveOutline20, { size: wide ? 16 : 18 }),
1711
+ wide && h("span", { className: "mneme-trigger-label" }, t("memory.panel.open"))
1712
+ );
1713
+ }
1714
+
1715
+ function apply(ctx) {
1716
+ ctx.effect(() => ctx.locale.register(NS, dictionaries), "dsh-mneme: dictionaries");
1717
+
1718
+ // Register the memory entry beside Settings at the sidebar foot. The
1719
+ // entry renders a wide row (icon + label) when the sidebar is expanded
1720
+ // and a rail icon when collapsed; clicking activates the memory
1721
+ // library conversation view.
1722
+ ctx.slots.inject("sidebar.footer.action", () => {
1723
+ const t = ctx.locale.bind(NS);
1724
+ return ctx.slots.register({
1725
+ name: "sidebar.footer.action",
1726
+ id: "dsh-mneme",
1727
+ order: 0,
1728
+ label: () => t("memory.panel.open")
1729
+ }, (props) => h(react.Fragment, null,
1730
+ h(SidebarTrigger, { ...props, t }),
1731
+ // The overlay mounts from the always-rendered sidebar slot so it
1732
+ // survives conversation switches; the portal moves it to <body>.
1733
+ h(MemoryOverlay, { t })
1734
+ ));
1735
+ });
1736
+
1737
+ // Register the memory library as a conversation view tab, beside
1738
+ // Chat / Trajectory. The view ignores its session props: the library
1739
+ // reads the memory store over HTTP. It hosts every memory feature —
1740
+ // browse, graph and settings — in the main content area.
1741
+ ctx.slots.inject("conversation.view", () => {
1742
+ const t = ctx.locale.bind(NS);
1743
+ return ctx.slots.register({
1744
+ name: "conversation.view",
1745
+ id: "dsh-mneme-memory",
1746
+ order: 30,
1747
+ locale: NS,
1748
+ label: () => t("memory.view.label")
1749
+ }, () => h(MemoryExplorer, { t }));
1750
+ });
1751
+ }
1752
+
1753
+ exports.apply = apply;
1754
+ exports.inject = inject;
1755
+ return module.exports;
1756
+ }
1757
+ });