sage-mem 2.4.3 → 2.5.1

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
@@ -2396,6 +2396,10 @@ const StarmapRuntime = (function (require, _react) {
2396
2396
 
2397
2397
  const CSS = [
2398
2398
  ".smem-root{display:flex;flex-direction:column;gap:12px;font-size:13px;line-height:1.55;color:var(--dsw-alias-label-primary);max-width:760px;}",
2399
+ // 2.5.0:一屏两张卡(自动整理 = 做梦 + 归档;库健康 = 冲突 + 体检)。
2400
+ // `.smem-root` 本身是 gap 12 的列,但这两张卡中间有标题层级差,间距给大一点。
2401
+ ".smem-stack{display:flex;flex-direction:column;gap:18px;}",
2402
+ ".smem-stack > .smem-stack-sep{height:1px;background:var(--dsw-alias-border-l2);}",
2399
2403
  ".smem-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}",
2400
2404
  /**
2401
2405
  * 头部带里的「弹簧」:吃掉本行的剩余空间,把后面的主操作顶到右边。
@@ -2453,7 +2457,6 @@ const StarmapRuntime = (function (require, _react) {
2453
2457
  ".smem-ar-hint{font-size:11.5px;color:var(--dsw-alias-label-secondary);}",
2454
2458
  ".smem-ar-meta{font-size:11.5px;color:var(--dsw-alias-label-secondary);white-space:nowrap;}",
2455
2459
  ".smem-ar-reason{font-size:12px;color:var(--dsw-alias-label-secondary);border-left:2px solid var(--dsw-alias-border-l2);padding-left:8px;white-space:pre-wrap;overflow-wrap:anywhere;}",
2456
- ".smem-ar-confirm{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:8px 10px;background:var(--dsw-alias-bg-layer-2);display:flex;flex-direction:column;gap:6px;font-size:12px;line-height:1.55;}",
2457
2460
  ".smem-ar-banner{font-size:12px;color:var(--dsw-alias-label-secondary);border:1px solid var(--dsw-alias-border-l1);border-radius:8px;padding:6px 10px;}",
2458
2461
  // ── 工具行 + 工具面板(体检 / 归档候选 / 保留名原样编辑)──
2459
2462
  // 三个新入口收在一行**轻量**文字按钮里:不跟主工具栏抢重量级,也不各占一块版面。
@@ -2525,7 +2528,6 @@ const StarmapRuntime = (function (require, _react) {
2525
2528
  ".smem-cand-main{display:flex;align-items:center;gap:8px;flex-wrap:wrap;cursor:pointer;}",
2526
2529
  ".smem-cand-reason{font-size:12px;color:var(--dsw-alias-label-secondary);border-left:2px solid var(--dsw-alias-border-l2);padding-left:8px;overflow-wrap:anywhere;}",
2527
2530
  ".smem-cand-act{display:flex;justify-content:flex-end;}",
2528
- ".smem-cand-confirm{display:flex;align-items:center;gap:8px;flex-wrap:wrap;font-size:12px;border-top:1px dashed var(--dsw-alias-border-l2);padding-top:6px;}",
2529
2531
  ".smem-raw-names{display:flex;gap:6px;flex-wrap:wrap;}",
2530
2532
  // 自动做梦的二级分组:导航用**下划线**(跟筛选 chip、动作按钮区分开,三种层级三种形状)。
2531
2533
  ".smem-subtabs{display:flex;gap:16px;border-bottom:1px solid var(--dsw-alias-border-l2);margin:2px 0 8px;}",
@@ -2538,8 +2540,32 @@ const StarmapRuntime = (function (require, _react) {
2538
2540
  ".smem-ad-field{display:flex;align-items:center;gap:8px;min-width:0;}",
2539
2541
  ".smem-ad-field .smem-autodream-key{min-width:96px;}",
2540
2542
  ".smem-actions{display:flex;gap:6px;}",
2541
- ".smem-form{border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:12px;background:var(--dsw-alias-bg-layer-2);display:flex;flex-direction:column;gap:8px;}",
2542
- ".smem-form-title{font-weight:600;font-size:13px;}",
2543
+ // ── 模态确认框(2.5.0,D39):替掉全部就地确认条 ──
2544
+ // 遮罩压暗全屏并**吃掉**背景的点击;卡片居中,键盘焦点被 Tab 圈不住时
2545
+ // 至少还有 Esc / 点遮罩 / 取消三条出口(Esc 由调用方的 effect 挂)。
2546
+ ".smem-modal-host{position:fixed;inset:0;z-index:50;display:flex;align-items:center;justify-content:center;padding:16px;}",
2547
+ ".smem-modal-scrim{position:absolute;inset:0;background:rgba(0,0,0,.5);animation:smem-fade 220ms cubic-bezier(.2,0,0,1);}",
2548
+ ".smem-modal{position:relative;width:min(460px,92vw);max-height:calc(100vh - 48px);overflow:auto;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);border-radius:12px;padding:16px;box-shadow:0 24px 60px rgba(0,0,0,.34);animation:smem-pop 220ms cubic-bezier(.2,0,0,1);}",
2549
+ "@keyframes smem-pop{from{opacity:0;transform:translateY(6px) scale(.985);}to{opacity:1;transform:none;}}",
2550
+ ".smem-modal-title{font-weight:600;font-size:13.5px;margin-bottom:8px;}",
2551
+ ".smem-modal-body{font-size:12.5px;line-height:1.65;color:var(--dsw-alias-label-secondary);display:flex;flex-direction:column;gap:8px;}",
2552
+ ".smem-modal-foot{display:flex;gap:8px;justify-content:flex-end;margin-top:14px;}",
2553
+ "@media (prefers-reduced-motion:reduce){.smem-modal,.smem-modal-scrim{animation:none;}}",
2554
+ // ── 编辑抽屉(2.5.0,D39):替掉 1.0 起的页面内联表单 `.smem-form` ──
2555
+ // 抽屉从右侧滑入,遮罩自带淡入。两处都用 `animation` 而不是 transition:mount 即播,
2556
+ // 不依赖二次渲染(页面不可见时 rAF 不触发)。
2557
+ ".smem-drawer-host{position:fixed;inset:0;z-index:40;display:flex;justify-content:flex-end;}",
2558
+ ".smem-drawer-scrim{position:absolute;inset:0;background:rgba(0,0,0,.42);animation:smem-fade 220ms cubic-bezier(.2,0,0,1);}",
2559
+ "@keyframes smem-fade{from{opacity:0;}to{opacity:1;}}",
2560
+ "@keyframes smem-slide{from{transform:translateX(100%);}to{transform:none;}}",
2561
+ ".smem-drawer-panel{position:relative;display:flex;flex-direction:column;width:min(560px,94vw);height:100%;background:var(--dsw-alias-bg-layer-1);border-left:1px solid var(--dsw-alias-border-l2);box-shadow:-18px 0 42px rgba(0,0,0,.28);animation:smem-slide 220ms cubic-bezier(.2,0,0,1);}",
2562
+ ".smem-drawer-head{display:flex;align-items:flex-start;gap:10px;padding:14px 16px;border-bottom:1px solid var(--dsw-alias-border-l2);}",
2563
+ ".smem-drawer-title{font-weight:600;font-size:13.5px;}",
2564
+ ".smem-drawer-x{margin-left:auto;flex:none;width:26px;height:26px;border-radius:8px;border:1px solid var(--dsw-alias-border-l1);background:transparent;color:var(--dsw-alias-label-secondary);font-size:15px;line-height:1;font-family:inherit;cursor:pointer;}",
2565
+ ".smem-drawer-body{flex:1;overflow:auto;padding:14px 16px;display:flex;flex-direction:column;gap:8px;}",
2566
+ ".smem-drawer-foot{display:flex;gap:8px;justify-content:flex-end;padding:12px 16px;border-top:1px solid var(--dsw-alias-border-l2);}",
2567
+ ".smem-drawer-ta{font-family:inherit;min-height:220px;resize:vertical;}",
2568
+ "@media (prefers-reduced-motion:reduce){.smem-drawer-scrim,.smem-drawer-panel{animation:none;}}",
2543
2569
  ".smem-label{font-size:12px;color:var(--dsw-alias-label-secondary);}",
2544
2570
  ".smem-input,.smem-textarea{width:100%;box-sizing:border-box;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;background:var(--dsw-alias-bg-base);color:inherit;padding:6px 10px;font-size:13px;outline:none;font-family:inherit;}",
2545
2571
  ".smem-input:focus,.smem-textarea:focus{border-color:var(--dsw-alias-brand-primary,#4d76e6);}",
@@ -2549,6 +2575,19 @@ const StarmapRuntime = (function (require, _react) {
2549
2575
  ".smem-chip:hover{border-color:var(--dsw-alias-border-l2);}",
2550
2576
  '.smem-chip[data-on="1"]{border-color:var(--dsw-alias-brand-primary,#4d76e6);color:var(--dsw-alias-brand-primary,#4d76e6);}',
2551
2577
  ".smem-chips{display:flex;gap:6px;flex-wrap:wrap;}",
2578
+ // ── 模块卡头(2.5.0,D39)────────────────────────────────────────────
2579
+ // 一屏两张模块卡,就必须看得出「这张卡到哪儿为止」——
2580
+ // 否则下面那张会被读成「当前这一段的内容」(2026-11-08 文歌子一眼看到的正是这个)。
2581
+ // 所以每张卡自己带边框;卡头(开关 + 标题 + 状态)在分段之外,底下一条线把它和内容分开。
2582
+ ".smem-module-card{border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-layer-1);padding:14px 16px;}",
2583
+ ".smem-module-head{padding-bottom:10px;border-bottom:1px solid var(--dsw-alias-border-l1);margin-bottom:12px;}",
2584
+ ".smem-module-card .smem-modcard-head{padding-bottom:10px;border-bottom:1px solid var(--dsw-alias-border-l1);margin-bottom:12px;}",
2585
+ ".smem-module-card .smem-subtabs{margin-bottom:12px;}",
2586
+ ".smem-modcard-head{display:flex;align-items:center;gap:8px;margin-bottom:8px;}",
2587
+ ".smem-modcard-title{font-weight:600;font-size:13px;}",
2588
+ ".smem-modcard-head .smem-modcard-status{margin-left:auto;color:var(--dsw-alias-label-tertiary);font-size:12px;}",
2589
+ // 关掉时的说明条:只解释「运行被停了」,不暗示设置被锁。
2590
+ ".smem-modcard-off{margin:6px 0 10px;padding:7px 10px;border-radius:8px;font-size:12px;line-height:1.6;color:var(--dsw-alias-state-warn-primary,#d97706);background:var(--dsw-alias-bg-layer-2);}",
2552
2591
  ".smem-err{border:1px solid var(--dsw-alias-state-error-primary,#ef4444);color:var(--dsw-alias-state-error-primary,#ef4444);border-radius:8px;padding:6px 10px;font-size:12px;white-space:pre-wrap;overflow-wrap:anywhere;}",
2553
2592
  ".smem-ok{color:var(--dsw-alias-state-success-primary,#16a34a);font-size:12px;}",
2554
2593
  ".smem-empty{border:1px dashed var(--dsw-alias-border-l2);border-radius:10px;padding:22px 14px;text-align:center;color:var(--dsw-alias-label-secondary);font-size:12.5px;}",
@@ -2591,7 +2630,6 @@ const StarmapRuntime = (function (require, _react) {
2591
2630
  ".smem-ad-badge{display:inline-flex;align-items:center;font-size:11px;color:var(--dsw-alias-label-secondary);border:1px solid var(--dsw-alias-border-l1);border-radius:6px;padding:0 5px;white-space:nowrap;}",
2592
2631
  ".smem-ad-badge--warn{color:var(--dsw-alias-state-warn-primary,#d97706);border-color:var(--dsw-alias-state-warn-primary,#d97706);}",
2593
2632
  ".smem-ad-badge--legacy{opacity:.78;}",
2594
- ".smem-ad-confirm{border:1px solid var(--dsw-alias-state-error-primary,#ef4444);border-radius:8px;padding:7px 9px;font-size:12px;line-height:1.55;display:flex;flex-direction:column;gap:6px;overflow-wrap:anywhere;}",
2595
2633
  ".smem-ad-rb{font-size:12px;line-height:1.5;overflow-wrap:anywhere;}",
2596
2634
  ".smem-ad-run{display:flex;align-items:center;gap:8px;flex-wrap:wrap;width:100%;box-sizing:border-box;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;padding:6px 9px;background:transparent;color:inherit;text-align:left;font-family:inherit;font-size:12px;cursor:pointer;}",
2597
2635
  ".smem-ad-run:hover{border-color:var(--dsw-alias-border-l2);}",
@@ -2618,6 +2656,16 @@ const StarmapRuntime = (function (require, _react) {
2618
2656
  ".smem-skeleton-row{height:44px;border-radius:8px;background:var(--dsw-alias-bg-layer-2);opacity:.55;}",
2619
2657
  ".smem-empty{display:flex;flex-direction:column;align-items:flex-start;gap:8px;}",
2620
2658
  ".smem-tabs-sep{width:1px;align-self:stretch;margin:6px 2px;background:var(--dsw-alias-border-l2);}",
2659
+ // ── 切页签的进场动画(2.5.0,D39)──────────────────────────────────────
2660
+ // 只在**换页签**时播一次:`.smem-view` 带 key={tab},换页签=换 key=换 DOM 节点,
2661
+ // CSS 动画自然重播;同一页签内的任何 setState 都复用同一个节点,不会重播。
2662
+ // 所以这里不需要「记住上一个 tab」的 JS 状态 —— 让 DOM 节点身份来回答这个问题。
2663
+ // 首次打开也播一次:在用户看来那就是切了一页(D39)。
2664
+ "@keyframes smem-enter{from{opacity:0;transform:translateY(4px);}to{opacity:1;transform:none;}}",
2665
+ "@keyframes smem-under{from{transform:scaleX(.35);opacity:0;}to{transform:scaleX(1);opacity:1;}}",
2666
+ ".smem-view{animation:smem-enter 220ms cubic-bezier(.2,0,0,1);}",
2667
+ ".smem-tab--on:after{animation:smem-under 220ms cubic-bezier(.2,0,0,1);}",
2668
+ "@media (prefers-reduced-motion:reduce){.smem-view{animation:none;}.smem-tab--on:after{animation:none;}}",
2621
2669
  // 窄窗口(≤420px):头部带与筛选条各让一行,卡片动作允许换行、分隔符隐藏。
2622
2670
  "@media (max-width:420px){.smem-root{gap:10px;}.smem-head{gap:6px;}.smem-head-actions{flex-wrap:wrap;}.smem-card-title{min-width:0;word-break:break-word;}.smem-actions{flex-wrap:wrap;}.smem-actions-sep{display:none;}.smem-num{width:56px;}.smem-search{min-width:100px;}}",
2623
2671
  // ── 1.8.0:侧栏入口(设置开关 + 中央主区整页)──
@@ -2638,8 +2686,22 @@ const StarmapRuntime = (function (require, _react) {
2638
2686
  ".smem-plugin-card-note{font-size:12px;line-height:1.55;color:var(--dsw-alias-label-secondary,#6b7280);}",
2639
2687
  ".smem-plugin-card-err{font-size:12px;line-height:1.5;color:var(--dsw-alias-state-error-primary,#ef4444);overflow-wrap:anywhere;}",
2640
2688
  // ── 底部一行仓库链接(引流,不抢视线)──
2641
- ".smem-star{display:flex;align-items:center;justify-content:flex-end;gap:8px;flex-wrap:wrap;font-size:12px;color:var(--dsw-alias-label-tertiary,#8b93a1);padding:2px 2px 0;}",
2642
- ".smem-star-link{color:var(--dsw-alias-brand-primary,#4d76e6);text-decoration:underline;}",
2689
+ // ── 求 Star 的角标(2.5.0,D39):不占正文位置,hover 才展开半句话 ──
2690
+ // 2026-11-09 调(文歌子:原来「太不显眼了」):既然它固定在右下角、不压任何内容,
2691
+ // 就没必要靠 0.62 的透明度自我隐形 —— 那是给「怕打扰阅读」的按下去的做法,
2692
+ // 而这一颗星本来就该被看见。现在:不透明、底色抬到 layer-2、字色从 tertiary 抬到 secondary、
2693
+ // 加一层轻阴影让它离底;hover 才把「喜欢这个插件的话」展开(不常显,仍不占阅读视线)。
2694
+ ".smem-star-corner{position:fixed;right:16px;bottom:16px;z-index:20;display:flex;align-items:center;gap:6px;padding:6px 11px;border:1px solid var(--dsw-alias-border-l2);border-radius:999px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary,#6b7482);font-size:12.5px;font-weight:500;line-height:1.4;box-shadow:0 4px 14px rgba(0,0,0,.16);transition:border-color 220ms cubic-bezier(.2,0,0,1),background 220ms cubic-bezier(.2,0,0,1),box-shadow 220ms cubic-bezier(.2,0,0,1);}",
2695
+ ".smem-star-corner:hover{border-color:var(--dsw-alias-brand-primary,#4d76e6);color:var(--dsw-alias-label-primary);box-shadow:0 6px 18px rgba(0,0,0,.22);}",
2696
+ ".smem-star-glyph{font-size:13px;color:var(--dsw-alias-state-warn-primary,#d97706);transition:transform 220ms cubic-bezier(.2,0,0,1);}",
2697
+ ".smem-star-corner:hover .smem-star-glyph{transform:scale(1.12);}",
2698
+ ".smem-star-link{color:inherit;text-decoration:none;border-bottom:1px dashed transparent;}",
2699
+ ".smem-star-corner:hover .smem-star-link{border-bottom-color:currentColor;}",
2700
+ ".smem-star-more{max-width:0;overflow:hidden;white-space:nowrap;transition:max-width 220ms cubic-bezier(.2,0,0,1);}",
2701
+ ".smem-star-corner:hover .smem-star-more{max-width:180px;}",
2702
+ ".smem-star-x{border:0;background:transparent;color:inherit;cursor:pointer;font-size:11px;line-height:1;padding:0 1px;opacity:.7;font-family:inherit;}",
2703
+ ".smem-star-x:hover{opacity:1;}",
2704
+ "@media (prefers-reduced-motion:reduce){.smem-star-corner,.smem-star-more{transition:none;}}",
2643
2705
  // ── 冲突页签(2.0.0 矛盾检测)──
2644
2706
  // 一屏里要同时看「两份正文对照 + 动作 + 后果」,所以独立成套类名,不复用文件卡片:
2645
2707
  // 文件卡片的灰系 / 动作语义是「这条记忆怎么处置」,裁决是另一件事,混用会让
@@ -2653,7 +2715,6 @@ const StarmapRuntime = (function (require, _react) {
2653
2715
  ".smem-conf-file{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;max-width:100%;overflow-wrap:anywhere;white-space:normal;text-align:left;}",
2654
2716
  ".smem-conf-arrow{color:var(--dsw-alias-label-secondary);}",
2655
2717
  ".smem-conf-acts{display:flex;gap:6px;flex-wrap:wrap;align-items:center;}",
2656
- ".smem-conf-confirm{border:1px solid var(--dsw-alias-state-error-primary,#ef4444);border-radius:8px;padding:8px 10px;background:var(--dsw-alias-bg-layer-2);font-size:12px;line-height:1.55;display:flex;flex-direction:column;gap:6px;overflow-wrap:anywhere;}",
2657
2718
  // 两份正文并排对照:窄窗口自动堆成一列(grid auto-fit,不用媒体查询)。
2658
2719
  ".smem-conf-bodies{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:8px;}",
2659
2720
  ".smem-conf-body{border:1px solid var(--dsw-alias-border-l1);border-radius:8px;padding:7px 9px;display:flex;flex-direction:column;gap:4px;min-width:0;}",
@@ -2919,6 +2980,28 @@ const StarmapRuntime = (function (require, _react) {
2919
2980
  }, props.label);
2920
2981
  }
2921
2982
 
2983
+ /**
2984
+ * 开关(2.5.0,D39)。两个自动模块的启停用它,**开关是唯一的关闭入口**。
2985
+ *
2986
+ * 为什么不是 `Seg` 的两颗按钮:`Seg` 表达「几个里选一个」,开关表达「开 / 关」。
2987
+ * 后者不用读两边的字就能看出现在的状态,也不会让人以为还有第三个选项。
2988
+ * 语义走 `role="switch"` + `aria-checked`(键盘与读屏都按开关处理),不是样式上的门面。
2989
+ */
2990
+ function Switch(props) {
2991
+ const on = props.on === true;
2992
+ return h("button", {
2993
+ type: "button",
2994
+ className: "smem-switch",
2995
+ "data-on": on ? "1" : "0",
2996
+ role: "switch",
2997
+ "aria-checked": on ? "true" : "false",
2998
+ "aria-label": props.label || "",
2999
+ title: props.title || "",
3000
+ disabled: props.disabled === true,
3001
+ onClick: props.onClick,
3002
+ });
3003
+ }
3004
+
2922
3005
  /**
2923
3006
  * 活动卡片「展开态」的合并(Q16,2.1.21)。
2924
3007
  *
@@ -2957,9 +3040,51 @@ const StarmapRuntime = (function (require, _react) {
2957
3040
  title: isOpen ? t("card.collapse") : t("card.expand"),
2958
3041
  }, String(item.description))
2959
3042
  : null,
2960
- h("div", { className: "smem-card-foot", key: "foot" },
3043
+ h("div", { className: "smem-card-foot", key: "foot" }, [
3044
+ h("div", { className: "smem-actions", key: "acts" }, [
3045
+ h("button", { type: "button", className: "smem-btn", key: "edit", onClick: function () { props.onEdit(item.file); } }, t("card.edit")),
3046
+ // 语义分组:左边只放「编辑」(安全、可逆),右边放「锁定 / 归档 / 删除」(对这条记忆的退役/保护决定)。
3047
+ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
3048
+ /**
3049
+ * 锁定排在「编辑」与「归档」之间(用户 2026-10-11 指定):
3050
+ * 它是「别归档这条」的开关,紧挨着归档才说得通。
3051
+ *
3052
+ * 锁上的记忆**自动与手动都不许归档** —— 候选计算会跳过它,
3053
+ * 手动点归档会被宿主拒绝。图标用 🔒 / 🔓 之外还留着文字,
3054
+ * 因为「锁」在这里是**拒绝归档**,不是「只读」。
3055
+ */
3056
+ h("button", {
3057
+ type: "button",
3058
+ className: "smem-btn" + (item.locked ? " smem-btn-locked" : ""),
3059
+ key: "lock",
3060
+ title: item.locked
3061
+ ? t("card.lockedTitle")
3062
+ : t("card.unlockedTitle"),
3063
+ disabled: props.busy,
3064
+ onClick: function () { props.onToggleLock(item.file, !item.locked); },
3065
+ }, item.locked ? t("card.locked") : t("card.lock")),
3066
+ // 归档排在「锁定」和「删除」之间:比编辑轻、比删除温和,且随时可逆。
3067
+ h("button", {
3068
+ type: "button", className: "smem-btn", key: "arch",
3069
+ title: item.locked
3070
+ ? t("card.archiveLockedTitle")
3071
+ : t("card.archiveTitle"),
3072
+ disabled: props.busy || item.locked === true,
3073
+ onClick: function () { props.onAskArchive(item.file); },
3074
+ }, t("card.archive")),
3075
+ h("button", { type: "button", className: "smem-btn smem-btn-danger-ghost", key: "del", onClick: function () { props.onAskDelete(item.file); } }, t("card.delete")),
3076
+ ]),
3077
+ // 两个确认框都是**模态**(2.5.0):DOM 里留在卡片内只是为了「状态跟着这一行走」,
3078
+ // 视觉上是全屏一层 —— 列表被滚出视野时,按下去那一刻看到的东西还在眼前。
2961
3079
  props.archiving
2962
- ? h("div", { className: "smem-ar-confirm", key: "ar" }, [
3080
+ ? h(Confirm, {
3081
+ key: "ar",
3082
+ title: t("card.archiveAsk", { file: item.file }),
3083
+ confirmLabel: t("card.confirmArchive"),
3084
+ busy: props.busy === true,
3085
+ onConfirm: function () { props.onConfirmArchive(item.file); },
3086
+ onCancel: props.onCancelArchive,
3087
+ }, [
2963
3088
  h("div", { key: "what" }, t("card.archiveWhat", { file: item.file })),
2964
3089
  // 「可留空」必须跟输入框贴在一起、并且独立成行 —— 它是这一块最容易
2965
3090
  // 被误读成「必须填才能按」的地方。
@@ -2971,50 +3096,20 @@ const StarmapRuntime = (function (require, _react) {
2971
3096
  onChange: function (e) { props.onArchiveReason(e.target.value); },
2972
3097
  }),
2973
3098
  h("div", { className: "smem-ar-hint", key: "hint" }, t("card.archiveHint")),
2974
- h("div", { className: "smem-row", key: "btns" }, [
2975
- h("button", { type: "button", className: "smem-btn smem-btn-primary", key: "ok", disabled: props.busy, onClick: function () { props.onConfirmArchive(item.file); } }, t("card.confirmArchive")),
2976
- h("button", { type: "button", className: "smem-btn", key: "no", disabled: props.busy, onClick: props.onCancelArchive }, t("card.cancel")),
2977
- ]),
2978
3099
  ])
2979
- : h("div", { className: "smem-actions", key: "acts" },
2980
- props.deleting
2981
- ? [
2982
- h("button", { type: "button", className: "smem-btn smem-btn-danger", key: "yes", onClick: function () { props.onConfirmDelete(item.file); } }, t("card.confirmDelete")),
2983
- h("button", { type: "button", className: "smem-btn", key: "no", onClick: function () { props.onCancelDelete(); } }, t("card.cancel")),
2984
- ]
2985
- : [
2986
- h("button", { type: "button", className: "smem-btn", key: "edit", onClick: function () { props.onEdit(item.file); } }, t("card.edit")),
2987
- // 语义分组:左边只放「编辑」(安全、可逆),右边放「锁定 / 归档 / 删除」(对这条记忆的退役/保护决定)。
2988
- h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
2989
- /**
2990
- * 锁定排在「编辑」与「归档」之间(用户 2026-10-11 指定):
2991
- * 它是「别归档这条」的开关,紧挨着归档才说得通。
2992
- *
2993
- * 锁上的记忆**自动与手动都不许归档** —— 候选计算会跳过它,
2994
- * 手动点归档会被宿主拒绝。图标用 🔒 / 🔓 之外还留着文字,
2995
- * 因为「锁」在这里是**拒绝归档**,不是「只读」。
2996
- */
2997
- h("button", {
2998
- type: "button",
2999
- className: "smem-btn" + (item.locked ? " smem-btn-locked" : ""),
3000
- key: "lock",
3001
- title: item.locked
3002
- ? t("card.lockedTitle")
3003
- : t("card.unlockedTitle"),
3004
- disabled: props.busy,
3005
- onClick: function () { props.onToggleLock(item.file, !item.locked); },
3006
- }, item.locked ? t("card.locked") : t("card.lock")),
3007
- // 归档排在「锁定」和「删除」之间:比编辑轻、比删除温和,且随时可逆。
3008
- h("button", {
3009
- type: "button", className: "smem-btn", key: "arch",
3010
- title: item.locked
3011
- ? t("card.archiveLockedTitle")
3012
- : t("card.archiveTitle"),
3013
- disabled: props.busy || item.locked === true,
3014
- onClick: function () { props.onAskArchive(item.file); },
3015
- }, t("card.archive")),
3016
- h("button", { type: "button", className: "smem-btn smem-btn-danger-ghost", key: "del", onClick: function () { props.onAskDelete(item.file); } }, t("card.delete")),
3017
- ])),
3100
+ : null,
3101
+ props.deleting
3102
+ ? h(Confirm, {
3103
+ key: "del",
3104
+ title: t("card.deleteAsk", { file: item.file }),
3105
+ confirmLabel: t("card.confirmDelete"),
3106
+ busy: props.busy === true,
3107
+ danger: true,
3108
+ onConfirm: function () { props.onConfirmDelete(item.file); },
3109
+ onCancel: function () { props.onCancelDelete(); },
3110
+ }, h("div", { key: "what" }, t("card.deleteHint")))
3111
+ : null,
3112
+ ]),
3018
3113
  ]);
3019
3114
  }
3020
3115
 
@@ -3082,37 +3177,43 @@ const StarmapRuntime = (function (require, _react) {
3082
3177
  h("button", { type: "button", className: "smem-btn", key: "cancel", disabled: props.busy, onClick: props.onCancel }, t("card.cancel")),
3083
3178
  ]),
3084
3179
  ])
3085
- : props.deleting
3086
- ? h("div", { className: "smem-ar-confirm", key: "del" }, [
3087
- h("div", { key: "what" }, t("card.purgeWhat", { file: item.file })),
3088
- h("div", { className: "smem-ar-hint", key: "hint" }, t("card.purgeHint")),
3089
- h("div", { className: "smem-row", key: "btns" }, [
3090
- h("button", { type: "button", className: "smem-btn smem-btn-danger", key: "yes", disabled: props.busy, onClick: function () { props.onConfirmDelete(item.file); } }, t("card.confirmPurge")),
3091
- h("button", { type: "button", className: "smem-btn", key: "no", disabled: props.busy, onClick: props.onCancelDelete }, t("card.cancel")),
3092
- ]),
3093
- ])
3094
- : h("div", { className: "smem-card-foot", key: "foot" },
3095
- h("div", { className: "smem-actions", key: "acts" }, [
3096
- h("button", {
3097
- type: "button", className: "smem-btn", key: "edit",
3098
- disabled: props.busy,
3099
- onClick: function () { props.onEdit(item.file); },
3100
- }, t("card.edit")),
3180
+ : h("div", { className: "smem-card-foot", key: "foot" },
3181
+ h("div", { className: "smem-actions", key: "acts" }, [
3182
+ h("button", {
3183
+ type: "button", className: "smem-btn", key: "edit",
3184
+ disabled: props.busy,
3185
+ onClick: function () { props.onEdit(item.file); },
3186
+ }, t("card.edit")),
3101
3187
  h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
3102
- h("button", {
3103
- type: "button", className: "smem-btn smem-btn-primary", key: "restore",
3104
- disabled: props.busy,
3105
- onClick: function () { props.onRestore(item.file); },
3106
- }, props.restoring ? t("card.restoring") : t("card.restore")),
3107
- // 放在最后、且用 danger-ghost:它是这张卡上唯一不可逆的动作,
3108
- // 不该跟「恢复」长得一样。
3109
- h("button", {
3110
- type: "button", className: "smem-btn smem-btn-danger-ghost", key: "purge",
3111
- disabled: props.busy,
3112
- title: t("card.purgeTitle"),
3113
- onClick: function () { props.onAskDelete(item.file); },
3114
- }, t("card.purge")),
3115
- ])),
3188
+ h("button", {
3189
+ type: "button", className: "smem-btn smem-btn-primary", key: "restore",
3190
+ disabled: props.busy,
3191
+ onClick: function () { props.onRestore(item.file); },
3192
+ }, props.restoring ? t("card.restoring") : t("card.restore")),
3193
+ // 放在最后、且用 danger-ghost:它是这张卡上唯一不可逆的动作,
3194
+ // 不该跟「恢复」长得一样。
3195
+ h("button", {
3196
+ type: "button", className: "smem-btn smem-btn-danger-ghost", key: "purge",
3197
+ disabled: props.busy,
3198
+ title: t("card.purgeTitle"),
3199
+ onClick: function () { props.onAskDelete(item.file); },
3200
+ }, t("card.purge")),
3201
+ ])),
3202
+ // 彻底删除的确认框:模态(2.5.0),文案一字未改,只是从行内条搬到全屏一层。
3203
+ props.deleting
3204
+ ? h(Confirm, {
3205
+ key: "del",
3206
+ title: t("card.purgeAsk", { file: item.file }),
3207
+ confirmLabel: t("card.confirmPurge"),
3208
+ busy: props.busy === true,
3209
+ danger: true,
3210
+ onConfirm: function () { props.onConfirmDelete(item.file); },
3211
+ onCancel: props.onCancelDelete,
3212
+ }, [
3213
+ h("div", { key: "what" }, t("card.purgeWhat", { file: item.file })),
3214
+ h("div", { className: "smem-ar-hint", key: "hint" }, t("card.purgeHint")),
3215
+ ])
3216
+ : null,
3116
3217
  ]);
3117
3218
  }
3118
3219
 
@@ -3292,22 +3393,86 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
3292
3393
  * 跟问题一样染红会让人去「修」一条本来正确的链接 —— 那正是审计里那条分界线。
3293
3394
  */
3294
3395
  /**
3295
- * 面板底部的一行仓库链接。
3396
+ * 右下角的「求 Star」角标(2.5.0,D39)。
3296
3397
  *
3297
- * 参照手机访问插件那一行的做法:放在不抢视线的地方、语气轻、点开是新标签页。
3298
- * 它**是引流不是功能** —— 所以不占一屏、不做成按钮、失败也没有任何副作用;
3299
- * 四个 tab 下都在(谁也没必要为看它切页)。
3398
+ * 1.0~2.4 是一行居中的正文:`⭐ 顺手留颗 Star,作者能高兴一整天` + `行,给你一颗 Star`。
3399
+ * 那套说辞两个问题 —— ①它**占正文位置**,四个页签底部各一行,读起来像广告;
3400
+ * ②「作者能高兴一整天」是求人,把读者放到施舍的位置上。
3401
+ *
3402
+ * 改成一个角落里的小东西:恒显一个星号 + `Star`,hover 才展开半句话,可关。
3403
+ * 关闭状态**只存这个模块的变量**:不落 localStorage(面板端口每次启动都变,
3404
+ * 存了也读不回来 —— sage-reminder 踩过),也不落设置(不为一个装饰件改宿主半身)。
3405
+ * 于是它的语义正好是「本次启动不再打扰」,下次打开 DSH 再说一次。
3300
3406
  */
3407
+ let starDismissed = false;
3408
+
3301
3409
  function StarLine() {
3302
- return h("div", { className: "smem-star" }, [
3303
- h("span", { key: "ask" }, t("star.ask")),
3410
+ const s = react.useState(starDismissed);
3411
+ const gone = s[0], setGone = s[1];
3412
+ if (gone) return null;
3413
+ return h("div", { className: "smem-star-corner", key: "star" }, [
3414
+ h("span", { className: "smem-star-glyph", key: "g", "aria-hidden": "true" }, "★"),
3304
3415
  h("a", {
3305
3416
  key: "cta",
3306
3417
  href: "https://github.com/gezi-wen/sage-mem",
3307
3418
  target: "_blank",
3308
3419
  rel: "noreferrer",
3309
3420
  className: "smem-star-link",
3421
+ title: t("star.aria"),
3310
3422
  }, t("star.cta")),
3423
+ h("span", { className: "smem-star-more", key: "more" }, t("star.hover")),
3424
+ h("button", {
3425
+ type: "button", key: "x", className: "smem-star-x",
3426
+ title: t("star.dismiss"),
3427
+ "aria-label": t("star.dismiss"),
3428
+ onClick: function () { starDismissed = true; setGone(true); },
3429
+ }, "×"),
3430
+ ]);
3431
+ }
3432
+
3433
+ /**
3434
+ * 危险动作的模态确认框(2.5.0,D39)。
3435
+ *
3436
+ * 1.0~2.4 用的是「就地确认条」:在那一行下面展开一条,按钮和列表挤在一起。
3437
+ * 换成模态的两个理由,都是实测出来的:
3438
+ * ① 就地条与「取消」在同一张列表里,误点路径太短;同一屏出现两条时
3439
+ * 也分不清正在确认哪一条(归档候选面板能同时展开两条)。
3440
+ * ② 那些动作会改盘上文件,而条子可以被滚出视野 —— 按下去的那一刻
3441
+ * 用户看到的东西必须还在。
3442
+ *
3443
+ * 两条纪律:
3444
+ * - **两段不能变一段**:模态是第二段,第一段是那颗按钮。永远不合并。
3445
+ * - **文案来自调用方**:每个动作的后果不一样,这里只提供壳子(问句 + 危险按钮 + 取消)。
3446
+ *
3447
+ * `busy` 时按钮禁用、确认键换成进行中的字样:默认是本文件通用的「保存中…」,
3448
+ * 但调用方原来若有自己那一句(回滚是「回滚中…」、冲突裁决是「处理中…」),
3449
+ * 用 `busyLabel` 传进来 —— 换模态不该把每一处具体的进度措辞压成同一句。
3450
+ *
3451
+ * ⚠️ 它靠 `position:fixed` 铺满视口,所以祖先节点**不能有静态的 transform / filter**
3452
+ * (有的话 `fixed` 会改成相对那个祖先定位,模态就跑进列表里去了)。换页签那条
3453
+ * `smem-enter` 动画带 transform,但只活 220ms、不带 fill,播完就没了 —— 别把确认框
3454
+ * 塞进另一层还在跑动效的容器里就行。
3455
+ */
3456
+ function Confirm(props) {
3457
+ const busy = props.busy === true;
3458
+ return h("div", { className: "smem-modal-host" }, [
3459
+ h("div", { className: "smem-modal-scrim", key: "scrim", onClick: busy ? undefined : props.onCancel }),
3460
+ h("div", {
3461
+ className: "smem-modal", key: "m",
3462
+ role: "dialog", "aria-modal": "true",
3463
+ "aria-label": props.title || "",
3464
+ }, [
3465
+ h("div", { className: "smem-modal-title", key: "t" }, props.title),
3466
+ h("div", { className: "smem-modal-body", key: "b" }, props.children),
3467
+ h("div", { className: "smem-modal-foot", key: "f" }, [
3468
+ h("button", { type: "button", className: "smem-btn", key: "c", disabled: busy, onClick: props.onCancel }, t("card.cancel")),
3469
+ h("button", {
3470
+ type: "button", key: "ok", disabled: busy,
3471
+ className: "smem-btn " + (props.danger ? "smem-btn-danger" : "smem-btn-primary"),
3472
+ onClick: props.onConfirm,
3473
+ }, busy ? (props.busyLabel || t("card.saving")) : props.confirmLabel),
3474
+ ]),
3475
+ ]),
3311
3476
  ]);
3312
3477
  }
3313
3478
 
@@ -3508,14 +3673,33 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
3508
3673
  return parts;
3509
3674
  }
3510
3675
 
3511
- /** 危险动作的两段式确认条(照「彻底归档删除」先例:先问,再动手)。 */
3676
+ /**
3677
+ * 危险动作的两段式确认(照「彻底归档删除」先例:先问,再动手)。
3678
+ *
3679
+ * 2.5.0 起是**模态**(D39):同一屏能同时展开好几条记录,行内条会让人分不清
3680
+ * 正在确认哪一条;搬到全屏一层之后,「问的是哪条」写在标题上。
3681
+ * 三个动作的后果不同,所以确认键的措辞也各自保留(`busyLabel` 传原来那句
3682
+ * 「处理中…」)。
3683
+ */
3512
3684
  function confirmBlock(item) {
3513
3685
  if (!confirm || confirm.id !== item.id) return null;
3514
3686
  const act = confirm.action;
3515
3687
  const file = confirm.file || item.file;
3688
+ const busy = busyId === item.id;
3516
3689
  if (act === "invalidate") {
3517
- return h("div", { className: "smem-conf-confirm", key: "cf" }, [
3518
- h("div", { key: "what" }, t("conflict.confirmInvalidate", { file: file })),
3690
+ return h(Confirm, {
3691
+ key: "cf",
3692
+ title: t("conflict.invalidateAsk", { file: file }),
3693
+ confirmLabel: t("conflict.confirmYesInvalidate"),
3694
+ busyLabel: t("conflict.busy"),
3695
+ busy: busy,
3696
+ danger: true,
3697
+ onConfirm: props.onRun,
3698
+ onCancel: props.onCancelConfirm,
3699
+ }, [
3700
+ h("div", { key: "what" }, t("conflict.confirmInvalidate")),
3701
+ // 「两句互斥,选哪一边作废」的选择器必须留在确认框里:
3702
+ // 它改的正是这一问的宾语,挪到框外就会出现「框里问 A、按钮标 B」。
3519
3703
  item.otherFile
3520
3704
  ? h("div", { className: "smem-chips", key: "pick" }, [
3521
3705
  h(Chip, {
@@ -3529,49 +3713,35 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
3529
3713
  ])
3530
3714
  : null,
3531
3715
  h("div", { className: "smem-ar-hint", key: "hint" }, t("conflict.confirmInvalidateHint")),
3532
- h("div", { className: "smem-row", key: "btns" }, [
3533
- h("button", {
3534
- type: "button", className: "smem-btn smem-btn-danger", key: "yes",
3535
- disabled: busyId === item.id,
3536
- onClick: props.onRun,
3537
- }, busyId === item.id ? t("conflict.busy") : t("conflict.confirmYesInvalidate")),
3538
- h("button", {
3539
- type: "button", className: "smem-btn", key: "no",
3540
- disabled: busyId === item.id, onClick: props.onCancelConfirm,
3541
- }, t("conflict.cancel")),
3542
- ]),
3543
3716
  ]);
3544
3717
  }
3545
3718
  if (act === "undo") {
3546
- return h("div", { className: "smem-conf-confirm", key: "cf" }, [
3547
- h("div", { key: "what" }, t("conflict.confirmUndo", { file: item.invalidatedFile || item.file })),
3719
+ return h(Confirm, {
3720
+ key: "cf",
3721
+ title: t("conflict.undoAsk", { file: item.invalidatedFile || item.file }),
3722
+ confirmLabel: t("conflict.confirmYesUndo"),
3723
+ busyLabel: t("conflict.busy"),
3724
+ busy: busy,
3725
+ onConfirm: props.onRun,
3726
+ onCancel: props.onCancelConfirm,
3727
+ }, [
3728
+ h("div", { key: "what" }, t("conflict.confirmUndo")),
3548
3729
  h("div", { className: "smem-ar-hint", key: "hint" }, t("conflict.confirmUndoHint")),
3549
- h("div", { className: "smem-row", key: "btns" }, [
3550
- h("button", {
3551
- type: "button", className: "smem-btn smem-btn-primary", key: "yes",
3552
- disabled: busyId === item.id, onClick: props.onRun,
3553
- }, busyId === item.id ? t("conflict.busy") : t("conflict.confirmYesUndo")),
3554
- h("button", {
3555
- type: "button", className: "smem-btn", key: "no",
3556
- disabled: busyId === item.id, onClick: props.onCancelConfirm,
3557
- }, t("conflict.cancel")),
3558
- ]),
3559
3730
  ]);
3560
3731
  }
3561
3732
  if (act === "removeRecord") {
3562
- return h("div", { className: "smem-conf-confirm", key: "cf" }, [
3733
+ return h(Confirm, {
3734
+ key: "cf",
3735
+ title: t("conflict.removeAsk"),
3736
+ confirmLabel: t("conflict.confirmYesRemove"),
3737
+ busyLabel: t("conflict.busy"),
3738
+ busy: busy,
3739
+ danger: true,
3740
+ onConfirm: props.onRun,
3741
+ onCancel: props.onCancelConfirm,
3742
+ }, [
3563
3743
  h("div", { key: "what" }, t("conflict.confirmRemove")),
3564
3744
  h("div", { className: "smem-ar-hint", key: "hint" }, t("conflict.confirmRemoveHint")),
3565
- h("div", { className: "smem-row", key: "btns" }, [
3566
- h("button", {
3567
- type: "button", className: "smem-btn smem-btn-danger", key: "yes",
3568
- disabled: busyId === item.id, onClick: props.onRun,
3569
- }, busyId === item.id ? t("conflict.busy") : t("conflict.confirmYesRemove")),
3570
- h("button", {
3571
- type: "button", className: "smem-btn", key: "no",
3572
- disabled: busyId === item.id, onClick: props.onCancelConfirm,
3573
- }, t("conflict.cancel")),
3574
- ]),
3575
3745
  ]);
3576
3746
  }
3577
3747
  return null;
@@ -3694,6 +3864,11 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
3694
3864
  const pickedN = cands.filter((x) => props.picked[x.file]).length;
3695
3865
  const mode = autoArchiveOf(c.autoArchive);
3696
3866
  const allPicked = cands.length > 0 && pickedN === cands.length;
3867
+ // 2.5.0(D39):自动归档关掉时,手动「执行归档」一起置灰 —— 开关是唯一的关闭入口,
3868
+ // 关掉还说「能手动跑」等于开关只挡住了自动那部分。清单照常可看(要看得先看得见。
3869
+ // 想看会被收走哪些,正是决定要不要打开开关的依据)。
3870
+ // ⚠️ 这是**界面决定**:宿主侧 `archiveCandidates` 不看 autoArchive,手动归档随时能跑。
3871
+ const offNow = mode === "off";
3697
3872
  return h("div", { className: "smem-tool-panel smem-tool-panel--cap", key: "cand" }, [
3698
3873
  h("div", { className: "smem-tool-head", key: "head" }, [
3699
3874
  h("span", { className: "smem-tool-title", key: "t" }, "归档候选"),
@@ -3711,6 +3886,9 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
3711
3886
  : h("div", { className: "smem-cand-list", key: "list" }, [
3712
3887
  h("div", { className: "smem-cand-note", key: "note" },
3713
3888
  "候选只按「多久没被注入过」算。归档 = 移进 archive/(不删),随时可以恢复。"),
3889
+ offNow
3890
+ ? h("div", { className: "smem-modcard-off", key: "off" }, t("archive.note.offStop"))
3891
+ : null,
3714
3892
  cands.map((x) => h("div", { className: "smem-cand", key: x.file }, [
3715
3893
  h("label", { className: "smem-cand-main", key: "m" }, [
3716
3894
  h("input", { type: "checkbox", key: "cb", checked: !!props.picked[x.file], onChange: function () { props.onPick(x.file); } }),
@@ -3721,27 +3899,35 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
3721
3899
  h("div", { className: "smem-cand-reason", key: "why" },
3722
3900
  "为什么建议归档:" + (x.reason || ("闲置 " + String(x.days) + " 天,超过该类型阈值 " + String(x.limit) + " 天"))),
3723
3901
  props.confirm === x.file
3724
- ? h("div", { className: "smem-cand-confirm", key: "cf" }, [
3725
- h("span", { key: "t" }, "确认把「" + x.file + "」移进档案馆?它不再参与检索与星图,随时可以恢复。"),
3726
- h("button", { type: "button", className: "smem-btn smem-btn-primary", key: "yes", disabled: props.busy, onClick: function () { props.onArchiveOne(x); } }, "确认归档"),
3727
- h("button", { type: "button", className: "smem-btn", key: "no", disabled: props.busy, onClick: props.onCancelConfirm }, "取消"),
3728
- ])
3902
+ ? h(Confirm, {
3903
+ key: "cf",
3904
+ title: t("card.candAsk", { file: x.file }),
3905
+ confirmLabel: t("card.confirmArchive"),
3906
+ busy: props.busy === true,
3907
+ onConfirm: function () { props.onArchiveOne(x); },
3908
+ onCancel: props.onCancelConfirm,
3909
+ }, h("div", { key: "why" }, t("card.candAskHint")))
3729
3910
  : h("div", { className: "smem-cand-act", key: "act" },
3730
- h("button", { type: "button", className: "smem-btn", key: "one", disabled: props.busy, onClick: function () { props.onAskOne(x.file); } }, "归档这一条")),
3911
+ h("button", { type: "button", className: "smem-btn", key: "one", disabled: props.busy || offNow, title: offNow ? t("archive.note.offStop") : "", onClick: function () { props.onAskOne(x.file); } }, "归档这一条")),
3731
3912
  ])),
3732
3913
  ]),
3733
3914
  cands.length
3734
3915
  ? h("div", { className: "smem-tool-foot", key: "foot" }, [
3735
3916
  h("button", { type: "button", className: "smem-btn", key: "all", disabled: props.busy, onClick: props.onPickAll }, allPicked ? "全不选" : "全选"),
3736
- props.confirm === "__batch__"
3737
- ? [
3738
- h("span", { className: "smem-ar-hint", key: "t" }, "确认把选中的 " + pickedN + " 条一起移进档案馆?"),
3739
- h("button", { type: "button", className: "smem-btn smem-btn-primary", key: "yes", disabled: props.busy || !pickedN, onClick: props.onArchivePicked }, "确认归档"),
3740
- h("button", { type: "button", className: "smem-btn", key: "no", disabled: props.busy, onClick: props.onCancelConfirm }, "取消"),
3741
- ]
3742
- : h("button", { type: "button", className: "smem-btn smem-btn-primary", key: "go", disabled: props.busy || !pickedN, onClick: props.onAskPicked }, "执行归档(选中 " + pickedN + " 条)"),
3917
+ h("button", { type: "button", className: "smem-btn smem-btn-primary", key: "go", disabled: props.busy || !pickedN || offNow, title: offNow ? t("archive.note.offStop") : "", onClick: props.onAskPicked }, "执行归档(选中 " + pickedN + " 条)"),
3743
3918
  ])
3744
3919
  : null,
3920
+ // 批量确认:同一屏里逐条确认也走模态,两者不并存(confirm 一次只持有一个值)。
3921
+ props.confirm === "__batch__"
3922
+ ? h(Confirm, {
3923
+ key: "batch",
3924
+ title: t("card.candBatchAsk", { count: pickedN }),
3925
+ confirmLabel: t("card.confirmArchive"),
3926
+ busy: props.busy === true,
3927
+ onConfirm: props.onArchivePicked,
3928
+ onCancel: props.onCancelConfirm,
3929
+ }, h("div", { key: "why" }, t("card.candBatchHint")))
3930
+ : null,
3745
3931
  ]);
3746
3932
  }
3747
3933
 
@@ -3841,10 +4027,8 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
3841
4027
 
3842
4028
  "tab.files": "文件列表",
3843
4029
  "tab.star": "时间线",
3844
- "tab.autodream": "自动做梦",
3845
- "tab.archive": "自动归档",
3846
- "tab.audit": "体检",
3847
- "tab.conflict": "冲突",
4030
+ "tab.auto": "自动整理",
4031
+ "tab.health": "库健康",
3848
4032
 
3849
4033
  "type.user": "用户",
3850
4034
  "type.feedback": "反馈",
@@ -3932,6 +4116,7 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
3932
4116
  "card.typeTitle": "类型 {type}",
3933
4117
  "card.collapse": "点击收起",
3934
4118
  "card.expand": "点击展开全文",
4119
+ "card.archiveAsk": "归档「{file}」?",
3935
4120
  "card.archiveWhat": "把「{file}」收进档案馆:它不再参与检索与星图,文件移进 archive/,随时可以恢复。",
3936
4121
  "card.archiveReason": "归档理由(可留空)",
3937
4122
  "card.archiveReasonPlaceholder": "例如:结论已被 project_beta 取代",
@@ -3948,6 +4133,8 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
3948
4133
  "card.archiveTitle": "收进档案馆:不再参与检索与星图,随时可以恢复",
3949
4134
  "card.archive": "归档",
3950
4135
  "card.delete": "删除",
4136
+ "card.deleteAsk": "删除「{file}」?",
4137
+ "card.deleteHint": "删除是直接把文件从记忆目录里拿掉,不进档案馆 —— 没有撤销,也没有回收站。",
3951
4138
  "card.archivedAtTitle": "归档时间",
3952
4139
  "card.archivedAt": "已归档 {at}",
3953
4140
  "card.unknownTime": "(时间未知)",
@@ -3957,16 +4144,23 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
3957
4144
  "card.reading": "读取中…",
3958
4145
  "card.saving": "保存中…",
3959
4146
  "card.save": "保存",
4147
+ "card.purgeAsk": "彻底删除「{file}」?",
3960
4148
  "card.purgeWhat": "彻底删除「{file}」:它会从 archive/ 里被真的删掉,不能恢复。要保留它就先点「恢复」,把它放回活动记忆。",
3961
4149
  "card.purgeHint": "这一步之后没有任何界面能把内容找回来 —— 只剩你自己在磁盘上留过备份。",
3962
4150
  "card.confirmPurge": "确认彻底删除",
4151
+ "card.candAsk": "把「{file}」移进档案馆?",
4152
+ "card.candAskHint": "它不再参与检索与星图,文件移进 archive/,随时可以恢复。",
4153
+ "card.candBatchAsk": "把选中的 {count} 条一起移进档案馆?",
4154
+ "card.candBatchHint": "归档 = 移进 archive/(不删),随时可以恢复。",
3963
4155
  "card.restoring": "恢复中…",
3964
4156
  "card.restore": "恢复",
3965
4157
  "card.purgeTitle": "从 archive/ 里彻底删掉,不可恢复(要留就先恢复)",
3966
4158
  "card.purge": "彻底删除",
3967
4159
 
3968
- "star.ask": "⭐ 顺手留颗 Star,作者能高兴一整天",
3969
- "star.cta": "行,给你一颗 Star",
4160
+ "star.cta": "Star",
4161
+ "star.hover": "· 喜欢这个插件的话",
4162
+ "star.dismiss": "不再显示(下次打开 DSH 还会出现)",
4163
+ "star.aria": "在 GitHub 上给 gezi-wen/sage-mem 点一颗星",
3970
4164
 
3971
4165
  "pluginCard.saveFailed": "保存失败(未知原因)",
3972
4166
  "pluginCard.noSetSettings": "宿主侧还没有 setSettings 接口(插件版本较旧)",
@@ -4065,16 +4259,18 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4065
4259
  "conflict.actionMissing": "宿主侧还没有 conflict.{method} 接口(插件版本较旧),这个动作已禁用",
4066
4260
  "conflict.actionMissingHint": "版本较旧:缺少 {methods} 接口 —— 对应按钮已禁用。可升级插件,或手工编辑记忆 frontmatter(加 invalid_at)。",
4067
4261
  "conflict.readMissingHint": "版本较旧:没有 conflict.read 接口 —— 正文对照已禁用,仍可裁决。",
4068
- "conflict.confirmInvalidate": "确认把「{file}」标为失效?会往它的 frontmatter 写 invalid_at(可撤销),文件仍在原目录、正文不变。",
4262
+ "conflict.invalidateAsk": "把「{file}」标为失效?",
4263
+ "conflict.confirmInvalidate": "会往它的 frontmatter 写 invalid_at(可撤销),文件仍在原目录、正文不变。",
4069
4264
  "conflict.confirmInvalidateHint": "只改 frontmatter 里的一行;不删除、不移动文件,也不改 updated。",
4070
- "conflict.confirmUndo": "撤销「{file}」的失效标记?会删掉那行 invalid_at(以及 superseded_by),它重新参与注入。",
4265
+ "conflict.undoAsk": "撤销「{file}」的失效标记?",
4266
+ "conflict.confirmUndo": "会删掉那行 invalid_at(以及 superseded_by),它重新参与注入。",
4071
4267
  "conflict.confirmUndoHint": "只删我们写的那一行,其他字节不动。",
4072
- "conflict.confirmRemove": "删除这条冲突记录?记录会从队列里消失,不能恢复(记忆文件不受影响)。",
4268
+ "conflict.removeAsk": "删除这条冲突记录?",
4269
+ "conflict.confirmRemove": "记录会从队列里消失,不能恢复(记忆文件不受影响)。",
4073
4270
  "conflict.confirmRemoveHint": "只删状态目录里的记录文件;记忆本身一个字节都不动。",
4074
4271
  "conflict.confirmYesInvalidate": "确认标失效",
4075
4272
  "conflict.confirmYesUndo": "确认撤销",
4076
4273
  "conflict.confirmYesRemove": "确认删除记录",
4077
- "conflict.cancel": "取消",
4078
4274
  "conflict.busy": "处理中…",
4079
4275
  "conflict.decision.invalidate": "标失效",
4080
4276
  "conflict.decision.dismiss": "标记已裁决",
@@ -4115,6 +4311,8 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4115
4311
  "ad.status.idle": "已启用 · 空闲",
4116
4312
  "ad.status.off": "未启用",
4117
4313
  "ad.note.what": "把记忆目录通读一遍:合并重复的、删掉被推翻的、修索引与断链、把相对日期换成绝对日期。",
4314
+ "ad.note.offStop": "模块已关闭 —— 「立即整理」已置灰,打开上面的开关才能跑。设置与记录不受影响,随时可改。",
4315
+ "ad.note.offStopTitle": "模块已关闭,先打开上面的开关",
4118
4316
  "ad.row.trigger": "触发",
4119
4317
  "ad.trigger.manual": "只手动",
4120
4318
  "ad.trigger.auto": "满足条件自动跑",
@@ -4233,7 +4431,6 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4233
4431
  "ad.snap.confirmSuffix": "。回滚前会先存一份保护快照,所以这一步本身也能退。",
4234
4432
  "ad.action.rollingBack": "回滚中…",
4235
4433
  "ad.action.confirmRollback": "确认回滚",
4236
- "ad.action.cancel": "取消",
4237
4434
  "ad.rb.rolledBack": "已回滚到 {at}:恢复 {restored} 个文件,移走 {parked} 个",
4238
4435
  "ad.rb.skipped": ",跳过 {count} 个(详见回滚报告)",
4239
4436
  "ad.rb.protection": " · 保护快照 {id}",
@@ -4285,6 +4482,11 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4285
4482
  "archive.intro2": "它不调模型:候选完全按访问台账的天数算,同一份输入每次算出来都一样。",
4286
4483
  "archive.intro3": "想现在看看会被收走哪些:文件列表 →「工具 ▾」→ 归档候选。",
4287
4484
  "archive.modeTitle": "自动归档",
4485
+ "archive.switch.onTitle": "点击停用自动归档(关掉后一个文件都不动)",
4486
+ "archive.switch.offTitle": "点击启用自动归档",
4487
+ "archive.status.on": "已启用 · {mode}",
4488
+ "archive.status.off": "已关闭(一个文件都不动)",
4489
+ "archive.note.offStop": "关闭:一个文件都不动 —— 候选只列在清单里,等你手点。开关是唯一的关闭入口;设置与记录不受影响,随时可改。",
4288
4490
  "archive.mode.off": "关闭",
4289
4491
  "archive.mode.report": "只出报告",
4290
4492
  "archive.mode.auto": "满足条件自动跑",
@@ -4324,10 +4526,8 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4324
4526
 
4325
4527
  "tab.files": "Files",
4326
4528
  "tab.star": "Timeline",
4327
- "tab.autodream": "Autodream",
4328
- "tab.archive": "Auto-archive",
4329
- "tab.audit": "Audit",
4330
- "tab.conflict": "Conflicts",
4529
+ "tab.auto": "Automation",
4530
+ "tab.health": "Health",
4331
4531
 
4332
4532
  "type.user": "User",
4333
4533
  "type.feedback": "Feedback",
@@ -4415,6 +4615,7 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4415
4615
  "card.typeTitle": "Type {type}",
4416
4616
  "card.collapse": "Click to collapse",
4417
4617
  "card.expand": "Click to expand",
4618
+ "card.archiveAsk": "Archive “{file}”?",
4418
4619
  "card.archiveWhat": "Move “{file}” into the archive: it stops participating in retrieval and the star map, moves under archive/, and can be restored at any time.",
4419
4620
  "card.archiveReason": "Archive reason (optional)",
4420
4621
  "card.archiveReasonPlaceholder": "e.g. superseded by project_beta",
@@ -4431,6 +4632,8 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4431
4632
  "card.archiveTitle": "Move into the archive: out of retrieval and the star map, restorable at any time",
4432
4633
  "card.archive": "Archive",
4433
4634
  "card.delete": "Delete",
4635
+ "card.deleteAsk": "Delete “{file}”?",
4636
+ "card.deleteHint": "Deleting removes the file from the memory directory instead of moving it into the archive — there is no undo and no trash.",
4434
4637
  "card.archivedAtTitle": "Archived at",
4435
4638
  "card.archivedAt": "Archived {at}",
4436
4639
  "card.unknownTime": "(time unknown)",
@@ -4440,16 +4643,23 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4440
4643
  "card.reading": "Reading…",
4441
4644
  "card.saving": "Saving…",
4442
4645
  "card.save": "Save",
4646
+ "card.purgeAsk": "Permanently delete “{file}”?",
4443
4647
  "card.purgeWhat": "Permanently delete “{file}”: it is really removed from archive/ and cannot be recovered. To keep it, press Restore first and put it back among the active memories.",
4444
4648
  "card.purgeHint": "After this step no screen can bring the content back — only a backup you kept on disk can.",
4445
4649
  "card.confirmPurge": "Confirm permanent delete",
4650
+ "card.candAsk": "Move “{file}” into the archive?",
4651
+ "card.candAskHint": "It stops participating in retrieval and the star map; the file moves under archive/ and can be restored at any time.",
4652
+ "card.candBatchAsk": "Move the {count} selected memories into the archive?",
4653
+ "card.candBatchHint": "Archiving moves files under archive/ (nothing is deleted) and they can be restored at any time.",
4446
4654
  "card.restoring": "Restoring…",
4447
4655
  "card.restore": "Restore",
4448
4656
  "card.purgeTitle": "Remove permanently from archive/, irreversible (restore first to keep it)",
4449
4657
  "card.purge": "Delete permanently",
4450
4658
 
4451
- "star.ask": "⭐ Leave a star — it makes the author's day",
4452
- "star.cta": "Sure, here's a star",
4659
+ "star.cta": "Star",
4660
+ "star.hover": "· if you like this plugin",
4661
+ "star.dismiss": "Hide until next launch",
4662
+ "star.aria": "Star gezi-wen/sage-mem on GitHub",
4453
4663
 
4454
4664
  "pluginCard.saveFailed": "Save failed (unknown reason)",
4455
4665
  "pluginCard.noSetSettings": "The host has no setSettings API yet (older plugin version)",
@@ -4546,16 +4756,18 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4546
4756
  "conflict.actionMissing": "The host has no conflict.{method} API yet (older plugin version), so this action is disabled",
4547
4757
  "conflict.actionMissingHint": "Older version: missing {methods} — the matching buttons are disabled. Upgrade the plugin, or edit the memory frontmatter by hand (add invalid_at).",
4548
4758
  "conflict.readMissingHint": "Older version: no conflict.read API — the body comparison is disabled, but decisions still work.",
4549
- "conflict.confirmInvalidate": "Mark “{file}” as invalid? This writes invalid_at into its frontmatter (reversible); the file stays in place and the body is unchanged.",
4759
+ "conflict.invalidateAsk": "Mark “{file}” as invalid?",
4760
+ "conflict.confirmInvalidate": "This writes invalid_at into its frontmatter (reversible); the file stays in place and the body is unchanged.",
4550
4761
  "conflict.confirmInvalidateHint": "Only one frontmatter line changes; nothing is deleted or moved, and updated is left alone.",
4551
- "conflict.confirmUndo": "Undo the invalidation of “{file}”? The invalid_at line (and superseded_by) is removed and it takes part in injection again.",
4762
+ "conflict.undoAsk": "Undo the invalidation of “{file}”?",
4763
+ "conflict.confirmUndo": "The invalid_at line (and superseded_by) is removed and it takes part in injection again.",
4552
4764
  "conflict.confirmUndoHint": "Only the line we wrote is removed; every other byte stays the same.",
4553
- "conflict.confirmRemove": "Delete this conflict record? It disappears from the queue and cannot be restored (the memory file is not affected).",
4765
+ "conflict.removeAsk": "Delete this conflict record?",
4766
+ "conflict.confirmRemove": "It disappears from the queue and cannot be restored (the memory file is not affected).",
4554
4767
  "conflict.confirmRemoveHint": "Only the record file in the state directory is deleted; the memory itself is untouched.",
4555
4768
  "conflict.confirmYesInvalidate": "Confirm invalidation",
4556
4769
  "conflict.confirmYesUndo": "Confirm undo",
4557
4770
  "conflict.confirmYesRemove": "Confirm delete record",
4558
- "conflict.cancel": "Cancel",
4559
4771
  "conflict.busy": "Working…",
4560
4772
  "conflict.decision.invalidate": "invalidated",
4561
4773
  "conflict.decision.dismiss": "decided",
@@ -4596,6 +4808,8 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4596
4808
  "ad.status.idle": "Enabled · idle",
4597
4809
  "ad.status.off": "Disabled",
4598
4810
  "ad.note.what": "Read the whole memory directory: merge duplicates, drop what has been overruled, fix the index and broken links, and turn relative dates into absolute ones.",
4811
+ "ad.note.offStop": "Module is off - \"Run now\" is disabled. Turn the switch above back on to run it. Settings and records stay editable.",
4812
+ "ad.note.offStopTitle": "Module is off - turn the switch above on first",
4599
4813
  "ad.row.trigger": "Trigger",
4600
4814
  "ad.trigger.manual": "Manual only",
4601
4815
  "ad.trigger.auto": "Run when conditions are met",
@@ -4714,7 +4928,6 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4714
4928
  "ad.snap.confirmSuffix": ". A protection snapshot is taken first, so this step can itself be undone.",
4715
4929
  "ad.action.rollingBack": "Rolling back…",
4716
4930
  "ad.action.confirmRollback": "Confirm rollback",
4717
- "ad.action.cancel": "Cancel",
4718
4931
  "ad.rb.rolledBack": "Rolled back to {at}: restored {restored} files, moved away {parked}",
4719
4932
  "ad.rb.skipped": ", skipped {count} (see the rollback report)",
4720
4933
  "ad.rb.protection": " · protection snapshot {id}",
@@ -4766,6 +4979,11 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4766
4979
  "archive.intro2": "It does not call a model: candidates are computed purely from the access ledger's day counts, so the same input always yields the same result.",
4767
4980
  "archive.intro3": "To see what would be collected right now: Files → “Tools ▾” → Archive candidates.",
4768
4981
  "archive.modeTitle": "Auto-archive",
4982
+ "archive.switch.onTitle": "Turn auto-archive off (no file is touched while it is off)",
4983
+ "archive.switch.offTitle": "Turn auto-archive on",
4984
+ "archive.status.on": "On · {mode}",
4985
+ "archive.status.off": "Off (no file is touched)",
4986
+ "archive.note.offStop": "Off: not a single file is touched - candidates are only listed for you to act on. The switch is the only way to turn it off; settings and records stay editable.",
4769
4987
  "archive.mode.off": "Off",
4770
4988
  "archive.mode.report": "Report only",
4771
4989
  "archive.mode.auto": "Run when conditions are met",
@@ -4955,6 +5173,24 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
4955
5173
  setModel(c.model || "");
4956
5174
  }, [c.provider, c.model]);
4957
5175
 
5176
+ /**
5177
+ * 回滚确认框的 Esc(2.5.0,D39)。
5178
+ *
5179
+ * 确认框搬进模态之后,键盘出口由调用方挂:这里只认 Escape(不挂「点外部关闭」——
5180
+ * 遮罩自己就是 onCancel,再挂一个 document click 会在同一次点击里关两遍)。
5181
+ */
5182
+ react.useEffect(function () {
5183
+ if (confirmSnap === null) return undefined;
5184
+ if (typeof document === "undefined" || !document.addEventListener) return undefined;
5185
+ function onKey(e) {
5186
+ if (!e || (e.key !== "Escape" && e.keyCode !== 27)) return;
5187
+ if (rbBusy !== null) return; // 回滚进行中不给半路的出口,等它回话
5188
+ setConfirmSnap(null);
5189
+ }
5190
+ document.addEventListener("keydown", onKey);
5191
+ return function () { document.removeEventListener("keydown", onKey); };
5192
+ }, [confirmSnap, rbBusy]);
5193
+
4958
5194
  /**
4959
5195
  * 所有 remote 调用的统一出口。
4960
5196
  *
@@ -5205,20 +5441,18 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
5205
5441
  onClick: function () { if (props.onSeg) props.onSeg(name); },
5206
5442
  }, [label, note ? h("span", { className: "smem-subtab-n", key: "n" }, note) : null]);
5207
5443
 
5208
- return h("div", { className: "smem-autodream" }, [
5209
- h("div", { className: "smem-subtabs", key: "segs" }, [
5210
- subtab("run", t("ad.tab.run")),
5211
- subtab("settings", t("ad.tab.settings")),
5212
- subtab("records", t("ad.tab.records"), String(snaps.length + runs.length + reports.length)),
5213
- ]),
5214
- segOf === "run" ? h("div", { key: "seg-run" }, [
5215
- h("div", { className: "smem-autodream-sec", key: "top" }, [
5444
+ return h("div", { className: "smem-autodream smem-module-card" }, [
5445
+ // 模块卡头(2.5.0 修):开关 + 标题 + 状态**必须活在分段之外**。
5446
+ // 它回答的是「这是哪个模块、现在是开是关」,和「这次看哪一段」是两个维度;
5447
+ // 原先它长在「运行」段里,切到设置 / 记录就一起消失 —— 于是下面那张
5448
+ // 自动归档的卡看起来像是当前这一段的内容(文歌子 2026-11-08 一眼看到的就是这个)。
5449
+ h("div", { className: "smem-autodream-sec smem-module-head", key: "top" }, [
5216
5450
  h("div", { className: "smem-autodream-row", key: "r" }, [
5217
- h("button", {
5218
- type: "button", key: "sw", className: "smem-switch",
5219
- "data-on": c.enabled ? "1" : "0",
5451
+ h(Switch, {
5452
+ key: "sw",
5453
+ on: !!c.enabled,
5454
+ label: t("ad.switch.aria"),
5220
5455
  title: c.enabled ? t("ad.switch.onTitle") : t("ad.switch.offTitle"),
5221
- "aria-label": t("ad.switch.aria"),
5222
5456
  onClick: function () { patch({ enabled: !c.enabled }); },
5223
5457
  }),
5224
5458
  h("span", { key: "t", style: { fontWeight: "600" } }, t("ad.title")),
@@ -5227,6 +5461,12 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
5227
5461
  ]),
5228
5462
  h("div", { className: "smem-autodream-note", key: "n" }, t("ad.note.what")),
5229
5463
  ]),
5464
+ h("div", { className: "smem-subtabs", key: "segs" }, [
5465
+ subtab("run", t("ad.tab.run")),
5466
+ subtab("settings", t("ad.tab.settings")),
5467
+ subtab("records", t("ad.tab.records"), String(snaps.length + runs.length + reports.length)),
5468
+ ]),
5469
+ segOf === "run" ? h("div", { key: "seg-run" }, [
5230
5470
  h("div", { className: "smem-autodream-sec", key: "mode" }, [
5231
5471
  h("div", { className: "smem-autodream-row", key: "a" }, [
5232
5472
  h("span", { className: "smem-autodream-key", key: "k" }, t("ad.row.trigger")),
@@ -5307,10 +5547,14 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
5307
5547
  : null,
5308
5548
  ]),
5309
5549
  h("div", { className: "smem-autodream-sec", key: "run" }, [
5550
+ // 2.5.0(D39):开关是唯一的关闭入口,所以关掉时手动跑也一起置灰 ——
5551
+ // 「关了还能点」等于开关只挡住了定时器,说的和做的不是一件事。
5552
+ c.enabled ? null : h("div", { className: "smem-modcard-off", key: "off" }, t("ad.note.offStop")),
5310
5553
  h("div", { className: "smem-autodream-row", key: "r" }, [
5311
5554
  h("button", {
5312
5555
  type: "button", className: "smem-btn smem-btn-primary", key: "go",
5313
- disabled: busy || running || !remote,
5556
+ disabled: busy || running || !remote || !c.enabled,
5557
+ title: c.enabled ? "" : t("ad.note.offStopTitle"),
5314
5558
  onClick: run,
5315
5559
  }, running ? t("ad.action.runningNow") : t("ad.action.runNow")),
5316
5560
  h("span", { className: "smem-status", key: "s" },
@@ -5500,20 +5744,18 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
5500
5744
  }, t("ad.action.rollback")),
5501
5745
  ]),
5502
5746
  confirmSnap === sp.name
5503
- ? h("div", { className: "smem-ad-confirm", key: "cf" }, [
5504
- h("div", { key: "t", style: { fontWeight: "600" } }, t("ad.snap.confirmTitle", { at: String(sp.atHuman || "") })),
5505
- h("div", { key: "d" }, t("ad.snap.confirmScope", {
5506
- scope: rollbackScope === "all" ? t("ad.snap.scopeAll") : t("ad.snap.scopeFiles"),
5507
- }) + t("ad.snap.confirmSuffix")),
5508
- h("div", { className: "smem-autodream-row", key: "acts" }, [
5509
- h("button", {
5510
- type: "button", className: "smem-btn smem-btn-danger", key: "yes",
5511
- disabled: rbBusy === sp.name,
5512
- onClick: function () { doRollback(sp); },
5513
- }, rbBusy === sp.name ? t("ad.action.rollingBack") : t("ad.action.confirmRollback")),
5514
- h("button", { type: "button", className: "smem-btn", key: "no", onClick: function () { setConfirmSnap(null); } }, t("ad.action.cancel")),
5515
- ]),
5516
- ])
5747
+ ? h(Confirm, {
5748
+ key: "cf",
5749
+ title: t("ad.snap.confirmTitle", { at: String(sp.atHuman || "") }),
5750
+ confirmLabel: t("ad.action.confirmRollback"),
5751
+ busyLabel: t("ad.action.rollingBack"),
5752
+ busy: rbBusy === sp.name,
5753
+ danger: true,
5754
+ onConfirm: function () { doRollback(sp); },
5755
+ onCancel: function () { setConfirmSnap(null); },
5756
+ }, h("div", { key: "d" }, t("ad.snap.confirmScope", {
5757
+ scope: rollbackScope === "all" ? t("ad.snap.scopeAll") : t("ad.snap.scopeFiles"),
5758
+ }) + t("ad.snap.confirmSuffix")))
5517
5759
  : null,
5518
5760
  ]);
5519
5761
  })),
@@ -5606,6 +5848,10 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
5606
5848
  const sErr = react.useState(null); const err = sErr[0], setErr = sErr[1];
5607
5849
  const sNotice = react.useState(""); const notice = sNotice[0], setNotice = sNotice[1];
5608
5850
  const sBusy = react.useState(false); const busy = sBusy[0], setBusy = sBusy[1];
5851
+ // 2.5.0(D39):「关闭」这一档从三档里拿掉了,开关是唯一的关闭入口。
5852
+ // 开关要能「关掉再打开回到原档位」,所以记住关之前是哪一档(不落配置,
5853
+ // 配置里只有 autoArchive 一个真身;重开时先回 report 也是安全的默认)。
5854
+ const sLast = react.useState("report"); const lastMode = sLast[0], setLastMode = sLast[1];
5609
5855
 
5610
5856
  const TH_KEYS = ["archiveAfterDaysProject", "archiveAfterDaysReference", "archiveAfterDaysUser"];
5611
5857
  // 2.1.0:标签与提示走字典(键在这里,文案在 55-i18n-memory.js)。
@@ -5678,6 +5924,7 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
5678
5924
  }
5679
5925
 
5680
5926
  function saveMode(v) {
5927
+ if (v !== "off") setLastMode(v);
5681
5928
  save({ autoArchive: v }, t("archive.notice.modeSet", { mode: modeLabel(v) }), function (got) {
5682
5929
  const g = got ? autoArchiveOf(got.autoArchive) : null;
5683
5930
  return g === v ? null : t("archive.err.modeStill", { mode: g ? modeLabel(g) : t("archive.unknown") });
@@ -5703,67 +5950,114 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
5703
5950
  });
5704
5951
  }
5705
5952
 
5706
- return h("div", { }, [
5707
- // ── ① 它做什么(这一屏存在的理由写在最前面)──
5708
- h("div", { className: "smem-autodream-sec", key: "what" }, [
5709
- h("div", { className: "smem-autodream-row", key: "h" }, [
5710
- h("span", { key: "t", style: { fontWeight: "600", fontSize: "12.5px" } }, t("archive.title")),
5711
- ]),
5712
- h("div", { className: "smem-autodream-note", key: "p1" }, t("archive.intro1")),
5713
- h("div", { className: "smem-autodream-note", key: "p2" }, t("archive.intro2")),
5714
- h("div", { className: "smem-autodream-note", key: "p3" }, t("archive.intro3")),
5953
+ // 根节点带一个稳定的类名(2.5.0):它现在与「自动做梦」同屏堆叠,
5954
+ // 断言与样式都得能把这一张卡与旁边那张分开 —— 没有类名就只能按整棵树找文本。
5955
+ //
5956
+ // 2.5.0 补(文歌子 2026-11-08 提的结构):这张卡也要有自己的「运行 / 设置」分段。
5957
+ // 并屏之后两张卡形状必须一样 —— 否则「设置」那一段里出现阈值、档位、说明混在一起,
5958
+ // 读起来像没分过类。分法照自动做梦那张卡:
5959
+ // 运行 = 它做什么 + 现在什么档(要看状态、要改档位的人在这一段)
5960
+ // 设置 = 三个阈值 + 保存(要调「多久算闲置」的人在这一段)
5961
+ // 没有「记录」段:归档产物写进整理记录,那一份列表在自动做梦 → 记录里,
5962
+ // 这里再开一段只是把它复制一遍(旧版那段本来就是一句「与记录共用」的空态)。
5963
+ const sSeg = react.useState("run");
5964
+ const seg = sSeg[0], setSeg = sSeg[1];
5965
+ const subtab = (name, label) => h("button", {
5966
+ type: "button", key: name,
5967
+ className: "smem-subtab" + (seg === name ? " smem-subtab--on" : ""),
5968
+ "aria-current": seg === name ? "true" : "false",
5969
+ onClick: function () { setSeg(name); },
5970
+ }, label);
5971
+
5972
+ return h("div", { className: "smem-archive-settings smem-module-card" }, [
5973
+ // ── ① 模块卡头(2.5.0,D39):开关 + 标题 + 状态 ──
5974
+ // 开关的真身就是 `autoArchive === 'off'`,没有新增配置键。
5975
+ h("div", { className: "smem-modcard-head", key: "head" }, [
5976
+ h(Switch, {
5977
+ key: "sw",
5978
+ on: mode !== "off",
5979
+ label: t("archive.modeTitle"),
5980
+ title: mode !== "off" ? t("archive.switch.onTitle") : t("archive.switch.offTitle"),
5981
+ disabled: busy || !cfg,
5982
+ onClick: function () { saveMode(mode === "off" ? lastMode : "off"); },
5983
+ }),
5984
+ h("span", { className: "smem-modcard-title", key: "t" }, t("archive.modeTitle")),
5985
+ h("span", { className: "smem-modcard-status", key: "s" },
5986
+ busy ? t("archive.action.saving")
5987
+ : (mode === "off" ? t("archive.status.off") : t("archive.status.on", { mode: modeLabel(mode) }))),
5715
5988
  ]),
5716
- // ── ② 三档策略 ──
5717
- h("div", { className: "smem-autodream-sec", key: "mode" }, [
5718
- h("div", { className: "smem-autodream-row", key: "r" }, [
5719
- h("span", { className: "smem-autodream-key", key: "k" }, t("archive.modeTitle")),
5720
- h(Seg, {
5721
- key: "v", value: mode,
5722
- options: [
5723
- { value: "off", label: modeLabel("off") },
5724
- { value: "report", label: modeLabel("report") },
5725
- { value: "auto", label: modeLabel("auto") },
5726
- ],
5727
- onChange: function (v) { if (v !== mode) saveMode(v); },
5728
- }),
5729
- busy ? h("span", { className: "smem-status", key: "s" }, t("archive.action.saving")) : null,
5730
- ]),
5731
- h("div", { className: "smem-autodream-note", key: "cost" }, costText(mode)),
5732
- mode === "auto" && !c.apply
5733
- ? h("div", { className: "smem-autodream-note smem-ad-warn", key: "deg" }, t("archive.degrade"))
5734
- : null,
5989
+ // ── ② 分段(与自动做梦那张卡同一个词:ad.tab.run / ad.tab.settings)──
5990
+ h("div", { className: "smem-subtabs", key: "segs" }, [
5991
+ subtab("run", t("ad.tab.run")),
5992
+ subtab("settings", t("ad.tab.settings")),
5735
5993
  ]),
5736
- // ── ③ 三个阈值 ──
5737
- h("div", { className: "smem-autodream-sec", key: "th" }, [
5738
- h("div", { className: "smem-autodream-row", key: "h" }, [
5739
- h("span", { key: "t", style: { fontWeight: "600", fontSize: "12.5px" } }, t("archive.th.title")),
5994
+ seg === "run" ? h("div", { key: "seg-run" }, [
5995
+ // ── 它做什么(这一屏存在的理由写在最前面)──
5996
+ h("div", { className: "smem-autodream-sec", key: "what" }, [
5997
+ h("div", { className: "smem-autodream-row", key: "h" }, [
5998
+ h("span", { key: "t", style: { fontWeight: "600", fontSize: "12.5px" } }, t("archive.title")),
5999
+ ]),
6000
+ h("div", { className: "smem-autodream-note", key: "p1" }, t("archive.intro1")),
6001
+ h("div", { className: "smem-autodream-note", key: "p2" }, t("archive.intro2")),
6002
+ h("div", { className: "smem-autodream-note", key: "p3" }, t("archive.intro3")),
5740
6003
  ]),
5741
- draft
5742
- ? h("div", { key: "rows" }, TH_KEYS.map(function (k) {
5743
- return h("div", { className: "smem-autodream-row", key: k }, [
5744
- h("span", { className: "smem-autodream-key", key: "k" }, thLabel(k)),
5745
- h("input", {
5746
- className: "smem-input smem-num", key: "i", type: "number", min: 1, max: 3650,
5747
- value: draft[k],
5748
- onChange: function (e) {
5749
- const nx = Object.assign({}, draft);
5750
- nx[k] = e.target.value;
5751
- setDraft(nx);
5752
- },
5753
- }),
5754
- h("span", { className: "smem-muted", key: "u" }, thHint(k)),
5755
- ]);
5756
- }))
5757
- : h("div", { className: "smem-muted", key: "loading" }, t("archive.loadingConfig")),
5758
- h("div", { className: "smem-autodream-row", key: "btns" }, [
5759
- h("button", {
5760
- type: "button", className: "smem-btn smem-btn-primary", key: "save",
5761
- disabled: busy || !draft, onClick: saveThresholds,
5762
- }, busy ? t("archive.action.saving") : t("archive.action.save")),
5763
- h("span", { className: "smem-muted", key: "n" }, t("archive.note.immediate")),
6004
+ // ── 两档策略(「关闭」已删,见 D39)──
6005
+ h("div", { className: "smem-autodream-sec", key: "mode" }, [
6006
+ mode === "off"
6007
+ ? h("div", { className: "smem-modcard-off", key: "offNote" }, t("archive.note.offStop"))
6008
+ : null,
6009
+ h("div", { className: "smem-autodream-row", key: "r" }, [
6010
+ h("span", { className: "smem-autodream-key", key: "k" }, t("archive.modeTitle")),
6011
+ h(Seg, {
6012
+ key: "v", value: mode === "off" ? lastMode : mode,
6013
+ options: [
6014
+ { value: "report", label: modeLabel("report") },
6015
+ { value: "auto", label: modeLabel("auto") },
6016
+ ],
6017
+ onChange: function (v) { if (v !== mode) saveMode(v); },
6018
+ }),
6019
+ busy ? h("span", { className: "smem-status", key: "s" }, t("archive.action.saving")) : null,
6020
+ ]),
6021
+ h("div", { className: "smem-autodream-note", key: "cost" }, costText(mode)),
6022
+ mode === "auto" && !c.apply
6023
+ ? h("div", { className: "smem-autodream-note smem-ad-warn", key: "deg" }, t("archive.degrade"))
6024
+ : null,
5764
6025
  ]),
5765
- h("div", { className: "smem-autodream-note", key: "note" }, t("archive.note.thresholds")),
5766
- ]),
6026
+ ]) : null,
6027
+ seg === "settings" ? h("div", { key: "seg-settings" }, [
6028
+ // ── 三个阈值 ──
6029
+ h("div", { className: "smem-autodream-sec", key: "th" }, [
6030
+ h("div", { className: "smem-autodream-row", key: "h" }, [
6031
+ h("span", { key: "t", style: { fontWeight: "600", fontSize: "12.5px" } }, t("archive.th.title")),
6032
+ ]),
6033
+ draft
6034
+ ? h("div", { key: "rows" }, TH_KEYS.map(function (k) {
6035
+ return h("div", { className: "smem-autodream-row", key: k }, [
6036
+ h("span", { className: "smem-autodream-key", key: "k" }, thLabel(k)),
6037
+ h("input", {
6038
+ className: "smem-input smem-num", key: "i", type: "number", min: 1, max: 3650,
6039
+ value: draft[k],
6040
+ onChange: function (e) {
6041
+ const nx = Object.assign({}, draft);
6042
+ nx[k] = e.target.value;
6043
+ setDraft(nx);
6044
+ },
6045
+ }),
6046
+ h("span", { className: "smem-muted", key: "u" }, thHint(k)),
6047
+ ]);
6048
+ }))
6049
+ : h("div", { className: "smem-muted", key: "loading" }, t("archive.loadingConfig")),
6050
+ h("div", { className: "smem-autodream-row", key: "btns" }, [
6051
+ h("button", {
6052
+ type: "button", className: "smem-btn smem-btn-primary", key: "save",
6053
+ disabled: busy || !draft, onClick: saveThresholds,
6054
+ }, busy ? t("archive.action.saving") : t("archive.action.save")),
6055
+ h("span", { className: "smem-muted", key: "n" }, t("archive.note.immediate")),
6056
+ ]),
6057
+ h("div", { className: "smem-autodream-note", key: "note" }, t("archive.note.thresholds")),
6058
+ ]),
6059
+ ]) : null,
6060
+ // 错误与回执留在分段之外:不管你在哪一段按的,结果都得看得见。
5767
6061
  err ? h("div", { className: "smem-err", key: "err" }, "⚠ " + err) : null,
5768
6062
  notice ? h("div", { className: "smem-ok", key: "ok" }, "✓ " + notice) : null,
5769
6063
  ]);
@@ -6214,6 +6508,48 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
6214
6508
  setFormRevision(null);
6215
6509
  }
6216
6510
 
6511
+ /**
6512
+ * 抽屉的 Esc 关闭(2.5.0,D39)。
6513
+ *
6514
+ * 只挂 Escape,不挂「点外部关闭」:抽屉带遮罩,遮罩自己就是 onClick={closeForm},
6515
+ * 再叠一个 document 级监听会在同一次点击里关两遍(tools 抽屉那次
6516
+ * 「弹层活 2 毫秒」的 bug 就是这么来的)。Esc 是这个交互唯一的键盘出口。
6517
+ */
6518
+ react.useEffect(function () {
6519
+ if (!formOpen) return undefined;
6520
+ function onKey(e) { if (e && (e.key === "Escape" || e.keyCode === 27)) closeForm(); }
6521
+ document.addEventListener("keydown", onKey);
6522
+ return function () { document.removeEventListener("keydown", onKey); };
6523
+ }, [formOpen]);
6524
+
6525
+ /**
6526
+ * 确认框的 Esc(2.5.0,D39)。
6527
+ *
6528
+ * `Confirm` 只提供壳子,键盘出口归调用方 —— 这里一处盖住本屏全部五个确认态:
6529
+ * 归档 / 删除 / 彻底删除 / 归档候选(逐条与批量)/ 冲突裁决。五个状态互斥
6530
+ * (同一时刻最多一个为真),所以一次 Escape 就是「全清」:清一个本来就没开的状态无害。
6531
+ *
6532
+ * ⚠️ 只挂 Escape,**不挂「点外部关闭」**:遮罩自己就是 `onClick={onCancel}`,
6533
+ * 再叠一个 document 级 click 监听会在同一次点击里关两遍 —— tools 抽屉那个
6534
+ * 「弹层活 2 毫秒」的 bug 就是这么来的。
6535
+ */
6536
+ const confirmOpen = archiving !== null || deleting !== null || archDelete !== null ||
6537
+ candConfirm !== null || confConfirm !== null;
6538
+ react.useEffect(function () {
6539
+ if (!confirmOpen) return undefined;
6540
+ if (typeof document === "undefined" || !document.addEventListener) return undefined;
6541
+ function onKey(e) {
6542
+ if (!e || (e.key !== "Escape" && e.keyCode !== 27)) return;
6543
+ setArchiving(null); setArchiveReason("");
6544
+ setDeleting(null);
6545
+ setArchDelete(null);
6546
+ setCandConfirm(null);
6547
+ setConfConfirm(null);
6548
+ }
6549
+ document.addEventListener("keydown", onKey);
6550
+ return function () { document.removeEventListener("keydown", onKey); };
6551
+ }, [confirmOpen]);
6552
+
6217
6553
  // 空记忆库时「建立索引骨架」写进去的引导文件。它只是给人看的起点:
6218
6554
  // 索引本来就是手写维护的,这里不猜用户有什么记忆、也不生成假条目。
6219
6555
  const INDEX_SKELETON = [
@@ -6646,10 +6982,23 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
6646
6982
  }
6647
6983
 
6648
6984
  /** 打开「冲突」这一屏。数据没取过才取(挂载时已经取过一次,通常直接有内容)。 */
6649
- function openConflictTab() {
6650
- setTab("conflict");
6985
+ /**
6986
+ * 2.5.0:体检与冲突并进「库健康」一屏(D39)。
6987
+ *
6988
+ * 两样都是「按需取」的,所以进这一屏时把没取过的都取上 —— 用户点「库健康」
6989
+ * 想看的就是这一屏的整体状态,不该还得再点一次才发现另一半是空的。
6990
+ * 体检是只读扫描(不改盘),冲突列表是一次状态目录读,都不贵。
6991
+ */
6992
+ function openHealthTab() {
6993
+ setTab("health");
6651
6994
  setError(null); setNotice("");
6652
- if (!conf || confErr) loadConflicts();
6995
+ if (!audit) runAudit();
6996
+ if (conflictOk && (!conf || confErr)) loadConflicts();
6997
+ }
6998
+
6999
+ /** 体检徽标 / 冲突徽标都落到同一屏,只是落点不同(滚动交给浏览器)。 */
7000
+ function openConflictTab() {
7001
+ openHealthTab();
6653
7002
  }
6654
7003
 
6655
7004
  /**
@@ -6804,9 +7153,13 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
6804
7153
  * 切过来通常直接就看得见内容。
6805
7154
  */
6806
7155
  function openAuditTab() {
6807
- setTab("audit");
6808
- setError(null);
6809
- if (!audit) runAudit();
7156
+ openHealthTab();
7157
+ }
7158
+
7159
+ /** 「自动整理」一屏 = 自动做梦 + 自动归档两张卡(D39)。 */
7160
+ function openAutoTab() {
7161
+ setTab("auto");
7162
+ setError(null); setNotice("");
6810
7163
  }
6811
7164
 
6812
7165
  /** 打开某个工具(抽屉里点进来时用)。 */
@@ -7013,25 +7366,18 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
7013
7366
 
7014
7367
 
7015
7368
  return h("div", { className: "smem-root" }, [
7369
+ // 2.5.0:六个页签收成四个,**按动作而不是按数据源分**(D39)。
7370
+ // 自动做梦 / 自动归档原本各占一屏、体检 / 冲突也各占一屏 —— 它们回答的是
7371
+ // 同一句话(「自动化现在什么状态」「我的库有没有事」),拆开就逼用户来回切。
7016
7372
  h("div", { className: "smem-tabs", key: "tabs", role: "tablist" }, [
7017
7373
  h("button", { type: "button", className: "smem-tab" + (tab === "files" ? " smem-tab--on" : ""), key: "f", role: "tab", "aria-selected": tab === "files" ? "true" : "false", onClick: function () { setTab("files"); } }, t("tab.files")),
7018
7374
  h("button", { type: "button", className: "smem-tab" + (tab === "star" ? " smem-tab--on" : ""), key: "s", role: "tab", "aria-selected": tab === "star" ? "true" : "false", onClick: function () { setTab("star"); } }, t("tab.star")),
7019
- h("button", { type: "button", className: "smem-tab" + (tab === "autodream" ? " smem-tab--on" : ""), key: "d", role: "tab", "aria-selected": tab === "autodream" ? "true" : "false", onClick: function () { setTab("autodream"); } }, t("tab.autodream")),
7020
- // 视觉分组:左边「看」(文件列表 / 星图),右边「设置 · 自动化」。
7375
+ // 视觉分组:左边「看」(文件列表 / 时间线),右边「管」(自动整理 / 库健康)。
7021
7376
  h("span", { className: "smem-tabs-sep", key: "sep", "aria-hidden": "true" }),
7022
- // 「自动归档」排在自动做梦之后:归档是**确定性规则**(按闲置天数算,不调模型),
7023
- // 和「让模型整理记忆」是两件事。塞在自动做梦的设置里用户找不到
7024
- // (2026-10-11 用户原话:「扔这里连我都找不到」)。
7025
- h("button", { type: "button", className: "smem-tab" + (tab === "archive" ? " smem-tab--on" : ""), key: "ar", role: "tab", "aria-selected": tab === "archive" ? "true" : "false", onClick: function () { setTab("archive"); } }, t("tab.archive")),
7026
- // 体检再排在「自动归档」之后、独立成一屏:它只报告不修改,和「文件列表」里的
7027
- // 增删改是两件事,塞在工具抽屉里会让人以为它是个动作按钮。
7028
- h("button", { type: "button", className: "smem-tab" + (tab === "audit" ? " smem-tab--on" : ""), key: "a", role: "tab", "aria-selected": tab === "audit" ? "true" : "false", onClick: openAuditTab }, t("tab.audit")),
7029
- // 2.0.0「冲突」:裁决是**多动作、要逐条看两份正文**的流程,所以独立成第 6 个
7030
- // 页签,而不是塞进工具抽屉(抽屉方案的前提是「一年用不了几次」)。
7031
- // 没有 conflict 面的旧宿主:整颗不出现(能力检测,不是渲染错误)。
7032
- conflictOk
7033
- ? h("button", { type: "button", className: "smem-tab" + (tab === "conflict" ? " smem-tab--on" : ""), key: "c", role: "tab", "aria-selected": tab === "conflict" ? "true" : "false", onClick: openConflictTab }, t("tab.conflict"))
7034
- : null,
7377
+ h("button", { type: "button", className: "smem-tab" + (tab === "auto" ? " smem-tab--on" : ""), key: "d", role: "tab", "aria-selected": tab === "auto" ? "true" : "false", onClick: openAutoTab }, t("tab.auto")),
7378
+ // 库健康 = 体检(机器扫)+ 冲突(要人裁决)。两者回答的是同一句话,
7379
+ // 所以并成一屏;没有 conflict 面的旧宿主只少一张卡,页签照常在。
7380
+ h("button", { type: "button", className: "smem-tab" + (tab === "health" ? " smem-tab--on" : ""), key: "a", role: "tab", "aria-selected": tab === "health" ? "true" : "false", onClick: openHealthTab }, t("tab.health")),
7035
7381
  ]),
7036
7382
  // 四个 tab 共用的一条头部带:我在哪 · 目录状态 · 活动/归档计数 · 体检徽标 · 待重启徽标 · 主操作。
7037
7383
  // 层级形状固定:导航=下划线(.smem-tabs)、筛选=chip、动作=按钮,三种不混用。
@@ -7065,7 +7411,7 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
7065
7411
  ? h("button", {
7066
7412
  type: "button", key: "rs", className: "smem-tool-badge smem-ad-badge--warn",
7067
7413
  title: t("header.restartTitle"),
7068
- onClick: function () { setTab("autodream"); setAdSeg("settings"); },
7414
+ onClick: function () { setTab("auto"); setAdSeg("settings"); },
7069
7415
  }, t(pendingDiff ? "header.restartPendingCount" : "header.restartPending", { count: pendingDiff }))
7070
7416
  : null,
7071
7417
  // 主操作:只放当前 tab 的那一个(星图/自动做梦的主操作在各自面板里,不重复摆一份)。
@@ -7131,9 +7477,8 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
7131
7477
  ]) : null,
7132
7478
  ]),
7133
7479
  ]) : null,
7134
- tab === "audit"
7135
- ? h(AuditPanel, { report: audit, busy: auditBusy, onRun: runAudit, standalone: true })
7136
- : null,
7480
+ // 2.5.0:体检不再单独一屏 —— 它和冲突并进「库健康」,见下面的 tab === "health"。
7481
+ // 这里保留 null 会落进 filesView 兜底(历史上踩过),所以整块删掉。
7137
7482
  tab === "files" && tool === "candidates"
7138
7483
  ? h(CandidatePanel, {
7139
7484
  data: cand,
@@ -7204,69 +7549,97 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
7204
7549
  error ? h("div", { className: "smem-err", key: "err", role: "alert" }, "⚠ " + error) : null,
7205
7550
  notice && !error ? h("div", { className: "smem-ok", key: "ok", role: "status" }, "✓ " + notice) : null,
7206
7551
  formOpen
7207
- ? h("div", { className: "smem-form", key: "form" }, [
7208
- h("div", { className: "smem-form-title", key: "ft" }, editingFile ? t("form.editTitle", { file: editingFile }) : t("form.addTitle")),
7209
- h("div", { className: "smem-row", key: "cat" }, [
7210
- h("span", { className: "smem-label", key: "l" }, t("form.type")),
7211
- h("div", { className: "smem-chips", key: "chips" }, TYPE_ORDER.map(function (id) {
7212
- const meta = TYPE_META[id];
7213
- return h(Chip, { key: id, label: meta[0] + " " + typeLabel(id), on: formType === id, onClick: function () { setFormType(id); } });
7214
- })),
7215
- ]),
7216
- h("label", { className: "smem-label", key: "ln" }, t("form.fileName", { type: formType })),
7217
- h("input", { className: "smem-input", key: "tn", value: formName, onChange: function (e) { setFormName(e.target.value); }, placeholder: t("form.fileNamePlaceholder") }),
7218
- h("label", { className: "smem-label", key: "ld" }, t("form.desc")),
7219
- h("input", { className: "smem-input", key: "td", value: formDesc, onChange: function (e) { setFormDesc(e.target.value); }, placeholder: t("form.descPlaceholder") }),
7220
- h("label", { className: "smem-label", key: "lt" }, t("form.tags")),
7221
- h("input", { className: "smem-input", key: "tt", value: formTags, onChange: function (e) { setFormTags(e.target.value); }, placeholder: t("form.tagsPlaceholder") }),
7222
- h("label", { className: "smem-label", key: "lb" }, t("form.body")),
7223
- h("textarea", { className: "smem-textarea", key: "tb", rows: 6, value: formBody, onChange: function (e) { setFormBody(e.target.value); }, placeholder: t("form.bodyPlaceholder") }),
7224
- h("div", { className: "smem-row", key: "btns" }, [
7225
- h("button", { type: "button", className: "smem-btn smem-btn-primary", key: "save", disabled: busy, onClick: submit }, busy ? t("card.saving") : t("card.save")),
7226
- h("button", { type: "button", className: "smem-btn", key: "cancel", disabled: busy, onClick: closeForm }, t("card.cancel")),
7552
+ // 2.5.0(D39):编辑从「页面内联表单」搬进右侧抽屉。
7553
+ // 字段、校验、保存路径一个字没动 —— 换的只是它挂在哪。
7554
+ // 为什么要搬:内联表单会把列表顶下去,长正文时整页跟着滚,
7555
+ // 「改一条记忆」这件事在视觉上霸占了整屏。
7556
+ // 动画用 CSS `animation` 而不是 transition + 二次渲染:mount 即播,
7557
+ // 不依赖 requestAnimationFrame(页面不可见时 rAF 不触发,会把抽屉卡在屏幕外)。
7558
+ ? h("div", { className: "smem-drawer-host", key: "form" }, [
7559
+ // 遮罩点一下就关:与「取消」同义,且它挡着列表,不给第二种解释。
7560
+ h("div", { className: "smem-drawer-scrim", key: "scrim", onClick: closeForm }),
7561
+ h("aside", {
7562
+ className: "smem-drawer-panel", key: "panel",
7563
+ role: "dialog", "aria-modal": "true",
7564
+ "aria-label": editingFile ? t("form.editTitle", { file: editingFile }) : t("form.addTitle"),
7565
+ }, [
7566
+ h("div", { className: "smem-drawer-head", key: "h" }, [
7567
+ h("div", { className: "smem-drawer-title", key: "t" }, editingFile ? t("form.editTitle", { file: editingFile }) : t("form.addTitle")),
7568
+ h("button", {
7569
+ type: "button", className: "smem-drawer-x", key: "x",
7570
+ title: t("card.cancel"), "aria-label": t("card.cancel"),
7571
+ onClick: closeForm,
7572
+ }, "×"),
7573
+ ]),
7574
+ h("div", { className: "smem-drawer-body", key: "b" }, [
7575
+ h("div", { className: "smem-row", key: "cat" }, [
7576
+ h("span", { className: "smem-label", key: "l" }, t("form.type")),
7577
+ h("div", { className: "smem-chips", key: "chips" }, TYPE_ORDER.map(function (id) {
7578
+ const meta = TYPE_META[id];
7579
+ return h(Chip, { key: id, label: meta[0] + " " + typeLabel(id), on: formType === id, onClick: function () { setFormType(id); } });
7580
+ })),
7581
+ ]),
7582
+ h("label", { className: "smem-label", key: "ln" }, t("form.fileName", { type: formType })),
7583
+ h("input", { className: "smem-input", key: "tn", value: formName, onChange: function (e) { setFormName(e.target.value); }, placeholder: t("form.fileNamePlaceholder") }),
7584
+ h("label", { className: "smem-label", key: "ld" }, t("form.desc")),
7585
+ h("input", { className: "smem-input", key: "td", value: formDesc, onChange: function (e) { setFormDesc(e.target.value); }, placeholder: t("form.descPlaceholder") }),
7586
+ h("label", { className: "smem-label", key: "lt" }, t("form.tags")),
7587
+ h("input", { className: "smem-input", key: "tt", value: formTags, onChange: function (e) { setFormTags(e.target.value); }, placeholder: t("form.tagsPlaceholder") }),
7588
+ h("label", { className: "smem-label", key: "lb" }, t("form.body")),
7589
+ h("textarea", { className: "smem-textarea smem-drawer-ta", key: "tb", rows: 12, value: formBody, onChange: function (e) { setFormBody(e.target.value); }, placeholder: t("form.bodyPlaceholder") }),
7590
+ ]),
7591
+ h("div", { className: "smem-drawer-foot", key: "f" }, [
7592
+ h("button", { type: "button", className: "smem-btn", key: "cancel", disabled: busy, onClick: closeForm }, t("card.cancel")),
7593
+ h("button", { type: "button", className: "smem-btn smem-btn-primary", key: "save", disabled: busy, onClick: submit }, busy ? t("card.saving") : t("card.save")),
7594
+ ]),
7227
7595
  ]),
7228
7596
  ])
7229
7597
  : null,
7230
- tab === "autodream"
7231
- ? h(AutodreamPanel, { key: "autodream", ctx: props.ctx, seg: adSeg, onSeg: setAdSeg })
7598
+ tab === "auto"
7599
+ // 两张模块卡同屏(D39):各自保留运行 / 设置 / 记录三段,只是不再各占一屏。
7600
+ // 顺序固定「自动做梦 → 自动归档」:前者要调模型、有 token 成本,先看见它。
7601
+ ? h("div", { className: "smem-stack smem-view", key: "auto" }, [
7602
+ h(AutodreamPanel, { key: "ad", ctx: props.ctx, seg: adSeg, onSeg: setAdSeg }),
7603
+ h(ArchiveSettingsPanel, { key: "ar", ctx: props.ctx }),
7604
+ ])
7232
7605
  : tab === "star"
7233
- ? h("div", { className: "smem-starmap", key: "starmap" },
7606
+ ? h("div", { className: "smem-starmap smem-view", key: "starmap" },
7234
7607
  h(StarmapRuntime.StarMap, { ctx: props.ctx }))
7235
- : tab === "archive"
7236
- ? h(ArchiveSettingsPanel, { key: "archive", ctx: props.ctx })
7237
- : tab === "audit"
7238
- // 体检面板在上面已经渲染过了 —— 这里必须显式给 null,
7239
- // 否则会落进下面那个 `filesView` 兜底分支,变成「体检 + 一整套文件列表」同屏。
7240
- ? null
7241
- : tab === "conflict"
7242
- ? h(ConflictPanel, {
7243
- key: "conflict",
7244
- data: conf,
7245
- loading: confLoad,
7246
- error: confErr,
7247
- bodies: confBodies,
7248
- expanded: confExpanded,
7249
- confirm: confConfirm,
7250
- busyId: confBusy,
7251
- // 逐动作的能力检测:宿主有 list 没 invalidate(版本错配)时按钮要禁用,
7252
- // 不是点了之后才报错。
7253
- tools: {
7254
- list: conflictOk,
7255
- read: !!(conflictOk && typeof conflictRemote.read === "function"),
7256
- invalidate: !!(conflictOk && typeof conflictRemote.invalidate === "function"),
7257
- dismiss: !!(conflictOk && typeof conflictRemote.dismiss === "function"),
7258
- undo: !!(conflictOk && typeof conflictRemote.undo === "function"),
7259
- removeRecord: !!(conflictOk && typeof conflictRemote.removeRecord === "function"),
7260
- },
7261
- onReload: loadConflicts,
7262
- onToggleBodies: toggleConflictBodies,
7263
- onAsk: askConflict,
7264
- onPickFile: pickConflictFile,
7265
- onCancelConfirm: cancelConflictConfirm,
7266
- onRun: runConflictAction,
7267
- onOpenFile: openConflictFile,
7268
- })
7269
- : filesView,
7608
+ : tab === "health"
7609
+ // 冲突在前、体检在后:前者是**要你裁决的有限队列**,后者是一份长清单。
7610
+ ? h("div", { className: "smem-stack smem-view", key: "health" }, [
7611
+ conflictOk
7612
+ ? h(ConflictPanel, {
7613
+ key: "conflict",
7614
+ data: conf,
7615
+ loading: confLoad,
7616
+ error: confErr,
7617
+ bodies: confBodies,
7618
+ expanded: confExpanded,
7619
+ confirm: confConfirm,
7620
+ busyId: confBusy,
7621
+ // 逐动作的能力检测:宿主有 list 没 invalidate(版本错配)时按钮要禁用,
7622
+ // 不是点了之后才报错。
7623
+ tools: {
7624
+ list: conflictOk,
7625
+ read: !!(conflictOk && typeof conflictRemote.read === "function"),
7626
+ invalidate: !!(conflictOk && typeof conflictRemote.invalidate === "function"),
7627
+ dismiss: !!(conflictOk && typeof conflictRemote.dismiss === "function"),
7628
+ undo: !!(conflictOk && typeof conflictRemote.undo === "function"),
7629
+ removeRecord: !!(conflictOk && typeof conflictRemote.removeRecord === "function"),
7630
+ },
7631
+ onReload: loadConflicts,
7632
+ onToggleBodies: toggleConflictBodies,
7633
+ onAsk: askConflict,
7634
+ onPickFile: pickConflictFile,
7635
+ onCancelConfirm: cancelConflictConfirm,
7636
+ onRun: runConflictAction,
7637
+ onOpenFile: openConflictFile,
7638
+ })
7639
+ : null,
7640
+ h(AuditPanel, { key: "audit", report: audit, busy: auditBusy, onRun: runAudit, standalone: true }),
7641
+ ])
7642
+ : h("div", { className: "smem-view", key: "files" }, filesView),
7270
7643
  // 底部一行仓库链接(四个 tab 都在,不抢视线)
7271
7644
  h(StarLine, { key: "star-line" }),
7272
7645
  ]);