@modusensus/dsh-mneme 0.7.16 → 0.7.18

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.
package/lib/client.js CHANGED
@@ -74,6 +74,24 @@ window.__ModuleLoader__.load({
74
74
  }, parts.map(([tag, attrs], i) => h(tag, { key: i, ...attrs })));
75
75
  };
76
76
 
77
+ // Lucide v1.42 star path(morphicons 官方配套的数据包)。morphicons 本体
78
+ // 是 ESM-only 的变形动画引擎、插件运行时不允许 require 第三方库,故按
79
+ // 既有惯例内联图标数据静态渲染;实心/空心由 fill 区分,空心降透明度。
80
+ const STAR_PATH_D = "M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z";
81
+ const ImportanceStars = ({ value = 0, size = 13, className }) => {
82
+ const filled = Math.min(5, Math.max(0, Math.round(value || 0)));
83
+ return h("span", { className: `mneme-stars${className ? ` ${className}` : ""}`, role: "img", "aria-label": `${filled}/5` },
84
+ [0, 1, 2, 3, 4].map((i) => h("svg", { key: i, width: size, height: size, viewBox: "0 0 24 24", "aria-hidden": "true" },
85
+ h("path", {
86
+ d: STAR_PATH_D,
87
+ fill: i < filled ? "currentColor" : "none",
88
+ stroke: "currentColor",
89
+ strokeWidth: 1.6,
90
+ strokeLinejoin: "round",
91
+ opacity: i < filled ? 1 : 0.35
92
+ }))));
93
+ };
94
+
77
95
  // Unified API fetcher: attaches the optional apiToken (set in the settings
78
96
  // view, persisted in localStorage) as a Bearer header. When no token has
79
97
  // been configured the header is omitted and the API stays open (default).
@@ -87,7 +105,7 @@ window.__ModuleLoader__.load({
87
105
  return fetch(path, { ...opts, headers });
88
106
  }
89
107
 
90
- const inject = ["slots", "locale"];
108
+ const inject = ["slots", "locale", "betterSidebar"];
91
109
 
92
110
  const NS = "memory";
93
111
 
@@ -156,6 +174,11 @@ window.__ModuleLoader__.load({
156
174
  "memory.explorer.updated": "更新",
157
175
  "memory.explorer.tags": "标签",
158
176
  "memory.explorer.importance": "重要性",
177
+ "memory.explorer.sourceFilter": "来源",
178
+ "memory.explorer.filterDeposited": "沉淀",
179
+ "memory.explorer.filterDepositedHint": "只看 autoDream 巩固/更新过的记忆",
180
+ "memory.explorer.filterArchived": "已归档",
181
+ "memory.explorer.filterArchivedHint": "只看已归档的记忆(默认列表不含它们)",
159
182
  "memory.explorer.topK": "返回数量",
160
183
  "memory.explorer.topKOption": "返回 {n} 条",
161
184
  "memory.card.open": "在记忆库中查看全文",
@@ -311,6 +334,8 @@ window.__ModuleLoader__.load({
311
334
  "memory.explorer.detail.save": "保存",
312
335
  "memory.explorer.detail.cancel": "取消",
313
336
  "memory.explorer.detail.archive": "归档",
337
+ "memory.explorer.detail.restore": "恢复",
338
+ "memory.explorer.detail.restored": "已恢复到主列表",
314
339
  "memory.explorer.detail.archived": "已归档",
315
340
  "memory.explorer.detail.archiveFailed": "操作失败",
316
341
  "memory.explorer.detail.saved": "已保存,镜像同步更新",
@@ -337,6 +362,9 @@ window.__ModuleLoader__.load({
337
362
  "memory.status.consolidated": "沉淀的记忆",
338
363
  "memory.status.consolidatedEmpty": "autoDream / autoSummarize 沉淀的记忆会出现在这里",
339
364
  "memory.status.archivedMemories": "已归档的记忆",
365
+ "memory.status.viewAll": "查看全部",
366
+ "memory.status.depositedCount": "沉淀的记忆({n})",
367
+ "memory.status.archivedCount": "已归档的记忆({n})",
340
368
  "memory.status.archivedEmpty": "没有归档的记忆",
341
369
  "memory.status.restore": "恢复",
342
370
  "memory.settings.extapi.reveal": "显示",
@@ -420,6 +448,11 @@ window.__ModuleLoader__.load({
420
448
  "memory.explorer.updated": "Updated",
421
449
  "memory.explorer.tags": "Tags",
422
450
  "memory.explorer.importance": "Importance",
451
+ "memory.explorer.sourceFilter": "Source",
452
+ "memory.explorer.filterDeposited": "Deposited",
453
+ "memory.explorer.filterDepositedHint": "Only memories autoDream consolidated or updated",
454
+ "memory.explorer.filterArchived": "Archived",
455
+ "memory.explorer.filterArchivedHint": "Only archived memories (excluded from the default list)",
423
456
  "memory.explorer.topK": "Results limit",
424
457
  "memory.explorer.topKOption": "Return {n}",
425
458
  "memory.card.open": "Open full text in the memory library",
@@ -575,6 +608,8 @@ window.__ModuleLoader__.load({
575
608
  "memory.explorer.detail.save": "Save",
576
609
  "memory.explorer.detail.cancel": "Cancel",
577
610
  "memory.explorer.detail.archive": "Archive",
611
+ "memory.explorer.detail.restore": "Restore",
612
+ "memory.explorer.detail.restored": "Restored to the main list",
578
613
  "memory.explorer.detail.archived": "Archived",
579
614
  "memory.explorer.detail.archiveFailed": "Action failed",
580
615
  "memory.explorer.detail.saved": "Saved; mirrors re-rendered",
@@ -601,6 +636,9 @@ window.__ModuleLoader__.load({
601
636
  "memory.status.consolidated": "Deposited memories",
602
637
  "memory.status.consolidatedEmpty": "Memories deposited by autoDream / autoSummarize appear here",
603
638
  "memory.status.archivedMemories": "Archived memories",
639
+ "memory.status.viewAll": "View all",
640
+ "memory.status.depositedCount": "Deposited memories ({n})",
641
+ "memory.status.archivedCount": "Archived memories ({n})",
604
642
  "memory.status.archivedEmpty": "Nothing archived",
605
643
  "memory.status.restore": "Restore",
606
644
  "memory.settings.extapi.reveal": "Reveal",
@@ -668,6 +706,8 @@ window.__ModuleLoader__.load({
668
706
  // text-turns-brand-blue active states — no boxed panels).
669
707
  const CSS_TAG = "@modusensus/dsh-mneme/drawer.css";
670
708
  const css = [
709
+ // --- explorer shell: container queries let the toolbar adapt to the
710
+ // better-sidebar workbench pane (~500px) as well as the 1240px sheet ---
671
711
  // --- sidebar foot trigger (wide row / collapsed rail icon) ---
672
712
  ".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}",
673
713
  ".mneme-trigger:hover{background:var(--dsw-alias-interactive-bg-hover)}",
@@ -687,7 +727,11 @@ window.__ModuleLoader__.load({
687
727
  ".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}",
688
728
  ".mneme-entitychip:hover{background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 10%,transparent)}",
689
729
  // --- main-area memory library page ---
690
- ".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)}",
730
+ ".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);container-type:inline-size}",
731
+ // 窄容器(better-sidebar 工作台 ~500px)下中央 vtabs 会与右缘悬浮的
732
+ // 卡片/时间线切换器重叠:容器查询收窄时隐藏切换器,视图模式沿用
733
+ // localStorage 记忆,宽容器(sheet)不受影响。
734
+ "@container (max-width: 640px){.mneme-xtools .mneme-seg{display:none}}",
691
735
  ".mneme-xbar{position:relative;flex:none;display:flex;align-items:center;justify-content:center;gap:6px;border-bottom:1px solid var(--dsw-alias-border-l2);padding:0 16px;min-height:52px}",
692
736
  ".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)}",
693
737
  ".mneme-vtabs{display:flex;align-items:stretch;height:46px}",
@@ -695,7 +739,9 @@ window.__ModuleLoader__.load({
695
739
  ".mneme-vtab:hover{color:var(--dsw-alias-label-primary)}",
696
740
  ".mneme-vtab.mneme-active{color:var(--dsw-alias-state-business-primary)}",
697
741
  ".mneme-vtab.mneme-active::after{content:\"\";position:absolute;left:10px;right:10px;bottom:-1px;height:2.5px;border-radius:2px;background:var(--dsw-alias-state-business-primary)}",
698
- ".mneme-xtools{position:absolute;right:14px;top:50%;transform:translateY(-50%);display:flex;align-items:center;gap:8px;padding:0}",
742
+ // transform 会自建层叠上下文:层级必须给在容器上——3 高于吸顶月份头(2)
743
+ // 低于详情抽屉(6),下拉菜单/导入对话框随容器整体上浮。
744
+ ".mneme-xtools{position:absolute;right:14px;top:50%;transform:translateY(-50%);display:flex;align-items:center;gap:8px;padding:0;z-index:3}",
699
745
  ".mneme-xcount{flex:none;font-size:12px;line-height:16px;color:var(--dsw-alias-label-tertiary);white-space:nowrap}",
700
746
  // --- three-column browse layout: hairline separators, no outer box ---
701
747
  ".mneme-xmain{flex:1;min-height:0;display:flex;flex-direction:row;overflow:hidden}",
@@ -824,10 +870,13 @@ window.__ModuleLoader__.load({
824
870
  // --- 侧边栏顶部入口:借宿主「新会话」按钮的原生类名对齐 ---
825
871
  // wrapper display:contents 隐身,按钮成为侧边栏弹性布局的直接子元素;
826
872
  // 盒模型/间距/收起态 rail 几何全部继承宿主,我们只覆盖配色为次级观感。
873
+ ".mneme-stars{display:inline-flex;align-items:center;gap:2px}",
874
+ // width:100% 与「新会话」同宽(同一弹性父容器);文案对齐交给复制来的
875
+ // 原生类,不再用 flex:1/text-align:left 覆盖成左对齐。
827
876
  ".mneme-topentry{display:contents}",
828
- ".mneme-topentry-native{background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary)}",
877
+ ".mneme-topentry-native{width:100%;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary)}",
829
878
  ".mneme-topentry-native:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}",
830
- ".mneme-topentry-native .mneme-topentry-label{flex:1;min-width:0;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}",
879
+ ".mneme-topentry-native .mneme-topentry-label{white-space:nowrap}",
831
880
  // --- 功能开关:一行一开关,Claude 式安静排版 ---
832
881
  ".mneme-featgroup{flex:none;font-size:12px;font-weight:600;letter-spacing:.02em;color:var(--dsw-alias-label-tertiary);margin:16px 0 2px}",
833
882
  ".mneme-featrow{display:flex;align-items:center;gap:14px;padding:11px 2px;border-bottom:1px solid var(--dsw-alias-border-l1)}",
@@ -915,7 +964,7 @@ window.__ModuleLoader__.load({
915
964
  ".mneme-statusgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px;max-width:1000px;margin:0 auto}",
916
965
  ".mneme-statuscard{min-width:0;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:14px}",
917
966
  // --- 状态页工作台:让用户看见插件在干活(动态/沉淀/归档) ---
918
- ".mneme-wbhead{margin:28px auto 10px;font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);max-width:1000px}",
967
+ ".mneme-wbhead{margin:28px auto 10px;font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);max-width:1000px;display:flex;align-items:baseline;justify-content:space-between;gap:8px}",
919
968
  ".mneme-wblist{max-width:1000px;display:flex;flex-direction:column;gap:6px}",
920
969
  ".mneme-wbrow{display:flex;gap:10px;align-items:flex-start;padding:10px 14px;border:1px solid var(--dsw-alias-border-l1);border-radius:10px}",
921
970
  ".mneme-wbdot{flex:none;width:8px;height:8px;border-radius:50%;margin-top:6px;background:var(--dsw-alias-state-success,#2a9d6a)}",
@@ -2356,11 +2405,14 @@ window.__ModuleLoader__.load({
2356
2405
  return [];
2357
2406
  }
2358
2407
 
2359
- function WorkbenchSection({ t }) {
2408
+ function WorkbenchSection({ t, onBrowse }) {
2360
2409
  const [feed, setFeed] = useState(null); // audit rows(已过滤巩固/总结)
2361
- const [deposited, setDeposited] = useState(null); // ids 解析出的记忆
2362
- const [archived, setArchived] = useState(null);
2410
+ const [deposited, setDeposited] = useState(null); // 沉淀记忆首页(服务端 deposited 视图)
2411
+ const [depositedTotal, setDepositedTotal] = useState(0);
2412
+ const [archived, setArchived] = useState(null); // 归档记忆首页
2413
+ const [archivedTotal, setArchivedTotal] = useState(0);
2363
2414
  const [restoring, setRestoring] = useState("");
2415
+ const [wbReload, setWbReload] = useState(0);
2364
2416
 
2365
2417
  const load = useCallback(() => {
2366
2418
  let cancelled = false;
@@ -2368,30 +2420,31 @@ window.__ModuleLoader__.load({
2368
2420
  .then((r) => (r.ok ? r.json() : { items: [] }))
2369
2421
  .then((d) => {
2370
2422
  if (cancelled) return;
2371
- const rows = (d.items || []).filter((r) =>
2372
- r.trigger_source === "autoDream" || r.trigger_source === "autoSummarize");
2373
- setFeed(rows);
2374
- const ids = [];
2375
- for (const row of rows) {
2376
- for (const id of parseRelatedIds(row.related_memory_ids)) {
2377
- if (!ids.includes(id)) ids.push(id);
2378
- }
2379
- }
2380
- return apiFetch("/api/dsh-mneme/list?limit=300&order=chrono")
2381
- .then((r2) => (r2.ok ? r2.json() : { items: [] }))
2382
- .then((d2) => {
2383
- if (cancelled) return;
2384
- const byId = new Map((d2.items || []).map((m) => [m.id, m]));
2385
- setDeposited(ids.map((id) => byId.get(id)).filter(Boolean).slice(0, 8));
2386
- });
2423
+ setFeed((d.items || []).filter((r) =>
2424
+ r.trigger_source === "autoDream" || r.trigger_source === "autoSummarize"));
2387
2425
  })
2388
2426
  .catch(() => { if (!cancelled) setFeed([]); });
2389
- apiFetch("/api/dsh-mneme/list?archived=only&limit=50&order=chrono")
2390
- .then((r) => (r.ok ? r.json() : { items: [] }))
2391
- .then((d) => { if (!cancelled) setArchived(d.items || []); })
2427
+ // 仪表盘数据源:沉淀/归档各取一小页 + 服务端 total(/list 的 total
2428
+ // 与行同过滤)。长列表不再进状态页——「查看全部」跳记忆库的筛选
2429
+ // 视图,查询收敛到有搜索/分页的地方。
2430
+ apiFetch("/api/dsh-mneme/list?deposited=only&limit=8&order=chrono")
2431
+ .then((r) => (r.ok ? r.json() : { items: [], total: 0 }))
2432
+ .then((d) => {
2433
+ if (cancelled) return;
2434
+ setDeposited(d.items || []);
2435
+ setDepositedTotal(d.total || 0);
2436
+ })
2437
+ .catch(() => { if (!cancelled) setDeposited([]); });
2438
+ apiFetch("/api/dsh-mneme/list?archived=only&limit=3&order=chrono")
2439
+ .then((r) => (r.ok ? r.json() : { items: [], total: 0 }))
2440
+ .then((d) => {
2441
+ if (cancelled) return;
2442
+ setArchived(d.items || []);
2443
+ setArchivedTotal(d.total || 0);
2444
+ })
2392
2445
  .catch(() => { if (!cancelled) setArchived([]); });
2393
2446
  return () => { cancelled = true; };
2394
- }, []);
2447
+ }, [wbReload]);
2395
2448
  useEffect(() => { load(); }, [load]);
2396
2449
 
2397
2450
  const restore = (id) => {
@@ -2402,7 +2455,10 @@ window.__ModuleLoader__.load({
2402
2455
  body: JSON.stringify({ id, archived: false })
2403
2456
  })
2404
2457
  .then((r) => { if (!r.ok) throw new Error("http"); })
2405
- .then(() => setArchived((cur) => (cur || []).filter((m) => m.id !== id)))
2458
+ .then(() => {
2459
+ setArchived((cur) => (cur || []).filter((m) => m.id !== id));
2460
+ setArchivedTotal((n) => Math.max(0, n - 1));
2461
+ })
2406
2462
  .catch(() => {})
2407
2463
  .finally(() => setRestoring(""));
2408
2464
  };
@@ -2434,8 +2490,14 @@ window.__ModuleLoader__.load({
2434
2490
  row.error_message ? ` · ${row.error_message}` : "")
2435
2491
  ));
2436
2492
  })),
2437
- deposited !== null && deposited.length > 0 && h(react.Fragment, null,
2438
- h("div", { className: "mneme-wbhead" }, t("memory.status.consolidated")),
2493
+ deposited !== null && depositedTotal > 0 && h(react.Fragment, null,
2494
+ h("div", { className: "mneme-wbhead" },
2495
+ h("span", null, t("memory.status.depositedCount").replace("{n}", String(depositedTotal))),
2496
+ onBrowse && depositedTotal > deposited.length && h("button", {
2497
+ type: "button",
2498
+ className: "mneme-footbtn",
2499
+ onClick: () => onBrowse({ deposited: true })
2500
+ }, t("memory.status.viewAll"))),
2439
2501
  h("div", { className: "mneme-wblist" },
2440
2502
  deposited.map((m) => h("div", { className: "mneme-wbmemo", key: m.id },
2441
2503
  h("span", { className: "mneme-xdot", style: { color: memoryTypeColor(m.type) }, "aria-hidden": "true" }),
@@ -2445,7 +2507,13 @@ window.__ModuleLoader__.load({
2445
2507
  `${typeLabel(t, m.type)} · ${formatRelativeTime(m.updated_at || m.created_at, t)}`))
2446
2508
  )))),
2447
2509
  archived !== null && h(react.Fragment, null,
2448
- h("div", { className: "mneme-wbhead" }, t("memory.status.archivedMemories")),
2510
+ h("div", { className: "mneme-wbhead" },
2511
+ h("span", null, t("memory.status.archivedCount").replace("{n}", String(archivedTotal))),
2512
+ onBrowse && archivedTotal > archived.length && h("button", {
2513
+ type: "button",
2514
+ className: "mneme-footbtn",
2515
+ onClick: () => onBrowse({ archived: true })
2516
+ }, t("memory.status.viewAll"))),
2449
2517
  archived.length === 0
2450
2518
  ? h("div", { className: "mneme-featsubhint" }, t("memory.status.archivedEmpty"))
2451
2519
  : h("div", { className: "mneme-wblist" },
@@ -2465,7 +2533,7 @@ window.__ModuleLoader__.load({
2465
2533
  );
2466
2534
  }
2467
2535
 
2468
- function StatusPanel({ t }) {
2536
+ function StatusPanel({ t, onBrowse }) {
2469
2537
  return h("div", { className: "mneme-status" },
2470
2538
  h("div", { className: "mneme-statusgrid" },
2471
2539
  h(MemoriesStatusCard, { t }),
@@ -2474,7 +2542,7 @@ window.__ModuleLoader__.load({
2474
2542
  h(LlmStatusCard, { t }),
2475
2543
  h(DreamStatusCards, { t })
2476
2544
  ),
2477
- h(WorkbenchSection, { t })
2545
+ h(WorkbenchSection, { t, onBrowse })
2478
2546
  );
2479
2547
  }
2480
2548
 
@@ -2560,7 +2628,7 @@ window.__ModuleLoader__.load({
2560
2628
  value: importance,
2561
2629
  onChange: (e) => setImportance(Number(e.target.value))
2562
2630
  }, [1, 2, 3, 4, 5].map((n) => h("option", { key: n, value: n }, "★".repeat(n))))
2563
- : h("span", { className: "mneme-dmetaval" }, "★".repeat(Math.min(5, Math.max(0, memory.importance || 0)))),
2631
+ : h(ImportanceStars, { className: "mneme-dmetaval", value: memory.importance || 0 }),
2564
2632
  memory.source && h(react.Fragment, null,
2565
2633
  h("span", { className: "mneme-dmetakey" }, t("memory.explorer.source")),
2566
2634
  h("span", { className: "mneme-dmetaval", title: memory.source }, memory.source)),
@@ -2610,8 +2678,11 @@ window.__ModuleLoader__.load({
2610
2678
  )
2611
2679
  : h(react.Fragment, null,
2612
2680
  h("button", { type: "button", className: "mneme-btn", onClick: () => setEditing(true) }, t("memory.explorer.detail.edit")),
2613
- h("button", { type: "button", className: "mneme-btn", disabled: busy, onClick: () => postUpdate({ archived: true }, { archived: true }) },
2614
- t("memory.explorer.detail.archive")),
2681
+ memory.archived
2682
+ ? h("button", { type: "button", className: "mneme-btn", disabled: busy, onClick: () => postUpdate({ archived: false }, { restored: true }) },
2683
+ t("memory.explorer.detail.restore"))
2684
+ : h("button", { type: "button", className: "mneme-btn", disabled: busy, onClick: () => postUpdate({ archived: true }, { archived: true }) },
2685
+ t("memory.explorer.detail.archive")),
2615
2686
  h("button", {
2616
2687
  type: "button",
2617
2688
  className: "mneme-btn",
@@ -2724,6 +2795,9 @@ window.__ModuleLoader__.load({
2724
2795
  catch { return "timeline"; }
2725
2796
  });
2726
2797
  const [dateRange, setDateRange] = useState("all"); // all | 7d | 30d | 90d
2798
+ // 方案 A:沉淀/归档筛选 chip——状态页「查看全部」也会带着它们跳转过来。
2799
+ const [depositedOnly, setDepositedOnly] = useState(false); // 只看 autoDream 巩固
2800
+ const [archivedOnly, setArchivedOnly] = useState(false); // 只看已归档
2727
2801
  const [pendingIds, setPendingIds] = useState(() => new Set());
2728
2802
  const [menuOpen, setMenuOpen] = useState(false);
2729
2803
  const [importOpen, setImportOpen] = useState(false);
@@ -2762,7 +2836,9 @@ window.__ModuleLoader__.load({
2762
2836
  ).toISOString();
2763
2837
  const filterQS = (type === "all" ? "" : `&type=${encodeURIComponent(type)}`)
2764
2838
  + (minImp ? `&minImportance=${minImp}` : "")
2765
- + (dateFromIso ? `&updatedFrom=${encodeURIComponent(dateFromIso)}` : "");
2839
+ + (dateFromIso ? `&updatedFrom=${encodeURIComponent(dateFromIso)}` : "")
2840
+ + (depositedOnly ? "&deposited=only" : "")
2841
+ + (archivedOnly ? "&archived=only" : "");
2766
2842
  const listUrl = (offset) => `/api/dsh-mneme/list?limit=${PAGE_SIZE}&offset=${offset}${filterQS}&order=chrono`;
2767
2843
 
2768
2844
  // Browse = paged, pure-chronological pages (order=chrono). The default
@@ -2948,7 +3024,7 @@ window.__ModuleLoader__.load({
2948
3024
 
2949
3025
  // 编辑保存 / 归档的本地落账:替换或移除对应行;镜像由后端重渲染。
2950
3026
  // 归档从所有视图移除(列表默认只看未归档),并清空选中。
2951
- const applyMemoryUpdate = (updated, { archived = false } = {}) => {
3027
+ const applyMemoryUpdate = (updated, { archived = false, restored = false } = {}) => {
2952
3028
  if (archived) {
2953
3029
  setItems((cur) => cur.filter((m) => m.id !== updated.id));
2954
3030
  setRemoteItems((cur) => (cur ? cur.filter((m) => m.id !== updated.id) : cur));
@@ -2958,6 +3034,17 @@ window.__ModuleLoader__.load({
2958
3034
  setTimeout(() => setToastMsg(""), 2500);
2959
3035
  return;
2960
3036
  }
3037
+ if (restored) {
3038
+ // 恢复:行离开当前视图(抽屉只能从归档浏览列表打开它),回主列表
3039
+ // 由下一次筛选切换/刷新自然带出。
3040
+ setItems((cur) => cur.filter((m) => m.id !== updated.id));
3041
+ setRemoteItems((cur) => (cur ? cur.filter((m) => m.id !== updated.id) : cur));
3042
+ setTotal((n) => Math.max(0, n - 1));
3043
+ setSelectedId(null);
3044
+ setToastMsg(t("memory.explorer.detail.restored"));
3045
+ setTimeout(() => setToastMsg(""), 2500);
3046
+ return;
3047
+ }
2961
3048
  const replace = (m) => (m.id === updated.id ? updated : m);
2962
3049
  setItems((cur) => cur.map(replace));
2963
3050
  setRemoteItems((cur) => (cur ? cur.map(replace) : cur));
@@ -3003,6 +3090,16 @@ window.__ModuleLoader__.load({
3003
3090
  setView("entity");
3004
3091
  };
3005
3092
 
3093
+ // 状态页「查看全部」入口:跳回记忆库并预置对应筛选(清搜索态,避免
3094
+ // remoteItems 盖过浏览列表)。
3095
+ const browseWithFilter = (patch) => {
3096
+ setDepositedOnly(!!patch.deposited);
3097
+ setArchivedOnly(!!patch.archived);
3098
+ setQuery("");
3099
+ setRemoteItems(null);
3100
+ setView("memory");
3101
+ };
3102
+
3006
3103
  const subviews = [
3007
3104
  { key: "memory", label: t("memory.explorer.tabMemory") },
3008
3105
  { key: "entity", label: t("memory.explorer.tabEntities") },
@@ -3121,6 +3218,19 @@ window.__ModuleLoader__.load({
3121
3218
  onClick: () => setMinImp(v)
3122
3219
  }, v === 0 ? t("memory.tab.all") : `★${v}+`))
3123
3220
  ),
3221
+ h("div", { className: "mneme-xcolhead" }, t("memory.explorer.sourceFilter")),
3222
+ h("div", { className: "mneme-xrow" },
3223
+ h("button", {
3224
+ className: depositedOnly ? "mneme-chip mneme-active" : "mneme-chip",
3225
+ title: t("memory.explorer.filterDepositedHint"),
3226
+ onClick: () => setDepositedOnly(!depositedOnly)
3227
+ }, t("memory.explorer.filterDeposited")),
3228
+ h("button", {
3229
+ className: archivedOnly ? "mneme-chip mneme-active" : "mneme-chip",
3230
+ title: t("memory.explorer.filterArchivedHint"),
3231
+ onClick: () => setArchivedOnly(!archivedOnly)
3232
+ }, t("memory.explorer.filterArchived"))
3233
+ ),
3124
3234
  h("div", { className: "mneme-xcolhead" }, t("memory.explorer.types")),
3125
3235
  h("button", {
3126
3236
  className: type === "all" ? "mneme-xtype mneme-active" : "mneme-xtype",
@@ -3160,7 +3270,7 @@ window.__ModuleLoader__.load({
3160
3270
  h("div", { className: "mneme-cardtitle" }, m.title || (m.content || "").slice(0, 60)),
3161
3271
  h("div", { className: "mneme-cardexcerpt" }, m.content || ""),
3162
3272
  h("div", { className: "mneme-cardfoot" },
3163
- h("span", null, "★".repeat(Math.min(5, Math.max(0, m.importance || 0)))),
3273
+ h(ImportanceStars, { value: m.importance || 0, size: 12 }),
3164
3274
  m.source && h("span", { className: "mneme-cardsrc", title: m.source }, m.source)
3165
3275
  )
3166
3276
  )),
@@ -3253,7 +3363,7 @@ window.__ModuleLoader__.load({
3253
3363
  })
3254
3364
  ),
3255
3365
  view === "entity" && h(EntityPanel, { t, focusEntity: graphFocus, onJumpMemory: jumpToMemory }),
3256
- view === "status" && h(StatusPanel, { t }),
3366
+ view === "status" && h(StatusPanel, { t, onBrowse: browseWithFilter }),
3257
3367
  view === "settings" && h("div", { className: "mneme-set" },
3258
3368
  h("div", { className: "mneme-set-inner" }, h(SettingsContent, { t }))
3259
3369
  ),
@@ -3287,19 +3397,34 @@ window.__ModuleLoader__.load({
3287
3397
  const [nativeCls, setNativeCls] = useState("");
3288
3398
  useEffect(() => {
3289
3399
  if (!reactDom || typeof document === "undefined") return undefined;
3290
- let tries = 0, timer = null, created = null;
3400
+ let tries = 0, timer = null, created = null, mo = null;
3401
+ const findNative = (region) =>
3402
+ region.parentElement.querySelector('[class*="newSession"]')
3403
+ || region.previousElementSibling;
3291
3404
  const attempt = () => {
3292
3405
  const region = document.querySelector('[class*="regionArea"]');
3293
3406
  if (region && region.parentElement) {
3294
3407
  // 「新会话」按钮与 regionArea 同级且紧邻其前,借它的类名获得
3295
3408
  // 与原生侧边栏项完全一致的盒模型(含收起态 rail 几何)。
3296
- const ns = region.parentElement.querySelector('[class*="newSession"]')
3297
- || region.previousElementSibling;
3409
+ // 宿主/皮肤会异步改写按钮类名,用 MutationObserver 持续同步,
3410
+ // 否则 portal 按钮会停留在捕获时刻的旧类上(宽度/对齐失配)。
3298
3411
  created = document.createElement("div");
3299
3412
  created.dataset.pluginEntry = "@modusensus/dsh-mneme";
3300
3413
  region.parentElement.insertBefore(created, region);
3301
3414
  setHost(created);
3302
- setNativeCls(ns?.className || "");
3415
+ setNativeCls(findNative(region)?.className || "");
3416
+ mo = new MutationObserver(() => {
3417
+ const cur = document.querySelector('[class*="regionArea"]');
3418
+ if (!cur || !cur.parentElement) return;
3419
+ const cls = findNative(cur)?.className || "";
3420
+ setNativeCls((prev) => (prev === cls ? prev : cls));
3421
+ });
3422
+ mo.observe(region.parentElement, {
3423
+ attributes: true,
3424
+ attributeFilter: ["class"],
3425
+ childList: true,
3426
+ subtree: true
3427
+ });
3303
3428
  return;
3304
3429
  }
3305
3430
  if (++tries > 40) return; // 放弃 portal,footer 回退保持可用
@@ -3308,6 +3433,7 @@ window.__ModuleLoader__.load({
3308
3433
  attempt();
3309
3434
  return () => {
3310
3435
  clearTimeout(timer);
3436
+ if (mo) mo.disconnect();
3311
3437
  if (created && created.parentElement) created.parentElement.removeChild(created);
3312
3438
  };
3313
3439
  }, []);
@@ -3330,6 +3456,7 @@ window.__ModuleLoader__.load({
3330
3456
  }
3331
3457
 
3332
3458
  function apply(ctx) {
3459
+ const t = ctx.locale.bind(NS);
3333
3460
  ctx.effect(() => ctx.locale.register(NS, dictionaries), "dsh-mneme: dictionaries");
3334
3461
 
3335
3462
  // 记忆库唯一入口:sidebar.footer.action 注册作为锚点与回退;真实按钮
@@ -3338,7 +3465,6 @@ window.__ModuleLoader__.load({
3338
3465
  // v0.7.15 起不再注册 conversation.view tab——对话内嵌的面板被悬浮
3339
3466
  // 输入框遮挡、挤压会话布局,sheet 页是更舒服的承载方式。
3340
3467
  ctx.slots.inject("sidebar.footer.action", () => {
3341
- const t = ctx.locale.bind(NS);
3342
3468
  return ctx.slots.register({
3343
3469
  name: "sidebar.footer.action",
3344
3470
  id: "dsh-mneme",
@@ -3353,6 +3479,40 @@ window.__ModuleLoader__.load({
3353
3479
  h(MemoryOverlay, { t })
3354
3480
  ));
3355
3481
  });
3482
+
3483
+ // --- better-sidebar 生态 tab(可选软依赖,官方指南 §2.2 契约)---
3484
+ // 'betterSidebar' 声明进 inject + package.json optional peer:未安装
3485
+ // better-sidebar 时服务解析为 undefined,探测条件不成立即静默跳过,
3486
+ // 现有 footer 锚点 + 顶部入口 + sheet 完全不受影响(实测:DSH 运行时
3487
+ // 对 ctx 属性访问有 inject 闸门,未声明就取值会直接 fail 整个 loader
3488
+ // entry,§15 的「不声明纯探测」模式在 DSH 上不可用)。
3489
+ // 服务提供时序仍无保证(better-sidebar 可能晚于本插件加载),短重试
3490
+ // 兜底;注册包在 ctx.effect 里,HMR/禁用时随作用域自动清理。
3491
+ ctx.effect(() => {
3492
+ let tries = 0, timer = null;
3493
+ const attempt = () => {
3494
+ const bs = ctx.betterSidebar;
3495
+ if (bs && typeof bs.registerTab === "function") {
3496
+ try {
3497
+ bs.registerTab({
3498
+ id: "dsh-mneme:memory",
3499
+ title: () => t("memory.view.label"),
3500
+ icon: (size) => h(IconArchiveOutline20, { size }),
3501
+ order: 60,
3502
+ component: () => h(MemoryExplorer, { t })
3503
+ });
3504
+ return;
3505
+ } catch (err) {
3506
+ // id 重复等注册失败不应拖垮其余功能,留一条线索即可
3507
+ console.error("[dsh-mneme] better-sidebar registerTab failed:", err);
3508
+ return;
3509
+ }
3510
+ }
3511
+ if (++tries <= 10) timer = setTimeout(attempt, 1000);
3512
+ };
3513
+ attempt();
3514
+ return () => clearTimeout(timer);
3515
+ }, "dsh-mneme: better-sidebar tab");
3356
3516
  }
3357
3517
 
3358
3518
  exports.apply = apply;
package/lib/store.js CHANGED
@@ -637,7 +637,7 @@ export function createStore(path) {
637
637
  return ts;
638
638
  }
639
639
 
640
- function count(type, { minImportance = null, source = null, includeForgotten = false, includeArchived = false, onlyArchived = false, updatedFrom = null, updatedTo = null } = {}) {
640
+ function count(type, { minImportance = null, source = null, includeForgotten = false, includeArchived = false, onlyArchived = false, depositedOnly = false, updatedFrom = null, updatedTo = null } = {}) {
641
641
  const clauses = [];
642
642
  const params = [];
643
643
  if (type !== undefined) {
@@ -673,6 +673,12 @@ export function createStore(path) {
673
673
  } else if (!includeArchived) {
674
674
  clauses.push("archived = 0");
675
675
  }
676
+ // 与 list() 同过滤:deposited 视图的 total 才能和行保持一致。
677
+ if (depositedOnly) {
678
+ clauses.push(
679
+ "(id IN (SELECT record_id FROM receipt_chain WHERE kind IN ('merge', 'update') AND verdict = 'live') OR source = 'dream')"
680
+ );
681
+ }
676
682
  const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
677
683
  return db.prepare(`SELECT count(*) AS c FROM memories ${where}`).get(...params).c;
678
684
  }
@@ -924,7 +930,7 @@ export function createStore(path) {
924
930
  return rows.map(toRow);
925
931
  }
926
932
 
927
- function list({ type, limit = 50, offset = 0, order = "importance", includeForgotten = false, includeArchived = false, onlyArchived = false, minImportance = null, source = null, updatedFrom = null, updatedTo = null } = {}) {
933
+ function list({ type, limit = 50, offset = 0, order = "importance", includeForgotten = false, includeArchived = false, onlyArchived = false, depositedOnly = false, minImportance = null, source = null, updatedFrom = null, updatedTo = null } = {}) {
928
934
  const clauses = [];
929
935
  const params = [];
930
936
  if (type) {
@@ -962,6 +968,14 @@ export function createStore(path) {
962
968
  } else if (!includeArchived) {
963
969
  clauses.push("archived = 0");
964
970
  }
971
+ // depositedOnly:只看 autoDream 巩固过的记忆——receipt_chain 的 merge /
972
+ // update live verdict(record_id 即保留/更新目标)∪ source="dream" 的
973
+ // 直写沉淀(记忆库总览)。conflict 不算沉淀:两侧只被仲裁,内容未落。
974
+ if (depositedOnly) {
975
+ clauses.push(
976
+ "(id IN (SELECT record_id FROM receipt_chain WHERE kind IN ('merge', 'update') AND verdict = 'live') OR source = 'dream')"
977
+ );
978
+ }
965
979
  const { limit: lim, offset: off } = sanitizePage(limit, offset, 50);
966
980
  const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
967
981
  // "chrono" is pure newest-first — the stable order paged browsing (month
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@modusensus/dsh-mneme",
3
3
  "description": "Cross-session memory plugin for DeepSeek Harness with autoDream consolidation: SQLite store, Markdown mirrors, 7 model tools, automatic injection, session summarization, user profile/rules, custom slash commands, vector (semantic) search, and a Web GUI panel",
4
- "version": "0.7.16",
4
+ "version": "0.7.18",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -39,7 +39,8 @@
39
39
  "slots",
40
40
  "locale",
41
41
  "layout",
42
- "connection"
42
+ "connection",
43
+ "betterSidebar"
43
44
  ],
44
45
  "platform": "web"
45
46
  },
@@ -53,7 +54,13 @@
53
54
  "@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
54
55
  "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.6",
55
56
  "@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
56
- "@deepseek-ai/schemastery": "^3.18.1"
57
+ "@deepseek-ai/schemastery": "^3.18.1",
58
+ "dsh-better-sidebar": "*"
59
+ },
60
+ "peerDependenciesMeta": {
61
+ "dsh-better-sidebar": {
62
+ "optional": true
63
+ }
57
64
  },
58
65
  "devDependencies": {
59
66
  "@deepseek-ai/cordis": "^4.0.1",
package/src/api.js CHANGED
@@ -178,7 +178,11 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
178
178
  // archived=only:只看归档(状态页的归档列表用)。归档行不进默认列表,
179
179
  // 所以这是独立的视图开关,而不是 includeArchived 的混看模式。
180
180
  const onlyArchived = url.searchParams.get("archived") === "only";
181
- const rows = service.list({ type, limit, offset, order, minImportance, source, updatedFrom, updatedTo, onlyArchived });
181
+ // deposited=only:只看 autoDream 巩固过的记忆(receipt_chain
182
+ // merge/update verdict ∪ source=dream 直写)——状态页「查看全部」
183
+ // 与记忆库的「沉淀」筛选 chip 共用这个视图。
184
+ const depositedOnly = url.searchParams.get("deposited") === "only";
185
+ const rows = service.list({ type, limit, offset, order, minImportance, source, updatedFrom, updatedTo, onlyArchived, depositedOnly });
182
186
  // 面板行在 wire DTO 之上补 archived/quality_score——模型工具的输出
183
187
  // schema 严格复用 toApiList,扩展只发生在 HTTP 层。
184
188
  const items = service.toApiList(rows).map((m, i) => ({
@@ -189,7 +193,7 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
189
193
  // Total honors the same filters as the rows, or the pager's
190
194
  // has-more math breaks whenever minImportance/source/updated-at
191
195
  // bounds are active.
192
- sendJson(res, 200, { items, total: service.count(type, { minImportance, source, updatedFrom, updatedTo, onlyArchived }) });
196
+ sendJson(res, 200, { items, total: service.count(type, { minImportance, source, updatedFrom, updatedTo, onlyArchived, depositedOnly }) });
193
197
  } catch {
194
198
  sendJson(res, 500, { error: "internal" });
195
199
  }