@modusensus/dsh-mneme 0.7.9 → 0.7.10

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.
@@ -69,11 +69,11 @@ window.__ModuleLoader__.load({
69
69
  "memory.tab.fact": "事实",
70
70
  "memory.settings.title": "记忆库设置",
71
71
  "memory.settings.profile": "用户画像",
72
- "memory.settings.profileHint": "描述你自己(角色、背景、偏好),Agent 会在每轮遵循",
72
+ "memory.settings.profileHint": "写一段自我介绍(角色、背景、偏好),Agent 每轮对话都会自动带上。",
73
73
  "memory.settings.profileSave": "保存画像",
74
74
  "memory.settings.profileSaved": "画像已保存",
75
75
  "memory.settings.rules": "规则",
76
- "memory.settings.rulesHint": "Agent 必须遵守的行为规则,每轮注入",
76
+ "memory.settings.rulesHint": "Agent 立下必须遵守的规矩,随时增删,下一轮即生效。",
77
77
  "memory.settings.autoTagTitle": "自动打标签",
78
78
  "memory.settings.autoTagHint": "开启后,新记忆由轻量模型自动打标签,目录页按标签自动分类(opt-in,默认关)",
79
79
  "memory.settings.autoTagEnabled": "自动打标签",
@@ -87,7 +87,7 @@ window.__ModuleLoader__.load({
87
87
  "memory.settings.ruleAdd": "添加规则",
88
88
  "memory.settings.rulePlaceholder": "例如:回答时总是先给结论",
89
89
  "memory.settings.commands": "自定义指令",
90
- "memory.settings.commandsHint": "注册斜杠命令(/名称),触发时把指令内容交给 Agent",
90
+ "memory.settings.commandsHint": "把常用提示词存成 / 命令,聊天时直接键入调用。",
91
91
  "memory.settings.cmdName": "命令名",
92
92
  "memory.settings.cmdDesc": "描述",
93
93
  "memory.settings.cmdInstruction": "指令内容",
@@ -156,12 +156,13 @@ window.__ModuleLoader__.load({
156
156
  "memory.graph.related": "关联记忆",
157
157
  "memory.graph.relation": "关系",
158
158
  "memory.graph.sourceMemory": "查看来源记忆",
159
- "memory.graph.hint": "点击节点展开 · 拖拽调整布局",
159
+ "memory.graph.hint": "拖拽节点调整布局 · 空白处拖动平移 · 滚轮缩放",
160
+ "memory.graph.resetView": "重置视图",
160
161
  "memory.graph.viewInGraph": "在图谱中查看",
161
162
  "memory.graph.loading": "加载中…",
162
163
  "memory.graph.distance": "距中心 {n} 跳",
163
164
  "memory.settings.vectorTitle": "向量搜索",
164
- "memory.settings.vectorHint": "接入 OpenAI 兼容的 embeddings API 做语义搜索,可匹配字面不同但语义相近的记忆",
165
+ "memory.settings.vectorHint": "搜索不再只认字面:接入任意 OpenAI 兼容的 embedding 接口后,意思相近的记忆也能被召回。",
165
166
  "memory.settings.vectorEnabled": "启用向量搜索",
166
167
  "memory.settings.vectorBaseUrl": "API 地址 (Base URL)",
167
168
  "memory.settings.vectorApiKey": "API Key",
@@ -172,7 +173,7 @@ window.__ModuleLoader__.load({
172
173
  "memory.settings.vectorReindexing": "索引中…",
173
174
  "memory.settings.vectorReindexDone": "已索引 {n} 条",
174
175
  "memory.settings.apiTokenTitle": "API Token(可选)",
175
- "memory.settings.apiTokenHint": "设置后,写操作与密钥接口(画像/规则/命令/向量配置)需携带 Authorization: Bearer <token>;面板只读操作不受影响。清空并保存可关闭鉴权。",
176
+ "memory.settings.apiTokenHint": "给面板的写操作加一道锁:设置后,修改记忆或配置需要携带 Token,纯浏览不受影响。清空并保存可关闭。",
176
177
  "memory.settings.apiTokenPlaceholder": "留空 = 不鉴权(默认)",
177
178
  "memory.settings.apiTokenSave": "保存 Token",
178
179
  "memory.settings.apiTokenSaved": "Token 已保存",
@@ -194,11 +195,11 @@ window.__ModuleLoader__.load({
194
195
  "memory.tab.fact": "Facts",
195
196
  "memory.settings.title": "Memory Settings",
196
197
  "memory.settings.profile": "User Profile",
197
- "memory.settings.profileHint": "Describe yourself — the agent follows this every turn",
198
+ "memory.settings.profileHint": "Describe yourself once — the agent reads it every turn.",
198
199
  "memory.settings.profileSave": "Save Profile",
199
200
  "memory.settings.profileSaved": "Profile saved",
200
201
  "memory.settings.rules": "Rules",
201
- "memory.settings.rulesHint": "Behavior rules the agent must follow every turn",
202
+ "memory.settings.rulesHint": "Rules the agent must follow add or remove anytime, effective next turn.",
202
203
  "memory.settings.autoTagTitle": "Auto-Tag",
203
204
  "memory.settings.autoTagHint": "When on, new memories are auto-tagged by a light model and the directory auto-grouped by tag (opt-in, off by default)",
204
205
  "memory.settings.autoTagEnabled": "Auto-tagging",
@@ -212,7 +213,7 @@ window.__ModuleLoader__.load({
212
213
  "memory.settings.ruleAdd": "Add Rule",
213
214
  "memory.settings.rulePlaceholder": "e.g. Always lead with a conclusion",
214
215
  "memory.settings.commands": "Custom Commands",
215
- "memory.settings.commandsHint": "Register slash commands (/name) whose instruction is handed to the agent",
216
+ "memory.settings.commandsHint": "Save prompts as slash commands and invoke them right from the composer.",
216
217
  "memory.settings.cmdName": "Name",
217
218
  "memory.settings.cmdDesc": "Description",
218
219
  "memory.settings.cmdInstruction": "Instruction",
@@ -281,12 +282,13 @@ window.__ModuleLoader__.load({
281
282
  "memory.graph.related": "Related memories",
282
283
  "memory.graph.relation": "Relation",
283
284
  "memory.graph.sourceMemory": "View source memory",
284
- "memory.graph.hint": "Click a node to expand · drag to rearrange",
285
+ "memory.graph.hint": "Drag nodes to rearrange · drag the background to pan · scroll to zoom",
286
+ "memory.graph.resetView": "Reset view",
285
287
  "memory.graph.viewInGraph": "View in graph",
286
288
  "memory.graph.loading": "Loading…",
287
289
  "memory.graph.distance": "{n} hop(s) from root",
288
290
  "memory.settings.vectorTitle": "Vector Search",
289
- "memory.settings.vectorHint": "Connect an OpenAI-compatible embeddings API for semantic search by meaning, not just keywords",
291
+ "memory.settings.vectorHint": "Match by meaning, not just wording connect any OpenAI-compatible embeddings API.",
290
292
  "memory.settings.vectorEnabled": "Enable vector search",
291
293
  "memory.settings.vectorBaseUrl": "Base URL",
292
294
  "memory.settings.vectorApiKey": "API Key",
@@ -297,7 +299,7 @@ window.__ModuleLoader__.load({
297
299
  "memory.settings.vectorReindexing": "Indexing…",
298
300
  "memory.settings.vectorReindexDone": "Indexed {n} items",
299
301
  "memory.settings.apiTokenTitle": "API Token (optional)",
300
- "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.",
302
+ "memory.settings.apiTokenHint": "Lock write operations behind a token browsing stays open. Clear and save to disable.",
301
303
  "memory.settings.apiTokenPlaceholder": "Empty = no auth (default)",
302
304
  "memory.settings.apiTokenSave": "Save Token",
303
305
  "memory.settings.apiTokenSaved": "Token saved",
@@ -315,6 +317,14 @@ window.__ModuleLoader__.load({
315
317
  return label && label !== key ? label : String(type);
316
318
  }
317
319
 
320
+ // Entity types live under their own i18n subtree (person/project/...);
321
+ // unknown or "other" falls back to the raw type string.
322
+ function entityTypeLabel(t, type) {
323
+ const key = `memory.entity.${type}`;
324
+ const label = t(key);
325
+ return label && label !== key ? label : String(type);
326
+ }
327
+
318
328
  function formatDate(value) {
319
329
  if (!value) return "—";
320
330
  const date = new Date(value);
@@ -392,10 +402,10 @@ window.__ModuleLoader__.load({
392
402
  ".mneme-xsearch{width:100%}",
393
403
  ".mneme-xselect{flex:1;min-width:0}",
394
404
  ".mneme-xcolhead{flex:none;font-size:12px;font-weight:500;color:var(--dsw-alias-label-tertiary);padding:2px 8px 8px}",
395
- ".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}",
405
+ ".mneme-xtype{display:flex;justify-content:flex-start;align-items:center;gap:8px;width:100%;padding:5px 8px;border:none;border-radius:8px;background:none;color:var(--dsw-alias-label-secondary);cursor:pointer;font-family:inherit;font-size:13px;line-height:18px;text-align:left}",
396
406
  ".mneme-xtype:hover{background:var(--dsw-alias-interactive-bg-hover)}",
397
407
  ".mneme-xtype.mneme-active{background:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-primary);font-weight:500}",
398
- ".mneme-xcount2{flex:none;font-size:12px;color:var(--dsw-alias-label-tertiary)}",
408
+ ".mneme-xcount2{flex:none;margin-left:auto;font-size:12px;color:var(--dsw-alias-label-tertiary)}",
399
409
  ".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}",
400
410
  ".mneme-xmonth:first-child{margin-top:0}",
401
411
  ".mneme-xmonth:hover{background:var(--dsw-alias-interactive-bg-hover)}",
@@ -407,7 +417,11 @@ window.__ModuleLoader__.load({
407
417
  ".mneme-xitem.mneme-active{background:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-primary)}",
408
418
  ".mneme-xtime{flex:none;font-size:12px;line-height:16px;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums}",
409
419
  ".mneme-xname{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
410
- ".mneme-xempty{padding:32px 16px;text-align:center;color:var(--dsw-alias-label-tertiary);font-size:13px}",
420
+ ".mneme-xempty{padding:18px 16px;text-align:center;color:var(--dsw-alias-label-tertiary);font-size:13px}",
421
+ // --- memory-type color dots (mirrored across filter / timeline / detail) ---
422
+ ".mneme-xdot{flex:none;display:inline-block;width:8px;height:8px;border-radius:50%;background:currentColor}",
423
+ ".mneme-xdot--all{background:transparent;border:1.5px solid currentColor;opacity:.55;box-sizing:border-box}",
424
+ ".mneme-xitem .mneme-xdot{align-self:center}",
411
425
  // --- memory explorer root ---
412
426
  ".mneme-xmain{display:flex;flex-direction:column}",
413
427
  ".mneme-xfilter-bar{flex:none;height:48px;display:flex;align-items:center;gap:8px;padding:0 12px;border-bottom:1px solid var(--dsw-alias-border-l2,#ddd)}",
@@ -419,6 +433,7 @@ window.__ModuleLoader__.load({
419
433
  ".mneme-xdinner{max-width:720px}",
420
434
  ".mneme-xdtitle{font-size:16px;font-weight:600;line-height:24px;color:var(--dsw-alias-label-primary);margin-bottom:10px;word-break:break-word}",
421
435
  ".mneme-xdmeta{display:flex;flex-wrap:wrap;gap:4px 14px;margin-bottom:6px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}",
436
+ ".mneme-xdsrc{display:inline-block;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:bottom}",
422
437
  ".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}",
423
438
  ".mneme-xdactions{display:flex;gap:8px;margin-top:18px}",
424
439
  // --- wiki-link backlinks panel (detail pane footer) ---
@@ -486,7 +501,28 @@ window.__ModuleLoader__.load({
486
501
  ".mneme-dir-itime{flex:none;font-size:12px;line-height:16px;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums}",
487
502
  ".mneme-dir-empty{color:var(--dsw-alias-label-tertiary);padding:24px 0;text-align:center;font-size:13px}",
488
503
  // --- settings sub-view ---
489
- ".mneme-xsettings{flex:1;min-height:0;overflow-y:auto;padding:24px 24px 48px}",
504
+ ".mneme-xsettings{flex:1;min-height:0;overflow-y:auto;padding:8px 24px 48px;box-sizing:border-box}",
505
+ ".mneme-set-sec{padding:20px 0 24px;border-bottom:1px solid var(--dsw-alias-border-l2)}",
506
+ ".mneme-xsettings .mneme-set-sec:last-child{border-bottom:none}",
507
+ ".mneme-set-title{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary);margin-bottom:4px}",
508
+ ".mneme-set-desc{font-size:13px;line-height:19px;color:var(--dsw-alias-label-tertiary);margin-bottom:14px}",
509
+ ".mneme-set-input{box-sizing:border-box;width:100%;height:34px;padding:0 12px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base,transparent);color:var(--dsw-alias-label-primary);font-family:inherit;font-size:13px;outline:none;margin-bottom:10px;transition:border-color .12s,box-shadow .12s}",
510
+ ".mneme-set-input: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)}",
511
+ ".mneme-set-input::placeholder{color:var(--dsw-alias-label-tertiary)}",
512
+ ".mneme-set-input--area{height:auto;min-height:76px;padding:8px 12px;resize:vertical;line-height:1.6}",
513
+ ".mneme-btn{flex:none;height:30px;padding:0 12px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:none;color:var(--dsw-alias-label-primary);cursor:pointer;font-family:inherit;font-size:12px;line-height:16px;display:inline-flex;align-items:center;gap:6px}",
514
+ ".mneme-btn:hover{background:var(--dsw-alias-interactive-bg-hover)}",
515
+ ".mneme-btn:disabled{opacity:.5;cursor:default}",
516
+ ".mneme-saved{font-size:12px;color:var(--dsw-alias-state-success,#2a7)}",
517
+ ".mneme-set-row{display:flex;align-items:flex-start;gap:10px;padding:10px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;margin-bottom:8px;background:var(--dsw-alias-bg-base,transparent)}",
518
+ ".mneme-set-idx{flex:none;width:20px;height:20px;border-radius:50%;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:18px;display:inline-flex;align-items:center;justify-content:center;font-variant-numeric:tabular-nums}",
519
+ ".mneme-set-ruletext{flex:1;min-width:0;font-size:13px;line-height:20px;color:var(--dsw-alias-label-primary);word-break:break-word}",
520
+ ".mneme-set-del{flex:none;opacity:0;transition:opacity .12s;border:none;background:none;color:var(--dsw-alias-label-tertiary);cursor:pointer;font-size:14px;line-height:20px;padding:0 4px;border-radius:6px}",
521
+ ".mneme-set-row:hover .mneme-set-del,.mneme-set-row:focus-within .mneme-set-del{opacity:1}",
522
+ ".mneme-set-del:hover{color:var(--dsw-alias-state-error,#c33);background:var(--dsw-alias-interactive-bg-hover)}",
523
+ ".mneme-set-cmd{flex:1;min-width:0}",
524
+ ".mneme-set-cmdname{font-size:13px;font-weight:600;color:var(--dsw-alias-state-business-primary)}",
525
+ ".mneme-set-cmddesc{font-size:12px;line-height:17px;color:var(--dsw-alias-label-tertiary);margin-top:1px;word-break:break-word}",
490
526
  ".mneme-xsettings-inner{max-width:640px}",
491
527
  // --- overview sub-view (layered stats dashboard, .mneme-ov-* only) ---
492
528
  ".mneme-ov-wrap{flex:1;min-height:0;overflow-y:auto;display:flex;flex-direction:column;gap:12px;padding:12px 16px 28px}",
@@ -553,6 +589,31 @@ window.__ModuleLoader__.load({
553
589
  function typeColor(type) {
554
590
  return TYPE_COLORS[type] || "#94a3b8";
555
591
  }
592
+
593
+ // Memory-type → dot fill, shared by the type filter, timeline rows and the
594
+ // detail meta so one hue always means one memory type across the panel.
595
+ // Same palette rules as TYPE_COLORS: fixed hues, medium saturation,
596
+ // similar luminance, legible on light and dark themes.
597
+ const MEMORY_TYPE_COLORS = {
598
+ preference: "#f59e0b",
599
+ project: "#22c55e",
600
+ decision: "#3b82f6",
601
+ summary: "#a855f7",
602
+ user: "#f59e0b",
603
+ fact: "#22c55e",
604
+ history: "#94a3b8"
605
+ };
606
+ function memoryTypeColor(type) {
607
+ return MEMORY_TYPE_COLORS[type] || "#94a3b8";
608
+ }
609
+
610
+ // Compact date for the detail meta ("2026/9/7"); the full timestamp
611
+ // stays in the tooltip.
612
+ function formatDateShort(value) {
613
+ if (!value) return "—";
614
+ const date = new Date(value);
615
+ return Number.isNaN(date.getTime()) ? "—" : date.toLocaleDateString();
616
+ }
556
617
  function nodeRadius(n) {
557
618
  // mention_count → area-ish growth, clamped so hubs stay legible.
558
619
  const base = 7 + Math.min(20, Math.max(1, n.mention_count ?? 1)) * 0.55;
@@ -681,6 +742,7 @@ window.__ModuleLoader__.load({
681
742
  const svgRef = useRef(null);
682
743
  const posRef = useRef([]); // live simulation positions, not React state
683
744
  const dragRef = useRef(null); // { id, moved }
745
+ const viewRef = useRef({ x: 0, y: 0, k: 1 }); // pan/zoom viewport, viewBox units
684
746
  const [frame, setFrame] = useState(0); // re-render tick driven by the simulation
685
747
 
686
748
  useEffect(() => {
@@ -786,12 +848,13 @@ window.__ModuleLoader__.load({
786
848
  // drag handling on the svg surface
787
849
  const onNodeMouseDown = (e, node) => {
788
850
  e.preventDefault();
851
+ e.stopPropagation(); // node drag must not start a background pan
789
852
  dragRef.current = { node, moved: false };
790
853
  const startX = e.clientX, startY = e.clientY;
791
854
  const origX = node.x, origY = node.y;
792
855
  const svg = svgRef.current;
793
856
  const rect = svg.getBoundingClientRect();
794
- const scale = 380 / Math.max(1, rect.width); // viewBox width / css width
857
+ const scale = 380 / Math.max(1, rect.width) / viewRef.current.k; // viewBox units per css px, zoom-aware
795
858
  const onMove = (ev) => {
796
859
  dragRef.current.moved = true;
797
860
  node.x = origX + (ev.clientX - startX) * scale;
@@ -807,6 +870,70 @@ window.__ModuleLoader__.load({
807
870
  window.addEventListener("mouseup", onUp);
808
871
  };
809
872
 
873
+ // --- canvas pan & zoom ---
874
+ // The viewport (translate + scale) lives in viewRef so the simulation
875
+ // and the handlers share it without re-mounting; every mutation bumps
876
+ // `frame` to re-render. Panning starts on the bare svg surface (node
877
+ // mousedowns stopPropagation above); zooming is a native non-passive
878
+ // wheel listener because React attaches wheel handlers passively and
879
+ // could not preventDefault the page scroll.
880
+ const svgToView = (clientX, clientY) => {
881
+ const svg = svgRef.current;
882
+ const rect = svg.getBoundingClientRect();
883
+ const sx = VIEW_W / Math.max(1, rect.width);
884
+ const sy = VIEW_H / Math.max(1, rect.height);
885
+ return { x: (clientX - rect.left) * sx, y: (clientY - rect.top) * sy };
886
+ };
887
+
888
+ const onSurfaceMouseDown = (e) => {
889
+ if (e.button !== 0) return;
890
+ e.preventDefault();
891
+ const start = { x: e.clientX, y: e.clientY };
892
+ const orig = { ...viewRef.current };
893
+ const onMove = (ev) => {
894
+ const rect = svgRef.current?.getBoundingClientRect();
895
+ if (!rect) return;
896
+ const sx = VIEW_W / Math.max(1, rect.width);
897
+ const sy = VIEW_H / Math.max(1, rect.height);
898
+ const v = viewRef.current;
899
+ v.x = orig.x + (ev.clientX - start.x) * sx;
900
+ v.y = orig.y + (ev.clientY - start.y) * sy;
901
+ setFrame((f) => f + 1);
902
+ };
903
+ const onUp = () => {
904
+ window.removeEventListener("mousemove", onMove);
905
+ window.removeEventListener("mouseup", onUp);
906
+ };
907
+ window.addEventListener("mousemove", onMove);
908
+ window.addEventListener("mouseup", onUp);
909
+ };
910
+
911
+ useEffect(() => {
912
+ const svg = svgRef.current;
913
+ if (!svg) return undefined;
914
+ const onWheel = (e) => {
915
+ e.preventDefault();
916
+ const v = viewRef.current;
917
+ const factor = e.deltaY < 0 ? 1.12 : 1 / 1.12;
918
+ const k = Math.min(3, Math.max(0.5, v.k * factor));
919
+ const s = svgToView(e.clientX, e.clientY);
920
+ // keep the point under the cursor pinned while the scale changes
921
+ const px = (s.x - v.x) / v.k;
922
+ const py = (s.y - v.y) / v.k;
923
+ v.k = k;
924
+ v.x = s.x - px * k;
925
+ v.y = s.y - py * k;
926
+ setFrame((f) => f + 1);
927
+ };
928
+ svg.addEventListener("wheel", onWheel, { passive: false });
929
+ return () => svg.removeEventListener("wheel", onWheel);
930
+ });
931
+
932
+ const resetView = () => {
933
+ viewRef.current = { x: 0, y: 0, k: 1 };
934
+ setFrame((f) => f + 1);
935
+ };
936
+
810
937
  const onNodeClick = (node) => {
811
938
  if (dragRef.current?.moved) return; // it was a drag, not a click
812
939
  setSelected({ kind: "node", node });
@@ -871,7 +998,12 @@ window.__ModuleLoader__.load({
871
998
  className: depth === 2 ? "mneme-chip mneme-active" : "mneme-chip",
872
999
  title: t("memory.graph.depth"),
873
1000
  onClick: () => setDepth(depth === 1 ? 2 : 1)
874
- }, `${depth} ${t("memory.graph.depth")}`)
1001
+ }, `${depth} ${t("memory.graph.depth")}`),
1002
+ h("button", {
1003
+ className: "mneme-chip",
1004
+ title: t("memory.graph.resetView"),
1005
+ onClick: resetView
1006
+ }, t("memory.graph.resetView"))
875
1007
  ),
876
1008
  status === "idle" && h("div", { className: "mneme-hint" }, t("memory.graph.empty")),
877
1009
  status === "loading" && h("div", { className: "mneme-hint" }, t("memory.graph.loading")),
@@ -883,8 +1015,13 @@ window.__ModuleLoader__.load({
883
1015
  h("svg", {
884
1016
  ref: svgRef,
885
1017
  className: "mneme-graphsvg",
886
- viewBox: `0 0 ${VIEW_W} ${VIEW_H}`
1018
+ viewBox: `0 0 ${VIEW_W} ${VIEW_H}`,
1019
+ style: { height: 320 },
1020
+ onMouseDown: onSurfaceMouseDown
887
1021
  },
1022
+ h("g", {
1023
+ transform: `translate(${viewRef.current.x},${viewRef.current.y}) scale(${viewRef.current.k})`
1024
+ },
888
1025
  data.edges.map((e) => {
889
1026
  const a = nodeById.get(e.from), b = nodeById.get(e.to);
890
1027
  if (!a || !b) return null;
@@ -906,6 +1043,7 @@ window.__ModuleLoader__.load({
906
1043
  h("circle", { r: nodeRadius(n), fill: typeColor(n.type), fillOpacity: nodeOpacity(n) }),
907
1044
  h("text", { className: "mneme-glabel", y: nodeRadius(n) + 13 }, n.name)
908
1045
  ))
1046
+ )
909
1047
  ),
910
1048
  h("div", { className: "mneme-graphhint" }, t("memory.graph.hint"))
911
1049
  )),
@@ -1078,109 +1216,135 @@ window.__ModuleLoader__.load({
1078
1216
  } catch { /* ignore */ }
1079
1217
  }
1080
1218
 
1081
- 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 };
1082
- const labelStyle = { fontSize: 12, fontWeight: 600, margin: "12px 0 4px", color: "var(--dsw-alias-label-primary)" };
1083
- const hintStyle = { fontSize: 11, color: "var(--dsw-alias-label-tertiary, #999)", marginBottom: 8 };
1084
- const rowStyle = { display: "flex", justifyContent: "space-between", alignItems: "center", gap: 8, padding: "6px 0", borderBottom: "1px solid var(--dsw-alias-border-l1, #eee)" };
1085
-
1086
- return h("div", { style: { paddingBottom: 8 } },
1087
- // api token (optional)
1088
- h("div", { style: { ...labelStyle, marginTop: 20 } }, t("memory.settings.apiTokenTitle")),
1089
- h("div", { style: hintStyle }, t("memory.settings.apiTokenHint")),
1090
- h("div", { style: { display: "flex", gap: 6 } },
1091
- h("input", { style: { ...inputStyle, flex: 1, marginBottom: 0 }, type: "password", value: apiToken, placeholder: t("memory.settings.apiTokenPlaceholder"), onChange: (e) => setApiToken(e.target.value) }),
1092
- h("button", { style: styles.footerButton, onClick: saveToken }, t("memory.settings.apiTokenSave"))
1093
- ),
1094
- apiTokenSaved && h("div", { style: { fontSize: 12, color: "var(--dsw-alias-state-success, #2a7)" } }, t("memory.settings.apiTokenSaved")),
1095
- // profile
1096
- h("div", { style: labelStyle }, t("memory.settings.profile")),
1097
- h("div", { style: hintStyle }, t("memory.settings.profileHint")),
1098
- h("textarea", {
1099
- style: { ...inputStyle, minHeight: 72, resize: "vertical", fontFamily: "inherit", padding: "8px 12px", height: "auto" },
1100
- value: profile,
1101
- placeholder: t("memory.settings.profile"),
1102
- onChange: (e) => setProfile(e.target.value)
1103
- }),
1104
- h("div", null,
1105
- h("button", { style: styles.footerButton, onClick: saveProfile }, t("memory.settings.profileSave")),
1106
- saved && h("span", { style: { fontSize: 12, color: "var(--dsw-alias-state-success, #2a7)" } }, t("memory.settings.profileSaved"))
1107
- ),
1108
- // rules
1109
- h("div", { style: labelStyle }, t("memory.settings.rules")),
1110
- h("div", { style: hintStyle }, t("memory.settings.rulesHint")),
1111
- rules.length === 0 && h("div", { style: { fontSize: 12, color: "var(--dsw-alias-label-tertiary, #999)", padding: "8px 0" } }, t("memory.settings.empty")),
1112
- rules.map((rule, i) =>
1113
- h("div", { key: i, style: rowStyle },
1114
- h("span", { style: { fontSize: 13, flex: 1 } }, rule),
1115
- h("button", { style: { ...styles.footerButton, color: "var(--dsw-alias-state-error, #c33)" }, onClick: () => removeRule(i) }, "×")
1219
+ return h("div", null,
1220
+ h("section", { className: "mneme-set-sec" },
1221
+ h("div", { className: "mneme-set-title" }, t("memory.settings.profile")),
1222
+ h("div", { className: "mneme-set-desc" }, t("memory.settings.profileHint")),
1223
+ h("textarea", {
1224
+ className: "mneme-set-input mneme-set-input--area",
1225
+ value: profile,
1226
+ placeholder: t("memory.settings.profile"),
1227
+ onChange: (e) => setProfile(e.target.value)
1228
+ }),
1229
+ h("div", { style: { display: "flex", alignItems: "center", gap: 8 } },
1230
+ h("button", { className: "mneme-btn", onClick: saveProfile }, t("memory.settings.profileSave")),
1231
+ saved && h("span", { className: "mneme-saved" }, t("memory.settings.profileSaved"))
1116
1232
  )
1117
1233
  ),
1118
- h("div", { style: { display: "flex", gap: 6 } },
1119
- h("input", {
1120
- style: { ...inputStyle, flex: 1, marginBottom: 0 },
1121
- value: newRule,
1122
- placeholder: t("memory.settings.rulePlaceholder"),
1123
- onChange: (e) => setNewRule(e.target.value)
1124
- }),
1125
- h("button", { style: styles.footerButton, onClick: addRule }, t("memory.settings.ruleAdd"))
1234
+ h("section", { className: "mneme-set-sec" },
1235
+ h("div", { className: "mneme-set-title" }, t("memory.settings.rules")),
1236
+ h("div", { className: "mneme-set-desc" }, t("memory.settings.rulesHint")),
1237
+ rules.length === 0 && h("div", { className: "mneme-xempty" }, t("memory.settings.empty")),
1238
+ rules.map((rule, i) =>
1239
+ h("div", { key: i, className: "mneme-set-row" },
1240
+ h("span", { className: "mneme-set-idx" }, String(i + 1)),
1241
+ h("span", { className: "mneme-set-ruletext" }, rule),
1242
+ h("button", {
1243
+ className: "mneme-set-del",
1244
+ title: t("memory.settings.cmdDelete"),
1245
+ "aria-label": t("memory.settings.cmdDelete"),
1246
+ onClick: () => removeRule(i)
1247
+ }, "×")
1248
+ )
1249
+ ),
1250
+ h("div", { style: { display: "flex", gap: 8 } },
1251
+ h("input", {
1252
+ className: "mneme-set-input",
1253
+ style: { marginBottom: 0 },
1254
+ value: newRule,
1255
+ placeholder: t("memory.settings.rulePlaceholder"),
1256
+ onChange: (e) => setNewRule(e.target.value),
1257
+ onKeyDown: (e) => { if (e.key === "Enter") addRule(); }
1258
+ }),
1259
+ h("button", { className: "mneme-btn", onClick: addRule }, t("memory.settings.ruleAdd"))
1260
+ )
1126
1261
  ),
1127
- // custom commands
1128
- h("div", { style: labelStyle }, t("memory.settings.commands")),
1129
- h("div", { style: hintStyle }, t("memory.settings.commandsHint")),
1130
- commands.length === 0 && h("div", { style: { fontSize: 12, color: "var(--dsw-alias-label-tertiary, #999)", padding: "8px 0" } }, t("memory.settings.empty")),
1131
- commands.map((cmd) =>
1132
- h("div", { key: cmd.id, style: rowStyle },
1133
- h("div", { style: { flex: 1 } },
1134
- h("div", { style: { fontSize: 13, fontWeight: 600 } }, `/${cmd.name}`),
1135
- h("div", { style: { fontSize: 11, color: "var(--dsw-alias-label-tertiary, #999)" } }, cmd.description || cmd.instruction)
1262
+ h("section", { className: "mneme-set-sec" },
1263
+ h("div", { className: "mneme-set-title" }, t("memory.settings.commands")),
1264
+ h("div", { className: "mneme-set-desc" }, t("memory.settings.commandsHint")),
1265
+ commands.length === 0 && h("div", { className: "mneme-xempty" }, t("memory.settings.empty")),
1266
+ commands.map((cmd) =>
1267
+ h("div", { key: cmd.id, className: "mneme-set-row" },
1268
+ h("div", { className: "mneme-set-cmd" },
1269
+ h("div", { className: "mneme-set-cmdname" }, `/${cmd.name}`),
1270
+ (cmd.description || cmd.instruction) && h("div", { className: "mneme-set-cmddesc" }, cmd.description || cmd.instruction)
1271
+ ),
1272
+ h("button", {
1273
+ className: "mneme-set-del",
1274
+ title: t("memory.settings.cmdDelete"),
1275
+ "aria-label": t("memory.settings.cmdDelete"),
1276
+ onClick: () => removeCommand(cmd.id)
1277
+ }, "×")
1278
+ )
1279
+ ),
1280
+ h("div", { style: { display: "grid", gap: 8, marginTop: 10 } },
1281
+ h("div", { style: { display: "flex", gap: 8 } },
1282
+ h("input", { className: "mneme-set-input", style: { marginBottom: 0 }, value: newCmd.name, placeholder: t("memory.settings.cmdName"), onChange: (e) => setNewCmd({ ...newCmd, name: e.target.value }) }),
1283
+ h("input", { className: "mneme-set-input", style: { marginBottom: 0 }, value: newCmd.description, placeholder: t("memory.settings.cmdDesc"), onChange: (e) => setNewCmd({ ...newCmd, description: e.target.value }) })
1136
1284
  ),
1137
- h("button", { style: { ...styles.footerButton, color: "var(--dsw-alias-state-error, #c33)" }, onClick: () => removeCommand(cmd.id) }, t("memory.settings.cmdDelete"))
1285
+ h("textarea", { className: "mneme-set-input mneme-set-input--area", style: { minHeight: 56 }, value: newCmd.instruction, placeholder: t("memory.settings.cmdInstruction"), onChange: (e) => setNewCmd({ ...newCmd, instruction: e.target.value }) }),
1286
+ h("div", { style: { display: "flex", alignItems: "center", gap: 8 } },
1287
+ h("button", { className: "mneme-btn", onClick: addCommand }, t("memory.settings.cmdAdd")),
1288
+ cmdError && h("span", { style: { fontSize: 12, color: "var(--dsw-alias-state-error, #c33)" } }, cmdError)
1289
+ )
1138
1290
  )
1139
1291
  ),
1140
- h("div", { style: { display: "grid", gap: 6, marginTop: 6 } },
1141
- h("input", { style: { ...inputStyle, marginBottom: 0 }, value: newCmd.name, placeholder: t("memory.settings.cmdName"), onChange: (e) => setNewCmd({ ...newCmd, name: e.target.value }) }),
1142
- h("input", { style: { ...inputStyle, marginBottom: 0 }, value: newCmd.description, placeholder: t("memory.settings.cmdDesc"), onChange: (e) => setNewCmd({ ...newCmd, description: e.target.value }) }),
1143
- 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 }) }),
1144
- h("button", { style: styles.footerButton, onClick: addCommand }, t("memory.settings.cmdAdd")),
1145
- cmdError && h("div", { style: { fontSize: 12, color: "var(--dsw-alias-state-error, #c33)" } }, cmdError)
1146
- ),
1147
- // auto-tag switch
1148
- h("div", { style: { ...labelStyle, marginTop: 20 } }, t("memory.settings.autoTagTitle")),
1149
- h("div", { style: hintStyle }, t("memory.settings.autoTagHint")),
1150
- h("label", { style: { display: "flex", alignItems: "center", gap: 6, marginBottom: 8, fontSize: 13 } },
1151
- h("input", { type: "checkbox", checked: autoTag, onChange: (e) => setAutoTag(e.target.checked) }),
1152
- h("span", null, t("memory.settings.autoTagEnabled"))
1153
- ),
1154
- h("div", { style: { display: "flex", alignItems: "center", gap: 6 } },
1155
- h("button", { style: styles.footerButton, onClick: saveAutoTag }, t("memory.settings.autoTagSave")),
1156
- autoTagSaved && h("span", { style: { fontSize: 12, color: "var(--dsw-alias-state-success, #2a7)" } }, t("memory.settings.autoTagSaved"))
1157
- ),
1158
- // sidebar trigger switch (issue #38)
1159
- h("div", { style: { ...labelStyle, marginTop: 20 } }, t("memory.settings.sidebarTriggerTitle")),
1160
- h("div", { style: hintStyle }, t("memory.settings.sidebarTriggerHint")),
1161
- h("label", { style: { display: "flex", alignItems: "center", gap: 6, marginBottom: 8, fontSize: 13 } },
1162
- h("input", { type: "checkbox", checked: showSidebarTrigger, onChange: (e) => setShowSidebarTrigger(e.target.checked) }),
1163
- h("span", null, t("memory.settings.sidebarTriggerEnabled"))
1292
+ h("section", { className: "mneme-set-sec" },
1293
+ h("div", { className: "mneme-set-title" }, t("memory.settings.autoTagTitle")),
1294
+ h("div", { className: "mneme-set-desc" }, t("memory.settings.autoTagHint")),
1295
+ h("label", { style: { display: "flex", alignItems: "center", gap: 8, marginBottom: 10, fontSize: 13, color: "var(--dsw-alias-label-primary)" } },
1296
+ h("input", { type: "checkbox", checked: autoTag, onChange: (e) => setAutoTag(e.target.checked) }),
1297
+ h("span", null, t("memory.settings.autoTagEnabled"))
1298
+ ),
1299
+ h("div", { style: { display: "flex", alignItems: "center", gap: 8 } },
1300
+ h("button", { className: "mneme-btn", onClick: saveAutoTag }, t("memory.settings.autoTagSave")),
1301
+ autoTagSaved && h("span", { className: "mneme-saved" }, t("memory.settings.autoTagSaved"))
1302
+ )
1164
1303
  ),
1165
- h("div", { style: { display: "flex", alignItems: "center", gap: 6 } },
1166
- h("button", { style: styles.footerButton, onClick: saveSidebarTrigger }, t("memory.settings.sidebarTriggerSave")),
1167
- sidebarTriggerSaved && h("span", { style: { fontSize: 12, color: "var(--dsw-alias-state-success, #2a7)" } }, t("memory.settings.sidebarTriggerSaved"))
1304
+ h("section", { className: "mneme-set-sec" },
1305
+ h("div", { className: "mneme-set-title" }, t("memory.settings.sidebarTriggerTitle")),
1306
+ h("div", { className: "mneme-set-desc" }, t("memory.settings.sidebarTriggerHint")),
1307
+ h("label", { style: { display: "flex", alignItems: "center", gap: 8, marginBottom: 10, fontSize: 13, color: "var(--dsw-alias-label-primary)" } },
1308
+ h("input", { type: "checkbox", checked: showSidebarTrigger, onChange: (e) => setShowSidebarTrigger(e.target.checked) }),
1309
+ h("span", null, t("memory.settings.sidebarTriggerEnabled"))
1310
+ ),
1311
+ h("div", { style: { display: "flex", alignItems: "center", gap: 8 } },
1312
+ h("button", { className: "mneme-btn", onClick: saveSidebarTrigger }, t("memory.settings.sidebarTriggerSave")),
1313
+ sidebarTriggerSaved && h("span", { className: "mneme-saved" }, t("memory.settings.sidebarTriggerSaved"))
1314
+ )
1168
1315
  ),
1169
- // vector search
1170
- h("div", { style: { ...labelStyle, marginTop: 20 } }, t("memory.settings.vectorTitle")),
1171
- h("div", { style: hintStyle }, t("memory.settings.vectorHint")),
1172
- h("label", { style: { display: "flex", alignItems: "center", gap: 6, marginBottom: 8, fontSize: 13 } },
1173
- h("input", { type: "checkbox", checked: !!vector.enabled, onChange: (e) => setVector({ ...vector, enabled: e.target.checked }) }),
1174
- h("span", null, t("memory.settings.vectorEnabled"))
1316
+ h("section", { className: "mneme-set-sec" },
1317
+ h("div", { className: "mneme-set-title" }, t("memory.settings.vectorTitle")),
1318
+ h("div", { className: "mneme-set-desc" }, t("memory.settings.vectorHint")),
1319
+ h("label", { style: { display: "flex", alignItems: "center", gap: 8, marginBottom: 10, fontSize: 13, color: "var(--dsw-alias-label-primary)" } },
1320
+ h("input", { type: "checkbox", checked: !!vector.enabled, onChange: (e) => setVector({ ...vector, enabled: e.target.checked }) }),
1321
+ h("span", null, t("memory.settings.vectorEnabled"))
1322
+ ),
1323
+ h("input", { className: "mneme-set-input", value: vector.baseUrl, placeholder: t("memory.settings.vectorBaseUrl"), onChange: (e) => setVector({ ...vector, baseUrl: e.target.value }) }),
1324
+ h("input", { className: "mneme-set-input", type: "password", value: vector.apiKey, placeholder: t("memory.settings.vectorApiKey"), onChange: (e) => setVector({ ...vector, apiKey: e.target.value }) }),
1325
+ h("input", { className: "mneme-set-input", value: vector.model, placeholder: t("memory.settings.vectorModel"), onChange: (e) => setVector({ ...vector, model: e.target.value }) }),
1326
+ h("div", { style: { display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" } },
1327
+ h("button", { className: "mneme-btn", onClick: saveVector }, t("memory.settings.vectorSave")),
1328
+ vectorSaved && h("span", { className: "mneme-saved" }, t("memory.settings.vectorSaved")),
1329
+ h("button", { className: "mneme-btn", onClick: reindex, disabled: reindexing }, reindexing ? t("memory.settings.vectorReindexing") : t("memory.settings.vectorReindex")),
1330
+ reindexMsg && h("span", { style: { fontSize: 12, color: "var(--dsw-alias-label-secondary, #666)" } }, reindexMsg)
1331
+ )
1175
1332
  ),
1176
- h("input", { style: inputStyle, value: vector.baseUrl, placeholder: t("memory.settings.vectorBaseUrl"), onChange: (e) => setVector({ ...vector, baseUrl: e.target.value }) }),
1177
- h("input", { style: inputStyle, type: "password", value: vector.apiKey, placeholder: t("memory.settings.vectorApiKey"), onChange: (e) => setVector({ ...vector, apiKey: e.target.value }) }),
1178
- h("input", { style: inputStyle, value: vector.model, placeholder: t("memory.settings.vectorModel"), onChange: (e) => setVector({ ...vector, model: e.target.value }) }),
1179
- h("div", { style: { display: "flex", alignItems: "center", gap: 6, flexWrap: "wrap" } },
1180
- h("button", { style: styles.footerButton, onClick: saveVector }, t("memory.settings.vectorSave")),
1181
- vectorSaved && h("span", { style: { fontSize: 12, color: "var(--dsw-alias-state-success, #2a7)" } }, t("memory.settings.vectorSaved")),
1182
- h("button", { style: styles.footerButton, onClick: reindex, disabled: reindexing }, reindexing ? t("memory.settings.vectorReindexing") : t("memory.settings.vectorReindex")),
1183
- reindexMsg && h("span", { style: { fontSize: 12, color: "var(--dsw-alias-label-secondary, #666)" } }, reindexMsg)
1333
+ h("section", { className: "mneme-set-sec" },
1334
+ h("div", { className: "mneme-set-title" }, t("memory.settings.apiTokenTitle")),
1335
+ h("div", { className: "mneme-set-desc" }, t("memory.settings.apiTokenHint")),
1336
+ h("div", { style: { display: "flex", gap: 8 } },
1337
+ h("input", {
1338
+ className: "mneme-set-input",
1339
+ style: { marginBottom: 0 },
1340
+ type: "password",
1341
+ value: apiToken,
1342
+ placeholder: t("memory.settings.apiTokenPlaceholder"),
1343
+ onChange: (e) => setApiToken(e.target.value)
1344
+ }),
1345
+ h("button", { className: "mneme-btn", onClick: saveToken }, t("memory.settings.apiTokenSave"))
1346
+ ),
1347
+ apiTokenSaved && h("div", { style: { marginTop: 8 } }, h("span", { className: "mneme-saved" }, t("memory.settings.apiTokenSaved")))
1184
1348
  )
1185
1349
  );
1186
1350
  }
@@ -1197,28 +1361,49 @@ window.__ModuleLoader__.load({
1197
1361
  // screen. There the tab simply does not exist, so activateExplorerTab
1198
1362
  // resolves false and the sidebar entry falls back to the full-viewport
1199
1363
  // overlay surface below.
1200
- function findExplorerTab(label) {
1201
- const tabs = document.querySelectorAll('[role="tab"]');
1202
- for (const tab of tabs) {
1203
- if ((tab.textContent || "").trim() === label) return tab;
1364
+ // Candidate tabs whose label matches ours exactly. The conversation tab
1365
+ // ring carries no registrant id in the DOM, so the label is all we have —
1366
+ // but other plugins may register views (or settings dialogs may render
1367
+ // panes) with the same text, and hidden panes still sit in the document.
1368
+ // Restrict to rendered tabs (offsetParent filters display:none subtrees)
1369
+ // and verify by rendering, not by label alone (see activateExplorerTab).
1370
+ function findExplorerTabs(label) {
1371
+ const matches = [];
1372
+ for (const tab of document.querySelectorAll('[role="tab"]')) {
1373
+ if ((tab.textContent || "").trim() !== label) continue;
1374
+ if (tab.offsetParent === null && getComputedStyle(tab).position !== "fixed") continue;
1375
+ matches.push(tab);
1204
1376
  }
1205
- return null;
1377
+ return matches;
1206
1378
  }
1207
1379
 
1208
1380
  // Click the memory library tab and wait (bounded, rAF-polled) until the
1209
1381
  // host actually marks it selected — a silent React re-render gap must not
1210
1382
  // be mistaken for success, or the fallback would never kick in.
1383
+ // Click the memory library tab and wait (bounded, rAF-polled) until the
1384
+ // host marks it selected AND our explorer actually rendered (`.mneme-x`).
1385
+ // aria-selected alone cannot tell our tab from a same-labelled tab of
1386
+ // another plugin — the sidebar entry must not activate theirs. On a
1387
+ // failed candidate, move on to the next one; resolve false when the
1388
+ // fallback overlay should take over.
1211
1389
  function activateExplorerTab(label) {
1212
1390
  return new Promise((resolve) => {
1213
- const tab = findExplorerTab(label);
1214
- if (!tab) { resolve(false); return; }
1215
- tab.click();
1216
- const deadline = Date.now() + 400;
1217
- (function check() {
1218
- if (tab.getAttribute("aria-selected") === "true") { resolve(true); return; }
1219
- if (Date.now() >= deadline) { resolve(false); return; }
1220
- requestAnimationFrame(check);
1221
- })();
1391
+ const candidates = findExplorerTabs(label);
1392
+ if (candidates.length === 0) { resolve(false); return; }
1393
+ let index = 0;
1394
+ const tryNext = () => {
1395
+ if (index >= candidates.length) { resolve(false); return; }
1396
+ const tab = candidates[index++];
1397
+ tab.click();
1398
+ const deadline = Date.now() + 400;
1399
+ (function check() {
1400
+ const rendered = document.querySelector(".mneme-x") !== null;
1401
+ if (tab.getAttribute("aria-selected") === "true" && rendered) { resolve(true); return; }
1402
+ if (Date.now() >= deadline) { setTimeout(tryNext, 0); return; }
1403
+ requestAnimationFrame(check);
1404
+ })();
1405
+ };
1406
+ tryNext();
1222
1407
  });
1223
1408
  }
1224
1409
 
@@ -1802,7 +1987,7 @@ window.__ModuleLoader__.load({
1802
1987
  className: type === "all" ? "mneme-xtype mneme-active" : "mneme-xtype",
1803
1988
  "aria-pressed": type === "all",
1804
1989
  onClick: () => setType("all")
1805
- }, h("span", null, t("memory.tab.all")), h("span", { className: "mneme-xcount2" }, String(items.length))),
1990
+ }, h("span", { className: "mneme-xdot mneme-xdot--all", "aria-hidden": "true" }), h("span", null, t("memory.tab.all")), h("span", { className: "mneme-xcount2" }, String(items.length))),
1806
1991
  knownTypes.concat(extraTypes).map((key) =>
1807
1992
  h("button", {
1808
1993
  key,
@@ -1810,6 +1995,7 @@ window.__ModuleLoader__.load({
1810
1995
  "aria-pressed": type === key,
1811
1996
  onClick: () => setType(key)
1812
1997
  },
1998
+ h("span", { className: "mneme-xdot", style: { color: memoryTypeColor(key) }, title: typeLabel(t, key), "aria-hidden": "true" }),
1813
1999
  h("span", null, typeLabel(t, key)),
1814
2000
  h("span", { className: "mneme-xcount2" }, String(counts[key]))
1815
2001
  ))
@@ -1887,6 +2073,7 @@ window.__ModuleLoader__.load({
1887
2073
  className: m.id === selectedId ? "mneme-xitem mneme-active" : "mneme-xitem",
1888
2074
  onClick: () => setSelectedId(m.id)
1889
2075
  },
2076
+ h("span", { className: "mneme-xdot", style: { color: memoryTypeColor(m.type) }, title: typeLabel(t, m.type), "aria-hidden": "true" }),
1890
2077
  h("span", { className: "mneme-xtime" }, time),
1891
2078
  h("span", { className: "mneme-xname" }, m.title || m.content?.slice(0, 40))
1892
2079
  );
@@ -1902,10 +2089,15 @@ window.__ModuleLoader__.load({
1902
2089
  h("div", { className: "mneme-xdinner" },
1903
2090
  h("div", { className: "mneme-xdtitle" }, selected.title),
1904
2091
  h("div", { className: "mneme-xdmeta" },
1905
- h("span", null, `${typeLabel(t, selected.type)} · ${t("memory.explorer.importance")} ★${selected.importance}`),
1906
- selected.source && h("span", null, `${t("memory.explorer.source")}: ${selected.source}`),
1907
- h("span", { title: formatDate(selected.created_at) }, `${t("memory.explorer.created")}: ${formatDate(selected.created_at)}`),
1908
- h("span", { title: formatDate(selected.updated_at) }, `${t("memory.explorer.updated")}: ${formatDate(selected.updated_at)}`)
2092
+ h("span", { style: { display: "inline-flex", alignItems: "center", gap: "6px" } },
2093
+ h("span", { className: "mneme-xdot", style: { color: memoryTypeColor(selected.type) }, "aria-hidden": "true" }),
2094
+ `${typeLabel(t, selected.type)} · ${t("memory.explorer.importance")} ★${selected.importance}`
2095
+ ),
2096
+ selected.source && h("span", { title: selected.source },
2097
+ `${t("memory.explorer.source")}: `,
2098
+ h("span", { className: "mneme-xdsrc" }, selected.source)),
2099
+ h("span", { title: formatDate(selected.created_at) }, `${t("memory.explorer.created")}: ${formatDateShort(selected.created_at)}`),
2100
+ h("span", { title: formatDate(selected.updated_at) }, `${t("memory.explorer.updated")}: ${formatRelativeTime(selected.updated_at, t)}`)
1909
2101
  ),
1910
2102
  h("div", { className: "mneme-xdmeta" },
1911
2103
  h("span", null, t("memory.explorer.tags")),